Rails(Docker)をCodeBuildでCIする (CI/CDまでの道⑧)
はじめに
前回はFargateにデプロイすることができるようになりました。本日はついにタイトル「CI/CDまでの道」にあるとおり、CodeBuildによるCIに挑戦していきます。
第8回にしてやっとタイトル回収できました (当初はできるかすら不安でしたのでよかったです)
CodeBuildに挑戦していくのですが、そこでCIを早く回すためにDockerにマルチステージビルドの変更を加えるのも同時にやっていきます。
作成に利用するリポジトリはこちら
CI/CDまでの道シリーズ
- rails6+mysqlのdocker環境構築 (CI/CDまでの道①)
- dockerにwebpacker環境構築(jquery, bootstrap5, vue) (CI/CDまでの道②)
- rails(docker)に必要なgemを追加する (CI/CDまでの道③)
- rails(docker)にnginxを導入する (CI/CDまでの道④)
- rails(docker)をproductionモードで起動してみる (CI/CDまでの道⑤)
- ec2にdocker-composeでrailsをデプロイする (CI/CDまでの道⑥)
- Fargateにrailsをデプロイする (CI/CDまでの道⑦)
- Rails(Docker)をCodeBuildでCIする (CI/CDまでの道⑧)
環境
- wsl2 (ubuntu20.04)
- docker 20.10.9
- docker-compose 1.29.1
- git 2.25.1
- vscode
マルチステージビルド
まずはdevelopmentとproductionで同じDockerfileを利用しているので、分離したいと思います。
今後Dockerイメージを作る際に不要なビルドが発生してしまい時間がかかってしまうのを防ぐために行います。
マルチステージビルドについて知らない方は以下を参考にしてみてください。詳しい説明は省略します。
まずはDockerfileを以下に変更します。
FROM ruby:alpine3.13 as Base
ARG UID
RUN adduser -D app -u ${UID:-1000} && \
apk update \
&& apk add --no-cache gcc make libc-dev g++ mariadb-dev tzdata nodejs~=14 yarn
WORKDIR /myapp
COPY Gemfile .
COPY Gemfile.lock .
COPY package.json .
COPY yarn.lock .
RUN bundle install --jobs=4
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
# Development
FROM base as development
RUN yarn install
COPY --chown=app:app . /myapp
USER app
RUN mkdir -p tmp/sockets tmp/pids
EXPOSE 3000
CMD ["sh", "-c", "./bin/webpack && bundle exec rails s -p 3000 -b '0.0.0.0'"]
# build
FROM base as build
RUN mkdir -p tmp/sockets tmp/pids
COPY --chown=app:app . /myapp
RUN yarn install
# compile
FROM build as compile
ENV NODE_ENV=production
RUN ./bin/webpack
# production
FROM compile as production
ENV RAILS_ENV=production
VOLUME /myapp/public
VOLUME /myapp/tmp
CMD /bin/sh -c "bundle exec puma -C config/puma.rb"
マルチステージビルドで分けた以外はほぼ同じです。
次にdocker-compose.ymlを変更します。
version: "3.9"
services:
rails:
build:
context: .
target: development
container_name: rails
volumes:
- .:/myapp
- /myapp/node_modules
- public-data:/myapp/public
ports:
- "3000:3000"
env_file:
- .env
depends_on:
- db
environment:
WEBPACKER_DEV_SERVER_HOST: webpacker
webpacker:
build:
context: .
target: development
container_name: webpacker
volumes:
- .:/myapp
- /myapp/node_modules
- public-data:/myapp/public
command: ./bin/webpack-dev-server
environment:
WEBPACKER_DEV_SERVER_HOST: 0.0.0.0
ports:
- "3035:3035"
db:
image: mysql:8.0.27
container_name: db
environment:
TZ: Asia/Tokyo
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
ports:
- "3306:3306"
volumes:
- db:/var/lib/mysql
volumes:
db:
driver: local
bundle:
driver: local
public-data:
Dockerfileのどの位置のビルド(development, productionなど)をするかをtargetで選択しました。
docker-compose.production.ymlも変更します。
version: "3.7"
services:
rails:
build:
context: .
target: production
container_name: rails
volumes:
- .:/myapp
- public-data:/myapp/public
- tmp-data:/myapp/tmp
- log-data:/myapp/log
- /myapp/node_modules
env_file:
- .env
depends_on:
- db
environment:
DB_HOST: db
DB_USERNAME: root
db:
image: mysql:8.0.27
container_name: db
environment:
TZ: Asia/Tokyo
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
ports:
- "3306:3306"
volumes:
- db:/var/lib/mysql
web:
build:
context: containers/nginx
volumes:
- public-data:/myapp/public
- tmp-data:/myapp/tmp
ports:
- 80:80
depends_on:
- rails
volumes:
db:
driver: local
public-data:
tmp-data:
log-data:
こちらもtargetを変更しました。
また、DBで.envの中にあるRDS用の変数が適応されて起動できなくなってしまうためenvironmentを設定して上書きしています。
次にNginxのDockerfileを修正します。
FROM nginx:alpine
# インクルード用のディレクトリ内を削除
RUN rm -f /etc/nginx/conf.d/*
# Nginxの設定ファイルをコンテナにコピー
ADD nginx.conf /etc/nginx/conf.d/myapp.conf
# docker buildのときは以下のコメントをはずす
# ADD /containers/nginx/nginx.conf /etc/nginx/conf.d/myapp.conf
# ビルド完了後にNginxを起動
CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf
ECRにPushする際にはADDのところを切り替えてPushするように変更しました。(nginxは頻繁にECRにPushすることはないのでほぼ手間は発生しません)
これはdocker buildとdocker-composeでのファイルパスに違いがあるためエラーになるのでこのようにしています。
次にconfig/webpacker.ymlを修正します。
# Note: You must restart bin/webpack-dev-server for changes to take effect
default: &default
source_path: app/javascript
source_entry_path: packs
public_root_path: public
public_output_path: packs
cache_path: tmp/cache/webpacker
webpack_compile_output: true
# Additional paths webpack should lookup modules
# ['app/assets', 'engine/foo/app/assets']
additional_paths: []
# Reload manifest.json on all requests so we reload latest compiled packs
cache_manifest: false
# Extract and emit a css file
extract_css: true # 変更
(省略)
CSSをコンパイルしない設定にしていたので変更しました。
最後にentrypoint.shも変更しておきます。
このファイルはFargateにデプロイする際に適宜変えていきます。
#!/bin/sh
set -e
rm -f /myapp/tmp/pids/server.pid
# bundle exec rails db:create
# bundle exec rails db:migrate
# bundle exec rails db:seed
exec "$@"
環境変数RAILS_ENVをなくしました。これはDockerfile内で定義しているので不要になりました。
起動確認
config/credentials.yml.encを削除して新たなconfig/master.keyとconfig/credentials.yml.encを配置してください。
まずは開発環境から起動していきます。
$ docker-compose build --no-cache
$ docker-compose up
# 別のターミナルを開く
$ docker exec -it rails sh
$ rails db:create
# loclahost:3000/testにアクセス
# 終了したら
$ docker-compose down -v
たまにキャッシュで以前のRAILS_ENV=productionが残って反映されてしまうことがあるので開発と本番を切り替えるときは--no-cacheでビルドしましょう
localhost:3000/testにアクセスできれば成功です。(CSSとJSが効いていることを確認してください)
次に本番環境で起動します。
$ docker-compose -f docker-compose.production.yml build --no-cache
$ docker-compose -f docker-compose.production.yml up
# 別のターミナルを開く
$ docker exec -it rails sh
$ rails db:create
# localhost/testにアクセス
# 終了したら
$ docker-compose -f docker-compose.production.yml down -v
localhost/testにアクセスできれば成功です。
CodeBuild (ビルド)
まずはCodeBuildを利用して、Gitのmasterブランチにプルリクエストがマージされた時点でCodeBuildが動いて、最新のイメージがECRにPushされるところまでを行っていきます。
buildspec.yml作成
そのためにはCodeBuildで動かす処理を書いたbuildspec.ymlを作成します。
version: 0.2
env:
variables:
DOCKER_BUILDKIT: "1"
phases:
pre_build:
commands:
- echo Logiging in to Amazon ECR...
- aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
- docker pull $AWS_ECR_BUILD_REPOSITORY:latest
- docker tag $AWS_ECR_BUILD_REPOSITORY:latest build:latest
- IMAGE_TAG=$(echo ${CODEBUILD_RESOLVED_SOURCE_VERSION} | head -c 7)
build:
commands:
- echo Build started on `date`
- echo Building the Docker image...
- docker build --target production --cache-from build:latest --build-arg SECRET_KEY_BASE=$SECRET_KEY_BASE --build-arg BUILDKIT_INLINE_CACHE=1 -t $IMAGE_NAME:$IMAGE_TAG .
- docker build --target build --cache-from build:latest --build-arg SECRET_KEY_BASE=$SECRET_KEY_BASE --build-arg BUILDKIT_INLINE_CACHE=1 -t build:latest .
- docker tag $IMAGE_NAME:$IMAGE_TAG $AWS_ECR_BUILD_REPOSITORY:$IMAGE_TAG
- docker tag build:latest $AWS_ECR_BUILD_REPOSITORY:latest
post_build:
commands:
- echo Build completed on `data`
- echo Pushing the Docker image...
- docker images
- docker push $AWS_ECR_BUILD_REPOSITORY:latest
- docker push $AWS_ECR_BUILD_REPOSITORY:$IMAGE_TAG
- echo "[{\"name\":\"sample-rails\",\"imageUri\":\"${AWS_ECR_BUILD_REPOSITORY}:${IMAGE_TAG}\"}]" > imagedefinitions.json
artifacts:
files:
- imagedefinitions.json
ほぼほぼコード通りですので、特徴的なところだけ説明します。
- build:latestというタグを前回のイメージにつけてキャッシュとして機能させることでビルドを早くする
- BUILDKIT_INLINE_CACHEを設定すると早くなるらしい
- 最後にCodeDeployで利用するらしい`imagedefinitions.jsonを出力、これはイメージとの対応関係を示すファイル
Dockerfileのbuildのステージまでをうまくキャッシュとして利用する工夫がされています。
GitHubリポジトリ作成
実際にGitのリポジトリを作成します。
今回は「CICD_Road_Sample」という名前でGitHubにリポジトリを作成しました。

ここまでの作業内容を作成したリポジトリにPushします。
$ git init
$ git add .
$ git commit -m "リポジトリ作成"
$ git remote add origin [リポジトリのURL]
$ git push origin master
Pushが確認できました。

S3バケット作成
S3で`バケットを作成します。
名前の重複ができないので、それぞれ名前をつけてください。
作り方は直感でわかると思いますので省略します。
名前以外はデフォルトで大丈夫です。

ECRリポジトリ作成
ECRでsample-railsというリポジトリを作成します。

ここでbuildspec.ymlで利用するキャッシュ用のイメージをPushしておきます。
buildステージまでをキャッシュ用のイメージとして利用します。
右上のプッシュコマンドの表示を実行します。
2つ目だけ以下を実行して下さい。
# 2つ目
$ docker build --target build --no-cache -t sample-rails .
latestのタグでPushできていればOKです。

CodeBuildの作成
次にAWSマネジメントコンソールの「サービス」メニューからCodeBuildのダッシュボードを開きます。

「ビルドプロジェクトを作成」をクリックします。
デフォルト値から変更があるものを表に示します。
| 項目 | 値 |
|---|---|
| プロジェクト名 | sample-build |
| ソースプロバイダ | GitHub(接続してください) |
| リポジトリ | GitHubのリポジトリ |
| GitHubのリポジトリ | CICD_Road_Sample |
| コードの変更がこのレポジトリにプッシュされるたびに再構築する | ☑ |
| イベントタイプ | PULL_REQUEST_MERGED |
| これらの条件でビルドを開始する: BASE_REF - オプショナル | ^refs/heads/master$ |
| オペレーションシステム | Amazon Linux2 |
| ランタイム | Standard |
| イメージ | aws/codebuild/amazonlinux2-x86_64-standard:3.0 |
| イメージのバージョン | aws/codebuild/amazonlinux2-x86_64-standard:3.0-21.10.15 |
| 特権付与 | ☑ |
| サービスロール | 新しいサービスロール |
| ロール名 | sample-build-role |
| 追加設定:環境変数 | - AWS_DEFAULT_REGION: ap-northeast-1 - AWS_ACCOUNT_ID: [アカウントID] - AWS_ECR_BUILD_REPOSITORY: [作成したECRのURL] - IMAGE_NAME: sample-rails - SECRET_KEY_BASE: [マスターキーの値] |
| アーティファクト:タイプ | Amazon S3 |
| アーティファクト:バケット名 | [作成したS3バケットの名前] |


BASE_REFでどのブランチにマージされたときに動くかを決められます。今回はmasterブランチにマージされたときに走るようにしました。何も設定しないとPushで起動するようになります。

既存のサービスロールがある場合は選択してください
既にポリシーがある場合はエラーになります(IAMから消せます)
あとで作成したロールにECRのアクセス権をアタッチします。

AWS_ECR_BUILD_REPOSITORYはタグを入れないでください(:latestなど)

「ビルドプロジェクトを作成する」をクリックして作成します。
ロールのポリシー変更
IAMを開いて先ほど作成したロールにEC2InstanceProfileForImageBuilderECRContainerBuildsをアタッチします。

これでECRを操作できるようになります。
確認
実際にCodeBuildが動くか確認します。
まずはブランチを変更します。
$ git checkout -b feature/#test
buildspec.ymlの一部を変更します。
(省略)
phases:
pre_build:
commands:
- echo Logiging in to Amazon ECR.... #変更
(省略)
GitにPushします。
$ git add .
$ git commit -m "buildspec確認"
$ git push origin feature/#test

Gitのリポジトリから「Compare & pull request」を選択して

「Create pull request」をクリック

「Merge pull request」→「Confirm merge」をクリックしてmasterにマージします。
CodeBuildで先ほど作成したsample-buildをみると動いているのがわかります。

クリックするとビルドのログが確認できるので、エラーが出たら対処します (環境変数のタイポミスなど)
成功したら、ECRにイメージがPushされていること

S3(バケット名/sample-build/)にファイルが作成されていることが確認できれば成功です。

CodeBuild (テスト)
次はCIらしく、自動テストをCodeBuildでしたいと思います。
まずはブランチを変更します。
$ git checkout master
$ git pull origin master
$ git checkout feature/#test2
Rspecを行うためにtestspec.ymlを作成します。
version: 0.2
phases:
pre_build:
commands:
- echo Logging in to Test....
- chown -R 1000:1000 .
- docker-compose run --rm rails sleep 10
- docker-compose run --rm rails rails db:create RAILS_ENV=test
- docker-compose run --rm rails rails db:migrate RAILS_ENV=test
build:
commands:
- echo Build started on `data`
- echo Building the Rspec test...
- docker-compose run --rm rails rspec
コンテナを起動してRspecを行っています。
rails sleep 10のところですが、これを入れている理由はDBが準備できる前にdb:createをするとエラーになるので起動待ちするために10秒待っています。
次に、CodeBuildを作成します。
先ほどと手順はほとんど同じですが、今回はビルドに用いるファイルの指定と、オペレーションシステムを変更しています。
デフォルト値から変更があるものを表に示します。
| 項目 | 値 |
|---|---|
| プロジェクト名 | sample-build-test |
| ソースプロバイダ | GitHub(接続してください) |
| リポジトリ | GitHubのリポジトリ |
| GitHubのリポジトリ | CICD_Road_Sample |
| コードの変更がこのレポジトリにプッシュされるたびに再構築する | ☑ |
| イベントタイプ | PULL_REQUEST_MERGED |
| これらの条件でビルドを開始する: BASE_REF - オプショナル | ^refs/heads/master$ |
| オペレーションシステム | Ubuntu |
| ランタイム | Standard |
| イメージ | aws/codebuild/standard:5.0 |
| イメージのバージョン | aws/codebuild/standard:5.0-21.10.15 |
| 特権付与 | ☑ |
| サービスロール | 既存のサービスロール |
| ロールのARN | 作成したロール |
| Buildspec 名 - オプショナル | testspec.yml |


ここで先ほどと違うオペレーションシステムを使うのは、testspec.ymlでdocker-composeを使う際にバージョン問題が対応しておらず--targetが利用できなかったため変更しています。

アーティファクトと環境変数は今回は不要です。
「ビルドプロジェクトを作成する」をクリックして作成します。
では実際にPushしてテストが動くか確認します。
$ git add .
$ git commit -m "testspec.yml確認"
$ git push origin feature/#test2
先ほどと同じ手順でマージしてCodeBuildの結果を確認します。

CodePipeline
ではここまで作成した2つのCodeBuildを1つにまとめて、マージされたら2つが動くように設定していきます。
AWSマネジメントコンソールの「サービス」メニューから「CodePipeline」のダッシュボードを開きます。

「パイプラインを作成する」をクリックします。
デフォルト値から変更があるものを表に示します。
| 項目 | 値 |
|---|---|
| パイプライン名 | sample-pipeline |
| サービスロール | 新しいサービスロール |
| ロールのARN | sample-pipeline |
| ソースプロバイダー | GitHub (バージョン1) |
| リポジトリ | CICD_Road_Sample |
| ブランチ | master |
| プロバイダーを構築する | AWS CodeBuild |
| プロジェクト名 | sample-build |

既にロールがあれば魏損のサービスロールを選択してください
ポリシーが同じものがあるとエラーになります。
「次に」をクリック

「次に」をクリック

この段階では1つしか登録できないのでsample-buildを登録する
「次に」をクリックする
デプロイステージは「導入段階をスキップ」→「スキップ」をクリック
「パイプラインを作成する」をクリックして作成
この段階でパイプラインが起動しますが、実行を停止して編集を行います。

「編集」をクリックします。

下の「ステージを追加する」をクリックします。
「build-test」とステージに名前をつけ「ステージを追加する」をクリックします

「アクショングループを追加する」をクリックします。
| 項目名 | 値 |
|---|---|
| アクション | test |
| アクションプロバイダー | CodeBuild |
| 入力アーティファクト | SourceArtifact |
| プロジェクト名 | sample-build-test |
設定したら「完了」をクリックして「保存」をクリックします。
パイプラインが正しく動作するか確認します。
$ git checkout master
$ git pull origin master
$ git checkout -b feature/#test3
buildspec.ymlを修正します。
(省略)
phases:
pre_build:
commands:
- echo Logiging in to Amazon ECR... #変更
(省略)
$ git add .
$ git commit -m "CodePipeline確認"
$ git push origin feature/#test3
先ほどの手順でマージを行い、CodePipelineが動作していれば成功です。

おわりに
作成したものはこちらのリポジトリに用意しています。
今回はDockerイメージをとりすぎて、DockerHubからイメージをとれなくなってしまいパイプラインが止まったり、環境変数をミスしたりでかなり時間がかかりましたがなんとかできました。
次はラスボスのCodeDeployに挑戦していきたいと思います。
Discussion