目次

Introduction

今回作成するもの

AWSリソース

CFn相関図

HandsOn

ECR

アプリケーションをコンテナで動かすためにアプリケーションをイメージ化した物を保存しておく格納場所が必要です。そのためにECS専用のレジストリサービスであるECRを作成します。

.pre-commit-config.yaml
cloudformation
 L iam-user.yaml
   iam-role.yaml
   cloudtrail.yaml
   vpc.yaml
   subnet.yaml
   sg.yaml
   route53.yaml
   acm.yaml
   s3-bucket.yaml
   secretsmanager.yaml
   prefix
    L alb.yaml
      cf.yaml
      ecs-cluster.yaml
      ecs-task-def.yaml
      ecs-service.yaml
      cw-logs.yaml
      ecr.yaml(new)
AWSTemplateFormatVersion: "2010-09-09"
Description: "stackName: {Prefix}-{Env}-ecr. AWS::ECR::Repository"

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Environment Name Configuration
        Parameters:
          - Prefix
          - Env
          - Project
Parameters:
  Prefix:
    Type: String
    Description: AWS resource version management prefix
    Default: v1
  Env:
    Type: String
    Description: Project environment
    AllowedValues: [dev, prd]
  Project:
    Type: String
    Description: Project Name
    Default: example

Mappings:
  EnvMaps:
    common:
      ECRLifecyclePolicy: |
        {
          "rules": [
            {
              "rulePriority": 1,
              "description": "Delete more than 10 images",
              "selection": {
                "tagStatus": "any",
                "countType": "imageCountMoreThan",
                "countNumber": 10
              },
              "action": {
                "type": "expire"
              }
            }
          ]
        }        

Resources:
  ECR:
    Type : "AWS::ECR::Repository"
    Properties:
      RepositoryName: !Sub "${Prefix}/${Env}/${Project}"
      LifecyclePolicy:
        LifecyclePolicyText: !FindInMap [EnvMaps, common, ECRLifecyclePolicy]

Outputs:
  ECRUri:
    Description: Output Export AWS::ECS::Cluster.RepositoryUri
    Value: !GetAtt ECR.RepositoryUri
    Export:
      Name: !Sub ${Prefix}-${Env}-${Project}-ecr-uri

スタック名は、v1-dev-ecr
ECRライフサイクルポリシーを設定しています。10イメージだけ残して古い順に削除されるように設定しています。

CloudWatchLogs

次にCodePipeline中のCodeBuildの実行ログを保存するCloudWatchLogsを作成します

.pre-commit-config.yaml
cloudformation
 L iam-user.yaml
   iam-role.yaml
   cloudtrail.yaml
   vpc.yaml
   subnet.yaml
   sg.yaml
   route53.yaml
   acm.yaml
   s3-bucket.yaml
   secretsmanager.yaml
   prefix
    L alb.yaml
      cf.yaml
      ecs-cluster.yaml
      ecs-task-def.yaml
      ecs-service.yaml
      cw-logs.yaml(modify)
      ecr.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: "stackName: {Prefix}-cw-logs. AWS::Logs::LogGroup"

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "Environment Name Configuration"
        Parameters:
          - Prefix
          - Env
          - Project

Parameters:
  Prefix:
    Type: String
    Description: AWS resource version management prefix
    Default: v1
  Env:
    Type: String
    Description: Project environment
    AllowedValues: [dev, prd]
  Project:
    Type: String
    Description: Project Name
    Default: example

Resources:
  ECSCWLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub ecs/${Prefix}-${Env}-${Project}
      RetentionInDays: 7

  DeployCodeBuildCwLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub codebuild/${Prefix}-${Env}-${Project}
      RetentionInDays: 7

Outputs:
  ECSCWLogGroupName:
    Description:  Output Export AWS::Logs::LogGroup.Ref ECSCWLogGroup
    Value: !Ref ECSCWLogGroup
    Export:
      Name: !Sub ${Prefix}-${Env}-${Project}-ecs-cw-loggroup-name
  DeployCodeBuildCwLogGroupName:
    Description:  Output Export AWS::Logs::LogGroup.Ref DeployCodeBuildCwLogGroup
    Value: !Ref DeployCodeBuildCwLogGroup
    Export:
      Name: !Sub ${Prefix}-${Env}-${Project}-deploy-codebuild-cw-loggroup-name

Artifact用のS3

CodePipelineで使用されるアーティファクトを保存するS3バケットを作成します。

