AWS CDKでLambdaのサンプルを作る
はじめに
個人的に必要だったツールを開発した際にCDKを使ったので、手順などを含めて忘れないようにサンプルと共にまとめておく。
サンプルはCDK Workshopと同じ内容で、API GatewayとLambdaを使ってHello, CDK!
という文字列を返すアプリ。
https://github.com/tkt182/cdk_lambda_sample
やったこと
ちなみに、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を通す。
- bashの場合
$ 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をどの単位で分割はいろいろと検討の余地があるが、サンプルなので特に気にせずやっている。
- 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を利用する.
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 }}
以上。
自分用のメモなので、間違っていることがあったらすいません。 気づいたら訂正します。