🔧

AWS CodeシリーズとECSを使ったCI/CD環境構築

2024/03/01に公開

はじめに

概要

AWS Code シリーズを使った CI/CD 環境の記事をあまり見かけないかも...という単純な理由から今回記事を書くことにしました。
デプロイ先は構成的にありがちな Amazon ECS をターゲットにしています。
今回の構成を利用するケースとしては下記を想定しています。

  • 環境別(本番/開発など)へのデプロイを考慮した AWS Code シリーズ実装方式
  • Amazon ECS へのアプリケーション開発における CI/CD 自動化(一部承認などは手動)

対象読者

今回の記事では、利用するサービスの詳細まで記載していません。
そのため下記サービスの概要レベルの知識を有していることを前提としています。

  • AWS Code シリーズのサービス概要/機能概要
  • ECS のサービス概要/機能概要 + ECR との連携
  • IAM のサービス概要/機能概要
  • ネットワークに関する一般的な知識

免責事項

本記事に掲載されている情報は筆者の個人的見解に基づくものであり、品質を保証するものではありません。皆様は自己責任での利用をお願いします。
また、本記事の情報に関連して発生した損失や被害については筆者は一切の責任を負いません。

全体概要図

いきなりですが、全体構成図はこんな感じです。
Alternate textAWS Code シリーズを用いた CI/CD 全体概要構成図

構成図について

  • 環境は、開発環境と本番環境の 2 面を想定しています。
    • 環境に合わせて 2 つのパイプラインを構築しています。
    • 本番環境へのデプロイの前には手動承認を入れています。
  • 今回の環境ではアプリケーションのデプロイ先は ECS on fargate を使用します。
  • アプリケーションの開発には Cloud9 を使用することを前提としています。(必須ではありません。)
  • 開発したアプリケーションは ECR に Push してイメージを管理しています。

主に利用するサービスの概要説明

サービス名 概要説明 今回の利用用途/役割
CodeCommit プライベート Git リポジトリを安全にホストし、コードで共同作業する コード管理
CodeBuild 自動スケーリングによるコードのビルド コードのビルド、コンテナイメージの作成
CodeDeploy 様々なコンピューティングサービスへのデプロイを自動化する アプリケーションのデプロイ
CodePipeline パイプラインの継続的デリバリーの自動化 CI/CD パイプライン
Amazon ECS コンテナ化されたアプリケーションをデプロイ、管理する ※本記事ではアプリのデプロイ先として使用
Amazon ECR コンテナレジストリであり、アプリケーションイメージを管理する コンテナイメージの格納先
Amazon EventBridge イベント駆動で各種サービスのトリガーを実施するサービス パイプラインの起動などのトリガー
Amazon SNS フルマネージドの Pub/Sub サービス 承認用のメール通知
IAM ID と AWS のサービスおよびリソースへのアクセス、権限管理を行う 権限制御
AWS Cloud9 コードを記述、実行、デバッグできるクラウドベースの統合開発環境 (IDE) コード開発

処理方式

デプロイまでの処理方式について記載します。
基本的に環境でリリース処理を分割しているので、開発環境向けと本番環境向けの 2 つの処理フローがあります。

開発環境向けの処理方式

Alternate text開発環境向け処理フロー

  1. ソースコードを clone
    CodeCommit から ソースファイルを clone する。

  2. 対象ブランチへの Push
    ソースファイルの修正を実施し、開発用ブランチ(以降、develop ブランチ)に Push する。

  3. パイプラインの起動
    develop ブランチへのイベントをトリガーに EventBridge が CodePipeline を起動する。

  4. ビルド処理
    4-1. CodeCommit をコード元としてビルド処理を実行する。
    4-2. ビルドして作成されたコンテナイメージを ECR に Push する。

  5. デプロイ処理
    前処理で作成された ECR 上のコンテナイメージを ECS にデプロイを実施する。

本番環境向けの処理方式

Alternate text本番環境向け処理フロー

  1. プルリクエストのマージ
    CodeCommit で develop ブランチをベースにプルリクエストを作成し、本番用ブランチ(以降、main ブランチ)にマージする。

  2. パイプラインの起動
    main ブランチへのイベントをトリガーに EventBridge が CodePipeline を起動する。

  3. ビルド処理
    3-1. CodeCommit をコード元としてビルド処理を実行する。
    3-2. ビルドして作成されたコンテナイメージを ECR に Push する。

  4. 手動承認処理
    4-1. パイプラインの手動承認ステージで Amazon SNS をトリガーして承認者宛てにメールを送付する。
    4-2. 承認者が処理内容を確認し、デプロイを承認するか否かを決定する。
       申請を承認した場合は処理が継続され、却下した場合は処理終了となる。

  5. デプロイ処理
    前処理で作成された ECR 上のコンテナイメージを ECS にデプロイする。

