はじめに
この記事はNEアドベントカレンダー2022(2)の2日目の記事です。
AWS CDKに入門してみたよと言う記事です。
日頃の業務でもAWSに関わる機会が増えてきたので、個人的にAWSには触れるようにしています。
ただ、LambdaとEventBridgeで定期的に何かをするくらいのことしかやったことがなかったので、
今回は最近気になっていたAWS CDKを触ってみました。
AWS CDK (AWS Cloud Development Kit)
AWSが提供している、 AWS CloudFormationというサービスを我々になじみがあるプログラミング言語で使えるようにしたものという認識でいいと思います。
もっと具体的に説明するとAWSの提供するリソースである、LambdaやVPC、S3などと言ったリソースを、Pythonなどで定義し、デプロイまでできるツールキットです。
AWS CDKのワークショップをやるとなんとなく雰囲気を掴めるのでおすすめです。
CDKワークショップ
最初の準備などはここも参考にしています。
AWS CDK の使用を開始する
入門ガイド
Pythonで AWS CDKを使ってみる
業務では主にPHPを使用していますが、学生時代からお世話になっているPythonで書いてみようと思います。
(PHPのCDKはないとPHP Conference Japan 2022の発表で聞きました)
AWS CDK に魅入られた PHPer がオススメする IaC から入るインフラの話
準備
ここにあるように npm で aws-cdkのCLIをインストールする。
https://cdkworkshop.com/ja/15-prerequisites/500-toolkit.html
CLIのインストールが済んだら、CDKを使うための準備としてbootstrapコマンドを実行する。
https://aws.amazon.com/jp/getting-started/guides/setup-cdk/module-two/
以下のコマンドで account IDを確認する
aws sts get-caller-identity
XXXXXXはaccount ID、東京リージョンで実行
cdk bootstrap aws://XXXXXXX/ap-northeast-1
これを実行することで、CloudFormationに CDKToolKitというスタックが追加され、AWSのリソースを取り扱えるようになる。
Pythonで開発するため、開発ディレクトリに移動し、以下のように入力すると雛形のファイルが生成される。
cdk init --language python
ここから先はPythonで作業するので、init中のREADMEにもある通りPythonの仮想環境に入って必要なライブラリをインストールする。私は個人的にpipenvが好きなのでpipenvで作業した。
pipenv shell
pipenv install -r aws-cdk/requirements.txt
AWS CDKでLambdaとAPIを定義してみる。
この内容をなぞってみる。
https://cdkworkshop.com/ja/30-python/30-hello-cdk.html
lambdaのコードを定義する。
今回の作業ディレクトリは以下のようになっている。
README.md cdk.json source.bat
app.py cdk.out requirements-dev.txt tests
aws_cdk_python cdk_api_test requirements.txt
ここに新しく lambdaなるディレクトリを切り、その中にlambdaのコードを書いていく。
まったく同じようにやるのは面白くないので日付を表示するLambdaを書いている。
import json
from datetime import datetime,timezone,timedelta
def handler(event, context):
return {
'statusCode': 200,
'body': f'Hello, AWS CDK! {datetime.now(timezone(timedelta(hours=9)))}'
}
LambdaとApiGatewayを定義する。
ここではStackと呼ばれるAWSのリソース群(LambdaとAPIGateway)を定義している。
AWSのリソースはConstructと呼ばれるもの(Construct LibraryにS3とかLambdaは定義されている)で、スタックはConstructが複数集まったものという認識で良さそう。
スタックがCloudFormationでの管理単位になる。
AWS CDK の3種類の Construct を使ってデプロイしてみた
ここでは、ApiStackに、LambdaとApiGatewayを定義している。
from constructs import Construct
from aws_cdk import (
Stack,
aws_lambda as _lambda,
aws_apigateway as apigateway,
)
class ApiStack(Stack):
def __init__(self, scope:Construct, id:str, **kwargs):
super().__init__(scope, id, **kwargs)
# APIGatewayで呼ばれるlambda
lambda_function = _lambda.Function(
self, 'HelloHandler',
runtime = _lambda.Runtime.PYTHON_3_8,
function_name = 'Hello',
code = _lambda.Code.from_asset('lambda'),
handler = 'hello.handler',
)
# apiGatewayを用意する
api = apigateway.LambdaRestApi(
self, 'hello-api',
handler = lambda_function
)
デプロイの流れと便利なコマンドまとめ
ここまで定義してきたリソースをdeployするためには、CloudFormationのテンプレートファイルに変換する必要がある。
そのため、cdk initをすると以下のようなファイルが生成される。(テンプレートファイルの合成というらしい)
ここで先ほど作ったApiStackを呼び出してあげるようにすると、cdk deployというコマンド叩くことで、
app.pyが実行され、それがAWSに反映されるという仕組みになっている。
#!/usr/bin/env python3
import aws_cdk as cdk
from cdk_api_test.api_stack import ApiStack
app = cdk.App()
ApiStack(app, "cdk-api-test")
app.synth()
cdk synthというコマンドを実行するとテンプレートファイルの合成ができる。
実際に実行してみるとこんな感じになった。
Resources:
HelloHandlerServiceRole11EF7C63:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: lambda.amazonaws.com
Version: "2012-10-17"
ManagedPolicyArns:
- Fn::Join:
- ""
- - "arn:"
- Ref: AWS::Partition
- :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Metadata:
aws:cdk:path: cdk-api-test/HelloHandler/ServiceRole/Resource
HelloHandler2E4FBA4D:
Type: AWS::Lambda::Function
Properties:
Code:
S3Bucket:
Fn::Sub: cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}
S3Key: 858be1ac788ecac49277e3b302cd59f1680dedd75249e49a23aa3b70b40f2e4a.zip
Role:
Fn::GetAtt:
- HelloHandlerServiceRole11EF7C63
- Arn
FunctionName: Hello
Handler: hello.handler
Runtime: python3.8
DependsOn:
- HelloHandlerServiceRole11EF7C63
Metadata:
aws:cdk:path: cdk-api-test/HelloHandler/Resource
aws:asset:path: asset.858be1ac788ecac49277e3b302cd59f1680dedd75249e49a23aa3b70b40f2e4a
aws:asset:is-bundled: false
aws:asset:property: Code
helloapi4446A35B:
Type: AWS::ApiGateway::RestApi
Properties:
Name: hello-api
Metadata:
aws:cdk:path: cdk-api-test/hello-api/Resource
helloapiDeploymentFA89AEEC67438e664a52adc5b41dc10017360ad4:
Type: AWS::ApiGateway::Deployment
Properties:
RestApiId:
Ref: helloapi4446A35B
Description: Automatically created by the RestApi construct
DependsOn:
- helloapiproxyANY9220DBE4
- helloapiproxy705C7382
- helloapiANY67B044D3
- helloapitestGET1C16631F
- helloapitestDFB1842D
Metadata:
aws:cdk:path: cdk-api-test/hello-api/Deployment/Resource
helloapiDeploymentStageprod677E2C4F:
Type: AWS::ApiGateway::Stage
Properties:
RestApiId:
Ref: helloapi4446A35B
DeploymentId:
Ref: helloapiDeploymentFA89AEEC67438e664a52adc5b41dc10017360ad4
StageName: prod
Metadata:
aws:cdk:path: cdk-api-test/hello-api/DeploymentStage.prod/Resource
cdk deployというコマンドを実行するとAWS上に実際にリソースを展開して文字通りAPIがデプロイされる。
実装したAPIを実行すると以下のように実行した日時とメッセージが表示される。
12/2公開の記事を12/2の深夜に執筆しているとか、余計なことには気づかないで欲しい。
Hello, AWS CDK! 2022-12-02 02:04:51.518130+09:00
このほかにも、Constructを使う例とか、DynamoDBを使う例などがあるので、このワークショップにチャレンジしてみるのもいいと思う。
修正していくうちにどう変化するのかをみたくなることがあると思う、そうgitでdiffを確認するかのように。
そんなときは前回との差分を出してくれるdiffコマンドが便利だ。
cdk diff
これは一部抜粋だが、stack名をcdk-api-test2に変えたときにどこが更新されるのかを出してくれている。
Stack cdk-api-test2
IAM Statement Changes
┌───┬─────────────────────────────────────────┬────────┬───────────────────────┬─────────────────────────────────────────┬──────────────────────────────────────────┐
│ │ Resource │ Effect │ Action │ Principal │ Condition │
├───┼─────────────────────────────────────────┼────────┼───────────────────────┼─────────────────────────────────────────┼──────────────────────────────────────────┤
│ + │ ${HelloHandler.Arn} │ Allow │ lambda:InvokeFunction │ Service:apigateway.amazonaws.com │ "ArnLike": { │
│ │ │ │ │ │ "AWS:SourceArn": "arn:${AWS::Partition │
│ │ │ │ │ │ }:execute-api:${AWS::Region}:${AWS::Acco │
│ │ │ │ │ │ untId}:${helloapi4446A35B}/${hello-api/D │
│ │ │ │ │ │ eploymentStage.prod}/*/*" │
│ │ │ │ │ │ } │
│ + │ ${HelloHandler.Arn} │ Allow │ lambda:InvokeFunction │ Service:apigateway.amazonaws.com │ "ArnLike": { │
│ │ │ │ │ │ "AWS:SourceArn": "arn:${AWS::Partition │
│ │ │ │ │ │ }:execute-api:${AWS::Region}:${AWS::Acco │
│ │ │ │ │ │ untId}:${helloapi4446A35B}/test-invoke-s │
│ │ │ │ │ │ tage/*/*" │
│ │ │ │ │ │ } │
│ + │ ${HelloHandler.Arn} │ Allow │ lambda:InvokeFunction │ Service:apigateway.amazonaws.com │ "ArnLike": { │
│ │ │ │ │ │ "AWS:SourceArn": "arn:${AWS::Partition │
│ │ │ │ │ │ }:execute-api:${AWS::Region}:${AWS::Acco │
│ │ │ │ │ │ untId}:${helloapi4446A35B}/${hello-api/D │
│ │ │ │ │ │ eploymentStage.prod}/*/" │
│ │ │ │ │ │ }
また開発中は、ローカルで修正しては反映して、という作業を繰り返すことになるのだが、その時の心強い見方が、cdk watchである。
watchという名の通り、変更のあったファイルを見つけ次第、cdk deployの簡易版を実行してAWSに変更を反映してくれる。
実際の動作時の様子
'watch' is observing the file 'app.py' for changes
'watch' is observing directory 'aws_cdk_python' for changes
'watch' is observing directory 'cdk_api_test' for changes
'watch' is observing directory 'lambda' for changes
'watch' is observing the file 'lambda/hello.py' for changes
'watch' is observing the file 'lambda/store_data.py' for changes
'watch' is observing the file 'cdk_api_test/api_caller.py' for changes
'watch' is observing the file 'cdk_api_test/api_stack.py' for changes
'watch' is observing the file 'aws_cdk_python/api_stack.py' for changes
'watch' is observing the file 'aws_cdk_python/fitbit_api_caller.py' for changes
Triggering initial 'cdk deploy'
✨ Synthesis time: 8.74s
⚠️ The --hotswap flag deliberately introduces CloudFormation drift to speed up deployments
⚠️ It should only be used for development - never use it for your production Stacks!
cdk-api-test2: building assets...
最後に不要になったスタックは cdk destroyで削除することができる。(AWSのリソースは使えば使うほどお金がかかるので、こまめに不要なものは削除すべし)
Are you sure you want to delete: cdk-api-test (y/n)? y
cdk-api-test: destroying...
2:19:19 AM | DELETE_IN_PROGRESS | AWS::CloudFormation::Stack | cdk-api-test
2:19:35 AM | DELETE_IN_PROGRESS | AWS::ApiGateway::Deployment | hello-api/Deployment
まとめ
AWSのリソースをコードベースで取り扱える AWS CDK に入門してみた。チュートリアルなどが豊富で概念の理解は比較的すぐにできた。何よりも画面ぽちぽちか、Terraformなどの学習コストの高いものを使わなくてもある程度のレベルのインフラを使い慣れた言語で構築できるのはありがたい。
一方で AWS CDK の問題なのか、AWSの理解が甘いからなのか、エラーが発生した時の対処には正直困った。
本当は、AWS CDKで、外部のAPIをコールする実装済みのLambdaを呼び出してきて、その結果をDynamoDBに保存してAPIの無駄なアクセスを減らすということをやりたかったのだが、LambdaからDynamoDBを呼び出す部分で権限エラーが発生してしまい、先に進めなかった。
コンソールからロールを確認するとちゃんとwrite & read 権限がついているんだけどな...
ApiStoreData is not authorized to perform: dynamodb:GetItem on resource: arn:aws:dynamodb:ap-northeast-1:xxxxxxxx:table/fitbit-api-result because no identity-based policy allows the dynamodb:GetItem action"
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"dynamodb:BatchGetItem",
"dynamodb:BatchWriteItem",
"dynamodb:ConditionCheckItem",
"dynamodb:DeleteItem",
"dynamodb:DescribeTable",
"dynamodb:GetItem",
"dynamodb:GetRecords",
"dynamodb:GetShardIterator",
"dynamodb:PutItem",
"dynamodb:Query",
"dynamodb:Scan",
"dynamodb:UpdateItem"
],
"Resource": [
"arn:aws:dynamodb:ap-northeast-1:xxxxxxxxx:table/fitbit-api-store"
],
"Effect": "Allow"
}
]
}
というわけで、本来やりたかったところまでは行けなかったが、 CDKというものをじっくり体験できたと思う。
追記
Lambdaで呼んでいるDynamoDBのテーブル名とStackなどで定義したDynamoDBのテーブル名が違っていたのが問題だっただけでした。お騒がせしました。