AWS CDK v2を使ってさくっとALB×ECS(Fargate)でWebサーバを立ててみた。

初投稿です。お手柔らかにお願いします。
本日はAWSのインフラをコードで管理することでお馴染みの**AWS CDK**（AWS Cloud Development Kit）を利用して、さくっとALB×ECS(Fargate)でWebサーバを構築してみたいと思います。

ちなみに自分はCloudFormationはある程度習得済みで、CDK（v1）は少し齧った程度でほぼ忘れています。

# 実行環境
今回は、**Cloud9**（AWSのIDEサービス）の力をお借りします。
* OS : Amazon Linux2 (AL2)
* CDKで利用する開発言語：Python

CDKを利用する場合は、以下の環境を準備しておく必要がありますが、Cloud9(AL2)はすでにすべて揃っている状態で利用できます。
環境を整えるのって地味に面倒なので、大変ありがたいです。
* AWS CLI
* AWS Account and User
* Node.js
* IDE for your programming language
* AWS CDK Toolkit
* Python（開発言語）

※2022/03/19に起動したCloud9(AL2)のソフトウェアバージョンは以下の通り。
```
$ aws --version
aws-cli/1.19.112 Python/2.7.18 Linux/4.14.268-205.500.amzn2.x86_64 botocore/1.20.112
$ node -v
v16.14.0
$ cdk --version
2.16.0 (build 4c77925)
$ python3 --version
Python 3.7.10
```

# とりあえずやってみる
## 1. プロジェクト作成
まずは、AWS CDKを利用してPython向けのプロジェクトを作成します。
```bash
$ mkdir cdk_sample $$ cd cdk_sample
$ cdk init --language python

...
Please run 'python3 -m venv .venv'!
Executing Creating virtualenv...
✅ All done!
```
成功すると`All done!`と表示されました。
そして作成されたプロジェクトフォルダ内のファイル構成はこんな感じです。
```bash
$ tree ./
./
├── app.py
├── cdk.json
├── cdk_sample
│   ├── cdk_sample_stack.py
│   └── __init__.py
├── README.md
├── requirements-dev.txt
├── requirements.txt
├── source.bat
└── tests
    ├── __init__.py
    └── unit
        ├── __init__.py
        └── test_cdk_sample_stack.py

3 directories, 11 files
```
プロジェクト内にはファイルが色々詰まっておりますが、今回編集するファイルは以下のファイルになりますので、それ以外の説明は今回は割愛します。（編集内容は以降の[4. ソースコード編集]参照）
* **cdk_sample/cdk_sample_stack.py**
    * メインで編集するクラスファイル。基本的にクラスの単位がCloudFormationのスタックになるはず。
    * ファイル名などは適宜変更できますが今回はこのまま利用します。
    * ここに利用するAWSリソースのコーディングしていきます。
* **app.py**
    * 上記クラスのインスタンス化を定義するクラスファイル。
    * ここではデプロイ先のリージョン・アカウント指定やリソースタグ付け（一括）などを設定できます。

## 2. 仮想環境アクティブ化
既存環境または今後新規で建てる環境と干渉しないよう、本プロジェクト向けの仮想環境を起動し、その中で作業します。
```
$ source .venv/bin/activate
```
## 3. 必要モジュールのインストール
本開発に必要なAWS CDKのPythonモジュール群をインストールします。
必要なモジュールは`requirements.txt`にデフォルトで記載あるのでそれを叩けばよいです。
また、今回はECSを構築するので、ECS関連のモジュールもインストールします。
AWS CDKのモジュールは[こちらのリファレンス](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-construct-library.html)から確認できます。
```
$ python -m pip install -r requirements.txt
$ python -m pip install aws-cdk.aws-ecs-patterns
```
なお、今回は`aws-ecs`ではなく、`aws-ecs-patterns`を利用します。
`aws-ecs-patterns`は、ALBやNLBなどのECSと一緒に利用する可能性の高いAWSサービスの構築が盛り込まれているライブラリなので、コード数を抑えて手軽に構築ができそうです。

