現状の何が問題か
前提のリポジトリ構成
現在担当しているプロダクトは複数のフロントエンドとAPIサーバーのバックエンドが、下記のように単一のリポジトリで構成されています。
root
├─ フロントA
├─ フロントB
├─ フロントC
├─ バックエンド
└─ 環境変数ファイル #.gitignore対象
このような構成の中で「フロント」「バックエンド」どの環境からも利用される共通の環境変数ファイルが1つある状態です。
この構成自体には問題はないのですが、デプロイフローには解決しなければいけない課題がありました。
解決しなければいけない課題
その課題というのが、各環境をデプロイするときにローカルの環境変数ファイルの値を元にデプロイが行われているということです。
ローカルの環境変数ファイルではdevelopment
の値だけではなくstaging
およびproduction
の値も管理していました。
そしてこのローカルの環境変数ファイルのproduction
の値を元にデプロイを行う、というフローになっていました。
以下イメージ図です。
バックエンドのデプロイフローイメージ
各フロントエンドのデプロイフローイメージ
ローカル環境変数ファイルのproduction
の値を元にデプロイを行う ということで、デプロイを行う開発者間で環境変数ファイルを一意に保たないといけないという問題がありました。
もちろん秘匿情報が含まれているので環境変数ファイルはGit管理をできず、アナログな方法で一貫性を保っていました。
実際のやり取りがこちら。
いつまでもこの手法に頼っていられないということで、デプロイフローの改善を行いました。
改善方針
達成したい要件
- ローカルで
production
の環境変数を管理をせずにSecrets Managerで一元管理をしたい - 環境変数に追加や変更があった場合はSecrets Managerを修正するだけにしたい
- S3に秘匿情報を含んだファイルがアップロードされているがこれをやめたい
- 既存バックエンドのデプロイ構成にはあまり手を入れたくない
上記を踏まえて諸々検討して下記のようなデプロイフローに落ち着きました。
改善後バックエンドについて
改善後バックエンドのデプロイフローイメージ
詳細
CodeBuild上にてSecrets Managerから値を取得するようにしました。
取得した値をどのように使うかが悩みポイントでしたが、元の実装にてenv.js
が存在することを前提に作り込まれていたので、CodeBuild上で動的にenv.js
を作成してビルドにて使用するようにしました。
buildspec.yml
からシェルを起動して実現しています。
# フロントエンドとバックエンド用のそれぞれのシークレットから値を取得
echo "Fetching environment variables from '$SECRETS_NAME_BACK'"
BACK_RAW_ENV_CONTENT=$(aws secretsmanager get-secret-value --secret-id $SECRETS_NAME_BACK --query SecretString --output text)
# Secrets Managerに保管されている値が変な形になっていたらエラーを起こして中断
echo "$BACK_RAW_ENV_CONTENT" | jq -e .
if [[ $? -ne 0 ]]; then
echo "Error: '$SECRETS_NAME_BACK' contains invalid JSON"
exit 1
fi
echo "Fetching environment variables from '$SECRETS_NAME_FRONT'"
FRONT_RAW_ENV_CONTENT=$(aws secretsmanager get-secret-value --secret-id $SECRETS_NAME_FRONT --query SecretString --output text)
# Secrets Managerに保管されている値が変な形になっていたらエラーを起こして中断
echo "$FRONT_RAW_ENV_CONTENT" | jq -e .
if [[ $? -ne 0 ]]; then
echo "Error: '$SECRETS_NAME_FRONT' contains invalid JSON"
exit 1
fi
# バックエンドとフロントエンドのシークレットを統合
echo "Merging back-end and front-end environment variables"
COMBINED_RAW_ENV_CONTENT=$(echo $BACK_RAW_ENV_CONTENT $FRONT_RAW_ENV_CONTENT | jq -s '.[0] * .[1]')
# バックエンドで使う秘匿情報なども全て含めたenv.jsを生成
echo "Generating env.js"
cat <<EOF > env.js
module.exports = function () {
const env = JSON.parse(\`$COMBINED_RAW_ENV_CONTENT\`)
console.log("Loaded env config for back-end.")
console.log({ env })
return env
}
EOF
バックエンドではDBの接続情報などの秘匿情報を含んだ状態のenv.js
が必要なので、Secrets Managerから取得した値をすべてenv.js
に展開しています。
このenv.js
を後のdockerビルドに使用しています。
一方、この後に行われるフロントエンド用の環境変数ファイルもこの段階で作成し、S3にアップロードしておきます。
フロントエンド用の環境変数ファイルには秘匿情報は不要なので、フロントエンド用の環境変数を保存したSecrets Managerを元にenvForFront.js
を作成します。
# フロントエンドで使う秘匿情報などは含んでいない envForFront.js を生成
echo "Generating envForFront.js"
cat <<EOF > envForFront.js
module.exports = function () {
const env = JSON.parse(\`$FRONT_RAW_ENV_CONTENT\`)
console.log("Loaded env config for front-end.")
console.log({ env })
return env
}
EOF
# フロントデプロイ時に利用するのでS3にenvForFront.jsをアップロード
echo "Uploading envForFront.js to S3"
aws s3 cp ./envForFront.js s3://$ENV_FILES_BUCKET/envForFront.js
フロントエンドデプロイ時にenvForFront.js
を作成することも考えられます。
しかしフロントが複数存在するのでenvForFront.js
を作成する箇所に一工夫必要なこと、現状のプロダクトのデプロイ性質上、環境変数が追加される場合はほぼ間違いなくバックエンドもデプロイが必要になるので、この方式を取りました。
改善後フロントエンドについて
改善後フロントエンドのデプロイフローイメージ
詳細
今までローカルでビルドを行ってS3に成果物をアップロードしていましたが、それをCodeBuild上で行うようにしました。
バックエンドのデプロイ時に作成したenvForFront.js
をフロント側のCodeBuildでダウンロードしてビルドに使用しています。
phases:
install:
commands:
- echo 'Fetching envForFront.js from S3 bucket'
- aws s3 cp s3://$S3_ENV_FILES_BUCKET/envForFront.js ./envForFront.js
CodeBuildのビルドプロジェクトはフロントの数用意していますが、buildspec.yml
は共通のものを使っています。
CodeBuildに埋め込んだ環境変数や条件分岐を利用することで、共通化を図っています。
※例 各フロントに合わせてビルドコマンドを変更しています
build:
commands:
- echo 'Building the application'
- if [[ $FRONT_DIRECTORY == "front-a" ]]; then yarn generate; elif [[ $FRONT_DIRECTORY == "front-b" ]]; then ./node_modules/.bin/webpack --mode production; else NODE_ENV=$NODE_ENV yarn build; fi
複数のフロントエンドを一括でデプロイ
おまけとして今までのフロント毎のデプロイの他に、以下のようなシェルをローカルに用意することで、複数のフロントエンドを一括でデプロイできるようにしました。
#!/bin/bash
# 必須環境変数のチェック
if [[ "$AWS_PROFILE" == "" ]] || [[ "$PROJECT_NAMES" == "" ]]; then
echo "必須の環境変数が設定されていません。"
echo "AWS_PROFILE: $AWS_PROFILE"
echo "PROJECT_NAMES: $PROJECT_NAMES"
exit 1
fi
# 各プロジェクトのビルドを開始
for PROJECT_NAME in $PROJECT_NAMES; do
echo "Starting deploy for project: $PROJECT_NAME"
aws codebuild start-build --project-name "$PROJECT_NAME" --region ap-northeast-1 --profile "$AWS_PROFILE" --no-cli-pager
if [[ $? -ne 0 ]]; then
echo "Failed to start build for project: $PROJECT_NAME"
exit 1
fi
done
echo "All fronts deploy started successfully."
PROJECT_NAMES
の変数で渡された複数のCodeBuildプロジェクトをデプロイしていきます。
起動コマンドはpackage.json
にscriptsとして用意したこちら yarn deployAllFront:prod
{
"scripts": {
"deployAllFront:prod": "AWS_PROFILE=hoge PROJECT_NAMES='front-a front-b front-c' ./deployAllFront.sh"
}
}
勉強になったAWSリソースの挙動
Secrets ManagerはJSONで定義できる
今までSecrets ManagerはコンソールのUIから打ち込んでいましたが、これだと数値や真偽値が文字列型として保管されてしまいました。
こちらはプレーンテキストタブからJSONを編集することで解決しました。
環境変数が大量にあるときはUIから一つずつ入力するのが手間なので、JSONで一括定義できるのは非常に便利でした。(知らんかった)
ただ、こちらプレーンテキストをコンソールから直接編集すると、誤った形のJSONを保存してしまうなどオペレーションミスを引き起こす運用上の懸念がありました。
下記にてこちらを解決できるようなシェルツールを作成しています。
CodeBuildでインストールした依存関係はキャッシュできる
フロントエンドをCodeBuildでビルドするようになったので、依存関係をインストールする必要がありました。
CodeBuildはインスタンスが変わるので毎回初回インストール扱いとなり、パッケージのインストールyarn
に時間がかかっていました。
S3にキャッシュを保存することで速度改善を試みています。
※インストール自体は速くなりますがS3のキャッシュをダウンロードするのに多少時間がかかります。
設定方法
buildspec.yml
にキャッシュ対象を記載する
cache:
paths:
- 'node_modules/**/*'
- 'yarn.lock'
CodeBuildのビルドが完了すると設定したバケットにキャッシュが保存される
キャッシュ結果の比較
◆before
S3からのキャッシュダウンロード 0秒
yarnでのパッケージインストール 127秒
合計 127秒
◆after
S3からのキャッシュダウンロード 61秒
yarnでのパッケージインストール 0秒
合計 61秒
ということで、127秒→61秒と66秒の短縮になりました。