Help us understand the problem. What is going on with this article?

AWS CDKでのFargateデプロイ

前回の記事「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)はできないようでした。

bin/cdkdeploy.ts
#!/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の設定が必要となっています。

lib/alb-fargate-stack.ts
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

設定(プロファイル)

https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-configure-profiles.html

設定

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いいですね。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした