zaki work log

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

Ansibleのk8sモジュールでKubernetesクラスタ上のリソースを操作する

〇 2021.02.20: APIトークンを指定した方法について追記

Ansibleのk8sモジュールを使って、Kubernetes上のリソースを操作してみる。
意外といままで試してなかった…というか実は5月頃に途中まで試したけど当時はPython2環境でpipから入れたりして文書化に手間取ってお蔵入りしてた内容。

docs.ansible.com

環境

pip install ansibleしたAnsible 2.10環境

$ ansible --version
ansible 2.10.2
  config file = /home/zaki/src/ansible-sample/k8s/ansible.cfg
  configured module search path = ['/home/zaki/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/zaki/src/ansible-sample/venv/lib64/python3.6/site-packages/ansible
  executable location = /home/zaki/src/ansible-sample/venv/bin/ansible
  python version = 3.6.8 (default, Apr  2 2020, 13:34:55) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)]

KubernetesはローカルのDocker上に立てたkindKubernetes v1.18クラスタ

$ kubeconfig get node -o wide
NAME                         STATUS   ROLES    AGE    VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE       KERNEL-VERSION                CONTAINER-RUNTIME
helm-cluster-control-plane   Ready    master   7d1h   v1.18.2   172.20.0.2    <none>        Ubuntu 19.10   3.10.0-1127.19.1.el7.x86_64   containerd://1.3.3-14-g449e9269
helm-cluster-worker          Ready    <none>   7d1h   v1.18.2   172.20.0.5    <none>        Ubuntu 19.10   3.10.0-1127.19.1.el7.x86_64   containerd://1.3.3-14-g449e9269
helm-cluster-worker2         Ready    <none>   7d1h   v1.18.2   172.20.0.4    <none>        Ubuntu 19.10   3.10.0-1127.19.1.el7.x86_64   containerd://1.3.3-14-g449e9269

(pip) openshiftパッケージ

openshift(>=0.6)が必要なので入れる。

$ pip install openshift

これが無いとエラーになる。

k8sモジュール

Kubernetes認証情報有り環境

まずは、kubectl create namespaceとか実行できるユーザー&Kubernetes設定で、Ansibleも実行できるという条件で以下実行。
(ansible-playbookを実行するユーザーの環境変数$KUBECONFIG~/.kube/configの設定内容でKubernetesリソース操作が可能である、という条件)

namespace作成

- hosts: localhost
  gather_facts: no

  tasks:
  - name: Create a k8s namespace
    community.kubernetes.k8s:
      name: testing
      api_version: v1
      kind: Namespace
      state: present

このplaybookを使ってansible-playbookを実行。

(venv) [zaki@cloud-dev k8s]$ ansible-playbook playbook.yml 
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [localhost] **************************************************************

TASK [Create a k8s namespace] *************************************************
changed: [localhost]

PLAY RECAP ********************************************************************
localhost                  : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

すると、namespaceが作成される。

(venv) [zaki@cloud-dev k8s]$ kubectl get ns testing
NAME      STATUS   AGE
testing   Active   12s

再度ansible-playbookを実行すると

TASK [Create a k8s namespace] *************************************************
ok: [localhost]

ちゃんとokになる。

要は、以下のマニフェストapplyしたのと同じ状態。

apiVersion: v1
kind: Namespace
metadata:
  name: testing

srcを使ってマニフェストファイル指定でdeployment

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: sample-http
  name: sample-http
  namespace: testing
spec:
  replicas: 2
  selector:
    matchLabels:
      app: sample-http
  template:
    metadata:
      labels:
        app: sample-http
    spec:
      containers:
      - image: httpd
        name: httpd

こんな内容のマニフェストファイルが既にある状態で、このファイルのパスを指定して以下のplaybookを作成。
(前述のnamespace作成のplaybookに追記)

  - name: Create a Deployment by reading the definition from a local file
    community.kubernetes.k8s:
      state: present
      src: ./deployment.yaml
(venv) [zaki@cloud-dev k8s]$ ansible-playbook playbook.yml 
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [localhost] **************************************************************

TASK [Create a k8s namespace] *************************************************
ok: [localhost]

TASK [Create a Deployment by reading the definition from a local file] ********
changed: [localhost]

PLAY RECAP ********************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

これでDeploymentリソースが作られて(そこから更にReplicaSetが作られて)Podがデプロイされる。

(venv) [zaki@cloud-dev k8s]$ kubectl get pod -n testing 
NAME                           READY   STATUS    RESTARTS   AGE
sample-http-744f56bdc6-fgrdm   1/1     Running   0          9s
sample-http-744f56bdc6-m6qv5   1/1     Running   0          9s

動いたね。
再実行すると、ちゃんとokになります。

(追記)
上記の例はマニフェスト内にnamespaceを指定してるけど、マニフェストからはnamespace指定を無くし、k8sモジュールのnamespaceパラメタをplaybookに記述しても動く。

マニフェスト

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: sample-http
  name: sample-http
spec:
  replicas: 2

# ...

playbook

    community.kubernetes.k8s:
      state: present
      namespace: testing
      src: ./deployment.yaml

Jinja2テンプレートでマニフェスト生成

上記の例だと、例えばネームスペースは可変にしたいよなーとかあるかもしれません。
そういう既存マニフェストをそのままでなく、テンプレート的に使うには、AnsibleでおなじみJinja2が使えます。
k8sモジュールだとtemplateを使用。

deployment.yaml.j2というファイル名で、下記マニフェストファイルのj2テンプレートファイルを用意。

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: sample-http
  name: {{resourcename}}
  namespace: {{namespace}}
spec:
  replicas: {{replica}}
  selector:
    matchLabels:
      app: sample-http
  template:
    metadata:
      labels:
        app: sample-http
    spec:
      containers:
      - image: httpd
        name: httpd