## 4. ソースコード編集
それではさっそくコードを書いていきます。
今回は、`ALB×ECS(Fargate)`構成を構築したいので、`aws-ecs-patterns`で用意されているコンストラクタのうち`ApplicationLoadBalancedFargateService`を利用します。（NLB×ECSやECSのスケジュール実行などのコンストラクタもありますので、気になる方は[こちら](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns-readme.html)をご確認ください）
そして実際に書いたソースコードはこちら。

* cdk_sample_stack.py
```bash
from aws_cdk import (
    Stack,
    aws_ecs as ecs,
    aws_ecs_patterns as ecs_patterns,
)
from constructs import Construct

class ECSWebSampleStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        ecs_patterns.ApplicationLoadBalancedFargateService(self, "Nginx",
            task_image_options=ecs_patterns.ApplicationLoadBalancedTaskImageOptions(
                image=ecs.ContainerImage.from_registry("public.ecr.aws/nginx/nginx:latest")),
            public_load_balancer=True
        )        
```
はい、たったの18行です。
本来であればCPU・メモリのリソースサイズやECSサービス内に起動するタスク数などの細かいパラメータを指定すると思いますが、デフォルトだとどういうものになるのかも気になるのであえてこれだけです。（設定できるパラメータは[APIリファレンス](https://docs.aws.amazon.com/cdk/api/v1/python/aws_cdk.aws_ecs_patterns/ApplicationLoadBalancedFargateService.html)参照）
また、今回ECSで利用するコンテナイメージですが、今回は気分的に[AWSで公開しているコンテナリポジトリ](https://gallery.ecr.aws/?page=1)からNginxを利用させてもらいます。

* app.py
```bash
#!/usr/bin/env python3
import aws_cdk as cdk

from cdk_sample.cdk_sample_stack import ECSWebSampleStack

app = cdk.App()
ecs_web_sample_stack = ECSWebSampleStack(app, "ECSWebSampleStack")
cdk.Tags.of(ecs_web_sample_stack).add("User", "matsumikan")

app.synth()
```
上記ソースはデフォルトでも利用できますがですが、今回クラス名を変更した（ECSWebSampleStack）のと、タグ付け設定も追加したかったので、その部分だけ修正しています。

## 5. 環境をデプロイ
準備ができたのでさっそくデプロイ…と言いたいところですが、AWS CDKを初めて利用する場合は以下のコマンドを実行する必要がありますので、とりあえず実行します。
```bash
$ cdk bootstrap
...

 ✅  Environment aws://{AWSAccountId}/{Region} bootstrapped.
```
本コマンドはCDKではソースをS3に配置してりようするなどの動作が含まれているので、CDKで利用するリソース群を設定してくれるおまじないです。難しいことは考えずにやってしまいます。

そして念願のデプロイ実行！
```bash
$ cdk deploy
```

実際に上記コマンドを実行すると、以下画像のように、事前に構築する際に確認などがあり、構築中や構築完了のステータスを確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2601200/63a94408-f34e-bec8-c966-6409fc0a8ee5.png)

今回は構築完了とともにALBのURLも出力してくれているので、そのURLを叩いてみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2601200/6c4f6a27-3f85-bf43-fd07-ad53abde1b96.png)

Nginxの画面が出力されています！成功です！:clap:

CloudFormation側を確認してみると、今回構築したスタック`ECSWebSampleStack
`で計38個のリソースが無事に構築されていることが確認できました。
意外と多いように感じますが、ALB、ECSを構築するために必要なあれやこれ（VPCやセキュリティグループなどなど）が詰まっているのだと思います。
（ちなみに左側のスタックのリストに`CDKToolkit`が表示されていると思いますが、こちらは先ほど`cdk bootstrap`した際にできたスタックです）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2601200/51349b99-b02e-fe3d-d382-72eb6c46d6a0.png)

そして地味に仕込んだタグ付けもうまくいっているようでした。（以下はAWS Resource Groups画面からタグ検索してみた結果です。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2601200/0d33aef1-7f9c-a47f-fa97-4dce53dd9c4e.png)

