AWS CDKでのFargateデプロイ

前回の記事「[Cloudformationを使ったFargateサービスのデプロイプロセスについて考えてみた](https://qiita.com/nyasba/items/26ebcba9858d4f722aba)」のCDK版です。

CDKを実際に使ってみたかったので、比較も兼ねて前回のCloudformationファイルをCDKに置き換えてみました。
（一部設定を変えている部分がありますので全て完全に一致しているわけではないです）

前回記事の「[4.Fargateへのデプロイ](https://qiita.com/nyasba/items/26ebcba9858d4f722aba#4-fargate%E3%81%AE%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4)」の部分の代替手段となる方法ですので前回記事とあわせてご確認ください。

コードはこちら。
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いいですね。