Receptor (2): Kubernetes 上でのワークの実行


はじめに

前回のエントリ では、Receptor によるメッシュネットワークの構成と、メッシュネットワークを介したリモートノードでのコマンド実行、障害からの回復性を紹介しました。

本エントリでは、リモートノードで何らかの処理をさせるもう一つの手段である、Kubernetes クラスタ上でのワークの実行Kubernetes ワーク)を紹介します。

この機能を利用すると、Receptor の Executor ノード から 任意の Kubernetes クラスタに任意の構成の Pod を作成 できます。Pod で利用するイメージ、そこで実行する コマンドと引数 だけでなく、Pod 自体もカスタマイズでき ます。また、前回のエントリ で紹介したリモートノードでのコマンド実行と同様、任意のペイロードをコマンドの標準入力に渡す ことも引き続き可能なため、非常に柔軟な処理を行えます。

構成例

今回は、図の 4 ノードで構成します。node01 から node03 を前回同様 Docker 上のコンテナとして起動し、node04 は Kubernetes 上の Pod として起動します。

Controller は前回同様 controller01node01)、Executor は controller01node01)と executor01node03)、executor02node04)の 3 ノードです。

必要なファイル群は、前回同様 GitHub のリポジトリに配置 しています。

前提: 基本的な考え方

前回のエントリ で紹介したリモートノードでのコマンド実行(コマンドワーク)では、Executor は指定されたコマンドをそのノード自身で直接実行しました。下図は cat /etc/hostname をリモートノードで実行させた場合の動作イメージです。

一方で、今回扱う Kubernetes クラスタ上でのワークKubernetes ワーク)では、Executor の役割は Kuberentes 上への Worker と呼ばれる Pod の作成 です。ワークに指定したコマンドや引数は、この Worker Pod の起動コマンド(エントリポイント)として渡されます。したがって、実際のコマンドは Executor 自身ではなくさらにその先の Worker Pod で実行されることになります。下図は cat /etc/hostname を Kubernetes ワークとして実行させた場合の動作イメージです。

こうした Pod の作成や、Pod の標準入力へのペイロードの連携、標準出力の取得は、技術的にはすべて Kubernetes の API 経由で行われます。

前提: Kubernetes クラスタへの認証

Kubernetes クラスタ上に Pod を作成するには、当然ながら認証(と認可)が必要です。Receptor は、次の 2 種類の認証をサポートしています。

  • kubeconfig
    • 任意の kubeconfig ファイルを利用して認証する方式
    • kubeconfig ファイルさえあれば Executor ノードの構成に関わらず任意の Kubernetes クラスタ上に Pod を作成できる
  • incluster
    • Executor ノード自体が Kubernetes クラスタ上の Pod として起動している場合に利用できる方式
    • Executor の Pod を動作させている Service Account に認可された範囲で Pod を作成できる

デフォルトは incluster ですが、本エントリでは両方のパタンを実際に試します。今回の構成で executor02node04)を Kubernetes クラスタ上の Pod として配置しているのは、incluster を試すためです。

デモ (1): 基本の動きの確認

まずは単純な例として、前回のエントリ でも利用した、5 秒の sleep を 12 回繰り返すだけの簡単なコマンドを Kubernetes ワークとして動作させます。

設定ファイルの作成

必要なファイルを準備します。実ファイル群は GitHub のリポジトリに配置 しています。

Receptor の Executor 用の設定ファイル

Kubernetes ワークを実行させる Executor として、Docker 上の executor01node03)を利用します。設定ファイルを抜粋します。

...
- work-kubernetes:
    worktype: echo-sleep
    authmethod: kubeconfig
    kubeconfig: /tmp/.kube/config
    namespace: receptor
    image: quay.io/centos/centos:stream8
    command: bash
    params: "-c 'for i in $(seq 1 12); do echo ${i}: $(date +%T); sleep 5; done'"
...

work-kubernetes が、Kubernetes ワークを定義する部分で、ここではこのワークにワークタイプ名として echo-sleep と名前を付けています。

authmethod は認証方式の指定で、kubeconfig ファイルを利用する kubeconfig とし、パスを次の設定 kubeconfig で指定しています。この指定の仕方をする場合、kubeconfig ファイルは Controller 側ではなく Executor 側(つまりこのノード上)に存在している必要があります(少しややこしいですが、後述する receptorctl に kubeconfig のパスを渡す書き方の場合は Controller 側のパスで記述するため、混同に注意です)。

