ECS Fargate 楽々構築テンプレート
見出し画像

ECS Fargate 楽々構築テンプレート

この記事は電通デジタルアドベントカレンダー2020の22日目の記事になります。前回の記事は「ADH APIを効率的に呼び出すために開発したHooksの紹介」でした。

改めましてこんにちは!
Docker使ってますか?

AWSでDockerを使おうと思うと以下の3つの選択肢があります。

・Elastic Container Service
・Elastic Kubernetes Service
・EC2に構築する

この中でもECSいいですよね、僕も好きです。運用に手間もかからなくて気軽に使えるところに好感もてます。さすがAWSのマネージドサービス。

ただし実際にECSで構築しようとすると周辺のリソースが色々と必要になるので初心者にとってハードルが高く見えるのも事実です。そんなわけで初心者にも使えるようなテンプレートを提供したいと思います。

このテンプレートでは最低限の機能しか提供しません。何番煎じかもわかりませんしありふれた内容なので初心者以外スルー推奨です。AppMeshやCodeDeployにも触れませんしね。

ECSでFargateを利用するために必要なリソース群

それではFargateの起動に必要な表面上のリソースです。実際にはもう少しリソースが必要になりますが、概念程度の理解を目指します。

Case1:インターネットアクセスを伴う場合

・ALB
・ECS Cluster
・IAMロール(Fargate起動時に必要なロールとアプリが必要とするロール)
・タスク定義(アプリを動かす環境変数などの定義)
・ECSサービス

Case2:VPCのみでアクセスが行われる場合

・Cloud Map(VPC内でのみ利用できるPrivateDNSNameを設定する)
・ECS Cluster
・IAMロール(Fargate起動時に必要なロールとアプリが必要とするロール)
・タスク定義(アプリを動かす環境変数などの定義)
・ECSサービス

Case3:スケジュールで起動するバッチ利用の場合

・ECS Cluster
・IAMロール(Fargate起動時に必要なロールとアプリが必要とするロール)
・タスク定義(アプリを動かす環境変数などの定義)
・CloudWatch イベントルール

ECSサービスの構築

それではCloudFormationのテンプレートファイルです。

前提

・ネットワークが構築済み(VPCやSubnet等)
・アプリケーションはDBと通信しない

Case1:インターネットアクセスを伴う場合

画像2

AWSTemplateFormatVersion: '2010-09-09'

Parameters:
 ProjectName:
   Type: String
   Default: 'sample1'
 VpcId:
   Type: AWS::EC2::VPC::Id
 PublicSubnets:
   Type: List<AWS::EC2::Subnet::Id>
   # Descriotion: 'インターネットゲートウェイがアタッチされたSubnet'
 ProtectedSubnets:
   Type: List<AWS::EC2::Subnet::Id>
   # Descriotion: 'NATゲートウェイがアタッチされたSubnet。なかったらPublicSubnetsと同じ値で。'
 AllowEcsPolicy:
   Type: List<String>
   Default: 's3:List*,s3:Get*,s3:Put*'
   # Descriotion: 'アプリケーションが必要なAWSリソースのポリシー'
 TaskCpu:
   Type: Number
   Default: 256
 TaskMemory:
   Type: Number
   Default: 512
 DesiredCount:
   Type: Number
   Default: 0  # 作成時はイメージがないので起動しないように設定
   # Descriotion: 'ECSサービスの常時起動タスク数'

