マイ備忘録

あくまで個人の意見、メモです。

AWS CDKでLambdaのサンプルを作る

はじめに

個人的に必要だったツールを開発した際にCDKを使ったので、手順などを含めて忘れないようにサンプルと共にまとめておく。 サンプルはCDK Workshopと同じ内容で、API GatewayとLambdaを使ってHello, CDK!という文字列を返すアプリ。

https://github.com/tkt182/cdk_lambda_sample

やったこと

  • LambdaとAPI GatewayのリソースをCDKで構築
  • Lambdaの動作確認はsam-beta-cdkを使ってローカルで行う
  • Github ActionsでDeploy(テストはやってない)

ちなみに、LambdaはRubyで書いています(普段Rubyを使っているので)。

開発環境構築

1. Node.jsインストール

nodenv経由でinstallする。

https://github.com/nodenv/nodenv

$ brew install nodenv
$ eval "$(nodenv init -)"

このrepositoryで利用するversionをインストールする(作成時点でCDKでの動作確認済み最新バージョンが16.3.0)。

$ nodenv install 16.3.0

ローカルで利用するnodeのバージョンを指定する。

$ nodenv local 16.3.0

ターミナルを閉じるとnodeが見えなくなるため、PATHを通す。 cdkコマンドはlocalのnode_modulesのbinを参照させたいので、相対パスの形でPATHを通す。

