はじめに
個人的に必要だったツールを開発した際に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
- lib
- lambda-stack.ts
- apigateway-stack.ts
bin/cdk_lambda_sample.ts
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!"}
$ 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 }}
以上。
自分用のメモなので、間違っていることがあったらすいません。
気づいたら訂正します。