namespaceimagecommandparams が、作成される Pod の構成を変更する部分です。それぞれ、Pod が作成される Namespace、Pod 内のコンテナで利用するイメージ、そのイメージで起動するコマンドとその引数を指定しています。

kubeconfig ファイル

利用する kubeconfig ファイルは、今回は Kubernetes クラスタが K3s のため、/etc/rancher/k3s/k3s.yaml をコピーして利用します。既定でエンドポイントに 127.0.0.1:6443 が指定されているため、コンテナ内からも到達できるように Docker ホストの実 IP アドレス(今回は 192.168.0.219)に書き換えます。

cp /etc/rancher/k3s/k3s.yaml .kube/config
sed -i 's/127\.0\.0\.1/192.168.0.219/g' .kube/config

Docker Compose ファイル

用意した kubeconfig ファイルは、Docker Compose ファイルで node03/tmp/.kube/config にマウントさせています。

...
  node03:
    image: ${RECEPTOR_IMAGE:?err}
    container_name: node03
    hostname: node03.example.internal
    command: "receptor -c /etc/receptor/receptor.conf"
    volumes:
      - "./conf/node03.yml:/etc/receptor/receptor.conf"
      - "./.kube/config:/tmp/.kube/config"
...

実際の動き

Kubernetes クラスタに Namespace receptor を作成して、コンテナを起動させて動きを確認します。

$ kubectl apply -f manifests/namespace.yml
namespace/receptor created
$ docker compose up -d
$ docker compose exec -it node01 bash
[root@node01 /]#

ワークを実行するコマンドは、前回のエントリ と同じです。

[root@node01 /]# receptorctl work submit \
  --node executor01 \
  --no-payload \
  echo-sleep
Result: Job Started
Unit ID: jBLBH6pV

ワークが実行されたら、Kubernetes クラスタ側の様子を確認します。Namespace に receptor を指定していたので、その Namespace の Pod を追いかけます。

$ kubectl -n receptor get pod
NAME               READY   STATUS    RESTARTS   AGE
echo-sleep-mqnjl   1/1     Running   0          13s

$ kubectl -n receptor get pod echo-sleep-mqnjl -o yaml
apiVersion: v1
kind: Pod
metadata:
  ...
  generateName: echo-sleep-
  name: echo-sleep-mqnjl
  namespace: receptor
  ...
spec:
  containers:
  - args:
    - -c
    - 'for i in $(seq 1 12); do echo ${i}: $(date +%T); sleep 5; done'
    command:
    - bash
    image: quay.io/centos/centos:stream8
    imagePullPolicy: IfNotPresent
    name: worker
    ...

Pod echo-sleep-mqnjl が Namespace receptor に作成され動作していることがわかります。Pod 名は、ワークの名称(echo-sleep)から生成されるようです。また、Pod の imagecommandargs も指定した通りに構成されていることがわかります。

前回のエントリ で紹介した一時ファイルの様子も確認します。

[root@node01 /]# ls -l /tmp/receptor/controller01/jBLBH6pV/
total 8
-rw-------. 1 root root 317 Dec  4 19:46 status
-rw-------. 1 root root   0 Dec  4 19:46 status.lock
-rw-------. 1 root root   0 Dec  4 19:45 stdin
-rw-------. 1 root root 147 Dec  4 19:46 stdout

[root@node01 /]# cat /tmp/receptor/controller01/jBLBH6pV/status
{
  "State": 2,
  "Detail": "Finished",
  "StdoutSize": 147,
  ...
  "ExtraData": {
    ...
    "RemoteUnitID": "zfr7GxnR",
    ...
  }
}
[root@node03 /]# ls -l /tmp/receptor/executor01/zfr7GxnR/
total 8
-rw-------. 1 root root 3292 Dec  4 19:46 status
-rw-------. 1 root root    0 Dec  4 19:46 status.lock
-rw-------. 1 root root    0 Dec  4 19:45 stdin
-rw-------. 1 root root  147 Dec  4 19:46 stdout

[root@node03 /]# cat /tmp/receptor/executor01/zfr7GxnR/status
{
  "State": 2,
  "Detail": "Finished",
  "StdoutSize": 147,
  ...
  "ExtraData": {
    "Image": "quay.io/centos/centos:stream8",
    "Command": "bash",
    "Params": "-c 'for i in $(seq 1 12); do echo ${i}: $(date +%T); sleep 5; done'",
    "KubeNamespace": "receptor",
    "KubeConfig": "apiVersion: v1\n...",
    "KubePod": "",
    "PodName": "echo-sleep-mqnjl"
  }
}

