AWS CDK(Cloud Development Kit )を使用した各種のLambda設置

1 はじめに

CX事業本部の平内(SIN)です。

AWS Cloud Development Kit (以下、AWS CDK)では、TypeScriptを使用して、CFnのテンプレートが作成可能ですが、LambdaもTypeScriptで作成している場合、コードとリソースが一元管理できて、いい感じにまとまる気がして好きです。

今回は、Lambda関数を設置する場合の色々な場面について、AWS CDKの利用方法を確認してみました。

2 簡単なLambdaの設置

最も簡単なLambdaの設置例です。

lambda.Function()の第3パラメータであるFunctionPropsは、下記の3項目の設定が必須です。

  • runtime
  • code
  • handler

functionName(関数名)は、必須ではないですが、認識しやすいように設定しました。

import cdk = require('@aws-cdk/core');
import * as lambda from '@aws-cdk/aws-lambda';

export class CdkLamdaSampleStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const myFunction = new lambda.Function( this, 'my-function', {
      functionName: 'my-function',
      runtime: lambda.Runtime.NODEJS_10_X,
      handler: 'index.handler',
      code: lambda.Code.asset('lambda')
    })
  }
}

また、codeの指定は、lambda.Code.asset()を使用し、階層下にディレクトリ(lambda)を作成して本体を設置しています。

$ tree -L 1
.
├── README.md
├── bin
├── cdk.json
├── cdk.out
├── lambda //このディレクトリを作成
├── lib
├── node_modules
├── package-lock.json
├── package.json
├── tsconfig.json
└── yarn.lock

AWS CDKのプロジェクトの階層下でTypeScriptのコードを展開する場合、トップのtsconfig.jsonに従って、トランスパイルされるため、同じフォルダ内にjsファイルが生成されることになります。

lambda/index.ts(簡単なLambda関数の例)

export async function handler(event:any) {
    console.log(JSON.stringify(event))
    return {
        statusCode: 200,
        body: "hello"
    }
}

3 デフォルトのポリシー

先のコードでは、Lambdaのロールに関して特に記述がありませんが、この場合、デフォルトのロールが自動的に生成されます。

生成されたロールを確認すると、管理ポリシー(AWSLambdaBasicExecutionRole)のみが設定されていることを確認できます。

4 ポリシーの追加

Lambda関数の中で、DynamoDBにアクセスするような場合、パーミッションの追加が必要になります。

lambda/index.ts (DynamoDBへのアクセス例)

import * as AWS from 'aws-sdk';
const client = new AWS.DynamoDB.DocumentClient();

const tableName =  process.env.TABLE_NAME!;

export async function handler(event:any) {
    console.log(JSON.stringify(event));
    const id = '001';

    const data = await client.get({
        TableName: tableName,
        Key:{ 'id': id }
    }).promise();

    return {
        statusCode: 200,
        body: data.Item!.message
    }
}

AWS CDKでは、addToRolePolicyで簡単にパーミッションを追加できます。

import * as iam from '@aws-cdk/aws-iam';

//・・・略・・・

const tableArn = 'arn:aws:dynamodb:' + this.region + ':' + this.account + ':table/' + tableName;
myFunction.addToRolePolicy(new iam.PolicyStatement({
    resources: [tableArn],
    actions: ['dynamodb:GetItem'] }
));

追加後、Lambdaのロールは、以下のようになります。

上の例では、ポリシーのリソースを指定するために、TableのARNを文字列の連結で作成していますが、Table自体が、AWS CDKで作成されている場合、tableArnプロパティでARNが取得できます。

AWS CDKで生成しているリソースの場合、循環依存の回避ぐらいしか、ARN等を文字列操作で作成する必要は無いでしょう。

import * as dynamodb from '@aws-cdk/aws-dynamodb';

//・・・略・・・

// テーブルの作成
const table = new dynamodb.Table(this, 'sample-table', {
    tableName: tableName,
    partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }
});
myFunction.addToRolePolicy(new iam.PolicyStatement({
    resources: [table.tableArn], // リソースのARNは、プロパティで指定
    actions: ['dynamodb:GetItem'] }
));

ちなみに、DynamoDBテーブルには、各種のgrantメソッドが用意されており、こちらを使用してもLambdaにパーミッションを追加することができます。

下記では、テーブルにmyFunctionからのReadWriteを追加しています。

table.grantReadWriteData(myFunction);

このように、関連するリソースからLambdaを指定した場合も、必要なパーミッションが追加される仕組みが、AWS CDKにはあります。

5 環境変数

AWS CDK内で指定したリソースの名前などは、環境変数を使用してLambdaと同期させるのが適切でしょう。

下記では、テーブル名をAWS CDK側で定義し(const tableName = 'sample-table')、Lambdaへは、環境変数で渡しています。

const tableName = 'sample-table';
const myFunction = new lambda.Function( this, 'my-function', {
    functionName: 'my-function',
    runtime: lambda.Runtime.NODEJS_10_X,
    handler: 'index.handler',
    code: lambda.Code.asset('lambda'),
    environment:{
        "TABLE_NAME": tableName
    }
})

