Open19
CodePipeline, CodeBuildを用いてRails on DockerアプリをElastic Beanstalk環境にデプロイ
やりたいこと
- すでにproduction環境が動いているサービスのstaging環境を作成する
技術選定
- AWS Elastic Beanstalk
- AWS CodePipeline, CodeBuild
- Rails / Nginx / Docker
- ローカルの作業環境はMac M2 Pro
全体像
- ローカルのアプリケーションコードを、staging環境用に変更
- ローカルのソースバンドルの作成(.envなどを含まないように)
- EBに新しい環境を作成 & ソースバンドルをアップロード
- データベースの構築
- DNSの設定等
staging環境の位置付けや運用方針
- developブランチが基本デプロイ対象だが、適宜付け替え可能
- staging.rbを利用すべきではないという話
- https://qiita.com/tonluqclml/items/196280783bbefbd9ac76
- https://neko314.hatenablog.com/entry/2020/11/21/231326
- => production.rbを使って、環境変数を変えることで対応しよう
アプリケーションコードのテスト環境対応
要対応
- production.rb(
action_mailer.default_url_options
が今まではベタ書きだったので、環境変数化する)
production.rb
config.action_mailer.perform_caching = false
config.action_mailer.default_url_options = { :protocol => 'https', :host => 'app.production-domain.com' }
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
enable_starttls_auto: true,
address: "smtp.gmail.com",
domain: "our-domain",
port: PortNum,
authentication: "login",
user_name: ENV['GMAIL_USER_NAME'],
password: ENV['GMAIL_PASSWORD']
}
対応不要なもの
- database.yml(環境変数で調整)
database.yml
production:
<<: *default
database: <%= ENV['RDS_DB_NAME'] %>
username: <%= ENV['RDS_USERNAME'] %>
password: <%= ENV['RDS_PASSWORD'] %>
host: <%= ENV['RDS_HOSTNAME'] %>
port: <%= ENV['RDS_PORT'] %>
ローカルのソースバンドルの作成
- 公式の記事
- Git管理されないファイルはいれないように作ったほうが良い感?
- このコマンドで作成
- HEADの状態でアーカイブが作られるので、まだcommitされていない差分は含まれない模様
$ git archive -v -o myapp.zip --format=zip HEAD
imageの切り替え
- production環境と、staging環境ではこれを分けたい
- 新しいgemの導入など、staging環境とproduction環境で内容が変わることがあるから。
- 現在production環境で利用しているECRのimageのpathで全検索をしたところ、以下の2つのファイルが該当している。
Dockerrun.aws.json
- Dockerrun.aws.jsonの中で、pullするimageを指定している。
CodeBuildの設定(buildspec.yml)
- 現在、buildspec.ymlでは、push先のリポジトリの指定などを全て直書きで、production環境用に行っている。これをstaging環境へ出し分けられるようにしたい
- https://dev.classmethod.jp/articles/codebuild-env/
どうやってDockerrun.aws.jsonの中身を動的に変更する?
調査に苦戦したのでAWS Supportに問い合わせた
- 質問
# 前提
- AWS Elastic Beanstalkの、Dockerプラットフォーム、ECS running on 64bit Amazon Linux 2での環境構築
# やりたいこと
- production環境を今まで運用していたが、今回新たにstaging環境をデプロイする。EBの環境自体を新しく作る。
- Dockerrun.aws.jsonでimageのURLを指定するが、それをproductionとstagingでうまく切り分けられるようにしたい
# 詳細
- CodeBuildを用いて、GitHubからpullし、docker imageをbuildしています。
- その際、staging環境用のimageとproduction環境用のimageは別としたいと考えています。(そのため、CodeBuildも2つ別で設定し、buildspec.ymlで環境変数を見ることによって、imageに対してのタグ付けや、push先のECRのURLを分けたいと考えています)
- production環境とstaging環境で別のimageがECRにpushされた際に、Dockerrun.aws.jsonが参照するURLもそれぞれ別々にしたいと考えています。
# 調査過程
- Dockerrun.aws.jsonのver.2を用いている場合は、以下のことができないという情報を得ました。
- "image": "${DOCKER_IMAGE_URL}" のような形で.jsonに記述し、EBのGUIから設定することでDOCKER_IMAGE_URLを取得する
# 問
- どのようにしたら、Dockerrun.aws.jsonを共通としながら、それぞれの環境に合わせたimageをpullできるか?
- 回答(抜粋)
■ 回答
ECS マネージド Docker プラットフォームにおいては、アプリケーションのデプロイ時に、Dockerrun.aws.json の設定に基づいて新しい ECS タスク定義のリビジョンが作成されます。
Elastic Beanstalk は、こちらの新しいリビジョンを使用して、アプリケーション用のコンテナを作成いたします。
こちらのリビジョンの作成に関して、Elastic Beanstalk 環境内の EC2 インスタンス側ではなく、アプリケーションをデプロイした側がリビジョンを作成いたします。
従いまして、それぞれの環境に合わせた Docker イメージを pull するためには、アプリケーションをデプロイする側で Dockerrun.aws .json を動的に書き換える仕組みが必要となります。
こちらの動的に書き換える仕組みに関して、参考となりそうな資料がございましたので紹介させていただきます。[2]
外部ドキュメントとなりますため内容の正当性について保証できるものではございませんが、お客様が直面されておられる課題解決の一助になればと考え、紹介させていただく次第でございます。
こちらの資料においては、テンプレート用の Dockerrun.aws .json.template を作成し、変数に応じて Dockerrun.aws .json を作成しております。
さらに AWS CLI や EB CLI 等を組み合わせていただくことで、取得する変数やイメージをカスタマイズできるかと存じます。
[2] Separate Dockerrun.aws .json files for staging and production
https://stackoverflow.com/questions/50162321/separate-dockerrun-aws-json-files-for-staging-and-production
ChatGPTへの質問回答
- ChatGPT曰く、AWSEBDockerrunVersion: 2の場合は、環境変数を直接Dockerrun.aws.jsonに挿入することができない模様。
- シェルスクリプトを使って、自分で置換しろと言っている。その例↓
mode: "000755"
owner: root
group: root
content: |
#!/bin/bash
DOCKER_IMAGE_URL=$(aws ssm get-parameter --name DOCKER_IMAGE_URL --region us-east-1 --with-decryption --query 'Parameter.Value' --output text)
ESCAPED_DOCKER_IMAGE_URL=$(echo $DOCKER_IMAGE_URL | sed 's/[\/&]/\\&/g')
sed -i "s/\${DOCKER_IMAGE_URL}/$ESCAPED_DOCKER_IMAGE_URL/g" /var/app/current/Dockerrun.aws.json
container_commands:
01_replace_imageurl:
command: "/tmp/docker_imageurl.sh"
現時点で考えていること・方針
やりたいこと
- prodとstgで、image自体を変える(gemが変わっていたりして、imageがずれることがあるから)
- Dockerrun.aws.jsonのpull元のURLを変更
- CodeBuildの際も、別のタグ付け & push先のECRのURLを変更
- 動的にDockerrun.aws.jsonを書き換える
- eb deployの直前に、prodかstgかに応じて書き換えることができればとりあえずOK
- docker imageの中のファイル(もともとのファイル)は、eb deploy後にソースバンドルによってマウントされることで上書きされる認識
- これをCodePiplineでできたら理想。Buildの前後?とかでスクリプトを変更し、そのまま変更されたスクリプトをデプロイする。
- eb deployの直前に、prodかstgかに応じて書き換えることができればとりあえずOK
超えるべき壁
- 動的にDockerrun.aws.jsonを書き換えること(ローカルのshell script実行、またはCodePipline)
- (動的に書き換えるとか関係なく)CodePiplineでのデプロイに成功すること
- 特にrails assets:precompileが難所?
- CodeBuildで、imageのタグやpush先を環境変数から変更すること
とりあえずテスト環境作成
- とりあえず何も考えずに環境自体を作ろう。まずはそこから。
新規環境作成
- 新しく環境を作成し、zipをアップロード
- => 環境を作成、完了
- ヘルスチェックは緑
- URLにアクセスすると永遠にロード中
- curlコマンドで確認 => 無限リダイレクトされている
$ curl -v URL
...省略...
<head><title>301 Moved Permanently</title></head>
...省略
- この問題は、HTTPSの設定ができていないことが原因。参考: 以前自分が書いたこちらの記事
証明書を作成し、Application Load Balancerに設定する
Route 53で、親となるホストゾーンの元でサブドメインを設定する
- Route53 > ホストゾーン > 親ドメインから、「レコードを作成」
- Aレコード
- トラフィックのルーティング先をEB環境に設定
AWS Certificate Managerで、パブリック証明書をリクエスト
Route 53でレコード作成をする
Elastic Beanstalkのロードバランサーの設定を変更する
環境変数の設定
- 何も環境変数を設定しない状態でアクセスしたら以下のようなエラーがでた
- RAILS_ENV等の環境変数をprodと(ほぼ)同じ用に設定したところ、Railsの本番環境でのエラーに変わった
- ってことは、環境変数を設定しない場合、development環境になってるのかもな(エラーがdev環境のレイアウトだったため)
- ってことは、環境変数を設定しない場合、development環境になってるのかもな(エラーがdev環境のレイアウトだったため)
- ログを調べたところ、上記エラーはデータベースへつながっていないことによるエラー(設定していないので当たり前)
ActiveRecord::ConnectionNotEstablished (Can't connect to local MySQL server through socket '/run/mysqld/mysqld.sock' (2)):
DBを作成する
- EBの設定画面から、RDSを作成する
- エラーが以下のように変わる
ActiveRecord::StatementInvalid: Mysql2::Error: Table 'ebdb.users' doesn't exist
- まだmigrationを実行していないため
EBの環境にSSH接続する
- db:migrateを実行するためには、本番環境のインスタンスに入る必要がある
- --setupを実行し、keypairを設定する。(新規作成するか、既存のものを使うか)
% eb ssh --setup EnvName
- SSHでアクセスした後、db:migrateコマンドを実行
アセットパイプライン関連のエラーの解決
- 次に出ているエラー
ActionView::Template::Error (The asset "application.css" is not present in the asset pipeline.
- ソースバンドルに
rails assets:precompile
したものが含まれていないことが原因 - 今回は、本番環境で直接precompileを実施 => 結果変わらない
$ sudo docker exec -it ID rails assets:precompile
- サーバーを再起動 => これでいけた!🎉
$ sudo docker exec -it ID bundle exec pumactl restart
Command restart sent success
CodePipelineでビルドしてみる!
- とりあえず、prodとstgの出し分けとかは考えずに、stg環境に特化して設定を組んで(buildspec.ymlどか、Dockerrun.aws.jsonとか)デプロイできるようにしてみる。
1. ECRにリポジトリを作る
- これは簡単。普通にリポジトリ作成するだけ
2. CodePipelineの作成
- どんどん進めていく
- CodeBuildは、CodePiplineの中から作成できる。それが一番シンプルそう
- ソースステージの追加
- GitHubはバージョン1と2を選べるが、2が推奨。公式doc
- GitHubはバージョン1と2を選べるが、2が推奨。公式doc
- ビルドステージの追加
- 中段の「プロジェクトの作成」をクリック
- CodeBuildの設定は次のコメントで
- デプロイステージの追加
CodeBuildの設定
- CodePipelineの作成画面から作成
- buildspec.ymlはあとで貼る(今調整中)
必要なIAMを付与する
- 自分の場合は、以下2つのIAMを、作成したCodeBuildのロールに対して付与する必要があった。
- AmazonSSMReadOnlyAccess(環境変数を参照しているため)
- AmazonEC2ContainerRegistryPowerUser(ECRにpushするため)
assets:precompile周りの処理
現状の問題
- サービスはエラーを返す
ActionView::Template::Error (The asset "application.css" is not present in the asset pipeline.
- 本番環境にSSH接続し、コンテナの中を覗くと、public/assetsディレクトリは存在していない。
解決の方針
- (色々苦戦した挙げ句出た結論)
- Dockerfileの中で
assets:precompile
を実施 - precompileされたものを、ディレクトリごとdockerコンテナの外にコピー(ソースバンドルに含めるため)
- これでデプロイすると、デプロイされた後のコンテナの中にpublic/assetsが存在している
試行錯誤中にあたった問題
- Dockerfileの中で
assets:precompile
をすると、containeの中ではディレクトリが生成されるため、ECRにpushされるimageには含まれていると思われる。 - だが、containerの外のソース(GitHubから持ってきたやつ)の中には含まれてない。含まれていないソースでソースバンドルを作ってデプロイすると、デプロイ後のコンテナにはpublic/assetsは含まれなくなっている(imageにあるのに?。たぶんマウントのタイミングとかで消されているとか)
- よって、ソースバンドルにもprecompileされた後のディレクトリを含めたい、という方針
実装
- 最終形態は後で書くけどとりあえず変更部分
- docker-compose.prod.ymlからDockerfileに環境変数を渡す。参考
docker-compose.prod.yml
version: '3'
services:
web:
build:
context: .
args:
- SECRET_KEY_BASE=${SECRET_KEY_BASE}
- Dockerfileでprecompile. 参考
Dockerfile
FROM ruby:3.1.2
# 追記
ARG SECRET_KEY_BASE
...略...
RUN bundle install
COPY . ./
...略...
# 追記
RUN SECRET_KEY_BASE=${SECRET_KEY_BASE} bundle exec rails assets:precompile \
&& yarn cache clean \
&& rm -rf node_modules tmp/cache
COPY entrypoint.sh /usr/bin/
...略...
- buildspec.ymlで、buildフェーズに追記
buildspec.yml
build:
commands:
- echo Building the Docker image...
- docker-compose -f ./docker-compose.prod.yml build --no-cache
# 追記
- docker create --name tmp-container-for-assets appName-prod-web
- docker cp tmp-container-for-assets:/var/www/AppName/public/assets ./public/assets
- docker rm -f tmp-container-for-assets
- docker tag appName-prod-web:latest xxxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/AppName-web:latest
- docker tag appName-prod-server:latest xxxxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/appName-server:latest
調査中: SECRET_KEY_BASEについて
- 明示的に
RAILS_ENV=production
をつけてassets:precompile
コマンドを呼ぶ場合、SECRET_KEY_BASEを渡さないと以下のようなエラーがでる
ArgumentError: Missing `secret_key_base` for 'production' environment, set this string with `bin/rails
- なお、直近(2022/12/8)のPRで、
SECRET_KEY_BASE_DUMMY=1
と指定したらSECRET_KEY_BASE
が不要になる開発がマージされた。該当PRについて書かれた記事 - だが自分の使っているRails7.0.4ではその機能は未実装。
- 今回、
RAILS_ENV=production
を渡してprecompileする必要性を感じなかったため、SECRET_KEY_BASE
も渡さない対応とした。(上のソースコードでは記述しているが、書き換えた)
buildspec.ymlで環境変数を使用する
- buildspec.ymlの書き換え
buildspec.yml
build:
commands:
...略...
- docker tag appName-prod-web:latest xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/${ECR_REPOSITORY_WEB}:latest
- docker tag appName-prod-server:latest xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/${ECR_REPOSITORY_SERVER}:latest
post_build:
commands:
- echo Build completed on `date`
- echo Pushing the Docker image...
- docker push xxxxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/${ECR_REPOSITORY_WEB}:latest
- docker push xxxxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/${ECR_REPOSITORY_SERVER}:latest
- CodeBuildのコンソールで環境変数を登録
- ECRのプッシュされた日時も、環境変数を用いてビルドした時間になってて成功!
動的にDockerrun.aws.jsonを変更する
方針
- Dockerrun.aws.jsonを変更するshell scriptを作成し、それをCodeBuildのpre_buildで呼ぶ。
- 必要な値はbuildspec.ymlから渡す。
実装
- deploy/dockerrun-setter.sh
dockerrun-setter.sh
#!/bin/bash
ECR_REPOSITORY_WEB=$1
ECR_REPOSITORY_SERVER=$2
MEMORY=$3
sed -e "s/\${ECR_REPOSITORY_WEB}/$ECR_REPOSITORY_WEB/g" \
-e "s/\${ECR_REPOSITORY_SERVER}/$ECR_REPOSITORY_SERVER/g" \
-e "s/\${MEMORY}/$MEMORY/g" \
./deploy/Dockerrun.aws.json.template > ./Dockerrun.aws.json
- deploy/Dockerrun.aws.json.template
Dockerrun.aws.json.template
{
"AWSEBDockerrunVersion": 2,
"volumes": [
{
"name": "source-bundle",
"host": {
"sourcePath": "/var/app/current"
}
},
{
"name": "public-data",
"host": {
"sourcePath": "/var/app/current/public"
}
},
{
"name": "tmp-data"
}
],
"containerDefinitions": [
{
"name": "appName-web",
"image": "xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/${ECR_REPOSITORY_WEB}:latest",
"essential": true,
"memory": ${MEMORY},
"command": ["bundle","exec","puma","-C","config/puma.rb"],
...略...
- buildspec.yml
buildspec.yml
pre_build:
commands:
...略...
- sh ./deploy/dockerrun-setter.sh ${ECR_REPOSITORY_WEB} ${ECR_REPOSITORY_SERVER} ${MEMORY}
- Dockerrun.aws.jsonは、中身に何を書いても上書きされるので空ファイルにしておいた
- CodeBuildの環境変数に必要な値を入れておく