このあたりの動きも、前回のエントリ で紹介したコマンドワークの時と同じです。statusExtraData に Kubernetes 固有の情報が含まれるくらいで、大きくは変わらないことがわかります。

結果もこれまで通り確認できます。

[root@node01 /]# receptorctl work list --unit_id jBLBH6pV
{
    "jBLBH6pV": {
        "Detail": "Finished",
        "ExtraData": {
            ...
        },
        "State": 2,
        "StateName": "Succeeded",
        "StdoutSize": 147,
        ...
    }
}

[root@node01 /]# receptorctl work results jBLBH6pV
1: 19:45:18
2: 19:45:23
...
11: 19:46:08
12: 19:46:13

ここまでで、Kubernetes ワークの基本的な動作が確認できました。Pod の構成と kubeconfig は Kubernetes ならではですが、見かけ上の動きは コマンドワーク と大きくは変わりません。

ワークはリリースしておきます。

[root@node01 /]# receptorctl work release --all
Released:
(jBLBH6pV, released)

デモ (2): コマンドへの引数と標準入力の連携

前回のエントリ で紹介したコマンドワークへの引数と標準入力の連携は、Kuberentes ワークでも同じように利用できます。このデモでは、次のことを試します(三つ目はオマケです)。

  • 実行時に、任意の引数をコマンドへ追加する
  • 実行時に、任意のペイロードを標準入力としてコマンドへ渡す
  • 実行時に、任意のイメージでコマンドを実行する(参考)

設定ファイルの作成

引き続き、Executor として executor01node03)を利用します。設定ファイルのうち、関連する部分を抜粋します。実ファイル群は GitHub のリポジトリに配置 しています。

...
- work-kubernetes:
    worktype: cat-files
    authmethod: kubeconfig
    kubeconfig: /tmp/.kube/config
    namespace: receptor
    image: quay.io/centos/centos:stream8
    command: grep
    params: "-n -H ''"
    allowruntimeparams: true
    allowruntimecommand: true

- work-kubernetes:
    worktype: echo-reply
    authmethod: kubeconfig
    kubeconfig: /tmp/.kube/config
    namespace: receptor
    image: quay.io/centos/centos:stream8
    command: bash
    params: "-c 'while read -r line; do echo Reply from $(cat /etc/hostname): ${line^^}; done'"
...

引数で渡されたファイルを grep する cat-files と、標準入力を echo し返す echo-reply を定義しています。

cat-files では、allowruntimeparamsallowruntimecommandtrue にしています。allowruntimeparams が実行時の params の追加を許可する指定です。allowruntimecommand は、デモの本筋とは若干逸れますが、実行時の image の変更を許可する指定です。

実際の動き

コンテナを起動して動きを確認します。

$ docker compose up -d
$ docker compose exec -it node01 bash
[root@node01 /]#

実行時の引数の追加

コマンドワークでは、引数の追加は --param params=<追加したい引数> でしたが、Kubernetes ワークに引数を追加する場合は、--param kube_params=<追加したい引数> で指定します。

[root@node01 /]# receptorctl work submit \
  --node executor01 \
  --rm \
  --follow \
  --no-payload \
  --param "kube_params=/etc/hostname /etc/os-release" \
  cat-files
/etc/hostname:1:cat-files-ld5zs
/etc/os-release:1:NAME="CentOS Stream"
/etc/os-release:2:VERSION="8"
/etc/os-release:3:ID="centos"
/etc/os-release:4:ID_LIKE="rhel fedora"
/etc/os-release:5:VERSION_ID="8"
/etc/os-release:6:PLATFORM_ID="platform:el8"
/etc/os-release:7:PRETTY_NAME="CentOS Stream 8"
/etc/os-release:8:ANSI_COLOR="0;31"
/etc/os-release:9:CPE_NAME="cpe:/o:centos:centos:8"
/etc/os-release:10:HOME_URL="https://centos.org/"
/etc/os-release:11:BUG_REPORT_URL="https://bugzilla.redhat.com/"
/etc/os-release:12:REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux 8"
/etc/os-release:13:REDHAT_SUPPORT_PRODUCT_VERSION="CentOS Stream"
(y6UCfqYt, released)

結果から指定したファイルが grep できていることがわかります。

実際の Pod はすぐに消されてしまいますが、急げば定義も確認できます。

$ kubectl -n receptor get pod
NAME              READY   STATUS      RESTARTS   AGE
cat-files-ld5zs   0/1     Completed   0          1s