Lambda側では、環境変数から値を受け取って作業します。

const tableName =  process.env.TABLE_NAME!;

6 API Gateway

Lambda関数をRestAPIのバックエンドに使用するパターンは、よくある話だと思います。そして この場合、AWS Gatewayを設置して、そこからLambdaを呼び出す仕組みを作るのですが、AWS CDKでは、次のような簡単なコードで、これが作成できます。

最初に、apigateway.RestApi()で、RestAPIのオブジェクトを生成し、次に、apigateway.LambdaIntegration()でLambdaを使用した接続先を生成します。最後に、RestAPIにメソッドとしてこれを追加する流れです。

import * as apigateway from '@aws-cdk/aws-apigateway';

//・・・略・・・

const api = new apigateway.RestApi(this, "api", {
    restApiName: 'lambda-sample'
});
const integration = new apigateway.LambdaIntegration(myFunction);
api.root.addMethod('POST', integration);

上のコードをdeployすると最後に下記のようにEndpointが表示されます。

 ✅  CdkLamdaSampleStack

Outputs:
CdkLamdaSampleStack.apiEndpoint9349E63C = https://yc9d0apm30.execute-api.ap-northeast-1.amazonaws.com/prod/

作成されたリソースを確認すると、次のようになっています。

そして、prodステージでデプロイも完了しています。

Endpointに対してリクエストするとLambdaがレスポンスできている事を確認できます。

$ curl -X POST https://yc9d0apm30.execute-api.ap-northeast-1.amazonaws.com/prod/
good morning

7 トリガー

Lambdaを他のサービスから使用するためのトリガーは、利用側のリソースのとの関連を記述した時点で、自動的に付与されます。

先の例では、API Gatewayの接続先として指定した時点で、これが完了していることになります。

const integration = new apigateway.LambdaIntegration(myFunction);

もう一つの例として、S3バケットにファイルが追加された時にLambdaが発火する仕組みを作ってみます。

手順としては、addEventNotification()でイベントの通知先をLambdaに指定する感じです。

import * as s3n from '@aws-cdk/aws-s3-notifications';

//・・・略・・・

const myBucket = new s3.Bucket(this, "my-bucket", {
    bucketName: "lambda-sample-bucket" 
})
myBucket.addEventNotification (
    s3.EventType.OBJECT_CREATED_PUT,
    new s3n.LambdaDestination(myFunction));

8 ログの保持期間

Lambdaのログは、CloudWatchLogsに保存されますが、その 保存期間は、デフォルトで無期限となっています。

有効期限を設定したい場合は、ロググループ自体をAWS CDKで生成する必要があります。(注:Lambdaの実行などで、既にロググループが作成されている場合、deployに失敗します。ロググループが無い状態でスタックを作成して下さい)

import * as logs from '@aws-cdk/aws-logs';

//・・・略・・・

const loggroup = new logs.LogGroup(this, 'log-group', {
    logGroupName: '/aws/lambda/' + myFunction.functionName,
    retention: logs.RetentionDays.ONE_DAY
})

9 CloudWatchのログ監視

エラーが発生した場合に、それを検出して通知などを行う処理は、よくあるパターンだと思います。 下記の例は、my-functionというLambdaのログに「Error」というパターンが見つかった際に、error-functionというLambdaを起動するようにしたものです。

my-functionの出力先であるロググループに、addSubscriptionFilter()でサブスクリプションを設定しています。そして、サブスクリプションの先は、error-functionに繋がっています。

import cdk = require('@aws-cdk/core');
import * as lambda from '@aws-cdk/aws-lambda';
import * as logs from '@aws-cdk/aws-logs';
import * as logsn from '@aws-cdk/aws-logs-destinations';

export class CdkLamdaSampleStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const myFunction = new lambda.Function( this, 'my-function', {
      functionName: 'my-function',
      runtime: lambda.Runtime.NODEJS_10_X,
      handler: 'index.handler',
      code: lambda.Code.asset('lambda'),
    })

    const errorFunction = new lambda.Function( this, 'error-function', {
      functionName: 'error-function',
      runtime: lambda.Runtime.NODEJS_10_X,
      handler: 'index.handler',
      code: lambda.Code.asset('lambda/error'),
    })

    const loggroup = new logs.LogGroup(this, 'log-group', {
      logGroupName: '/aws/lambda/' + myFunction.functionName,
      retention: logs.RetentionDays.ONE_DAY
    })
    loggroup.addSubscriptionFilter("subscription", {
      filterPattern: logs.FilterPattern.literal("Error"),
      destination: new logsn.LambdaDestination(errorFunction)
    })
  }
}

10 最後に

今回は、色々な場面でのLambdaの設置をAWS CDKで書いてみました。

全ては、全然網羅できていませんが、AWS CDKを使用する場合のパターンが、少し見えてきたように感じています。