実際の各種処理の設定がどうなっているのかは次項目以降で記載していきます。

構築・説明

0. 前提事項

  • Code シリーズの実装方式・設定内容を重点的に記載しています。
  • ネットワークおよびデプロイ先となる環境(ECS)は既に構築済みと想定して詳細を記載しません。
  • 本記事で利用したネットワークや ECS などを構築する CloudFormation のコードは本記事の最後に記載しています。

1. CodeCommit の作成

まずは CodeCommit でリポジトリを作成します。
Alternate text

作成後、本番環境用ブランチ(main)と開発環境用ブランチ(develop)を作成します。
※main が デフォルトブランチになるように設定します。

Alternate text

作成したリポジトリには下記のファイル群を Push します。
各ファイルは使用する工程で内容を記載します。

ファイル名 内容
buildspec.yaml CodeBuild で使用されるビルドの処理が記載されたファイル
taskdef.json ECS のタスク定義を更新するためのファイル
appspec.yaml CodeDeploy で使用されるデプロイ処理が記載されたファイル
src/Dockerfile コンテナイメージ作成用ファイル
src/index.html WEB アプリケーションファイル(テスト用)

ここではテスト用に利用するアプリ作成のための Dockerfileindex.html だけ載せておきます。

Dockerfile
FROM nginx:latest
COPY index.html /usr/share/nginx/html
EXPOSE 80
index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>test-web</title>
  </head>
  <body>
    <h1>Welcome to Test Website!!</h1>
    <a>This is website on ecs fargate!!</a>
  </body>
</html>

2. CodeBuild の作成

次に CodeBuild にビルドプロジェクトを作成します。
ちょっと長いですが、設定項目は下記の通りです。

Alternate text

ポイントを記載します。

  • ソース
    • ソースプロバイダは先ほど作成した CodeCommit を指定しています。
    • 開発環境向けのビルドプロジェクトはdevelopブランチ、本番環境向けはmainブランチを指定します。
  • Buildspec
    • Buildspec 名には、先ほど Codecommit に Push したbuildspec.yamlを設定します。
    • buildspec.yamlのコードは下記に記載します。
  • 環境
    • 追加設定の環境変数に下記の項目の値を設定します。
      • AWS_ACCOUNT_ID
      • AWS_DEFAULT_REGION
      • IMAGE_REPO_NAME
      • IMAGE_TAG
      • TASK_ROLE_ARN
      • EXECUTION_ROLE_ARN
      • TASK_FAMILY
      • CONTAINER_NAME

Alternate textCodeBuild の環境変数設定

buildspec.yaml
version: 0.2

phases:
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...
      - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG ./src
      - echo docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
      - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker image...
      - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
      - printf '{"Version":"1.0","ImageURI":"%s"}' $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG > imageDetail.json
      - sed -i -e "s#<CONTAINER_NAME>#${CONTAINER_NAME}#" taskdef.json
      - sed -i -e "s#<TASK_FAMILY>#${TASK_FAMILY}#" taskdef.json
      - sed -i -e "s#<TASK_ROLE_ARN>#${TASK_ROLE_ARN}#" taskdef.json
      - sed -i -e "s#<EXECUTION_ROLE_ARN>#${EXECUTION_ROLE_ARN}#" taskdef.json
      - sed -i -e "s#<CONTAINER_NAME>#${CONTAINER_NAME}#" appspec.yaml

artifacts:
  files:
    - imageDetail.json
    - taskdef.json
    - appspec.yaml

このbuildspec.yamlの内容を端的に説明すると、ECR へのログインを実施後に docker ビルドでアプリのコンテナイメージを作成し、ECR にプッシュしています。
また post_build ステージ の ECR にプッシュ(docker push)後に実施している処理は、各ファイル内に環境固有の文字列(アカウント ID を含む ARN、デプロイ先のコンテナ名など)が含まれているため、少し力技になりますがこのビルド処理の中で文字列を置換しています。(CodeBuild の環境変数として設定した値が入力されます。)

参考までに CodeBuild のビルドプロジェクト作成の CloudFormation コードの載せておきますので参考にしてみてください。

CodeBuild 構築用の CoudFormation コード
Codebuild.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: "Sample CodeBuild"

