zaki work log

作業ログやら生活ログやらなんやら

[RHEL / Ansible] 最小構成でセットアップしたRHELにAAPをオフラインインストールするときの諸注意(備忘録)

注意!
現時点で、開発者アカウントの場合はサブスクリプション登録に必要なマニフェストのzipファイル作成がポータルでできなさげ(作成権限がない)。
(なので開発者アカウントの場合はオンラインでやりましょう)

商用アカウントなどでカスタマポータルへログインし、サブスクリプション登録割り当ての画面で「新規サブスクリプションの割り当て」のボタンがあれば大丈夫。のはず。

ちなみにAnsible Towerのドキュメントもサブスクリプション周りは同じなので、手順は同様のはず。

構成

  • RHEL: 8.6
  • AAP: 2.2.0-7

RHEL自体のオフラインインストールについては以下参照で、この記事通り、マウントしたDVDイメージのYumリポジトリがある前提。

zaki-hmkc.hatenablog.com

インストール

インストーラ

インストーラは、必要なパッケージが同梱されているBundle版(Ansible Automation Platform 2.2.0 Setup Bundle)を入手する。

インベントリ

インストール対象ホスト、AAPのadminパスワード、PostgreSQLのパスワードまでは通常通りで、オフライン用の設定としてregistry_urlを空に設定する。

access.redhat.com

ざっくりこんな感じ。(localインストールの場合)

--- inventory.org       2022-06-30 02:56:29.000000000 +0900
+++ inventory   2022-08-03 22:12:40.864176120 +0900
@@ -9,6 +9,7 @@
 # hybrid.example  node_type=hybrid
 # hybrid2.example <- this will default to hybrid
 [automationcontroller]
+rhel8-restricted  ansible_connection=local
 
 [automationcontroller:vars]
 peers=execution_nodes
@@ -39,20 +40,20 @@
 [sso]
 
 [all:vars]
-admin_password=''
+admin_password='p@ssword'
 
 pg_host=''
 pg_port=5432
 
 pg_database='awx'
 pg_username='awx'
-pg_password=''
+pg_password='p@ssword'
 pg_sslmode='prefer'  # set to 'verify-full' for client-side enforced SSL
 
 # Execution Environment Configuration
 # Credentials for container registry to pull execution environment images from,
 # registry_username and registry_password are required for registry.redhat.io
-registry_url='registry.redhat.io'
+registry_url=''
 registry_username=''
 registry_password=''

追加パッケージ

あとは普通にインストール…と行きたいところだけど、最小構成RHELの場合、依存するパッケージがBundle版でもちょっと足りず、そのままsetup.shを実行すると以下のエラーで失敗する。

TASK [ansible.automation_platform_installer.nginx : Open up permissions on nginx.] ***
An exception occurred during task execution. To see the full traceback, use -vvv. The error was: ModuleNotFoundError: No module named 'semanage'
failed: [rhel8-restricted] (item=httpd_can_network_connect) => {"ansible_loop_var": "item", "changed": false, "item": "httpd_can_network_connect", "msg": "Failed to import the required Python library (libsemanage-python) on rhel8-restricted's Python /usr/libexec/platform-python. Please read the module documentation and install it in the appropriate location. If the required library is installed, but Ansible is using the wrong Python interpreter, please consult the documentation on ansible_python_interpreter"}

semanageが無いので以下のコマンドでインストールしておく。(インストーラで入れて欲しい気もするけど…)

sudo dnf install policycoreutils-python-utils

※ 以下はpolicycoreutils-python-utilsをインストールしない(不要な)場合

RHELが最小構成でなくpolicycoreutils-python-utilsが既にインストール済みの場合でも、dnfでパッケージインストールを一度も実行しておらずGPGキー未インポート状態だとなぜか(?)www.redhat.comリモートホストへの接続を試みようとして失敗するので、GPGキーのインポートを行っておく。

Curl error (28): Timeout was reached for https://www.redhat.com/security/data/fd431d51.txt [Connection timed out after 30000 milliseconds]
./setup.sh: 行 376: ansible-playbook: コマンドが見つかりません
[error] Oops!  An error occurred while running setup.
[warn] /var/log/tower does not exist. Setup log saved to setup.log.

GPGキーのインポートを行えばこのエラーは回避できた。(試行錯誤の結果なので詳細不明…)

sudo rpm --import /mnt/cdrom/RPM-GPG-KEY-redhat-release

もしくは、適当なパッケージをインストールする。

インストール

あとは普通に。

$ time sudo ./setup.sh 

サブスクリプション登録

インストール完了後のサブスクリプション登録は、オフラインだとRed Hatアカウントを入力した登録ができない。かわりに、サブスクリプションマニフェストをカスタマポータルで別途作成し、取得したマニフェスト(zipファイル)を登録することでサブスクリプション登録を行う。

マニフェストファイルの作成

access.redhat.com

カスタマポータルの「サブスクリプション割り当て」から。
これが開発者アカウントだと、以下の通り割り当て操作ができないようになっている。

商用アカウントだとこの画面から「新規サブスクリプションの割り当て」ボタンが表示されるので、以下のドキュメント通り操作すればOK

名前にシステムの名称などを入力、TypeにSatellite 6.8を選択、サブスクリプションへAAPの資格情報(Entitlements)を追加すれば、Detailsタブでマニフェストのエクスポートするボタンが表示されるので、これを押下してzipファイルをダウンロードする。

マニフェストのアップロード

取得したzipファイルをAAPのサブスクリプション画面でアップロードすれば完了。
(情報収集系のオプションはオフラインなので無効にしておく)

Kubernetes v1.24でServiceAccountのトークンを生成・取得する

ここのところKubernetes関連の情報をキャッチアップできておらず、今月作ったk8sクラスタで期待する動作をせずにプチハマりしたので、トークン取得に関連した確認した内容の備忘録。

qiita.com

