🐈

Rails(Docker)をCodeBuildでCIする (CI/CDまでの道⑧)

2022/01/28に公開

はじめに

前回はFargateにデプロイすることができるようになりました。本日はついにタイトル「CI/CDまでの道」にあるとおり、CodeBuildによるCIに挑戦していきます。

第8回にしてやっとタイトル回収できました (当初はできるかすら不安でしたのでよかったです)

CodeBuildに挑戦していくのですが、そこでCIを早く回すためにDockerマルチステージビルドの変更を加えるのも同時にやっていきます。

作成に利用するリポジトリはこちら

CI/CDまでの道シリーズ

環境

  • wsl2 (ubuntu20.04)
  • docker 20.10.9
  • docker-compose 1.29.1
  • git 2.25.1
  • vscode

マルチステージビルド

まずはdevelopmentproductionで同じDockerfileを利用しているので、分離したいと思います。
今後Dockerイメージを作る際に不要なビルドが発生してしまい時間がかかってしまうのを防ぐために行います。

マルチステージビルドについて知らない方は以下を参考にしてみてください。詳しい説明は省略します。

Dockerのマルチステージビルドを使う

まずはDockerfileを以下に変更します。

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を変更します。

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も変更します。

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を設定して上書きしています。

次にNginxDockerfileを修正します。

containers/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 builddocker-composeでのファイルパスに違いがあるためエラーになるのでこのようにしています。

次にconfig/webpacker.ymlを修正します。

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にデプロイする際に適宜変えていきます。

entrypoint.sh
#!/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.keyconfig/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を作成します。

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リポジトリ作成

ECRsample-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の一部を変更します。

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を作成します。

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を修正します。

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に挑戦していきたいと思います。

参考

GitHubで編集を提案

Discussion