Kubernetes Python Clientを使いPodからのデプロイメント

# はじめに
アプリケーションサービスからk8s環境にDeployment、Podリソースを提供する方法を調査し実装する。アプリケーションサービスもk8s上に提供されPodとして稼働する前提とします。

# 実装に利用する環境について

実装に利用したk8s環境は以下の通りです。

```bash
$ kubectl get nodes
NAME        STATUS     ROLES           AGE   VERSION
master      Ready      control-plane   42d   v1.31.6
worker1     NotReady   <none>          14h   v1.31.6
worker2     Ready      <none>          14h   v1.31.6
$ kubectl version
Client Version: v1.31.6
Kustomize Version: v5.4.2
Server Version: v1.31.5
$ kubeadm version
kubeadm version: &version.Info{Major:"1", Minor:"31", GitVersion:"v1.31.6", GitCommit:"6b3560758b37680cb713dfc71da03c04cadd657c", GitTreeState:"clean", BuildDate:"2025-02-12T21:31:09Z", GoVersion:"go1.22.12", Compiler:"gc", Platform:"linux/amd64"}
```

# Kubernetes Python Client 用Podの作成

アプリケーションサービスを作成するのは面倒なので、今回はPythonを実行可能なPodを作成し、手動でpythonコマンドを実行することで実装します。Python用のPodは、以下の定義で作ります。

```yaml:pod-python.yaml
apiVersion: v1
kind: Pod
metadata:
  name: python-pod
spec:
  containers:
  - name: python-container
    image: python:3.11-slim
    command: ["python3"]
    tty: true
```

```bash
$ kubectl create -f pod-python.yaml 
pod/python-pod created
$ kubectl get pods
NAME                                READY   STATUS        RESTARTS   AGE
python-pod                          1/1     Running       0          6s
$ kubectl exec -it python-pod -- bash
root@python-pod:/# python
Python 3.11.11 (main, Feb 25 2025, 02:38:55) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print("Hello Python!")
Hello Python!
>>> 
root@python-pod:/# exit
```

# Kubernetes Python Client モジュールのインストール

手動であらかじめ必要なパッケージをダウンロードしておき、python-dpo:/tmp配下にコピーしておきます。
```bash
$ kubectl exec -it python-pod -- bash
root@python-pod:/# python3 -m venv myenv
root@python-pod:/# source myenv/bin/activate
(myenv) root@python-pod:/# cd /tmp
(myenv) root@python-pod:/tmp# pip install certifi-2025.1.31-py3-none-any.whl six-1.17.0-py2.py3-none-any.whl charset_normalizer-2.0.12-py3-none-any.whl urllib3-1.26.20-py2.py3-none-any.whl pyasn1-0.5.1-py2.py3-none-any.whl rsa-4.9-py3-none-any.whl idna-3.10-py3-none-any.whl durationpy-0.9-py3-none-any.whl websocket_client-1.3.1-py3-none-any.whl oauthlib-3.2.2-py3-none-any.whl requests-2.27.1-py2.py3-none-any.whl requests_oauthlib-2.0.0-py2.py3-none-any.whl cachetools-4.2.4-py3-none-any.whl pyasn1_modules-0.3.0-py2.py3-none-any.whl python_dateutil-2.9.0.post0-py2.py3-none-any.whl google_auth-2.22.0-py2.py3-none-any.whl 
〜省略〜
Installing collected packages: durationpy, websocket-client, urllib3, six, pyasn1, oauthlib, idna, charset-normalizer, certifi, cachetools, rsa, requests, python-dateutil, pyasn1-modules, requests-oauthlib, google-auth
Successfully installed cachetools-4.2.4 certifi-2025.1.31 charset-normalizer-2.0.12 durationpy-0.9 google-auth-2.22.0 idna-3.10 oauthlib-3.2.2 pyasn1-0.5.1 pyasn1-modules-0.3.0 python-dateutil-2.9.0.post0 requests-2.27.1 requests-oauthlib-2.0.0 rsa-4.9 six-1.17.0 urllib3-1.26.20 websocket-client-1.3.1
(myenv) root@python-pod:/tmp# gzip -dc PyYAML-6.0.1.tar.gz | tar xf -
(myenv) root@python-pod:/tmp# cd PyYAML-6.0.1
(myenv) root@python-pod:/tmp/PyYAML-6.0.1# python3 setup.py install
〜省略〜
gcc -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/myenv/include -I/usr/local/include/python3.11 -c yaml/_yaml.c -o build/temp.linux-x86_64-cpython-311/yaml/_yaml.o
Error compiling module, falling back to pure Python
〜省略〜
Installed /myenv/lib/python3.11/site-packages/PyYAML-6.0.1-py3.11-linux-x86_64.egg
Processing dependencies for PyYAML==6.0.1
Finished processing dependencies for PyYAML==6.0.1
(myenv) root@python-pod:/tmp/PyYAML-6.0.1# cd ..
(myenv) root@python-pod:/tmp# pip install kubernetes-32.0.1-py2.py3-none-any.whl 
〜省略〜
Installing collected packages: kubernetes
Successfully installed kubernetes-32.0.1
(myenv) root@python-pod:/tmp# 
```
途中でgccコマンドエラーが出力されていましたが、最終的にSuccessfullyのため一旦無視することにします。

