CloudFormationでECSクラスターとCodePipelineを設定する

# 作成されるリソース
- 基本的なネットワーク
- ECSクラスター（EC2 on ECS）
- CodePipeline
    - CodeCommit
    - CodeBuild BuildProject
    - (ECS Deploy)

※CodePipelineのデプロイステージは未検証のためコメントアウト

# テンプレート
ECSクラスターの作成は以下の公式ドキュメントを参考にしています。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-cluster.html

```yaml:
AWSTemplateFormatVersion: 2010-09-09
Parameters:
  VPCCidrBlock:
    Description: The primary IPv4 CIDR block for the VPC.
    Type: String
    MinLength: '9'
    MaxLength: '18'
    Default: 10.0.0.0/16
    AllowedPattern: '(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})'

  SubnetCidrBits:
    Description: Subnet CidrBits, alowed bitween 8 and 30.
    Type: String
    MinLength: '1'
    MaxLength: '2'
    Default: '24'
    AllowedPattern: '(\d{1,2})'

  PJPrefix:
    Type: String
    Default: ecs

  ECSAMI:
    Description: The Amazon Machine Image ID used for the cluster
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Default: /aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id

  CodeCommitSourceBucket:
    Type: String
    Default: example-bucket

  CodeCommitSourceObjectKey:
    Type: String
    Default: example-key.zip

Mappings:
  CidrBits:
    '30':
      bit: 2
    '29':
      bit: 3
    '28':
      bit: 4
    '27':
      bit: 5
    '26':
      bit: 6
    '25':
      bit: 7
    '24':
      bit: 8
    '23':
      bit: 9
    '22':
      bit: 10
    '21':
      bit: 11
    '20':
      bit: 12
    '19':
      bit: 13
    '18':
      bit: 14
    '17':
      bit: 15
    '16':
      bit: 16
    '15':
      bit: 17
    '14':
      bit: 18
    '13':
      bit: 19
    '12':
      bit: 20
    '11':
      bit: 21
    '10':
      bit: 22
    '9':
      bit: 23
    '8':
      bit: 24

Resources:
  VPC:
    Type: 'AWS::EC2::VPC'
    Properties:
      EnableDnsSupport: 'true'
      EnableDnsHostnames: 'true'
      CidrBlock: !Ref VPCCidrBlock
      Tags:
        - Key: Name
          Value: !Sub '${PJPrefix}-VPC'
  PublicSubnet1:
    Type: 'AWS::EC2::Subnet'
    Properties:
      AvailabilityZone: !Select
        - 0
        - 'Fn::GetAZs': !Ref 'AWS::Region'
      CidrBlock:
        Fn::Select:
          - 0
          - Fn::Cidr:
            - Fn::GetAtt:
              - VPC
              - CidrBlock
            - 4
            - Fn::FindInMap:
              - CidrBits
              - !Ref SubnetCidrBits
              - bit
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub '${PJPrefix}-PublicSubnet1'
      VpcId: !Ref VPC
  PublicSubnet2:
    Type: 'AWS::EC2::Subnet'
    Properties:
      CidrBlock:
        Fn::Select:
          - 1
          - Fn::Cidr:
            - Fn::GetAtt:
              - VPC
              - CidrBlock
            - 4
            - Fn::FindInMap:
              - CidrBits
              - !Ref SubnetCidrBits
              - bit
      AvailabilityZone: !Select
        - 1
        - 'Fn::GetAZs': !Ref 'AWS::Region'
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub '${PJPrefix}-PublicSubnet2'
      VpcId: !Ref VPC
  PrivateSubnet1:
    Type: 'AWS::EC2::Subnet'
    Properties:
      CidrBlock:
        Fn::Select:
          - 2
          - Fn::Cidr:
            - Fn::GetAtt:
              - VPC
              - CidrBlock
            - 4
            - Fn::FindInMap:
              - CidrBits
              - !Ref SubnetCidrBits
              - bit
      AvailabilityZone: !Select
        - 0
        - 'Fn::GetAZs': !Ref 'AWS::Region'
      Tags:
        - Key: Name
          Value: !Sub '${PJPrefix}-PrivateSubnet1'
      VpcId: !Ref VPC
  PrivateSubnet2:
    Type: 'AWS::EC2::Subnet'
    Properties:
      CidrBlock:
        Fn::Select:
          - 3
          - Fn::Cidr:
            - Fn::GetAtt:
              - VPC
              - CidrBlock
            - 4
            - Fn::FindInMap:
              - CidrBits
              - !Ref SubnetCidrBits
              - bit
      AvailabilityZone: !Select
        - 1
        - 'Fn::GetAZs': !Ref 'AWS::Region'
      Tags:
        - Key: Name
          Value: !Sub '${PJPrefix}-PrivateSubnet2'
      VpcId: !Ref VPC
  InternetGateway:
    Type: 'AWS::EC2::InternetGateway'
    DependsOn: VPC
    Properties:
      Tags:
        - Key: Name
          Value: InternetGateway
  VPCGatewayAttachment:
    Type: 'AWS::EC2::VPCGatewayAttachment'
    DependsOn:
      - VPC
      - InternetGateway
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway
  PublicRouteTable:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      Tags:
        - Key: Name
          Value: PublicRouteTable
      VpcId: !Ref VPC
  PublicRoute:
    Type: 'AWS::EC2::Route'
    DependsOn: VPCGatewayAttachment
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
  PublicSubnetRouteTableAssociation1:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    DependsOn:
      - PublicSubnet1
      - PublicRouteTable
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRouteTable
  PublicSubnetRouteTableAssociation2:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    DependsOn:
      - PublicSubnet2
      - PublicRouteTable
    Properties:
      SubnetId: !Ref PublicSubnet2
      RouteTableId: !Ref PublicRouteTable
  ElasticIP1:
    Type: 'AWS::EC2::EIP'
    Properties:
      Domain: vpc
      Tags:
        - Key: Name
          Value: ElasticIP1
  ElasticIP2:
    Type: 'AWS::EC2::EIP'
    Properties:
      Domain: vpc
      Tags:
        - Key: Name
          Value: ElasticIP2
  NATGateway1:
    Type: 'AWS::EC2::NatGateway'
    Properties:
      AllocationId: !GetAtt
        - ElasticIP1
        - AllocationId
      SubnetId: !Ref PublicSubnet1
      Tags:
        - Key: Name
          Value: NATGateway1
  NATGateway2:
    Type: 'AWS::EC2::NatGateway'
    Properties:
      AllocationId: !GetAtt
        - ElasticIP2
        - AllocationId
      SubnetId: !Ref PublicSubnet2
      Tags:
        - Key: Name
          Value: NATGateway2
  PrivateRoutetable1:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: PrivateRouteTable1
  PrivateRoute1:
    Type: 'AWS::EC2::Route'
    DependsOn:
      - NATGateway1
    Properties:
      RouteTableId: !Ref PrivateRoutetable1
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NATGateway1
  PrivateSubnetRouteTableAssociation1:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties:
      RouteTableId: !Ref PrivateRoutetable1
      SubnetId: !Ref PrivateSubnet1
  PrivateRoutetable2:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: PrivateRouteTable2
  PrivateRoute2:
    Type: 'AWS::EC2::Route'
    Properties:
      RouteTableId: !Ref PrivateRoutetable2
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NATGateway2
  PrivateSubnetRouteTableAssociation2:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties:
      SubnetId: !Ref PrivateSubnet2
      RouteTableId: !Ref PrivateRoutetable2
  PrivateRoutetable3:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: PrivateRouteTable3

########### Create ECS Resource ############

  ECSCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterSettings:
        - Name: containerInsights
          Value: enabled

  ECSAutoScalingGroup:
    Type: AWS::AutoScaling::AutoScalingGroup
    DependsOn:
      # This is to ensure that the ASG gets deleted first before these
    # resources, when it comes to stack teardown.
      - ECSCluster
      - EC2Role
    Properties:
      VPCZoneIdentifier:
        - !Ref PrivateSubnet1
        - !Ref PrivateSubnet2
      LaunchTemplate:
        LaunchTemplateId: !Ref ContainerInstances
        Version: !GetAtt ContainerInstances.LatestVersionNumber
      MinSize: 2
      MaxSize: 10
      DesiredCapacity: 2
      NewInstancesProtectedFromScaleIn: true
    UpdatePolicy:
      AutoScalingReplacingUpdate:
        WillReplace: "true"

  ContainerInstances:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateName: "asg-launch-template"
      LaunchTemplateData:
        ImageId: !Ref ECSAMI
        InstanceType: t3.medium
        IamInstanceProfile:
          Name: !Ref EC2InstanceProfile
        SecurityGroupIds:
          - !Ref ContainerHostSecurityGroup
        # This injected configuration file is how the EC2 instance
      # knows which ECS cluster on your AWS account it should be joining
        UserData:
          Fn::Base64: !Sub |
           #!/bin/bash -xe
            echo ECS_CLUSTER=${ECSCluster} >> /etc/ecs/ecs.config
            yum install -y aws-cfn-bootstrap
            /opt/aws/bin/cfn-init -v --stack ${AWS::StackId} --resource ContainerInstances --configsets full_install --region ${AWS::Region} &
         # Disable IMDSv1, and require IMDSv2
        MetadataOptions:
          HttpEndpoint: enabled
          HttpTokens: required
        TagSpecifications:
          - ResourceType: instance
            Tags:
            - Key: Name
              Value: ECS Cluster
  EC2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: /
      Roles: 
      - !Ref EC2Role

  CapacityProvider:
    Type: AWS::ECS::CapacityProvider
    Properties:
      AutoScalingGroupProvider:
        AutoScalingGroupArn: !Ref ECSAutoScalingGroup
        ManagedScaling:
          InstanceWarmupPeriod: 60
          MinimumScalingStepSize: 1
          MaximumScalingStepSize: 100
          Status: ENABLED
          # Percentage of cluster reservation to try to maintain
          TargetCapacity: 100
        ManagedTerminationProtection: ENABLED
   # Create a cluster capacity provider assocation so that the cluster
  # will use the capacity provider
  CapacityProviderAssociation:
    Type: AWS::ECS::ClusterCapacityProviderAssociations
    Properties:
      CapacityProviders:
        - !Ref CapacityProvider
      Cluster: !Ref ECSCluster
      DefaultCapacityProviderStrategy:
        - Base: 0
          CapacityProvider: !Ref CapacityProvider
          Weight: 1
  # A security group for the EC2 hosts that will run the containers.
  # This can be used to limit incoming traffic to or outgoing traffic
  # from the container's host EC2 instance.
  ContainerHostSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Access to the EC2 hosts that run containers
      VpcId: !Ref VPC
  # Role for the EC2 hosts. This allows the ECS agent on the EC2 hosts
  # to communciate with the ECS control plane, as well as download the docker
  # images from ECR to run on your host.
  EC2Role:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns:
      # See reference: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/security-iam-awsmanpol.html#security-iam-awsmanpol-AmazonEC2ContainerServiceforEC2Role
        - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role
      # This managed policy allows us to connect to the instance using SSM
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
  # This is a role which is used within Fargate to allow the Fargate agent
  # to download images, and upload logs.
  ECSTaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ecs-tasks.amazonaws.com
            Action:
              - sts:AssumeRole
            Condition:
              ArnLike:
                aws:SourceArn: !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:*
              StringEquals:
                aws:SourceAccount: AWS::AccountId
      Path: /
      # This role enables all features of ECS. See reference:
    # https://docs.aws.amazon.com/AmazonECS/latest/developerguide/security-iam-awsmanpol.html#security-iam-awsmanpol-AmazonECSTaskExecutionRolePolicy
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy



  CodeCommit:
    Type: AWS::CodeCommit::Repository
    Properties:
      RepositoryName: app
      RepositoryDescription: This is a repository for my project.
      Code:
        BranchName: main
        S3: 
          Bucket: !Ref CodeCommitSourceBucket
          Key: !Ref CodeCommitSourceObjectKey

  CodeBuild:         
    Type: AWS::CodeBuild::Project
    Properties:
      Artifacts: 
        Type: CODEPIPELINE
      Environment: 
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/amazonlinux2-x86_64-standard:4.0
        PrivilegedMode: true
        Type: LINUX_CONTAINER
      LogsConfig: 
        CloudWatchLogs:
          Status: ENABLED
      ServiceRole: !GetAtt CodeBuildRole.Arn
      Source: 
        Type: CODEPIPELINE
      TimeoutInMinutes: 5

  CodeBuildRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument: 
        Version: "2012-10-17"
        Statement: 
          - Effect: "Allow"
            Principal: 
              Service: 
                - "codebuild.amazonaws.com"
            Action: 
              - "sts:AssumeRole"
      ManagedPolicyArns: 
        - arn:aws:iam::aws:policy/AWSCodeBuildAdminAccess
        - arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess 
      Path: /

  CodePipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      ArtifactStore: 
        Location: !Ref S3Bucket
        Type: S3
      RoleArn: !GetAtt CodePipelineRole.Arn
      Stages: 
        - Name: Source
          Actions: 
            - ActionTypeId: 
                Category: Source
                Owner: AWS
                Provider: CodeCommit
                Version: 1
              Name: Source
              Configuration: 
                BranchName: main
                RepositoryName: !GetAtt CodeCommit.Name
                FileName: imagedefinitions.json
              OutputArtifacts: 
                - Name: sourceArtifact
              RunOrder: 1
        - Name: Build
          Actions: 
            - ActionTypeId: 
                Category: Build
                Owner: AWS
                Provider: CodeBuild
                Version: 1
              Name: Build
              Configuration:
                ProjectName: !Ref CodeBuild
              InputArtifacts: 
                - Name: sourceArtifact
              OutputArtifacts: 
                - Name: buildArtifact
              RunOrder: 2
        # - Name: Deploy
        #   Actions: 
        #     - ActionTypeId: 
        #         Category: Deploy
        #         Owner: AWS
        #         Provider: Amazon ECS
        #         Version: 1
        #       Name: Build
        #       Configuration:
        #         ClusterName: !Ref ECSCluster
        #         ServiceName: !Ref ECSCluster
        #       InputArtifacts: 
        #         - Name: buildArtifact
        #       RunOrder: 3

  S3Bucket:
    Type: 'AWS::S3::Bucket'

  CodePipelineRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument: 
        Version: "2012-10-17"
        Statement: 
          - Effect: "Allow"
            Principal: 
              Service: 
                - "codepipeline.amazonaws.com"
            Action: 
              - "sts:AssumeRole"

      Path: /
  RolePolicies: 
    Type: "AWS::IAM::Policy"
    Properties: 
      PolicyName: "CodePipelinePolicy"
      Roles:
        - !Ref CodePipelineRole
      PolicyDocument: 
        Version: "2012-10-17"
        Statement: 
        - Effect: "Allow"
          Action: 
          - "codecommit:CancelUploadArchive"
          - "codecommit:GetBranch"
          - "codecommit:GetCommit"
          - "codecommit:GetUploadArchiveStatus"
          - "codecommit:UploadArchive"
          Resource: "*"
        - Effect: "Allow"
          Action: 
          - "codedeploy:CreateDeployment"
          - "codedeploy:GetApplicationRevision"
          - "codedeploy:GetDeployment"
          - "codedeploy:GetDeploymentConfig"
          - "codedeploy:RegisterApplicationRevision"
          Resource: "*"
        - Effect: "Allow"
          Action: 
          - "codebuild:BatchGetBuilds"
          - "codebuild:StartBuild"
          Resource: "*"
        - Effect: "Allow"
          Action: 
          - "devicefarm:ListProjects"
          - "devicefarm:ListDevicePools"
          - "devicefarm:GetRun"
          - "devicefarm:GetUpload"
          - "devicefarm:CreateUpload"
          - "devicefarm:ScheduleRun"
          Resource: "*"
        - Effect: "Allow"
          Action: 
          - "lambda:InvokeFunction"
          - "lambda:ListFunctions"
          Resource: "*"
        - Effect: "Allow"
          Action: 
          - "iam:PassRole"
          Resource: "*"
        - Effect: "Allow"
          Action: 
          - "elasticbeanstalk:*"
          - "ec2:*"
          - "elasticloadbalancing:*"
          - "autoscaling:*"
          - "cloudwatch:*"
          - "s3:*"
          - "sns:*"
          - "cloudformation:*"
          - "rds:*"
          - "sqs:*"
          - "ecs:*"
          Resource: "*"
```