$ kubectl -n receptor get pod -o yaml
...
- apiVersion: v1
  kind: Pod
  metadata:
    ...
    generateName: cat-files-
    name: cat-files-ld5zs
    namespace: receptor
    ...
  spec:
    containers:
    - args:
      - -n
      - -H
      - ""
      - /etc/hostname
      - /etc/os-release
      command:
      - grep
      image: quay.io/centos/centos:stream8
      imagePullPolicy: IfNotPresent
      name: worker
      ...

Pod の定義から、確かに args に引数が追加されていることが確認できます。

なお、コマンドワークではワークタイプ名に続けて追加したい引数をベタ書きできました(cat-files -- /etc/hostname /etc/os-release など)が、Kubernetes ワークではこの表記は機能しません。

実行時の標準入力の利用

標準入力の渡し方は コマンドワーク と全く同じです。--payload でファイルまたは receptorctl への標準入力を、--payload-literal で任意の文字列をそれぞれ渡せます。実行例だけ列挙します。

[root@node01 /]# echo "Hello Receptor!" > /tmp/input.txt
[root@node01 /]# receptorctl work submit \
  --node executor01 \
  --rm \
  --follow \
  --payload /tmp/input.txt \
  echo-reply
Reply from echo-reply-64l6z: HELLO RECEPTOR!
(mW6CH1D9, released)

[root@node01 /]# echo "Hello Receptor!" | receptorctl work submit \
  --node executor01 \
  --rm \
  --follow \
  --payload - \
  echo-reply
Reply from echo-reply-jf4bd: HELLO RECEPTOR!
(vmjsnjSd, released)

[root@node01 /]# receptorctl work submit \
  --node executor01 \
  --rm \
  --follow \
  --payload-literal "Hello Receptor!" \
  echo-reply
Reply from echo-reply-4jzxn: HELLO RECEPTOR!
(MUdlZNqN, released)

実行時のイメージの変更(参考)

デモの本筋とは若干逸れますが、cat-files では、設定ファイルで allowruntimecommandtrue にして、実行時の image の変更を許可していました。これを利用するには、--param kube_image=<イメージ> を追加します。例えば、ubuntu:22.04 を指定して実行します。

[root@node01 /]# receptorctl work submit \
  --node executor01 \
  --rm \
  --follow \
  --no-payload \
  --param kube_image=ubuntu:22.04 \
  --param "kube_params=/etc/hostname /etc/os-release" \
  cat-files
/etc/hostname:1:cat-files-tqwj9
/etc/os-release:1:PRETTY_NAME="Ubuntu 22.04.1 LTS"
/etc/os-release:2:NAME="Ubuntu"
/etc/os-release:3:VERSION_ID="22.04"
/etc/os-release:4:VERSION="22.04.1 LTS (Jammy Jellyfish)"
/etc/os-release:5:VERSION_CODENAME=jammy
/etc/os-release:6:ID=ubuntu
/etc/os-release:7:ID_LIKE=debian
/etc/os-release:8:HOME_URL="https://www.ubuntu.com/"
/etc/os-release:9:SUPPORT_URL="https://help.ubuntu.com/"
/etc/os-release:10:BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
/etc/os-release:11:PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
/etc/os-release:12:UBUNTU_CODENAME=jammy
(jplGpy12, released)

Ubuntu らしい /etc/os-release の中身が返ってきました。Pod の定義上も image が変わっていることが確認できます。

$ kubectl -n receptor get pod
NAME              READY   STATUS      RESTARTS   AGE
cat-files-tqwj9   0/1     Completed   0          1s

$ kubectl -n receptor get pod -o yaml
...
- apiVersion: v1
  kind: Pod
  metadata:
    ...
    generateName: cat-files-
    name: cat-files-vkgzj
    namespace: receptor
    ...
  spec:
    containers:
    - args:
      - -n
      - -H
      - ""
      - /etc/hostname
      - /etc/os-release
      command:
      - grep
      image: ubuntu:22.04
      imagePullPolicy: IfNotPresent
      name: worker
      ...

デモ (3): Pod のカスタマイズ

これまでのデモで示したように、設定ファイルの work-kubernetes 中の namespaceimagecommandparams を活用することで、Executor によって起動される Pod の構成を変更できます。また、allowruntime* を指定すれば、これらは実行時の --paramkube_* により動的にも指定できます(対応表は ドキュメント に掲載されています)。

一方で、この 4 項目だけでは充分なカスタマイズができるとは言えません。例えば、Pod の CPU やメモリを管理したい場合や、プライベートレジストリの認証情報を渡したい場合、PV をマウントしたい場合、Pod で Init コンテナやサイドカーコンテナを動かしたい場合などには対応できません。