# 呼び出し用 Deployment マニフェストの準備

Clientが呼び出すマニフェストは、以下内容をもとにPython Clientコードへ変換していきます。変換後のPythonコードはapply_in_cluster.pyになります。

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80
```

```python:apply_in_cluster.py
from kubernetes import client, config

config.load_incluster_config()

deployment = client.V1Deployment(
    api_version="apps/v1",
    kind="Deployment",
    metadata=client.V1ObjectMeta(name="nginx-deployment"),
    spec=client.V1DeploymentSpec(
        replicas=3,
        template=client.V1PodTemplateSpec(
            spec=client.V1PodSpec(
                containers=[
                    client.V1Container(
                        name="nginx",
                        image="nginx:latest",
                        ports=[client.V1ContainerPort(container_port=80)],
                    )
                ]
            ),
        ),
    ),
)

api = client.AppsV1Api()
namespace = "default"

try:
    existing_deployment = api.read_namespaced_deployment("nginx-deployment", namespace)
    print("Deployment already exists, Updating...")
    api.patch_namespaced_deployment(name="nginx-deployment", namespace=namespace, body=deployment)
    print("Deployment updated successfully.")
except client.exceptions.ApiException as e:
    if e.status == 404:
        print("Deployment does not exist. Creating...")
        api.create_namespaced_deployment(namespace=namespace, body=deployment)
        print("Deployment created successfully.")
    else:
        print(f"Error: {e}")
```

# Clientスクリプトの実行

先程のpython-podにapply_in_cluster.pyをコピーします。

```bash
$ kubectl cp apply_in_cluster.py python-pod:/
```

python-podに接続し先程のPythonファイルを実行してみます。
```bash
$ kubectl exec -it python-pod -- bash
root@python-pod:/# source myenv/bin/activate
(myenv) root@python-pod:/# python apply_in_cluster.py 
Error: (403)
Reason: Forbidden
HTTP response headers: HTTPHeaderDict({'Audit-Id': 'd598c4ea-0d8c-46cc-a90f-10fdf4de33d5', 'Cache-Control': 'no-cache, private', 'Content-Type': 'application/json', 'X-Content-Type-Options': 'nosniff', 'X-Kubernetes-Pf-Flowschema-Uid': '2094e53b-be81-4453-a7ce-d136df21c242', 'X-Kubernetes-Pf-Prioritylevel-Uid': 'e4dae82a-0dd7-4ac1-b0ef-4ad9e5a9b3a6', 'Date': 'Mon, 10 Mar 2025 09:53:29 GMT', 'Content-Length': '373'})
HTTP response body: {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"deployments.apps \"nginx-deployment\" is forbidden: User \"system:serviceaccount:default:default\" cannot get resource \"deployments\" in API group \"apps\" in the namespace \"default\"","reason":"Forbidden","details":{"name":"nginx-deployment","group":"apps","kind":"deployments"},"code":403}