# Parameters
Parameters:
  ### Env Prefix ###
  EnvPrefix:
    Type: String

# Resources
Resources:
  # IAM Role
  CodeBuildServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "CodeBuildBasePolicy-sample-${EnvPrefix}-build-role"
      Path: /
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - codebuild.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: !Sub "CodeBuildBasePolicy-sample-${EnvPrefix}-build-policy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Action:
                  - ecr:BatchCheckLayerAvailability
                  - ecr:CompleteLayerUpload
                  - ecr:GetAuthorizationToken
                  - ecr:InitiateLayerUpload
                  - ecr:PutImage
                  - ecr:UploadLayerPart
                Effect: Allow
                Resource: "*"
              - Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Effect: Allow
                Resource:
                  - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/sample-${EnvPrefix}-build"
                  - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/sample-${EnvPrefix}-build:*"
              - Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:GetObjectVersion
                  - s3:GetBucketAcl
                  - s3:GetBucketLocation
                Effect: Allow
                Resource:
                  - !Sub "arn:aws:s3:::codepipeline-${AWS::Region}-*"
              - Action:
                  - codecommit:GitPull
                Effect: Allow
                Resource:
                  - !Sub "arn:aws:codecommit:${AWS::Region}:${AWS::AccountId}:sample-${EnvPrefix}-repo"
              - Action:
                  - codebuild:CreateReportGroup
                  - codebuild:CreateReport
                  - codebuild:UpdateReport
                  - codebuild:BatchPutTestCases
                  - codebuild:BatchPutCodeCoverages
                Effect: Allow
                Resource:
                  - !Sub "arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:report-group/sample-${EnvPrefix}-build*"

  # Code Build
  CodeBuildProject:
    Type: "AWS::CodeBuild::Project"
    Properties:
      Name: !Sub "sample-${EnvPrefix}-build"
      Source:
        BuildSpec: "buildspec.yml "
        GitCloneDepth: 1
        GitSubmodulesConfig:
          FetchSubmodules: false
        InsecureSsl: false
        Location: !Sub "https://git-codecommit.${AWS::Region}.amazonaws.com/v1/repos/sample-${EnvPrefix}-repo"
        Type: "CODECOMMIT"
      Artifacts:
        Type: "NO_ARTIFACTS"
      Cache:
        Type: "NO_CACHE"
      Environment:
        ComputeType: "BUILD_GENERAL1_SMALL"
        EnvironmentVariables:
          - Name: "AWS_ACCOUNT_ID"
            Type: "PLAINTEXT"
            Value: !Sub ${AWS::AccountId}
          - Name: "AWS_DEFAULT_REGION"
            Type: "PLAINTEXT"
            Value: !Sub ${AWS::Region}
          - Name: "IMAGE_REPO_NAME"
            Type: "PLAINTEXT"
            Value: !Sub "sample-${EnvPrefix}-app"
          - Name: "IMAGE_TAG"
            Type: "PLAINTEXT"
            Value: "latest"
          - Name: "TASK_ROLE_ARN"
            Type: "PLAINTEXT"
            Value: !Sub "arn:aws:iam::${AWS::AccountId}:role/${EnvPrefix}-ecsTaskExecutionRole"
          - Name: "EXECUTION_ROLE_ARN"
            Type: "PLAINTEXT"
            Value: !Sub "arn:aws:iam::${AWS::AccountId}:role/${EnvPrefix}-ecsTaskExecutionRole"
          - Name: "TASK_FAMILY"
            Type: "PLAINTEXT"
            Value: !Sub "sample-${EnvPrefix}-cluster-task"
          - Name: "CONTAINER_NAME"
            Type: "PLAINTEXT"
            Value: !Sub "sample-${EnvPrefix}-app"
        Image: "aws/codebuild/amazonlinux2-x86_64-standard:corretto11"
        ImagePullCredentialsType: "CODEBUILD"
        PrivilegedMode: false
        Type: "LINUX_CONTAINER"
      ServiceRole: !GetAtt CodeBuildServiceRole.Arn
      TimeoutInMinutes: 15
      QueuedTimeoutInMinutes: 480
      EncryptionKey: !Sub "arn:aws:kms:${AWS::Region}:${AWS::AccountId}:alias/aws/s3"
      BadgeEnabled: false
      LogsConfig:
        CloudWatchLogs:
          Status: "ENABLED"
        S3Logs:
          Status: "DISABLED"
          EncryptionDisabled: false
      Visibility: "PRIVATE"

ちなみに下記が taskdef.jsonappspec.yaml の内容です。

