GCE コンテナ最適化OSにSecret Managerから環境変数を渡す

# 動機

コンテナ化されている常時起動サーバをGCP上で「簡単に」「安く」立ち上げたい。

**そのサーバが利用するAPI KEYなどの秘密情報の生の値をGCPサービスの設定に直接書くのではなく、Secret Manager で管理したい。**

解決: 
- GCEでContainer-Optimized-OS イメージのe2-microを常時起動
- 起動時に secret を gcloud なしで取得する
- docker コマンドに --env-file 取得した secret の内容をで渡す

以下は詳細です。

## アプローチと課題

- Cloud Run?
  - secret を環境変数に設定する機能はある
  - 常時起動させる場合の最小コンテナが n1-standard からで、e2-micro など節約インスタンスが選択できない
- GKE (k8s) ?
  - secret を環境変数に設定する機能はある(かなり yaml を書く必要はありそう)
  - クラスタがある程度のノードプールを必要とするので、やはり e2-micro x 1 などの節約インスタンスには向かない
- GCE でコンテナ付きVMを使う?
  - secret からとってきた値を設定する機能は提供されていない
  - 環境変数を直書きすることはできるが、これを避けたい 
- コンテナ付きVMではなく Container Optimized OS を使い、自分で secret を取得してその結果をもとに `dcoker run -eKEY1=VALUE1 IMAGE` する?
  - gcloud コマンドが使えないので secret を取得することができない
  - gcloud コマンドをインストールしようとすると権限周りの問題が発生してすぐにはできなさそう
- 標準の各種Linux VMイメージを使う?
  - gcloud は入っている
  - が、今度は逆に docker コマンドが使えない
  - docker コマンドをインストールすることはできそう
  - こちらを掘っても良かったが、Cotainer Optimized OS を使いたい気がしてくる


## 解決1：※アプリケーションの改修が必要なので解決2がおすすめ

「GCE でコンテナ付きVMを使う」アプローチになりました。

- gcloud が入っていなくても curl で直接 Secret Manager のAPIを叩けばよい
  - 適切な権限をもった access token が必要だが、VMの中からそれを取得する方法がある
- VM の startup script で secret を取得してホストVMの /tmp/.env などに書き込んでおく
- この /tmp/.env をコンテナから参照できるようにする
  - コンテナ起動時に /tmp を /mnt などにマウントしておく
    - ただし、このマウント設定はコマンドラインではVMインスタンス作成時にしか指定できない
    - 一度 GCEインスタンスを作ってしまったら GCPコンソールから修正するか、非公開の仕様になっている metadata を修正するしかない
- アプリケーション側で `/mnt/.env` を参照して環境変数を設定するような修正をいれる
  - アプリケーションに手を入れなければならないところが非常にいけていないので、もっと良い方法があれば探したい→解決2ができたのでそちらを推奨

## 作成方法

### secret の登録
設定したい環境変数の内容を`.env`というファイル名(何でもよい)で作成しておきます

```.env
KEY1=VALUE1
KEY2=VALUE2
...
```

これを Secret Manager に登録します。以降、`[YOUR_*]` とあるところは適切な値に置き換えてください。

```bash
gcloud compute secrets create [YOUR_SECRET_NAME] \
  --data-file=.env
```

### startup script の作成

VM用の startup script を `startup.sh` というファイル名で作っておきます

```startup.sh
#!/bin/sh

# this script is an equivalent to:
# `gcloud secrets versions access latest --secret=$SECRET_NAME > .env`

SECRET_NAME=[YOUR_SECRET_NAME]

PROJECT_ID=$(curl -s -H 'Metadata-Flavor: Google' 'http://metadata.google.internal/computeMetadata/v1/project/project-id')
ACCESS_TOKEN="$(curl -s -H 'Metadata-Flavor: Google' \
'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token' \
 | jq -r '.access_token')"

curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://secretmanager.googleapis.com/v1/projects/$PROJECT_ID/secrets/$SECRET_NAME/versions/latest:access" \
| jq -r '.payload.data' | base64 --decode \
> /tmp/.env
```

### アプリケーションの修正

アプリケーションを、`/mnt/.env` から環境変数を取得するように改修します。

Python だと dotenv-python を使うなど

### VMインスタンスの作成

まず、VM用のサービスアカウントを作成します。

必要な権限は以下のようですが、もう少し絞れるかもしれません。Compute Engine のデフォルトのサービスアカウントを使ってもよいのですが、`roles/secretmanager.secretAccessor` は追加でつける必要があります。

- roles/secretmanager.secretAccessor
- roles/containerregistry.ServiceAgent
- roles/logging.bucketWriter
- roles/logging.logWriter