.pre-commit-config.yaml
cloudformation
 L iam-user.yaml
   iam-role.yaml
   cloudtrail.yaml
   vpc.yaml
   subnet.yaml
   sg.yaml
   route53.yaml
   acm.yaml
   s3-bucket.yaml(modify)
   secretsmanager.yaml
   prefix
    L alb.yaml
      cf.yaml
      ecs-cluster.yaml
      ecs-task-def.yaml
      ecs-service.yaml
      cw-logs.yaml
      ecr.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: "stackName: s3-bucket. AWS::S3::Bucket"

Parameters:
  Project:
    Type: String
    Description: Project Name
    Default: example

Mappings:
  EnvMaps:
    Common:
      ApNortheast1ElbAccountId: "582318560864" # https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/application/enable-access-logging.html

Resources:
  S3LogsS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub ${Project}-s3-logs
      AccessControl: LogDeliveryWrite
      OwnershipControls:
        Rules:
          - ObjectOwnership: ObjectWriter
      VersioningConfiguration:
        Status: Suspended

  AwsResourceAccessLogsS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub ${Project}-aws-resource-access-logs
      AccessControl: Private
      OwnershipControls:
        Rules:
          - ObjectOwnership: ObjectWriter
      VersioningConfiguration:
        Status: Suspended
      LifecycleConfiguration:
        Rules:
          - Id: ExpireAfter90Days
            Status: Enabled
            ExpirationInDays: 90
      LoggingConfiguration:
        DestinationBucketName: !Ref S3LogsS3Bucket
        LogFilePrefix: !Sub ${Project}-aws-resource-access-logs/
  AwsResourceAccessLogsS3BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref AwsResourceAccessLogsS3Bucket
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: Policy4ALB
            Effect: Allow
            Principal:
              AWS: !Sub
                - arn:aws:iam::${ElbAccountId}:root
                - ElbAccountId: !FindInMap [EnvMaps, Common, ApNortheast1ElbAccountId]
            Action:
              - s3:PutObject
            Resource: !Sub
              - ${BucketArn}/elb/*
              - BucketArn: !GetAtt AwsResourceAccessLogsS3Bucket.Arn

  CodePipelineArtifactsS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub ${Project}-codepipeline-artifacts
      AccessControl: Private
      VersioningConfiguration:
        Status: Suspended
      LifecycleConfiguration:
        Rules:
          - Id: ExpireAfter30Days
            Status: Enabled
            ExpirationInDays: 30
      LoggingConfiguration:
        DestinationBucketName: !Ref S3LogsS3Bucket
        LogFilePrefix: !Sub ${Project}-codepipeline-artifacts/

Outputs:
  AwsResourceAccessLogsS3BucketName:
    Description: Output Export AWS::S3::Bucket.Ref AwsResourceAccessLogsS3Bucket
    Value: !Ref AwsResourceAccessLogsS3Bucket
    Export:
      Name: !Sub ${Project}-aws-resource-access-logs-s3-bucket-name
  CodePipelineArtifactsS3BucketName:
    Description: Output Export AWS::S3::Bucket.Ref CodePipelineArtifactsS3Bucket
    Value: !Ref CodePipelineArtifactsS3Bucket
    Export:
      Name: !Sub ${Project}-codepipeline-artifacts-s3-bucket-name
  AwsResourceAccessLogsS3BucketArn:
    Description: Output Export AWS::S3::Bucket.Arn AwsResourceAccessLogsS3Bucket
    Value: !GetAtt AwsResourceAccessLogsS3Bucket.Arn
    Export:
      Name: !Sub ${Project}-aws-resource-access-logs-s3-bucket-arn
  CodePipelineArtifactsS3BucketArn:
    Description: Output Export AWS::S3::Bucket.Arn CodePipelineArtifactsS3Bucket
    Value: !GetAtt CodePipelineArtifactsS3Bucket.Arn
    Export:
      Name: !Sub ${Project}-codepipeline-artifacts-s3-bucket-arn
  AwsResourceAccessLogsS3BucketDomainName:
    Description: Output Export AWS::S3::Bucket.DomainName AwsResourceAccessLogsS3Bucket
    Value: !GetAtt AwsResourceAccessLogsS3Bucket.DomainName
    Export:
      Name: !Sub ${Project}-aws-resource-access-logs-s3-bucket-domain-name

Outputsも忘れないように記載してください。バケットの中のオブジェクトの保存期限は少なめに30日にしました。デプロイ時に出力されたデータはあまり長期間見ることはないと思うので。

CodePipelineのIAM Role

CodePipelineのIAMロールとCodePipeline構築の中で使用するCodeBuildのIAMロールを両方作成します。

.pre-commit-config.yaml
cloudformation
 L iam-user.yaml
   iam-role.yaml(modify)
   cloudtrail.yaml
   vpc.yaml
   subnet.yaml
   sg.yaml
   route53.yaml
   acm.yaml
   s3-bucket.yaml
   secretsmanager.yaml
   prefix
    L alb.yaml
      cf.yaml
      ecs-cluster.yaml
      ecs-task-def.yaml
      ecs-service.yaml
      cw-logs.yaml
      ecr.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: "stackName: iam-role. AWS::IAM::Role AWS::IAM::InstanceProfile"

Parameters:
  Project:
    Type: String
    Description: Project Name
    Default: example

Resources:
  CloudTrailRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: cloudtrail-role
      Path: /service-role/
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - cloudtrail.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: cloudtrail-policy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Sid: CloudWatchPutPolicy
                Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*

  ECSInstanceProfileRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: ecs-instance-profile-role
      Path: /
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
        - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role

  ECSInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: /
      Roles:
        - !Ref ECSInstanceProfileRole

  TaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: task-execution-role
      Path: /service-role/
      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

  ECSCodeBuildServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: ecs-codebuild-service-role
      Path: /service-role/
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: codebuild.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: ecs-codebuild-service-policy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Sid: S3AccessPolicy
                Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:PutObject
                  - s3:GetObjectVersion
                Resource:
                  - !Sub
                    - ${ArtifactBucket}/*
                    - ArtifactBucket:
                        Fn::ImportValue: !Sub ${Project}-codepipeline-artifacts-s3-bucket-arn
              - Sid: ECRAccessPolicy
                Effect: Allow
                Action:
                  - ecr:BatchCheckLayerAvailability
                  - ecr:BatchGetImage
                  - ecr:CompleteLayerUpload
                  - ecr:GetAuthorizationToken
                  - ecr:InitiateLayerUpload
                  - ecr:PutImage
                  - ecr:UploadLayerPart
                  - ecr:CreateRepository
                  - ecr:GetDownloadUrlForLayer
                Resource:
                  - !Sub arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/*
              - Sid: ECRAuthPolicy
                Effect: Allow
                Action:
                  - ecr:GetAuthorizationToken
                Resource: "*"
              - Sid: SSMGetParameterPolicy
                Effect: Allow
                Action:
                  - ssm:GetParameters
                Resource: !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/*
              - Sid: KmsDecryptPolicy
                Effect: Allow
                Action:
                  - kms:Decrypt
                Resource: !Sub arn:aws:kms:${AWS::Region}:${AWS::AccountId}:key/*
              - Sid: SSMCodeBuildAccessPolicy # https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/session-manager.html
                Effect: Allow
                Action:
                  - ssmmessages:CreateControlChannel
                  - ssmmessages:CreateDataChannel
                  - ssmmessages:OpenControlChannel
                  - ssmmessages:OpenDataChannel
                Resource: "*"
              - Sid: CWAccessPolicy
                Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:*

  ECSCodePipelineServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: ecs-codepipeline-service-role
      Path: /service-role/
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - codepipeline.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: ecs-codepipeline-service-policy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Sid: S3AccessPolicy
                Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:GetObjectVersion
                  # - s3:GetBucketVersioning
                  - s3:PutObject
                Resource:
                  - !Sub
                    - ${ArtifactBucket}/*
                    - ArtifactBucket:
                        Fn::ImportValue: !Sub ${Project}-codepipeline-artifacts-s3-bucket-arn
              - Sid: ECSAccessPolicy
                Effect: Allow
                Action:
                  - ecs:DescribeServices
                  - ecs:DescribeTaskDefinition
                  - ecs:DescribeTasks
                  - ecs:ListTasks
                  - ecs:RegisterTaskDefinition
                  - ecs:UpdateService
                Resource:
                  - "*"
              - Sid: CodeBuildAccessPolicy
                Effect: Allow
                Action:
                  - codebuild:StartBuild
                  - codebuild:BatchGetBuilds
                Resource: !Sub arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:project/*
              - Effect: Allow
                Action:
                  - codestar-connections:UseConnection
                Resource: !Sub arn:aws:codestar-connections:${AWS::Region}:${AWS::AccountId}:connection/*
              - Sid: PassRole
                Effect: Allow
                Action: iam:PassRole
                Resource:
                 - "*"
                Condition:
                  StringEqualsIfExists:
                    iam:PassedToService: ecs-tasks.amazonaws.com

Outputs:
  CloudTrailRoleArn:
    Description: Output Export AWS::IAM::Role.Arn CloudTrailRole
    Value: !GetAtt CloudTrailRole.Arn
    Export:
      Name: cloudtrail-role-arn
  ECSInstanceProfileArn:
    Description: Output Export AWS::IAM::InstanceProfile.Arn ECSInstanceProfile
    Value: !GetAtt ECSInstanceProfile.Arn
    Export:
      Name: ecs-instance-profile-arn
  TaskExecutionRoleArn:
    Description: Output Export AWS::IAM::Role.Arn TaskExecutionRole
    Value: !GetAtt TaskExecutionRole.Arn
    Export:
      Name: task-execution-role-arn
  ECSCodeBuildServiceRoleArn:
    Description: Output Export AWS::IAM::Role.Arn ECSCodeBuildServiceRole
    Value: !GetAtt ECSCodeBuildServiceRole.Arn
    Export:
      Name: ecs-codebuild-service-role-arn
  ECSCodePipelineServiceRoleArn:
    Description: Output Export AWS::IAM::Role.Arn ECSCodePipelineServiceRole
    Value: !GetAtt ECSCodePipelineServiceRole.Arn
    Export:
      Name: ecs-codepipeline-service-role-arn

Parametersが追記されているため注意してください。

CodeStar

次にCodePipelineとGitHubを疎通するためのリソースであるCodeStarConnectionを作成します。
今まではGitHubのアクセストークンを利用してGitHubとの疎通を行なっていましたが、CodeStarConnectionができたことによりアクセストークンを利用しないためトークンのローテーションなしで安全に疎通できるようになりました。

.pre-commit-config.yaml
cloudformation
 L iam-user.yaml
   iam-role.yaml
   cloudtrail.yaml
   vpc.yaml
   subnet.yaml
   sg.yaml
   route53.yaml
   acm.yaml
   s3-bucket.yaml
   secretsmanager.yaml
   codestar.yaml(new)
   prefix
    L alb.yaml
      cf.yaml
      ecs-cluster.yaml
      ecs-task-def.yaml
      ecs-service.yaml
      cw-logs.yaml
      ecr.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: "stackName: codestar. AWS::CodeStarConnections::Connection"

Resources:
  CodeStarGitHubConnection:
    Type: AWS::CodeStarConnections::Connection
    Properties:
      ConnectionName: github-connection
      ProviderType: GitHub

Outputs:
  CodeStarGitHubConnectionArn:
    Description: Output Export AWS::CodeStarConnections::Connection.ConnectionArn
    Value: !GetAtt CodeStarGitHubConnection.ConnectionArn
    Export:
      Name: codestar-github-connection-arn

スタック名は、codestar
CodeStarを作成しただけでは、GitHubとの疎通はできていません。下記画像のようになっているので手動で対応が必要になってきます。

手順を説明しようと思いましたが、やはりDevelopersIOさんが解説してくれていました。
AWS CodePipeline のGitHub への接続(バージョン2)を作成してみた

CodePipeline

やっと本命のCodePipelineを構築しようと思います。

.pre-commit-config.yaml
cloudformation
 L iam-user.yaml
   iam-role.yaml
   cloudtrail.yaml
   vpc.yaml
   subnet.yaml
   sg.yaml
   route53.yaml
   acm.yaml
   s3-bucket.yaml
   secretsmanager.yaml
   codestar.yaml
   prefix
    L alb.yaml
      cf.yaml
      ecs-cluster.yaml
      ecs-task-def.yaml
      ecs-service.yaml
      cw-logs.yaml
      ecr.yaml
      codepipeline.yaml(new)
AWSTemplateFormatVersion: "2010-09-09"
Description: "stackName: {Prefix}-{Env}-codepipeline. AWS::CodeBuild::Project AWS::CodePipeline::Pipeline"

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Environment Name Configuration
        Parameters:
          - Prefix
          - Env
          - Project
      - Label:
          default: GitHub Configuration
        Parameters:
          - GitHubDeployBranch
Parameters:
  Prefix:
    Type: String
    Description: AWS resource version management prefix
    Default: v1
  Env:
    Type: String
    Description: Project environment
    AllowedValues: [dev, prd]
  Project:
    Type: String
    Description: Project Name
    Default: example
  GitHubDeployBranch:
    Type: String
    Description: GitHub deploy branch
    Default: main

Mappings:
  EnvMaps:
    common:
      GitHubOrganization: kita21
      GitHubRepository: tech-blog-sample 

Resources:
  ECSDeployCodeBuildProject:
    Type: AWS::CodeBuild::Project
    Properties:
      Name: !Sub ${Prefix}-${Env}-${Project}-ecs-deploy-codebuild
      Description: ${Prefix}-${Env}-${Project}-ecs-deploy-codebuild
      ServiceRole: !ImportValue ecs-codebuild-service-role-arn
      QueuedTimeoutInMinutes: 30
      TimeoutInMinutes: 30
      Artifacts:
        Type: CODEPIPELINE
      Cache:
        Type: LOCAL
        Modes:
          - LOCAL_DOCKER_LAYER_CACHE
      LogsConfig:
        CloudWatchLogs:
          GroupName:
            Fn::ImportValue: !Sub ${Prefix}-${Env}-${Project}-deploy-codebuild-cw-loggroup-name
          Status: ENABLED
      Source:
        BuildSpec: 022-cfn07-cicd/app/buildspec.yml
        Type: CODEPIPELINE
      Environment:
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/amazonlinux2-x86_64-standard:4.0
        Type: LINUX_CONTAINER
        PrivilegedMode: true
        EnvironmentVariables:
          - Name: ENV
            Type: PLAINTEXT
            Value: !Ref Env
          - Name: PREFIX
            Type: PLAINTEXT
            Value: !Ref Prefix
          - Name: PROJECT
            Type: PLAINTEXT
            Value: !Ref Project
          - Name: AWS_ACCOUNT_ID
            Type: PLAINTEXT
            Value: !Ref AWS::AccountId
          - Name: AWS_REGION_NAME
            Type: PLAINTEXT
            Value: !Ref AWS::Region

  ECSDeployCodePipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      Name: !Sub ${Prefix}-${Env}-${Project}-ecs-codepipeline
      RoleArn: !ImportValue ecs-codepipeline-service-role-arn
      ArtifactStore:
        Type: S3
        Location:
          Fn::ImportValue: !Sub ${Project}-codepipeline-artifacts-s3-bucket-name
      Stages:
        - Name: Source
          Actions:
            - Name: SourceStage
              ActionTypeId:
                Category: Source
                Owner: AWS
                Version: 1
                Provider: CodeStarSourceConnection
              Configuration:
                FullRepositoryId: !Sub
                  - ${GitHubOrganization}/${GitHubRepository}
                  - GitHubOrganization: !FindInMap [EnvMaps, common, GitHubOrganization]
                    GitHubRepository: !FindInMap [EnvMaps, common, GitHubRepository]
                ConnectionArn: !ImportValue codestar-github-connection-arn
                BranchName: !Ref GitHubDeployBranch
                DetectChanges: false
                OutputArtifactFormat: CODE_ZIP
              RunOrder: 1
              OutputArtifacts:
                - Name: SourceArtifact
        - Name: Build
          Actions:
            - Name: BuildStage
              ActionTypeId:
                Category: Build
                Owner: AWS
                Version: 1
                Provider: CodeBuild
              Configuration:
                ProjectName: !Ref ECSDeployCodeBuildProject
              InputArtifacts:
                - Name: SourceArtifact
              OutputArtifacts:
                - Name: BuildArtifact
              RunOrder: 1
        - Name: Deploy
          Actions:
            - Name: DeployStage
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Version: 1
                Provider: ECS
              Configuration:
                ClusterName:
                  Fn::ImportValue: !Sub ${Prefix}-${Env}-${Project}-ecs-cluster-name
                ServiceName:
                  Fn::ImportValue: !Sub ${Prefix}-${Env}-${Project}-ecs-service-name
                FileName: imagedefinitions.json
              InputArtifacts:
                - Name: BuildArtifact
              RunOrder: 1

スタック名は、v1-dev-codepipeline
以下にポイントを記載します。

  • 作成したと同時にCodePipelineが実行されます。
    buildspec.ymlを記載していないためビルドは失敗しますが、作成と同時に実行されないようにする方法を知っている方いたら教えてください。
  • AWS::CodeBuild::ProjectのEnvironment.EnvironmentVariablesはCodeBuildで使用される値を入れています。
  • AWS::CodeBuild::ProjectのSource.BuildSpecは、サンプルリポジトリ用に記載しているためリポジトリに合わせて変更してください。
  • AWS::CodePipeline::PipelineのStages[0].Actions[0].Configuration.DetectChangesはコミット時にCodePipelineを実行するか決めています。
    私は他のサンプルコードもコミットするのでfalseにしていますが、GitHubと環境の乖離が発生しないようにtrueにすることをおすすめします。
  • DeployはECSに選択しています。他にもいろいろなデプロイ方法がありますが、今回は一番簡単なECSを選択します。

ECSタスク定義の取得イメージをECRに

前回の記事でECSで動くコンテナのイメージをnginxにしました。今回GitHubから取得したコードで生成したイメージからコンテナを立てるのでイメージを格納するECRから取得するようにします。

.pre-commit-config.yaml
cloudformation
 L iam-user.yaml
   iam-role.yaml
   cloudtrail.yaml
   vpc.yaml
   subnet.yaml
   sg.yaml
   route53.yaml
   acm.yaml
   s3-bucket.yaml
   secretsmanager.yaml
   codestar.yaml
   prefix
    L alb.yaml
      cf.yaml
      ecs-cluster.yaml
      ecs-task-def.yaml(modify)
      ecs-service.yaml
      cw-logs.yaml
      ecr.yaml
      codepipeline.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: "stackName: {Prefix}-{Env}-ecs-task-def. AWS::ECS::TaskDefinition"

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Environment Name Configuration
        Parameters:
          - Prefix
          - Env
          - Project
Parameters:
  Prefix:
    Type: String
    Description: AWS resource version management prefix
    Default: v1
  Env:
    Type: String
    Description: Project environment
    AllowedValues: [dev, prd]
  Project:
    Type: String
    Description: Project Name
    Default: example

Mappings:
  EnvMaps:
    dev:
      Cpu: 256
      Memory: 516
    prd:
      Cpu: 256
      Memory: 516

Resources:
  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: !Sub ${Prefix}-${Env}-${Project}-task
      RuntimePlatform:
        CpuArchitecture: X86_64
      RequiresCompatibilities:
        - EC2
      Cpu: !FindInMap [EnvMaps, !Ref Env, Cpu]
      Memory: !FindInMap [EnvMaps, !Ref Env, Memory]
      ExecutionRoleArn: !ImportValue task-execution-role-arn
      ContainerDefinitions:
        - Name: !Sub ${Prefix}-${Env}-${Project}-container
          Cpu: !FindInMap [EnvMaps, !Ref Env, Cpu]
          Memory: !FindInMap [EnvMaps, !Ref Env, Memory]
          Image: !Sub
            - ${URI}:latest
            - URI:
                Fn::ImportValue: !Sub ${Prefix}-${Env}-${Project}-ecr-uri
          PortMappings:
            - ContainerPort: 80
              HostPort: 0
              Protocol: tcp
          Essential: true
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group:
                Fn::ImportValue: !Sub ${Prefix}-${Env}-${Project}-ecs-cw-loggroup-name
              awslogs-region: ap-northeast-1

ContainerDefinitions[0].Imageを変更しました。変更は一行なので大きな変更ではないです。
これでAWSリソースは完成です!

デプロイ用アプリケーションを作成

AWSリソースは完成したので、次はデプロイするアプリケーションを作成します。今回はただhello worldをレスポンスで返すだけのアプリケーションを作成します。筆者はTypescriptばかり書いているので今回もTypescriptで作成します。

.pre-commit-config.yaml
cloudformation
 L iam-user.yaml
   iam-role.yaml
   cloudtrail.yaml
   vpc.yaml
   subnet.yaml
   sg.yaml
   route53.yaml
   acm.yaml
   s3-bucket.yaml
   secretsmanager.yaml
   codestar.yaml
   prefix
    L alb.yaml
      cf.yaml
      ecs-cluster.yaml
      ecs-task-def.yaml
      ecs-service.yaml
      cw-logs.yaml
      ecr.yaml
      codepipeline.yaml
app(new)

アプリケーションは全て記載すると収まらないのでサンプルをご覧ください。
重要なところは、buildspec.ymlです。ここだけyamlではなくymlですが、どっちでもいいです。AWSのデフォルトがbuildspec.ymlだったので公式に合わせました。

version: 0.2
run-as: root

phases:
  install:
    runtime-versions:
      docker: 20

  pre_build:
    commands:
      - export AWS_REPOSITORY_URI=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION_NAME}.amazonaws.com/${PREFIX}/${ENV}/${PROJECT}
      - export DATETIME=`date +"%Y%m%d%H%M%S"`
      - export LATEST_IMAGE_URI=${AWS_REPOSITORY_URI}:latest
      - export IMAGE_URI=${AWS_REPOSITORY_URI}:${DATETIME}
      - export APP_DIR=022-cfn07-cicd/app # プロジェクトによって変更

      - aws ecr get-login-password --region ${AWS_REGION_NAME} | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION_NAME}.amazonaws.com
      # 対象Imageがない場合でもビルド失敗にならないように
      - docker pull ${LATEST_IMAGE_URI} || true

  build:
    commands:
      - |
        docker build -f ${APP_DIR}/Dockerfile \
          -t ${LATEST_IMAGE_URI} \
          -t ${IMAGE_URI} \
          ${APP_DIR}        

  post_build:
    commands:
     - docker push --all-tags ${AWS_REPOSITORY_URI}
     - echo '[{"name":"'${PREFIX}-${ENV}-${PROJECT}-container'","imageUri":"'${IMAGE_URI}'"}]' > imagedefinitions.json

artifacts:
  files:
    - imagedefinitions.json

yamlファイルの内容としては以下になります。

  1. 扱いやすいように環境変数に入れていく。CodeBuildで設定した値を使用している。
  2. ECRにログインし、ECRの最新イメージをPullする
  3. Dockerfileからイメージをビルドする
    この時、ECRからPullしたイメージがあればそれをキャッシュ利用する
    最新のlatestと一意の時刻によるタグ付けをする
  4. ビルドしたイメージをECRにPushする
  5. コンテナ名とECRのURIを記載したアーティファクト作成

アプリケーションのコードをGitHubにあげてCodePipelineを実行するとECSのコンテナに反映されると思います。
反映されたアプリケーションにアクセスしてみましょう。

curl -i https://looseller.com

HTTP/2 200 
content-type: text/html; charset=utf-8
content-length: 12
date: Wed, 10 May 2023 00:11:11 GMT
x-powered-by: Express
etag: W/"c-Lve95gjOVATpfV8EL5X4nxwjKHE"
x-cache: Miss from cloudfront
via: 1.1 1b3fd5e3e9b3fd38054dc45b58346688.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-C3
x-amz-cf-id: O86hc2jhLai6xd3_HGq7B77035i3jMcVswMeJ7H4T4ZwRyg-C6eObQ==

Hello World!

200レスポンスでHello World!が返ってきましたね! これでやっと全て完了です!長かった…

かかった金額

今回のサンプルAWSリソースを作成してかかった金額が以下になります。
無料枠も使用しています。

EC2その他は、NatGatewayになります。
こう見るとNatGatewayが高いですね。一日1.5ドルほどかかっているので日数が経つと、ちりつもで痛い…
NatGateway使用しないで構築する方法もあるにはあるのですが、NatGatewayの方が一般的で分かりやすいので
また、月末で料金がプラスでかかってくるかもしれないので、コスト確定の通知が来たら追加で貼ろうかなと思います。

終わりに

最後のCICD編が一番テンプレート多かったですね…
それにhello world!をただ出すアプリケーションにしてはリッチすぎるかもしれません
(´・ω・`)
RDSやWafなどプロジェクトによっては他にも追加するリソースはあるかもしれませんが、ほとんどのプロジェクトはこの記事のサンプルコードをただ流せば1時間もかけずに環境が構築できてしまいますね。ドメインの取得はお忘れなく
あ、また繰り返しになりますが、このサンプルコードのテンプレートの切り分けはあくまで記事用に作成したものです。AWSのベストプレクティスからは外れているのでまんまコピペして先輩エンジニアに怒られないように気をつけてくださいね。
この記事を作成するにあたって、クラスメソッドさんのDevelopersIOに大変お世話になりました。困ったらDevelopersIOで検索すれば大体は解決しますね。
長くなりましたが、記事で利用したドメインもあと1日で切れそうですし、筆(指)を置きたいと思います。

参考