$ echo 'export PATH="$HOME/.nodenv/bin:$PATH"' >> ~/.bash_profile`
$ echo 'eval "$(nodenv init -)"' >> ~/.bash_profile`
$ echo 'export PATH="$PATH:./node_modules/.bin"' >> ~/.bash_profile`

動作確認

$ node -v
v16.3.0
$ npm -v
7.15.1

参考

https://qiita.com/282Haniwa/items/a764cf7ef03939e4cbb1

2. CDKのインストール

cdk initでプロジェクトを初期化するので、一旦cdkはグローバルにインストールする。

$ nodenv global 16.3.0
$ npm install -g aws-cdk
$ nodenv rehash
$ cdk init app --language=typescript

cdk init app --language=typescriptで初期化が終わったら、globalにインストールしたcdkを削除。

$ npm uninstall -g aws-cdk
$ node rehash

その後改めてaws-cdkをローカルにインストール

$ npm install aws-cdk

3. sam-beta-cdkのインストール

以下のコマンドでインストール。

$ brew install aws-sam-cli-beta-cdk

4. ESLint, Prettierの設定

Typescriptで開発するので、ESLintとPrettierを入れる。 VSCodeを使っているので、VSCode側からESLintとPrettierを使えるようextentionも入れる。

※ Prettirのextensionは、ローカルにPrettierがなかった場合、extensionにbundleされているものが使われるとのこと

Should prettier not be installed locally with your project's dependencies or globally on the machine, the version of prettier that is bundled with the extension will be used.
$ npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
$ npm install -D prettier

https://zenn.dev/teppeis/articles/2021-02-eslint-prettier-vscode https://takeken1.hatenablog.com/entry/2020/12/25/005441

ソースの整形はPrettierに任せたほうがよいとのことなので、競合する部分ではESLintの設定をDisableにする。

これをするのに eslint-config-prettier が必要なため、npm でインストール。

$ npm install -D eslint-config-prettier

インストールが完了したらnpx eslint --initを実行して.eslintrc.js を作る。

.eslintrc.jsのextendsの最後にprettierを追加する。extendsへの追記はprettierだけでよいとのこと。

https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md#version-800-2021-02-21

Prettierの設定ファイルとして

  • .prettierrc.js
  • .prettierignore

を作る

一応CLIでも操作できるようにpackage.jsonにコマンドを追加。

コマンドは上記のteppeisさんの記事を参考に。

設定ファイルはcommitしているので、ここではESLintとPrettierをインストールするだけ。

5. rubocopの設定

lambdaをrubyで書くので、linterとしてrubocopをインストールする。

$ bundle init

作成されたGemfileにrubocopの設定を追加。

group :development do
  gem 'rubocop', require: false
end

gemのinstall。

bundle install --path vendor/bundle

vendorは.gitignoreに追加しておく。

VSCodeのplugin、ruby-rubocopをインストール。

設定をplouginの設定をsetting.jsonに追加し、rubocop自体の設定を.rubocop.ymlに追加。

CDK App

CDKのアプリケーションは、Construct, Stack, Appの3階層で構成される。 Constructが最も基本的な要素で、Constructを組み合わせてStackを作り、Stackを組み合わせてAppを作る要素となる。

https://d2908q01vomqb2.cloudfront.net/da4b9237bacccdf19c0760cab7aec4a8359010b0/2018/12/17/appstack.png https://aws.amazon.com/jp/blogs/aws/boost-your-infrastructure-with-cdk/

Constructを作るライブラリ(Construct Library)は以下の3つのレイヤーに分類される。

  • L1 Construct
    • 最も低レベルなConstruct
    • Cloudformationのテンプレートと同じ粒度で設定する(Cfnリソースと呼ばれる)
  • L2 Construct
    • L1よりも高レベルで抽象化されたConstruct
    • (推奨とされる)デフォルト値、ボイラプレートなど用いることで、単純に記述量を減らせる
  • L3 Construct
    • 最も抽象化されたConstruct
    • L3 Contruct単体で1つのアプリケーションのようなものを構築することができる(複数のAWSリソース作成される)

https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/home.html

基本的にはL2以上のContruct Libraryを使ってアプリケーションを記述していくのがよい。

Deploy Lifecycle(App Lifecycle)

CDKアプリは上記の図のように、Appを頂点とする木構造になっている。 ディレクトリ構造としては、Appがエントリポイントになるためbin配下に置き、そこから取り込まれる各Stackをlib配下に置く、という形になる。

CDKのdeployは以下の図が示す状態遷移をたどる。

https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/images/Lifecycle.png https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/apps.html

1. Construction (or Initialization)

  • CDKのコードを実行し、定義されているConstructのインスタンス生成及びリンクを行う

2. Preparation

  • 最終的な状態を設定する処理で、自動で行われる(開発者は基本的には何もしない)

3. Validation

  • 各Construct自身が、Deploy可能な状態であるかどうかを検証する

4. Synthesis

  • app.synth()を実行し、各Constructを合成されたCloud Assemblyに変換する
  • Cloud Assemblyのschema定義はこちら

5. Deployment

  • 生成されたCloud AssemblyをAWS環境にdeployする(=Cloudformationのdeploy)

1〜4のフェーズを経て、CDKのソースコードはdeploy可能なCloud Assemblyに変換される。 そして最後にDeployされる、というサイクルになる。

作成したサンプルアプリ

作成したサンプルではLambdaとApiGatewayを別々のstackにした。 stackをどの単位で分割はいろいろと検討の余地があるが、サンプルなので特に気にせずやっている。

https://speakerdeck.com/tomoki10/know-how-from-initial-development-to-operation-on-how-to-use-aws-cdk?slide=13

  • bin
    • cdk_lambda_sample.ts
  • lib
    • lambda-stack.ts
    • apigateway-stack.ts

bin/cdk_lambda_sample.ts

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';

import { LambdaStack } from '../lib/lambda-stack';
import { ApigatewayStack } from '../lib/apigateway-stack';

const app = new cdk.App();

const lambdaStack = new LambdaStack(app, 'sample-lambda', {});
const apigatewayStack = new ApigatewayStack(
  app,
  'sample-apigateway',
  lambdaStack.lambdaFunction,
  {}
);

apigatewayStack.addDependency(lambdaStack);

lib/lambda-stack.ts

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';

export class LambdaStack extends Stack {
  public readonly lambdaFunction: lambda.Function;

  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    this.lambdaFunction = new lambda.Function(this, 'SampleHelloHandler', {
      runtime: lambda.Runtime.RUBY_2_7,
      code: lambda.Code.fromAsset('lambda'),
      handler: 'hello.lambda_handler',
      functionName: 'hello',
    });
  }
}

lib/apigateway-stack.ts

import { Stack, StackProps, RemovalPolicy } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as apigw from 'aws-cdk-lib/aws-apigateway';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { LogGroup } from 'aws-cdk-lib/aws-logs';

export class ApigatewayStack extends Stack {
  constructor(
    scope: Construct,
    id: string,
    lambdaFunction: lambda.Function,
    props?: StackProps
  ) {
    super(scope, id, props);

    const restApiLogAccessLogGroup = new LogGroup(
      this,
      'SampleRestApiAccessLogGroup',
      {
        logGroupName: `/aws/apigateway/hello-cdk-rest-api-access-log`,
        retention: 1,
        removalPolicy: RemovalPolicy.DESTROY,
      }
    );

    new apigw.LambdaRestApi(this, 'SampleEndpoint', {
      handler: lambdaFunction,
      deployOptions: {
        //実行ログの設定
        dataTraceEnabled: true,
        loggingLevel: apigw.MethodLoggingLevel.INFO,
        //アクセスログの設定
        accessLogDestination: new apigw.LogGroupLogDestination(
          restApiLogAccessLogGroup
        ),
        accessLogFormat: apigw.AccessLogFormat.clf(),
      },
    });
  }
}

Lambaのローカル実行

sam-beta-cdk

ローカルで実行するために、sam-beta-cdkを利用する.

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-cdk-testing.html

AWS SAM(ServerlessApplicationModel)はLambdaをベースとしたServerlessアプリケーションを作成するフレームワーク. SAMを操作するためのツールsam-cliでLambdaのローカル実行などができる.

今回利用しているsam-beta-cdkはsamとcdkで作成したリソースを連携させるもの(まだパブリックプレビューの段階). cdkのリソースからsam localのコンテナを作ってローカル実行できるようにするようなものに見えた.

Lambda単体での実行

$ sam-beta-cdk local invoke --project-type CDK sample-lambda/SampleHelloHandler

$ sam-beta-cdk local invoke --project-type CDK sample-lambda/SampleHelloHandler
Synthesizing CDK App
Invoking hello.lambda_handler (ruby2.7)
Skip pulling image and use local one: public.ecr.aws/sam/emulation-ruby2.7:rapid-1.29.0.dev202108311500.

START RequestId: 657fdd46-ff49-4fa2-bd0d-14e9950b6396 Version: $LATEST
END RequestId: 657fdd46-ff49-4fa2-bd0d-14e9950b6396
REPORT RequestId: 657fdd46-ff49-4fa2-bd0d-14e9950b6396  Init Duration: 2.78 ms  Duration: 400.00 ms     Billed Duration: 500 ms Memory Size: 128 MB     Max Memory Used: 128 MB
{"statusCode":200,"headers":{"Content-Type":"text/plain"},"body":"Hello, CDK!"}

API Gateway経由

$ sam-beta-cdk local start-api --project-type CDK

このコマンドを実行することでhttp://127.0.0.1:3000/にサーバが起動するが、curlでリクエストを出したところ以下のようなエラーがでる.

Traceback (most recent call last):
  File "/usr/local/Cellar/aws-sam-cli-beta-cdk/202108311500/libexec/lib/python3.8/site-packages/flask/app.py", line 2447, in wsgi_app
    response = self.full_dispatch_request()
  File "/usr/local/Cellar/aws-sam-cli-beta-cdk/202108311500/libexec/lib/python3.8/site-packages/flask/app.py", line 1952, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/usr/local/Cellar/aws-sam-cli-beta-cdk/202108311500/libexec/lib/python3.8/site-packages/flask/app.py", line 1821, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/usr/local/Cellar/aws-sam-cli-beta-cdk/202108311500/libexec/lib/python3.8/site-packages/flask/_compat.py", line 39, in reraise
    raise value
  File "/usr/local/Cellar/aws-sam-cli-beta-cdk/202108311500/libexec/lib/python3.8/site-packages/flask/app.py", line 1950, in full_dispatch_request
    rv = self.dispatch_request()
  File "/usr/local/Cellar/aws-sam-cli-beta-cdk/202108311500/libexec/lib/python3.8/site-packages/flask/app.py", line 1936, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/usr/local/Cellar/aws-sam-cli-beta-cdk/202108311500/libexec/lib/python3.8/site-packages/samcli/local/apigw/local_apigw_service.py", line 317, in _request_handler
    self.lambda_runner.invoke(route.function_name, event, stdout=stdout_stream_writer, stderr=self.stderr)
  File "/usr/local/Cellar/aws-sam-cli-beta-cdk/202108311500/libexec/lib/python3.8/site-packages/samcli/commands/local/lib/local_lambda.py", line 107, in invoke
    function = self.provider.get(function_identifier)
  File "/usr/local/Cellar/aws-sam-cli-beta-cdk/202108311500/libexec/lib/python3.8/site-packages/samcli/lib/providers/sam_function_provider.py", line 74, in get
    raise ValueError("Function name is required")
ValueError: Function name is required

sam-beta-cdkはまだmultistack未対応とのこと. https://github.com/aws/aws-sam-cli/issues/3521#issuecomment-1007239656

また、sam-beta-cdk local start-apiでKeyErrorが発生する場合はcdk.jsonのcontextに"@aws-cdk/core:newStyleStackSynthesis": falseを追加する.

https://github.com/aws/aws-sam-cli/issues/2849#issuecomment-831887699

Deploy

  • CDKの初回デプロイ前にはbootstrap処理が必要になるため、以下を実行しておく

$ cdk bootstrap --profile xxxxx

github actionsでCDK deploy

以下のworkflowを設定して、mainへのmerge or pushがあればcdk deployが行われるようにしている.

name: cdk
on:
  push:
    branches:
      - main
  pull_request:
jobs:
  aws_cdk:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Setup Node
        uses: actions/setup-node@v2
        with:
          node-version: '16.3'

      - name: Setup dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: CDK Diff Check
        if: contains(github.event_name, 'pull_request')
        run: npm run cdk:diff
        env:
          AWS_DEFAULT_REGION: 'ap-northeast-1'
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

      - name: CDK Deploy
        if: contains(github.event_name, 'push')
        run: npm run cdk:deploy
        env:
          AWS_DEFAULT_REGION: 'ap-northeast-1'
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

以上。

自分用のメモなので、間違っていることがあったらすいません。 気づいたら訂正します。