LegacyServiceAccountTokenNoAutoGeneration feature gateがベータとなり、デフォルトで有効化されます。有効化されるとService account tokenを含むSecret APIオブジェクトは自動生成されません。service account tokenを取得するためのTokenRequest APIを利用するか、もし期限切れを起こさないトークンが必要な場合は、このガイドにしたがってService account tokenを追加するためのtoken controllerのためのSecret APIオブジェクトを作成してください。(#108309, @zshihang)

https://qiita.com/uesyn/items/90ca3789ce88cb9ea7a4

speakerdeck.com

[KEP 2799][beta] Secret-based ServiceAccount トークンの⾃動⽣成の停⽌

Feature gate: LegacyServiceAccountTokenNoAutoGeneration, デフォルト有効

  • これまで ServiceAccount を作成すると⾃動的に⽣成されていたトークンを含む Secret オブジェクトが作成されなくなる
  • 有効期限のないトークンが必要な場合は Token Request API を使⽤してBound ServiceAccount トークンを⽣成するか、いくつかのステップを踏んで Secret-based トークンを⽣成できる
  • Bound ServiceAccount Token を⽣成するのに直接 Token Request API を使うのは⾯倒なので 1.24 で kubectl にトークンを⽣成を作成するコマンドが追加された
  • Bound ServiceAccount Token の詳細はBound Service Account Tokenとは何か - Qiita を参照。

https://speakerdeck.com/superbrothers/du-duan-topian-jian-dexuan-nda-kubernetes-1-dot-24-falsezhu-mu-ji-neng-tojin-hou?slide=9

k8s v1.24ではトークンのSecretが自動で生成されなくなった

上で引用した通りで、デフォルトの動作が変化しました。
v1.24における実際の動作は以下の通り。

[zaki@cloud-dev2 kind]$ kubectl get node
NAME                 STATUS   ROLES           AGE    VERSION
kind-control-plane   Ready    control-plane   4m7s   v1.24.0

Namespaceの作成。

[zaki@cloud-dev2 kind]$ kubectl create ns zzz
namespace/zzz created
[zaki@cloud-dev2 kind]$ kubectl get secret -n zzz
No resources found in zzz namespace.

ServiceAccountの作成。

[zaki@cloud-dev2 kind]$ kubectl create sa sauser -n zzz
serviceaccount/sauser created
[zaki@cloud-dev2 kind]$ kubectl get secret -n zzz
No resources found in zzz namespace.
[zaki@cloud-dev2 kind]$ kubectl get sa -n zzz
NAME      SECRETS   AGE
default   0         30s
sauser    0         6s

このように、NamespaceやServiceAccountを作成しても、トークンのSecretリソースが生成されない動作になっている。

ServiceAccountのトークンを取得する

kubectl create token コマンド使用

トークンを生成するためのTokenRequest APIを扱うコマンドがあるので、それを実行すれば対象ServiceAccount用のトークンを生成できる。

まずは検証用のServiceAccountとRoleBindingを生成。
使用したマニフェストファイルはこちら

[zaki@cloud-dev2 tmp]$ kubectl apply -f rbac-sample.yml 
namespace/zzz created
serviceaccount/sauser created
role.rbac.authorization.k8s.io/sample-role created
rolebinding.rbac.authorization.k8s.io/sample-rolebinding created
[zaki@cloud-dev2 tmp]$ kubectl get secret -n zzz
No resources found in zzz namespace.

従来バージョンであればこの時点でトークンのSecretが生成されるけど、v1.24では生成されないので、次のコマンドを実行してトークンを取得する。

kubectl create token <ServiceAccountのユーザー名>

実行結果が以下。

[zaki@cloud-dev2 tmp]$ kubectl create token sauser -n zzz
eyJhbGciOiJSUzI1NiIsImtpZCI6IlpEY3dZVW82dURkRU9QckFEQTdEV2RwYTVOZ1NXSUFBTzByYllHM3ZLcDgifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNjU4NzYyMjUwLCJpYXQiOjE2NTg3NTg2NTAsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJ6enoiLCJzZXJ2aWNlYWNjb3VudCI6eyJuYW1lIjoic2F1c2VyIiwidWlkIjoiNmU5NmNhZmYtNGYxZS00NjY1LWI1YTYtZTg2ODllMjFlZmY3In19LCJuYmYiOjE2NTg3NTg2NTAsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDp6eno6c2F1c2VyIn0.tJzQKH2ll_TsN6iFWAP4vL-HZ7nCwUFa1qqeFunceH6i-FQHPcFQOnNlL5wdMoyMetavdiHcJWNsA21wQfDaRIajUBTiA-GnO7LnNcfstJ6Z6acnLzLLz00oS4MvFxPw7-VDEUJvBkLjNrEXnMMzAtL3i1Q1tRsYxw4btgtla0VkS-AMuZAPwzGbUd3Hf8eJl4ZLx7a2bGA-dFZ0gkNMUr2fj3p4NxyqWFkWQcPzWFPCQPRQyx3Q7wLtFu7sjQryNHx1guXosr-BwMWPwxW2PgbaEXZtiiwGRc9C3vUOoxpOcFdmrhwYjH424PEyLXVKtqZqseApX5C6iCtvxCp2fw

取れてますね。実行のたびにトークンは再作成される。

このトークンを使ってRESTを叩くサンプルは以下。

[zaki@cloud-dev2 tmp]$ TOKEN=$(kubectl create token sauser -n zzz)
[zaki@cloud-dev2 tmp]$ curl -k -H "Authorization: Bearer ${TOKEN}" https://127.0.0.1:38253/api/v1/namespaces/zzz/pods
{
  "kind": "PodList",
  "apiVersion": "v1",
  "metadata": {
    "resourceVersion": "7087"
  },
  "items": []
}

Podが何も無くて分かりづらいので、サンプルをデプロイして確認。

[zaki@cloud-dev2 tmp]$ kubectl apply -f https://raw.githubusercontent.com/zaki-lknr/k8s-samples/master/sample-web/httpd-nodeport/sample-http.yaml -n zzz
deployment.apps/sample-http created
service/sample-http created
[zaki@cloud-dev2 tmp]$ kubectl get pod -n zzz
NAME                           READY   STATUS    RESTARTS   AGE
sample-http-776d7585c9-4l4xt   1/1     Running   0          46s
sample-http-776d7585c9-kmb96   1/1     Running   0          46s

こんな感じ。

[zaki@cloud-dev2 tmp]$ curl -k -H "Authorization: Bearer ${TOKEN}" https://127.0.0.1:38253/api/v1/namespaces/zzz/pods
{
  "kind": "PodList",
  "apiVersion": "v1",
  "metadata": {
    "resourceVersion": "7254"
  },
  "items": [
    {
      "metadata": {
        "name": "sample-http-776d7585c9-4l4xt",
        "generateName": "sample-http-776d7585c9-",
        "namespace": "zzz",
        "uid": "ed3004ef-d897-433c-9d62-9b2916e16626",
        "resourceVersion": "7241",
        "creationTimestamp": "2022-07-25T14:23:00Z",
        "labels": {
          "app": "sample-http",
          "pod-template-hash": "776d7585c9"
        },

...

この通り、Podの情報を取得できている。

トークンの期限

kubectl create tokenコマンドを使うと(従来のServiceAccountのSecretトークンと異なり)期限が存在し、期限が切れると無効になる。
ドキュメントを見ても期限のデフォルトがよくわからず。。。

helpを見る限りでは、期限は--durationオプションで制御できそう。

[zaki@cloud-dev2 ~]$ kubectl create token --help
:
    --duration=0s:
        Requested lifetime of the issued token. The server may return a token with a longer or
        shorter lifetime.
:

0s(0秒)を指定すると無期限になりそうに見えるけど、実は(少なくとも手元の環境では)そんな事はなさそうで、コマンドの実行時のRESTのやり取りを-vオプションでログレベルを上げて確認すると以下の通り。

[zaki@cloud-dev2 ~]$ kubectl create token sauser -n zzz --duration=0s -v=8
I0726 22:25:32.205677  433932 loader.go:372] Config loaded from file:  /home/zaki/.kube/config
I0726 22:25:32.206785  433932 request.go:1073] Request Body: {"kind":"TokenRequest","apiVersion":"authentication.k8s.io/v1","metadata":{"creationTimestamp":null},"spec":{"audiences":null,"expirationSeconds":null,"boundObjectRef":null},"status":{"token":"","expirationTimestamp":null}}
:
:
I0726 22:25:32.218442  433932 request.go:1073] Response Body: {"kind":"TokenRequest","apiVersion":"authentication.k8s.io/v1","metadata":{"name":"sauser","namespace":"zzz","creationTimestamp":"2022-07-26T13:25:32Z","managedFields":[{"manager":"kubectl","operation":"Update","apiVersion":"authentication.k8s.io/v1","time":"2022-07-26T13:25:32Z","fieldsType":"FieldsV1","fieldsV1":{"f:spec":{"f:expirationSeconds":{}}},"subresource":"token"}]},"spec":{"audiences":["https://kubernetes.default.svc.cluster.local"],"expirationSeconds":3600,"boundObjectRef":null},"status":{"token":"eyJhbGciOi ...

この通り、0s指定時は"expirationSeconds":3600というレスポンスになっており、1時間の期限になってるように見える。

ちなみにこれは、--duration未指定時の場合と動作が同じ。

[zaki@cloud-dev2 ~]$ kubectl create token sauser -n zzz -v=8
I0726 22:27:22.681313  434460 loader.go:372] Config loaded from file:  /home/zaki/.kube/config
I0726 22:27:22.682016  434460 request.go:1073] Request Body: {"kind":"TokenRequest","apiVersion":"authentication.k8s.io/v1","metadata":{"creationTimestamp":null},"spec":{"audiences":null,"expirationSeconds":null,"boundObjectRef":null},"status":{"token":"","expirationTimestamp":null}}
:
:
I0726 22:27:22.692562  434460 request.go:1073] Response Body: {"kind":"TokenRequest","apiVersion":"authentication.k8s.io/v1","metadata":{"name":"sauser","namespace":"zzz","creationTimestamp":"2022-07-26T13:27:22Z","managedFields":[{"manager":"kubectl","operation":"Update","apiVersion":"authentication.k8s.io/v1","time":"2022-07-26T13:27:22Z","fieldsType":"FieldsV1","fieldsV1":{"f:spec":{"f:expirationSeconds":{}}},"subresource":"token"}]},"spec":{"audiences":["https://kubernetes.default.svc.cluster.local"],"expirationSeconds":3600,"boundObjectRef":null},"status":{"token":"eyJhbGciOi ...

この場合も、レスポンスは"expirationSeconds":3600となっている。 もしかするとリクエストに0sが渡っておらずnullになっているので、コマンド側に不備がある可能性もあるけど、現状0s指定は効力がなさそう。

API仕様側を確認する限りでは、expirationSecondsに指定する期限(秒)の値はint64とある

expirationSeconds (int64)

ExpirationSeconds is the requested duration of validity of the request. The token issuer may return a token with a different validity duration so a client needs to check the 'expiration' field in a response.

なんだけど、実際に動かすと以下の通りunsigned int32っぽい…

[zaki@cloud-dev2 ~]$ kubectl create token sauser -n zzz --duration=4294967297s -v=8
:
:
error: failed to create token: TokenRequest.authentication.k8s.io "" is invalid: spec.expirationSeconds: Invalid value: 4294967297: may not specify a duration larger than 2^32 seconds

少なくとも手元の環境でMAXは4294967296秒(ざっくり136年)

[zaki@cloud-dev2 ~]$ kubectl create token sauser -n zzz --duration=4294967296s -v=8
:
:
I0726 22:37:35.590377  438330 request.go:1073] Response Body: {"kind":"TokenRequest","apiVersion":"authentication.k8s.io/v1","metadata":{"name":"sauser","namespace":"zzz","creationTimestamp":"2022-07-26T13:37:35Z","managedFields":[{"manager":"kubectl","operation":"Update","apiVersion":"authentication.k8s.io/v1","time":"2022-07-26T13:37:35Z","fieldsType":"FieldsV1","fieldsV1":{"f:spec":{"f:expirationSeconds":{}}},"subresource":"token"}]},"spec":{"audiences":["https://kubernetes.default.svc.cluster.local"],"expirationSeconds":4294967296,"boundObjectRef":null},"status":{"token":"eyJhbGciOiJSU ...

期限があるとはいえ、この値であれば期限が切れるのは自分が死んだ後なので知ったこっちゃねーや実質無期限として使用できそう。

Secretベースのトークンを作成する

kubectl create tokenコマンドの実行でなく、従来と同じようにトークンを保持するSecretリソースを作成することもできる。
ServiceAccount名を指定したアノテーションを含むSecretリソースを明示的に作成することで、トークンが含まれる内容のSecretリソースが生成される。

kubernetes.io

ServiceAccount名がsauserの場合、具体的なマニフェストは以下の通り。

---
apiVersion: v1
kind: Secret
metadata:
  name: sa-token-sample
  namespace: zzz
  annotations:
    kubernetes.io/service-account.name: "sauser"
type: kubernetes.io/service-account-token

以下のアノテーションで、sauserというServiceAccount名を指定することで、sa-token-sampleという名前でSecretリソースが生成される。

  annotations:
    kubernetes.io/service-account.name: "sauser"  # <- ここ
[zaki@cloud-dev2 ~]$ kubectl apply -f tmp/rbac-sample.yml 
secret/sa-token-sample created
[zaki@cloud-dev2 ~]$ kubectl get secret -n zzz
NAME              TYPE                                  DATA   AGE
sa-token-sample   kubernetes.io/service-account-token   3      2s

内容は以下の通り。

[zaki@cloud-dev2 ~]$ kubectl get secret -n zzz sa-token-sample -o yaml
apiVersion: v1
data:
  ca.crt: LS0tLS1CRUd ...
  namespace: enp6
  token: ZXlKaGJHY2lPaU ...
kind: Secret
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"v1","kind":"Secret","metadata":{"annotations":{"kubernetes.io/service-account.name":"sauser"},"name":"sa-token-sample","namespace":"zzz"},"type":"kubernetes.io/service-account-token"}
    kubernetes.io/service-account.name: sauser
    kubernetes.io/service-account.uid: e373e307-33c8-4af9-b9b7-9cb09b681126
  creationTimestamp: "2022-07-26T14:16:26Z"
  name: sa-token-sample
  namespace: zzz
  resourceVersion: "119765"
  uid: 678149ac-1a46-41b2-badd-3a68d7921d18
type: kubernetes.io/service-account-token

このトークンであれば従来と同じく期限がない(とドキュメントには書かれてる)ので、永続的に使用可能。

v1.24のSecretトークンが従来と動作が違う点

以下は手元の環境で試したらこうだった、という話。
仕様なのか環境固有なのかまでは確認してない。

削除時に自動再生成されない

旧バージョンの自動で生成されるSecretのトークンは、Secretリソースを削除すると自動で再生成される。

ubuntu@oci-g-a1-ubuntu:~$ kubectl get node
NAME              STATUS   ROLES                  AGE    VERSION
oci-g-a1-ubuntu   Ready    control-plane,master   129d   v1.22.7+k3s1
ubuntu@oci-g-a1-ubuntu:~$ kubectl get secret -n rbac-sample
NAME                         TYPE                                  DATA   AGE
default-token-twg8z          kubernetes.io/service-account-token   3      24s
sample-account-token-dkubectl89   kubernetes.io/service-account-token   3      24s
ubuntu@oci-g-a1-ubuntu:~$ kubectl delete secret -n rbac-sample sample-account-token-dkubectl89  # ここでトークンのSecretを削除
secret "sample-account-token-dkubectl89" deleted
ubuntu@oci-g-a1-ubuntu:~$ kubectl get secret -n rbac-sample
NAME                         TYPE                                  DATA   AGE
default-token-twg8z          kubernetes.io/service-account-token   3      30s
sample-account-token-fkt6g   kubernetes.io/service-account-token   3      2s      # 新しいトークンのSecretが生成されている

これに対して、v1.24でアノテーションを使って生成したトークンのSecretリソースは、削除しても自動で再生成はされない。

[zaki@cloud-dev2 ~]$ kubectl delete secret -n zzz sa-token-sample
secret "sa-token-sample" deleted
[zaki@cloud-dev2 ~]$ kubectl get secret -n zzz 
No resources found in zzz namespace.                    # 削除すると消えたまま

Secretリソース再生成時にトークンがリフレッシュされない

上記の再生成時の動作として、従来バージョンの場合はトークンの内容がリフレッシュされる。 (むしろ、トークン漏洩などで無効化したい場合に、Secretリソースを削除して入れ替える、みたいな使い方…だったはず)

ubuntu@oci-g-a1-ubuntu:~$ kubectl get secret -n rbac-sample sample-account-token-fkt6g -o jsonpath='{.data.token}' | sha256sum 
e980d99fb8ed5b95dd58bbd4913825ef6f8e44e93f5392f3b334f68bbbed455f  -                 # 削除前のトークンのハッシュ値 (比較確認用)
ubuntu@oci-g-a1-ubuntu:~$ kubectl delete secret -n rbac-sample sample-account-token-fkt6g
secret "sample-account-token-fkt6g" deleted
ubuntu@oci-g-a1-ubuntu:~$ kubectl get secret -n rbac-sample
NAME                         TYPE                                  DATA   AGE
default-token-twg8z          kubernetes.io/service-account-token   3      4m13s
sample-account-token-dql7l   kubernetes.io/service-account-token   3      7s
ubuntu@oci-g-a1-ubuntu:~$ kubectl get secret -n rbac-sample sample-account-token-dql7l -o jsonpath='{.data.token}' | sha256sum 
33b60565e646431fef1dbf1d46f95f86d2278bc9a21ea3fd363472d6901a47db  -                 # 削除前のトークンのハッシュ値と異なっている

これに対してv1.24の場合、トークンのSecretリソースの削除前と削除後の再生成で、トークンの内容は変化しなかった。

[zaki@cloud-dev2 ~]$ kubectl get secret -n zzz sa-token-sample -o jsonpath='{.data.token}' | sha256sum
5d4e5cf27eb93e7843c49c1df1ac6e2dc08a810b3efa0980006792f01010f04b  -                 # 削除前のトークンのハッシュ値と異なっている
[zaki@cloud-dev2 ~]$ kubectl delete secret -n zzz sa-token-sample
secret "sa-token-sample" deleted
[zaki@cloud-dev2 ~]$ kubectl get secret -n zzz 
No resources found in zzz namespace.
[zaki@cloud-dev2 ~]$ 
[zaki@cloud-dev2 ~]$ 
[zaki@cloud-dev2 ~]$ kubectl apply -f tmp/rbac-sample.yml 
secret/sa-token-sample created
[zaki@cloud-dev2 ~]$ kubectl get secret -n zzz sa-token-sample -o jsonpath='{.data.token}' | sha256sum
5d4e5cf27eb93e7843c49c1df1ac6e2dc08a810b3efa0980006792f01010f04b  -                 # 削除前のトークンのハッシュ値と同一
[zaki@cloud-dev2 ~]$ 

この通り、トークンの内容(のハッシュ値)が変化せず、削除前の値を保持している。 トークンをリフレッシュしたい場合は、ServiceAccountから再生成すると変化し、削除したトークンは無効になる。

まずはServiceAccountを削除(ServiceAccountを削除すると、紐づいてるトークンのSecretも自動で削除される)

[zaki@cloud-dev2 ~]$ kubectl get sa -n zzz
NAME      SECRETS   AGE
default   0         9h
sauser    0         9h
[zaki@cloud-dev2 ~]$ kubectl get secret -n zzz
NAME              TYPE                                  DATA   AGE
sa-token-sample   kubernetes.io/service-account-token   4      2m33s
[zaki@cloud-dev2 ~]$ kubectl delete sa -n zzz sauser 
serviceaccount "sauser" deleted
[zaki@cloud-dev2 ~]$ kubectl get secret -n zzz
No resources found in zzz namespace.                   # Secretは明示的に削除してないが、ServiceAccount削除に連動して消える

ServiceAccountとSecretを再作成

[zaki@cloud-dev2 ~]$ kubectl apply -f tmp/rbac-sample.yml 
serviceaccount/sauser created
secret/sa-token-sample created
[zaki@cloud-dev2 ~]$ kubectl get secret -n zzz
NAME              TYPE                                  DATA   AGE
sa-token-sample   kubernetes.io/service-account-token   3      4s

これでトークンの内容が変化する。

[zaki@cloud-dev2 ~]$ kubectl get secret -n zzz sa-token-sample -o jsonpath='{.data.token}' | sha256sum
ed4c28d94720340f07ba7c041c221fce780a7a047a2148640062ca9b44bfa551  -                 # 削除前のトークンのハッシュ値(5d4e5cf2...)と異なっている

環境

確認した環境は以下の通り。

v1.24 (kindで構築)

[zaki@cloud-dev2 ~]$ kind --version
kind version 0.14.0
[zaki@cloud-dev2 ~]$ cat /etc/redhat-release 
Fedora release 35 (Thirty Five)
[zaki@cloud-dev2 ~]$ kubectl version --short
Flag --short has been deprecated, and will be removed in the future. The --short output will become the default.
Client Version: v1.24.3
Kustomize Version: v4.5.4
Server Version: v1.24.0

v1.22 (k3sで構築)

ubuntu@oci-g-a1-ubuntu:~$ k3s --version
k3s version v1.22.7+k3s1 (8432d7f2)
go version go1.16.10
ubuntu@oci-g-a1-ubuntu:~$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=20.04
DISTRIB_CODENAME=focal
DISTRIB_DESCRIPTION="Ubuntu 20.04.4 LTS"
ubuntu@oci-g-a1-ubuntu:~$ kubectl version --short
Client Version: v1.22.7+k3s1
Server Version: v1.22.7+k3s1

トークンを使って何をするかの例はこちら。

zaki-hmkc.hatenablog.com

WSLでVM環境のコピー(exportとimport)

WSLにはVMの内容をtar形式のアーカイブファイルへエクスポートする機能がある。
これでVMのバックアップやスナップショット的な状態保存、別名でインポートすることで同じディストリビューションVMを複数作ったりできる。
Dockerでいうとexport/importに相当。

VM環境の保存(エクスポート)

tar形式にエクスポートするには--exportオプションを使う。
Ubuntu-20.04」ディストリビューションUbuntu-20.04.tarファイルへエクスポートするには以下。

PS C:\Users\zaki> wsl --export Ubuntu-20.04 Ubuntu-20.04.tar

tarファイルのインポート

エクスポートして保存したtarファイルは、--importオプションを使ってインポートできる。 名前(ディストリビューション名)をエクスポート元のディストリビューションと異なるものを指定することで、環境のコピーとなる。

書式は wsl --import ディストリビューション名 インストール先ファイルパス インポートするtarファイル

PS C:\Users\zaki> wsl --import ubuntu-20.04-container .\work\wsl\ubuntu-20.04-container .\Ubuntu-20.04.tar

インストール先ファイルパスには、インポートしたあとのvhdxファイルの保存場所を指定する。

デフォルトユーザーの変更

tarファイルをインポートしただけの状態だと、シェルを起動するとなぜかrootユーザーになる(もともと作成済みのユーザーへsuで切り替えは可能)。
ちょっと不便なのでデフォルトユーザーを以下PowerShellで更新する。

PS C:\Users\zaki> Function WSL-SetDefaultUser ($distro, $user) { Get-ItemProperty Registry::HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Lxss\*\ DistributionName | Where-Object -Property DistributionName -eq $distro | Set-ItemProperty -Name DefaultUid -Value ((wsl -d $distro -u $user -e id -u) | Out-String); };
PS C:\Users\zaki> WSL-SetDefaultUser ubuntu-20.04-container zaki

やっていることはレジストリ値の更新で、指定ディストリビューションDefaultUidの値を指定のユーザーからuidを取得して設定している。
元ネタはこちら

github.com

一つ目のコマンドで更新用関数を定義し、二つ目のコマンドで、第1引数に対象ディストリビューション名、第2引数にデフォルトにするユーザー名を指定する。

VMの削除

削除するにはディストリビューション名を引数に--unregisterオプションを使う。

PS C:\Users\zaki> wsl --unregister ubuntu-20.04-container
登録を解除しています...

これで気軽に環境のコピーを作れるので、コンテナほどサイズは小さくはないけど、サービス用・開発用・ステージング用などいろいろ試して壊す環境を気軽に用意できる。

オフライン環境へのRHEL8.6インストールとサブスクリプション割り当て

ちょっと構築する機会があったのでメモ。

OSのインストール

インストール用のisoファイルを入手し、これは普通に。
メディアから起動し、OSのインストールを行う。
お試しとして、今回は最小構成設定のインストール。

今回の環境は、ESXi上のVMで、CPUは2コア・メモリ8GBで、閉塞ネットワークには繋がっている状態。
(インターネットへの接続は無し)

サブスクリプション割り当て

オフライン環境のサブスクリプション割り当ての手順はこちら。

access.redhat.com

大まかな流れは、以下の通り。

  1. カスタマポータル上で手動でシステムを登録
  2. 証明書をダウンロード
  3. ダウンロードした証明書をオフラインRHELsubscription-manager で登録

カスタマポータル上でシステム登録

インターネット接続できるPCのブラウザでカスタマポータルへアクセスし、システムの画面で、「新規作成」を押下。

システム情報の入力画面になるので、ホスト名やCPU数などの項目を入力。
今回はVMなので仮想システムを選択。

システムを作成すると、サブスクリプションのアタッチボタンが表示されるので押下。

割り当て可能なサブスクリプションがチェックできるようになるので、使用するサブスクリプションをチェックして「サブスクリプションのアタッチ」を押下

証明書をダウンロードできる画面に遷移するので、「証明書のダウンロード」を押下。

これで証明書ファイル(pem形式)を含むzipファイルがダウンロードできる。
このzipファイルまたは展開したpemファイルをサブスクリプション登録したいRHELへ転送する。

証明書ファイルを使ったサブスクリプション登録

ファイルシステム上にダウンロードしたpemファイルを配置し、subscription-manager import --certificate コマンドを使ってインポートする。
複数のサブスクリプション割り当ての場合は、ファイル数分実行すればOK

[root@rhel8-restricted ~]# subscription-manager import --certificate /tmp/4199243939243262298.pem 
証明書 4199243939243262298.pem は正常にインポートされました
[root@rhel8-restricted ~]# subscription-manager import --certificate /tmp/8034611573323406627.pem 
証明書 8034611573323406627.pem は正常にインポートされました
[root@rhel8-restricted ~]# 

Yumリポジトリ設定

デフォルトのリポジトリの無効化

オフライン環境でセットアップしたRHELでも、デフォルトでは https://cdn.redhat.com/リポジトリを見に行くように設定されているので、これを無効にする。

[root@rhel8-restricted etc]# dnf repolist
サブスクリプション管理リポジトリーを更新しています。
コンシューマー識別子を読み込めません
repo id                                        repo の名前
rhel-8-for-x86_64-appstream-rpms               Red Hat Enterprise Linux 8 for x86_64 - AppStream (RPMs)
rhel-8-for-x86_64-baseos-rpms                  Red Hat Enterprise Linux 8 for x86_64 - BaseOS (RPMs)
[root@rhel8-restricted etc]# dnf config-manager --disable rhel-8-for-x86_64-appstream-rpms rhel-8-for-x86_64-baseos-rpms
サブスクリプション管理リポジトリーを更新しています。
コンシューマー識別子を読み込めません

ISOイメージ内のパッケージファイルをYumリポジトリとして利用する

access.redhat.com

マウント

まずはDVDのマウント。
ESXiの場合で、ホストの設定としてデータストアのISOファイルを使用する設定にしてる場合であれば、/etc/fstab に以下設定を追加しておき、mount -amount /mnt/cdrom でメディアをマウントする。

/dev/cdrom              /mnt/cdrom           iso9660    loop           0 0
[root@rhel8-restricted ~]# ls -F /mnt/cdrom/
AppStream/  EULA                     RPM-GPG-KEY-redhat-release  images/
BaseOS/     GPL                      TRANS.TBL                   isolinux/
EFI/        RPM-GPG-KEY-redhat-beta  extra_files.json            media.repo

ISOファイルがファイルシステム上にある場合は、/dev/cdrom のところをファイルシステム上のパスに置き換えればOK

/path/to/rhel-8.6-x86_64-dvd.iso /mnt/tmp iso9660 loop 0 0 

mountコマンドを使う場合は以下。

# mount -o loop /path/to/rhel-8.6-x86_64-dvd.iso /mnt/tmp

リポジトリ設定

ファイルを確認できたら、Yumリポジトリ設定を行う。
ファイル名は任意なので、サンプルとして /etc/yum.repos.d/local.repo ファイルを作成。内容は以下の通り。
これはマウントポイントが /mnt/cdrom の場合。異なる場合は baseurl= の行の設定を環境に合わせる。

[dvd-BaseOS]
name=DVD for RHEL - BaseOS
baseurl=file:///mnt/cdrom/BaseOS
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release

[dvd-AppStream]
name=DVD for RHEL - AppStream
baseurl=file:///mnt/cdrom/AppStream
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release

最後にキャッシュをクリアする。

[root@rhel8-restricted ~]# yum clean all
サブスクリプション管理リポジトリーを更新しています。
コンシューマー識別子を読み込めません
13 ファイルが削除されました

これでDVD内のパッケージをYumリポジトリとして参照できるようになるので、追加のパッケージインストールなどがあればdnf installが可能になる。

[root@rhel8-restricted ~]# dnf repolist
サブスクリプション管理リポジトリーを更新しています。
コンシューマー識別子を読み込めません
repo id                              repo の名前
dvd-AppStream                        DVD for RHEL - AppStream
dvd-BaseOS                           DVD for RHEL - BaseOS

その他、制限環境で使える参考情報

zaki-hmkc.hatenablog.com

zaki-hmkc.hatenablog.com

zaki-hmkc.hatenablog.com

Docker版GuacamoleでSSH接続の日本語対応 (Debianイメージへの日本語フォント追加)

前夜まとめたDockerコンテナでデプロイした素のGuacamoleは、SSH接続しても日本語の表示や入力が文字化けする。

zaki-hmkc.hatenablog.com

今回はこの対応。

といっても先に結論を書くと、guacdコンテナに日本語フォントを追加すればOK

日本語フォント入りイメージビルド

今回は(個人的に見慣れたフォントということで)VLゴシックフォントをapt-getで追加。
fonts-takao-gothicもあるみたい。

ビルドに使うDockerfileは以下の通り、フォントをインストールするのみ。
実行権限がrootじゃなくなってるので少しイジる。

FROM guacamole/guacd:1.4.0

USER 0
RUN apt-get update \
    && apt-get install -y fonts-vlgothic \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

USER 1000

ビルド

docker build . -t guacad:1.4.0-ja -f Dockerfile.guacd-ja-vlgothic

ここではイメージ名をguacad:1.4.0-jaにしている。Docekrfileのファイル名はDockerfile.guacd-ja-vlgothic

デプロイ

動作しているguacamole/guacdコンテナがある場合は、先に停止・削除する(guacamole/guacamoleとDBはそのままでOK)。
停止させたら、オリジナルの替わりにビルドした日本語フォント入りのコンテナをデプロイする。
コマンドは以下の通り。というかオリジナルと同じ。

docker run --name guacd -d --network guacamole-network guacd:1.4.0-ja 

これでGuacamoleのwebからSSHアクセスすると、日本語の入出力ができるようになる。


あとはCompose対応すれば構築は簡単にできるけど、DBの初期化ファイル作成、あの処理をComposeに入れられるんだろーか。。

参考

3. 接続方法 | Apache Guacamole利用マニュアルの「3.2.1. 日本語入力方法」

Apache Guacamoleを使ってwebブラウザでSSH接続をお試し (Docker版)

ネットワークの制限でwebアクセス(HTTP/HTTPS)しか許可されておらず、SSHアクセスが使えないような環境でリモートのLinuxサーバーのシェルへアクセスしたい場合、Apache Guacamoleを使うことでwebブラウザの画面でSSHログインを行う。(webページ内にSSHアクセスしたリモートサーバーのシェルが画面表示される)

接続イメージは以下の通り。

本エントリではとりあえずSSH接続のみ。
ただしGuacamoleはSSH以外にもVNCやRDPなどいくつかのリモート接続用のプロトコルに対応している。
Kubernetesもあるのが気になる。

Guacamoleのデプロイ

サーバーへ直接インストールする方式と、Docker Hubで公開されているDockerコンテナ版のGuacamoleを使う方法がある。 本エントリではDockerイメージを使うパターンで構築。

guacamole.apache.org

Guacamoleは、以下の3つのコンポーネント(説明は意訳)で動作し、それぞれコンテナを実行する。(DBだけ外部とかできるけど、とりあえず全部Dockerで)

ちなみにGuacamoleのコンテナイメージは、latestタグはデイリーで更新されているため、最新安定版のバージョンのタグを指定した方が良さそう(おそらくCIで自動ビルドされてるっぽい)。
現時点で1.4.0が最新。

docker networkの作成

公式ドキュメントだと--link使ってコンテナ間通信する手順になってるけどさすがにもうこのオプションを使うのも微妙なので、コンテナ間通信を行うためのDocekr Networkを作成。

docker network create guacamole-network

以降のコンテナデプロイは、このDocker Networkを--networkで指定。

guacdの起動

docker run --name guacd -d --network guacamole-network guacamole/guacd:1.4.0

MySQL

MySQLPostgreSQLが使用できるが、今回はMySQLを使用。

初期化ファイル作成

まずは事前にguacamole/guacamoleコンテナ内のスクリプトを使って、DBの初期化スクリプトを作成。

docker run --rm guacamole/guacamole:1.4.0 /opt/guacamole/bin/initdb.sh --mysql > initdb.sql

DBの起動

MySQLコンテナは、起動時に/docker-entrypoint-initdb.dディレクトリ以下の*.sqlファイルでDBを初期化するので、↑で作成した初期化スクリプトをバインドマウントして起動する。
また、DBのデータ(/var/lib/mysql)をボリュームマウントする。

docker run --name guacamole_db \
  -d \
  -e MYSQL_DATABASE=guacamole_db \
  -e MYSQL_USER=guacamole_user \
  -e MYSQL_PASSWORD=guacamole_passwd \
  -e MYSQL_RANDOM_ROOT_PASSWORD=yes \
  -v $PWD:/docker-entrypoint-initdb.d \
  -v guacamole_db:/var/lib/mysql \
  --network guacamole-network \
  mysql

guacamole

アプリケーションサーバーを起動する。
GUACD_HOSTNAMEにguacdコンテナのコンテナ名、MYSQL_HOSTNAMEmysqlコンテナのコンテナ名を指定する。
DBの接続情報はmysql起動時に指定したものと同じものを指定する。

また、このコンテナにwebブラウザでアクセスするので、ポート番号をpublishする。

docker run --name guacamole \
  -d \
  -e GUACD_HOSTNAME=guacd \
  -e MYSQL_HOSTNAME=guacamole_db \
  -e MYSQL_DATABASE=guacamole_db \
  -e MYSQL_USER=guacamole_user \
  -e MYSQL_PASSWORD=guacamole_passwd \
  --network guacamole-network \
  -p 8080:8080 \
  guacamole/guacamole:1.4.0

デプロイはこれで完了。

webアクセスと接続設定

http://<docker host>:8080/guacamole にアクセス。(ドキュメントルートでなくパス指定があるので注意)

認証

初期値としてユーザー名・パスワードともにguacadminがセットされている(たぶんDBの初期化ファイル作成の時点にこれになってる)のでこれでログイン。

Verifying the Guacamole install

adminのパスワードを変更するには、ログインして右上にあるメニューから「設定」を選択。

ユーザ設定メニューにパスワード変更のフィールドがあるので現在のパスワードと新しいパスワードを入力する。

接続先設定

接続先の設定は、メニューの「接続」→「接続の追加」でサーバー設定を行う。 SSH接続のために最低限必要なのは以下の通り。

  • 接続の編集
    • 名前: 設定の名前。わかりやすいのを入力しておく
    • プロトコル: SSH
  • パラメータ
    • ネットワーク
      • ホスト名: 接続先SSHサーバーのアドレス

これだけ入力して「保存」、ホームに戻ると接続情報に設定が追加されるのでマウスクリックすれば、ユーザー名とパスワードを聞かれてパスワード認証でSSH接続できる。

認証パラメータの「ユーザ名」「パスワード」を事前に入力しておけば、接続時の認証も省略できる。

公開鍵認証設定

公開鍵認証の設定は、パラメータセクションに最低限以下を入力すればOK。

ただし、秘密鍵の登録はOpenSSH形式とドキュメントに記載があるが、実際にはPEM形式である必要がある。

The private key must be in OpenSSH format, as would be generated by the OpenSSH ssh-keygen utility.

https://guacamole.apache.org/doc/gug/configuring-guacamole.html#ssh-authentication

現バージョンのOpenSSHのssh-keygenでキーペア作成すると、デフォルトではOpenSSH形式(ファイル先頭が「-----BEGIN OPENSSH PRIVATE KEY-----」で始まる)になっているが、この形式だと公開鍵認証が上手くいかない。
PEM形式(ファイル先頭が「-----BEGIN RSA PRIVATE KEY-----」で始まる)のキーペアを作成するには、ssh-keygen-mオプションで形式を指定する。

$ ssh-keygen -m PEM ...

既存のOpenSSH形式の鍵をPEM形式に変換するには以下。この場合元のファイルが更新されるので、必要があれば事前にコピーしておく。

$ ssh-keygen -p -m PEM -f ~/.ssh/id_rsa

秘密鍵の入力フィールドに、秘密鍵ファイルの中身を全て貼り付けて保存する。

これでホームに戻り、接続先を選択すれば、公開鍵認証でSSH接続できる。

環境

  • ホストOS: Fedora 35
  • Docker: 20.10
  • Guacamole: 1.4.0

補足

Guacamoleのイメージはamd64版しかないので、arm64ではそのままでは動かせない。

デフォルトだと日本語入出力できないので要調査。
多分コンテナ内のロケールがどうとか、、、と思う。
ロケールの追加と設定は関係なく(ja_JP.UTF-8とか入れても変化無し)、原因は日本語フォントでした。あとで別途まとめる。

まとめました。

zaki-hmkc.hatenablog.com

あと今回はSSHサーバーとGuacamoleを立てたDockerホストを別にしてるが、ここは同じホストでも疎通があるなら問題ない。はず。

[Ansible] Gitリポジトリにあるコレクションのインストール

Ansible Galaxyと疎通がないとか、ローカル内でGit管理されているプライベート開発なコレクションのように、任意のGitリポジトリにあるコレクションをインストールする際のrequirements.ymlの記述例は以下の通り。

---
collections:
  - name: https://github.com/zaki-lknr/esxissh-ansible.git
    version: main
    type: git

指定するキーと値は以下の通り。

キー
name リポジトリURL
version ブランチ名
type gitを指定

この内容のファイルを指定することで、各種環境でコレクションをインストールできる。

(配布側) リポジトリ構成

リポジトリのルートディレクトリにgalaxy.ymlファイルやpluginsディレクトリなどのコレクションを構成するディレクトリとその中身を配置する。
ない場合(サブディレクトリ以下にある等)だとエラーになるので注意。

今回のサンプルコレクションはこちら。

github.com

コレクション作成の参考

qiita.com

(利用側) インストール

ansible-galaxy

任意のファイル名で前述の書式のファイルを作成し、以下コマンドを実行。
お作法的には collections/requirements.yml が好ましい。多分。

$ ansible-galaxy install -r collections/requirements.yml

インストール先ディレクトリはデフォルトでは~/.ansible以下。
変更したい場合はansible.cfgなどでインストール先を指定する。

[defaults]
collections_path = /path/to/ansible/collections

ansible-builder

execution-environment.ymldependenciesに指定するrequirements.ymlでインストール内容を記述して、あとは普通にビルドする。

$ ansible-builder build -t esxi-ssh:latest -v 3
Ansible Builder is building your execution environment image, "esxi-ssh:latest".
File context/_build/requirements.yml is already up-to-date.
File context/_build/requirements.txt is already up-to-date.
File context/_build/bindep.txt is already up-to-date.
File context/_build/ansible.cfg is already up-to-date.
:
:
[1/3] STEP 8/8: RUN ansible-galaxy collection install $ANSIBLE_GALAXY_CLI_COLLECTION_OPTS -r requirements.yml --collections-path /usr/share/ansible/collections
Starting galaxy collection install process
Process install dependency map
Cloning into '/home/runner/.ansible/tmp/ansible-local-116wclers/tmptw5tpno2/esxissh-ansible347k3r33'...
Already on 'main'
Your branch is up to date with 'origin/main'.
Starting collection install process
Installing 'zaki_lknr.esxissh:1.1.0' to '/usr/share/ansible/collections/ansible_collections/zaki_lknr/esxissh'
Created collection for zaki_lknr.esxissh:1.1.0 at /usr/share/ansible/collections/ansible_collections/zaki_lknr/esxissh
zaki_lknr.esxissh:1.1.0 was installed successfully
--> 304eaaf07f6
:
:

AAP / AWX / Tower

(確認はAAP 2.1のみ実施)
リポジトリcollections/requirements.yml にインストールしたいコレクションを記述する。

サンプルとしては以下。

github.com

これでソースコントロールの更新時にコレクションがインストールされる。

参考

docs.ansible.com

ドキュメントは以下、Install multiple collections with a requirements fileより。

また、Git上にあるRoleのインストール関連は以下。

zaki-hmkc.hatenablog.com