Resources:
 # -------------------------------------
 # 外からのアクセスを許可するSG
 # -------------------------------------
 AllowFromWeb:
   Type: AWS::EC2::SecurityGroup
   Properties:
     Tags:
       - Key: Name
         Value: !Sub allow-from-web-for-${ProjectName}
     GroupName: !Sub allow-from-web-for-${ProjectName}
     GroupDescription: Security group for the service
     VpcId: !Ref VpcId

 AllowFromWebIngress:
   Type: AWS::EC2::SecurityGroupIngress
   Properties:
     GroupId: !Ref AllowFromWeb
     IpProtocol: -1
     SourceSecurityGroupId: !Ref AllowFromWeb

 # -------------------------------------
 # LBとFargateの通信を許可するSG
 # -------------------------------------
 SecurityGroup:
   Type: AWS::EC2::SecurityGroup
   Properties:
     Tags:
       - Key: Name
         Value: !Ref ProjectName
     GroupName: !Ref ProjectName
     GroupDescription: Security group for the service
     VpcId: !Ref VpcId

 SecurityGroupIngress:
   Type: AWS::EC2::SecurityGroupIngress
   Properties:
     GroupId: !Ref SecurityGroup
     IpProtocol: -1
     SourceSecurityGroupId: !Ref SecurityGroup

 LoadBalancer:
   Type: AWS::ElasticLoadBalancingV2::LoadBalancer
   Properties:
     Name: !Sub ${ProjectName}-alb
     Scheme: internet-facing
     Subnets: !Ref PublicSubnets
     Type: application
     SecurityGroups:
       - !Ref SecurityGroup
       - !Ref AllowFromWeb

 HttpListnener:
   Type: AWS::ElasticLoadBalancingV2::Listener
   Properties:
     DefaultActions:
       - Type: fixed-response
         FixedResponseConfig:
           ContentType: text/plain
           StatusCode: 503
     LoadBalancerArn: !Ref LoadBalancer
     Port: 80
     Protocol: HTTP

 Cluster:
   Type: AWS::ECS::Cluster
   Properties:
     ClusterName: !Ref ProjectName

 Repository:
   Type: AWS::ECR::Repository
   Properties:
     RepositoryName: !Ref ProjectName

 LogGroup:
   Type: AWS::Logs::LogGroup
   Properties:
     LogGroupName: !Sub /ecs/${ProjectName}

 # -------------------------------------
 # タスク起動時に必要なロールを定義
 # -------------------------------------
 EcsTaskExecutionRole:
   Type: AWS::IAM::Role
   Properties:
     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
     RoleName: !Sub ${ProjectName}-task-execution-role

 EcsTaskExecutionRolePolicy:
   Type: AWS::IAM::Policy
   Properties:
     PolicyName: !Sub ${ProjectName}-task-execution-role-policy
     PolicyDocument:
       Version: 2012-10-17
       Statement:
         - Effect: Allow
           Action:
             - ecr:GetLifecyclePolicyPreview
             - ecr:GetDownloadUrlForLayer
             - ecr:BatchGetImage
             - ecr:DescribeImages
             - ecr:ListTagsForResource
             - ecr:BatchCheckLayerAvailability
             - ecr:GetLifecyclePolicy
             - ecr:GetRepositoryPolicy
           Resource: !Sub "arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/*"
         - Effect: Allow
           Action:
             - ecr:GetAuthorizationToken
             - ssm:GetParameters
             - secretsmanager:GetSecretValue
           Resource:
             - !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/*"
     Roles:
       - Ref: EcsTaskExecutionRole

 # -------------------------------------
 # アプリケーションに必要なロールを定義
 # -------------------------------------
 EcsTaskRole:
   Type: AWS::IAM::Role
   Properties:
     AssumeRolePolicyDocument:
       Version: 2012-10-17
       Statement:
         - Effect: Allow
           Principal:
             Service:
               - ecs-tasks.amazonaws.com
               - events.amazonaws.com
           Action:
             - sts:AssumeRole
     ManagedPolicyArns:
       - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceEventsRole
     RoleName: !Sub ${ProjectName}-task-role

 EcsTaskRolePolicy:
   Type: AWS::IAM::Policy
   Properties:
     PolicyName: !Sub ${ProjectName}-task-role-policy
     PolicyDocument:
       Version: 2012-10-17
       Statement:
         - Effect: Allow
           Action: !Ref AllowEcsPolicy
           Resource: '*'
     Roles:
       - Ref: EcsTaskRole

 # --------------------------------------------------------------------------
 # 1つのALBに複数のサービスを起動する場合は以下のリソースを複数作成する必要があります
 # --------------------------------------------------------------------------

 # -------------------------------------
 # Fargate タスク定義
 # -------------------------------------
 TaskDefinition:
   Type: AWS::ECS::TaskDefinition
   Properties:
     Family: !Ref ProjectName
     RequiresCompatibilities:
       - FARGATE
     Cpu: !Ref TaskCpu
     Memory: !Ref TaskMemory
     NetworkMode: awsvpc
     ExecutionRoleArn: !GetAtt EcsTaskExecutionRole.Arn
     TaskRoleArn: !GetAtt EcsTaskRole.Arn
     ContainerDefinitions:
       - Name: app
         Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${Repository}
         PortMappings:
           - ContainerPort: 80
             HostPort: 80
             Protocol: tcp
         Environment:
           - Name: TZ
             Value: Asia/Tokyo
         LogConfiguration:
           LogDriver: awslogs
           Options:
             awslogs-region: !Ref 'AWS::Region'
             awslogs-group: !Ref LogGroup
             awslogs-stream-prefix: app
         Essential: true

 TargetGroup:
   Type: AWS::ElasticLoadBalancingV2::TargetGroup
   Properties:
     HealthCheckIntervalSeconds: 30
     HealthCheckPath: /
     HealthCheckPort: 80
     HealthCheckProtocol: HTTP
     HealthCheckTimeoutSeconds: 6
     HealthyThresholdCount: 3
     Name: !Ref ProjectName
     Port: 80
     Protocol: HTTP
     UnhealthyThresholdCount: 3
     TargetType: ip
     VpcId: !Ref VpcId

 ListnenerRule:
   Type: AWS::ElasticLoadBalancingV2::ListenerRule
   Properties:
     Actions:
       - Type: forward
         TargetGroupArn: !Ref TargetGroup
         Order: 1
     Conditions:
       - Field: path-pattern
         Values:
           - "*"
     ListenerArn: !Ref HttpListnener
     Priority: 100

 Service:
   Type: AWS::ECS::Service
   Properties:
     Cluster: !Ref Cluster
     DeploymentConfiguration:
       MaximumPercent: 200
       MinimumHealthyPercent: 100
     DesiredCount: !Ref DesiredCount
     LaunchType: FARGATE
     LoadBalancers:
       - ContainerName: app
         ContainerPort: 80
         TargetGroupArn: !Ref TargetGroup
     NetworkConfiguration:
       AwsvpcConfiguration:
         AssignPublicIp: DISABLED  # PublicSubnetを利用する場合はENABLEDにする必要があります。
         SecurityGroups:
           - !Ref SecurityGroup
         Subnets: !Ref ProtectedSubnets
     ServiceName: !Ref ProjectName
     TaskDefinition: !Ref TaskDefinition
   DependsOn:
       - ListnenerRule

Case2:VPCのみでアクセスが行われる場合

画像4

AWSTemplateFormatVersion: '2010-09-09'

Parameters:
 ProjectName:
   Type: String
   Default: 'sample2'
 VpcId:
   Type: AWS::EC2::VPC::Id
 ProtectedSubnets:
   Type: List<AWS::EC2::Subnet::Id>
   # Descriotion: 'NATゲートウェイがアタッチされたSubnet。なかったらPublicSubnetsと同じ値で。'
 Namespace:
   Type: String
   Default: 'sample.local'
 AllowEcsPolicy:
   Type: List<String>
   Default: 's3:List*,s3:Get*,s3:Put*'
   # Descriotion: 'アプリケーションが必要なAWSリソースのポリシー'
 TaskCpu:
   Type: Number
   Default: 256
 TaskMemory:
   Type: Number
   Default: 512
 ServiceDiscoveryName:
   Type: String
   Default: 'app'
 DesiredCount:
   Type: Number
   Default: 0  # 作成時はイメージがないので起動しないように設定
   # Descriotion: 'ECSサービスの常時起動タスク数'

Resources:
 # -------------------------------------
 # Fargate同士の通信を許可するSG
 # -------------------------------------
 SecurityGroup:
   Type: AWS::EC2::SecurityGroup
   Properties:
     Tags:
       - Key: Name
         Value: !Ref ProjectName
     GroupName: !Ref ProjectName
     GroupDescription: Security group for the service
     VpcId: !Ref VpcId

 SecurityGroupIngress:
   Type: AWS::EC2::SecurityGroupIngress
   Properties:
     GroupId: !Ref SecurityGroup
     IpProtocol: -1
     SourceSecurityGroupId: !Ref SecurityGroup

 PrivateDnsNamespace:
   Type: AWS::ServiceDiscovery::PrivateDnsNamespace
   Properties:
     Vpc: !Ref VpcId
     Name: !Ref Namespace

 Cluster:
   Type: AWS::ECS::Cluster
   Properties:
     ClusterName: !Ref ProjectName

 Repository:
   Type: AWS::ECR::Repository
   Properties:
     RepositoryName: !Ref ProjectName

 LogGroup:
   Type: AWS::Logs::LogGroup
   Properties:
     LogGroupName: !Sub /ecs/${ProjectName}

 # -------------------------------------
 # タスク起動時に必要なロールを定義
 # -------------------------------------
 EcsTaskExecutionRole:
   Type: AWS::IAM::Role
   Properties:
     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
     RoleName: !Sub ${ProjectName}-task-execution-role

 EcsTaskExecutionRolePolicy:
   Type: AWS::IAM::Policy
   Properties:
     PolicyName: !Sub ${ProjectName}-task-execution-role-policy
     PolicyDocument:
       Version: 2012-10-17
       Statement:
         - Effect: Allow
           Action:
             - ecr:GetLifecyclePolicyPreview
             - ecr:GetDownloadUrlForLayer
             - ecr:BatchGetImage
             - ecr:DescribeImages
             - ecr:ListTagsForResource
             - ecr:BatchCheckLayerAvailability
             - ecr:GetLifecyclePolicy
             - ecr:GetRepositoryPolicy
           Resource: !Sub "arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/*"
         - Effect: Allow
           Action:
             - ecr:GetAuthorizationToken
             - ssm:GetParameters
             - secretsmanager:GetSecretValue
           Resource:
             - !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/*"
     Roles:
       - Ref: EcsTaskExecutionRole

 # -------------------------------------
 # アプリケーションに必要なロールを定義
 # -------------------------------------
 EcsTaskRole:
   Type: AWS::IAM::Role
   Properties:
     AssumeRolePolicyDocument:
       Version: 2012-10-17
       Statement:
         - Effect: Allow
           Principal:
             Service:
               - ecs-tasks.amazonaws.com
               - events.amazonaws.com
           Action:
             - sts:AssumeRole
     ManagedPolicyArns:
       - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceEventsRole
     RoleName: !Sub ${ProjectName}-task-role

 EcsTaskRolePolicy:
   Type: AWS::IAM::Policy
   Properties:
     PolicyName: !Sub ${ProjectName}-task-role-policy
     PolicyDocument:
       Version: 2012-10-17
       Statement:
         - Effect: Allow
           Action: !Ref AllowEcsPolicy
           Resource: '*'
     Roles:
       - Ref: EcsTaskRole

 # --------------------------------------------------------------------------
 # 複数のサービスを起動する場合は以下のリソースを複数作成する必要があります
 # --------------------------------------------------------------------------

 # -------------------------------------
 # Fargate タスク定義
 # -------------------------------------
 TaskDefinition:
   Type: AWS::ECS::TaskDefinition
   Properties:
     Family: !Ref ProjectName
     RequiresCompatibilities:
       - FARGATE
     Cpu: !Ref TaskCpu
     Memory: !Ref TaskMemory
     NetworkMode: awsvpc
     ExecutionRoleArn: !GetAtt EcsTaskExecutionRole.Arn
     TaskRoleArn: !GetAtt EcsTaskRole.Arn
     ContainerDefinitions:
       - Name: app
         Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${Repository}
         PortMappings:
           - ContainerPort: 80
             HostPort: 80
             Protocol: tcp
         Environment:
           - Name: TZ
             Value: Asia/Tokyo
         LogConfiguration:
           LogDriver: awslogs
           Options:
             awslogs-region: !Ref 'AWS::Region'
             awslogs-group: !Ref LogGroup
             awslogs-stream-prefix: app
         Essential: true

 ServiceDiscovery:
   Type: AWS::ServiceDiscovery::Service
   Properties:
     HealthCheckCustomConfig:
       FailureThreshold: 1
     DnsConfig:
       DnsRecords:
         - Type: A
           TTL: 60
       NamespaceId: !GetAtt PrivateDnsNamespace.Id
     Name: !Ref ServiceDiscoveryName  # http通信で処理されるアプリケーションの場合、http://app.sample.local で呼び出すことができます。

 Service:
   Type: AWS::ECS::Service
   Properties:
     Cluster: !Ref Cluster
     DeploymentConfiguration:
       MaximumPercent: 200
       MinimumHealthyPercent: 100
     DesiredCount: !Ref DesiredCount
     LaunchType: FARGATE
     NetworkConfiguration:
       AwsvpcConfiguration:
         AssignPublicIp: DISABLED  # PublicSubnetを利用する場合はENABLEDにする必要があります。
         SecurityGroups:
           - !Ref SecurityGroup
         Subnets: !Ref ProtectedSubnets
     ServiceName: !Ref ProjectName
     ServiceRegistries:
       - RegistryArn: !GetAtt ServiceDiscovery.Arn
     TaskDefinition: !Ref TaskDefinition

Case3:スケジュールで起動するバッチ利用の場合

画像4

AWSTemplateFormatVersion: '2010-09-09'

Parameters:
 ProjectName:
   Type: String
   Default: 'sample3'
 VpcId:
   Type: AWS::EC2::VPC::Id
 ProtectedSubnets:
   Type: List<AWS::EC2::Subnet::Id>
   # Descriotion: 'NATゲートウェイがアタッチされたSubnet。なかったらPublicSubnetsと同じ値で。'
 AllowEcsPolicy:
   Type: List<String>
   Default: 's3:List*,s3:Get*,s3:Put*'
   # Descriotion: 'アプリケーションが必要なAWSリソースのポリシー'
 TaskCpu:
   Type: Number
   Default: 256
 TaskMemory:
   Type: Number
   Default: 512
 TaskCount:
   Type: Number
   Default: 1
   # Descriotion: 'スケジュールタスクの起動数'

Resources:
 # -------------------------------------
 # Fargate同士の通信を許可するSG
 # -------------------------------------
 SecurityGroup:
   Type: AWS::EC2::SecurityGroup
   Properties:
     Tags:
       - Key: Name
         Value: !Ref ProjectName
     GroupName: !Ref ProjectName
     GroupDescription: Security group for the service
     VpcId: !Ref VpcId

 SecurityGroupIngress:
   Type: AWS::EC2::SecurityGroupIngress
   Properties:
     GroupId: !Ref SecurityGroup
     IpProtocol: -1
     SourceSecurityGroupId: !Ref SecurityGroup

 Cluster:
   Type: AWS::ECS::Cluster
   Properties:
     ClusterName: !Ref ProjectName

 Repository:
   Type: AWS::ECR::Repository
   Properties:
     RepositoryName: !Ref ProjectName

 LogGroup:
   Type: AWS::Logs::LogGroup
   Properties:
     LogGroupName: !Sub /ecs/${ProjectName}

 # -------------------------------------
 # タスク起動時に必要なロールを定義
 # -------------------------------------
 EcsTaskExecutionRole:
   Type: AWS::IAM::Role
   Properties:
     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
     RoleName: !Sub ${ProjectName}-task-execution-role

 EcsTaskExecutionRolePolicy:
   Type: AWS::IAM::Policy
   Properties:
     PolicyName: !Sub ${ProjectName}-task-execution-role-policy
     PolicyDocument:
       Version: 2012-10-17
       Statement:
         - Effect: Allow
           Action:
             - ecr:GetLifecyclePolicyPreview
             - ecr:GetDownloadUrlForLayer
             - ecr:BatchGetImage
             - ecr:DescribeImages
             - ecr:ListTagsForResource
             - ecr:BatchCheckLayerAvailability
             - ecr:GetLifecyclePolicy
             - ecr:GetRepositoryPolicy
           Resource: !Sub "arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/*"
         - Effect: Allow
           Action:
             - ecr:GetAuthorizationToken
             - ssm:GetParameters
             - secretsmanager:GetSecretValue
           Resource:
             - !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/*"
     Roles:
       - Ref: EcsTaskExecutionRole

 # -------------------------------------
 # アプリケーションに必要なロールを定義
 # -------------------------------------
 EcsTaskRole:
   Type: AWS::IAM::Role
   Properties:
     AssumeRolePolicyDocument:
       Version: 2012-10-17
       Statement:
         - Effect: Allow
           Principal:
             Service:
               - ecs-tasks.amazonaws.com
               - events.amazonaws.com
           Action:
             - sts:AssumeRole
     ManagedPolicyArns:
       - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceEventsRole
     RoleName: !Sub ${ProjectName}-task-role

 EcsTaskRolePolicy:
   Type: AWS::IAM::Policy
   Properties:
     PolicyName: !Sub ${ProjectName}-task-role-policy
     PolicyDocument:
       Version: 2012-10-17
       Statement:
         - Effect: Allow
           Action: !Ref AllowEcsPolicy
           Resource: '*'
     Roles:
       - Ref: EcsTaskRole

 # --------------------------------------------------------------------------
 # 複数のスケジュールタスクを起動する場合は以下のリソースを複数作成する必要があります
 # --------------------------------------------------------------------------

 # -------------------------------------
 # Fargate タスク定義
 # -------------------------------------
 TaskDefinition:
   Type: AWS::ECS::TaskDefinition
   Properties:
     Family: !Ref ProjectName
     RequiresCompatibilities:
       - FARGATE
     Cpu: !Ref TaskCpu
     Memory: !Ref TaskMemory
     NetworkMode: awsvpc
     ExecutionRoleArn: !GetAtt EcsTaskExecutionRole.Arn
     TaskRoleArn: !GetAtt EcsTaskRole.Arn
     ContainerDefinitions:
       - Name: app
         Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${Repository}
         EntryPoint:
           - 'sh'
           - '-c'
         Environment:
           - Name: TZ
             Value: Asia/Tokyo
         LogConfiguration:
           LogDriver: awslogs
           Options:
             awslogs-region: !Ref 'AWS::Region'
             awslogs-group: !Ref LogGroup
             awslogs-stream-prefix: app
         Essential: true

 EventRule:
   Type: AWS::Events::Rule
   Properties:
     Name: !Ref ProjectName
     State: ENABLED
     ScheduleExpression: cron(0 9 * * ? *)  # UTCなので+9:00で記載する必要があるため、この設定は00:00で起動する意味になります。
     Targets:
       - Id: !Ref ProjectName
         Arn: !GetAtt Cluster.Arn
         RoleArn: !GetAtt EcsTaskExecutionRole.Arn
         EcsParameters:
           TaskDefinitionArn: !Ref TaskDefinition
           TaskCount: !Ref TaskCount
           LaunchType: FARGATE
           NetworkConfiguration:
             AwsVpcConfiguration:
               AssignPublicIp: DISABLED  # PublicSubnetを利用する場合はENABLEDにする必要があります。
               SecurityGroups:
                 - !Ref SecurityGroup
               Subnets: !Ref ProtectedSubnets
         Input: !Join [
           '',
           [
             "{",
             "   \"containerOverrides\": [ ",
             "     {",
             "       \"name\": \"app\", ",  # タスク定義のコンテナ名を指定する必要があります。
             "       \"command\": [\"echo hello\"]",
             "     }",
             "   ]",
             "}",
           ]
         ]

ECSにデプロイ

今回は下図で示したように開発環境で動かすためにビルドしたイメージを本番環境にデプロイするところまで書いて行こうと思います。

画像1

Fargateをデプロイするために必要なリソース群

AWSにDockerをデプロイするために必要な表面上のリソースです。こちらも実際にはもう少しリソースが必要になります。

・IAMロール(CodeBuildとCodePipelineでそれぞれ必要)
・CodeBuildプロジェクト
・CodePipeline​

前提

・ネットワークが構築済み(VPCやSubnet等)
・AWS::ECR::Repositoryが構築済み
・AWS::ECS::Serviceが構築済み
・Secrets Managerに以下の情報を登録済み(Valueが設定されている状態)

{
 "GitHubPersonalAccessToken": "",
 "GitHubUserName": "",
 "DockerHubID": "",
 "DockerHubPassword": ""
}

このテンプレートではECR Publicに対応していないためDockerのログイン情報を必要としています。詳しい理由はこちらをご覧ください。

開発環境

GitHubのmainブランチが変更されるとCodePipelineが実行されるように構築します。

AWSTemplateFormatVersion: '2010-09-09'

Parameters:
 ProjectName:
   Type: String
   Default: 'sample'
 VpcId:
   Type: AWS::EC2::VPC::Id
 ProtectedSubnets:
   Type: List<AWS::EC2::Subnet::Id>
   # Descriotion: 'NATゲートウェイがアタッチされたSubnet。'
 CICredentialArn:
   Type: String
   # Descriotion: SecretsManagerに登録した機密情報のARN
 AccountIdForProduction:
   Type: String
   # Description: 本番環境のAWSアカウントID
 EcrRepository:
   Type: String
 BuildDir:
   Type: String
   Default: '.'
 ContainerName:
   Type: String
   Default: 'app'
 ClusterName:
   Type: String
 ServiceName:
   Type: String
 GitHubOwner:
   Type: String
 GitHubRepository:
   Type: String
 GitHubBranch:
   Type: String
   Default: 'master'
   # Description: このブランチの変更をトリガーにCodePipelineが実行されます。
 ReleaseVersion:
   Type: String
   Default: 'v1.0'

Resources:
 ArtifactBucket:
   Type: AWS::S3::Bucket
   Properties:
     BucketName: !Sub ${AWS::AccountId}-arfifacts
     AccessControl: Private
     PublicAccessBlockConfiguration:
       BlockPublicAcls: True
       BlockPublicPolicy: True
       IgnorePublicAcls: True
       RestrictPublicBuckets: True

 # -------------------------------------
 # CodeBuildにアタッチされます。例えばCodeBuildでDBのマイグレーションを実行する場合に必要になります。
 # -------------------------------------
 SecurityGroup:
   Type: AWS::EC2::SecurityGroup
   Properties:
     Tags:
       - Key: Name
         Value: !Ref ProjectName
     GroupName: !Ref ProjectName
     GroupDescription: Security group for the service
     VpcId: !Ref VpcId

 SecurityGroupIngress:
   Type: AWS::EC2::SecurityGroupIngress
   Properties:
     GroupId: !Ref SecurityGroup
     IpProtocol: -1
     SourceSecurityGroupId: !Ref SecurityGroup

 CodePipelineRole:
   Type: AWS::IAM::Role
   Properties:
     AssumeRolePolicyDocument:
       Version: 2012-10-17
       Statement:
         - Effect: Allow
           Principal:
             Service:
               - codepipeline.amazonaws.com
           Action:
             - sts:AssumeRole
     ManagedPolicyArns:
       - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryFullAccess
       - arn:aws:iam::aws:policy/AWSCodeBuildAdminAccess
       - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess
       - arn:aws:iam::aws:policy/AmazonS3FullAccess
       - arn:aws:iam::aws:policy/CloudWatchFullAccess
       - arn:aws:iam::aws:policy/AmazonEC2ContainerServiceFullAccess
     RoleName: !Sub ${ProjectName}-codepipeline-role

 CodeBuildRole:
   Type: AWS::IAM::Role
   Properties:
     AssumeRolePolicyDocument:
       Version: 2012-10-17
       Statement:
         - Effect: Allow
           Principal:
             Service:
               - codebuild.amazonaws.com
               - events.amazonaws.com
           Action:
             - sts:AssumeRole
     ManagedPolicyArns:
       - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryFullAccess
       - arn:aws:iam::aws:policy/AWSCodeBuildAdminAccess
       - arn:aws:iam::aws:policy/AmazonS3FullAccess
       - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess
     RoleName: !Sub ${ProjectName}-codebuild-role

 CodeBuildRolePolicy:
   Type: AWS::IAM::Policy
   Properties:
     PolicyName: !Sub ${ProjectName}-codebuild-role-policy
     PolicyDocument:
       Version: 2012-10-17
       Statement:
         - Effect: Allow
           Action:
             - ecr:GetAuthorizationToken
             - ssm:GetParameters
             - secretsmanager:GetSecretValue
           Resource:
             - '*'
         - Effect: Allow
           Action:
             - sts:AssumeRole
           Resource:
             - '*'
         - Effect: Allow
           Action:
             - ec2:CreateNetworkInterface
             - ec2:DescribeDhcpOptions
             - ec2:DescribeNetworkInterfaces
             - ec2:DeleteNetworkInterface
             - ec2:DescribeSubnets
             - ec2:DescribeSecurityGroups
             - ec2:DescribeVpcs
           Resource:
             - '*'
         - Effect: Allow
           Action:
             - '*'
           Resource:
             - !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:network-interface/*"
     Roles:
       - Ref: CodeBuildRole

 AppBuildProject:
   Type: AWS::CodeBuild::Project
   Properties:
     Name: !Sub ${ProjectName}-build
     Artifacts:
       Type: CODEPIPELINE
     Description: !Sub Building stage for ${ProjectName}
     Environment:
       ComputeType: BUILD_GENERAL1_LARGE
       Image: aws/codebuild/standard:4.0
       Type: LINUX_CONTAINER
       PrivilegedMode: True
       EnvironmentVariables:
         - Name: REPOSITORY_URI
           Value: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${EcrRepository}"
         - Name: CONTAINER_NAME
           Value: !Ref ContainerName
         - Name: BUILD_DIR
           Value: !Ref BuildDir
     ServiceRole: !Ref CodeBuildRole
     Source:
       Type: CODEPIPELINE
       BuildSpec:
         !Join [
           "\n",
           [
             "version: 0.2",
             "",
             "env:",
             "  variables:",
             "    DOCKER_BUILDKIT: \"1\"",
             "  secrets-manager:",
       !Sub  "    GITHUB_TOKEN: \"${CICredentialArn}:GitHubPersonalAccessToken\"",
       !Sub  "    DOCKERHUB_ID: \"${CICredentialArn}:DockerHubID\"",
       !Sub  "    DOCKERHUB_PASSWORD: \"${CICredentialArn}:DockerHubPassword\"",
             "",
             "phases:",
             "  pre_build:",
             "    commands:",
             "      - echo Logging in to DockerHub...",
             "      - docker login -u ${DOCKERHUB_ID} -p ${DOCKERHUB_PASSWORD}",
             "      - echo Logging in to Amazon ECR...",
             "      - $(aws ecr get-login --no-include-email --region $AWS_DEFAULT_REGION)",
             "      - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)",
             "      - IMAGE_TAG=${COMMIT_HASH:=latest}",
             "  build:",
             "    commands:",
             "      - echo Build started on `date`",
             "      - docker build -t $REPOSITORY_URI:latest $BUILD_DIR --build-arg GITHUB_TOKEN=$GITHUB_TOKEN",
             "      - docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG",
             "  post_build:",
             "    commands:",
             "      - echo Build completed on `date`",
             "      - docker push $REPOSITORY_URI:latest",
             "      - docker push $REPOSITORY_URI:$IMAGE_TAG",
             "      - echo \"[{\\\"name\\\":\\\"${CONTAINER_NAME}\\\",\\\"imageUri\\\":\\\"${REPOSITORY_URI}:${IMAGE_TAG}\\\"}]\" > imagedefinitions.json",
             "artifacts:",
             "  files:",
             "    - imagedefinitions.json",
             ""
           ]
         ]
     # VpcConfig:
     #   VpcId: !Ref VpcId
     #   Subnets: !Ref ProtectedSubnets
     #   SecurityGroupIds:
     #     - !Ref SecurityGroup
     TimeoutInMinutes: 30
     Cache:
       Type: LOCAL  # 有効時間が短いためこの効果に過度な期待をしないでください。
       Modes:
         - LOCAL_DOCKER_LAYER_CACHE

 TaggingProject:
   Type: AWS::CodeBuild::Project
   Properties:
     Name: !Sub ${ProjectName}-tagging
     Artifacts:
       Type: CODEPIPELINE
     Description: !Sub Tagging stage for ${ProjectName}
     Environment:
       ComputeType: BUILD_GENERAL1_SMALL
       Image: aws/codebuild/standard:4.0
       Type: LINUX_CONTAINER
       PrivilegedMode: True
       EnvironmentVariables:
         - Name: GITHUB_OWNER
           Value: !Ref GitHubOwner
         - Name: GITHUB_PROJECT
           Value: !Ref GitHubRepository
         - Name: RELEASE_VERSION
           Value: !Ref ReleaseVersion
     ServiceRole: !Ref CodeBuildRole
     Source:
       Type: CODEPIPELINE
       BuildSpec: !Join [
         "\n",
         [
           "version: 0.2",
           "",
           "env:",
           "  variables:",
           "    DOCKER_BUILDKIT: \"1\"",
           "  secrets-manager:",
     !Sub  "    GITHUB_TOKEN: \"${CICredentialArn}:GitHubPersonalAccessToken\"",
     !Sub  "    DOCKERHUB_ID: \"${CICredentialArn}:DockerHubID\"",
     !Sub  "    DOCKERHUB_PASSWORD: \"${CICredentialArn}:DockerHubPassword\"",
           "",
           "phases:",
           "  pre_build:",
           "    commands:",
           "      - echo Logging in to DockerHub...",
           "      - docker login -u ${DOCKERHUB_ID} -p ${DOCKERHUB_PASSWORD}",
           "      - tag_name=\"${RELEASE_VERSION}\"",
           "      - description=''",
           "      - apt-get install -y tzdata",
           "      - ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime",
           "      - released=`date '+%Y%m%d%H%M'`",
           "  build:",
           "    commands:",
           "      - authorization=\"Authorization:token $GITHUB_TOKEN\"",
           "      - content_type=\"Content-Type:application/json\"",
           "      - release_tag=$tag_name.$released",
           "      - release_name=$tag_name.$released",
           "      - params=\"{\\\"tag_name\\\":\\\"$release_tag\\\",\\\"target_commitish\\\":\\\"$CODEBUILD_RESOLVED_SOURCE_VERSION\\\",\\\"name\\\":\\\"$release_name\\\",\\\"body\\\":\\\"$description\\\",\\\"draft\\\":false,\\\"prerelease\\\":false}\"",
           "  post_build:",
           "    commands:",
           "      - curl -X POST -H \"$authorization\" -H \"$content_type\" -d \"$params\" https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_PROJECT}/releases",
           "",
         ]
       ]
     # VpcConfig:
     #   VpcId: !Ref VpcId
     #   Subnets: !Ref ProtectedSubnets
     #   SecurityGroupIds:
     #     - !Ref SecurityGroup
     TimeoutInMinutes: 30

 ReleaseProject:
   Type: AWS::CodeBuild::Project
   Properties:
     Name: !Sub ${ProjectName}-release
     Artifacts:
       Type: CODEPIPELINE
     Description: !Sub Release stage for ${ProjectName}
     Environment:
       ComputeType: BUILD_GENERAL1_SMALL
       Image: aws/codebuild/standard:4.0
       Type: LINUX_CONTAINER
       PrivilegedMode: True
       EnvironmentVariables:
         - Name: DEV_APP_CONTAINER_IMAGE_URI
           Value: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${EcrRepository}"
         - Name: PRD_APP_CONTAINER_IMAGE_URI
           Value: !Sub "${AccountIdForProduction}.dkr.ecr.${AWS::Region}.amazonaws.com/${EcrRepository}"
         - Name: GITHUB_OWNER
           Value: !Ref GitHubOwner
         - Name: GITHUB_PROJECT
           Value: !Ref GitHubRepository
         - Name: RELEASE_VERSION
           Value: !Ref ReleaseVersion
         - Name: PRD_ACCOUNT_ID
           Value: !Ref AccountIdForProduction
         - Name: ASSUME_ROLE_ARN
           Value: !Sub "arn:aws:iam::${AccountIdForProduction}:role/${ProjectName}-release-role"  # 本番環境の構築時に作成します。
     ServiceRole: !Ref CodeBuildRole
     Source:
       Type: CODEPIPELINE
       BuildSpec: !Join [
         "\n",
         [
           "version: 0.2",
           "",
           "env:",
           "  variables:",
           "    DOCKER_BUILDKIT: \"1\"",
           "  secrets-manager:",
     !Sub  "    GITHUB_TOKEN: \"${CICredentialArn}:GitHubPersonalAccessToken\"",
     !Sub  "    DOCKERHUB_ID: \"${CICredentialArn}:DockerHubID\"",
     !Sub  "    DOCKERHUB_PASSWORD: \"${CICredentialArn}:DockerHubPassword\"",
           "",
           "phases:",
           "  pre_build:",
           "    commands:",
           "      - echo Logging in to DockerHub...",
           "      - docker login -u ${DOCKERHUB_ID} -p ${DOCKERHUB_PASSWORD}",
           "      - echo Logging in to Amazon ECR...",
           "      - aws --version",
           "      - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)",
           "      - IMAGE_TAG=${COMMIT_HASH:=latest}",
           "      - RELEASE_TAG=`curl -X GET -H \"Authorization:token ${GITHUB_TOKEN}\" https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_PROJECT}/releases | jq -r '. | select(.[0].tag_name | startswith(\"'$RELEASE_VERSION'\"))' | jq -r .[0].tag_name`",
           "      - if [ -z $RELEASE_TAG ]; then RELEASE_TAG=$RELEASE_VERSION; fi",
           "      - echo $RELEASE_TAG",
           "  build:",
           "    commands:",
           "      - echo Build started on `date`",
           "      - $(aws ecr get-login --no-include-email --region $AWS_DEFAULT_REGION)",
           "      - docker pull $DEV_APP_CONTAINER_IMAGE_URI:$IMAGE_TAG",
           "      - docker tag  $DEV_APP_CONTAINER_IMAGE_URI:$IMAGE_TAG $DEV_APP_CONTAINER_IMAGE_URI:$RELEASE_VERSION",
           "      - docker tag  $DEV_APP_CONTAINER_IMAGE_URI:$IMAGE_TAG $DEV_APP_CONTAINER_IMAGE_URI:$RELEASE_TAG",
           "      - docker push $DEV_APP_CONTAINER_IMAGE_URI:$RELEASE_VERSION",
           "      - docker push $DEV_APP_CONTAINER_IMAGE_URI:$RELEASE_TAG",
           "      - docker tag  $DEV_APP_CONTAINER_IMAGE_URI:$IMAGE_TAG $PRD_APP_CONTAINER_IMAGE_URI:$RELEASE_VERSION",
           "      - docker tag  $DEV_APP_CONTAINER_IMAGE_URI:$IMAGE_TAG $PRD_APP_CONTAINER_IMAGE_URI:$RELEASE_TAG",
           "      - docker tag  $DEV_APP_CONTAINER_IMAGE_URI:$IMAGE_TAG $PRD_APP_CONTAINER_IMAGE_URI:latest",
           "  post_build:",
           "    commands:",
           "      - echo Pushing the Docker images...",
           "      - mkdir -p credentials",
           "      - echo \"[profile production]\" > credentials/config",
           "      - echo \"role_arn = ${ASSUME_ROLE_ARN}\" >> credentials/config",
           "      - echo \"credential_source = EcsContainer\" >> credentials/config",
           "      - export AWS_CONFIG_FILE=${CODEBUILD_SRC_DIR}/credentials/config",
           "      - aws sts get-caller-identity --profile production",
           "      - $(aws ecr get-login --registry-ids ${PRD_ACCOUNT_ID} --no-include-email --region $AWS_DEFAULT_REGION --profile production)",
           "      - docker push $PRD_APP_CONTAINER_IMAGE_URI:$RELEASE_VERSION",
           "      - docker push $PRD_APP_CONTAINER_IMAGE_URI:$RELEASE_TAG",
           "      - docker push $PRD_APP_CONTAINER_IMAGE_URI:latest",
           "",
         ]
       ]
     # VpcConfig:
     #   VpcId: !Ref VpcId
     #   Subnets: !Ref ProtectedSubnets
     #   SecurityGroupIds:
     #     - !Ref SecurityGroup
     TimeoutInMinutes: 30

 CodePipeline:
   Type: AWS::CodePipeline::Pipeline
   Properties:
     ArtifactStore:
       Location: !Ref ArtifactBucket
       Type: S3
     Name: !Ref ProjectName
     RestartExecutionOnUpdate: false
     RoleArn: !GetAtt CodePipelineRole.Arn
     Stages:
       - Name: Source
         Actions:
           - Name: SourceCode
             ActionTypeId:
               Category: Source
               Owner: ThirdParty
               Version: 1
               Provider: GitHub
             Configuration:
               Owner: !Ref GitHubOwner
               Repo: !Ref GitHubRepository
               Branch: !Ref GitHubBranch
               OAuthToken: !Sub '{{resolve:secretsmanager:${CICredentialArn}:SecretString:GitHubPersonalAccessToken}}'
             OutputArtifacts:
               - Name: SourceCode
             RunOrder: 1
       - Name: Build
         Actions:
           - Name: CodeBuild
             InputArtifacts:
               - Name: SourceCode
             ActionTypeId:
               Category: Build
               Owner: AWS
               Provider: CodeBuild
               Version: 1
             Configuration:
               ProjectName: !Ref AppBuildProject
             OutputArtifacts:
               - Name: BuildImage
             RunOrder: 1
       - Name: Deploy
         Actions:
           - Name: Deploy
             InputArtifacts:
               - Name: BuildImage
             ActionTypeId:
               Category: Deploy
               Owner: AWS
               Provider: ECS
               Version: 1
             Configuration:
               ClusterName: !Ref ClusterName
               FileName: imagedefinitions.json
               ServiceName: !Ref ServiceName
             OutputArtifacts: []
             RunOrder: 1
       - Name: Approval
         Actions:
           - Name: Approval
             ActionTypeId:
               Category: Approval
               Owner: AWS
               Version: 1
               Provider: Manual
             RunOrder: 1
       - Name: GitHubTagging
         Actions:
           - Name: CodeBuild
             InputArtifacts:
               - Name: SourceCode
             ActionTypeId:
               Category: Build
               Owner: AWS
               Provider: CodeBuild
               Version: 1
             Configuration:
               ProjectName: !Ref TaggingProject
             RunOrder: 1
       - Name: Release
         Actions:
           - Name: CodeBuild
             InputArtifacts:
               - Name: SourceCode
             ActionTypeId:
               Category: Build
               Owner: AWS
               Provider: CodeBuild
               Version: 1
             Configuration:
               ProjectName: !Ref ReleaseProject
             RunOrder: 1

CodeBuildからVPC内のリソースにアクセスしたい場合など、CodeBuildをVPC内に配置する際はVpcConfigのコメントアウトを外す必要があります。ただしNATゲートウェイをアタッチしたSubnetが必要です。

本番環境

開発環境のCodeBuildから本番環境のECRにPushされたことをトリガーにCodePipelineが実行されるように構築します。

AWSTemplateFormatVersion: '2010-09-09'

Parameters:
 ProjectName:
   Type: String
   Default: 'sample'
 VpcId:
   Type: AWS::EC2::VPC::Id
 ProtectedSubnets:
   Type: List<AWS::EC2::Subnet::Id>
   # Descriotion: 'NATゲートウェイがアタッチされたSubnet。'
 CICredentialArn:
   Type: String
   # Descriotion: SecretsManagerに登録した機密情報のARN
 AccountIdForDevelopment:
   Type: String
   # Description: 開発環境のAWSアカウントID
 EcrRepository:
   Type: String
 ContainerName:
   Type: String
   Default: 'app'
 ClusterName:
   Type: String
 ServiceName:
   Type: String
 GitHubOwner:
   Type: String
 GitHubRepository:
   Type: String
 ReleaseVersion:
   Type: String
   Default: 'v1.0'

Resources:
 ArtifactBucket:
   Type: AWS::S3::Bucket
   Properties:
     BucketName: !Sub ${AWS::AccountId}-arfifacts
     AccessControl: Private
     PublicAccessBlockConfiguration:
       BlockPublicAcls: True
       BlockPublicPolicy: True
       IgnorePublicAcls: True
       RestrictPublicBuckets: True

 # -------------------------------------
 # CodeBuildにアタッチされます。例えばCodeBuildでDBのマイグレーションを実行する場合に必要になります。
 # -------------------------------------
 SecurityGroup:
   Type: AWS::EC2::SecurityGroup
   Properties:
     Tags:
       - Key: Name
         Value: !Ref ProjectName
     GroupName: !Ref ProjectName
     GroupDescription: Security group for the service
     VpcId: !Ref VpcId

 SecurityGroupIngress:
   Type: AWS::EC2::SecurityGroupIngress
   Properties:
     GroupId: !Ref SecurityGroup
     IpProtocol: -1
     SourceSecurityGroupId: !Ref SecurityGroup

 # -------------------------------------
 # 開発環境からイメージをPushする際に利用されます。
 # -------------------------------------
 ReleaseRole:
   Type: AWS::IAM::Role
   Properties:
     AssumeRolePolicyDocument:
       Version: 2012-10-17
       Statement:
         - Effect: Allow
           Principal:
             AWS:
               - !Sub "arn:aws:iam::${AccountIdForDevelopment}:root"
           Action:
             - sts:AssumeRole
     ManagedPolicyArns:
       - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryFullAccess
     RoleName: !Sub ${ProjectName}-release-role

 CodePipelineRole:
   Type: AWS::IAM::Role
   Properties:
     AssumeRolePolicyDocument:
       Version: 2012-10-17
       Statement:
         - Effect: Allow
           Principal:
             Service:
               - codepipeline.amazonaws.com
           Action:
             - sts:AssumeRole
     ManagedPolicyArns:
       - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryFullAccess
       - arn:aws:iam::aws:policy/AWSCodeBuildAdminAccess
       - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess
       - arn:aws:iam::aws:policy/AmazonS3FullAccess
       - arn:aws:iam::aws:policy/CloudWatchFullAccess
       - arn:aws:iam::aws:policy/AmazonEC2ContainerServiceFullAccess
     RoleName: !Sub ${ProjectName}-codepipeline-role

 CodeBuildRole:
   Type: AWS::IAM::Role
   Properties:
     AssumeRolePolicyDocument:
       Version: 2012-10-17
       Statement:
         - Effect: Allow
           Principal:
             Service:
               - codebuild.amazonaws.com
               - events.amazonaws.com
           Action:
             - sts:AssumeRole
     ManagedPolicyArns:
       - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryFullAccess
       - arn:aws:iam::aws:policy/AWSCodeBuildAdminAccess
       - arn:aws:iam::aws:policy/AmazonS3FullAccess
       - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess
     RoleName: !Sub ${ProjectName}-codebuild-role

 CodeBuildRolePolicy:
   Type: AWS::IAM::Policy
   Properties:
     PolicyName: !Sub ${ProjectName}-codebuild-role-policy
     PolicyDocument:
       Version: 2012-10-17
       Statement:
         - Effect: Allow
           Action:
             - ecr:GetAuthorizationToken
             - ssm:GetParameters
             - secretsmanager:GetSecretValue
           Resource:
             - '*'
         - Effect: Allow
           Action:
             - sts:AssumeRole
           Resource:
             - '*'
         - Effect: Allow
           Action:
             - ec2:CreateNetworkInterface
             - ec2:DescribeDhcpOptions
             - ec2:DescribeNetworkInterfaces
             - ec2:DeleteNetworkInterface
             - ec2:DescribeSubnets
             - ec2:DescribeSecurityGroups
             - ec2:DescribeVpcs
           Resource:
             - '*'
         - Effect: Allow
           Action:
             - '*'
           Resource:
             - !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:network-interface/*"
     Roles:
       - Ref: CodeBuildRole

 # -------------------------------------
 # 開発環境からイメージをPushする際に利用されます。
 # -------------------------------------
 CodePipelineEventRole:
   Type: AWS::IAM::Role
   Properties:
     AssumeRolePolicyDocument:
       Version: 2012-10-17
       Statement:
         - Effect: Allow
           Principal:
             Service:
               - events.amazonaws.com
           Action: sts:AssumeRole
     Path: /
     Policies:
       - PolicyName: !Sub ${ProjectName}-cloudwatch-event
         PolicyDocument:
           Version: 2012-10-17
           Statement:
             - Effect: Allow
               Action: codepipeline:StartPipelineExecution
               Resource: !Sub "arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${ProjectName}"
     RoleName: !Sub ${ProjectName}-codepipeline-execution-role

 AppBuildProject:
   Type: AWS::CodeBuild::Project
   Properties:
     Name: !Sub ${ProjectName}-build
     Artifacts:
       Type: CODEPIPELINE
     Description: !Sub Building stage for ${ProjectName}
     Environment:
       ComputeType: BUILD_GENERAL1_SMALL
       Image: aws/codebuild/standard:4.0
       Type: LINUX_CONTAINER
       PrivilegedMode: True
       EnvironmentVariables:
         - Name: REPOSITORY_URI
           Value: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${EcrRepository}"
         - Name: CONTAINER_NAME
           Value: !Ref ContainerName
         - Name: RELEASE_VERSION
           Value: !Ref ReleaseVersion
         - Name: GITHUB_OWNER
           Value: !Ref GitHubOwner
         - Name: GITHUB_PROJECT
           Value: !Ref GitHubRepository
     ServiceRole: {'Fn::ImportValue': !Join [':', [!Ref ProjectName, CodeBuildRole]]}
     Source:
       Type: CODEPIPELINE
       BuildSpec: !Join [
         "\n",
         [
           "version: 0.2",
           "",
           "env:",
           "  variables:",
           "    DOCKER_BUILDKIT: \"1\"",
           "  secrets-manager:",
     !Sub  "    GITHUB_TOKEN: \"${CICredentialArn}:GitHubPersonalAccessToken\"",
     !Sub  "    DOCKERHUB_ID: \"${CICredentialArn}:DockerHubID\"",
     !Sub  "    DOCKERHUB_PASSWORD: \"${CICredentialArn}:DockerHubPassword\"",
           "",
           "phases:",
           "  pre_build:",
           "    commands:",
           "      - echo Logging in to Amazon ECR...",
           "      - aws --version",
           "      - $(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email)",
           "      - IMAGE_TAG=`curl -X GET -H \"Authorization:token ${GITHUB_TOKEN}\" https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_PROJECT}/releases | jq -r '. | select(.[0].tag_name | startswith(\"'$RELEASE_VERSION'\"))' | jq -r .[0].tag_name`",
           "  post_build:",
           "    commands:",
           "      - echo Writing image definitions file...",
           "      - echo \"[{\\\"name\\\":\\\"${CONTAINER_NAME}\\\",\\\"imageUri\\\":\\\"${REPOSITORY_URI}:${IMAGE_TAG}\\\"}]\" > imagedefinitions.json",
           "artifacts:",
           "  files: imagedefinitions.json",
           ""
         ]
       ]
     # VpcConfig:
     #   VpcId: !Ref VpcId
     #   Subnets: !Ref ProtectedSubnets
     #   SecurityGroupIds:
     #     - !Ref SecurityGroup
     TimeoutInMinutes: 30
     Cache:
       Type: LOCAL
       Modes:
         - LOCAL_DOCKER_LAYER_CACHE

 # -------------------------------------
 # ECRへのPushイベントを拾います。
 # -------------------------------------
 CodePipelineEventRule:
   Type: AWS::Events::Rule
   Properties:
     EventPattern:
       source:
         - aws.ecr
       detail:
         eventName:
           - PutImage
         requestParameters:
           repositoryName:
             - !Ref EcrRepository
           imageTag:
             - !Ref ReleaseVersion
     Targets:
       - Arn: !Sub "arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${CodePipeline}"
         RoleArn: !GetAtt CodePipelineEventRole.Arn
         Id: !Ref ProjectName
     Name: !Ref ProjectName

 CodePipeline:
   Type: AWS::CodePipeline::Pipeline
   Properties:
     ArtifactStore:
       Location: !Ref ArtifactBucket
       Type: S3
     Name: !Ref ProjectName
     RestartExecutionOnUpdate: false
     RoleArn: !GetAtt CodePipelineRole.Arn
     Stages:
       - Name: Source
         Actions:
         - Name: SourceImage
           ActionTypeId:
             Category: Source
             Owner: AWS
             Version: 1
             Provider: ECR
           Configuration:
             ImageTag: !Ref ReleaseVersion
             RepositoryName: !Ref EcrRepository
           OutputArtifacts:
             - Name: SourceImage
           RunOrder: 1
       - Name: Build
         Actions:
           - Name: CodeBuild
             InputArtifacts:
               - Name: SourceImage
             ActionTypeId:
               Category: Build
               Owner: AWS
               Provider: CodeBuild
               Version: 1
             Configuration:
               ProjectName: !Ref AppBuildProject
             OutputArtifacts:
               - Name: BuildImage
             RunOrder: 1
       - Name: Deploy
         Actions:
           - Name: Deploy
             InputArtifacts:
               - Name: BuildImage
             ActionTypeId:
               Category: Deploy
               Owner: AWS
               Provider: ECS
               Version: 1
             Configuration:
               ClusterName: !Ref ClusterName
               FileName: imagedefinitions.json
               ServiceName: !Ref ServiceName
             OutputArtifacts: []
             RunOrder: 1

おわりに

いかがだったでしょうか?ECSでサービスを構築するさいのお役に立てればと思います。

23日目は「2020年秋、ビルドトラップに浸かりました。」です。次回もよろしくお願いいたします。