はじめに
Kubernetes/OpenShiftにおけるアプリケーション運用の高度化において「GitOps」は避けては通れません。しかし、GitOpsとSecret管理の両立には課題があります。Manifest群をGit Repositoryで管理する上で「Secretどうする」問題が付き物です。
というのも、Secretは機密情報を管理するためのK8sリソースですが、機密情報にあたる値はbase64エンコードされているだけなので誰でもデコードできちゃいます。つまり、公開Repositoryなんぞにアップしたら、それは平文のまま置いているのと同じです...
Hashicorp Vaultを使おう
K8s Secretに限らず、あらゆる機密情報管理ソリューションとして広く使われているのが「Hashicorp Vault (以下Vault)」です。Kubernetesとの連携もばっちりで、特定のNameSpaceの特定のSercvice Accountに対して認証認可を経て適切なSecret操作権限(read, create, delete 等々)を付与してくれます。
2023年3月に公開された「Vault Secret Operator (以下VSO)」によって、大変に使い勝手がよくなりました。
要はこういうことが実現したい
実現したいことを絵に描いてみました。
Manifest.yaml
をSingle Source of TruthとしてGitOpsを実現するのと全く同じ雰囲気で、Vaultに唯一無二の真実のSecret情報(Single "Secret" of Truth)を格納し、その更新をK8s/OpenSshift Clusterに自動的に反映させたいのです。また、Pod
やDeployment
等のManifest
に対しては、なんらかの工夫や特別な記載をしたくもないのです。
まさにこうしたことがVSOでは実現可能です!
余談(興味なければ読まずにOK)
- VSO登場前は主に2通りの方法でManifest/KubernetesにSecret情報を参照させていたのですが、ぶっちゃけ初心者の私にはしんどかった印象です...
-
Vault Agent方式: Secretを参照させたいユーザワークロード(Mysql 等)のコンテナに
init Container
、あるいはSidecar
としてAgentを仕込み、AgentがVaultからSecret情報を取得、Pod
内の共有メモリに展開する格好でユーザワークロードコンテナにSecret情報をシェアする。Sidecar方式を使えば、Vault上のSecret情報の更新をPod
に反映できる。 -
Vault CSI Provider方式:
CSI Driver
を応用したSecret Store CSI Driver
という仕組みによって、Pod
にVolumeをマウント、そのVolumeにファイルとしてのSecret情報を格納することで、Pod/Container
がSecret情報を参照できる方式。K8s ClusterにSecret.yamlが作られるわけではない。Vault上のSecret情報の更新はDeploy済みのPod
に反映できない。
-
Vault Agent方式: Secretを参照させたいユーザワークロード(Mysql 等)のコンテナに
- 各方式の詳細やメリデメはHashicorp Vaultの公式サイトをご覧ください。
さっそくOpenShiftで試していきます〜!環境情報は以下。
- Red Hat OpenShift Services on AWS (v4.15.0)
- 特段環境差分は関係ないはずです。
OpenShiftにVaultをDeployする
実はVaultをSetupしてから使えるようになるまでは、それなりにStepが必要です。以下に記載した流れでやっていきます。
- VaultをDeployする為のNameSpaceを作成
- Helm ChartからDeploy
- Route公開
- Initialize
- Kubernetesと接続(Authentication)
- Secret Engine作成(Key-Value)
VaultをDeployする為のNameSpaceを作成
適当なNamespaceを作って(vault
としました)ください。
oc new-project vault
Helm ChartからDeploy
VaultはHelm Chartを公開しておりますので、そちらを使います。
「追加」のリンクをクリックして、
「Helmチャート」をクリックして、
「vault」と検索して出てきたHelmチャートをクリックして、
「作成」をクリックして、
次も「作成」をクリックしてください。
すると、トポロジー画面に遷移して、2つのリソースがDeployされます。
-
vault
:StatefulSet
としてDeployされます。これがVault本体です。 -
vault-agent-injector
: こちらはAgent方式の際にInit Container
やSeidecar
を仕込む動作をするDeployment
です。SVO方式では不要です。
Vaultは2つのPortでそれぞれにService
が付いていますが、デフォルトだとRoute
は存在しません。
なので、Route
をくっつけてInternetからアクセスできるようにします。
Route公開
管理者向け表示の「ネットワーク」 → 「Route」と進んで、「Routeの作成」をクリック、
名前は適当なものをご自由にどうぞ。自分はroute-vault
としました。
Service
は「vault」を選択、TargetPort
は「8200 → 8200」を選びます。
これで「作成」をクリック。再び「開発者向け表示」の「トポロジー」画面に戻ります。
Route
が付きました。これでInternetからVaultのコンソール画面にアクセス可能になりました。
Initialize
Vaultはインストール直後はSealed(暗号化)されており使えません。そのため、Unseal(暗号化解除)の作業が必要です。この作業は初回のみ必要です。ただしVaultコンテナが停止したり、再作成された場合は再度Useal作業が必要になります。
早速RouteのURLをクリックしてVaultのコンソール画面にいきましょう。
こんな画面が出て「え...?」ってなりますよね。
-
Key Shares
: Unsealする為に必要なそれぞれユニークなKey
をいくつ払い出すか?を指定できます。 -
Key threshold
: 「払出したKey
をいくつ使わないとUnsealできないか」を指定できます。
つまり、「ユニークなKey
5つのうち、3つを使わないとUnsealできないようにする」みたいな感じです。
ちょっとやってみましょう。
それぞれ5
と3
と入れてみて、「Initialize」をクリック。root token
とkey
5つが生成されました。これらは無くすと大変なので、厳密に管理してください。
※無くした場合は再度初期化が必要です。その場合、Vaultに登録されたSecret情報も消えます。
「Download keys」をクリックして.json
ファイルを入手、適切に管理してください。つぎに「Continue to Unseal」をクリックしますと、
こんな画面になります。「Unseal Key Portion」の欄に、Key Shares
で指定した値(先程は5
を指定しました)だけ払い出されたKey
のうち、Key threshold
で指定した数(先程は3
を指定しました)だけ選んでコピペして「Unseal」をクリックします。
やってみましょう。
右下に「1/3 keys provided」と出ています。1つめに選んだKey
以外のもので同じことを2回繰り返すと、Unsealできます。
ログイン画面になります。
初期状態だとroot token
以外でのログイン手段がありませんので、先程の.json
ファイルの中にあるroot token
を使ってログインします。
やりました!これでVaultの初期設定は終了です!
次にKubernetenetes Clusterとの接続を行います。
Kubernetesと接続(Authentication)
TOP画面の左メニューから「Access」 → 「Authentication Methods」と進みます。
右上の「Enable new method」をクリックしてください。
「Kubernetes」を選んで、「Next」をクリックして、
「Kubernetes host」は、Kubernetes API
(Cluster外からKubernetesを操作するための情報)のホスト情報を入れます。なお、今回はVaultはKubernetes Clusterの上で動いている為、内部ホスト名でアクセス可能です。
Kubernetes API Serverの内部ホストはhttps://kubernetes.default.svc.cluster.local:443
です。
内部ホスト名を入力して「Save」をクリック。
これにてKubernetes Clusterとの連携が可能となりました。
Secret Engine作成(Key-Value)
VaultはSecret Engineの中にSecret情報を管理します。今回は最も一般的なSecret EngineであるKey-Valueタイプのものを使います。
VaultのTOP画面の左メニューから「Secret Engines」をクリックして、右上の「Enable new engine」をクリックしてください。
「Path」に任意のSecret Engine Pathを設定します。今回はsecret
にしてみました。
そのまま「Enable Engine」をクリックしてください。
secret
という名のSecret Engine(KV-V2タイプ)ができました。
この中にSecretファイルを作りましょう!右上の「Create secret」をクリック!
(Secretがゲシュタルト崩壊しそう...)
「Path for this secret」でSecretファイルへのPathを設定する事ができます。
まずはtest/test-secret
と入力し、SecretのKey,Value
に適当な値を入れてください。
これによって以下のようなSecretファイルをVault上に作成した事になります。
apiVersion: v1
kind: Secret
metadata:
name: mysql-secret
type: Opaque #特に型指定しない場合の一般的なSecretファイル形式です
Data:
hogehoge: ZnVnYWZ1Z2E= #fugafugaがBASE64エンコードされた文字列です
「Path」タブをクリックすると、今作ったSecretファイルのSecret Engine内におけるPathが確認できます。
すごいややこしいのですが、先ほど作成したSecret
を参照するためのPath指定は、
- APIで指定する場合:
/version/secret/data/test/test-secret
- CLIから指定する場合:
mount=Secret Engine名, Path=test/test-secret
となります。このPath情報は後ほどVault上でPolicy
を作成したり、SVOから提供されるCustomResource
を作成する際に使いますので、頭の片隅に入れておいてください。
お疲れ様でした。これでOpenShift Clusterの中でVaultが使えるようになり、これからOpenShiftのNameSpace
に同期させたいSecret
の準備もできました。では、Single "Secret" of Truthを実現させましょう!
Secret EngineとNameSpace内のSecretの同期を試す
ざっくり2つの事をやっていきます。
まずはVault内でPolicy
とRole
を作ります。
-
Policy
: Secret Engine内のSecretに対する権限(CRUD 等)を定義したもの -
Role
: どのNameSpace
のどのServiceAccount
にpolicy
を紐づけるのかを定義したもの
次に、Secretを同期したい宛先NameSpace内に「Vault Secret Operator(VSO)」が提供するCustomResouce
を3つ作成します。
-
VaultConnection
: Vaultとの接続情報を定義したもの -
VaultAuth
: どの認証Methodを使って、どのSeviceAccout
からのアクセスを許可し、更にどのRoleを適用するのか?」を定義したもの。今回は認証Method(Authentication)として既にKubernetesを設定済みです。 -
VaultStaticSecret
: VaultAuthを経て認証認可されたServiceAccountが、Vaultに格納されているどのSecret情報(Static)をどう取り扱うのか?を定義します。
がんばってやっていきましょう!
Policy
を作成
Secret Engine名secret
の中のtest/test-secret
を閲覧のみ可能(ReadOnly)なポリシーを作成します。
なお、VaultのポリシールールはVault policiesが参考になります。
VaultのTOP画面から左メニューの「Plicies」をクリックして、右上の「Create ACL Policy」をクリックします。
「Name」はポリシー名ですので、なんらかわかり易いものを入れておきます。とりあえずtest-readonly
としてみました。Vault policiesを参考にしながら作ります。今回は、先ほど作成したSecret
の参照権限のみをもたせたPolicy
を作成します。
path "secret/data/test/test-secret" {
capabilities = ["read"]
}
となります。Policy作成時のPathにはSecretの「API Path」を入れます。ちなみに、もし作成したSecret Engine内全てのSecret
に参照権限を与えたい場合は
path "secret/*" {
capabilities = ["read"]
}
こんな感じでPathにワイルドカード表現を設定すればOKです。今回は明示的に対象のSecret
ファイルのみを指定しましたが、以下の挙動はどちらのPolicy
でも同じです。
OKなら「Create policy」をクリックしてください。
次はRole
を作成します。
Role
を作成
再びVaultのTOP画面から「Access」 → 「Authentication Method」と進み、先程EnableしたMethodである「Kubernetes/」をクリックします。
今このAuthentication MethodにはRoleがありませんので、「Create role」をクリックしてください。
次の画面で、「どのNameSpaceのどのユーザアカウントにどういうポリシーを適用するのか?」を記載します。
まずは「Name」の所にお好きなRole名を入力します。
次に、「Bound service account names」には、NameSpace
に存在するServiceAccount
を入力します。
なお、OpenShiftはNameSpace
を作成するといくつかのServiceAccount
が勝手に作られます。
ちょっとOpenShiftのコンソール画面で「管理者向け表示」にして「ユーザー管理」 → 「ServiceAccounts」とクリックしてみてください。
NameSpace: test
内に存在するServiceAccount
の一覧が出てきます。特に、デフォルトで作られる、その名の通りdefaul
というServiceAccount
がありますので、こいつにSecretの閲覧権限を付与することにしましょう。
もちろん、なんらか任意のServiceAccount
を作って、そいつに権限を付与しても良いです。とりあえず適当なRole
名称を付けて、以下のように設定します。
なお、NameSpace
はNameSpace
名でも、同じSelector
名をキーにして複数指定もできます。
まだ完成ではありません。Policy
の指定をしましょう。「Tokens」というメニューを開くと、色々設定値が出てきます。「Generated Token's Policies」というところに、付与するPolicy
名を指定します。
先程作成したtest-readonly
を入れます。Policy
は複数指定することが可能です。
これで「Save」をクリックすればRole
ができました。
Vault Secret OperatorをInstall
OpenShift ClusterにVSOをInstallしましょう。
先にVSOをInstallするためのNameSpace
を作成してください。(とりあえずvso
としておきます)
oc new-project vso
Operator HubからInstall可能です。Installは一旦デフォルト設定のままで大丈夫です。
Installが完了すると、CustomResource
が作成可能になります。
CustomResource
を作成
あらためて今から作成するCustomResource
を振り返りましょう。
-
VaultConnection
: Vaultとの接続情報を定義したもの -
VaultAuth
: どの認証Methodを使って、どのSeviceAccout
からのアクセスを許可し、更にどのRoleを適用するのか?」を定義したもの。今回は認証Method(Authentication)として既にKubernetesを設定済みです。 -
VaultStaticSecret
: VaultAuthを経て認証認可されたServiceAccountが、Vaultに格納されているどのSecret情報(Static)をどう取り扱うのか?を定義します。
先にNameSpace: test
を作成しておいてください。
oc new-project test
空のProject画面を右クリックして、追加メニューから「Operator提供」をクリックしてください。
「vault」と検索すると、VSOが提供するCustomResource
一覧がでてきます。ここから上述の3つのResource
を作成します。
VaultConnectionを作成
VaultConnectionを選択して作成します。あるいは以下のYAMLをapply
してもOKです。
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultConnection
metadata:
name: vaultconnection-sample
namespace: test
spec:
skipTLSVerify: false
address: 'http://vault.vault.svc.cluster.local:8200'
spec.address
はVaultのアクセス先を設定します。元からhttp://vault.vault.svc.cluster.local:8200
が入っているかと思いますが、これはCluster内部で名前解決できる内部URLになります。一応VaultのService
を見ておきましょうか。
Cluster内でのみアクセス可能なホスト名としてvault.vault.svc.cluster.local
が存在します。これにSchema: http
とPort: 8200
をつければ、先程の内部URLになるわけです。もし別Clusterに存在するVaultであれば、Route
を指定する事になりますが、今回は同一Cluster内で完結した通信です。
VaultAuthを作成
VaultConnection
を作成した際と同様、あるいは以下のYAMLをapply
します。
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: vaultauth-sample
namespace: test
spec:
kubernetes:
tokenExpirationSeconds: 600
role: test-role
#Vault上で作成したRoleを指定してください
serviceAccount: default
#Roleで指定しているServiceAccountと同じものを指定してください
vaultConnectionRef: vaultconnection-sample
#参照するVaultConnection名を指定します。先程作成したものです。
method: kubernetes
mount: kubernetes
#今回はKubernetesをAuthentication Methodに指定しているので、Kubernetesを設定
VaultStaticSecretを作成
同様にVaultStaticSecret
を作成します。YAMLは以下です。
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: vaultstaticsecret-sample
namespace: test
spec:
hmacSecretData: true
destination:
create: true
#同名のSecretが存在しない場合は新規作成する設定
overwrite: true
#同名のSecretが存在する場合に更新する設定
name: test-secret
#作成するSecret名
vaultAuthRef: vaultauth-sample
#参照するVaultAuthを指定します
mount: secret
path: test/test-secret
#Vault上でSecretのPath情報を確認した場合にCLI pathとして示されるものを入れます
type: kv-v2
#Secret Engineのタイプです
refreshAfter: 5s
Secret EngineにあるSecret
がNameSpace
と同期されるか確認
まずはNameSpace: test
の開発社向け表示の左メニュー「シークレット」から、NameSpace
にapply
されたSecret
を見てみます。
あ!test-secret
が存在します。クリックしてみます。
VaultStaticSecret
によって管理されている事がわかります。Secret
の中身もVault上で作成した
apiVersion: v1
kind: Secret
metadata:
name: mysql-secret
type: Opaque #特に型指定しない場合の一般的なSecretファイル形式です
Data:
hogehoge: ZnVnYWZ1Z2E= #fugafugaがBASE64エンコードされた文字列です
が同期されています。Vault上でSecret
を編集してみます。適当にKey-Value
を追加してみて、
「Save」をクリックします。再びOpenShiftのConsoleを見てみてください。
即座にVault上で更新した内容がNameSpace
内のSecret
に反映されています。素晴らしい!
試しにVaultStaticSecret
をNameSpace
から削除してみてください。「インストール済みのOperator」メニューからVSOを選択し、VSOが提供するCustomResource
一覧から「VaultStaticSecretの削除」を選択します。
応用編: Secret
更新を受けてDeployment
のRollout
を試す
ここからは少し応用編です。アプリケーションのDeployment
内で参照するSecret
の情報が変更された場合、それを反映してRollout
させます。これでSecret
の更新がアプリケーション自体にも自動で反映されるわけです。
Kubernetes DeploymentのRolloutについては以下を御覧ください。
https://kubernetes.io/ja/docs/concepts/workloads/controllers/deployment/#updating-a-deployment
Mysql(Ephemeral)をDeploy
サンプルのDeployment
として、PVを利用しないEphemeralなMysqlをDeployしておきます。
適当なNameSpace
を作成し(私はmysql-ephemeral
としておきました)、以下のYAMLをoc apply
してください。
今回はSecret
を先にClusterにapply
してしまいます。
apiVersion: v1
kind: Secret
metadata:
name: mysql-ephemeral-secret
type: Opaque #特に型指定しない場合の一般的なSecretファイル形式です
stringData:
MYSQL_ROOT_PASSWORD: sousuke
#rootユーザのパスワードです
MYSQL_USER: gakenoueno
#user名です
MYSQL_PASSWORD: ponyo
#userのパスワードです
MYSQL_DATABASE: ghibli
#Deploy時に自動作成するDatabase名です
Red Hatが提供しているMysqlのContainer Imageに設定できる環境変数は以下をご参照ください
https://catalog.redhat.com/software/containers/rhel9/mysql-80/61a60915c17162a20c1c6a34?q=mys&architecture=amd64&image=6618250aa47a5279071a75a0
続いてDeployment
もapply
してください。
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql-ephemeral
spec:
selector:
matchLabels:
app: mysql-ephemeral
strategy:
type: Recreate
template:
metadata:
labels:
app: mysql-ephemeral
spec:
containers:
- image: registry.redhat.io/rhel9/mysql-80 #RedHatが公開している安心安全なMysqlコンテナイメージです。Container Hostからのrootログインに限ってはPWレスでログイン可能。
name: mysql-ephemeral
envFrom: #別ファイルから環境変数を読み込む様に指定します
- secretRef: #Secretを参照するように指定します
name: mysql-ephemeral-secret #対象のSecretファイル名
ports:
- containerPort: 3306 #Mysqlのポート番号です
name: mysql-ephemeral
アプリケーションがDeployできました。mysql-ephemeral-secret.yaml
で設定した環境変数が反映されているか確認しておきます。Pod
名をクリックし、
Pod
の詳細画面の「ターミナル」タブからoc exec
していきます。
Mysqlにuserでログインしてみます。
sh-5.1$ mysql -ugakenoueno -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
無事にログインできました。続いてDatabaseが作成されているかも見てみます。
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| ghibli |
| information_schema |
| performance_schema |
+--------------------+
3 rows in set (0.00 sec)
問題なくmysql-ephemeral-secret.yaml
からアプリケーションに環境変数が引き渡されています。
Vault上でSecret
を作成し、Policy
&Role
を作成
二度手間になってしまいますが、同じSecret
をSecret Engine(先ほど作ったkv-v2 typeのsecret
という名称のそれ)に、Path名mysql/mysql-ephemeral-secret
として作成しておきます。以下の通りです。
前回同様の手順で同Secret
に対する参照権限Policy
を定義します。
path "secret/data/mysql/mysql-ephemeral-secret" {
capabilities = ["read"]
}
同じく前回同様の手順でRole
も作成してください。設定値は以下の通りです。
VSOから提供されるCustomResources
を作成。
さて、また前回同様に3つのCustomResource
を作成します。NameSpace: mysql-ephemeral
内に以下のYAMLをoc apply
していってください。中身がどうなっているのか、前回の内容を振り返りながら復習してみてください。
kind: VaultConnection
apiVersion: secrets.hashicorp.com/v1beta1
metadata:
name: vaultconnection-mysql-ephemeral
namespace: mysql-ephemeral
spec:
address: 'http://vault.vault.svc.cluster.local:8200'
kind: VaultAuth
apiVersion: secrets.hashicorp.com/v1beta1
metadata:
name: vaultauth-mysql-ephemeral
namespace: mysql-ephemeral
spec:
vaultConnectionRef: vaultconnection-mysql-ephemeral
method: kubernetes
mount: kubernetes
kubernetes:
role: mysql-ephemeral-role
serviceAccount: default
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: vaultstaticsecret-mysql-ephemeral
namespace: mysql-ephemeral
spec:
hmacSecretData: true
rolloutRestartTargets: #mysql-ephemeral-secretが更新された場合のRollout対象のResourceを設定します。
- name: mysql-ephemeral
kind: Deployment
destination:
create: true
overwrite: true
name: mysql-ephemeral-secret
vaultAuthRef: vaultauth-mysql-ephemeral
mount: secret
type: kv-v2
path: mysql/mysql-ephemeral-secret
refreshAfter: 5s
VaultStaticSecret
に前回は無かった行が増えていますね。spec.rolloutRestartTargets
にRollout
対象のResource
を設定する事ができます。なおkind
はDepoyment
, DemonSet
, StatefulSet
の3種類が選択可能です。
VaultStaticSecret
作成画面の「フォームビュー」でGUIからもrolloutRestartTargets
は設定可能です。
全てのYAMLをapply
すると、Vault上とNameSpace: mysql-ephemeral
のSecret
が同期されます。
Vault上のSecret
を変更してRollout
がかかるか試す
Vault上で作成したmysql-ephemeral-secret
をお好きな様に変更してみてください。
「Save」をクリックすると、直ぐ様NameSpace: mysql-ephemeral
のSecret
も更新されます。ここまでは前回と全く同じ挙動です。
OpenShiftのConsole画面で、開発者向け表示「トポロジー」メニューに切り替え、Deployment: mysql-ephemeral
の詳細画面で「イベント」タブを見てみます。何やら自動的にPodが再作成されています。つまりRollout
したということです!
再作成されたPod
の詳細画面で「ターミナル」タブから、先ほどと同様にoc exec
して、user情報やDatabase情報の変更が反映されているか見てみます。
sh-5.1$ mysql -uneongenesis -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| color |
| information_schema |
| performance_schema |
+--------------------+
3 rows in set (0.00 sec)
rolloutRestartTargets
の設定を行うことで、Vault上のSecret
の更新がVaultStaticSecret
を介して、OpenShiftにDeploy
されたアプリケーションに自動的に反映されることが確認できました。
おわりに
VaultSecretOperatorを使ってSingle "Secret" of Truthが簡単に実現できる事がわかりました。今回はROSA(AWSのManaged ServiceとしてのOpenShift)で試しましたが、VaultはOpenShift Cluster内で動かすことができますので、同じことはオンプレミスのOpenShiftでも可能です。GitOpsを推進する上でぶち当たる「Secretどうする問題」に対する、ひとつの解決策としてVaultはとても役に立ちそうです。