## 6. 環境を削除
とりあえず目的は達成できたので、環境を消してしまいます。
CDKには環境を削除するコマンドも用意されているので、それを叩けば終了です。
※`cdk bootstrap`で作成された`CDKToolkit`スタックは削除されないので、すべて消したい人は手動で削除する必要があります。
```
$ cdk destroy
```
しばらく待つと、無事に削除できたことを教えてくれます。
AWS CLIのスタック削除コマンドは非同期（削除実行した結果までは教えてくれない）なので、いちいちマネージメントコンソールにアクセスする必要があったのですが、CDKのコマンドは親切でいいですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2601200/0aaa1c76-4f7d-feb6-b845-e03cbc45bd9f.png)

## X. 蛇足
`cdk deploy`コマンドを実行すると環境が構築されますが、その前にどんな環境が作られるか確認したい！ということもあるかと思います。
そんな時には`cdk synth`というコマンドを利用するのもありのようです。
上記コマンドを実行すると、実際にデプロイされるCloudFormationスタックのテンプレートが出力されますので、その内容をみて具体的な構築内容を確認することができます。
（CloudFormationを通った人じゃないと確認しづらいかもしれませんが…）

ちなみに今回試した結果、700行弱のYAMLが出力されました。

<details><summary>cdk synthの結果（CloudFormation Tamplate YAML）</summary>

