概要
DockerでPHP+Nginxの環境を作り、ECSにデプロイしブラウザでこれを表示することがゴールです。
備忘録としてECSにデプロイするまでの最低限の手順を辿ることが目的なので、各サービスや用語等の説明はあまりありません。
GitリポジトリにはAWS CodeCommitを、デプロイ時のビルド作業はCodeBuildを利用します。今回はCodeDeployは利用しません。
構成
本記事はデプロイまでの手順に主眼を置いているので、構成は以下の通り最低限のものになっています。一番上の buildspec.yml
はECSにデプロイする頃に作成するので、一旦は無視して大丈夫です。
.
├── buildspec.yml
├── docker-compose.yml
├── infra
│ ├── nginx
│ │ ├── Dockerfile
│ │ └── default.conf
│ └── php
│ └── Dockerfile
└── src
└── index.php
CodeCommitの設定
GitホスティングサービスはCodeCommitを使います。Githubを使ったことがあれば特に難しいことはないと思います。まずこの設定をしていきます。
AWSにログインし、CodeCommitのページにアクセス、右上の「リポジトリを作成」をクリック。
今回miniapp
という名前のプロジェクトにするので、リポジトリ名をminiapp
にします。リポジトリ名だけ入力して作成ボタンをクリック。
次の画面で接続の仕方が表示されるので、説明通りにやればOKです。
HTTPSでもSSHでもお好きな方法でリポジトリをローカルにクローンします。
$ git clone ssh://git-codecommit.{お使いのリージョン}.amazonaws.com/v1/repos/miniapp
Gitの初期操作
ローカルにリポジトリをクローンしてプロジェクトのディレクトリができたら一度リモートにpushしておきましょう。
$ cd miniapp
$ echo '<?php echo "Hello PHP + Nginx";' >> index.php
$ git add .
$ git commit -m 'first commit'
$ git push
index.phpは後ほど正しいディレクトリに移動させます。
ローカルでアプリを完成させる
docker-compose.ymlの作成
最初にdocker-compose.ymlを作成して、全体像を把握します。
version: '3.9'
services:
app:
build:
context: .
dockerfile: ./infra/php/Dockerfile
web:
build:
context: .
dockerfile: ./infra/nginx/Dockerfile
target: local
ports:
- 80:80
見ての通りとても簡単な構成になります。appという名でPHPが動くコンテナを、webという名でNginxが動くコンテナを作成します。ローカルで動かす場合とECSにデプロイした場合で使いたいdockerイメージに少し差があるので、webコンテナの方にtarget:local
との記述をしています。詳しくはDockerfileを作成する際に見ていきます。
infra/php以下の作成
このディレクトリで必要なのはDockerfileのみです。今回はとりあえずPHPが動けば良いので、最低限以上のことはしません。
FROM php:8.1-fpm
RUN apt-get update && \
apt-get -y install --no-install-recommends less git unzip libzip-dev libicu-dev libonig-dev && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* && \
docker-php-ext-install intl pdo_mysql zip bcmath
COPY ./src /data
FROM php:8.1-fpm
ベースとなるイメージは php:8.1-fpm
を利用します。
RUN apt-get update && \
apt-get -y install --no-install-recommends less git unzip libzip-dev libicu-dev libonig-dev && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* && \
docker-php-ext-install intl pdo_mysql zip bcmath
不要なものもありますが、とりあえず色々インストールしています。
COPY ./src /data
コンテナの /data
以下を公開用のディレクトリにするので、ローカルの src
を /data
にコピーします。
後ほど作成しますがsrcディレクトリの中にGitの初期操作時に作ったindex.phpを移動し、それをブラウザで表示させるのがゴールです。
infra/nginx以下の作成
ここではDockerfileと、appコンテナと通信するためのNginxの設定を書いたdefault.confを作成します。
default.conf
nginxのコンテナイメージに元々存在するdefault.confを上書きするためのファイルを作成します。
ここで確認しておくべき箇所はコメントを書いている2ヶ所です。1つ目は公開ディレクトリを/data
にしているroot /data
と、2つ目はPHPを扱うappコンテナとの通信に関するfastcgi_pass localhost:9000
の部分です。
fastcgi_pass localhost:9000
に関して簡単に説明すると、後ほどECSにデプロイする際にはPHPが動いているappコンテナと通信するために通信先をlocalhost
とする必要があるのですが、ローカルで動かす際にはapp
というコンテナ名で通信先を指定したいので、ローカルで動かす場合のみこの記述を修正する必要があります。これに関しては後ほどDockerfile内でそれに対応する処理を書いていきます。
server {
listen 80;
server_name example.com;
root /data; # 公開するディレクトリを/dataに設定
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
index index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_pass localhost:9000; # phpを処理する場合はappコンテナにバトンタッチするイメージ
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
src/index.php
<?php
echo 'Hello PHP + Nginx';
index.phpは最初に作成したので、src/
に移動させておきます。
$ mv index.php src/
Dockerfile
FROM nginx:latest AS production
COPY ./infra/nginx/default.conf /etc/nginx/conf.d/default.conf
COPY ./src /data
FROM production AS local
RUN sed -i -e s/localhost:9000/app:9000/ /etc/nginx/conf.d/default.conf
FROM nginx:latest AS production
ベースとなるイメージはnginx:latest
です。一番下にローカルでのみ実行するコマンドを記述しているのですが、ECSデプロイの際にはそれを適用したくないので、区別するためにproduction
という名前をつけています。
参考: マルチステージ ビルドを使う
COPY ./infra/nginx/default.conf /etc/nginx/conf.d/default.conf
先ほど作成したinfra/nginx/default.conf
(nginxの設定ファイル)でコンテナイメージに元々存在するdefault.confを上書きします。
COPY ./src /data
PHPの方のDockerfileと同様です。
FROM production AS local
RUN sed -i -e s/localhost:9000/app:9000/ /etc/nginx/conf.d/default.conf
こちらにはlocal
という名前をつけています。ローカルではdocker-compose.ymlでwebコンテナの記述にtarget: local
としてproduction
のイメージをビルドしてさらにlocal
も適用します。CodePipelineを使ってECSをデプロイする際には、後ほど出てくるbuildspec.yml
にwebコンテナのビルドを記述する際target: production
としてビルドにproduction
しか適用されないようにします。
いったん完成!
ここまでで以下のようなディレクトリ構成になりました。
.
├── docker-compose.yml
├── infra
│ ├── nginx
│ │ ├── Dockerfile
│ │ └── default.conf
│ └── php
│ └── Dockerfile
└── src
└── index.php
ではイメージをビルドしていきます。コマンドを実行するのはプロジェクトルートのディレクトリです。間違ってinfra/
やsrc/
で実行しないように気をつけましょう。
$ docker compose build
ビルドが完了したらイメージを確認します。
$ docker images
「REPOSITORY」にminiapp_app
とminiapp_web
が出てきたらOKです。
ビルドしたイメージからコンテナを作ります。
$ docker compose up -d
-d
をつけることでデタッチモードにしていますが、アクセスした時のログなどをみたい場合は-d
なしで実行します。その場合、コンテナを停止したいときはcontrol+C
でできます。
ブラウザでlocalhost
にアクセスするとこちらの画面が表示されます。
ここまで確認できたらコンテナを削除しておきます。
$ docker compose down
出来上がったコードをリモートにpushします。
$ git add .
$ git commit -m 'create php and nginx app'
$ git push
ECRへのpush
ECRにdockerイメージをpushします。ECRのコンソールへはECSのページの左にある「リポジトリ」からとべます。「リポジトリを作成」をクリック
appコンテナ用のリポジトリとwebコンテナ用のリポジトリの2つを作ります。appコンテナ用ののものはリポジトリ名をminiapp_app
、他はデフォルトのままで下の「リポジトリを作成」をクリック。webコンテナ用のものはリポジトリ名をmini_web
として作成します。
できたリポジトリを確認すると右上に「プッシュコマンドの表示」があります。これを開くとECRにプッシュするための手順が書いてあるので、この通りに進めてappコンテナのイメージとwebコンテナのイメージをそれぞれプッシュします。AWS CLIからECRへアクセスする権限が必要となります。
CodePipelineを使ってECSにデプロイする
ここからはECSにデプロイするための作業をしていきます。
タスク定義
最初にタスク定義を作成します。ECSコンソール画面の左のメニューからタスク定義を選択、タスク定義画面に移ったら「新しいタスク定義の作成」をクリック
タスク定義名はminiapp
とします。オペレーティングシステムにLinuxを選択します。
タスク実行ロールは、「ecsTaskExcecutionRole」が存在すればそれを選択、ない場合は「新しいロールの作成」を選択。タスクサイズはメモリもCPUも最小のもの(メモリ0.5GBとCPU0.25vCPU)を選択します。ここまでできたらコンテナの追加をクリック。
appコンテナとwebコンテナの2つを作ります。最初にappコンテナです。コンテナ名はapp
、イメージはECRで作ったminiapp_appリポジトリの「latest」というイメージタグがついているもののURI
を使います。ポートマッピングは9000
と入力してあとはデフォルトのままで「追加」をクリック。続いてもう一度「コンテナの追加」を押しwebコンテナを作ります。コンテナ名はweb
、イメージはminiapp_appリポジトリの「latest」というイメージタグがついているもののURI
、ポートマッピングは80
で作成してください。
これでタスク定義の設定は完了です。スクロールして一番下にある「作成」をクリックします。
クラスター
次はクラスターを作成します。ECSコンソールのクラスターから、「クラスターの作成」をクリック
今回はFargateでデプロイするのでデフォルトのままで「次のステップ」をクリック
クラスター名はminiapp
とします。ここで新しくVPCを作成したい場合はVPCの作成にチェックをつけて設定をしましょう。「作成」をクリック。
サービス
項目はたくさんありますが入力するところはあまり多くありません。
起動タイプにFARGATE
、タスク定義はminiapp
、サービス名はminiapp_service
、タスク数は1
とします。他はデフォルトのままで「次のステップ」へいきます。
次はネットワーク構成の設定です。適切なVPC、サブネット、セキュリティグループを選択すれば大丈夫です。httpでアクセス可能なら問題ないと思います。これから作成する場合はこちらに書いているとおり作成すれば良いと思います。Amazon ECS を使用するようにセットアップする
他はデフォルトのままで「次のステップ」。
次はAutoScalingの設定です。今回は必要ないので「次のステップ」。
最後に設定を確認し、「サービスの作成」でサービスの作成は完了です。
CodePipeline
CodePipelineに移ります。CodePipelineのページにアクセスし、右上の「パイプラインを作成する」をクリック。
パイプライン名はminiapp
にします。パイプライン名を入力すると新しいサービスロールのロール名も自動的に入力されます。このロール名のまま使うことにします。「次に」をクリック。
ソースプロバイダーとしてCodeCommit
を、リポジトリは先ほど作成したminiapp
、ブランチ名はmain
を選択します。あとは初期値のままにしておきます。今後miniappのmainブランチに変更が生じたときにパイプラインが自動的に起動しデプロイがされることになります。「次に」をクリック。
ここではCodeBuildの設定をします。「プロバイダーを構築する」でAWS CodeBuildを選択。リージョンはお使いのリージョンを選択します。
ここでCodeBuildのビルドプロジェクトを作成します。「プロジェクトの作成」をクリック。
別のウィンドウでCodeBuildの画面が立ち上がるので設定していきます。プロジェクト名はminiapp
にします。
これより下はデフォルトのままで大丈夫です。「CodePipelineに進む」をクリック。
これでCodeBuildのプロジェクトができたので、「次に」をクリック。
ここでCodePipelineの設定は中断し、ローカルで必要なファイルを作成していきます。プロジェクトルートにbuildspec.yml
を作成します。こちらに書いているものを雛形に使っています。Tutorial: Amazon ECS Standard Deployment with CodePipeline
日本語で記載している部分は適宜修正してください。
version: 0.2
phases:
pre_build:
commands:
- echo Logging in to Amazon ECR...
- aws --version
- aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin {12桁のアカウントID}.dkr.ecr.{ap-northeast-1などのリージョン名}.amazonaws.com
- REPOSITORY_URI_APP={ECRのminiapp_appリポジトリのURI}
- REPOSITORY_URI_WEB={ECRのminiapp_webリポジトリのURI}
- COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
- IMAGE_TAG=${COMMIT_HASH:=latest}
build:
commands:
- echo Build started on `date`
- echo Building the Docker image...
- docker build -t $REPOSITORY_URI_APP:latest -f infra/php/Dockerfile .
- docker build -t $REPOSITORY_URI_WEB:latest -f infra/nginx/Dockerfile --target production .
- docker tag $REPOSITORY_URI_APP:latest $REPOSITORY_URI_APP:$IMAGE_TAG
- docker tag $REPOSITORY_URI_WEB:latest $REPOSITORY_URI_WEB:$IMAGE_TAG
post_build:
commands:
- echo Build completed on `date`
- echo Pushing the Docker images...
- docker push $REPOSITORY_URI_APP:latest
- docker push $REPOSITORY_URI_WEB:latest
- docker push $REPOSITORY_URI_APP:$IMAGE_TAG
- docker push $REPOSITORY_URI_WEB:$IMAGE_TAG
- echo Writing image definitions file...
- printf '[{"name":"app","imageUri":"%s"}]' $REPOSITORY_URI_APP:$IMAGE_TAG > imagedefinitions.json
- printf '[{"name":"web","imageUri":"%s"}]' $REPOSITORY_URI_WEB:$IMAGE_TAG > imagedefinitions.json
artifacts:
files: imagedefinitions.json
mainブランチの変更でCodePipelineは発火します。CodeCommitからソースコードの情報がCodeBuildに渡され、それを元にCodeBuildのビルド環境でbuildspec.yml
に書かれたコマンドを実行しコンテナイメージをビルド、ECRにプッシュされます。
ここではwebコンテナのdocker build
にtarget production
を指定することで、webコンテナのDockerfileから適切なイメージがビルドできます。
ここで改めてCodeCommitにプッシュします。
$ git add .
$ git commit -m 'created buildspec.yml'
$ git push
ここでCodePipelineコンソールに戻ります。
デプロイプロバイダーにECS
を選択、クラスター名、サービス名は先ほど作成したものを選択し、「次に」をクリック。
「パイプラインを作成する」をクリックするとパイプラインが出来上がり、一連の流れが実行されます。
するとCodeBuildでパイプラインが失敗します。CodeBuildのビルドプロジェクトminiapp
から入り、ビルド履歴で最新の「ビルドの実行」をクリック、「フェーズ詳細」タブを確認すると「PRE_BUILD」でECRにログインするコマンドが失敗しています。
これはCodePipelineからECRにアクセスする権限がないためです。「ビルド詳細」タブの「環境」にサービスロールを見つけクリックします。
「AmazonEC2ContainerRegistryPowerUser」を探してポリシーをアタッチします。
これでビルドがうまくいくはずなのでCodePipelineに戻り再試行します。
これで完了です。この通りにやってうまく行かない場合は、権限周りに問題がある可能性が高いと思います。こけた所に関係するIAMロールなどの権限を確認してみましょう。
全て完了したら適宜ECSのサービスやECRリポジトリなど削除しておきましょう。