staticwebapp.config.json で環境変数を使う

# はじめに

Azure Static Web Apps では`staticwebapp.config.json`によってルーティングや認証、ネットワークの設定を行うことができます。
実際に運用する場合は、環境に応じて設定を切り替えたいというニーズが出てくると思います。
そこで、タイトルにある様に設定ファイル内で環境変数が参照できたら良いなーと思い、調べてみました。

# そんなことはできない

結論から言うと、残念ながら`staticwebapp.config.json`内で環境変数を使用することは(現時点では)できないようです。
Static Web Apps のデータベース接続の設定ファイル(`staticwebapp.database.config.json`)では`@env('<VARIABLE>')`みたいな感じで参照できるのに...

# ならば自分で何とかする

できないものは仕方ないので、力業で使えるようにしてみます。
やりたいことは単純で、設定ファイル内でプレースホルダー化(`${<VARIABLE>}`)された箇所に環境変数を埋め込みたい。

## ベースとなる設定ファイルを用意する

ベースとなる設定ファイルを用意して、それを置換して新たにファイルを生成するという方針で行きたいと思います。

```
.swaconfig
└ staticwebapp.config.json
```

```json:.swaconfig/staticwebapp.config.json
{
  "auth": {
    "identityProviders": {
      "azureActiveDirectory": {
        "registration": {
          "openIdIssuer": "https://login.microsoftonline.com/${TENANT_ID}/v2.0",
          "clientIdSettingName": "AZURE_CLIENT_ID",
          "clientSecretSettingName": "AZURE_CLIENT_SECRET"
        }
      }
    }
  },
  ・・・
```

↑の例では、Azure AD 認証のテナントを`TENANT_ID`という環境変数で切り替えようとしています。

## 設定ファイル変換用のスクリプトを作る

上記と同じディレクトリに、変換用のスクリプト(javascript)を作成します。
単純にプレースホルダーをキーとして環境変数を引っ張ってきて置換しているだけです。
(Static Web Apps 開発するならまず node は使えるはずだし、OSに依存しないので javascript にしてみました)

```javascript:.swaconfig/swaconfig-gen.js
const fs = require('fs');
const path = require('path');

// ベースとなるファイルを読み込み
const config = fs.readFileSync(path.join(__dirname, 'staticwebapp.config.json'), 'utf-8');

// ${<変数名>} を環境変数の値で置換してコンソールに出力
console.log(
  config.replace(/[$]{(.+?)}/g, (_match, name) => {
    const value = process.env[name];
    // 環境変数が設定されていない場合はエラー終了
    if (value === undefined) throw Error(`Missing environment variable '${name}'.`);
    return value;
  })
);
```

## スクリプトを呼び出す npm scripts を定義する

作成したスクリプトを実行し、標準出力をファイルに書き込んでいます。
※出力先は実際のディレクトリ構成に合わせてください

```diff_json:package.json
  "scripts": {
    ・・・
+   "swaconfig-gen": "node .swaconfig/swaconfig-gen.js > staticwebapp.config.json"
  },
  ・・・
```

以下のコマンドを実行すると、環境変数が埋め込まれた`staticwebapp.config.json`が生成されます。

```bash:bash
TENANT_ID=1234567890
npm run swaconfig-gen
```

```json:staticwebapp.config.json
{
  "auth": {
    "identityProviders": {
      "azureActiveDirectory": {
        "registration": {
          "openIdIssuer": "https://login.microsoftonline.com/1234567890/v2.0",
          "clientIdSettingName": "AZURE_CLIENT_ID",
          "clientSecretSettingName": "AZURE_CLIENT_SECRET"
        }
      }
    }
  },
  ・・・
```

::: note
自動生成されるファイルをソース管理に入れておくと、ローカルで実行したときに差分が出る可能性があるので、`.gitignore`等で除外しておくと良いと思います。
:::

## ローカル開発時に自動生成させる

ローカル開発時、SWA を起動したタイミングで自動的に設定ファイル生成するように npm scripts に定義を追加します。
SWA を起動するスクリプト(`start:swa`)を定義し、`prestart:swa`で SWA の起動前に設定ファイルの生成スクリプト(`swaconfig-gen`)を実行するようにしています。

```diff_json
  "scripts": {
    ・・・
    "swaconfig-gen": "node .swaconfig/swaconfig-gen.js > staticwebapp.config.json",
+   "start:swa": "swa start",
+   "prestart:swa": "npm run swaconfig-gen"
  },
  ・・・
```

## Azure Pipelines でのデプロイ時に自動生成させる

SWA のデプロイタスクの前に、`swaconfig-gen`を実行するステップを追加します。
こうすることで、Pipeline に設定された変数を埋め込むことができます。

```diff_yaml
  steps:
  - checkout: self
    submodules: true
+ - script: "npm run swaconfig-gen" # staticwebapp.config.jsonを生成する
  - task: AzureStaticWebApp@0
    inputs:
      azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
# For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
      app_location: "/" # App source code path
      api_location: "" # Api source code path - optional
      output_location: "out" # Built app content directory - optional
###### End of Repository/Build Configurations ######
```

# 他の方法

## jq コマンドを使用して置換する方法

https://aadojo.alterbooth.com/entry/2021/11/04/090000

こちらは javascript ではなく [jq](https://jqlang.github.io/jq/) コマンドを使用する方法です。
入ってない場合は別途インストールが必要。

## ファイル単位で切り替える方法(恐らくこれが正攻法)

こちらは環境に応じて使用する設定ファイルを切り替える方法です。
単純明快ですが、環境毎にファイルができるため管理が煩雑になる可能性も。
(差分だけマージとかではなく共通部分も含めて複製しないといけない)

こんな感じで設定ファイルを配置しておいて

```
.swaconfig
├ local
│ └ staticwebapp.config.json
├ dev
│ └ staticwebapp.config.json
└ prod
　 └ staticwebapp.config.json
```

### ローカルの場合


`--swa-config-location`で設定ファイルのディレクトリを指定します。

```bash:bash
SWA_CONFIG_LOCATION=.swaconfig/local
npx swa start --swa-config-location $SWA_CONFIG_LOCATION
```

https://azure.github.io/static-web-apps-cli/docs/cli/swa-start

### Pipeline の場合

デプロイタスクの入力パラメータ`config_file_location`に設定ファイルのディレクトリを指定します。

```diff_yaml
  - task: AzureStaticWebApp@0
    inputs:
      azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
# For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
      app_location: "/" # App source code path
      api_location: "" # Api source code path - optional
      output_location: "out" # Built app content directory - optional
+     config_file_location: $(SWA_CONFIG_LOCATION)
###### End of Repository/Build Configurations ######
```

https://learn.microsoft.com/ja-jp/azure/devops/pipelines/tasks/reference/azure-static-web-app-v0?view=azure-pipelines

# あとがき

ここまで書いておいて何ですが、やはり力業ではなく正攻法(環境毎に設定ファイルを分ける)で行くべきなのかな？と思いました。

よくある共通の設定ファイルがあって、各環境用のファイルには差分のみ設定(上書き)する、というのができたらいいのになー。