コンテナ・アプリケーションから Autonomous Database に接続する際の Wallet ファイルを Init Container を使って配置する

# はじめに

Kubenetes 上で動作するコンテナ・アプリケーションで、Autonomous Database に接続する際の Wallet ファイルをどのように扱うべきかというのは、いろいろと悩ましいところです。ConfigMap に Wallet ファイル一式をつっこんでおいて、アプリケーションでマウントするというのがシンプルですが、Wallet がローテーションされたり、Wallet の中にある証明書の有効期限が切れたりするイベントが発生するタイミングを意識して ConfigMap (= Wallet) をメンテナンスしないといけません。
メンテナンス・フリーにする解としてアプリケーションの起動時に都度最新の Wallet ファイルを取得して使用するという方法が考えられますが、この Wallet ファイルをダウンロードする一連の手続きをいちいちアプリケーション側に埋め込むのは非効率的です。
幸い Kubernetes には Init Containers という仕組みがあります。今回はこの仕組みを使って、Pod のアプリケーション・コンテナが起動する前に Init コンテナが Wallet ファイルを取得して指定したディレクトリに配置する方法を紹介します。アプリケーション・コンテナは Wallet ファイルの管理については関知せず、ディレクトリに展開された Wallet ファイルを使うだけです。また、Init コンテナ自体も Wallet を使う他のアプリケーションに使いまわすことができます。

仕組みはシンプルで、Init コンテナで取得した Wallet ファイルを emptyDir ボリューム経由でアプリケーション・コンテナに引き渡すことによって実現します。つまり、Init コンテナ側では emptyDir ボリュームをマウントしたディレクトリで
1. Wallet ファイル (zip) をダウンロードする
1. ダウンロードした zip ファイルを解凍 (unzip) する

の作業を行えば OK です。

# アプローチ #1 - public コンテナ・イメージを使って Wallet を配置する

1. Wallet ファイルのダウンロード → OCI CLI コンテナ・イメージ
1. zip ファイルの解凍 → busybox

の Init コンテナ 2段構えで Wallet ファイルを配置します。
OCI CLI ではシンプルにインスタンス・プリンシパルを使っています。

テスト用 Pod のマニフェスト

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: adb-wallet-download
spec:
  restartPolicy: Never
  containers:
  # Main Container - 本来は Wallet を使った ADB 接続アプリ
  - image: busybox:latest
    name: application-container
    volumeMounts:
    - mountPath: /var/cache
      name: cache-volume
    command: ["/bin/sh"]
    args:
    - -c
    - ls -la /var/cache/wallet && echo "Wallet password is $(cat /var/cache/wallet/password)"
  initContainers:
  # Init Container - step1: Wallet ファイルのダウンロード
  - image: ghcr.io/oracle/oci-cli:latest
    name: download-wallet
    volumeMounts:
    - mountPath: /var/cache
      name: cache-volume
    env:
      - name: ADB_OCID
        value: {{ Autonomous Database の OCID }}
    command: ["/bin/bash"]
    args:
    - -c
    - >-
        mkdir -p /var/cache/wallet &&
        PW=z0$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 14) &&
        oci --auth instance_principal
        db autonomous-database generate-wallet
        --autonomous-database-id $(ADB_OCID)
        --password $PW 
        --file /var/cache/wallet/wallet.zip &&
        echo -n $PW > /var/cache/wallet/password &&
        echo "Wallet password is saved in /var/cache/wallet/password" 
  # Init Container - step2: Wallet ファイルの unzip
  - image: busybox:latest
    name: unzip-wallet
    volumeMounts:
    - mountPath: /var/cache
      name: cache-volume
    command: ["unzip", "-d", "/var/cache/wallet", "/var/cache/wallet/wallet.zip"]
  volumes:
  - name: cache-volume
    emptyDir: {}
```

`PW=z0$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 14)` の部分では Wallet をダウンロードする際に必要なパスワードを生成しています。Wallet パスワードは実際には使う機会が少ないので、ここではランダムなパスワードを生成して Wallet のダウンロードに使用した後、再度利用する時に備えて "password" というファイルに書き出しています。パスワードの要件は [**ドキュメンテーション**](https://docs.public.oneportal.content.oci.oraclecloud.com/ja-jp/iaas/autonomous-database-shared/doc/connect-download-wallet.html) に書いてあります（"z0" で始まるようにしてあるのは要件を満たすため工夫）。

では、実行してみましょう。

```
$ kubectl apply -f adb-wallet-download.yaml 
pod/adb-wallet-download created

$ kubectl get pods
NAME                  READY   STATUS      RESTARTS   AGE
adb-wallet-download   0/1     Completed   0          18s
```

アプリケーション・コンテナのログを確認します。

```
$ kubectl logs adb-wallet-download
Defaulted container "main-container" out of: application-container, download-wallet (init), unzip-wallet (init)
total 84
drwxr-xr-x    2 1000     1000          4096 Mar 21 09:10 .
drwxrwxrwx    3 root     root            20 Mar 21 09:10 ..
-rw-r--r--    1 root     root          3037 Mar 21 09:10 README
-rw-r--r--    1 root     root          6765 Mar 21 09:10 cwallet.sso
-rw-r--r--    1 root     root          6720 Mar 21 09:10 ewallet.p12
-rw-r--r--    1 root     root          7520 Mar 21 09:10 ewallet.pem
-rw-r--r--    1 root     root          3225 Mar 21 09:10 keystore.jks
-rw-r--r--    1 root     root           691 Mar 21 09:10 ojdbc.properties
-rw-r--r--    1 1000     1000            16 Mar 21 09:10 password
-rw-r--r--    1 root     root           114 Mar 21 09:10 sqlnet.ora
-rw-r--r--    1 root     root          1310 Mar 21 09:10 tnsnames.ora
-rw-r--r--    1 root     root          3378 Mar 21 09:10 truststore.jks
-rw-r--r--    1 1000     1000         26695 Mar 21 09:10 wallet.zip
Wallet password is z0rS1W6eL6ny9W8T
```

Init コンテナ側のログも見てみましょう。

```
$ kubectl logs adb-wallet-download -c download-wallet
Downloading file
Wallet password is saved in /var/cache/wallet/password