taskdef.json
{
  "containerDefinitions": [
    {
      "name": "<CONTAINER_NAME>",
      "image": "<IMAGE1_NAME>",
      "portMappings": [
        {
          "containerPort": 80,
          "hostPort": 80,
          "protocol": "tcp"
        }
      ]
    }
  ],
  "family": "<TASK_FAMILY>",
  "taskRoleArn": "<TASK_ROLE_ARN>",
  "executionRoleArn": "<EXECUTION_ROLE_ARN>",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "1024",
  "memory": "3072"
}
appspec.yaml
version: 0.0
Resources:
  - TargetService:
      Type: AWS::ECS::Service
      Properties:
        TaskDefinition: <TASK_DEFINITION>
        LoadBalancerInfo:
          ContainerName: "<CONTAINER_NAME>"
          ContainerPort: 80

3. CodeDeploy の作成

次に CodeDeploy を構築します。
CodeDeploy はデプロイするアプリケーションの設定とデプロイグループを設定する必要があります。
また CodeBuild では buildspec.yaml にビルドで実行される内容を記載しましたが、CodeDeploy では appspec.yaml にデプロイ内容を記載します。
Alternate textアプリケーションの作成
Alternate textデプロイグループの作成

今回は ECS へのデプロイのため、デプロイパターンは Blue/Green で実装しており、デプロイ設定は All at Once にしています。
その他の項目は AWS 公式ページ(Blue/Green デプロイを使用した CodeDeploy)に記載があるため、そちらを参照してください。

4. CodePipeline の作成

続いて、CodePipeline を作成します。
その事前作業として、本番環境向けのパイプラインで使用する手動承認のための SNS トピックおよびサブスクリプションを作成します。
Alternate textSNS トピックの作成
Alternate textSNS サブスクリプションの作成
エンドポイントに設定したアドレスにメールが届くので、サブスクリプションを確認のリンクをクリックすれば完了です。
Alternate text

ここまでが事前準備で、本題のパイプライン作成をしていきます。
Alternate textパイプラインの設定
Alternate textソースの設定

Alternate textビルドの設定
Alternate textデプロイの設定
各設定項目には、SourceArtifactBuildArtifactがあります。
SourceArtifact は、ソースコードとしてリポジトリに格納したコードを使用してデプロイ処理を実施し、BuildArtifact はビルド処理で生成されたファイルを使用して処理を実施します。
今回は「CodeBuild の作成」でも記載した通り、buildspec.yamlの中で変数を置換したりする処理を実施しているため、BuildArtifact を設定しています。

また本番環境向けのパイプラインには、手動承認のステージを追加します。
ここで先ほど作成した SNS トピックの ARN を設定します。
Alternate text手動承認の追加

5. 動かしてみる

ここまでの作業で構築が完了したので、実際に起動してみます。

  1. まずは Cloud9 でアプリケーションコードを修正し、リポジトリに Push します。

    $ git add .
    $ git commit -m "fix html."
    $ git push origin develop
    
  2. ブランチへのイベントをトリガーにパイプラインが起動します。
    Alternate text
    パイプラインの処理が無事に成功しました。

  3. 次に develop ブランチを main ブランチにマージします。(プルリクエスト作成・実行)

  4. 上記のブランチへのイベントをトリガーにパイプラインが起動します。
    本番環境向けのパイプラインには手動承認があるため、承認処理でパイプラインが待機します。
    Alternate text

  5. 先ほど設定した SNS トピックを経由して承認者にメールが届きます。
    本文の中の「Approve or reject」に記載された URL をクリックすると、パイプラインの承認画面に遷移します。
    Alternate text

    レビューボダンをクリックすると、承認か却下を選択できる画面が出てくるので、入力します。
    Alternate text

  6. 承認されると処理が再開します。もちろん却下すると処理はそこで終了します。
    Alternate text
    無事に処理が完了しました。

おわりに

今回は Code シリーズを使用して CI/CD パイプラインの構築を実施してみました。
本記事内では実装していないのですが、ビルドステージでのテスト自動化や他のサービスとの連携する(CodeCommit の代わりに GitHub などを使用する)のも面白そうだと思いました。
本記事の構成は最低限の処理内容なのですが、実案件対応時の参考になれば幸いです。

おまけ:サンプルコード(簡易版ハンズオンのコード置き場)

本記事の検証で使用した構成のうち、Code シリーズ以外のリソースを作成する CloudFormation を記載します。何かの参考になれば幸いです。

構築順序

下記表の順番でファイル単位にスタックを作成することで環境を構築出来ます。
※各スタック間での依存関係があるため、項番通りに実施しないとエラーになります。

