はじめに
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 ボリュームをマウントしたディレクトリで
- Wallet ファイル (zip) をダウンロードする
- ダウンロードした zip ファイルを解凍 (unzip) する
の作業を行えば OK です。
アプローチ #1 - public コンテナ・イメージを使って Wallet を配置する
- Wallet ファイルのダウンロード → OCI CLI コンテナ・イメージ
- zip ファイルの解凍 → busybox
の Init コンテナ 2段構えで Wallet ファイルを配置します。
OCI CLI ではシンプルにインスタンス・プリンシパルを使っています。
テスト用 Pod のマニフェスト
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" というファイルに書き出しています。パスワードの要件は ドキュメンテーション に書いてあります("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 を使ってサクっとダウンロード用コンテナを作ってしまいましょう。マニフェストもすっきりします。
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
FROM python:3.9-slim
WORKDIR /oci
RUN pip install oci
COPY *.py ./
ENTRYPOINT [ "python", "download-adb-wallet.py" ]
テスト用 Pod のマニフェスト
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 ファイルの扱いが楽になるとよいですね。