こうした要件に対応するため、Executor が起動させる Pod のマニフェストを丸ごと指定できる ようになっています。本デモでは、次のことを試します(三つ目はオマケです)。

  • 事前に指定したマニフェストで Pod を作成させる
  • 実行時に指定したマニフェストで Pod を作成させる
  • 実行時に指定した kubeconfig ファイルで Pod を作成させる(参考)

設定ファイルの作成

必要なファイルを準備します。実ファイル群は GitHub のリポジトリに配置 しています。

マニフェストの作成

作成される Pod のマニフェストでは、少なくとも worker と名付けられたコンテナが一つ含まれる 必要があります。また、本エントリ公開時点の実装では、複数のコンテナを含めた場合、worker コンテナがあまりにも早く終了しすぎるとワークがエラーになる(Issue)ほか、worker コンテナが完了した段階でワークも完了になる(--rm を付けるとその時点でその他のコンテナの終了処理が始まる)ため、若干の注意が必要です。

また、Pod のマニフェストを指定すると、設定ファイルの imagecommandparams(やそれに対応する実行時の kube_*)の指定が許可されない(または効果がなくなる)ので、マニフェストの段階で完全なものを指定しておく必要があります。これらを実行時に都度指定したい要件がある場合は、マニフェストをまず修正し、実行時にマニフェストを指定するパタン(後述)を使う必要があるでしょう。

