CodePipelineでイメージをビルドしてFargateにデプロイする

目次

CodePipelineでイメージをビルドしてFargateにデプロイする

以下のページで、CodePipelineを使ってパイプラインを構成して、CodeCommitとCodeBuildを連携させて、ECRにイメージをプッシュする方法をご紹介しました。

あわせて読みたい
CodePipelineを使ってCodeCommitプッシュをトリガーにしてECRにイメージをプッシュする 【CodePipelineを使ってCodeCommitプッシュをトリガーにしてECRにイメージをプッシュする】 CodePipelineを使用することによって、CI/CD構成を構築することができます。...

今回はCodePipelineにデプロイステージを作成して、ビルドしたイメージをFargateにデプロイすることを目指します。

構築する環境

Diagram of use CodePipeline to build and deploy images to Fargate.

CodePipelineを構成し、2つのリソースを連携させます。

1つ目はCodeCommitです。
CodeCommitはCodePipelineのソースステージを担当します。
Gitリポジトリとして使用します。

2つ目はCodeBuildです。
CodeBuildはCodePipelineのビルドステージを担当します。
CodeCommitにプッシュされたコードから、Dockerイメージをビルドします。
ビルドしたイメージをECRにプッシュします。

CodePipelineにデプロイステージを作成します。
後述のFargateにデプロイするように設定します。

SSMパラメータストアにDockerHubアカウント情報を保存します。
DockerBuildでイメージを生成する際に、DockerHubにサインインした上でベースイメージをプルするために、これらを使用します。

CodePipelineが開始されるきっかけですが、CodeCommitへのプッシュを条件とします。
具体的には、EventBridgeに上記を満たすルールを用意します。

プライベートサブネットに、FargateタイプのECSを作成します。

ECRにプッシュしたイメージを取得するために、ECR用およびS3用VPCエンドポイントを作成します。

EC2インスタンスを作成します。
Fargate上に作成されたコンテナにアクセスするためのクライアントして使用します。

CloudFormationテンプレートファイル

上記の構成をCloudFormationで構築します。
以下のURLにCloudFormationテンプレートを配置しています。

https://github.com/awstut-an-r/awstut-fa/tree/main/076

テンプレートファイルのポイント解説

基本的な構成は冒頭にご紹介したページと同様です。
本ページは、CodePipelineにおいて、CodeBuildでビルドしたDockerイメージを、Fargateにデプロイする方法を中心に取り上げます。

プライベートサブネットにFargateを構築する方法については、以下のページをご確認ください。

あわせて読みたい
プライベートサブネットにECS(Fargate)を作成する 【プライベートサブネットにECS(Fargate)を作成する】 以下のページでFargateタイプのECSコンテナを作成する方法をご紹介しました。 https://awstut.com/2022/01/25/int...

CodeBuild

Resources:
  CodeBuildProject:
    Type: AWS::CodeBuild::Project
    Properties:
      Artifacts:
        Type: CODEPIPELINE
      Cache:
        Type: NO_CACHE
      Environment:
        ComputeType: !Ref ProjectEnvironmentComputeType
        EnvironmentVariables:
          - Name: CONTAINER_NAME
            Type: PLAINTEXT
            Value: !Ref ContainerName
          - Name: DOCKERHUB_PASSWORD
            Type: PARAMETER_STORE
            Value: !Ref SSMParameterDockerHubPassword
          - Name: DOCKERHUB_USERNAME
            Type: PARAMETER_STORE
            Value: !Ref SSMParameterDockerHubUsername
          - Name: IMAGE_DEFINITION
            Type: PLAINTEXT
            Value: !Ref ImageDefinitionFileName
        Image: !Ref ProjectEnvironmentImage
        ImagePullCredentialsType: CODEBUILD
        Type: !Ref ProjectEnvironmentType
        PrivilegedMode: true
      LogsConfig:
        CloudWatchLogs:
          Status: DISABLED
        S3Logs:
          Status: DISABLED
      Name: !Ref Prefix
      ServiceRole: !GetAtt CodeBuildRole.Arn
      Source:
        Type: CODEPIPELINE
        BuildSpec: !Sub |
          version: 0.2

          phases:
            pre_build:
              commands:
                - echo Logging in to Amazon ECR...
                - aws --version
                - aws ecr get-login-password --region ${AWS::Region} | docker login --username AWS --password-stdin ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com
                - REPOSITORY_URI=${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRRepositoryName}
                - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
                - IMAGE_TAG=${!COMMIT_HASH:=latest}
                - echo Logging in to Docker Hub...
                - echo $DOCKERHUB_PASSWORD | docker login -u $DOCKERHUB_USERNAME --password-stdin
            build:
              commands:
                - echo Build started on `date`
                - echo Building the Docker image...
                - docker build -t $REPOSITORY_URI:latest .
                - docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
            post_build:
              commands:
                - echo Build completed on `date`
                - echo Pushing the Docker images...
                - docker push $REPOSITORY_URI:latest
                - docker push $REPOSITORY_URI:$IMAGE_TAG
                - echo Writing image definitions file...
                - printf '[{"name":"%s","imageUri":"%s"}]' $CONTAINER_NAME $REPOSITORY_URI:$IMAGE_TAG > $IMAGE_DEFINITION
          artifacts:
            files: $IMAGE_DEFINITION
      Visibility: PRIVATE