$ kubectl logs adb-wallet-download -c unzip-wallet
Archive:  /var/cache/wallet/wallet.zip
  inflating: ewallet.pem
  inflating: README
  inflating: cwallet.sso
  inflating: tnsnames.ora
  inflating: truststore.jks
  inflating: ojdbc.properties
  inflating: sqlnet.ora
  inflating: ewallet.p12
  inflating: keystore.jks
```

# アプローチ #2 - 専用コンテナを使って Wallet を配置する

処理自体は大したことないので、 OCI SDK for Python を使ってサクっとダウンロード用コンテナを作ってしまいましょう。マニフェストもすっきりします。

```python
import os, random, string, shutil, argparse, oci
from oci.database import DatabaseClient
from oci.database.models import GenerateAutonomousDatabaseWalletDetails

def download_wallet(id, dir, password=None, update_sqlnet_ora=False) -> str:
    zip_file = os.path.join(dir, 'wallet.zip')
    os.makedirs(dir, exist_ok=True)

    pw = password if password else 'z0' + ''.join(random.choices(string.ascii_letters + string.digits, k=14))

    signer = oci.auth.signers.InstancePrincipalsSecurityTokenSigner()
    details = GenerateAutonomousDatabaseWalletDetails(password=pw, generate_type='SINGLE')
    db_client = DatabaseClient({}, signer=signer) # type: DatabaseClient
    response_data = db_client.generate_autonomous_database_wallet(id, details).data
    with open(zip_file, 'wb') as f:
        for chunk in response_data.raw.stream(1024 * 1024, decode_content=False):
            f.write(chunk)
    shutil.unpack_archive(zip_file, dir)
    print(f'zip file was unpacked in {dir}')

    if not password:
        password_file = os.path.join(dir, 'password')
        with open(password_file, 'wt') as f:
            f.write(pw)
        print(f'Wallet password is saved in {os.path.abspath(password_file)}')

    if update_sqlnet_ora:
        sqlnet_ora = os.path.join(dir, 'sqlnet.ora')
        with open(sqlnet_ora, 'w') as f:
            f.write(f'WALLET_LOCATION = (SOURCE = (METHOD = file) (METHOD_DATA = (DIRECTORY="{os.path.abspath(dir)}")))\n')
            f.write(f"SSL_SERVER_DN_MATCH=yes\n")
        print(f'sqlnet.ora was modified')

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("--id", required=True)
    parser.add_argument("--dir", required=True)
    parser.add_argument("--password") # パスワードは自動生成でなく指定も可
    parser.add_argument("--update-sqlnet-ora", action='store_true')
    args = parser.parse_args()
    download_wallet(args.id, args.dir, args.password, args.update_sqlnet_ora)
```

イメージ作成用 Dockerfile

```Dockerfile
FROM python:3.9-slim
WORKDIR /oci
RUN pip install oci
COPY *.py ./
ENTRYPOINT [ "python", "download-adb-wallet.py" ]
```

テスト用 Pod のマニフェスト

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: adb-wallet-download-python
spec:
  restartPolicy: Never
  containers:
  # Main Container - 本来は Wallet を使った ADB 接続アプリ
  - image: busybox:latest
    name: main-container
    volumeMounts:
    - mountPath: /var/cache
      name: cache-volume
    command: ["/bin/sh"]
    args:
    - -c
    - ls -la /var/cache/wallet && echo "Wallet password is $(cat /var/cache/wallet/password)"
  initContainers:
  # Init Container - Wallet ファイルのダウンロード
  - image: {{ 自作したコンテナ・イメージ }}
    imagePullPolicy: Always
    name: download-wallet
    volumeMounts:
    - mountPath: /var/cache
      name: cache-volume
    args:
    - --id 
    - {{ Autonomous Database の OCID }}
    - --dir
    - /var/cache/wallet
  volumes:
  - name: cache-volume
    emptyDir: {}
```

# まとめ

注意点
OKE 上の Pod であることを前提に OCI の認証にインスタンス・プリンシパルを使っています。OKEのワーカー・ノード（= コンピュート・インスタンス）を含む動的グループを作成して Autonomous Database の Wallet ファイルをダウンロードできるようにポリシーを設定しておくことが前作業として必要です。

OKE のワーカーノードを限定的に動的グループに含めるには、ワーカーノードのコンピュート・インスタンスに特定のタグを振っておいてこれを使うのが便利です。

```
All {instance.compartment.id='...', tag.some-namespace.sone-key.value='...'}
```

ということで、これでいくらか Wallet ファイルの扱いが楽になるとよいですね。