(myenv) root@python-pod:/# 
```

エラーが出力されました。どうやら403なので権限不足が原因の様です。エラーメッセージを整形し内容を確認してみる。HeaderとBodyパートに分かれているようです。ヘッダーにはエラー原因に繋がる内容は特になさそうです。Bodyにはいくつか情報がありそうです。

```
User "system:serviceaccount:default:default":
```

リクエストを実行したユーザーは default ネームスペースの default サービスアカウント みたいです。KubernetesではPodがAPIを操作する際、デフォルトのサービスアカウントとして (system:serviceaccount:<namespace>:default) が使用される様です。

```
"cannot get resource "deployments" in API group "apps":
```

deployments リソース（apps APIグループ）の get 操作（情報取得）を実行しようとしたが、権限がないようです。deployments get以外にも、Pods リソースのget操作やcreate操作等々、必要な感じがします。

```
Error: (403)
Reason: Forbidden
HTTP response headers: HTTPHeaderDict(
{
    'Audit-Id': 'd598c4ea-0d8c-46cc-a90f-10fdf4de33d5', 
    'Cache-Control': 'no-cache, 
    private', 
    'Content-Type': 'application/json', 
    'X-Content-Type-Options': 'nosniff',
    'X-Kubernetes-Pf-Flowschema-Uid': '2094e53b-be81-4453-a7ce-d136df21c242',
    'X-Kubernetes-Pf-Prioritylevel-Uid': 'e4dae82a-0dd7-4ac1-b0ef-4ad9e5a9b3a6',
    'Date': 'Mon, 10 Mar 2025 09:53:29 GMT',
    'Content-Length': '373'
}
)
HTTP response body: {
    "kind":"Status",
    "apiVersion":"v1",
    "metadata":{},
    "status":"Failure",
    "message":"deployments.apps "nginx-deployment" is forbidden: User "system:serviceaccount:default:default" cannot get resource "deployments" in API group "apps" in the namespace "default"",
    "reason":"Forbidden",
    "details":{
        "name":"nginx-deployment",
        "group":"apps",
        "kind":"deployments"
    },
    "code":403
}
```

とりあえずAPI group "apps"でdeplyomentsリソースに対しget操作可能なServiceAccountやRole/RoleBindingを作成しpython-podに適用してみます。

# ServiceAccount リソース
```yaml:sa.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-service-account
```

# Role リソース

```yaml:role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: default
  name: my-role
rules:
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get"]
```
# RoleBinding リソース

作成したRoleリソースをServiceAccountリソースにバインドするためのRoleBindingリソースを作成します。

```yaml:rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: my-rolebinding
  namespace: default
subjects:
  - kind: ServiceAccount
    name: my-service-account
    namespace: default
roleRef:
  kind: Role
  name: my-role
  apiGroup: rbac.authorization.k8s.io
```

```python:python.yaml
apiVersion: v1
kind: Pod
metadata:
  name: python-pod
spec:
  serviceAccountName: my-service-account # 追加
  containers:
  - name: python-container
    image: python:3.11-slim
    command: ["python3"]
    tty: true