Code language: YAML (yaml)

BuildSpecプロパティがポイントです。
今回はCloudFormationテンプレートにbuildspec.ymlの内容を直接記載しています。

CodeBuildでビルドしたイメージをECS(Fargate)にデプロイする上でのポイントは、post_buildフェーズです。
AWS公式ページによりますと、imagedefinition.jsonというファイルを作成する必要があるとされています。

Amazon ECS サービスのコンテナ名およびイメージとタグがあるビルドのルートに imagedefinitions.json という名前のファイルを作成します。CD パイプラインのデプロイステージでこの情報を使用してサービスのタスク定義の新しいリビジョンを作成し、新しいタスク定義を使用してサービスを更新します。imagedefinitions.jsonファイルは ECS ジョブワーカーに必須です。

チュートリアル: CodePipeline を使用した Amazon ECS 標準デプロイ

今回は環境変数IMAGE_DEFINITIONにimagedefinitions.jsonという文字列を格納し、2箇所で参照しています。

CodePipeline

Resources:
  Pipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      ArtifactStore:
        Location: !Ref BucketName
        Type: S3
      Name: !Ref Prefix
      RoleArn: !GetAtt CodePipelineRole.Arn
      Stages:
        - Actions:
            - ActionTypeId:
                Category: Source
                Owner: AWS
                Provider: CodeCommit
                Version: 1
              Configuration:
                BranchName: !Ref BranchName
                OutputArtifactFormat: CODE_ZIP
                PollForSourceChanges: false
                RepositoryName: !GetAtt CodeCommitRepository.Name
              Name: SourceAction
              OutputArtifacts:
                - Name: !Ref PipelineSourceArtifact
              Region: !Ref AWS::Region
              RunOrder: 1
          Name: Source
        - Actions:
            - ActionTypeId:
                Category: Build
                Owner: AWS
                Provider: CodeBuild
                Version: 1
              Configuration:
                ProjectName: !Ref CodeBuildProject
              InputArtifacts:
                - Name: !Ref PipelineSourceArtifact
              Name: Build
              OutputArtifacts:
                - Name: !Ref PipelineBuildArtifact
              Region: !Ref AWS::Region
              RunOrder: 1
          Name: Build
        - Actions:
            - ActionTypeId:
                Category: Deploy
                Owner: AWS
                Provider: ECS
                Version: 1
              Configuration:
                ClusterName: !Ref ECSClusterName
                FileName: !Ref ImageDefinitionFileName
                ServiceName: !Ref ECSServiceName
              InputArtifacts:
                - Name: !Ref PipelineBuildArtifact
              Name: Deploy
              Region: !Ref AWS::Region
              RunOrder: 1
          Name: Deploy
Code language: YAML (yaml)

Stagesプロパティがポイントです。
本プロパティの3つ目の要素が、ECS(Fargate)にデプロイするステージです。