```yaml
Resources:
  NginxLB9C42F6B2:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      LoadBalancerAttributes:
        - Key: deletion_protection.enabled
          Value: "false"
      Scheme: internet-facing
      SecurityGroups:
        - Fn::GetAtt:
            - NginxLBSecurityGroupE007CBA8
            - GroupId
      Subnets:
        - Ref: EcsDefaultClusterMnL3mNNYNVpcPublicSubnet1Subnet3C273B99
        - Ref: EcsDefaultClusterMnL3mNNYNVpcPublicSubnet2Subnet95FF715A
      Tags:
        - Key: User
          Value: matsumikan
      Type: application
    DependsOn:
      - EcsDefaultClusterMnL3mNNYNVpcPublicSubnet1DefaultRouteFF4E2178
      - EcsDefaultClusterMnL3mNNYNVpcPublicSubnet2DefaultRouteB1375520
    Metadata:
      aws:cdk:path: ECSWebSampleStack/Nginx/LB/Resource
  NginxLBSecurityGroupE007CBA8:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Automatically created Security Group for ELB ECSWebSampleStackNginxLBB7FA19C6
      SecurityGroupIngress:
        - CidrIp: 0.0.0.0/0
          Description: Allow from anyone on port 80
          FromPort: 80
          IpProtocol: tcp
          ToPort: 80
      Tags:
        - Key: User
          Value: matsumikan
      VpcId:
        Ref: EcsDefaultClusterMnL3mNNYNVpc7788A521
    Metadata:
      aws:cdk:path: ECSWebSampleStack/Nginx/LB/SecurityGroup/Resource
  NginxLBSecurityGrouptoECSWebSampleStackNginxServiceSecurityGroup1FFA79D6808317A837:
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      GroupId:
        Fn::GetAtt:
          - NginxLBSecurityGroupE007CBA8
          - GroupId
      IpProtocol: tcp
      Description: Load balancer to target
      DestinationSecurityGroupId:
        Fn::GetAtt:
          - NginxServiceSecurityGroup8D4F97B3
          - GroupId
      FromPort: 80
      ToPort: 80
    Metadata:
      aws:cdk:path: ECSWebSampleStack/Nginx/LB/SecurityGroup/to ECSWebSampleStackNginxServiceSecurityGroup1FFA79D6:80
  NginxLBPublicListener6F3FBDE4:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - TargetGroupArn:
            Ref: NginxLBPublicListenerECSGroup8547737E
          Type: forward
      LoadBalancerArn:
        Ref: NginxLB9C42F6B2
      Port: 80
      Protocol: HTTP
    Metadata:
      aws:cdk:path: ECSWebSampleStack/Nginx/LB/PublicListener/Resource
  NginxLBPublicListenerECSGroup8547737E:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Port: 80
      Protocol: HTTP
      Tags:
        - Key: User
          Value: matsumikan
      TargetGroupAttributes:
        - Key: stickiness.enabled
          Value: "false"
      TargetType: ip
      VpcId:
        Ref: EcsDefaultClusterMnL3mNNYNVpc7788A521
    Metadata:
      aws:cdk:path: ECSWebSampleStack/Nginx/LB/PublicListener/ECSGroup/Resource
  NginxTaskDefTaskRoleE7F42228:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
        Version: "2012-10-17"
      Tags:
        - Key: User
          Value: matsumikan
    Metadata:
      aws:cdk:path: ECSWebSampleStack/Nginx/TaskDef/TaskRole/Resource
  NginxTaskDef7E40B47A:
    Type: AWS::ECS::TaskDefinition
    Properties:
      ContainerDefinitions:
        - Essential: true
          Image: public.ecr.aws/nginx/nginx:latest
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group:
                Ref: NginxTaskDefwebLogGroup726FAFBB
              awslogs-stream-prefix: Nginx
              awslogs-region:
                Ref: AWS::Region
          Name: web
          PortMappings:
            - ContainerPort: 80
              Protocol: tcp
      Cpu: "256"
      ExecutionRoleArn:
        Fn::GetAtt:
          - NginxTaskDefExecutionRoleB15C3043
          - Arn
      Family: ECSWebSampleStackNginxTaskDefDECA1844
      Memory: "512"
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      Tags:
        - Key: User
          Value: matsumikan
      TaskRoleArn:
        Fn::GetAtt:
          - NginxTaskDefTaskRoleE7F42228
          - Arn
    Metadata:
      aws:cdk:path: ECSWebSampleStack/Nginx/TaskDef/Resource
  NginxTaskDefwebLogGroup726FAFBB:
    Type: AWS::Logs::LogGroup
    Properties:
      Tags:
        - Key: User
          Value: matsumikan
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
    Metadata:
      aws:cdk:path: ECSWebSampleStack/Nginx/TaskDef/web/LogGroup/Resource
  NginxTaskDefExecutionRoleB15C3043:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
        Version: "2012-10-17"
      Tags:
        - Key: User
          Value: matsumikan
    Metadata:
      aws:cdk:path: ECSWebSampleStack/Nginx/TaskDef/ExecutionRole/Resource
  NginxTaskDefExecutionRoleDefaultPolicy1DE03C9C:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Action:
              - logs:CreateLogStream
              - logs:PutLogEvents
            Effect: Allow
            Resource:
              Fn::GetAtt:
                - NginxTaskDefwebLogGroup726FAFBB
                - Arn
        Version: "2012-10-17"
      PolicyName: NginxTaskDefExecutionRoleDefaultPolicy1DE03C9C
      Roles:
        - Ref: NginxTaskDefExecutionRoleB15C3043
    Metadata:
      aws:cdk:path: ECSWebSampleStack/Nginx/TaskDef/ExecutionRole/DefaultPolicy/Resource
  NginxServiceB96E6F5B:
    Type: AWS::ECS::Service
    Properties:
      Cluster:
        Ref: EcsDefaultClusterMnL3mNNYN926A5246
      DeploymentConfiguration:
        MaximumPercent: 200
        MinimumHealthyPercent: 50
      EnableECSManagedTags: false
      HealthCheckGracePeriodSeconds: 60
      LaunchType: FARGATE
      LoadBalancers:
        - ContainerName: web
          ContainerPort: 80
          TargetGroupArn:
            Ref: NginxLBPublicListenerECSGroup8547737E
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: DISABLED
          SecurityGroups:
            - Fn::GetAtt:
                - NginxServiceSecurityGroup8D4F97B3
                - GroupId
          Subnets:
            - Ref: EcsDefaultClusterMnL3mNNYNVpcPrivateSubnet1Subnet075EFF4C
            - Ref: EcsDefaultClusterMnL3mNNYNVpcPrivateSubnet2SubnetE4CEDF73
      Tags:
        - Key: User
          Value: matsumikan
      TaskDefinition:
        Ref: NginxTaskDef7E40B47A
    DependsOn:
      - NginxLBPublicListenerECSGroup8547737E
      - NginxLBPublicListener6F3FBDE4
    Metadata:
      aws:cdk:path: ECSWebSampleStack/Nginx/Service/Service
  NginxServiceSecurityGroup8D4F97B3:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: ECSWebSampleStack/Nginx/Service/SecurityGroup
      SecurityGroupEgress:
        - CidrIp: 0.0.0.0/0
          Description: Allow all outbound traffic by default
          IpProtocol: "-1"
      Tags:
        - Key: User
          Value: matsumikan
      VpcId:
        Ref: EcsDefaultClusterMnL3mNNYNVpc7788A521
    Metadata:
      aws:cdk:path: ECSWebSampleStack/Nginx/Service/SecurityGroup/Resource
  NginxServiceSecurityGroupfromECSWebSampleStackNginxLBSecurityGroup10EE3EE980B9A04806:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      IpProtocol: tcp
      Description: Load balancer to target
      FromPort: 80
      GroupId:
        Fn::GetAtt:
          - NginxServiceSecurityGroup8D4F97B3
          - GroupId
      SourceSecurityGroupId:
        Fn::GetAtt:
          - NginxLBSecurityGroupE007CBA8
          - GroupId
      ToPort: 80
    Metadata:
      aws:cdk:path: ECSWebSampleStack/Nginx/Service/SecurityGroup/from ECSWebSampleStackNginxLBSecurityGroup10EE3EE9:80
  EcsDefaultClusterMnL3mNNYN926A5246:
    Type: AWS::ECS::Cluster
    Properties:
      Tags:
        - Key: User
          Value: matsumikan
    Metadata:
      aws:cdk:path: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Resource
  EcsDefaultClusterMnL3mNNYNVpc7788A521:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsHostnames: true
      EnableDnsSupport: true
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc
        - Key: User
          Value: matsumikan
    Metadata:
      aws:cdk:path: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/Resource
  EcsDefaultClusterMnL3mNNYNVpcPublicSubnet1Subnet3C273B99:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.0.0/18
      VpcId:
        Ref: EcsDefaultClusterMnL3mNNYNVpc7788A521
      AvailabilityZone:
        Fn::Select:
          - 0
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: true
      Tags:
        - Key: aws-cdk:subnet-name
          Value: Public
        - Key: aws-cdk:subnet-type
          Value: Public
        - Key: Name
          Value: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PublicSubnet1
        - Key: User
          Value: matsumikan
    Metadata:
      aws:cdk:path: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PublicSubnet1/Subnet
  EcsDefaultClusterMnL3mNNYNVpcPublicSubnet1RouteTableA1FD6ACC:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: EcsDefaultClusterMnL3mNNYNVpc7788A521
      Tags:
        - Key: Name
          Value: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PublicSubnet1
        - Key: User
          Value: matsumikan
    Metadata:
      aws:cdk:path: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PublicSubnet1/RouteTable
  EcsDefaultClusterMnL3mNNYNVpcPublicSubnet1RouteTableAssociation8B583A17:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: EcsDefaultClusterMnL3mNNYNVpcPublicSubnet1RouteTableA1FD6ACC
      SubnetId:
        Ref: EcsDefaultClusterMnL3mNNYNVpcPublicSubnet1Subnet3C273B99
    Metadata:
      aws:cdk:path: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PublicSubnet1/RouteTableAssociation
  EcsDefaultClusterMnL3mNNYNVpcPublicSubnet1DefaultRouteFF4E2178:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId:
        Ref: EcsDefaultClusterMnL3mNNYNVpcPublicSubnet1RouteTableA1FD6ACC
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: EcsDefaultClusterMnL3mNNYNVpcIGW9C2C2B8F
    DependsOn:
      - EcsDefaultClusterMnL3mNNYNVpcVPCGW2447264E
    Metadata:
      aws:cdk:path: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PublicSubnet1/DefaultRoute
  EcsDefaultClusterMnL3mNNYNVpcPublicSubnet1EIP8704DB2F:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: Name
          Value: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PublicSubnet1
        - Key: User
          Value: matsumikan
    Metadata:
      aws:cdk:path: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PublicSubnet1/EIP
  EcsDefaultClusterMnL3mNNYNVpcPublicSubnet1NATGateway5E3732C1:
    Type: AWS::EC2::NatGateway
    Properties:
      SubnetId:
        Ref: EcsDefaultClusterMnL3mNNYNVpcPublicSubnet1Subnet3C273B99
      AllocationId:
        Fn::GetAtt:
          - EcsDefaultClusterMnL3mNNYNVpcPublicSubnet1EIP8704DB2F
          - AllocationId
      Tags:
        - Key: Name
          Value: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PublicSubnet1
        - Key: User
          Value: matsumikan
    Metadata:
      aws:cdk:path: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PublicSubnet1/NATGateway
  EcsDefaultClusterMnL3mNNYNVpcPublicSubnet2Subnet95FF715A:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.64.0/18
      VpcId:
        Ref: EcsDefaultClusterMnL3mNNYNVpc7788A521
      AvailabilityZone:
        Fn::Select:
          - 1
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: true
      Tags:
        - Key: aws-cdk:subnet-name
          Value: Public
        - Key: aws-cdk:subnet-type
          Value: Public
        - Key: Name
          Value: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PublicSubnet2
        - Key: User
          Value: matsumikan
    Metadata:
      aws:cdk:path: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PublicSubnet2/Subnet
  EcsDefaultClusterMnL3mNNYNVpcPublicSubnet2RouteTable263DEAA5:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: EcsDefaultClusterMnL3mNNYNVpc7788A521
      Tags:
        - Key: Name
          Value: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PublicSubnet2
        - Key: User
          Value: matsumikan
    Metadata:
      aws:cdk:path: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PublicSubnet2/RouteTable
  EcsDefaultClusterMnL3mNNYNVpcPublicSubnet2RouteTableAssociation43E5803C:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: EcsDefaultClusterMnL3mNNYNVpcPublicSubnet2RouteTable263DEAA5
      SubnetId:
        Ref: EcsDefaultClusterMnL3mNNYNVpcPublicSubnet2Subnet95FF715A
    Metadata:
      aws:cdk:path: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PublicSubnet2/RouteTableAssociation
  EcsDefaultClusterMnL3mNNYNVpcPublicSubnet2DefaultRouteB1375520:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId:
        Ref: EcsDefaultClusterMnL3mNNYNVpcPublicSubnet2RouteTable263DEAA5
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: EcsDefaultClusterMnL3mNNYNVpcIGW9C2C2B8F
    DependsOn:
      - EcsDefaultClusterMnL3mNNYNVpcVPCGW2447264E
    Metadata:
      aws:cdk:path: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PublicSubnet2/DefaultRoute
  EcsDefaultClusterMnL3mNNYNVpcPublicSubnet2EIPF0764873:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: Name
          Value: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PublicSubnet2
        - Key: User
          Value: matsumikan
    Metadata:
      aws:cdk:path: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PublicSubnet2/EIP
  EcsDefaultClusterMnL3mNNYNVpcPublicSubnet2NATGateway4C855E00:
    Type: AWS::EC2::NatGateway
    Properties:
      SubnetId:
        Ref: EcsDefaultClusterMnL3mNNYNVpcPublicSubnet2Subnet95FF715A
      AllocationId:
        Fn::GetAtt:
          - EcsDefaultClusterMnL3mNNYNVpcPublicSubnet2EIPF0764873
          - AllocationId
      Tags:
        - Key: Name
          Value: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PublicSubnet2
        - Key: User
          Value: matsumikan
    Metadata:
      aws:cdk:path: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PublicSubnet2/NATGateway
  EcsDefaultClusterMnL3mNNYNVpcPrivateSubnet1Subnet075EFF4C:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.128.0/18
      VpcId:
        Ref: EcsDefaultClusterMnL3mNNYNVpc7788A521
      AvailabilityZone:
        Fn::Select:
          - 0
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: false
      Tags:
        - Key: aws-cdk:subnet-name
          Value: Private
        - Key: aws-cdk:subnet-type
          Value: Private
        - Key: Name
          Value: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PrivateSubnet1
        - Key: User
          Value: matsumikan
    Metadata:
      aws:cdk:path: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PrivateSubnet1/Subnet
  EcsDefaultClusterMnL3mNNYNVpcPrivateSubnet1RouteTable4F1D2E36:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: EcsDefaultClusterMnL3mNNYNVpc7788A521
      Tags:
        - Key: Name
          Value: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PrivateSubnet1
        - Key: User
          Value: matsumikan
    Metadata:
      aws:cdk:path: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PrivateSubnet1/RouteTable
  EcsDefaultClusterMnL3mNNYNVpcPrivateSubnet1RouteTableAssociation34B92275:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: EcsDefaultClusterMnL3mNNYNVpcPrivateSubnet1RouteTable4F1D2E36
      SubnetId:
        Ref: EcsDefaultClusterMnL3mNNYNVpcPrivateSubnet1Subnet075EFF4C
    Metadata:
      aws:cdk:path: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PrivateSubnet1/RouteTableAssociation
  EcsDefaultClusterMnL3mNNYNVpcPrivateSubnet1DefaultRouteA5ADF694:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId:
        Ref: EcsDefaultClusterMnL3mNNYNVpcPrivateSubnet1RouteTable4F1D2E36
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId:
        Ref: EcsDefaultClusterMnL3mNNYNVpcPublicSubnet1NATGateway5E3732C1
    Metadata:
      aws:cdk:path: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PrivateSubnet1/DefaultRoute
  EcsDefaultClusterMnL3mNNYNVpcPrivateSubnet2SubnetE4CEDF73:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.192.0/18
      VpcId:
        Ref: EcsDefaultClusterMnL3mNNYNVpc7788A521
      AvailabilityZone:
        Fn::Select:
          - 1
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: false
      Tags:
        - Key: aws-cdk:subnet-name
          Value: Private
        - Key: aws-cdk:subnet-type
          Value: Private
        - Key: Name
          Value: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PrivateSubnet2
        - Key: User
          Value: matsumikan
    Metadata:
      aws:cdk:path: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PrivateSubnet2/Subnet
  EcsDefaultClusterMnL3mNNYNVpcPrivateSubnet2RouteTableDCE46591:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: EcsDefaultClusterMnL3mNNYNVpc7788A521
      Tags:
        - Key: Name
          Value: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PrivateSubnet2
        - Key: User
          Value: matsumikan
    Metadata:
      aws:cdk:path: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PrivateSubnet2/RouteTable
  EcsDefaultClusterMnL3mNNYNVpcPrivateSubnet2RouteTableAssociation111C622F:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: EcsDefaultClusterMnL3mNNYNVpcPrivateSubnet2RouteTableDCE46591
      SubnetId:
        Ref: EcsDefaultClusterMnL3mNNYNVpcPrivateSubnet2SubnetE4CEDF73
    Metadata:
      aws:cdk:path: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PrivateSubnet2/RouteTableAssociation
  EcsDefaultClusterMnL3mNNYNVpcPrivateSubnet2DefaultRoute20CE2D89:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId:
        Ref: EcsDefaultClusterMnL3mNNYNVpcPrivateSubnet2RouteTableDCE46591
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId:
        Ref: EcsDefaultClusterMnL3mNNYNVpcPublicSubnet2NATGateway4C855E00
    Metadata:
      aws:cdk:path: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/PrivateSubnet2/DefaultRoute
  EcsDefaultClusterMnL3mNNYNVpcIGW9C2C2B8F:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc
        - Key: User
          Value: matsumikan
    Metadata:
      aws:cdk:path: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/IGW
  EcsDefaultClusterMnL3mNNYNVpcVPCGW2447264E:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId:
        Ref: EcsDefaultClusterMnL3mNNYNVpc7788A521
      InternetGatewayId:
        Ref: EcsDefaultClusterMnL3mNNYNVpcIGW9C2C2B8F
    Metadata:
      aws:cdk:path: ECSWebSampleStack/EcsDefaultClusterMnL3mNNYN/Vpc/VPCGW
  CDKMetadata:
    Type: AWS::CDK::Metadata
    Properties:
      Analytics: v2:deflate64:H4sIAAAAAAAA/31RQW7CMBB8C3fjUiq1Z0opQkJtFBBX5DhL2BLsyF6DUJS/105CSGnVk2fHk+x4Zswfn/loIM52KNPDMMeElysS8sA8tS1B2m0hiMAoyydFkaMUhFottUhfRS6UhPRdmEwQrMCcUAKDXFhCmXtFUitQZacxL//+2rDp7ufc16ElUK3minv3a78ZaG60K4KkN1YMpN+5AukM0qWT/E/MMgPW/qIXquE3hQx3m2jKIpd4EyuXKKBa36FYO4K1SHK48TduYq2WWJvvxAHMFlE4PgTNfZRncWGRwVNItfvxQoUWoBM0TtppQr6z/REUhZdbXralrIU9vMEOFV5X3jNakUCfa4+7K7SOo4W58y3UfbSwYiiOvIx18976jLTPpjbYoIrlOvOeljrrcr/iqgrTp6PCEYvBamealX3sTaa1tYpFF9pr9fDEX/jjaPBlEYfGKcIj8Lg5vwEs3r5V0gIAAA==
    Metadata:
      aws:cdk:path: ECSWebSampleStack/CDKMetadata/Default
    Condition: CDKMetadataAvailable
Outputs:
  NginxLoadBalancerDNSC10D9E34:
    Value:
      Fn::GetAtt:
        - NginxLB9C42F6B2
        - DNSName
  NginxServiceURLDB0E8C18:
    Value:
      Fn::Join:
        - ""
        - - http://
          - Fn::GetAtt:
              - NginxLB9C42F6B2
              - DNSName
Conditions:
  CDKMetadataAvailable:
    Fn::Or:
      - Fn::Or:
          - Fn::Equals:
              - Ref: AWS::Region
              - af-south-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-east-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-northeast-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-northeast-2
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-south-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-southeast-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-southeast-2
          - Fn::Equals:
              - Ref: AWS::Region
              - ca-central-1
          - Fn::Equals:
              - Ref: AWS::Region
              - cn-north-1
          - Fn::Equals:
              - Ref: AWS::Region
              - cn-northwest-1
      - Fn::Or:
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-central-1
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-north-1
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-south-1
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-west-1
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-west-2
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-west-3
          - Fn::Equals:
              - Ref: AWS::Region
              - me-south-1
          - Fn::Equals:
              - Ref: AWS::Region
              - sa-east-1
          - Fn::Equals:
              - Ref: AWS::Region
              - us-east-1
          - Fn::Equals:
              - Ref: AWS::Region
              - us-east-2
      - Fn::Or:
          - Fn::Equals:
              - Ref: AWS::Region
              - us-west-1
          - Fn::Equals:
              - Ref: AWS::Region
              - us-west-2
Parameters:
  BootstrapVersion:
    Type: AWS::SSM::Parameter::Value<String>
    Default: /cdk-bootstrap/hnb659fds/version
    Description: Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]
Rules:
  CheckBootstrapVersion:
    Assertions:
      - Assert:
          Fn::Not:
            - Fn::Contains:
                - - "1"
                  - "2"
                  - "3"
                  - "4"
                  - "5"
                - Ref: BootstrapVersion
        AssertDescription: CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.
```
</details>
さきほども少し触れましたが、ALBやECSはVPC内に構築されるサービスなので、本来であればVPC、セキュリティグループなどの様々なリソースを事前に用意しておく必要がありますが、今回はそれらの構築もいい感じにやってくれるライブラリを使っているので、CDKのソースはたったの18行で済んでいるというわけです。

