CodePipelineでイメージをビルドしてFargateにデプロイする
以下のページで、CodePipelineを使ってパイプラインを構成して、CodeCommitとCodeBuildを連携させて、ECRにイメージをプッシュする方法をご紹介しました。
今回はCodePipelineにデプロイステージを作成して、ビルドしたイメージを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を構築する方法については、以下のページをご確認ください。
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スタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。
各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。
- 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)を確認します。
正常にECSクラスター・サービス・タスクが作成されています。
Desired Countが0であり、タスクが生成されていないことがわかります。
ECRを確認します。
空です。
パイプラインが実行されることによって、ここにイメージがプッシュされることになります。
CodeCommitを確認します。
こちらも空です。
ここにコードをプッシュすることによって、パイプラインが実行されることになります。
CodeBuildを確認します。
CloudFormationテンプレートで指定した通りに、CodeBuildが作成されています。
CodePipelineを確認します。
パイプラインの実行に失敗しています。
これは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.py
Code 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を確認します。
確かに2ファイルがプッシュされています。
CodePipelineが実行が開始されました。
しばらく待機します。
パイプラインの実行が正常に完了しました。
ECRを確認します。
イメージがプッシュされています。
CodeBuildによってビルドされたイメージが、ECRにプッシュされたということです。
ECSサービスの設定変更を行います。
Desired Countを0から1に更新します。
ECSサービスを確認します。
1つタスクが作成されました。
作成されたタスクの詳細を確認します。
自動的に割り当てられたプライベートアドレスが「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の詳細につきましては、以下のページをご確認ください。
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を確認します。
再びパイプラインが実行されました。
しばらく待つと、パイプラインが完了します。
改めてECRを確認します。
新たなイメージがビルドされプッシュされました。
今後はこちらのイメージを使って、ECSタスクが生成されるはずです。
デプロイ途中のECSサービスの挙動を確認します。
ECSデプロイ途中は2つのタスクが動作していることが確認できます。
1つは元々動作していたタスクです。このタスクは古いイメージから生成されたものです。
もう1つは新たに生成されたタスクです。このタスクは新しいイメージから生成されたものです。
このようにデプロイが始まると、新たなタスクが生成され、新旧タスクが併存するタイミングがあります。
デプロイが完了した後に、改めてECSサービスを確認します。
新しいタスクだけが動作しています。
古いタスクは停止しました。
このようにデプロイが完了すると、新しいイメージから生成されたタスクだけが動作します。
新たに生成されたタスクの詳細を確認します。
新たに生成されたタスクに割り当てられたプライベートアドレスは「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にデプロイすることができました。