```bash
SERVICE_ACCOUNT=[YOUR_SERVICE_ACCOUNT]
PROJECT_ID=[YOUR_PROJECT_ID]

gcloud iam service-accounts create $SERVICE_ACCOUNT \
 --display-name=$SERVICE_ACCOUNT


for role in secretmanager.secretAccessor containerregistry.ServiceAgent logging.bucketWriter logging.logWriter; do
  gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member="serviceAccount:${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com" \
    --role=roles/$role
done
```


前述の startup script を使って コンテナ付きVM を作成します。

- ホストの `/tmp` を コンテナの `/mnt` にマウント
- `startup.sh` を起動時に実行して、Secret Manager から取得した secret の値を `/tmp/.env` に書き出す → コンテナから `/mnt/.env` として見える

```bash
INSTANCE=[YOUR-INSTANCE_NAME]
ZONE=[YOUR_ZONE]
IMAGE=[YOUR_IMAGE]
MACHINE_TYPE=[YOUR_MACHINE_TYPE]
SERVICE_ACCOUNT=[YOUR_SERVICE_ACCOUNT]
PROJECT_ID=[YOUR_PROJECT_ID]

gcloud compute instances create-with-container \
  $INSTANCE \
  --project_id=$PROJECT_ID \
  --zone=$ZONE \
  --container-image=$IMAGE \
  --machine-type=$MACHINE_TYPE \
  --service-account=$SERVICE_ACCOUNT \
  --scopes=https://www.googleapis.com/auth/cloud-platform \
  --container-mount-host-path=mode=ro,host-path=/tmp,mount-path=/mnt \
  --metadata-from-file startup-script=./startup.sh
```

これで完成!

もっと良い方法がありましたらアイデア歓迎です！マサカリも歓迎です！

## 解決2:アプリケーションの改修不要

解決1 ではアプリケーションに /tmp/.env を読ませるための改修が必要でしたが、docker の起動を自前でやることでこの対応が不要になりました。

Container-Optimized-OS を利用しつつ、startup-script を用いて docker を起動させます。

secret の設定とサービスアカウントの作成は解決1と同じです。startup-script に、自力で ECRにログインして docker を起動する記述を追加しました。

コンテナイメージの指定 `IMAGE=[YOUR_IMAGE]` はこのスクリプトにハードコードしていますが、.env の中に書いておいてそれを参照するようにしてしまえばこのスクリプトを修正せずに使いまわせるので、その方が良いかもしれません。

### startup-script

```bash
SECRET_NAME=[YOUR_SECRET_NAME]
IMAGE=[YOUR_IMAGE]

# this script is an equivalent to :
# gcloud secrets versions access latest --secret=$SECRET_NAME > .env

function meta {
  curl -s -H 'Metadata-Flavor: Google' 'http://metadata.google.internal/computeMetadata'$1
}

PROJECT_ID=$(meta '/v1/project/project-id')
ACCESS_TOKEN="$(meta '/v1/instance/service-accounts/default/token' | jq -r '.access_token')"

curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
    "https://secretmanager.googleapis.com/v1/projects/${PROJECT_ID}/secrets/${SECRET_NAME}/versions/latest:access" \
  | jq -r '.payload.data' \
  | base64 --decode \
  > /tmp/.env

HOME=/tmp docker-credential-gcr configure-docker 
HOME=/tmp docker run --name=mycloudservice --restart always --env-file=/tmp/.env $IMAGE &
```

起動後も /tmp/.env が残るのが少しいけていない感はあります。


### GCEインスタンスの作成

GCEインスタンスの作成は `create-with-container` ではなく普通の `create` になります。

```
gcloud compute instances create \
  $INSTANCE_NAME \
  --zone $ZONE \
  --machine-type=$MACHINE_TYPE \
  --service-account $SERVICE_ACCOUNT \
  --scopes=https://www.googleapis.com/auth/cloud-platform \
  --image-family cos-stable \
  --image-project cos-cloud \
  --metadata-from-file startup-script=./startup-script
```



### 補足

解決1での コンテナ設定部分の VM instance metadata は現時点では以下のようになっていました。

```yaml
    value: |-
      spec:
        restartPolicy: Always
        containers:
        - name: [YOUR_INSTANCE_NAME]
          image: '[YOUR_IMAGE]'
          volumeMounts:
          - name: host-path-0
            mountPath: /mnt
            readOnly: true
        volumes:
        - name: host-path-0
          hostPath:
            path: /tmp

      # This container declaration format is not public API and may change without notice. Please
      # use gcloud command-line tool or Google Cloud Console to run Containers on Google Compute Engine.
```