```

用意したファイルを用いて適用していきます。どうやらデプロイ済みのコンテナに適用するとエラーになるようです。具体的にはServiceAccount: "default"をServiceAccount: "my-service-account"に変更することが許可されていないみたいです。

# ServiceAccount リソースの適用

```bash
$ kubectl apply -f sa.yaml -f role.yaml -f rolebinding.yaml -f python.yaml 
serviceaccount/my-service-account created
role.rbac.authorization.k8s.io/my-role created
rolebinding.rbac.authorization.k8s.io/my-rolebinding created
Warning: resource pods/python-pod is missing the kubectl.kubernetes.io/last-applied-configuration annotation which is required by kubectl apply. kubectl apply should only be used on resources created declaratively by either kubectl create --save-config or kubectl apply. The missing annotation will be patched automatically.
The Pod "python-pod" is invalid: spec: Forbidden: pod updates may not change fields other than `spec.containers[*].image`,`spec.initContainers[*].image`,`spec.activeDeadlineSeconds`,`spec.tolerations` (only additions to existing tolerations),`spec.terminationGracePeriodSeconds` (allow it to be set to 1 if it was previously negative)
  core.PodSpec{
  	... // 7 identical fields
  	DNSPolicy:                    "ClusterFirst",
  	NodeSelector:                 nil,
- 	ServiceAccountName:           "default",
+ 	ServiceAccountName:           "my-service-account",
  	AutomountServiceAccountToken: nil,
  	NodeName:                     "worker2",
  	... // 21 identical fields
  }

# Role リソースへの create操作権限追加

```
一度python-podを再作成し適用したところ今度は、create操作権限が足りていないみたいです。最終的にRoleリソースを以下のように修正しました。

```yaml:role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: default
  name: my-role
rules:
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "create"]
```

初回作成は正常終了、再度実行した場合はpatch操作権限がなくエラーになるみたいです。

```bash
(myenv) root@python-pod:~# python apply_in_cluster.py 
Deployment does not exist. Creating...
Deployment created successfully.
(myenv) root@python-pod:~# python apply_in_cluster.py 
Deployment already exists, Updating...
Error: (403)
Reason: Forbidden
HTTP response headers: HTTPHeaderDict({'Audit-Id': '2f827d2b-d7e4-4d2e-9a75-d0015c7345b4', 'Cache-Control': 'no-cache, private', 'Content-Type': 'application/json', 'X-Content-Type-Options': 'nosniff', 'X-Kubernetes-Pf-Flowschema-Uid': '2094e53b-be81-4453-a7ce-d136df21c242', 'X-Kubernetes-Pf-Prioritylevel-Uid': 'e4dae82a-0dd7-4ac1-b0ef-4ad9e5a9b3a6', 'Date': 'Mon, 10 Mar 2025 11:29:33 GMT', 'Content-Length': '386'})
HTTP response body: {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"deployments.apps \"nginx-deployment\" is forbidden: User \"system:serviceaccount:default:my-service-account\" cannot patch resource \"deployments\" in API group \"apps\" in the namespace \"default\"","reason":"Forbidden","details":{"name":"nginx-deployment","group":"apps","kind":"deployments"},"code":403}


(myenv) root@python-pod:/tmp# 
```

# Client実行により作成された nginx Pod

Deploymentリソースnginx-deploymentがk8s上に反映されていることを確認できました。

```bash
$ kubectl get deployments,pods
NAME                               READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/nginx-deployment   3/3     3            3           3m40s

NAME                                    READY   STATUS        RESTARTS   AGE
pod/nginx-deployment-54b9c68f67-d7hlb   1/1     Running       0          3m40s
pod/nginx-deployment-54b9c68f67-dj2sj   1/1     Running       0          3m40s
pod/nginx-deployment-54b9c68f67-h2nh7   1/1     Running       0          3m40s
pod/nginx-deployment-54b9c68f67-ls9xv   1/1     Terminating   0          20h
pod/python-pod                          1/1     Running       0          19m
```

# 動作確認
手を抜いてServiceリソースを作成していないため、対象ノードから直接Podに割り当てられたIPをリクエストしての動作確認になります。

```bash
$ kubectl describe pods nginx-deployment-54b9c68f67-d7hlb | grep -e ^Node: -e ^IP:
Node:             worker2/192.168.11.51
IP:               10.244.223.166
$ ssh worker2
$ curl http://10.244.223.166
〜省略〜
<h1>Welcome to nginx!</h1>
〜省略〜
```

# おわりに
当初目的の通り、PodからDeploymentを作成することができました。