ActionTypeIdプロパティで、ECS(Fargate)にデプロイするように設定します。
Configurationプロパティで、デプロイするイメージやデプロイ先を設定します。FileNameプロパティで先述のimagedefinitions.jsonファイルのファイル名を指定します。ClusterNameおよびServiceNameプロパティでデプロイ先のECSクラスター・サービスを指定します。
InputArtifactsプロパティは、本ステージで使用するアーティファクトを設定します。ポイントはビルドステージで指定したOutputArtifactsプロパティの値と同一にする必要があるという点です。ビルドステージで生成されたアーティファクトを使ってデプロイするためです。

CodePipeline用のIAMロールを確認します。

Resources:
  CodePipelineRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - codepipeline.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: PipelinePolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - codecommit:CancelUploadArchive
                  - codecommit:GetBranch
                  - codecommit:GetCommit
                  - codecommit:GetRepository
                  - codecommit:GetUploadArchiveStatus
                  - codecommit:UploadArchive
                Resource:
                  - !GetAtt CodeCommitRepository.Arn
              - Effect: Allow
                Action:
                  - codebuild:BatchGetBuilds
                  - codebuild:StartBuild
                  - codebuild:BatchGetBuildBatches
                  - codebuild:StartBuildBatch
                Resource:
                  - !GetAtt CodeBuildProject.Arn
              - Effect: Allow
                Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:GetObjectVersion
                  - s3:GetBucketAcl
                  - s3:GetBucketLocation
                Resource:
                  - !Sub "arn:aws:s3:::${BucketName}"
                  - !Sub "arn:aws:s3:::${BucketName}/*"
              - Effect: Allow
                Action:
                  - ecs:*
                Resource: "*"
              - Effect: Allow
                Action:
                  - iam:PassRole
                Resource: "*"
                Condition:
                  StringLike:
                    iam:PassedToService:
                      - ecs-tasks.amazonaws.com
Code language: YAML (yaml)

ポイントはECSに関する権限と、iam:PassRoleです。
これらの権限はECSにイメージをデプロイするために必要となります。

このIAMロールはPipelineのRoleArnプロパティで参照されますが、本プロパティは以下の通り説明されています。

The Amazon Resource Name (ARN) for CodePipeline to use to either perform actions with no actionRoleArn, or to use to assume roles for actions with an actionRoleArn.

AWS::CodePipeline::Pipeline

つまり今回のデプロイステージにおいては、このIAMロールを使用してECSにイメージをデプロイすることになります。

このIAMロールの信頼ポリシーを確認します。
プリンシパルが「codepipeline.amazonaws.com」とありますので、CodePipelineが本IAMロールを使って、例えばECSの全アクションを実行することができることなります。

ただし今回のDockerイメージをデプロイするというアクションは、ECSタスクが実行することになります。
ですから本IAMロールにiam:PassRoleの権限を与え、ECSタスクに本IAMロールをパスすることを許可します。
この挙動はResourceに「*」を、Conditionでパスする先が「ecs-tasks.amazonaws.com」サービスであることを条件とすることで実装します。

(参考)ECSサービス

Resources:
  Service:
    Type: AWS::ECS::Service
    Properties:
      Cluster: !Ref Cluster
      DesiredCount: 0
      LaunchType: FARGATE
      NetworkConfiguration:
        AwsvpcConfiguration:
          SecurityGroups:
            - !Ref ContainerSecurityGroup
          Subnets:
            - !Ref ContainerSubnet
      ServiceName: !Sub "${Prefix}-service"
      TaskDefinition: !Ref TaskDefinition
Code language: YAML (yaml)

特別な設定は行いません。

1点ポイントがあります。
DesiredCountプロパティで、ECSサービス上に作成するタスク数を0にします。
この値が1以上の場合は、CloudFormationスタック作成時に、タスク作成を試みることになります。
しかし初期構築時はECRにイメージがプッシュされていないため、タスク作成に失敗します。
ですから本プロパティを0とすることで、タスクを生成しないように設定します。

(参考)アプリコンテナ

Dockerfile

FROM amazonlinux

RUN yum update -y && yum install python3 python3-pip -y

RUN pip3 install bottle

COPY main.py ./

CMD ["python3", "main.py"]

EXPOSE 8080
Code language: YAML (yaml)

冒頭でご紹介したページと同様です。