項番 CFn ファイル リソース
1 network.yaml VPC、サブネット、ルートテーブルなどのネットワーク全般
2 securitygroup.yaml ECS や ALB にアタッチするセキュリティグループ
3 ecr.yaml ECR のリポジトリ
4 webapp.yaml ECS on Fargate やそれに関連するサービス(IAM、ALB など)

コード

network.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: Test-WebSite-Network-Template

# Parameters
Parameters:
  ### Env Prefix ###
  EnvPrefix:
    Type: String
  ### VPC ###
  VPCCIDR:
    Type: String
    Default: "10.100.0.0/20"
  ### Public Subnet ###
  PublicSubnetACIDR:
    Type: String
    Default: "10.100.1.0/24"
  PublicSubnetCCIDR:
    Type: String
    Default: "10.100.2.0/24"
  ### Private Subnet ###
  PrivateSubnetACIDR:
    Type: String
    Default: "10.100.3.0/24"
  PrivateSubnetCCIDR:
    Type: String
    Default: "10.100.4.0/24"

### Resources ###
Resources:
  # VPC
  VPC:
    Type: "AWS::EC2::VPC"
    Properties:
      CidrBlock: !Ref VPCCIDR
      EnableDnsSupport: "true"
      EnableDnsHostnames: "true"
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: !Sub "${EnvPrefix}-vpc"

  # IGW
  InternetGateway:
    Type: "AWS::EC2::InternetGateway"
    Properties:
      Tags:
        - Key: Name
          Value: !Sub "${EnvPrefix}-igw"

  IGWAttachment:
    Type: "AWS::EC2::VPCGatewayAttachment"
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC

  # NatGW
  ### NAT Gateway ###
  NatGatewayA:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId:
        Fn::GetAtt:
          - NatGatewayEIPA
          - AllocationId
      SubnetId: !Ref PublicSubnetA
      Tags:
        - Key: Name
          Value: !Sub "${EnvPrefix}-NAT-Gateway-A"

  # NAT-GW
  NatGatewayC:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId:
        Fn::GetAtt:
          - NatGatewayEIPC
          - AllocationId
      SubnetId: !Ref PublicSubnetC
      Tags:
        - Key: Name
          Value: !Sub "${EnvPrefix}-NAT-Gateway-C"

  ### NAT Gateway EIP ###
  NatGatewayEIPA:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: Name
          Value: !Sub "${EnvPrefix}-NGW-EIP-A"

  # NAT-GW
  NatGatewayEIPC:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: Name
          Value: !Sub "${EnvPrefix}-NGW-EIP-C"

  # Subnet
  ### Public Subnet ###
  PublicSubnetA:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: "ap-northeast-1a"
      CidrBlock: !Ref PublicSubnetACIDR
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${EnvPrefix}-public-subnet-a"

  PublicSubnetC:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: "ap-northeast-1c"
      CidrBlock: !Ref PublicSubnetCCIDR
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${EnvPrefix}-public-subnet-c"

  ### Private Subnet ###
  PrivateSubnetA:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: "ap-northeast-1a"
      CidrBlock: !Ref PrivateSubnetACIDR
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${EnvPrefix}-private-subnet-a"

  PrivateSubnetC:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: "ap-northeast-1c"
      CidrBlock: !Ref PrivateSubnetCCIDR
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${EnvPrefix}-private-subnet-c"

  # RouteTable
  ### Public Subnet A Routing ###
  PublicARTB:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${EnvPrefix}-public-route-a"

  PublicASubnetRTBAssociation:
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      SubnetId: !Ref PublicSubnetA
      RouteTableId: !Ref PublicARTB

  PublicARoute:
    Type: "AWS::EC2::Route"
    Properties:
      RouteTableId: !Ref PublicARTB
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref InternetGateway

  ### Public Subnet C Routing ###
  PublicCRTB:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${EnvPrefix}-public-route-c"

  PublicCSubnetRTBAssociation:
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      SubnetId: !Ref PublicSubnetC
      RouteTableId: !Ref PublicCRTB

  PublicCRoute:
    Type: "AWS::EC2::Route"
    Properties:
      RouteTableId: !Ref PublicCRTB
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref InternetGateway

  ### Private Subnet A Routing ###
  PrivateARTB:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${EnvPrefix}-private-route-a"

  PrivateSubnetRTBAAssociation:
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      SubnetId: !Ref PrivateSubnetA
      RouteTableId: !Ref PrivateARTB

  PrivateRouteA01:
    Type: "AWS::EC2::Route"
    Properties:
      RouteTableId: !Ref PrivateARTB
      DestinationCidrBlock: "0.0.0.0/0"
      NatGatewayId: !Ref NatGatewayA

  ### Private Subnet C Routing ###
  PrivateCRTB:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${EnvPrefix}-private-route-c"

  PrivateSubnetRTBCAssociation:
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      SubnetId: !Ref PrivateSubnetC
      RouteTableId: !Ref PrivateCRTB

  PrivateRouteC01:
    Type: "AWS::EC2::Route"
    Properties:
      RouteTableId: !Ref PrivateCRTB
      DestinationCidrBlock: "0.0.0.0/0"
      NatGatewayId: !Ref NatGatewayA

