前回の記事「Cloudformationを使ったFargateサービスのデプロイプロセスについて考えてみた」のCDK版です。
CDKを実際に使ってみたかったので、比較も兼ねて前回のCloudformationファイルをCDKに置き換えてみました。
(一部設定を変えている部分がありますので全て完全に一致しているわけではないです)
前回記事の「4.Fargateへのデプロイ」の部分の代替手段となる方法ですので前回記事とあわせてご確認ください。
コードはこちら。
https://github.com/nyasba/fargate-spring-web/tree/qiita-cdk/cdkdeploy
Fargateへのデプロイ(CDK版)
プロジェクト構成
typescript+yarnで開発しました。
├── README.md
├── bin コマンド実行時のエンドポイントとなるtsファイル
├── cdk.context.json cdkのcontextで参照したデータの保存ファイル(VPC内のネットワーク構成など次回のStack作成時に変更すべきでないリソースの情報を保存する)
├── cdk.json cdk実行時のapp引数のデフォルト設定ファイル
├── cdk.out cdkの出力ファイル
├── jest.config.js テスト用の設定ファイル
├── lib cdkのコード
├── node_modules
├── package.json
├── test テストコード
├── tsconfig.json
└── yarn.lock yarnのlockファイル
※cdk.context.jsonはデプロイした環境が保存されるので本来はGit管理下においておくべきものです
https://docs.aws.amazon.com/cdk/latest/guide/context.html
CDKのコード説明
CDKのentrypointとなるコードです。ここの中で既存で存在するVPCをCDKのインスタンスとして取得する(Lookup)はできないようでした。
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';
import { AlbFargateStack } from '../lib/alb-fargate-stack';
import devProps from '../env/example.json'
const app = new cdk.App();
const stack = new AlbFargateStack(app, devProps.projectName, devProps);
app.synth();
Stackクラスです。 Props/ExportはCDKのコードの作法に合わせました。基本HighLevelなクラスを利用して作成することにしています。前回の記事から変更したところはPrivateSubnetにSubnetを配置し、PublicIPを割り当てないようにした点です。そのため、事前にPrivateLinkの設定が必要となっています。
import * as cdk from '@aws-cdk/core';
import ec2 = require("@aws-cdk/aws-ec2");
import ecs = require("@aws-cdk/aws-ecs");
import ecr = require("@aws-cdk/aws-ecr");
import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2');
import applicationautoscaling = require('@aws-cdk/aws-applicationautoscaling')
import iam = require('@aws-cdk/aws-iam')
import logs = require("@aws-cdk/aws-logs");
import { Duration, Tag } from '@aws-cdk/core';
import { StackPropsBase, StackExportsBase } from './stack-base';
import { SubnetSelection } from '@aws-cdk/aws-ec2';
interface AlbFargateStackProps extends StackPropsBase {
ecrRepositoryName: string,
vpcId: string,
albSubnetSelection?: SubnetSelection,
ecsSubnetSelection?: SubnetSelection,
logGroupName: string,
taskExecutionRoleName: string,
containerRoleName: string,
autoscalingRoleName: string
}
interface AlbFargateStackExports extends StackExportsBase {
endpointUrl: string
}
export class AlbFargateStack extends cdk.Stack {
readonly props: AlbFargateStackProps
readonly exports: AlbFargateStackExports
constructor(scope: cdk.Construct, id: string, props: AlbFargateStackProps) {
super(scope, id, props)
this.props = props
// vpc (既存リソースを参照する)
const appVpc = ec2.Vpc.fromLookup(this, 'VPC', {
vpcId: this.props.vpcId
})
// role (既存リソースを参照する)
const appTaskExecutionRole = iam.Role.fromRoleArn(this, 'task-exec-role',
`arn:aws:iam::${this.account}:role/${this.props.taskExecutionRoleName}`)
const appContainerRole = iam.Role.fromRoleArn(this, 'task-role',
`arn:aws:iam::${this.account}:role/${this.props.containerRoleName}`)
const appAutoscalingRole = iam.Role.fromRoleArn(this, 'autoscaling-role',
`arn:aws:iam::${this.account}:role/${this.props.autoscalingRoleName}`)
// tagの設定 全般に適用される
Tag.add(this, 'project', this.props.projectName)
// sg for alb
const albSecurityGroup = new ec2.SecurityGroup(this, this.createResourceName('alb-sg'), {
securityGroupName: this.createResourceName('alb-sg'),
vpc: appVpc
})
albSecurityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80))
albSecurityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443))
// sg for ecs serivce
const ecsSecurityGroup = new ec2.SecurityGroup(this, this.createResourceName('ecs-sg'), {
securityGroupName: this.createResourceName('ecs-sg'),
vpc: appVpc
})
ecsSecurityGroup.addIngressRule(albSecurityGroup, ec2.Port.tcp(80))
// alb target group
const albTargetGroup = new elbv2.ApplicationTargetGroup(this, this.createResourceName('tg'), {
vpc: appVpc,
targetGroupName: this.createResourceName('tg'),
protocol: elbv2.ApplicationProtocol.HTTP,
port: 80,
healthCheck: { path: "/healthcheck" }
})
// alb
const appAlb = new elbv2.ApplicationLoadBalancer(this, this.createResourceName('alb'), {
vpc: appVpc,
vpcSubnets: appVpc.selectSubnets(this.props.albSubnetSelection || { subnetType: ec2.SubnetType.PUBLIC }),
loadBalancerName: this.createResourceName('alb'),
internetFacing: true,
securityGroup: albSecurityGroup
})
appAlb.addListener(this.createResourceName('alb-listener'), {
protocol: elbv2.ApplicationProtocol.HTTP,
port: 80,
defaultTargetGroups: [albTargetGroup]
})
// ecs cluster
const appCluster = new ecs.Cluster(this, this.createResourceName('cluster'), {
vpc: appVpc,
clusterName: this.createResourceName('cluster')
})
// log group for ecs (import)
const appLogGroup = logs.LogGroup.fromLogGroupName(this,
this.createResourceName('log-group'), this.props.logGroupName)
// ecr repository (import)
const appEcrRepository = ecr.Repository.fromRepositoryName(this,
this.createResourceName('repository'), this.props.ecrRepositoryName)
// task definition
const appTaskDefinition = new ecs.TaskDefinition(this, this.createResourceName('task'), {
family: this.createResourceName('task'),
executionRole: appTaskExecutionRole,
taskRole: appContainerRole,
networkMode: ecs.NetworkMode.AWS_VPC,
compatibility: ecs.Compatibility.FARGATE,
cpu: '512',
memoryMiB: '1024'
})
// container definition
const appContainerDefinition = new ecs.ContainerDefinition(this, this.createResourceName('container'), {
image: ecs.ContainerImage.fromEcrRepository(appEcrRepository, 'latest'),
environment: { "Key": "Test" },
logging: ecs.LogDriver.awsLogs({
streamPrefix: this.createResourceName(''),
logGroup: appLogGroup
}),
taskDefinition: appTaskDefinition
})
appContainerDefinition.addPortMappings({
containerPort: 80,
hostPort: 80,
protocol: ecs.Protocol.TCP
})
// service
const appService = new ecs.FargateService(this, this.createResourceName('service'), {
cluster: appCluster,
assignPublicIp: false,
desiredCount: 1,
serviceName: this.createResourceName('service'),
taskDefinition: appTaskDefinition,
vpcSubnets: appVpc.selectSubnets(this.props.ecsSubnetSelection || { subnetType: ec2.SubnetType.PRIVATE }),
securityGroup: ecsSecurityGroup,
healthCheckGracePeriod: Duration.minutes(2),
deploymentController: { type: ecs.DeploymentControllerType.ECS }
})
albTargetGroup.addTarget(appService.loadBalancerTarget({
containerName: appContainerDefinition.containerName,
containerPort: appContainerDefinition.containerPort
}))
// auto scaling
const appAutoScaling = new applicationautoscaling.ScalableTarget(this, this.createResourceName('scalabletarget'), {
minCapacity: 1,
maxCapacity: 2,
resourceId: `service/${appCluster.clusterName}/${appService.serviceName}`,
role: appAutoscalingRole,
scalableDimension: "ecs:service:DesiredCount",
serviceNamespace: applicationautoscaling.ServiceNamespace.ECS
})
new applicationautoscaling.TargetTrackingScalingPolicy(this, this.createResourceName('policy'), {
policyName: this.createResourceName('policy'),
scalingTarget: appAutoScaling,
targetValue: 80,
predefinedMetric: applicationautoscaling.PredefinedMetric.ECS_SERVICE_AVERAGE_CPU_UTILIZATION,
disableScaleIn: false,
scaleInCooldown: Duration.minutes(5),
scaleOutCooldown: Duration.minutes(5),
})
// output設定
new cdk.CfnOutput(this, 'output', { exportName: 'endpointUrl', value: `http://${appAlb.loadBalancerDnsName}/` })
}
private createResourceName(suffix: string): string {
return `${this.props.projectName}-${this.props.profile}-${suffix}`
}
}
テスト
3種類のテストがあるが、いずれもあまりテストを書くほどの効果が見込めないのでテストは書かない
- snapshotテスト:cdkのバージョンアップに伴うテンプレートの出力内容の差分チェックができるのはよいが、そこまではしなくてもよいと判断
- full-grainedテスト:固定値を設定するところばかりのため、ユニットテストが有効な部分が見当たらなかった
- validationテスト:カスタムでバリデーション実装していないため意味がなさそう
デプロイ
事前準備
- プロファイルおよびインプットパラメータの設定を行っておくこと
- VPCにはPublic/Privateサブネットを用意しておくこと
- PrivateサブネットにはS3、ECR、CloudWatchLogsへのエンドポイント(PrivateLink)を設定しておくこと
- LogGroupを作成しておくこと
- 設定ファイルに指定したIAMロールを事前に作っておくこと
- 設定ファイルに指定したECRリポジトリを事前に作り、アプリケーションをpushしておくこと
環境構築
コードをCloneしてきて、ライブラリをインストール
yarn install
設定(プロファイル)
設定
env/xxx.json
という形式で管理し、cdkdeploy.tsの中で直接importする。 env/example.json
を書き換えても構わない
cloudformationのテンプレート出力
cdk synth --profile xxx
tsファイルのコンパイル(できてないと違うものがデプロイされるので要注意)
yarn build
実行
cdk deploy --profile xxx
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 │
├───┼────────────────────────────────────────────────────────────────────────┼────────┼─────────────────────────────────────────────────────────────────────────┼──────────────────────────┼───────────┤
│ + │ * │ Allow │ ecr:GetAuthorizationToken │ AWS:ecsTaskExecutionRole │ │
├───┼────────────────────────────────────────────────────────────────────────┼────────┼─────────────────────────────────────────────────────────────────────────┼──────────────────────────┼───────────┤
│ + │ arn:${AWS::Partition}:ecr:ap-northeast-1:xxxxxxxxxxxx:repository/webap │ Allow │ ecr:BatchCheckLayerAvailability │ AWS:ecsTaskExecutionRole │ │
│ │ p │ │ ecr:BatchGetImage │ │ │
│ │ │ │ ecr:GetDownloadUrlForLayer │ │ │
├───┼────────────────────────────────────────────────────────────────────────┼────────┼─────────────────────────────────────────────────────────────────────────┼──────────────────────────┼───────────┤
│ + │ arn:${AWS::Partition}:logs:ap-northeast-1:xxxxxxxxxxxx:log-group:farga │ Allow │ logs:CreateLogStream │ AWS:ecsTaskExecutionRole │ │
│ │ te-webapp-dev-log-group │ │ logs:PutLogEvents │ │ │
└───┴────────────────────────────────────────────────────────────────────────┴────────┴─────────────────────────────────────────────────────────────────────────┴──────────────────────────┴───────────┘
Security Group Changes
┌───┬──────────────────────────────────────┬─────┬────────────┬──────────────────────────────────────┐
│ │ Group │ Dir │ Protocol │ Peer │
├───┼──────────────────────────────────────┼─────┼────────────┼──────────────────────────────────────┤
│ + │ ${fargate-webapp-dev-alb-sg.GroupId} │ In │ TCP 80 │ Everyone (IPv4) │
│ + │ ${fargate-webapp-dev-alb-sg.GroupId} │ In │ TCP 443 │ Everyone (IPv4) │
│ + │ ${fargate-webapp-dev-alb-sg.GroupId} │ Out │ Everything │ Everyone (IPv4) │
├───┼──────────────────────────────────────┼─────┼────────────┼──────────────────────────────────────┤
│ + │ ${fargate-webapp-dev-ecs-sg.GroupId} │ In │ TCP 80 │ ${fargate-webapp-dev-alb-sg.GroupId} │
│ + │ ${fargate-webapp-dev-ecs-sg.GroupId} │ Out │ Everything │ Everyone (IPv4) │
└───┴──────────────────────────────────────┴─────┴────────────┴──────────────────────────────────────┘
(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
fargate-webapp-dev: deploying...
fargate-webapp-dev: creating CloudFormation changeset...
0/14 | 16:08:31 | CREATE_IN_PROGRESS | AWS::EC2::SecurityGroup | fargate-webapp-dev-alb-sg (fargatewebappdevalbsg)
0/14 | 16:08:31 | CREATE_IN_PROGRESS | AWS::IAM::Policy | task-exec-role/Policy (taskexecrolePolicy)
0/14 | 16:08:32 | CREATE_IN_PROGRESS | AWS::ElasticLoadBalancingV2::TargetGroup | fargate-webapp-dev-tg (fargatewebappdevtg)
0/14 | 16:08:32 | CREATE_IN_PROGRESS | AWS::CDK::Metadata | CDKMetadata
0/14 | 16:08:32 | CREATE_IN_PROGRESS | AWS::ECS::TaskDefinition | fargate-webapp-dev-task (fargatewebappdevtask)
0/14 | 16:08:32 | CREATE_IN_PROGRESS | AWS::ECS::Cluster | fargate-webapp-dev-cluster (fargatewebappdevcluster)
0/14 | 16:08:32 | CREATE_IN_PROGRESS | AWS::EC2::SecurityGroup | fargate-webapp-dev-ecs-sg (fargatewebappdevecssg)
0/14 | 16:08:32 | CREATE_IN_PROGRESS | AWS::ECS::TaskDefinition | fargate-webapp-dev-task (fargatewebappdevtask) Resource creation Initiated
0/14 | 16:08:32 | CREATE_IN_PROGRESS | AWS::ECS::Cluster | fargate-webapp-dev-cluster (fargatewebappdevcluster) Resource creation Initiated
1/14 | 16:08:32 | CREATE_COMPLETE | AWS::ECS::TaskDefinition | fargate-webapp-dev-task (fargatewebappdevtask)
1/14 | 16:08:33 | CREATE_IN_PROGRESS | AWS::ElasticLoadBalancingV2::TargetGroup | fargate-webapp-dev-tg (fargatewebappdevtg) Resource creation Initiated
2/14 | 16:08:33 | CREATE_COMPLETE | AWS::ElasticLoadBalancingV2::TargetGroup | fargate-webapp-dev-tg (fargatewebappdevtg)
3/14 | 16:08:33 | CREATE_COMPLETE | AWS::ECS::Cluster | fargate-webapp-dev-cluster (fargatewebappdevcluster)
3/14 | 16:08:33 | CREATE_IN_PROGRESS | AWS::IAM::Policy | task-exec-role/Policy (taskexecrolePolicy) Resource creation Initiated
3/14 | 16:08:34 | CREATE_IN_PROGRESS | AWS::CDK::Metadata | CDKMetadata Resource creation Initiated
4/14 | 16:08:34 | CREATE_COMPLETE | AWS::CDK::Metadata | CDKMetadata
4/14 | 16:08:37 | CREATE_IN_PROGRESS | AWS::EC2::SecurityGroup | fargate-webapp-dev-alb-sg (fargatewebappdevalbsg) Resource creation Initiated
4/14 | 16:08:37 | CREATE_IN_PROGRESS | AWS::EC2::SecurityGroup | fargate-webapp-dev-ecs-sg (fargatewebappdevecssg) Resource creation Initiated
5/14 | 16:08:39 | CREATE_COMPLETE | AWS::EC2::SecurityGroup | fargate-webapp-dev-alb-sg (fargatewebappdevalbsg)
6/14 | 16:08:39 | CREATE_COMPLETE | AWS::EC2::SecurityGroup | fargate-webapp-dev-ecs-sg (fargatewebappdevecssg)
6/14 | 16:08:41 | CREATE_IN_PROGRESS | AWS::EC2::SecurityGroupIngress | fargate-webapp-dev-ecs-sg/from fargatewebappdevfargatewebappdevalbsg:80 (fargatewebappdevecssgfromfargatewebappdevfargatewebappdevalbsg)
6/14 | 16:08:41 | CREATE_IN_PROGRESS | AWS::ElasticLoadBalancingV2::LoadBalancer | fargate-webapp-dev-alb (fargatewebappdevalb)
6/14 | 16:08:41 | CREATE_IN_PROGRESS | AWS::EC2::SecurityGroupIngress | fargate-webapp-dev-ecs-sg/from fargatewebappdevfargatewebappdevalbsg:80 (fargatewebappdevecssgfromfargatewebappdevfargatewebappdevalbsg) Resource creation Initiated
7/14 | 16:08:42 | CREATE_COMPLETE | AWS::EC2::SecurityGroupIngress | fargate-webapp-dev-ecs-sg/from fargatewebappdevfargatewebappdevalbsg:80 (fargatewebappdevecssgfromfargatewebappdevfargatewebappdevalbsg)
7/14 | 16:08:42 | CREATE_IN_PROGRESS | AWS::ElasticLoadBalancingV2::LoadBalancer | fargate-webapp-dev-alb (fargatewebappdevalb) Resource creation Initiated
8/14 | 16:08:50 | CREATE_COMPLETE | AWS::IAM::Policy | task-exec-role/Policy (taskexecrolePolicy)
8/14 Currently in progress: fargatewebappdevalb
9/14 | 16:10:44 | CREATE_COMPLETE | AWS::ElasticLoadBalancingV2::LoadBalancer | fargate-webapp-dev-alb (fargatewebappdevalb)
9/14 | 16:10:46 | CREATE_IN_PROGRESS | AWS::ElasticLoadBalancingV2::Listener | fargate-webapp-dev-alb/fargate-webapp-dev-alb-listener (fargatewebappdevalbfargatewebappdevalblistene)
9/14 | 16:10:47 | CREATE_IN_PROGRESS | AWS::ElasticLoadBalancingV2::Listener | fargate-webapp-dev-alb/fargate-webapp-dev-alb-listener (fargatewebappdevalbfargatewebappdevalblistene) Resource creation Initiated
10/14 | 16:10:47 | CREATE_COMPLETE | AWS::ElasticLoadBalancingV2::Listener | fargate-webapp-dev-alb/fargate-webapp-dev-alb-listener (fargatewebappdevalbfargatewebappdevalblistene)
10/14 | 16:11:01 | CREATE_IN_PROGRESS | AWS::ECS::Service | fargate-webapp-dev-service/Service (fargatewebappdevserviceService)
10/14 | 16:11:02 | CREATE_IN_PROGRESS | AWS::ECS::Service | fargate-webapp-dev-service/Service (fargatewebappdevserviceService) Resource creation Initiated
10/14 Currently in progress: fargatewebappdevserviceService
11/14 | 16:12:03 | CREATE_COMPLETE | AWS::ECS::Service | fargate-webapp-dev-service/Service (fargatewebappdevserviceService)
11/14 | 16:12:05 | CREATE_IN_PROGRESS | AWS::ApplicationAutoScaling::ScalableTarget | fargate-webapp-dev-scalabletarget (fargatewebappdevscalabletarget)
11/14 | 16:12:06 | CREATE_IN_PROGRESS | AWS::ApplicationAutoScaling::ScalableTarget | fargate-webapp-dev-scalabletarget (fargatewebappdevscalabletarget) Resource creation Initiated
12/14 | 16:12:06 | CREATE_COMPLETE | AWS::ApplicationAutoScaling::ScalableTarget | fargate-webapp-dev-scalabletarget (fargatewebappdevscalabletarget)
✅ fargate-webapp-dev
Outputs:
fargate-webapp-dev.output = http://fargate-webapp-dev-alb-xxxxxxx.ap-northeast-1.elb.amazonaws.com/
Stack ARN:
arn:aws:cloudformation:ap-northeast-1:xxxxxxxxxxxx:stack/fargate-webapp-dev/xxxxx
環境削除
cdk destroy --profile xxx
感想
- 環境依存をどう外出しにするか、StackのParameter部分をどう表現するかを悩みましたが、いい感じに表現できたと思います。
- CDKはリソース間の関連がコードで表現されるので
Ref
を書くよりかなり楽でした。リソースの命名も楽になりました。 - Typescriptの型定義ファイルを見ることでできることがわかるのでCloudformationよりは苦労しなかったです
- 残念ながら現時点(2020/01/18)ではCloudformationでFargateのBlueGreenデプロイメントが対応していないようなので、別途記事にまとめるつもりです。https://github.com/aws/containers-roadmap/issues/130
CDKいいですね。