AWS CDKでECS/FargateとRDSを作成
1. 前書き
1.1. CDKとは何か?
AWS Cloud Development Kit (AWS CDK) は、使い慣れたプログラミング言語を使用してクラウドアプリケーションリソースを定義するためのオープンソースのソフトウェア開発フレームワークです。
https://aws.amazon.com/jp/cdk/
使い慣れたプログラミング言語、ツール、ワークフローの使用
AWS CDK を使用すると、TypeScript、Python、Java、.NET、および Go (開発者プレビュー) を使用してアプリケーションインフラストラクチャをモデル化できます。CDK では、開発者は既存の IDE、テストツール、およびワークフローパターンを使用できます。自動入力ドキュメントやインラインドキュメントなどのツールを活用することで、AWS CDK では、サービスドキュメントとコードの切り替えにかかる時間を短縮できます。
https://aws.amazon.com/jp/cdk/features/
1.2. この記事の目的
この記事の目的は、AWS CDKでECS/FargateとRDSの環境を作成する手順を個人的なメモとして記すことです。
1.3. 試したかったこと
実は今回はCDKを試したのは、二次的な要素でした。
コンテナ構築の効率化とセキュリティー向上をテーマに「CloudNative Buildpacks」と「コンテナイメージのスキャン」を試そうとしていました。
そのための検証環境が必要だったため、CDKで作るのが効率が良さそうと考えて、CDKも試してみたという背景があります。
今回は、以下リソースをCDKで作成することを意図しました。
- VPC関連のリソース(VPC, Subnet, Gateway)
- RDS
- ECS/Fargate関連のリソース(Cluster, Service, TaskDefinition)
- ApplicationLoadBalancer
また、関連して、以下のリソースも作成します。
- IAM Poclicy
- SecretsManager
- SecurityGroup
1.4. 構成図(概略)
概略とはいえ、雑すぎる図になってしまいました。
1.5. 前提事項
以下のインストールについては済んでいるものとします。
- CDK
- typescript
ちなみに、私の環境は以下のようになっています。(2022/2/20時点)
- CDK 2.12.0
- typescript 3.9.7
- VSCode 1.64.2
- macOS Monterey 12.1
2. CDKでリソースを作ってみる
2.1. CDK利用の戦略
CDKには、大きく分けると、
- L2 Library(AWS Construct Library)
- L1 Library(AWS CloudFormation Resource)
の2つがあります。
L2は、高水準な仕様となっており、最低限のパラメーターで、いい感じに直接指定のないリソースも自動生成するようになっています。(その分細かい調整が難しいです)。
一方のL1は、生のCloudFormationを書くのとほぼ同等のパラメーター設定が必要で、細かい調整が可能ですが、記述が冗長になります。
状況にもよりますが、CDKのメリットを最大限享受するならば、なるべくL2を使うのが良いと思います。
今回は、L2を利用して必要な部分を都度オプションパラメーターで調整しています。
2.2. 実装を見てみる
以下が第一弾のソースコードです。基本的には最小限の定義にしており、関連するリソースは自動生成に委ねています。
ただし、このコードだとうまく生成されないリソースがあります。
import { Duration, Stack, StackProps } from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as ecs from "aws-cdk-lib/aws-ecs";
import * as ecsPatterns from "aws-cdk-lib/aws-ecs-patterns";
import { Construct } from "constructs";
import {
DatabaseCluster,
DatabaseClusterEngine,
} from "aws-cdk-lib/aws-rds";
export class TestCdkStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// VPC関連のリソース作成
const vpc: ec2.Vpc = new ec2.Vpc(this, "AbeTestVpc", {
cidr: "10.x.0.0/16",
subnetConfiguration: [ // Optional(省略すると、PUBLICとPRIVATE_WITH_NATのみ生成される)
{
cidrMask: 24,
name: "ingress",
subnetType: ec2.SubnetType.PUBLIC,
},
{
cidrMask: 24,
name: "application",
subnetType: ec2.SubnetType.PRIVATE_WITH_NAT,
},
{
cidrMask: 28,
name: "rds",
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
],
});
// Security Group(自動生成に任せる)
// RDS(最低限の設定としてある)
const rdsCluster = new DatabaseCluster(this, "AbeTestRds", {
engine: DatabaseClusterEngine.AURORA_MYSQL,
instanceProps: {
vpc,
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
instanceType: ec2.InstanceType.of(
ec2.InstanceClass.BURSTABLE2,
ec2.InstanceSize.SMALL
),
},
defaultDatabaseName: "abeTest",
});
// ECS Cluster
const cluster = new ecs.Cluster(this, "AbeTestCluster", {
vpc: vpc,
});
// ALB, FargateService, TaskDefinition
const loadBalancedFargateService =
new ecsPatterns.ApplicationLoadBalancedFargateService(
this,
"AbeTestService",
{
cluster: cluster, // Required
memoryLimitMiB: 512,
cpu: 256,
desiredCount: 1, // Optional(省略値は3)
listenerPort: 80,
taskImageOptions: {
image: ecs.ContainerImage.fromRegistry(
"my_account/spring-boot-docker"
),
containerPort: 8080,
},
healthCheckGracePeriod: Duration.seconds(240),
}
);
// HealthCheckの設定
loadBalancedFargateService.targetGroup.configureHealthCheck({
path: "/custom-health-path",
healthyThresholdCount: 2, // Optional
interval: Duration.seconds(15), // Optional
});
}
}
3. この時点での課題
- RDS(DB)のSecurityGroupにInboundルールが作成されていない
- ECSからSecretsManagerを参照するIAMポリシーが「タスク実行ロール」にアタッチされていない
- ECSからSecretsManagerを参照する「環境変数」の定義がタスク定義に作成されていない
3.1. RDS(DB)のSecurityGroupにInboundルールが作成されていない
3.2. ECSからSecretsManagerを参照するIAMポリシーが「タスク実行ロール」にアタッチされていない
実は、RDSを作成した段階で自動的に、SecretsManagerはできている。
しかし、CDKでそれをどこでどう使うか?を指定していないためにアタッチがされていない。
ちなみに、ECSからのLog出力のIAMポリシーはアタッチされている。
3.3. ECSからSecretsManagerを参照する「環境変数」の定義がタスク定義に作成されていない
4. 課題の解決
課題1.の解決
現状は、CDKがSecurityGroupを自動的に生成してくれています。
例えば、RDSのセキュリティーグループについては、API仕様をみると以下のように記載されています。
securityGroups?
Type: ISecurityGroup[] (optional, default: a new security group is created.)
Security group.
DatabaseClusterのコンストラクターに渡す、InstancePropsのsecurityGroupsは、省略可能で、省略した際には、新しいsecurity groupが(自動的に)作られるという記載があります。
従って、自前で、SecurityGroupを生成して、それを渡してあげれば良さそうです。
変更箇所は、以下のようになります。
+ // VPC定義の後で、RDS定義より前に追加
+ // RDS SecurityGroup設定
+ const ecsSG = new SecurityGroup(this, "AbeTestEcsSecurityGroup", {
+ vpc,
+ });
+
+ const rdsSG = new SecurityGroup(this, "AbeTestRdsSecurityGroup", {
+ vpc,
+ allowAllOutbound: true,
+ });
+ // ↓ ここがポイント
+ rdsSG.connections.allowFrom(ecsSG, Port.tcp(3306), "Ingress 3306 from ECS");
+
// RDS
const rdsCluster = new DatabaseCluster(this, "AbeTestRds", {
engine: DatabaseClusterEngine.AURORA_MYSQL,
instanceProps: {
vpc,
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
+ securityGroups: [rdsSG],
instanceType: ec2.InstanceType.of(
ec2.InstanceType.of(
ec2.InstanceClass.BURSTABLE2,
ec2.InstanceSize.SMALL
),
},
defaultDatabaseName: "abeTest",
});
// 〜 中略 〜
// ALB, FargateService, TaskDefinition
const loadBalancedFargateService =
new ecsPatterns.ApplicationLoadBalancedFargateService(
this,
"AbeTestService",
{
cluster: cluster, // Required
memoryLimitMiB: 512,
cpu: 256,
desiredCount: 1, // Optional(Default === 3 !!!)
listenerPort: 80,
taskImageOptions: {
image: ecs.ContainerImage.fromRegistry(
"akiraabe/spring-boot-docker"
),
containerPort: 8080,
},
+ securityGroups: [ecsSG],
healthCheckGracePeriod: Duration.seconds(240),
}
);
課題2.の解決
今回は、「aws-cdk-lib.aws_ecs_patterns module」を用いて、ALBとECSを作成しています。
一方で、RDSは個別に定義しているので、RDSとそこから生成されるSecretsManagerがいわば蚊帳の外の状態になっています。
従って、ECSのタスク実行ロールに、自前で生成したSecretsManager読み取りポリシーをアタッチすれば良さそうです。
コードの変更箇所は以下の通りです。
+ // RDS定義の後に追加
+ // SecretsManager(RDSにより自動設定)
+ const secretsmanager = rdsCluster.secret!;
+
+ // 最後に追加
+ // Add SecretsManager IAM policy to FargateTaskExecutionRole
+ const escExecutionRole = Role.fromRoleArn(
+ this,
+ "ecsExecutionRole",
+ loadBalancedFargateService.taskDefinition.executionRole!.roleArn,
+ {}
+ );
+ escExecutionRole.attachInlinePolicy(new Policy(this, 'abeTestSMGetPolicy', {
+ statements: [new PolicyStatement({
+ actions: ['secretsmanager:GetSecretValue'],
+ resources: [secretsmanager.secretArn],
+ })],
+ }));
課題3.の解決
課題2と原理は一緒です。SecretsManagerの値を、タスク定義に渡してあげればうまくいきます。
// ALB, FargateService, TaskDefinition
const loadBalancedFargateService =
new ecsPatterns.ApplicationLoadBalancedFargateService(
this,
"AbeTestService",
{
cluster: cluster, // Required
memoryLimitMiB: 512,
cpu: 256,
desiredCount: 1, // Optional(Default === 3 !!!)
listenerPort: 80,
taskImageOptions: {
image: ecs.ContainerImage.fromRegistry(
"akiraabe/spring-boot-docker"
),
containerPort: 8080,
+ // Secretの設定
+ secrets: {
+ "dbname": ecs.Secret.fromSecretsManager(secretsmanager, 'dbname'),
+ "username": ecs.Secret.fromSecretsManager(secretsmanager, 'username'),
+ "host": ecs.Secret.fromSecretsManager(secretsmanager, 'host'),
+ "password": ecs.Secret.fromSecretsManager(secretsmanager, 'password'),
+ }
},
securityGroups: [ecsSG],
healthCheckGracePeriod: Duration.seconds(240),
}
);
これにより、課題が解決され、ALB〜RDSまで一気通貫でつながります。
5. まとめ
5.1. 所感
今回は、手っ取り早くコンテナ環境とDBを作るということにフォーカスしています。
実験用なので、1セットのStackとしてあり、用が済んだら全てcdk destroy
で消してしまうという思想です。
もう少し実用的なインフラを構築するためには、さらに、以下の課題があると思います。
- Stackを分割して、毎回RDSが消えないようにするなど、リソースのライフサイクルに合わせる
- CodePipelineを用いて、アプリケーションのリリースの自動化にも対応する
- Blue/Greenデプロイなどにも対応する
5.2. CDKの展望?
AmplifyやChaliceがCDKと連携する機能をリリースしているので、今後、AWSのサービス間での連携にCDKが使われていく可能性が高いと思います。
5.3. 参考にしたサイト
以下のサイトを参考にしています。(特に、API仕様は必読です。)
APIドキュメント
Construct Hub
Example
ClassmethodさんのじっせんCDK(L1の記述例として有用です)
stackoverflow (SecurityGroupの設定の際に参考になりました)
5.4. ソースコード全体
ソースコード全体を添付しておきます。
Docker&VSCodeで動かす前提となっていますが、サブディレクトリのtestCdkだけ取り出して、ローカル環境にNodeやcdkをインストールすればDockerなしでも動かせます。
Discussion