CodeBuildのbuildspec.ymlの書き方を覚えて、Laravelを立ち上げよう!
おはようございます!
ベンジャミンの木村です!
みなさま、ECSでコンテナを立てられていますか?私は最初すごく苦労しました。
パイプラインを回すたび、CodeBuildが止まり、ようやくビルドができたと思ったところで、デプロイ時にコンテナが一瞬で落ちる。。。そんな経験、誰もがしてきたのではないでしょうか?
今回、せめてCodeBuildのビルドのところでつまずかないように、弊社のbuildspec.ymlの書き方をご紹介します。
※今回、Dockerfileの記載は載せておりません。Dockerfileの準備に関しましては、以下記事が大変参考になりましたので、こちらをご参照ください。
https://hanlabo.co.jp/memorandum/1857
【超入門】20分でLaravel開発環境を爆速構築するDockerハンズオン
https://qiita.com/ucan-lab/items/56c9dc3cf2e6762672f4
目次
- 構成図
- ファイル構成
- buildspec.ymlとは?
- buildspec.ymlの全体を見てみよう
- セクションとは?
- フェーズ(phases)とは?
- 1. installフェーズの解説
- 2. pre_buildフェーズの解説
- 3. buildフェーズの解説
- 4. post_buildフェーズの解説
- 5. ビルドを実行しよう!
- まとめ
構成図
今回はこの赤枠の部分を説明していきます。
ファイル構成
.
├── appspec.yml # デプロイの設定ファイル
├── buildspec.yml # 今回解説するbuildspec.yml
├── docker-compose.yml
├── infra
│ ├── mysql
│ │ ├── Dockerfile # dbコンテナ用のDockerfile※ローカル用です(AWSではAuroraがあるので使いません)
│ │ └── my.cnf
│ ├── nginx
│ │ ├── Dockerfile # nginxコンテナ用のDockerfile
│ │ └── default.conf
│ └── php # phpの設定ファイル集
│ ├── Dockerfile # phpコンテナ用のDockerfile
│ └── conf
│ ├── php-fpm.conf
│ ├── php.ini
│ └── www.conf
├── src # Laravelのsrcディレクトリ
│ ├── README.md
│ ├── app
│ …………
└── taskdef.json # ECSを立ち上げるための指示書
buildspec.ymlとは?
簡単に説明しますと、ビルドの指示書のようなものです。
書かれていることは以下の通りです。
- ビルドを行うための準備
- ビルド(コンテナイメージの構築)
- ビルド後に行う作業(イメージのpushやタスクの定義など)
buildspec.ymlに書かれていることが全て実行されると、ECRにコンテナイメージが挿入され、いよいよコンテナが立ち上がる準備が整ったという状態になります。
buildspec.ymlの全体を見てみよう
まずはLaravelのコンテナを立ち上げるためのbuildspec.ymlの全体像を下記に記載します。
『初見でこんなコード見せられても訳がわからないよ!』という気持ちはわかります。ですので、この後、まずはbuildspec.ymlに記載されている各セクションやフェーズというものについて解説させていただきます。
version: 0.2
phases:
install:
runtime-version:
docker: 20
pre_build:
commands:
# # ECRリポジトリのURIを環境変数に設定
- REPO_URI_NGINX="${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${ENV}-${SERVICE}-ecr/nginx"
- REPO_URI_PHPFPM="${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${ENV}-${SERVICE}-ecr/php-fpm"
# # タグ名にgitのコミットハッシュを利用
- IMAGE_TAG=$(echo ${CODEBUILD_RESOLVED_SOURCE_VERSION} | cut -c 1-7)
- TAG="${ENV}-${IMAGE_TAG}"
# # aws cliの設定
- aws ecr get-login-password --region ${REGION} | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com
# # nginxの設定ファイルの変更
- sed -i -e "s#fastcgi_pass php:9000#fastcgi_pass localhost:9000#" ./infra/nginx/default.conf
build:
commands:
# # docker buildを実行
- docker build -f ./infra/nginx/Dockerfile -t src_nginx:latest .
- docker build -f ./infra/php/Dockerfile -t src_php:latest .
# # doeckr imageのタグ付け
- docker tag src_nginx:latest ${REPO_URI_NGINX}:${ENV}-${IMAGE_TAG}
- docker tag src_php:latest ${REPO_URI_PHPFPM}:${ENV}-${IMAGE_TAG}
- docker images
post_build:
commands:
- echo Build completed on `date`
- echo Pushing the Docker images...
# # dockr pushを実行
- docker push ${REPO_URI_NGINX}:${ENV}-${IMAGE_TAG}
- docker push ${REPO_URI_PHPFPM}:${ENV}-${IMAGE_TAG}
# # taskdef.jsonの変数の変更
- sed -i -e "s#<AWS_ACCOUNT_ID>#${AWS_ACCOUNT_ID}#" taskdef.json
- sed -i -e "s#<REGION>#${REGION}#" taskdef.json
- sed -i -e "s#<SERVICE>#${SERVICE}#" taskdef.json
- sed -i -e "s#<ENV>#${ENV}#" taskdef.json
- sed -i -e "s#<TAG>#${TAG}#" taskdef.json
- sed -i -e "s#<TASK_CPU>#${TASK_CPU}#" taskdef.json
- sed -i -e "s#<TASK_MEMORY>#${TASK_MEMORY}#" taskdef.json
- cat taskdef.json
artifacts:
files:
- appspec.yml
- taskdef.json
※Laravelに必要なパッケージなどをインストールするためのコードは、Dockerfileに記載している想定でbuildspec.ymlを記載しております。
セクションとは?
セクションとは、特定の設定やデータをグループ化するためのブロックのことを指します。先ほどのコードで言うところの、version、phases、reports、artifactsがそれに当たります。
- version
version
: ビルド仕様のバージョンを指定します
- phases
- ビルドのプロセスの各段階を定義するメインセクションです。通常、
install
、pre_build
、build
、post_build
のビルドの段階的なプロセスが含まれます
- ビルドのプロセスの各段階を定義するメインセクションです。通常、
- artifacts
- ビルド成果物である、『デプロイに必要なファイル(
appspec.yml
)』や『ECSのタスク定義を記載するファイル(taskdef.json
)』を指定するためのセクションです。これにより、ビルド成果物を保存し、後続のデプロイプロセスなどで使用することができます
- ビルド成果物である、『デプロイに必要なファイル(
フェーズ(phases)とは?
『buildspec.ymlにはセクションというブロックがあるんだ〜』とご理解いただけましたか?次に、メインセクションのフェーズ(phases)について説明させていただきます。
フェーズは、ビルドプロセスを段階的に進行するためのステップを定義するためのもので、それぞれのフェーズには特定の役割があります。先ほどのコードで言うところの、install、pre_build、build、post_buildがそれに当たります。
以下より、各フェーズで行なっていることを詳細に説明させていただきます。
1. installフェーズの解説
このフェーズでは、ビルドプロセスに必要なツールや依存関係をインストールします。
install:
runtime-version:
docker: 20
ここでは、runtime-versionsセクションを使って、Dockerエンジンのバージョン20を指定しています。これにより、ビルドプロセスが始まる前に、CodeBuildの環境に必要なDockerエンジンがインストールされます。
最新のDockerエンジンのバージョンについては、下記Docker docsの公式ページよりご確認ください。
https://matsuand.github.io/docs.docker.jp.onthefly/engine/release-notes
また、installできるruntime-versionはDockerエンジン以外にも選択可能です。
用途に合わせて、他のものを使用する場合は、下記AWS公式のページをご確認ください。
https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/available-runtimes.html
2. pre_buildフェーズの解説
このフェーズでは、ビルドプロセスが始まる前に必要な設定や準備を行います。
pre_build:
commands:
# # ECRリポジトリのURIを環境変数に設定
- REPO_URI_NGINX="${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${ENV}-${SERVICE}-ecr/nginx"
- REPO_URI_PHPFPM="${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${ENV}-${SERVICE}-ecr/php-fpm"
# # タグ名にgitのコミットハッシュを利用
- IMAGE_TAG=$(echo ${CODEBUILD_RESOLVED_SOURCE_VERSION} | cut -c 1-7)
- TAG="${ENV}-${IMAGE_TAG}"
# # aws cliの設定
- aws ecr get-login-password --region ${REGION} | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com
# # nginxの設定ファイルの変更
- sed -i -e "s#fastcgi_pass php:9000#fastcgi_pass localhost:9000#" ./infra/nginx/default.conf
このフェーズでは、ECRリポジトリのURIを環境変数に設定したり、タグ名にgitのコミットハッシュを使用したり、AWS CLIの設定を行ったりします。また、nginxの設定ファイルを変更するためのコマンドも含まれています。
以下に詳細を説明させていだだきます。
- ECRリポジトリのURIを変数に定義
- REPO_URI_NGINX="${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${ENV}-${SERVICE}-ecr/nginx"
- REPO_URI_PHPFPM="${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${ENV}-${SERVICE}-ecr/php-fpm"
ECRリポジトリのURIを変数に定義しています。
ここでいきなり、${AWS_ACCOUNT_ID}、${REGION}、${ENV}、${SERVICE}といった変数が出てきたと思いますが、こちらはどこから登場したのでしょうか?
こちらは、CodeBuild上で定義した環境変数になります。Codebuildでは環境変数を設定することで、それら環境変数をbuildspec.yml上で使用することができます。
設定手順は以下に記載させていただきますので、こちらをご参照ください。
- CodePipelineのコンソール画面へ移動する
- 「編集」ボタンをクリックする
- 「Build」で「ステージを編集する」ボタンをクリックする
- ペンマーク(🖊️)をクリック
- 開いた画面の下記赤枠部分で環境変数の設定ができる
※${TASK_CPU}、${TASK_MEMORY}は項番4のpost_buildで使う環境変数になります。
- タグの定義
- IMAGE_TAG=$(echo ${CODEBUILD_RESOLVED_SOURCE_VERSION} | cut -c 1-7)
- TAG="${ENV}-${IMAGE_TAG}"
こちらはECRにつける以下のようなイメージタグをランダムな数値として、変数に定義しています。
ここでもいきなり、${CODEBUILD_RESOLVED_SOURCE_VERSION}という変数が登場しています。こちらも先ほどと同様に、CodeBuild上で設定した環境変数でしょうか?
いいえ、こちらは先ほどとは違い、すでにAWSから用意されたCodeBuild のビルドコマンドで使用できる環境変数になります。
こういった、こちらで用意しなくてもAWSから提供されている環境変数がいくつかございますので、以下AWS公式ページよりご確認ください。
https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-env-ref-env-vars.html
- AWS CLIを使ってECRにログイン
- aws ecr get-login-password --region ${REGION} | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com
こちらは、ECRにログインするためのコマンドになります。このコマンドを実行しないとECRへイメージをpushやpullするような、動作をすることができません。
- nginxの設定ファイル(default.conf)の変更
- sed -i -e "s#fastcgi_pass app:9000#fastcgi_pass localhost:9000#" ./infra/nginx/default.conf
fastcgi_pass app:9000
はnginxコンテナからappコンテナに対して9000番portで通信の設定する記述です。(私は初めてコンテナを立てる時、ここで大ハマりしました…)
ECSはコンテナ間で通信する際はlocalhostを指定して通信を行うため、記述をapp(コンテナ名)ではなく、localhostに変更する必要があります。上記はsedコマンドを用いて、nginxの設定ファイルから、その記述部分の置換を行なっております。
3. buildフェーズの解説
このフェーズでは、実際にビルドを行い、Dockerイメージを作成します。
# # docker buildを実行
- docker build -f ./infra/nginx/Dockerfile -t src_nginx:latest .
- docker build -f ./infra/php/Dockerfile -t src_php:latest .
# # doeckr imageのタグ付け
- docker tag src_nginx:latest ${REPO_URI_NGINX}:${ENV}-${IMAGE_TAG}
- docker tag src_php:latest ${REPO_URI_PHPFPM}:${ENV}-${IMAGE_TAG}
- docker images
Dockerfileを使ってDockerイメージをビルドし、タグ付けを行います。
以下に詳細を説明させていだだきます。
- buildの実行
- docker build -f ./infra/nginx/Dockerfile -t src_nginx:latest .
- docker build -f ./infra/php/Dockerfile -t src_php:latest .
上記はDockerfileのコマンドを実行し、イメージを作成するためのビルドコマンドになります。
- コマンドの詳細(nginxのビルドより)
docker build
:ビルドコマンド-f ./infra/nginx/Dockerfile
:Dockerfile の位置するパスを指定します- -t src_nginx:latest:イメージにタグを付けます
.
:Dockerfile内のコマンドを実行するパスを指定しています
※こちらの設定をローカルとAWSで合わせるためにdocker-composeではbuildの設定を下記のように記載してくださいbuild:
context: .
dockerfile: ./infra/nginx/Dockerfile
doeckr imageのタグ付け
- docker tag src_nginx:latest ${REPO_URI_NGINX}:${ENV}-${IMAGE_TAG}
- docker tag src_php:latest ${REPO_URI_PHPFPM}:${ENV}-${IMAGE_TAG}
こちらは先ほど作成したlatest
とタグのついたイメージ(src_nginx
, src_php
)を、${ENV}-${IMAGE_TAG}
とタグのついた<イメージ名> = <ECRのURI>という形にして、新しいイメージにしています。
src_nginx:latest
を<ECRのURI>:<タグ>
という形にしないと、ECRで使えないからです。
- docker imagesの確認
- docker images
最後にdockerイメージがコマンドの実行通りにできているか確認しています。ここまでうまくいっていれば、buildフェーズは完了となります。
[docker imagesの実行ログ]
[Container] 2024/07/19 13:13:18.663560 Running command docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
<AWS_ACCOUNT_ID>.dkr.ecr.ap-northeast-1.amazonaws.com/dev-verify-kim-ecr/php-fpm dev-b71e6ed 4f00b703741d Less than a second ago 482MB
src_php latest 4f00b703741d Less than a second ago 482MB
<AWS_ACCOUNT_ID>.dkr.ecr.ap-northeast-1.amazonaws.com/dev-verify-kim-ecr/nginx dev-b71e6ed 706f1da034da About a minute ago 21.8MB
src_nginx latest 706f1da034da About a minute ago 21.8MB
composer 2.2 e05114c93971 5 weeks ago 196MB
php 8.1-fpm-buster f04303983837 13 months ago 357MB
nginx 1.20-alpine d6a1e2ab00f7 2 years ago 21.8MB
4. post_buildフェーズの解説
このフェーズでは、ビルドが完了した後に行う作業を定義します。
post_build:
commands:
- echo Build completed on `date`
- echo Pushing the Docker images...
# # dockr pushを実行
- docker push ${REPO_URI_NGINX}:${ENV}-${IMAGE_TAG}
- docker push ${REPO_URI_PHPFPM}:${ENV}-${IMAGE_TAG}
# # taskdef.jsonの変数の変更
- sed -i -e "s#<AWS_ACCOUNT_ID>#${AWS_ACCOUNT_ID}#" taskdef.json
- sed -i -e "s#<REGION>#${REGION}#" taskdef.json
- sed -i -e "s#<SERVICE>#${SERVICE}#" taskdef.json
- sed -i -e "s#<ENV>#${ENV}#" taskdef.json
- sed -i -e "s#<TAG>#${TAG}#" taskdef.json
- sed -i -e "s#<TASK_CPU>#${TASK_CPU}#" taskdef.json
- sed -i -e "s#<TASK_MEMORY>#${TASK_MEMORY}#" taskdef.json
ここでは、ビルドが完了したことを通知し、DockerイメージをECRにプッシュします。また、taskdef.jsonファイルの変数を適切に設定し、最終的にtaskdef.jsonのファイルの中身を表示します。
以下に詳細を説明させていだだきます。
- イメージのpush
- docker push ${REPO_URI_NGINX}:${ENV}-${IMAGE_TAG}
- docker push ${REPO_URI_PHPFPM}:${ENV}-${IMAGE_TAG}
ECRにbuildしたイメージをpushしています。これで、ECRでDockerイメージが使用可能になります。
- taskdef.jsonの変数を置換
- sed -i -e "s#<AWS_ACCOUNT_ID>#${AWS_ACCOUNT_ID}#" taskdef.json
- sed -i -e "s#<REGION>#${REGION}#" taskdef.json
- sed -i -e "s#<SERVICE>#${SERVICE}#" taskdef.json
- sed -i -e "s#<ENV>#${ENV}#" taskdef.json
- sed -i -e "s#<TAG>#${TAG}#" taskdef.json
- sed -i -e "s#<TASK_CPU>#${TASK_CPU}#" taskdef.json
- sed -i -e "s#<TASK_MEMORY>#${TASK_MEMORY}#" taskdef.json
下記に示すECSタスクを定義するファイル(taskdef.json)に、いくつか変数を定義しています。Deployする前に、sedコマンドで置換することで、変数に値が入るようにしています。
※${TASK_CPU}、${TASK_MEMORY}は「2. pre_buildフェーズの解説」で解説したCodeBuild上で定義した環境変数
- taskdef.json
{
"containerDefinitions": [
{
"name": "nginx",
"image": "<AWS_ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com/<ENV>-<SERVICE>-ecr/nginx:<TAG>",
"cpu": 0,
"portMappings": [
{
"hostPort": 80,
"protocol": "tcp",
"containerPort": 80
}
],
"essential": true,
"environment": [],
"mountPoints": [],
"volumesFrom": [],
"logConfiguration": {
"logDriver": "awslogs",
"secretOptions": null,
"options": {
"awslogs-group": "/ecs/<ENV>-<SERVICE>-cluster/nginx",
"awslogs-region": "<REGION>",
"awslogs-stream-prefix": "nginx"
}
},
"systemControls": []
},
{
"name": "php-fpm",
"image": "<AWS_ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com/<ENV>-<SERVICE>-ecr/php-fpm:<TAG>",
"cpu": 0,
"portMappings": [
{
"name": "php-fpm",
"hostPort": 9000,
"protocol": "tcp",
"containerPort": 9000
}
],
"essential": true,
"entryPoint": [],
"command": [],
"environment": [],
"mountPoints": [],
"volumesFrom": [],
"secrets": [
{
"name": "APP_NAME",
"valueFrom": "arn:aws:ssm:<REGION>:<AWS_ACCOUNT_ID>:parameter/<ENV>/<SERVICE>/APP/APP_NAME"
},
{
"name": "APP_ENV",
"valueFrom": "<ENV>"
},
{
"name": "APP_DEBUG",
"valueFrom": "arn:aws:ssm:<REGION>:<AWS_ACCOUNT_ID>:parameter/<ENV>/<SERVICE>/APP/DEBUG"
},
{
"name": "APP_URL",
"valueFrom": "arn:aws:ssm:<REGION>:<AWS_ACCOUNT_ID>:parameter/<ENV>/<SERVICE>/APP/APP_URL"
},
{
"name": "LOG_CHANNEL",
"valueFrom": "arn:aws:ssm:<REGION>:<AWS_ACCOUNT_ID>:parameter/<ENV>/<SERVICE>/APP/LOG_CHANNEL"
},
{
"name": "LOG_LEVEL",
"valueFrom": "arn:aws:ssm:<REGION>:<AWS_ACCOUNT_ID>:parameter/<ENV>/<SERVICE>/APP/LOG_LEVEL"
},
{
"name": "DB_CONNECTION",
"valueFrom": "arn:aws:ssm:<REGION>:<AWS_ACCOUNT_ID>:parameter/<ENV>/<SERVICE>/AURORA/DB_CONNECTION"
},
{
"name": "DB_HOST",
"valueFrom": "arn:aws:ssm:<REGION>:<AWS_ACCOUNT_ID>:parameter/<ENV>/<SERVICE>/AURORA/DB_HOST"
},
{
"name": "DB_PORT",
"valueFrom": "arn:aws:ssm:<REGION>:<AWS_ACCOUNT_ID>:parameter/<ENV>/<SERVICE>/AURORA/DB_PORT"
},
{
"name": "DB_DATABASE",
"valueFrom": "arn:aws:ssm:<REGION>:<AWS_ACCOUNT_ID>:parameter/<ENV>/<SERVICE>/AURORA/DB_DATABASE"
},
{
"name": "DB_USERNAME",
"valueFrom": "arn:aws:ssm:<REGION>:<AWS_ACCOUNT_ID>:parameter/<ENV>/<SERVICE>/AURORA/DB_USERNAME"
},
{
"name": "APP_KEY",
"valueFrom": "arn:aws:ssm:<REGION>:<AWS_ACCOUNT_ID>:parameter/<ENV>/<SERVICE>/APP/APP_KEY"
},
{
"name": "DB_PASSWORD",
"valueFrom": "arn:aws:ssm:<REGION>:<AWS_ACCOUNT_ID>:parameter/<ENV>/<SERVICE>/AURORA/DB_PASSWORD"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"secretOptions": null,
"options": {
"awslogs-group": "/ecs/<ENV>-<SERVICE>-cluster/php",
"awslogs-region": "<REGION>",
"awslogs-stream-prefix": "php"
}
},
"systemControls": []
}
],
"family": "<ENV>-<SERVICE>-ecs-taskdef",
"taskRoleArn": "arn:aws:iam::<AWS_ACCOUNT_ID>:role/Role-<ENV>-<SERVICE>-ecs-service-cmn-task",
"executionRoleArn": "arn:aws:iam::<AWS_ACCOUNT_ID>:role/Role-<ENV>-<SERVICE>-ecs-service-cmn-taskexec",
"networkMode": "awsvpc",
"requiresCompatibilities": [
"FARGATE"
],
"cpu": "<TASK_CPU>",
"memory": "<TASK_MEMORY>",
"status": "ACTIVE",
"enableExecuteCommand": true,
"propagateTags": "SERVICE",
"runtimePlatform": {
"cpuArchitecture": "ARM64",
"operatingSystemFamily": "LINUX"
},
"tags": [
{
"key": "Name",
"value": "<ENV>-<SERVICE>-ecs-taskdef"
},
{
"key": "Description",
"value": "ECS Task Definition"
},
{
"key": "Env",
"value": "<ENV>"
},
{
"key": "Service",
"value": "<SERVICE>"
},
{
"key": "Terraform",
"value": "true"
}
]
}
- taskdef.jsonの確認
- cat taskdef.json
最後にtaskdef.jsonが問題なく定義されているかCodeBuildのログから確認します。catで出力した際に、環境変数が置換されていなかったりすると、再びbuildspec.ymlの修正が必要になります。
5. ビルドを実行しよう!
buildspec.ymlが書けたら、パイプラインからビルドを実行してみましょう!
ここまで書ければ、Dockerfileに問題がなければビルドは成功するはずです。パイプラインを回し、下記のようにBuildの部分が緑色になれば成功です!
まとめ
いかがでしたでしょうか?
ECSを立ち上げるのって、本当に難しいですよね。インフラ構築の壁の一つだと思っています。。。
私はbuildspec.ymlの記述でハマりまくったので、今回新たな犠牲者が出ないように今回このブログを記載しました。
このブログを読んでくださいました皆様のお役に立てますと幸いです。
また、buildspec.ymlでのUnitテストの実行方法についても、ブログを記載していますので、Unitテストを実行する際はこちらも併せてご拝見いただけますと幸いです。