Amazon Linux 2をベースとして、PythonおよびWebフレームワークBottleをインストールします。
アプリロジックを記載したPythonスクリプト(main.py)をコピーし、これを実行するように設定します。
アプリは8080/tcpでHTTPリクエストを待ち受けますので、このポートを公開します。

main.py

from bottle import route, run

@route('/')
def hello():
  return 'Hello CodePipeline.'


if __name__ == '__main__':
  run(host='0.0.0.0', port=8080)
Code language: YAML (yaml)

Bottleを使用して、簡易的なWebサーバを構築します。
8080/tcpでHTTPリクエストを待ち受け、「Hello CodePipeline.」を返すという単純な構成です。

環境構築

CloudFormationを使用して、本環境を構築し、実際の挙動を確認します。

CloudFormationスタックを作成し、スタック内のリソースを確認する

CloudFormationスタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。

あわせて読みたい
CloudFormationのネストされたスタックで環境を構築する 【CloudFormationのネストされたスタックで環境を構築する方法】 CloudFormationにおけるネストされたスタックを検証します。 CloudFormationでは、スタックをネストす...

各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。

  • ECSクラスター:fa-076-cluster
  • ECSサービス:fa-076-service
  • ECR:fa-076
  • CodeCommit:fa-076
  • CodeBuildプロジェクト:fa-076
  • CodePipeline:fa-076
  • SSMパラメータのパラメータ名1:fa-076-DcokerHubUsername
  • SSMパラメータのパラメータ名2:fa-076-DcokerHubPassword
  • EC2インスタンス:i-0024a483b8e4a8778

作成されたリソースをAWS Management Consoleから確認します。
ECS(Fargate)を確認します。

Detail of ECS 1.
Detail of ECS 2.

正常にECSクラスター・サービス・タスクが作成されています。
Desired Countが0であり、タスクが生成されていないことがわかります。

ECRを確認します。

Detail of ECR 1.

空です。
パイプラインが実行されることによって、ここにイメージがプッシュされることになります。

CodeCommitを確認します。

Detail of CodeCommit 1.

こちらも空です。
ここにコードをプッシュすることによって、パイプラインが実行されることになります。

CodeBuildを確認します。

Detail of CodeBuild 1.
Detail of CodeBuild 2.

CloudFormationテンプレートで指定した通りに、CodeBuildが作成されています。

CodePipelineを確認します。

Detail of CodePipeline 1.

パイプラインの実行に失敗しています。
これはCloudFormationスタック作成時に、CodeCommitおよびその内部にブランチが作成されたことがきっかけとして、パイプラインが実行されたためです。
現時点ではCodeCommitにコードをプッシュしていないため、パイプライン実行の過程でエラーが発生したということです。

動作確認

パイプライン実行 1回目

準備が整いましたので、CodeCommitにコードをプッシュします。

まずCodeCommitをプルします。

$ git clone https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/fa-076
Cloning into 'fa-076'warning: You appear to have cloned an empty repository.
Code language: Bash (bash)

空のリポジトリがプルされました。

リポジトリにDockerfileおよびmain.pyを加えます。

$ ls -al
total 8
drwxrwxr-x 3 ec2-user ec2-user 51 Aug 14 11:56 .
drwxrwxr-x 3 ec2-user ec2-user 20 Aug 14 11:56 ..
-rw-rw-r-- 1 ec2-user ec2-user 187 Aug 12 11:01 Dockerfile
drwxrwxr-x 7 ec2-user ec2-user 119 Aug 14 11:56 .git
-rw-rw-r-- 1 ec2-user ec2-user 681 Aug 13 12:30 main.pyCode language: Bash (bash)

2ファイルをCodeCommitにプッシュします。

$ git add .

$ git commit -m "first commit"
[master (root-commit) 738b02d2] first commit
...
 2 files changed, 39 insertions(+)
 create mode 100644 Dockerfile
 create mode 100644 main.py

$ git push
...
 * [new branch]      master -> master
Code language: Bash (bash)

正常にプッシュできました。

改めてCodeCommitを確認します。

Detail of CodeCommit 2.

確かに2ファイルがプッシュされています。