# Output
Outputs:
  ### VPC ###
  ## VPC ID ##
  VPC:
    Value: !Ref VPC
    Export:
      Name: !Sub "${EnvPrefix}-vpc"
  ## VPC CIDR ##
  VPCCidr:
    Value: !Ref VPCCIDR
    Export:
      Name: !Sub "${EnvPrefix}-vpc-cidr"

  ### Subnet ###
  ## Public Subnet A ##
  PublicSubnetA:
    Value: !Ref PublicSubnetA
    Export:
      Name: !Sub "${EnvPrefix}-public-subnet-a"
  ## Public Subnet C ##
  PublicSubnetC:
    Value: !Ref PublicSubnetC
    Export:
      Name: !Sub "${EnvPrefix}-public-subnet-c"
  ## Private Subnet A ##
  PrivateSubnetA:
    Value: !Ref PrivateSubnetA
    Export:
      Name: !Sub "${EnvPrefix}-private-subnet-a"
  ## Private Subnet C ##
  PrivateSubnetC:
    Value: !Ref PrivateSubnetC
    Export:
      Name: !Sub "${EnvPrefix}-private-subnet-c"

  ### NAT-GW ###
  ## NAT-GW-A ##
  NatGatewayEIPA:
    Value: !Ref NatGatewayEIPA
    Export:
      Name: !Sub "${EnvPrefix}-natgw-a"

  ## NAT-GW-C ##
  NatGatewayEIPC:
    Value: !Ref NatGatewayEIPC
    Export:
      Name: !Sub "${EnvPrefix}-natgw-c"
securitygroup.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: "Sample Security Group"

# Parameters
Parameters:
  ### Env Prefix ###
  EnvPrefix:
    Type: String

# Resources
Resources:
  ALBSecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupDescription: !Sub "sample-${EnvPrefix}-webapp-alb-sg"
      GroupName: !Sub "sample-${EnvPrefix}-webapp-alb-sg"
      VpcId: !ImportValue dev-vpc
      SecurityGroupIngress:
        - CidrIp: !ImportValue dev-vpc-cidr
          IpProtocol: "-1"
        - CidrIp: !Sub
            - ${Natgweipa}/32
            - Natgweipa: { Fn::ImportValue: dev-natgw-a }
          IpProtocol: "-1"
        - CidrIp: !Sub
            - ${Natgweipc}/32
            - Natgweipa: { Fn::ImportValue: dev-natgw-c }
          IpProtocol: "-1"
      SecurityGroupEgress:
        - CidrIp: "0.0.0.0/0"
          IpProtocol: "-1"

  ECSSecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupDescription: !Sub "sample-${EnvPrefix}-webapp-ecs-sg"
      GroupName: !Sub "sample-${EnvPrefix}-webapp-ecs-sg"
      VpcId: !ImportValue dev-vpc
      SecurityGroupIngress:
        - SourceSecurityGroupId: !Ref ALBSecurityGroup
          SourceSecurityGroupOwnerId: !Ref AWS::AccountId
          IpProtocol: "-1"
      SecurityGroupEgress:
        - CidrIp: "0.0.0.0/0"
          IpProtocol: "-1"

# Output
Outputs:
  ALBSecurityGroupId:
    Value: !Ref ALBSecurityGroup
    Export:
      Name: !Sub "${EnvPrefix}-alb-sg-id"
  ECSSecurityGroupId:
    Value: !Ref ECSSecurityGroup
    Export:
      Name: !Sub "${EnvPrefix}-ecs-sg-id"
ecr.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: "Sample ECR Repository"

# Parameters
Parameters:
  ### Env Prefix ###
  EnvPrefix:
    Type: String