Ansible用マニフェスト

  - name: Read definition template file from the Ansible controller file system
    vars:
      resourcename: tmpl-sample
      namespace: testing
      replica: 1
    community.kubernetes.k8s:
      state: present
      template: './deployment.yaml.j2'

このplaybookでansible-playbookを実行すると、tmpl-sampleという名前でDeploymentが作成されるので、Podの状態も以下の通りになる。

(venv) [zaki@cloud-dev k8s]$ kubectl get pod -n testing 
NAME                           READY   STATUS    RESTARTS   AGE
sample-http-744f56bdc6-fgrdm   1/1     Running   0          23m
sample-http-744f56bdc6-m6qv5   1/1     Running   0          23m
tmpl-sample-744f56bdc6-7nklj   1/1     Running   0          5s

resource_definitionでインライン定義

  - name: Create a Service object from an inline definition
    vars:
      namespace: testing
    community.kubernetes.k8s:
      state: present
      definition:
        apiVersion: v1
        kind: Service
        metadata:
          labels:
            app: sample-http
          name: sample-http
          namespace: "{{ namespace }}"
        spec:
          ports:
          - port: 80
            protocol: TCP
            targetPort: 80
            name: http
          selector:
            app: sample-http
          type: NodePort

マニフェストYAMLの内容をそのままPlaybookに書けばOK
Playbook内なので"{{ foobarbaz }}"というお馴染みの形式で変数参照も可能。

これでansible-playbookを実行すると

TASK [Create a Service object from an inline definition] **********************
changed: [localhost]

PLAY RECAP ********************************************************************
localhost                  : ok=4    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

この通り、Serviceリソースが作成できる。

(venv) [zaki@cloud-dev k8s]$ kubectl get svc -n testing 
NAME          TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
sample-http   NodePort   10.110.123.20   <none>        80:30965/TCP   6s

認証用kubeconfigの指定

CI/CDとか。
とりあえず、~/.kubeとか$KUBECONFIGとか設定されていないすっぴんOSユーザーを作って、そこで実行するための設定

[root@cloud-dev ~]# useradd -m sample-user
[root@cloud-dev ~]# passwd sample-user

要はこの状態のsample-userユーザーで前述のPlaybookと同じものを実行。

TASK [Create a k8s namespace] *************************************************
fatal: [localhost]: FAILED! => changed=false 
  msg: Failed to load kubeconfig due to Invalid kube-config file. No configuration found.

ただ実行するとこの通り。
Kubernetesクラスター情報が何もないので。

kubeconfigファイル指定

例えばkindであれば、kind get kubeconfig --name <kindクラスター名> > kubeconfig.ymlを実行すれば、認証情報であるkubeconfigファイルを生成できる。
このファイルを指定すれば.kube/config環境変数$KUBECONFIGが無くても動作する。

  tasks:
  - name: Create a k8s namespace
    community.kubernetes.k8s:
      name: testing
      api_version: v1
      kind: Namespace
      state: present
      kubeconfig: /home/sample-user/kubeconfig.yml

こんな感じ。

APIトークン指定

kubeconfig指定でなく、ca_certclient_certを個別に指定して動かせないか試したけど、ちょっとよくわからなかった。(実現可能かどうかも不明)

可能。
下記2つのパラメタを使用する。

  • api_key: Secretのtokenの値
  • ca_cert: Secretのca.crtの内容が保存されたファイルのパス

必要な準備の大まかな流れは、処理用のアカウント(通常はServiceAccountがよいと思う)を作成し、このアカウントに対して適切な(Ansibleで操作する内容に必要な)RoleBindigを行い、そのSecretトークンを使用する。

ServiceAccount作成

マニフェストで作ってもよいし、kubectlで作ってもよい。

$ kubectl create serviceaccount sample-account

Roleの作成

$ kubectl create role sample-role --verb=* --resource=*

※ これで作成するとapiGroupsが空文字1つを要素にもつRoleが作成されるが、この状態のRoleだとデプロイが成功しないため、kubectl edit"*"に変更した。

rules:
- apiGroups:
  - ""
  resources:
  - '*'
  verbs:
  - '*'

↑これを↓こうする

rules:
- apiGroups:
  - '*'           # <- ここ
  resources:
  - '*'
  verbs:
  - '*'

RoleBindig

作成したServiceAccountに作成したRoleを割り当てる。

$ kubectl create rolebinding sample-rolebinding --role=sample-role --serviceaccount=rbac-sample:sample-account

APIトークンの確認

Roleが設定されたServiceAccountのトークンを使用する。
トークンはSecretリソースに保持されている。

$ kubectl get secret   # secretリソース名を確認
$ kubectl get secret (ServiceAccount名)-token-(ランダム文字列) -o jsonpath='{.data.token}' | base64 -d

証明書の確認

トークンと同じ要領で確認。
ただしAnsibleのk8sモジュールからはファイルのパスで指定する必要があるので、リダイレクトでファイル出力する。

$ kubectl get secret (ServiceAccount名)-token-(ランダム文字列) -o jsonpath='{.data.ca\.crt}' | base64 -d > data/ca_crt.crt

absentで削除

ここまでstate: presentで試していたけど、absentを指定すれば削除動作になる。

- hosts: localhost
  gather_facts: no

  tasks:
  - name: delete namespace
    community.kubernetes.k8s:
      name: testing
      api_version: v1
      kind: Namespace
      state: absent

このPlaybookでansible-playbookを実行すれば

(venv) [zaki@cloud-dev k8s]$ kubectl get ns testing 
NAME      STATUS        AGE
testing   Terminating   129m

この通り削除実行が動き出す。


サンプルコード

github.com