ローカル環境変数ファイルに依存していたデプロイフローからの卒業

## 現状の何が問題か
### 前提のリポジトリ構成
現在担当しているプロダクトは複数のフロントエンドとAPIサーバーのバックエンドが、下記のように単一のリポジトリで構成されています。
```bash
root 
├─ フロントA
├─ フロントB
├─ フロントC
├─ バックエンド
└─ 環境変数ファイル #.gitignore対象
```
このような構成の中で「フロント」「バックエンド」どの環境からも利用される共通の環境変数ファイルが1つある状態です。

この構成自体には問題はないのですが、デプロイフローには解決しなければいけない課題がありました。

### 解決しなければいけない課題
その課題というのが、**各環境をデプロイするときにローカルの環境変数ファイルの値を元にデプロイが行われている**ということです。
ローカルの環境変数ファイルでは`development`の値だけではなく`staging`および`production`の値も管理していました。
そしてこのローカルの環境変数ファイルの`production`の値を元にデプロイを行う、というフローになっていました。
以下イメージ図です。

#### バックエンドのデプロイフローイメージ
[![Image from Gyazo](https://i.gyazo.com/5df47b3d40586c5ca48b433b8459ced2.png)](https://gyazo.com/5df47b3d40586c5ca48b433b8459ced2)

#### 各フロントエンドのデプロイフローイメージ
[![Image from Gyazo](https://i.gyazo.com/3b014c15e65a66d48e87f060544f725e.png)](https://gyazo.com/3b014c15e65a66d48e87f060544f725e)

**ローカル環境変数ファイルの`production`の値を元にデプロイを行う** ということで、デプロイを行う開発者間で環境変数ファイルを一意に保たないといけないという問題がありました。
もちろん秘匿情報が含まれているので環境変数ファイルはGit管理をできず、アナログな方法で一貫性を保っていました。
実際のやり取りがこちら。
[![Image from Gyazo](https://i.gyazo.com/e906b5b9de476e26a3cfec320c893f0a.png)](https://gyazo.com/e906b5b9de476e26a3cfec320c893f0a)

いつまでもこの手法に頼っていられないということで、デプロイフローの改善を行いました。

## 改善方針
**達成したい要件**
- ローカルで`production`の環境変数を管理をせずにSecrets Managerで一元管理をしたい
- 環境変数に追加や変更があった場合はSecrets Managerを修正するだけにしたい
- S3に秘匿情報を含んだファイルがアップロードされているがこれをやめたい
- 既存バックエンドのデプロイ構成にはあまり手を入れたくない

上記を踏まえて諸々検討して下記のようなデプロイフローに落ち着きました。

## 改善後バックエンドについて
### 改善後バックエンドのデプロイフローイメージ
[![Image from Gyazo](https://i.gyazo.com/8cf95d81f8b0a3aa779d2713c5ec703a.png)](https://gyazo.com/8cf95d81f8b0a3aa779d2713c5ec703a)

### 詳細
CodeBuild上にてSecrets Managerから値を取得するようにしました。
取得した値をどのように使うかが悩みポイントでしたが、元の実装にて`env.js`が存在することを前提に作り込まれていたので、CodeBuild上で動的に`env.js`を作成してビルドにて使用するようにしました。
`buildspec.yml`からシェルを起動して実現しています。
```shell
# フロントエンドとバックエンド用のそれぞれのシークレットから値を取得
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`を作成します。
```shell
# フロントエンドで使う秘匿情報などは含んでいない 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`を作成する箇所に一工夫必要なこと、現状のプロダクトのデプロイ性質上、環境変数が追加される場合はほぼ間違いなくバックエンドもデプロイが必要になるので、この方式を取りました。


## 改善後フロントエンドについて
### 改善後フロントエンドのデプロイフローイメージ
[![Image from Gyazo](https://i.gyazo.com/8c93917a4af64c201673546e59764d34.png)](https://gyazo.com/8c93917a4af64c201673546e59764d34)

### 詳細
今までローカルでビルドを行ってS3に成果物をアップロードしていましたが、それをCodeBuild上で行うようにしました。
バックエンドのデプロイ時に作成した`envForFront.js`をフロント側のCodeBuildでダウンロードしてビルドに使用しています。
```yml:buildspec.yml
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に埋め込んだ環境変数や条件分岐を利用することで、共通化を図っています。
※例 各フロントに合わせてビルドコマンドを変更しています
```yml:buildspec.yml
  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
```

#### 複数のフロントエンドを一括でデプロイ
おまけとして今までのフロント毎のデプロイの他に、以下のようなシェルをローカルに用意することで、複数のフロントエンドを一括でデプロイできるようにしました。

```shell:deployAllFront.sh
#!/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`
```json:pacage.json

{
  "scripts": {
    "deployAllFront:prod": "AWS_PROFILE=hoge PROJECT_NAMES='front-a front-b front-c' ./deployAllFront.sh"
  }
}
```

## 勉強になったAWSリソースの挙動

### Secrets ManagerはJSONで定義できる
今までSecrets ManagerはコンソールのUIから打ち込んでいましたが、これだと数値や真偽値が文字列型として保管されてしまいました。

[![Image from Gyazo](https://i.gyazo.com/b085d3f5fa4790165b292b4727a6ebfc.png)](https://gyazo.com/b085d3f5fa4790165b292b4727a6ebfc)

こちらはプレーンテキストタブからJSONを編集することで解決しました。
環境変数が大量にあるときはUIから一つずつ入力するのが手間なので、JSONで一括定義できるのは非常に便利でした。（知らんかった）

[![Image from Gyazo](https://i.gyazo.com/968b7e5e5be8930c77ad41681a09a9ef.png)](https://gyazo.com/968b7e5e5be8930c77ad41681a09a9ef)

ただ、こちらプレーンテキストをコンソールから直接編集すると、誤った形のJSONを保存してしまうなどオペレーションミスを引き起こす運用上の懸念がありました。
下記にてこちらを解決できるようなシェルツールを作成しています。

https://qiita.com/lyd-ryotaro/items/4f955fe26f1b6c32ca91




### CodeBuildでインストールした依存関係はキャッシュできる
フロントエンドをCodeBuildでビルドするようになったので、依存関係をインストールする必要がありました。
CodeBuildはインスタンスが変わるので毎回初回インストール扱いとなり、パッケージのインストール`yarn`に時間がかかっていました。
S3にキャッシュを保存することで速度改善を試みています。

:::note warn
**※インストール自体は速くなりますがS3のキャッシュをダウンロードするのに多少時間がかかります。**
:::

#### 設定方法
`buildspec.yml`にキャッシュ対象を記載する
```yaml:buildspec.yml
cache:
  paths:
    - 'node_modules/**/*'
    - 'yarn.lock'
```

CodeBuildの設定で保存先バケットを設定する
[![Image from Gyazo](https://i.gyazo.com/934034e1b567d53e34d22165390e85d5.png)](https://gyazo.com/934034e1b567d53e34d22165390e85d5)

CodeBuildのビルドが完了すると設定したバケットにキャッシュが保存される
[![Image from Gyazo](https://i.gyazo.com/cc883ec049d8c8b9ad739d4bb12a263c.png)](https://gyazo.com/cc883ec049d8c8b9ad739d4bb12a263c)

#### キャッシュ結果の比較
##### ◆before
**S3からのキャッシュダウンロード 0秒**
**yarnでのパッケージインストール 127秒**
[![Image from Gyazo](https://i.gyazo.com/23edf7817ece20db24331ed6d8600b39.png)](https://gyazo.com/23edf7817ece20db24331ed6d8600b39)
**合計 127秒**

##### ◆after
**S3からのキャッシュダウンロード 61秒**
[![Image from Gyazo](https://i.gyazo.com/b4315b730a777a9d7fc05cc3fb98fcbe.png)](https://gyazo.com/b4315b730a777a9d7fc05cc3fb98fcbe)
**yarnでのパッケージインストール 0秒**
[![Image from Gyazo](https://i.gyazo.com/7487439afc10b419bccd83a5f04173eb.png)](https://gyazo.com/7487439afc10b419bccd83a5f04173eb)
**合計 61秒**

ということで、127秒→61秒と**66秒**の短縮になりました。