# Resources
Resources:
  ECRRepository:
    Type: "AWS::ECR::Repository"
    Properties:
      RepositoryName: !Sub "sample-${EnvPrefix}-app"
      EncryptionConfiguration:
        EncryptionType: KMS

# Output
Outputs:
  ECRArn:
    Value: !GetAtt ECRRepository.Arn
webapp.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: "Sample ECS and ALB"

# Parameters
Parameters:
  ### Env Prefix ###
  EnvPrefix:
    Type: String

Resources:
  # IAM Role
  EcsTaskExecutionRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: !Sub "${EnvPrefix}-ecsTaskExecutionRole"
      Path: /
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ecs-tasks.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

  ECSCluster:
    Type: "AWS::ECS::Cluster"
    Properties:
      ClusterName: !Sub "sample-${EnvPrefix}-cluster"
      CapacityProviders:
        - "FARGATE"
        - "FARGATE_SPOT"

  ECSService:
    Type: "AWS::ECS::Service"
    Properties:
      ServiceName: !Sub "sample-${EnvPrefix}-app-service"
      Cluster: !GetAtt ECSCluster.Arn
      LoadBalancers:
        - TargetGroupArn: !Ref ElasticLoadBalancingV2TargetGroup
          ContainerName: !Sub "sample-${EnvPrefix}-app"
          ContainerPort: 80
      DesiredCount: 1
      LaunchType: "FARGATE"
      PlatformVersion: "1.4.0"
      TaskDefinition: !Ref ECSTaskDefinition
      DeploymentConfiguration:
        MaximumPercent: 200
        MinimumHealthyPercent: 100
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: "DISABLED"
          SecurityGroups:
            - !ImportValue dev-ecs-sg-id
          Subnets:
            - !ImportValue dev-private-subnet-a
            - !ImportValue dev-private-subnet-c
      HealthCheckGracePeriodSeconds: 0
      SchedulingStrategy: "REPLICA"
      DeploymentController:
        Type: "CODE_DEPLOY"

  ECSTaskDefinition:
    Type: "AWS::ECS::TaskDefinition"
    Properties:
      ContainerDefinitions:
        - Essential: true
          Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/sample-${EnvPrefix}-app:latest"
          Name: !Sub "sample-${EnvPrefix}-app"
          PortMappings:
            - ContainerPort: 80
              HostPort: 80
              Protocol: "tcp"
      Family: !Sub "${ECSCluster}-task"
      TaskRoleArn: !Ref EcsTaskExecutionRole
      ExecutionRoleArn: !Ref EcsTaskExecutionRole
      NetworkMode: "awsvpc"
      RequiresCompatibilities:
        - "FARGATE"
      Cpu: "1024"
      Memory: "3072"

  ElasticLoadBalancingV2LoadBalancer:
    Type: "AWS::ElasticLoadBalancingV2::LoadBalancer"
    Properties:
      Name: "sample-dev-app-alb"
      Scheme: "internet-facing"
      Type: "application"
      Subnets:
        - !ImportValue dev-public-subnet-a
        - !ImportValue dev-public-subnet-c
      SecurityGroups:
        - !ImportValue dev-alb-sg-id
      IpAddressType: "ipv4"
      LoadBalancerAttributes:
        - Key: "access_logs.s3.enabled"
          Value: "false"
        - Key: "idle_timeout.timeout_seconds"
          Value: "60"
        - Key: "deletion_protection.enabled"
          Value: "false"
        - Key: "routing.http2.enabled"
          Value: "true"
        - Key: "routing.http.drop_invalid_header_fields.enabled"
          Value: "false"
        - Key: "routing.http.xff_client_port.enabled"
          Value: "false"
        - Key: "routing.http.preserve_host_header.enabled"
          Value: "false"
        - Key: "routing.http.xff_header_processing.mode"
          Value: "append"
        - Key: "load_balancing.cross_zone.enabled"
          Value: "true"
        - Key: "routing.http.desync_mitigation_mode"
          Value: "defensive"
        - Key: "waf.fail_open.enabled"
          Value: "false"
        - Key: "routing.http.x_amzn_tls_version_and_cipher_suite.enabled"
          Value: "false"
        - Key: "connection_logs.s3.enabled"
          Value: "false"

  ElasticLoadBalancingV2Listener:
    Type: "AWS::ElasticLoadBalancingV2::Listener"
    Properties:
      LoadBalancerArn: !Ref ElasticLoadBalancingV2LoadBalancer
      Port: 80
      Protocol: "HTTP"
      DefaultActions:
        - TargetGroupArn: !Ref ElasticLoadBalancingV2TargetGroup
          Type: "forward"

  ElasticLoadBalancingV2TargetGroup:
    Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
    Properties:
      HealthCheckIntervalSeconds: 30
      HealthCheckPath: "/"
      Port: 80
      Protocol: "HTTP"
      HealthCheckPort: "traffic-port"
      HealthCheckProtocol: "HTTP"
      HealthCheckTimeoutSeconds: 5
      UnhealthyThresholdCount: 2
      TargetType: "ip"
      Matcher:
        HttpCode: "200"
      HealthyThresholdCount: 5
      VpcId: !ImportValue dev-vpc
      Name: !Sub "sample-${EnvPrefix}-app-tg"
      HealthCheckEnabled: true
      TargetGroupAttributes:
        - Key: "target_group_health.unhealthy_state_routing.minimum_healthy_targets.percentage"
          Value: "off"
        - Key: "deregistration_delay.timeout_seconds"
          Value: "300"
        - Key: "stickiness.type"
          Value: "lb_cookie"
        - Key: "stickiness.lb_cookie.duration_seconds"
          Value: "86400"
        - Key: "slow_start.duration_seconds"
          Value: "0"
        - Key: "stickiness.app_cookie.duration_seconds"
          Value: "86400"
        - Key: "target_group_health.dns_failover.minimum_healthy_targets.percentage"
          Value: "off"
        - Key: "load_balancing.cross_zone.enabled"
          Value: "use_load_balancer_configuration"
        - Key: "load_balancing.algorithm.type"
          Value: "round_robin"
        - Key: "target_group_health.unhealthy_state_routing.minimum_healthy_targets.count"
          Value: "1"
        - Key: "stickiness.enabled"
          Value: "false"
        - Key: "target_group_health.dns_failover.minimum_healthy_targets.count"
          Value: "1"
        - Key: "load_balancing.algorithm.anomaly_mitigation"
          Value: "off"
        - Key: "stickiness.app_cookie.cookie_name"
          Value: ""

  ElasticLoadBalancingV2ListenerTest:
    Type: "AWS::ElasticLoadBalancingV2::Listener"
    Properties:
      LoadBalancerArn: !Ref ElasticLoadBalancingV2LoadBalancer
      Port: 8080
      Protocol: "HTTP"
      DefaultActions:
        - TargetGroupArn: !Ref ElasticLoadBalancingV2TargetGroupTest
          Type: "forward"

  ElasticLoadBalancingV2TargetGroupTest:
    Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
    Properties:
      HealthCheckIntervalSeconds: 30
      HealthCheckPath: "/"
      Port: 8080
      Protocol: "HTTP"
      HealthCheckPort: "traffic-port"
      HealthCheckProtocol: "HTTP"
      HealthCheckTimeoutSeconds: 5
      UnhealthyThresholdCount: 2
      TargetType: "ip"
      Matcher:
        HttpCode: "200"
      HealthyThresholdCount: 5
      VpcId: !ImportValue dev-vpc
      Name: !Sub "sample-${EnvPrefix}-app-tg-test"
      HealthCheckEnabled: true
      TargetGroupAttributes:
        - Key: "target_group_health.unhealthy_state_routing.minimum_healthy_targets.percentage"
          Value: "off"
        - Key: "deregistration_delay.timeout_seconds"
          Value: "300"
        - Key: "stickiness.type"
          Value: "lb_cookie"
        - Key: "stickiness.lb_cookie.duration_seconds"
          Value: "86400"
        - Key: "slow_start.duration_seconds"
          Value: "0"
        - Key: "stickiness.app_cookie.duration_seconds"
          Value: "86400"
        - Key: "target_group_health.dns_failover.minimum_healthy_targets.percentage"
          Value: "off"
        - Key: "load_balancing.cross_zone.enabled"
          Value: "use_load_balancer_configuration"
        - Key: "load_balancing.algorithm.type"
          Value: "round_robin"
        - Key: "target_group_health.unhealthy_state_routing.minimum_healthy_targets.count"
          Value: "1"
        - Key: "stickiness.enabled"
          Value: "false"
        - Key: "target_group_health.dns_failover.minimum_healthy_targets.count"
          Value: "1"
        - Key: "load_balancing.algorithm.anomaly_mitigation"
          Value: "off"
        - Key: "stickiness.app_cookie.cookie_name"
          Value: ""

# Output Parameter
Outputs:
  ElasticLoadBalancingV2LoadBalancerInfo:
    Value: !Ref ElasticLoadBalancingV2LoadBalancer
    Export:
      Name: !Sub "${EnvPrefix}-alb"

Discussion