React SPA 〜S3 Web hostingで環境変数を仕込む方法〜

# はじめに

この記事はcreate-react-appで作ったSPAをAmazon S3のウェブホスティングを使用して外部公開する際の環境変数の設定方法をまとめた記事です。

S3へデプロイしているサービスから環境変数を読み込む際、下記2点の壁がありました。

- CDにGithubActionsを使用しているため`npm build`時に`.env`ファイルが無視されてしまう(.gitignoreに含めているため)
- AWS側でSystemManagerやSecretsManagerを使おうにもS3からの読み取りができない

じゃあどうしたらいいの？に対する答えをこの記事でまとめていきます。

# 記事を書こうと思ったきっかけ
筆者は現在[Fjord Bootcamp](https://bootcamp.fjord.jp/)で自作サービスの開発を行っているのですが、サービスのデプロイ先としてS3のウェブホスティングを使用しています。

はじめにで書いたように、本番環境で環境変数を読み込むにあたって壁にぶつかったため、Googleで調べてみましたが、力及ばずで解決策を見つけることができませんでした。

自分と同様の壁にぶつかった方がこの記事を読むことで、問題を解決できればという思いで執筆しました。

# 解決策
一番はじめに解決策からまとめますが、その方法は、
**GithubActionsのworkflowにて.envファイルを生成する**です。

1. Github Actions Secretsに環境変数を設定する
1. workflow内でrunを使って１を書き込んだファイルを生成する

CDパイプライン内でビルド実行前に`.env`ファイルを作っちゃおうということです。

# 実践
手順については先述のとおりです。
順番に詳細をまとめていきます。

尚、今回は複数の環境変数をセットする想定で解説をします。

## 1. GithubActionsの環境変数を用意
1. リポジトリ内のSettingsタブをクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/930395/ae9ed8ee-f455-5797-a28b-5f6711fa6340.png)
1. サイドバー内、Security→ Secrets and variables→ Actionsをクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/930395/d2bd6737-7a02-6eae-27cf-d67a57e1f665.png)
1. New repository secretをクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/930395/1978d265-3e93-2e47-0c41-56d7b0c75142.png)
1. 環境変数をセット
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/930395/73bcabe4-ec71-21c9-4300-baa7456c8290.png)

:::note info
Note
create-react-appの場合は環境変数の接頭辞として`REACT_APP`をつける必要があります。
:::

※同様の手順で`REACT_APP_FOO`にfoo、`REACT_APP_BAR`にbarを設定したものとします。

## 2. workflowファイルの編集
まずはじめに環境変数を仕込む前のworkflowファイルがこちらです。

```yml
name: build and deploy

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Install dependencies
        run: npm install

      - name: Build
        run: npm run build

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

      - name: Deploy
        run: aws s3 sync ./build s3://tasting-note --delete

      - name: Clear cache
        run: aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_CF_DISTRIBUTION_ID }} --paths "/*"
```
mainブランチにマージされたタイミングでS3へのデプロイとCloud frontのキャッシュクリアを走らせています。
このworkflowに`.env`ファイルを仕込むstepを追加していきます。
（今回は本番環境用のファイルなのでファイル名は`.env.production`でいきます。）

### 実装方針
1.で設定した環境変数を`.env.production`というファイル名でテキスト出力します。
今回は環境変数が複数ある想定なのでcat + ヒアドキュメントをリダイレクトさせてファイルを作成します。

#### 複数行のテキスト出力について
少し脱線しますがテキスト出力の方法については[こちら](https://www.pmi-sfbac.org/here-document/)の記事を参考にしました。
2年前の記事なので情報としては少し古いですが、Linuxコマンドのような枯れた技術であれば問題ないだろうという判断です。

それでは本題に戻ってstepを追加していきます。

```yml
- name: Prepare .env.production file
  run: |
    cat << EOF > .env.production
    REACT_APP_HOGE=${{ secrets.REACT_APP_HOGE }}
    REACT_APP_FOO=${{ secrets.REACT_APP_FOO }}
    REACT_APP_BAR=${{ secrets.REACT_APP_BAR }}
    EOF
```
コマンドの詳細な説明は前述の記事に任せるとして何をしているのかを簡単にまとめると、ヒアドキュメントの内容をcatで出力したものを`>`（リダイレクト）を使用して`.env.production`に書き込んでいます。

1.でセットした環境変数はworkflowファイル内から`${{ secrets.HOGE }}`のようにして読み込むことができます。

このstepによって作成される`.env.production`ファイルの中身は以下になります。

```text:.env.production
REACT_APP_HOGE=hoge
REACT_APP_FOO=foo
REACT_APP_BAR=bar
```

これをbuild前に仕込むことで本番環境において環境変数の読み込みが可能となります。
最終的なworkflowファイルは下記です。

```yaml
name: build and deploy

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Prepare .env.production file
        run: |
          cat << EOF > .env.production
          REACT_APP_HOGE=${{ secrets.REACT_APP_HOGE }}
          REACT_APP_FOO=${{ secrets.REACT_APP_FOO }}
          REACT_APP_BAR=${{ secrets.REACT_APP_BAR }}
          EOF

      - name: Install dependencies
        run: npm install

      - name: Build
        run: npm run build

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

      - name: Deploy
        run: aws s3 sync ./build s3://tasting-note --delete

      - name: Clear cache
        run: aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_CF_DISTRIBUTION_ID }} --paths "/*"
```

# この方法のデメリット
- 検証が難しい
ローカル環境であれば`console.log`を仕込んで値の確認ができますが、本番環境の場合はそれができないため、出たとこ勝負するしかありません。
例えば外部公開していないステージング環境をつくって、本番環境へデプロイする前に確認をする方法も考えられますが、S3の場合はBasic認証などの機能がない（もしあったらごめんなさい😥）ため、クローズドな環境で検証を行うことができません。
くれぐれも**秘匿情報を本番環境のコンソールに表示させて検証をするといったことがないよう**に気をつけましょう。

- 管理が大変
環境変数が増えてきた場合にActionsSecretsとworkflowファイルの2つの変更が必要になるため管理が煩雑で変更漏れのリスクも一定発生してしまいます。

# まとめ
記事を作成しておいてなんですが、この方法がベストプラクティスと思えないのが本音です。
他の方法を思いつけなかったのでこの実装を行いましたが、他に良い方法があればそっと教えていただけると嬉しいです😌