# 感想
公式のドキュメントやワークショップなどが潤沢なおかげで、そこまで迷うこともなく本当にさくっとALB×ECS(Fargate)を構築することができました。
ただ、手軽に構築できて嬉しい反面、AWS CDKの実用化に対する理解はあまり深まらなかったというのが現状です。
たとえば、以下のようなことはまだ試せていません。
* 他スタックと組み合わせて利用する場合はどうする？
    * 例えばVPCなどを事前に作った場合に、CloudFormationだと`Outputs`でエクスポートした値を`Fn::ImportValue`でインポートして利用する…というようなことをしていましたが、CDKだとどうするのがよいのでしょう。
* AWS CDKとCI/CDを組み合わせたいデプロイはどうする？
    * `CodeBuild`上で`cdk deploy`コマンドを実行し、環境をデプロイする？それとも、`cdk synth`でテンプレートを出力して`CodeDeploy`で`CloudFormation`を実行するようにする？

自分はCloudFormationに慣れてしまっているのですが、CDKの方がコード数も少なく汎用性の高いものを作れそうなので、引き続きCDKの検証を実施して、実用化を目指したいなと思います。

それでは、またお会いしましょう。

# 参考URL

https://cdkworkshop.com/15-prerequisites.html

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/tutorial-ecs-web-server-cdk.html

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-construct-library.html

https://docs.aws.amazon.com/cdk/v2/guide/tagging.html