AWS CDKをlocalstackで練習する
極力お金をかけないようにAWSのサービスを練習していきたいという思いから、何番煎じかわからないけどAWS CDKをlocalstackで練習してみます。
AWS CDK v2が出ているので、localstackでも動作するかなぁと思ってやってみたらできたので備忘録として残すことにしました。
環境としてはWindows10のWSL2上で試しているので、macでもlinuxでも多分動くでしょう。
API Gateway + Lambda + S3をAWS CDKで定義してlocalstackで動くことを確認していきます。
AWS CDKとlocalstack
AWS CDKとは
AWSを使ったアプリケーションは、多数のサービスを互いに連携することが多く、そのようなサービスのセットアップだったりクラウドリソースの管理だったりが面倒になってきます。
そこでAWSにはAWS CloudFormationというサービスがあり、これを使えば各リソースをテンプレートとして記述でき、このテンプレートからAWSのサービスをプロビジョニングしてくれます。
しかし、AWS CloudFormationのテンプレートを使って複雑なアプリケーションを記述する際に、サービスごとに必要なパラメータや固有の組み込み関数を覚える必要があったりするので、巨大なテンプレートほど管理が難しくなってきます。
そこで、登場するのがAWS CDKで、
AWS CDKを使うことでコードからCloudFormationのテンプレートをビルドすることができます。CloudFormationで覚えるべきものは特に必要なく、プログラミング言語の知識とプロビジョニングしたいリソースの設定が明確になっていれば、CloudFormation知らなくてもIaCできちゃいます。
下記のサポートされている言語を使ってAWSのリソースを定義していくことができるようです。(2022年1月現在) (参考)
- JavaScript
- TypeScript
- Python
- Java
- C#
- Go (in Developer Preview)
リソースのプロビジョニングは基本静的に行われると思うので、静的型付き言語でCDKを記述したほうが補完が効いて便利だと思います。
localstackとは
localstackとは簡単に言えば、AWSサービスのmockフレームワークです。
AWSの各サービスをローカル環境でmockすることで、AWSに課金せずにAWSのサービスを触れます。
用途としては、ローカル環境でAWSのサービスを試してみたり、AWSのサービスにデプロイする前のtestとしてCIに導入したりできます。
Free版とPro版があり、使用できるサービスに違いがでてきます(参考)。
AppSyncとかちょっとリッチなやつを使うならProなんですが、練習なんでFreeで。
必要なもの
- aws cli
- aws cli の docker を利用
aws-cli/2.4.11
- aws-cdk
v2.3.5
- docker
Docker version 20.10.12, build e91ed57
- docker-compose
docker-compose version 1.29.2, build 5becea4c
- localstack
- tag 0.12.19のdocker image
今回はdockerを使ってlocalstackを操作していきます。
https://hub.docker.com/r/localstack/localstack
下準備
aws cliを楽して使えるようにエイリアスの設定しておきましょう。
alias awsd='docker run --rm -ti -v ~/.aws:/root/.aws -v $(pwd):/aws amazon/aws-cli --profile=localstack'
docker aws cliでaws cliの設定します。
awsd configure
$ awsd configure
AWS Access Key ID [None]: dummy
AWS Secret Access Key [None]: dummy
Default region name [None]: ap-northeast-1
Default output format [None]: json
ip a
でdockerのipアドレスを確認しておきます(後で使う)。
$ ip a | grep docker
7: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
inet 172.18.0.1/16 brd 172.18.255.255 scope global docker0
面倒なので環境変数として登録しておきましょう。
export LOCALSTACK_IP=172.18.0.1
localstackを試す
まずはdocker単体でlocalstackを試してみます。S3だったりlambdaがうまく機能するか確認しましょう。
S3のサービスを試してみる
dockerでlocalstackを起動して、s3 bucketを作成して、s3にfileを適当にuploadできることを確認してみます。
別のウィンドウかなんかでdocker run
しておきます。
$ docker run --rm -it -p 4566:4566 -p 4571:4571 localstack/localstack:0.12.19
$ awsd s3 mb s3://test-bucket --endpoint-url=http://$LOCALSTACK_IP:4566
make_bucket: test-bucket
$ awsd s3 ls --endpoint-url=http://$LOCALSTACK_IP:4566
2022-01-16 10:22:17 test-bucket
$ echo "hello" > test.txt
$ awsd s3 --endpoint-url=http://$LOCALSTACK_IP:4566 cp test.txt s3://test-bucket
Completed 6 Bytes/6 Bytes (219 Bytesupload: ./test.txt to s3://test-bucket/test.txt
$ awsd s3 ls --endpoint-url=http://$LOCALSTACK_IP:4566 s3://test-bucket
2022-01-16 10:22:45 6 test.txt
Lambdaのサービスを試してみる
同様にして次はlambdaの動作を確認してみます。
まずはlambdaで動かす適当な関数を作成します。
今回はお手軽にpythonで、依存関係のあるデプロイメントパッケージについては別で考えることにして、簡単にs3のtest.txtを読み込むためのスクリプトを書いてみます。
lambda_function.py
import boto3
import os
LOCALSTACK_IP = os.environ['LOCALSTACK_IP']
# localstackのendpointを指定
s3 = boto3.resource(
aws_access_key_id='dummy',
aws_secret_access_key='dummy',
region_name='ap-northeast-1',
service_name='s3',
endpoint_url = f'http://{LOCALSTACK_IP}:4566'
)
def lambda_handler(event, context):
bucket = 'test-bucket'
key = 'test.txt'
res = s3.Bucket(bucket).Object(key).get()
body = res['Body'].read()
return {
"statusCode": 200,
"body": body,
}
この情報を参考にzip化します。
$ zip lambda.zip lambda_function.py
adding: lambda_function.py (deflated 35%)
zip化したlambdaをlocalstackにデプロイしてみましょう。
$ awsd lambda create-function --function-name="test-function" --runtime=python3.7 --role="arn:aws:iam::123456789012:role/service-role/lambda-test-role" --handler=lambda_function.lambda_handler --zip-file fileb://lambda.zip --endpoint-url=http://$LOCALSTACK_IP:4566 --environment "Variables={LOCALSTACK_IP=$LOCALSTACK_IP}"
{
"FunctionName": "test-function",
"FunctionArn": "arn:aws:lambda:ap-northeast-1:000000000000:function:test-function",
"Runtime": "python3.7",
"Role": "arn:aws:iam::123456789012:role/service-role/lambda-test-role",
"Handler": "lambda_function.lambda_handler",
"CodeSize": 552,
"Description": "",
"Timeout": 3,
"LastModified": "2022-01-16T10:24:36.017+0000",
"CodeSha256": "o25yVYgkWJA8JsW+cSYQu6o6psRwlg01DVsW/Rr2InU=",
"Version": "$LATEST",
"VpcConfig": {},
"Environment": {
"Variables": {
"LOCALSTACK_IP": "172.18.0.1"
}
},
"TracingConfig": {
"Mode": "PassThrough"
},
"RevisionId": "569fecc6-c8b4-4851-86de-cab341f5b5c2",
"State": "Active",
"LastUpdateStatus": "Successful",
"PackageType": "Zip"
}
間違って上げてしまったらdelete-functionして作り直すかupdate-function-codeします。
awsd lambda delete-function --function-name="test-function" --endpoint-url=http://${LOCALSTACK_IP}:4566
いい感じにデプロイできたら、invokeしてみましょう。
$ awsd lambda invoke --function-name test-function out.json --endpoint-url=http://$LOCALSTACK_IP:4566
{
"StatusCode": 200,
"LogResult": "",
"ExecutedVersion": "$LATEST"
}
lambdaはS3にファイルの内容を取得するやつなので、lambdaの実行結果(out.json
)でファイル内容が取得できているか確認してみる。
$ cat out.json
{"body":"hello\n","statusCode":200}
簡単にですが、localstack
を試すことができたので次はcdkも使って、リソースを定義してIaCしていきましょう。
ここまでのディレクトリ構成はこんな感じ。
.
├── lambda.zip
├── lambda_function.py
├── out.json
└── test.txt
cdkを使ってlocalstackにリソースをdeployする
cdklocalを使ってcdkプロジェクトを作成する
普通にnpm install aws-cdk-local aws-cdk
してcdklocal init
します。
ただし、cdklocal init は空のディレクトリで行わないといけないので、mkdir infra; cd infra
してそこでinitしましょう(なんとなくtypescriptを選びました)。今回はひとつ上のディレクトリでcdklocalをinstallしているので、../node_modules/.bin/cdklocal
としてcdklocalのコマンドを叩いていきます。
とりあえずsample-appを作ってみます。sample-appではリソースとしてSQSとSNSが定義されています。
../node_modules/.bin/cdklocal init sample-app --language=typescript
ついでにわかりやすいようにlambda_functionはlambda
ディレクトリとか作ってまとめておきます。
init後のディレクトリ構成。
.
├── infra
│ ├── README.md
│ ├── bin
│ ├── cdk.json
│ ├── jest.config.js
│ ├── lib
│ ├── node_modules
│ ├── package-lock.json
│ ├── package.json
│ ├── test
│ └── tsconfig.json
├── lambda
│ ├── lambda.zip
│ ├── lambda_function.py
│ ├── out.json
│ └── test.txt
├── node_modules
│ ├── aws-cdk
│ ├── aws-cdk-local
│ └── diff
├── package-lock.json
└── package.json
なんとなく、これも練習ということで、docker-composeでlocalstackを扱えるようにyamlを書いてみました。
version: "3.8"
services:
localstack:
container_name: "${LOCALSTACK_DOCKER_NAME-localstack_main}"
image: localstack/localstack:0.12.19
network_mode: bridge
ports:
- "4566:4566"
- "4571:4571"
environment:
- SERVICES=${SERVICES-}
- DEBUG=${DEBUG-}
- DATA_DIR=${DATA_DIR-}
- PORT_WEB_UI=${PORT_WEB_UI- }
- LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR-}
- KINESIS_ERROR_PROBABILITY=${KINESIS_ERROR_PROBABILITY- }
- HOST_TMP_FOLDER=${TMPDIR:-/tmp/}localstack
- DOCKER_HOST=unix:///var/run/docker.sock
- LOCALSTACK_HOSTNAME=localhost
volumes:
- "${TMPDIR:-/tmp}/localstack:/tmp/localstack"
- "/var/run/docker.sock:/var/run/docker.sock"
docker-compose up -d
でlocalstackを起動させておきます。
続いてdeployのためにまずはbootstrapします。
../node_modules/.bin/cdklocal bootstrap --profile=localstack
を実行して、
$ ../node_modules/.bin/cdklocal bootstrap --profile=localstack
⏳ Bootstrapping environment aws://000000000000/ap-northeast-1...
CDKToolkit: creating CloudFormation changeset...
✅ Environment aws://000000000000/ap-northeast-1 bootstrapped.
上記の出力が表示されればbootstrapが完了ですので、早速サンプルをdeployしてみましょう。
$ ../node_modules/.bin/cdklocal deploy --profile=localstack
✨ Synthesis time: 7.71s
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:
IAM Statement Changes
┌───┬──────────────────┬────────┬──────────────────┬──────────────────┬───────────────────┐
│ │ Resource │ Effect │ Action │ Principal │ Condition │
├───┼──────────────────┼────────┼──────────────────┼──────────────────┼───────────────────┤
│ + │ ${InfraQueue.Arn │ Allow │ sqs:SendMessage │ Service:sns.amaz │ "ArnEquals": { │
│ │ } │ │ │ onaws.com │ "aws:SourceArn" │
│ │ │ │ │ │ : "${InfraTopic}" │
│ │ │ │ │ │ } │
└───┴──────────────────┴────────┴──────────────────┴──────────────────┴───────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)
Do you wish to deploy these changes (y/n)? y
InfraStack: deploying...
[0%] start: Publishing 8d676cc0cf59dc2cf4a373ed0381e270c534d9be9521322b3d416393af5f8070:current_account-current_region
[100%] success: Published 8d676cc0cf59dc2cf4a373ed0381e270c534d9be9521322b3d416393af5f8070:current_account-current_region
InfraStack: creating CloudFormation changeset...
✅ InfraStack
✨ Deployment time: 5.84s
Stack ARN:
arn:aws:cloudformation:ap-northeast-1:000000000000:stack/InfraStack/df24d1bb
✨ Total time: 13.55s
ちゃんとdeploy出てきているか確認します。
cdk initのsample-appで作成されるリソースはsns topicがあるので、このリソースがlocalstack上で作成されているか確認していきます。
$ awsd sns list-topics --endpoint-url=http://$LOCALSTACK_IP:4566
{
"Topics": [
{
"TopicArn": "arn:aws:sns:ap-northeast-1:000000000000:InfraStack-InfraTopic44770A4E-b1ee5f5a"
}
]
}
これでlocalstackを使ったcdklocalの動作確認としては完了!
自前のlambdaとかS3とかAPI Gatewayをdeploy
SQSやSNSだけでなく、lambdaとS3もdeployしましょう。
また、実際のサービスで使用できるようにAPI Gatewayも定義して、endpointを叩けばlambdaが実行されて、S3にファイルがputされるようにCDKで作っていきます。
まずはS3 Bucket名をCDKから取得できるように、lambdaを少しだけ書き換えます。deployするためにちゃんとzip化するのを忘れないこと!
import json
import boto3
import os
from datetime import datetime
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
LOCALSTACK_IP = os.environ['LOCALSTACK_IP']
BUCKET_NAME = os.environ['BUCKET_NAME']
REGION = os.environ['AWS_REGION']
# localstackのendpointを指定
s3 = boto3.resource(
service_name='s3',
endpoint_url = f'http://{LOCALSTACK_IP}:4566',
region_name=REGION
)
def lambda_handler(event, context):
datestr = datetime.now().strftime('%Y%m%d-%H%M%S')
key = f'test_{datestr}.txt'
file_contents = 'Hello! Lambda File!!'
s3.Bucket(BUCKET_NAME).put_object(
Key=key,
Body=file_contents
)
return {
"statusCode": 200,
"body": json.dumps({
"message": 'File created'
}),
}
S3とLambda, API Gatewayをcdkで追加定義する。
import { Duration, Stack, StackProps } from "aws-cdk-lib";
import * as sns from "aws-cdk-lib/aws-sns";
import * as subs from "aws-cdk-lib/aws-sns-subscriptions";
import * as sqs from "aws-cdk-lib/aws-sqs";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as iam from "aws-cdk-lib/aws-iam";
import * as cdk from "aws-cdk-lib";
import * as apigw from "aws-cdk-lib/aws-apigateway";
import { Construct } from "constructs";
export class InfraStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const queue = new sqs.Queue(this, "InfraQueue", {
visibilityTimeout: Duration.seconds(300),
});
const topic = new sns.Topic(this, "InfraTopic");
topic.addSubscription(new subs.SqsSubscription(queue));
// Define S3 bucket resource
const testBucket = new s3.Bucket(this, "TestBucket");
const testLambdaRole = new iam.Role(this, "TestLambdaRole", {
assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
});
testLambdaRole.addManagedPolicy(
iam.ManagedPolicy.fromAwsManagedPolicyName("AWSLambdaBasicExecutionRole")
);
// Define Lambda resource
const testLambda = new lambda.Function(this, "TestLambda", {
runtime: lambda.Runtime.PYTHON_3_7,
handler: "lambda_function.lambda_handler",
code: lambda.Code.fromAsset("../lambda/lambda.zip"),
environment: {
LOCALSTACK_IP: `${process.env.LOCALSTACK_IP}`,
BUCKET_NAME: `${testBucket.bucketName}`,
},
role: testLambdaRole,
});
testBucket.grantPut(testLambda);
// Define API Gateway
// 本当ならLambdaRestApiで簡単にしたいけど、localstack上でendpointを作るので、、
const testApiGW = new apigw.LambdaRestApi(this, "TestApiGW", {
handler: testLambda,
});
new cdk.CfnOutput(this, "TestLambdaName", {
value: testLambda.functionName,
});
new cdk.CfnOutput(this, "TestS3BucketName", {
value: testBucket.bucketName,
});
new cdk.CfnOutput(this, "TestApiGwEndPoint", {
value: `http://localhost:4566/restapis/${testApiGW.restApiId}/prod/_user_request_${testApiGW.root.path}`,
});
}
}
これをdeployして、Outputを確認する。
✅ InfraStack
✨ Deployment time: 31.57s
Outputs:
InfraStack.TestApiGWEndpointF4D06F73 = https://vvv23joyjg.execute-api.ap-northeast-1.localhost/prod/
InfraStack.TestApiGwEndPoint = http://localhost:4566/restapis/vvv23joyjg/prod/_user_request_/
InfraStack.TestLambdaName = InfraStack-TestLambda2F70C45E-4c84ea0f
InfraStack.TestS3BucketName = infrastack-testbucket560b80bc-014d95ad
Stack ARN:
arn:aws:cloudformation:ap-northeast-1:000000000000:stack/InfraStack/df24d1bb
✨ Total time: 39.75s
deployされているはずなので、上記のTestApiGeEndPoint
を叩いてみる。
$ curl http://localhost:4566/restapis/vvv23joyjg/prod/_user_request_/
{"message": "File created"}
lambdaが呼び出されてS3にファイルができていることを確認する。
$ awsd s3 ls s3://infrastack-testbucket560b80bc-014d95ad --endpoint-url=http://$LOCALSTACK_IP:4566
2022-01-16 11:41:52 20 test_20220116-114152.txt
見事deployされていることが確認できました!!やったね!
まとめ
ということで、AWS CDKとlocalstackを使って、API Gateway + Lambda + S3の簡単なIaC練習をやってみました。今回のコードはここにあげていますので、何かの参考になれば。
AWS CDKとlocalstackの組み合わせはだいぶ昔から知っていましたが、今回AWS CDK v2でもなんとなく動作することが確認できたので良かったです。
今後も練習として、localstackで色々マイクロサービスアーキテクチャを試していけたらいいなと思います。
Discussion