CodePipelineが実行が開始されました。
しばらく待機します。

Detail of CodePipeline 2.

パイプラインの実行が正常に完了しました。

ECRを確認します。

Detail of ECR 2.

イメージがプッシュされています。
CodeBuildによってビルドされたイメージが、ECRにプッシュされたということです。

ECSサービスの設定変更を行います。

Detail of ECS 33.

Desired Countを0から1に更新します。

ECSサービスを確認します。

Detail of ECS 5.

1つタスクが作成されました。

作成されたタスクの詳細を確認します。

Detail of ECS 4.

自動的に割り当てられたプライベートアドレスが「10.0.2.246」であることがわかります。

コンテナにHTTPリクエストを行うために、EC2インスタンスにアクセスします。
インスタンスへのアクセスはSSM Session Managerを使用します。

% aws ssm start-session --target i-0024a483b8e4a8778

Starting session with SessionId: root-00be6118d01682117
sh-4.2$
Code language: Bash (bash)

SSM Session Managerの詳細につきましては、以下のページをご確認ください。

あわせて読みたい
LinuxインスタンスにSSM Session Manager経由でアクセスする 【LinuxインスタンスにSSM Session Manager経由でアクセスする】 EC2インスタンスにSSM Session Manager経由でアクセスする構成を確認します。 Session Manager は完全...

curlコマンドを使って、タスク内のコンテナにアクセスします。

sh-4.2$ curl http://10.0.2.246:8080
Hello CodePipeline.
Code language: Bash (bash)

コンテナから応答がありました。
確かにBottleアプリで設定した文字列です。
このことから、CodePipelineで作成したパイプラインを実行することよって、イメージがビルドされて、このイメージからECSタスクが生成されたことがわかります。

パイプライン実行 2回目

改めてパイプラインを実行します。

main.pyのコードを少し変更した上で、CodeComitにプッシュします。

$ cat main.py
from bottle import route, run

@route('/')
def hello():
  return 'Hello CodePipeline. Update!'

if __name__ == '__main__':
  run(host='0.0.0.0', port=8080)
Code language: Bash (bash)
$ git add .

$ git commit -m "second commit"
[master e3dc93e] second commit
 Committer: EC2 Default User <ec2-user@ip-172-31-26-46.ap-northeast-1.compute.internal>
...
 1 file changed, 1 insertion(+), 1 deletion(-)

$ git push
...
To https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/fa-076
   738b02d..e3dc93e  master -> master
Code language: Bash (bash)

CodePipelineを確認します。

Detail of CodePipeline 3.

再びパイプラインが実行されました。
しばらく待つと、パイプラインが完了します。

改めてECRを確認します。

Detail of ECR 3.

新たなイメージがビルドされプッシュされました。
今後はこちらのイメージを使って、ECSタスクが生成されるはずです。

デプロイ途中のECSサービスの挙動を確認します。

Detail of ECS 7.

ECSデプロイ途中は2つのタスクが動作していることが確認できます。
1つは元々動作していたタスクです。このタスクは古いイメージから生成されたものです。
もう1つは新たに生成されたタスクです。このタスクは新しいイメージから生成されたものです。
このようにデプロイが始まると、新たなタスクが生成され、新旧タスクが併存するタイミングがあります。

デプロイが完了した後に、改めてECSサービスを確認します。

Detail of ECS 8.

新しいタスクだけが動作しています。
古いタスクは停止しました。
このようにデプロイが完了すると、新しいイメージから生成されたタスクだけが動作します。

新たに生成されたタスクの詳細を確認します。

Detail of ECS 9.

新たに生成されたタスクに割り当てられたプライベートアドレスは「10.0.2.224」でした。

改めてEC2インスタンスからECSタスクにアクセスします。

sh-4.2$ curl http://10.0.2.224:8080/
Hello CodePipeline. Update!
Code language: Bash (bash)

コンテナから応答がありました。
更新したコードを反映した文字列です。
このことから、CodePipelineで作成したパイプラインを実行することよって、最新のイメージをECS(Fargate)にデプロイできたことがわかります。

まとめ

今回はCodePipelineにデプロイステージを作成して、ビルドしたイメージをFargateにデプロイすることができました。

目次