AWS App Runner の環境変数を SecretsManager で管理する

# はじめに

暑さ寒さも彼岸までとはよく言ったものです。
長野では朝夕の寒さで風邪をひきそうな [@\_\_\_nix___](https://twitter.com/___nix___)  です。

今回はいつまで経っても煮え切る様子の無い AWS App Runner の記事をご紹介します。

# 背景
AWS App Runner はリリース当初、「遂に来たか...」感満載なサービスでした。
Dockerコンテナを簡単に、そしてスケーリングまで自動でしてくれる画期的な機能を持ち合わせていました。

最初の壁は「RDSにどうやって接続するの？」でした。
早速、AWSさんは VPC Connector を用意してくれて RDS との接続を実現させました。
（この VPC Connector はネットワーキングの概念が少し特殊で悩ましかったことを覚えています）

次に出てきたのが「カスタムドメインで ZoneApex が使えない」でした。
顧客に出来ると説明しておきながら実際に設定しようとしたらまだ未対応だったのがツラミでした。

そして次なる要望は続きます。「WAFで守れないの？」
これも AWSさんは頑張って WAF が使えるようになりました。


# 概要

他愛も無い話はここまでにして、本題です。
App Runner の環境変数に SecretsManager や ParameterStore が使えるようになったのをご存知でしょうか。

ただ、この環境変数を使おうとすると少しだけ工夫が必要で、そもそもこの仕様を把握できていなければどうやって環境変数を使うのかさえ分からない状況です。

意外と説明されている記事が無かったので触れていきたいと思います。

# 登場人物

* AWS App Runner
* ECRリポジトリ
* Docker関連
    * Dockerfile
    * app.py
    * entrypoint.sh
    * requirements.txt
* IAMロール
    * インスタンスロール
    * ECRアクセスロール
* 環境変数管理
    * SecretsManager
    * ParameterStore 

# 主要関連リソース

## Docker関連
<details>
<summary>Dockerfile</summary>

```
FROM python:3.9-slim-buster

RUN apt-get update && apt-get install -y \
awscli \
jq \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

RUN mkdir -p /var/log/app

ENV PYTHONUNBUFFERED=1

ENTRYPOINT ["/entrypoint.sh"]
```
</details>

<details>
<summary>app.py</summary>

```
from flask import Flask, jsonify
import os

app = Flask(__name__)

@app.route('/api/hoge', methods=['GET'])
def get_all_environment_values():
    env_vars = {key: value for key, value in os.environ.items()}
    return jsonify(env_vars)

if __name__ == '__main__':
    port = int(os.environ.get('PORT', 8080))
    app.run(host='0.0.0.0', port=port)
```
</details>

<details>
<summary>entrypoint.sh</summary>

```
#!/bin/bash

set -e

while read -r line; do
    export "$line"
done < <(echo "$sm_env" | jq -r 'to_entries | .[] | "\(.key)=\(.value)"')


python app.py
```
</details>

<details>
<summary>requirements.txt</summary>

```
flask==2.0.1
werkzeug==2.0.1
```
</details>

## IAMロール

<details><summary>ECRアクセスロール（権限ポリシー）</summary>

```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage",
                "ecr:DescribeImages",
                "ecr:GetAuthorizationToken",
                "ecr:BatchCheckLayerAvailability"
            ],
            "Resource": "*"
        }
    ]
}
```
</details>

<details><summary>インスタンスロール（権限ポリシー）</summary>

```
{
  "Statement": [
    {
      "Sid": "SecretsManager",
      "Action": [
        "secretsmanager:GetSecretValue",
        "kms:Decrypt*"
      ],
      "Effect": "Allow",
      "Resource": [
        "arn:aws:secretsmanager:<region>:<aws_account_id>:secret:<secret_name>",
        "arn:aws:kms:<region>:<aws_account_id>:key/<key_id>"
      ]
    },
    {
      "Sid": "ParameterStore",
      "Action": [
        "ssm:GetParameters"
      ],
      "Effect": "Allow",
      "Resource": [
        "arn:aws:ssm:<region>:<aws_account_id>:parameter/<parameter_name>"
      ]
    }
  ],
  "Version": "2012-10-17"
}
```
</details>

# 環境構築

## ECR イメージ作成

```shell
# ECR に login
aws ecr get-login-password --region <region> | docker login --username AWS --password-stdin <aws_account_id>.dkr.ecr.<region>.amazonaws.com

# Docker ビルド
docker build -t test .

# タグ設定
docker tag test:latest <aws_account_id>.dkr.ecr.<region>.amazonaws.com/test:latest

# ECR に push
docker push <aws_account_id>.dkr.ecr.<region>.amazonaws.com/test:latest
```

## 環境変数の設定(AWSリソース)

### SecretsManager
ARN : `arn:aws:secretsmanager:<region>:<aws_account_id>:secret:<secret_name>`
値 : `{"sm_key1":"sm_value1","sm_key2":"sm_value2"}`

### ParamerterStore
ARN : `arn:aws:ssm:<region>:<aws_account_id>:parameter/<parameter_name>`
値 : `{"ps_key1":"ps_value1","ps_key2":"ps_value2"}`

## AWS App Runner のサービス作成

### サービス作成

ここは AWSマネージドコンソールでポチポチしてください。
また機会があったら Terraform のコードをご紹介します。

### 環境変数の設定(App Runner)

| 形式またはソース | 名前 | 値 |
| --- | --- | --- |
| プレーンテキスト | HOGE_ENV1 | hoge |
| プレーンテキスト | HOGE_ENV2 | fuga |
| SSM パラメータストア | ps_env | `arn:aws:ssm:<region>:<aws_account_id>:parameter/<parameter_name>` |
| Secrets Manager | sm_env | `arn:aws:secretsmanager:<region>:<aws_account_id>:secret:<secret_name>` |

## 環境変数の確認

構築後の AWS App Runner のデフォルトドメインを使って確認してみます。

`https://<service_name>.<region>.awsapprunner.com/api/hoge`
```
{
  ...
  "HOGE_ENV1": "hoge",
  "HOGE_ENV2": "fuga",
  "sm_env": "{\"sm_key1\":\"sm_value1\",\"sm_key2\":\"sm_value2\"}",
  "sm_key1": "sm_value1",
  "sm_key2": "sm_value2",
  "ps_env": "{\"ps_key1\":\"ps_value1\",\"ps_key2\":\"ps_value2\"}",
  ...
}
```

## 解説

環境変数は SecretsManager や ParameterStore に設定した値よりも階層が一段下がります。
つまり、環境変数はそれぞれ以下のように格納されます。

```
sm_env={"sm_key1":"sm_value1","sm_key2":"sm_value2"}
```


`sm_key1` や `sm_key2` の環境変数を使う場合は `entrypoint.sh` の以下の処理がポイントです。

```shell
while read -r line; do
    export "$line"
done < <(echo "$sm_env" | jq -r 'to_entries | .[] | "\(.key)=\(.value)"')
```

`$sm_env` の値を `jq` で展開してそれぞれの環境変数としてセットし直しています。

# 終わりに

誰にでもすぐにテストできるよう Docker関連のファイルもご紹介したことで記事のボリュームが増えてしまいました。
ただ、気軽に環境変数を AWSで管理したいと思った時にこれを知らないと大変です。

自分達で作成しているアプリであれば環境変数を展開する処理を追加すれば良いですが、公式の Dockerイメージを使う場合、所定の環境変数に合わせる必要があります。
そんな時は entrypoint.sh のような処理を追加してあげてみてください。


# 一言
この記事良かったと少しでも思って頂けたら是非 [@\_\_\_nix___](https://twitter.com/___nix___) をフォローしてあげてください。或いは記事に対してリアクションをお願い致します。