今回は、任意のマニフェストを読み込ませられることが確認できればよいので、ごく簡単な例として次の要件を考えます。

  • ラベル app: hello-receptor あり
  • コンテナに CPU のリクエスト値あり
  • Init コンテナ(sleep するだけ)あり
  • CentOS Stream 8 のイメージを利用
  • `grep -n -H “” /etc/hostname /etc/os-release” を実行

これに従い、以下の内容で manifests/pod.yml を作成します。

apiVersion: v1
kind: Pod
metadata:
  namespace: receptor
  labels:
    app: hello-receptor
spec:
  initContainers:
    - name: init-worker
      image: quay.io/centos/centos:stream8
      command:
        - sleep
      args:
        - "5"
  containers:
    - name: worker
      image: ubuntu:22.04
      command:
        - grep
      args:
        - -n
        - -H
        - ""
        - /etc/hostname
        - /etc/os-release
      resources:
        requests:
          cpu: 10m

Receptor の Executor 用の設定ファイル

マニフェストを 事前に設定ファイルで指定 するパタンでは、Executor に executor01node03)を利用します。このパタンでは、マニフェストは Executor 側に配置 する必要があります。設定ファイルの該当箇所は次の通りです。

...
- work-kubernetes:
    worktype: custom-predefined-pod
    authmethod: kubeconfig
    kubeconfig: /tmp/.kube/config
    pod: /tmp/pod.yml
    allowruntimecommand: true
...

pod が Pod のマニフェストファイルのパスを指定する箇所です。今回は Docker 環境なので、このパスには Docker ホスト側の ./manifests/pod.yml を後述の Docker Compose ファイルでマウントさせます。

allowruntimecommand は本来は不要ですが、本エントリ公開時点では pod を指定する場合に allowruntimecommandallowruntimepod(後述)が許可されていないとコケる(Issue / PR)ようなので仕方なく足しています(おそらくバグ)。

マニフェストを 実行時に receptorctl 経由で指定 するパタンでは、Executor に controller01node01)を指定します。executor01 を利用しないのは、次の仕様があるからです。

  • マニフェストを実行時に指定する場合、マニフェストファイルは Controller 側で読み取られる
  • 読み取った内容は Executor に送信 され利用される
  • マニフェストは Receptor 的には秘匿対象 である
  • 秘匿対象の情報は バックエンドが TLS で暗号化されていないと他のノードに送信できない

今回、TLS をまったく構成していないので、Controller で読み取ったマニフェストは Controller 以外のノードに送れません。逆に言えば他のノードに送らなければ使えるので、今回は Controller 兼 Executor である controller01 に閉じて実行できるように構成します。

なお、TLS を構成して他のノードの Executor を利用する場合、TLS は最低限 Controller から次のノードまでの間で構成されていればよいようです(盗聴対策としてはエンドツーエンドでなければあまり意味がないですが)。

controller01node01)の設定ファイルに次の work-kubernetes を追加します。

...
- work-kubernetes:
    worktype: custom-runtime-pod
    authmethod: kubeconfig
    allowruntimepod: true
    allowruntimeauth: true
...

allowruntimepod が、実行時に Pod のマニフェストを指定できるようにするオプションです。allowruntimeauth は本デモの本筋ではないですが、kubeconfig ファイルを実行時に指定する場合のオプションです。

Docker Compose ファイル

node01 には、マニフェストと kubeconfig ファイルを 実行時に指定するためvolumes にエントリを追加します。node03 にも追加しますが、これはマニフェストと kubeconfig ファイルを 設定ファイルで事前に指定したため です。

services:
  node01:
    ...
    volumes:
      - "./conf/node01.yml:/etc/receptor/receptor.conf"
      - "./.kube/config:/tmp/.kube/config"
      - "./manifests/pod.yml:/tmp/pod.yml"
...
  node03:
    ...
    volumes:
      - "./conf/node03.yml:/etc/receptor/receptor.conf"
      - "./.kube/config:/tmp/.kube/config"
      - "./manifests/pod.yml:/tmp/pod.yml"

実際の動き

コンテナを起動させて、動きを確認します。

$ docker compose up -d
$ docker compose exec -it node01 bash
[root@node01 /]#

設定ファイルで指定したマニフェストの利用

executor01 に対してワークを実行して、作成された Pod の定義を確認します。

[root@node01 /]# receptorctl work submit \
  --node executor01 \
  --rm \
  --follow \
  --no-payload \
  custom-predefined-pod
/etc/hostname:1:custom-predefined-pod-rcptp
/etc/os-release:1:PRETTY_NAME="Ubuntu 22.04.1 LTS"
/etc/os-release:2:NAME="Ubuntu"
/etc/os-release:3:VERSION_ID="22.04"
/etc/os-release:4:VERSION="22.04.1 LTS (Jammy Jellyfish)"
/etc/os-release:5:VERSION_CODENAME=jammy
/etc/os-release:6:ID=ubuntu
/etc/os-release:7:ID_LIKE=debian
/etc/os-release:8:HOME_URL="https://www.ubuntu.com/"
/etc/os-release:9:SUPPORT_URL="https://help.ubuntu.com/"
/etc/os-release:10:BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
/etc/os-release:11:PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
/etc/os-release:12:UBUNTU_CODENAME=jammy
(MM4c8e9s, released)
$ kubectl -n receptor get pod -o yaml
...
- apiVersion: v1
  kind: Pod
  metadata:
    ...
    labels:
      app: hello-receptor
    name: custom-predefined-pod-rcptp
    namespace: receptor
    ...
  spec:
    containers:
    - args:
      - -n
      - -H
      - ""
      - /etc/hostname
      - /etc/os-release
      command:
      - grep
      image: ubuntu:22.04
      imagePullPolicy: IfNotPresent
      name: worker
      resources:
        requests:
          cpu: 10m
      ...
    initContainers:
    - args:
      - "5"
      command:
      - sleep
      image: quay.io/centos/centos:stream8
      imagePullPolicy: IfNotPresent
      name: init-worker
      ...

マニフェストファイルで記述した通り、ラベルやリクエスト値、Init コンテナが構成されていることがわかります。

実行時に指定したマニフェストの利用

実行時にマニフェストを指定するには、receptorctl--param secret_kube_pod=@<ファイルパス> を指定します。前述の通り、これは Controller 側のファイルパス です。@ を付与することで、その中身が読み取られて利用されます(コマンドが長くなりますが、@ なしでマニフェストを直接文字列として渡すことも可能です、この場合は YAML でなく JSON がよいですね)。

同様に、--param secret_kube_config=@<ファイルパス> で kubeconfig ファイルも実行時に指定できます。

[root@node01 /]# receptorctl work submit \
  --node controller01 \
  --rm \
  --follow \
  --no-payload \
  --param secret_kube_config=@/tmp/.kube/config \
  --param secret_kube_pod=@/tmp/pod.yml \
  custom-runtime-pod
/etc/hostname:1:custom-runtime-pod-znh8k
/etc/os-release:1:PRETTY_NAME="Ubuntu 22.04.1 LTS"
/etc/os-release:2:NAME="Ubuntu"
/etc/os-release:3:VERSION_ID="22.04"
/etc/os-release:4:VERSION="22.04.1 LTS (Jammy Jellyfish)"
/etc/os-release:5:VERSION_CODENAME=jammy
/etc/os-release:6:ID=ubuntu
/etc/os-release:7:ID_LIKE=debian
/etc/os-release:8:HOME_URL="https://www.ubuntu.com/"
/etc/os-release:9:SUPPORT_URL="https://help.ubuntu.com/"
/etc/os-release:10:BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
/etc/os-release:11:PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
/etc/os-release:12:UBUNTU_CODENAME=jammy
(Jk6LcMtY, released)

Pod の定義からは、意図通りに構成されていることがわかります。

$ kubectl -n receptor get pod -o yaml
...
- apiVersion: v1
  kind: Pod
  metadata:
    ...
    labels:
      app: hello-receptor
    name: custom-runtime-pod-98bmc
    namespace: receptor
    ...
  spec:
    containers:
    - args:
      - -n
      - -H
      - ""
      - /etc/hostname
      - /etc/os-release
      command:
      - grep
      image: ubuntu:22.04
      imagePullPolicy: IfNotPresent
      name: worker
      resources:
        requests:
          cpu: 10m
      ...
    initContainers:
    - args:
      - "5"
      command:
      - sleep
      image: quay.io/centos/centos:stream8
      imagePullPolicy: IfNotPresent
      name: init-worker
      ...

デモ (4): In-Cluster Authentication の利用

これまでのデモでは、いずれの Executor も Kubernetes の外で動作しており、Kubernetes への認証には kubeconfig ファイルを利用していました。

一方で、Executor 自身が Kubernetes 上の Pod として動作している場合、その Pod を動作させている Service Account(SA)の権限を使って、kubeconfig ファイルを用意することなく認証することも可能です。これは、いわゆる In-Cluster Authentication などと呼ばれるもので、自動でマ /var/run/secrets/kubernetes.io/serviceaccount にマウントされる SA のトークンを使って認証するものです。

今回は、Kubernetes 上の Pod として動作させる Executor として executor02node04)を作成し、ここで In-Cluster Authentication を試します。

設定ファイルの作成

必要なファイルを準備して、環境を整えます。実ファイル群は GitHub のリポジトリに配置 しています。

Receptor の Executor 用の設定ファイル

executor02node04)の設定ファイルの抜粋です。

---
- node:
    id: executor02

...

- tcp-peer:
    address: node02.example.internal:7323

...

- work-kubernetes:
    worktype: echo-reply-incluster
    authmethod: incluster
    namespace: receptor
    image: quay.io/centos/centos:stream8
    command: bash
    params: "-c 'while read -r line; do echo Reply from $(cat /etc/hostname): ${line^^}; done'"

authmethodincluster にしている点が今回のポイントです。それ以外の部分は構成を簡単にするためにシンプルにしていますが、標準入力の動作は確認できるようにしています。実際には、これまでのデモで紹介したような allowruntime* などももちろん併用可能です。

また、tcp-peer で指定しているホスト名には Executor の Pod からアクセスが発生するため、上位の DNS で名前解決できるようにしておきます。

Docker Compose ファイル

node04 が接続しにいく対向の node02 側では、Docker Compose ファイルでポート 7323 を外部に公開しています。

...
  node02:
    ...
    ports:
      - 7323:7323
    ...
...

SA 関連のマニフェスト

In-Cluster Authentication を使うと、Pod の作成は SA の権限で行われます。つまり、SA が Pod の作成に必要な権限を持っている必要があります。このため、SA だけでなく Role と Role Binding の作成も必要です。

特別な工夫はなく、通常のお作法に従って作ればよいですが、Role には Pod の作成・削除や状態の確認のための pods への権限だけでなく、worker コンテナの標準出力を API 経由で取得するための pods/log、標準入力をコンテナに渡すための pods/attach も必要なようです。今回は以下の定義としています。

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: receptor
rules:
  - apiGroups:
      - ""
    resources:
      - pods
    verbs:
      - get
      - list
      - watch
      - create
      - delete
  - apiGroups:
      - ""
    resources:
      - pods/log
    verbs:
      - get
  - apiGroups:
      - ""
    resources:
      - pods/attach
    verbs:
      - create

また、Executor 自体の Pod のマニフェストも作成します(動作確認程度なので Deployment にはしていません)。以下の定義です。SA を指定しているほか、Receptor の設定ファイルとして ConfigMap をマウントさせています。image はここでは latest タグにしていますが、Kustomize で実際には v1.3.0 に置き換えます。

apiVersion: v1
kind: Pod
metadata:
  namespace: receptor
  name: node04
spec:
  hostname: node04
  containers:
    - name: node04
      image: quay.io/ansible/receptor:latest
      command:
        - receptor
        - -c
        - /etc/receptor/receptor.conf
      volumeMounts:
        - name: receptor-conf
          mountPath: /etc/receptor/receptor.conf
          subPath: receptor.conf
  volumes:
    - name: receptor-conf
      configMap:
        name: receptor-conf
        items:
          - key: node04.yml
            path: receptor.conf
  restartPolicy: Always
  serviceAccountName: receptor

その他、実際のファイルは GitHub のリポジトリ にあるので割愛して、まとめて Kustomize で必要なリソース群を作成します。

$ kubectl apply -k .
namespace/receptor created
serviceaccount/receptor created
role.rbac.authorization.k8s.io/receptor created
rolebinding.rbac.authorization.k8s.io/receptor created
configmap/receptor-conf created
pod/node04 created

Pod が起動しました。

$ kubectl -n receptor get pod
NAME     READY   STATUS    RESTARTS   AGE
node04   1/1     Running   0          5s

$ kubectl -n receptor get pod node04 -o yaml
apiVersion: v1
kind: Pod
metadata:
  ...
  name: node04
  namespace: receptor
  ...
spec:
  containers:
  - command:
    - receptor
    - -c
    - /etc/receptor/receptor.conf
    image: quay.io/ansible/receptor:v1.3.0
    imagePullPolicy: IfNotPresent
    name: node04
    ...
    volumeMounts:
    - mountPath: /etc/receptor/receptor.conf
      name: receptor-conf
      subPath: receptor.conf
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: kube-api-access-b2sck
      readOnly: true
  ...
  serviceAccount: receptor
  serviceAccountName: receptor
  ...
  volumes:
  - configMap:
      defaultMode: 420
      items:
      - key: node04.yml
        path: receptor.conf
      name: receptor-conf
    name: receptor-conf
  - name: kube-api-access-b2sck
    projected:
      ...

実際の動き

Docker 側でもコンテナを起動させて、動きを確認します。

$ docker compose up -d
$ docker compose exec -it node01 bash
[root@node01 /]#

まずは、Kubernetes 上で動作させている executor02 が Controller 側で認識され、ワーク echo-reply-incluster が使える状態であることを確認します。

[root@node01 /]# receptorctl status
...
Known Node   Known Connections
controller01 relayer01: 1 
executor01   relayer01: 1 
executor02   relayer01: 1 
relayer01    controller01: 1 executor01: 1 executor02: 1 

Route        Via
executor01   relayer01
executor02   relayer01
relayer01    relayer01

Node         Service   Type       Last Seen             Tags
controller01 control   Stream     2022-12-04 22:23:48   {'type': 'Control Service'}
executor01   control   Stream     2022-12-04 22:23:19   {'type': 'Control Service'}
executor02   control   Stream     2022-12-04 22:22:55   {'type': 'Control Service'}

Node         Work Types
controller01 custom-runtime-pod
executor01   echo-sleep, cat-files, echo-reply, custom-predefined-pod
executor02   echo-reply-incluster

ワークを実行します。

[root@node01 /]# echo "Hello Receptor!" | receptorctl work submit \
  --node executor02 \
  --rm \
  --follow \
  --payload - \
  echo-reply-incluster
Reply from echo-reply-incluster-ql9cp: HELLO RECEPTOR!
(JB484Cvb, released)

Pod が意図通り作成されました。

$ kubectl -n receptor get pod
NAME                         READY   STATUS      RESTARTS   AGE
node04                       1/1     Running     0          107s
echo-reply-incluster-ql9cp   0/1     Completed   0          2s

$ kubectl -n receptor get pod -o yaml
...
- apiVersion: v1
  kind: Pod
  metadata:
    ...
    generateName: echo-reply-incluster-
    name: echo-reply-incluster-ql9cp
    namespace: receptor
    ...
  spec:
    containers:
    - args:
      - -c
      - 'while read -r line; do echo Reply from $(cat /etc/hostname): ${line^^}; done'
      command:
      - bash
      image: quay.io/centos/centos:stream8
      imagePullPolicy: IfNotPresent
      name: worker
      ...

Executor が Pod として動作していて、かつ SA が充分な権限を持っていれば、kubeconfig を使わなくてもワークを実行できることが確認できました。

まとめ

前回のエントリ の続きとして、Kubernetes クラスタ上でのワークの実行を試しました。

Pod が自由に構成でき、ペイロードも渡せるので、相当に柔軟な対応ができそうです。

Receptor 関連エントリ

@kurokobo

くろいです。ギターアンサンブルやら音響やらがフィールドの IT やさんなアルトギター弾き。たまこう 48 期ぎたさん、SFC '07 おんぞう、新日本ギターアンサンブル、Rubinetto。今は野良気味。

コメントを残す

メールアドレスが公開されることはありません。