zaki work log

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

kind on Podman on RHEL8でK8sを入れて、Bookinfo on Istioが動くことを確認

先日のContainer Runtime Meetup #3で「Podmanでkind動かせるよ」という話になり、どういうわけか「Podmanではkindは(まだ)動かせない」と思い込んでいたため、実際に試してみました。

runtime.connpass.com

(少し前にRHEL環境で仕事してて動作確認用のKubernetesクラスタが必要になった時に「RHELだとDocker使えなくてPodmanになるけどそうするとkind使えないのでマネージドK8s使いましょう!」なんて言ってしまってスミマセン)

ちなみに、Docker Composeについては、Podman ver3.0から使える見込みのようです。

環境

[zaki@rhel8 ~]$ uname -a
Linux rhel8 4.18.0-240.10.1.el8_3.x86_64 #1 SMP Wed Dec 16 03:30:52 EST 2020 x86_64 x86_64 x86_64 GNU/Linux
[zaki@rhel8 ~]$ cat /etc/redhat-release 
Red Hat Enterprise Linux release 8.3 (Ootpa)
  • Podman 2.0.5
  • kind 0.9.0
  • MetalLB 0.9.5
  • Istio 1.8.2

Podmanのインストール

[zaki@rhel8 ~]$ sudo dnf install podman
Updating Subscription Management repositories.
メタデータの期限切れの最終確認: 0:20:09 時間前の 2021年01月28日 08時17分37秒 に実施しました。
依存関係が解決しました。
==================================================================================================================================================
 パッケージ                         Arch          バージョン                                        リポジトリー                            サイズ
==================================================================================================================================================
インストール:
 podman                             x86_64        2.0.5-5.module+el8.3.0+8221+97165c3f              rhel-8-for-x86_64-appstream-rpms         13 M
依存関係のインストール:
 conmon                             x86_64        2:2.0.20-2.module+el8.3.0+8221+97165c3f           rhel-8-for-x86_64-appstream-rpms         49 k
 containernetworking-plugins        x86_64        0.8.6-2.module+el8.3.0+8221+97165c3f              rhel-8-for-x86_64-appstream-rpms         20 M
 containers-common                  x86_64        1:1.1.1-3.module+el8.3.0+8221+97165c3f            rhel-8-for-x86_64-appstream-rpms         66 k
 criu                               x86_64        3.14-2.module+el8.3.0+8221+97165c3f               rhel-8-for-x86_64-appstream-rpms        500 k
 fuse-overlayfs                     x86_64        1.1.2-3.module+el8.3.0+8221+97165c3f              rhel-8-for-x86_64-appstream-rpms         67 k
 fuse3-libs                         x86_64        3.2.1-12.el8                                      rhel-8-for-x86_64-baseos-rpms            94 k
 libnet                             x86_64        1.1.6-15.el8                                      rhel-8-for-x86_64-appstream-rpms         67 k
 libslirp                           x86_64        4.3.1-1.module+el8.3.0+8221+97165c3f              rhel-8-for-x86_64-appstream-rpms         69 k
 libvarlink                         x86_64        18-3.el8                                          rhel-8-for-x86_64-baseos-rpms            44 k
 podman-catatonit                   x86_64        2.0.5-5.module+el8.3.0+8221+97165c3f              rhel-8-for-x86_64-appstream-rpms        308 k
 protobuf-c                         x86_64        1.3.0-4.el8                                       rhel-8-for-x86_64-appstream-rpms         37 k
 runc                               x86_64        1.0.0-68.rc92.module+el8.3.0+8221+97165c3f        rhel-8-for-x86_64-appstream-rpms        3.7 M
 slirp4netns                        x86_64        1.1.4-2.module+el8.3.0+8221+97165c3f              rhel-8-for-x86_64-appstream-rpms         50 k
弱い依存関係のインストール:
 container-selinux                  noarch        2:2.144.0-1.module+el8.3.0+8221+97165c3f          rhel-8-for-x86_64-appstream-rpms         49 k
モジュールストリームの有効化中:
 container-tools                                  rhel8                                                                                          

トランザクションの概要
==================================================================================================================================================
インストール  15 パッケージ

ダウンロードサイズの合計: 39 M
インストール済みのサイズ: 133 M
これでよろしいですか? [y/N]: 
[zaki@rhel8 ~]$ podman version
Version:      2.0.5
API Version:  1
Go Version:   go1.14.7
Built:        Wed Sep 23 12:18:02 2020
OS/Arch:      linux/amd64

インストールされたので一応挨拶。

[zaki@rhel8 kind]$ podman ps -a
CONTAINER ID  IMAGE   COMMAND  CREATED  STATUS  PORTS   NAMES
[zaki@rhel8 kind]$ podman run hello-world
Trying to pull registry.access.redhat.com/hello-world...
  name unknown: Repo not found
Trying to pull registry.redhat.io/hello-world...
  unable to retrieve auth token: invalid username/password: unauthorized: Please login to the Red Hat Registry using your Customer Portal credentials. Further instructions can be found here: https://access.redhat.com/RegistryAuthentication
Trying to pull docker.io/library/hello-world...
Getting image source signatures
Copying blob 0e03bdcc26d7 done  
Copying config bf756fb1ae done  
Writing manifest to image destination
Storing signatures

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

[zaki@rhel8 kind]$ 
[zaki@rhel8 kind]$ podman ps -a
CONTAINER ID  IMAGE                                 COMMAND  CREATED         STATUS                     PORTS   NAMES
8847ed15d8b3  docker.io/library/hello-world:latest  /hello   30 seconds ago  Exited (0) 29 seconds ago          gracious_pare

何も考えずに実行したけど、root権限あるいはグループ設定無くても動くというか、この場合はrootlessモードで動作し、ホストOS上ではpodmanを実行しているユーザーの実行権限で動作する。

Podmanのrootlessモード

Podmanを使ってalpineコンテナでコマンドを実行した場合。

[zaki@rhel8 kind]$ podman run -it alpine sh
Trying to pull registry.access.redhat.com/alpine...
  name unknown: Repo not found
Trying to pull registry.redhat.io/alpine...
  unable to retrieve auth token: invalid username/password: unauthorized: Please login to the Red Hat Registry using your Customer Portal credentials. Further instructions can be found here: https://access.redhat.com/RegistryAuthentication
Trying to pull docker.io/library/alpine...
Getting image source signatures
Copying blob 4c0d98bf9879 done  
Copying config e50c909a8d done  
Writing manifest to image destination
Storing signatures
/ # id
uid=0(root) gid=0(root)
/ # ps
PID   USER     TIME  COMMAND
    1 root      0:00 sh
    7 root      0:00 ps
/ # tail -f /dev/null

コンテナ内ではuid=0で動いているが、ホストOS上でpsで確認すると、

[zaki@rhel8 kind]$ ps auxf
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND

...

zaki      405746  0.0  0.0 143748  2664 ?        Ssl  23:17   0:00 /usr/bin/conmon --api-version 1 -c d ...
zaki      405756  0.0  0.0   1660  1004 pts/0    Ss   23:17   0:00  \_ sh
zaki      406164  0.0  0.0   1584     4 pts/0    S+   23:20   0:00      \_ tail -f /dev/null

このように、実行ユーザー権限で動作している。

一方、同じことをDocker(v20.10.2)でやると、

(a2.10) [zaki@cloud-dev ~]$ docker run -it alpine sh
/ # id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
/ # tail -f /dev/null

コンテナ内ではPodmanの場合と同じようにuid=0で動いているが、ホストOS上でpsすると、

[zaki@cloud-dev ~]$ ps auxf 
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND

...

root     1699148  0.0  0.0 113244  4900 ?        Sl   13:27   0:00 /usr/bin/containerd-shim-runc-v2 -names ...
root     1699168  0.1  0.0   1636   512 pts/0    Ss   13:27   0:00  \_ sh
root     1699219  0.0  0.0   1560   244 pts/0    S+   13:27   0:00      \_ tail -f /dev/null

root権限で動作している。
(このホスト上では、実行ユーザーはdockerグループに所属させてsudoなしで実行できるように設定している)

kindのインストール

kind.sigs.k8s.io

CLIのインストール

kindのインストール自体は通常通り。

[zaki@rhel8 ~]$ mkdir -p local/kind
[zaki@rhel8 ~]$ cd $_
[zaki@rhel8 kind]$ 
[zaki@rhel8 kind]$ curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.9.0/kind-linux-amd64
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    97  100    97    0     0    243      0 --:--:-- --:--:-- --:--:--   243
100   642  100   642    0     0    825      0 --:--:-- --:--:-- --:--:--  1893
100 7247k  100 7247k    0     0   928k      0  0:00:07  0:00:07 --:--:-- 1408k
[zaki@rhel8 kind]$ chmod +x ./kind
[zaki@rhel8 kind]$ sudo mv ./kind /usr/local/bin/
[zaki@rhel8 kind]$ 
[zaki@rhel8 kind]$ kind version
kind v0.9.0 go1.15.2 linux/amd64

K8sクラスタのデプロイ

せっかくなのでマルチノードで。

[zaki@rhel8 kind]$ cat multinode.yaml 
# three node (two workers) cluster config
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker

これをパラメタに、

KIND_EXPERIMENTAL_PROVIDER=podman kind create cluster --config multinode.yamlを実行…できればよかったんだけど、

[zaki@rhel8 kind]$ KIND_EXPERIMENTAL_PROVIDER=podman kind create cluster --config multinode.yaml
using podman due to KIND_EXPERIMENTAL_PROVIDER
enabling experimental podman provider
Creating cluster "kind" ...
podman provider does not work properly in rootless mode

Podmanのrootlessモードだとkindは動作しない模様。。 ということで、「『kindを使う場合は』podman実行時にroot権限が必要」という解釈でいいのかな?sudo付けて実行してみる。

[zaki@rhel8 kind]$ sudo KIND_EXPERIMENTAL_PROVIDER=podman kind create cluster --config multinode.yaml
using podman due to KIND_EXPERIMENTAL_PROVIDER
enabling experimental podman provider
Creating cluster "kind" ...
 ✓ Ensuring node image (kindest/node:v1.19.1) 🖼
 ✓ Preparing nodes 📦 📦 📦  
 ✓ Writing configuration 📜 
 ✓ Starting control-plane 🕹️ 
 ✓ Installing CNI 🔌 
 ✓ Installing StorageClass 💾 
 ✓ Joining worker nodes 🚜 
Set kubectl context to "kind-kind"
You can now use your cluster with:

kubectl cluster-info --context kind-kind

Have a question, bug, or feature request? Let us know! https://kind.sigs.k8s.io/#community 🙂

OKです。 自動生成されるクレデンシャル情報ファイル(kubeconfigファイル)は、sudoでrootユーザーで動作した影響でrootの$HOME/.kube/configにあるので、通常ユーザーの$HOME/.kube/configにもクレデンシャル情報を作成する。

[zaki@rhel8 kind]$ mkdir ~/.kube
[zaki@rhel8 kind]$ sudo kind get kubeconfig > ~/.kube/config
enabling experimental podman provider

あ、これもしかしてKIND_EXPERIMENTAL_PROVIDER=podmanは設定しなくても自動認識するのかも?(未確認)

[zaki@rhel8 kind]$ kubectl get node -o wide
NAME                 STATUS   ROLES    AGE    VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE                                     KERNEL-VERSION                 CONTAINER-RUNTIME
kind-control-plane   Ready    master   2m3s   v1.19.1   10.88.0.12    <none>        Ubuntu Groovy Gorilla (development branch)   4.18.0-240.10.1.el8_3.x86_64   containerd://1.4.0
kind-worker          Ready    <none>   90s    v1.19.1   10.88.0.13    <none>        Ubuntu Groovy Gorilla (development branch)   4.18.0-240.10.1.el8_3.x86_64   containerd://1.4.0
kind-worker2         Ready    <none>   90s    v1.19.1   10.88.0.11    <none>        Ubuntu Groovy Gorilla (development branch)   4.18.0-240.10.1.el8_3.x86_64   containerd://1.4.0

できました。

Bookinfo

クラスターが動いてることの確認といえばWordPressあたりが妥当だけど、せっかく(何がせっかくなんだろう)なので、MetalLBとIstio入れてBookinfoをデプロイしてみる。
Istioの動作にMetalLB(type:LoadBalancer Service)は必須ではないけど、デフォルトでデプロイされるIngressGateway Serviceがtype:LoadBalancerに設定されてるので、用意されてるものをそのまま動くようにするために使う感じ。

MetalLB

metallb.universe.tf

以前もやってるし、手順は同じ。
ただし、2021.01.30時点で、バージョン v0.9.5 になってる。

まず本体のデプロイのため以下実行。

kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.9.5/manifests/namespace.yaml
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.9.5/manifests/metallb.yaml
# On first install only
kubectl create secret generic -n metallb-system memberlist --from-literal=secretkey="$(openssl rand -base64 128)"

次にtype:LoadBalancer Serviceに割り当てるIPアドレスの設定。

…さて、そういえばPodmanで動いてるコンテナのIPアドレスってどうなってるんだろう?

使用状況はDockerと同じくpodman inspectで確認できた。

[zaki@rhel8 kind]$ sudo podman ps --quiet | sudo xargs podman inspect -f "{{.Name}} {{.NetworkSettings.IPAddress }}"
kind-worker2         10.88.0.11
kind-worker          10.88.0.13
kind-control-plane   10.88.0.12

使用中のコンテナネットワークは以下。

[zaki@rhel8 kind]$ sudo podman network ls
NAME    VERSION  PLUGINS
podman  0.4.0    bridge,portmap,firewall,tuning

サブネット(sudo podman network inspect podmanの該当箇所)を取り出すと、

[zaki@rhel8 kind]$ sudo podman network inspect podman -f '{{(index (index (index .plugins 0).ipam.ranges 0) 0).subnet}}'
10.88.0.0/16

こんな感じ?
(Go Templateぜんぜんわからん。特に配列)

/16なんで第3オクテットを大きい値にしておけばよさげ。(適当です)

apiVersion: v1
kind: ConfigMap
metadata:
  namespace: metallb-system
  name: config
data:
  config: |
    address-pools:
    - name: default
      protocol: layer2
      addresses:
      - 10.88.250.10-10.88.250.20

この内容でConfigMap作成。

[zaki@rhel8 metallb]$ sudo kubectl apply -f config.yaml 
configmap/config created

これでtype:LoadBalancer Serviceをデプロイしたときに、ここで設定したIPアドレスが自動でExternal-IPに割り当てられる。

Istio

istio.io

Istioのインストールも以前やったときとほぼ同じ。
2021.01.30時点でバージョンは1.8.2になっている。

[zaki@rhel8 istio]$ curl -L https://istio.io/downloadIstio | sh -
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   102  100   102    0     0    228      0 --:--:-- --:--:-- --:--:--   228
100  4579  100  4579    0     0   5536      0 --:--:-- --:--:-- --:--:-- 37227

Downloading istio-1.8.2 from https://github.com/istio/istio/releases/download/1.8.2/istio-1.8.2-linux-amd64.tar.gz ...

Istio 1.8.2 Download Complete!

Istio has been successfully downloaded into the istio-1.8.2 folder on your system.

Next Steps:
See https://istio.io/latest/docs/setup/install/ to add Istio to your Kubernetes cluster.

To configure the istioctl client tool for your workstation,
add the /home/zaki/local/istio/istio-1.8.2/bin directory to your environment path variable with:
         export PATH="$PATH:/home/zaki/local/istio/istio-1.8.2/bin"

Begin the Istio pre-installation check by running:
         istioctl x precheck 

Need more information? Visit https://istio.io/latest/docs/setup/install/ 

istioctl/usr/local/binに配置。

[zaki@rhel8 istio]$ sudo cp -a istio-1.8.2/bin/istioctl /usr/local/bin/
[zaki@rhel8 istio]$ 
[zaki@rhel8 istio]$ istioctl version
no running Istio pods in "istio-system"
1.8.2

demoプロファイルでインストール。

[zaki@rhel8 istio]$ istioctl install --set profile=demo -y
Detected that your cluster does not support third party JWT authentication. Falling back to less secure first party JWT. See https://istio.io/v1.8/docs/ops/best-practices/security/#configure-third-party-service-account-tokens for details.
✔ Istio core installed
✔ Istiod installed
✔ Egress gateways installed
✔ Ingress gateways installed
✔ Installation complete
[zaki@rhel8 istio]$ kubectl get pod,svc -n istio-system 
NAME                                        READY   STATUS    RESTARTS   AGE
pod/istio-egressgateway-c9c55457b-tpj6k     1/1     Running   0          3m6s
pod/istio-ingressgateway-865d46c7f5-9gsc6   1/1     Running   0          3m6s
pod/istiod-7f785478df-svc62                 1/1     Running   0          3m39s

NAME                           TYPE           CLUSTER-IP      EXTERNAL-IP    PORT(S)                                                                      AGE
service/istio-egressgateway    ClusterIP      10.96.125.248   <none>         80/TCP,443/TCP,15443/TCP                                                     3m6s
service/istio-ingressgateway   LoadBalancer   10.96.221.246   10.88.250.10   15021:32424/TCP,80:30905/TCP,443:32177/TCP,31400:31323/TCP,15443:31192/TCP   3m6s
service/istiod                 ClusterIP      10.96.29.215    <none>         15010/TCP,15012/TCP,443/TCP,15014/TCP                                        3m38s

デプロイできました。
(type:LoadBalancerが設定されているIstio IngressGatewayも設定どおり10.88.250.*のアドレスが付いている)

Bookinfo本体

namespaceとlabel設定

[zaki@rhel8 istio]$ kubectl create namespace bookinfo
namespace/bookinfo created
[zaki@rhel8 istio]$ kubectl label namespaces bookinfo istio-injection=enabled
namespace/bookinfo labeled

Bookinfoのデプロイ

[zaki@rhel8 istio]$ kubectl apply -f istio-1.8.2/samples/bookinfo/platform/kube/bookinfo.yaml -n bookinfo 
service/details created
serviceaccount/bookinfo-details created
deployment.apps/details-v1 created
service/ratings created
serviceaccount/bookinfo-ratings created
deployment.apps/ratings-v1 created
service/reviews created
serviceaccount/bookinfo-reviews created
deployment.apps/reviews-v1 created
deployment.apps/reviews-v2 created
deployment.apps/reviews-v3 created
service/productpage created
serviceaccount/bookinfo-productpage created
deployment.apps/productpage-v1 created
[zaki@rhel8 istio]$ kubectl get pod -n bookinfo 
NAME                              READY   STATUS            RESTARTS   AGE
details-v1-79c697d759-kllz9       0/2     PodInitializing   0          25s
productpage-v1-65576bb7bf-w7h79   0/2     Init:0/1          0          24s
ratings-v1-7d99676f7f-kschh       0/2     Init:0/1          0          25s
reviews-v1-987d495c-59hh6         0/2     PodInitializing   0          25s
reviews-v2-6c5bf657cf-l6q49       0/2     PodInitializing   0          25s
reviews-v3-5f7b9f4f77-q2wnn       0/2     Init:0/1          0          25s

この通りデプロイが始まるのでしばらく待つ。
(数十秒レベルでは完了せず、5分近く時間かかった)

[zaki@rhel8 istio]$ kubectl get pod -n bookinfo 
NAME                              READY   STATUS            RESTARTS   AGE
details-v1-79c697d759-kllz9       2/2     Running           0          4m10s
productpage-v1-65576bb7bf-w7h79   0/2     PodInitializing   0          4m9s
ratings-v1-7d99676f7f-kschh       0/2     PodInitializing   0          4m10s
reviews-v1-987d495c-59hh6         2/2     Running           0          4m10s
reviews-v2-6c5bf657cf-l6q49       1/2     Running           0          4m10s
reviews-v3-5f7b9f4f77-q2wnn       0/2     PodInitializing   0          4m10s
[zaki@rhel8 istio]$ kubectl get pod -n bookinfo 
NAME                              READY   STATUS    RESTARTS   AGE
details-v1-79c697d759-kllz9       2/2     Running   0          5m2s
productpage-v1-65576bb7bf-w7h79   2/2     Running   0          5m1s
ratings-v1-7d99676f7f-kschh       2/2     Running   0          5m2s
reviews-v1-987d495c-59hh6         2/2     Running   0          5m2s
reviews-v2-6c5bf657cf-l6q49       2/2     Running   0          5m2s
reviews-v3-5f7b9f4f77-q2wnn       2/2     Running   0          5m2s

全Podが2/2でRunningになったのを確認。

外部アクセス設定(Gateway + VirtualService)

[zaki@rhel8 istio]$ kubectl apply -f istio-1.8.2/samples/bookinfo/networking/bookinfo-gateway.yaml -n bookinfo 
gateway.networking.istio.io/bookinfo-gateway created
virtualservice.networking.istio.io/bookinfo created

HTTPアクセス

[zaki@rhel8 istio]$ curl http://10.88.250.10/productpage
<!DOCTYPE html>
<html>
  <head>
    <title>Simple Bookstore App</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
:
:

アクセスOK

ただしこの10.88.0.0/16、Podmanのネットワーク上のアドレスなので、ホストOSの外部からはアクセスできない。
外からアクセスしたい場合、DockerやPodman単体であれば-pでポートをpublishすればポートフォワードされるけど、kindの場合はextraPortMappingsを使えば良い。
が、今回この設定は入れなかったので、sshのポートフォワードで回避。

ブラウザを使うローカルPCからkindを動かしているホスト(ここではrhel8というホスト)へ-L ローカルでlistenするポート:リモート上でアクセスするホスト:リモート上でアクセスするポートを追加してsshログイン。

PS C:\Users\zaki> ssh rhel8 -L 25080:10.88.250.10:80
Activate the web console with: systemctl enable --now cockpit.socket

This system is not registered to Red Hat Insights. See https://cloud.redhat.com/
To register this system, run: insights-client --register

Last login: Sat Jan 30 01:35:15 2021 from 192.168.0.10
[zaki@rhel8 ~]$

この状態で、ブラウザでhttp://localhost:25080/productpageにアクセス。
(パスの/productpageはBookinfoのVirtualServiceによるもの)

f:id:zaki-hmkc:20210130154909p:plain

関連情報

rheb.hatenablog.com

www.slideshare.net

qiita.com

rheb.hatenablog.com

medium.com

[NetBox / Ansible] ダイナミックインベントリを使ってNetBoxに登録されたホスト情報をターゲットノードにAnsibleを実行

Ansibleのダイナミックインベントリ機能を使って、NetBoxに登録しているホスト情報をAnsibleのターゲットホストとして利用してみる。
(ちなみにダイナミックインベントリ機能は書籍とかで存在は知ってはいたけど、実際に手を動かして使ってみるのが今回初めてという…)

NetBoxのdynamic inventory

docs.ansible.com

NetBoxのAPIを使ったダイナミックインベントリは最低限以下の通り。

plugin: netbox.netbox.nb_inventory
api_endpoint: http://192.168.0.19:28080
token: 0123456789abcdef0123456789abcdef01234567

まずはこれらが設定されていればOK

  • pluginの指定。
  • api_endpointにNetBoxのエンドポイント
  • tokenトーク

playbook

とりあえずallに対してpingモジュールを実行。

---
- name: dynamic inventory sample
  hosts: all
  gather_facts: false

  tasks:
    - name: ping
      ping:

実行

ここまでとても雑だけど、この内容でansible-playbookを実行するとこの通り。

(a2.10) [zaki@cloud-dev netbox (master)]$ ansible-playbook -i netbox-dynamic-inventory.yml netbox-dynamic-inventory-sample.yml  | cat

PLAY [dynamic inventory sample] ************************************************

TASK [ping] ********************************************************************
fatal: [esxi]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: Permission denied (publickey,keyboard-interactive).'
  unreachable: true
fatal: [instance-20200126-1250]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: Could not resolve hostname instance-20200126-1250: Name or service not known'
  unreachable: true
fatal: [instance-20200127-2223]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: Could not resolve hostname instance-20200127-2223: Name or service not known'
  unreachable: true
[DEPRECATION WARNING]: Distribution Ubuntu 18.04 on host cheddar should use 
/usr/bin/python3, but is using /usr/bin/python for backward compatibility with 
prior Ansible releases. A future Ansible release will default to using the 
discovered platform python for this host. See https://docs.ansible.com/ansible/
2.10/reference_appendices/interpreter_discovery.html for more information. This
 feature will be removed in version 2.12. Deprecation warnings can be disabled 
by setting deprecation_warnings=False in ansible.cfg.
ok: [cheddar]
ok: [cloud-dev]
fatal: [client-dev]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.17 port 22: No route to host'
  unreachable: true
fatal: [k8s-master01]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.121 port 22: No route to host'
  unreachable: true
fatal: [k8s-master02]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.122 port 22: No route to host'
  unreachable: true
ok: [kubernetes-master]
ok: [kubernetes-worker0]
ok: [kubernetes-worker1]
ok: [manager]
fatal: [k8s-master03]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.123 port 22: No route to host'
  unreachable: true
ok: [manager-dev]
ok: [registry]
fatal: [k8s-worker01]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.125 port 22: No route to host'
  unreachable: true
ok: [rhel8]
fatal: [k8s-worker02]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.126 port 22: No route to host'
  unreachable: true
fatal: [rhel7]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.26 port 22: No route to host'
  unreachable: true
fatal: [wensley]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.10 port 22: Connection timed out'
  unreachable: true

PLAY RECAP *********************************************************************
cheddar                    : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
client-dev                 : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
cloud-dev                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
esxi                       : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
instance-20200126-1250     : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
instance-20200127-2223     : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
k8s-master01               : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
k8s-master02               : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
k8s-master03               : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
k8s-worker01               : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
k8s-worker02               : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
kubernetes-master          : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
kubernetes-worker0         : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
kubernetes-worker1         : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
manager                    : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
manager-dev                : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
registry                   : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
rhel7                      : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
rhel8                      : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
wensley                    : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   

色が付いてないので見づらいけど、okになっている(疎通の確認ができた)ホストは以下の通り。

f:id:zaki-hmkc:20210127222205p:plain

電源入れてるVMは以下の通り。 あとcheddarVMでなく物理のサーバーで別途動いている。

f:id:zaki-hmkc:20210127222052p:plain

これはNetBoxに情報登録されている全てのDevicesと全てのVirtual Machinesの情報がターゲットホストになるように、Ansibleのダイナミックインベントリ機能でホスト情報が取得できている。

f:id:zaki-hmkc:20210127221943p:plain

f:id:zaki-hmkc:20210127222017p:plain

ちなみに、NetBoxにIPアドレスが設定されていないデバイスまたはVMの場合は、登録しているNameが名前解決できれば(あるいは$HOME/.ssh/configに定義されていれば)アクセスできる。
要は、NetBoxに登録しているNameの内容でインベントリファイルが構成されるのと同等になる。 IPアドレスが付与されていればansible_host=でアドレスが設定されている状態。
(動きから考えておそらく)

allの場合の対象ホストを確認

allに対するpingモジュール実行時のPLAY RECAPに出てるので明確だけど、追試。
以下のタスクを追加して実行。

    - name: print ansible_play_hosts_all
      debug:
        msg: "{{ ansible_play_hosts_all }}"
      run_once: true

出力結果はこの通り。

TASK [print ansible_play_hosts_all] *******************************************
ok: [cheddar] => 
  msg:
  - cheddar
  - esxi
  - wensley
  - client-dev
  - cloud-dev
  - instance-20200126-1250
  - instance-20200127-2223
  - k8s-master01
  - k8s-master02
  - k8s-master03
  - k8s-worker01
  - k8s-worker02
  - kubernetes-master
  - kubernetes-worker0
  - kubernetes-worker1
  - manager
  - manager-dev
  - registry
  - rhel7
  - rhel8

ansible_play_hosts_allについては以下参照。

docs.ansible.com

ホストのグループ指定の設定

実際にはNetBoxに登録した全てのホストに同じ処理をすることはそうそう無く、「登録ホストのうち特定条件の一部のホスト群」という使い方をするはず。
例えば「〇〇Clusterのホストのうち、Roleに□□が設定されているもの」という具合。

まずインベントリファイルにgroup_byパラメタを追加。

plugin: netbox.netbox.nb_inventory
api_endpoint: http://192.168.0.19:28080
token: 0123456789abcdef0123456789abcdef01234567

group_by:
  - cluster

これで、Clusters情報を使ったホストのグルーピングが可能になる。
ただ、プレイブックのhostsにどんなグループ名を指定すれば良いかが、ドキュメントをざっと見渡しても書き方やサンプルが載っていない。

そこで(ググった結果、金魚の人が1年以上前に実施していたやり方が見つかり)ansible-inventory --listを使って指定のインベントリファイルだとグループ情報がどのように定義されているか確認。

(a2.10) [zaki@cloud-dev netbox (master)]$ ansible-inventory -i netbox-dynamic-inventory.yml --list
[WARNING]: Invalid characters were found in group names but not replaced, use -vvvv to see details
{
    "_meta": {
        "hostvars": {
            ..snip..
        }
    },
    "all": {
        "children": [
            "cluster_oci always free",
            "cluster_private-network-1",
            "cluster_vm network",
            "ungrouped"
        ]
    },
    "cluster_oci always free": {
        "hosts": [
            "instance-20200126-1250",
            "instance-20200127-2223"
        ]
    },
    "cluster_private-network-1": {
        "hosts": [
            "zzz"
        ]
    },
    "cluster_vm network": {
        "hosts": [
            "client-dev",
            "cloud-dev",
            "k8s-master01",
            "k8s-master02",
            "k8s-master03",
            "k8s-worker01",
            "k8s-worker02",
            "kubernetes-master",
            "kubernetes-worker0",
            "kubernetes-worker1",
            "manager",
            "manager-dev",
            "registry",
            "rhel7",
            "rhel8"
        ]
    },
    "ungrouped": {
        "hosts": [
            "cheddar",
            "esxi",
            "wensley"
        ]
    }
}
(a2.10) [zaki@cloud-dev netbox (master)]$ 

これでグループ名が判明……ん、、、

    "cluster_vm network": {

cluster_vm network って、、、

これはもしや、、、

グループ名のスペースは不可

---
- name: dynamic inventory sample
  hosts: 'cluster_vm network'
  gather_facts: false

みたいに指定しても、、、

[WARNING]: Invalid characters were found in group names but not replaced, use -vvvv to see details
[WARNING]: Could not match supplied host pattern, ignoring: cluster_vm
[WARNING]: Could not match supplied host pattern, ignoring: network

PLAY [dynamic inventory sample] ***********************************************
skipping: no hosts matched

REST使ってるのでワンチャンいけるかな、と、+とか%20とか指定してみても、、、

[WARNING]: Invalid characters were found in group names but not replaced, use -vvvv to see details
[WARNING]: Could not match supplied host pattern, ignoring: cluster_vm+network

PLAY [dynamic inventory sample] ***********************************************
skipping: no hosts matched
[WARNING]: Could not match supplied host pattern, ignoring: cluster_vm%20network

PLAY [dynamic inventory sample] ***********************************************
skipping: no hosts matched

はい、グループ名にスペースが入るためAnsibleからは指定できない。

グループ名にスペースまたはハイフンは使用しないでください。

インベントリーの構築 — Ansible Documentation

dynamic inventoryのグループ名

気を取り直し、NetBox側でCluster名のスペースを_に変更。
ちなみにDevice RolesやSitesのように、Slugを指定できるものは、そちらがスペース等入ってないようになっていればOK (Slugで指定したものがキーになる)

f:id:zaki-hmkc:20210128002443p:plain

これで再度ansible-inventory -i netbox-dynamic-inventory.yml --listを確認すれば、

    "all": {
        "children": [
            "cluster_oci_always_free",
            "cluster_private_network_1",
            "cluster_vm_network",
            "ungrouped"
        ]
    },
    "cluster_oci_always_free": {
        "hosts": [
            "instance-20200126-1250",
            "instance-20200127-2223"
        ]
    },
    "cluster_vm_network": {
        "hosts": [
            "client-dev",
            "cloud-dev",
            "k8s-master01",
            "k8s-master02",
            "k8s-master03",
            "k8s-worker01",
            "k8s-worker02",
            "kubernetes-master",
            "kubernetes-worker0",
            "kubernetes-worker1",
            "manager",
            "manager-dev",
            "registry",
            "rhel7",
            "rhel8"
        ]
    },
    "ungrouped": {
        "hosts": [
            "cheddar",
            "esxi",
            "wensley"
        ]
    }

この通り。(一部省略)

例えばcluster_vm_networkをグループに指定すれば、ターゲットノードはClusterがvm_networkのホストのみになる。
hostsに指定するグループの凡例は「"group_byで指定した要素のキー名" + _ + "指定した要素の値"」

---
- name: dynamic inventory sample
  hosts: cluster_vm_network
  gather_facts: false

  tasks:
    - name: ping
      ping:

このようにターゲットノードのグループ名を以下のように設定してansible-playbookを実行すると、

(a2.10) [zaki@cloud-dev netbox (master)]$ ansible-playbook -i netbox-dynamic-inventory.yml netbox-dynamic-inventory-sample.yml -v
Using /home/zaki/src/ansible-sample/netbox/ansible.cfg as config file
Fetching: http://192.168.0.19:28080/api/docs/?format=openapi
Fetching: http://192.168.0.19:28080/api/dcim/devices/?limit=0&exclude=config_context
Fetching: http://192.168.0.19:28080/api/virtualization/virtual-machines/?limit=0&exclude=config_context
Fetching: http://192.168.0.19:28080/api/dcim/sites/?limit=0
Fetching: http://192.168.0.19:28080/api/dcim/regions/?limit=0
Fetching: http://192.168.0.19:28080/api/tenancy/tenants/?limit=0
Fetching: http://192.168.0.19:28080/api/dcim/racks/?limit=0
Fetching: http://192.168.0.19:28080/api/dcim/rack-groups/?limit=0
Fetching: http://192.168.0.19:28080/api/dcim/device-roles/?limit=0
Fetching: http://192.168.0.19:28080/api/dcim/device-types/?limit=0
Fetching: http://192.168.0.19:28080/api/dcim/platforms/?limit=0
Fetching: http://192.168.0.19:28080/api/dcim/manufacturers/?limit=0
Fetching: http://192.168.0.19:28080/api/virtualization/clusters/?limit=0
Fetching: http://192.168.0.19:28080/api/ipam/services/?limit=0

PLAY [dynamic inventory sample] ***********************************************

TASK [ping] *******************************************************************
ok: [cloud-dev] => changed=false 
  ansible_facts:
    discovered_interpreter_python: /usr/bin/python
  ping: pong
fatal: [client-dev]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.17 port 22: No route to host'
  unreachable: true
fatal: [k8s-master01]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.121 port 22: No route to host'
  unreachable: true
fatal: [k8s-master02]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.122 port 22: No route to host'
  unreachable: true
fatal: [k8s-master03]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.123 port 22: No route to host'
  unreachable: true
fatal: [k8s-worker01]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.125 port 22: No route to host'
  unreachable: true
ok: [manager] => changed=false 
  ansible_facts:
    discovered_interpreter_python: /usr/bin/python
  ping: pong
ok: [manager-dev] => changed=false 
  ansible_facts:
    discovered_interpreter_python: /usr/bin/python
  ping: pong
fatal: [k8s-worker02]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.126 port 22: No route to host'
  unreachable: true
fatal: [kubernetes-master]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.131 port 22: No route to host'
  unreachable: true
fatal: [kubernetes-worker0]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.135 port 22: No route to host'
  unreachable: true
fatal: [kubernetes-worker1]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.136 port 22: No route to host'
  unreachable: true
ok: [rhel8] => changed=false 
  ansible_facts:
    discovered_interpreter_python: /usr/libexec/platform-python
  ping: pong
fatal: [registry]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.21 port 22: No route to host'
  unreachable: true
fatal: [rhel7]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.26 port 22: No route to host'
  unreachable: true

PLAY RECAP ********************************************************************
client-dev                 : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
cloud-dev                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
k8s-master01               : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
k8s-master02               : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
k8s-master03               : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
k8s-worker01               : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
k8s-worker02               : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
kubernetes-master          : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
kubernetes-worker0         : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
kubernetes-worker1         : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
manager                    : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
manager-dev                : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
registry                   : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
rhel7                      : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
rhel8                      : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

(a2.10) [zaki@cloud-dev netbox (master)]$ 

この通り、Clusterがvm_networkのホストのみが処理対象となった。
(Clusterがoci_always_freeのホストが除外されている)

グループの複合条件

例えばcluster = vm_networkかつdevice_role = serverのホストをターゲットにしたい場合、cluster_vm_networkかつdevice_roles_serverとなる。
複数グループのandでターゲットホストを指定するには:&で繋げばOK

---
- name: dynamic inventory sample
  hosts: cluster_vm_network:&device_roles_server
  gather_facts: false

  tasks:
    - name: ping
      ping:

このターゲットホストの内容で実行すると、

(a2.10) [zaki@cloud-dev netbox (master)]$ ansible-playbook -i netbox-dynamic-inventory.yml netbox-dynamic-inventory-sample.yml -v

(中略)

PLAY RECAP ********************************************************************
manager                    : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
manager-dev                : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
registry                   : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   

この通り。

ORや否定の場合は以下の通り。

記述 内容
group1:group2 group1のホスト + group2のホスト (OR)
group1:!group2 group1のホストのうちgroup2には含まれないホスト
group1:&group2 group1のホストのうちgroup2にも含まれるホスト (AND)

docs.ansible.com


まとめ

NetBoxのダイナミックインベントリを使うことで、NetBoxに登録しているホスト情報をAnsibleのターゲットノードにすることを確認。

今回はClustersとDevice Rolesをフィルタに使ってみたが、group_byに指定できれば他にもtagtenantなどいろいろキーにして、自由にホスト情報をNetBoxから取り出すことができる。

E×celのIPアドレス管理台帳だと、こういったツール間連携は絶望的なので大抵の場合では手書きでアドレス情報を書き写したりする必要があったが、NetBoxとAPIを使って自動化に組み込むことが容易になる。


これまで、

とやってきて、今回はNetBoxをインプットとしてAnsibleを実行してみました。
どうでしょうか?
そろそろNetBoxを使ってみたくなったのではないでしょうか?(ニッコリ


環境

  • NetBox: 2.10.3 (on Docker Compose)
  • Ansible: 2.10.2 (Python3 on CentOS 7)
  • netbox.netbox collection: 1.1.0

参考情報

netbox-ansible-collection.readthedocs.io

docs.ansible.com

tekunabe.hatenablog.jp

VS CodeのRemote Development SSHのサーバー側のプロセスを再起動する

Ctrl + Shift + p(winの場合)でコマンドパレットを出して「Remote-SSH: Kill VS Code Server on Host」を選択し、プロセスをkillする対象サーバーを指定する。

f:id:zaki-hmkc:20210126223848p:plain

実行するとどうなる

サーバー側のVS Codeプロセスが終了するため、一度VS Codeがリモートから切断される。
再度接続すればサーバー側のVS Codeプロセスが起動するため、また普通にRemote Developmentが使用可能。

f:id:zaki-hmkc:20210126223927p:plain

使いどころ

ユーザー情報を更新した場合

通常はこちら。

たとえばユーザーのグループ設定を変更(sudo usermod -aG docker $USERでグループ設定を更新した、とか)した場合、通常は新しくログインし直したシェルでなければ更新されたグループ情報は反映されない。

しかし、最近のVS CodeのRemote Development SSHは「VS Codeを終了」したり「リモートサーバーから切断」しても、サーバー上ではVS Code Serverプロセスが動き続けており、再度VS Codeから接続しても、動き続けていたVS Code Serverプロセスに再接続する形になるため、「再ログインすることで適用される設定」が反映されない。
平たく言うと切断してもscreentmuxのデタッチに似た状態になるため、再接続するとアタッチされて前の状態が継続される。

こういうときは「Kill VS Code Server on Host」を使うことで、古い情報を持っているServerプロセスを停止し、再接続することで新しい情報で接続できる。

接続先サーバーのOSを再起動してもよい。

原因は不明だがサーバーがハングってしまってとりあえず再起動したい

今日遭遇したのはこちら。

特定のPCのVS Codeで特定のホスト+ディレクトリに接続しようとするとなぜかVS Codeのウインドウが固まってしまい、そのままoomで死ぬという状況。

f:id:zaki-hmkc:20210126224345p:plain

f:id:zaki-hmkc:20210126224242p:plain

ちょっとどうにもならなかったので(OSリブートでもよかったんだけど)とりあえずVS Code Serverをkillしてみたら復旧した。


原因究明できなくて「とりあえず再起動したら治った」ってやつです…

[NetBox] AnsibleのNetBoxモジュールを使ってVMやIPアドレス情報を登録

以前REST APIを使った入力を試したけど、今回はAnsibleのNetboxモジュールを使った入力について。

docs.ansible.com

準備

pynetbox のインストール

NetBoxをAnsibleから操作する場合は、基本的に(全部チェックしたわけじゃないけどたぶん)全てpynetboxモジュールが必要。
pipでインストールする。

$ pip install pynetbox

無いとエラー。

TASK [create vm sample] *******************************************************
An exception occurred during task execution. To see the full traceback, use -vvv. The error was: ModuleNotFoundError: No module named 'pynetbox'
failed: [localhost] (item=k8s-master01) => changed=false 
  ansible_loop_var: item
  item: k8s-master01
  msg: Failed to import the required Python library (pynetbox) on cloud-dev's Python /home/zaki/src/ansible-sample/venv/a2.10/bin/python3. 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

netbox.netboxコレクション

Ansible 2.10のフルセット(pip install ansibleでインストールした場合)はNetBoxのコレクションが含まれるので追加インストールは不要。
Ansible 2.10をansible-baseでインストールした場合、また、Ansible 2.9の場合は、netbox.netboxコレクションが追加で必要なのでansible-galaxyでインストールする。

ansible-galaxy collection install netbox.netbox

galaxy.ansible.com

Playbookサンプル

全部をチェックしたわけじゃないけど、VMの作成を試してみた限りでは「REST APIをAnsibleモジュールでラッピングしている」感じ。
なので、RESTを使った操作をチェックすればAnsibleモジュールの使い方もそのまま行ける。

ただ、RESTと違ってIDでなくVM名とかをそのまま使うことができてわかりやすい。
また、既にある場合はokとなって冪等な処理になり、VM名はそのままで例えばstatusを変更すれば「更新」動作になるので使い勝手は良い。

APIエンドポイントとトーク

各モジュールで共通して以下のパラメタが用意されている。

  • netbox_url
  • netbox_token

書いてみればすぐわかるけど、さらにdataといパラメタに、モジュールに対するパラメタ(つまりNetBoxへ操作を行いたいパラメタ)を指定していく。
基本的にNetBoxのweb上の画面とモジュールのドキュメントのExampleを見比べれば使い方は雰囲気でわかるはず。

VM作成

docs.ansible.com

    - name: create vm sample
      netbox.netbox.netbox_virtual_machine:
        netbox_url: "{{ netbox_url }}"
        netbox_token: "{{ netbox_token }}"
        data:
          name: "{{ item }}"
          cluster: vm network
      with_items:
        - k8s-master01

RESTだと必須だったclusterはドキュメントにはRequired設定されてないが、やっぱり必須。

clusterを指定しなかったらこの通り。

TASK [create vm sample] *******************************************************
failed: [localhost] (item=k8s-master01) => changed=false 
  ansible_loop_var: item
  item: k8s-master01
  msg: '{"cluster":["This field is required."]}'

clusterを指定(RESTの場合と異なり、clusterのIDでなくcluster名の文字列を指定)して実行すればこの通り。

(a2.10) [zaki@cloud-dev netbox (master)]$ ansible-playbook sample.yml -v
Using /home/zaki/src/ansible-sample/netbox/ansible.cfg as config file
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

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

TASK [create vm sample] *******************************************************
changed: [localhost] => (item=k8s-master01) => changed=true 
  ansible_loop_var: item
  item: k8s-master01
  msg: virtual_machine k8s-master01 created
  virtual_machine:
    cluster: 3
    comments: ''
    config_context: {}
    created: '2021-01-13'
    custom_fields: {}
    disk: null
    id: 7
    last_updated: '2021-01-13T13:54:24.961868Z'
    local_context_data: null
    memory: null
    name: k8s-master01
    platform: null
    primary_ip: null
    primary_ip4: null
    primary_ip6: null
    role: null
    site: 1
    status: active
    tags: []
    tenant: null
    url: http://192.168.0.19:28080/api/virtualization/virtual-machines/7/
    vcpus: null

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

VM情報が登録されたのち、同じPlaybookで再実行するとokになり変化しない。

(a2.10) [zaki@cloud-dev netbox (master)]$ ansible-playbook sample.yml -v
Using /home/zaki/src/ansible-sample/netbox/ansible.cfg as config file
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

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

TASK [create vm sample] *******************************************************
ok: [localhost] => (item=k8s-master01) => changed=false 
  ansible_loop_var: item
  item: k8s-master01
  msg: virtual_machine k8s-master01 already exists
  virtual_machine:
    cluster: 3
    comments: ''
    config_context: {}
    created: '2021-01-13'
    custom_fields: {}
    disk: null
    id: 7
    last_updated: '2021-01-13T13:54:24.961868Z'
    local_context_data: null
    memory: null
    name: k8s-master01
    platform: null
    primary_ip: null
    primary_ip4: null
    primary_ip6: null
    role: null
    site: 1
    status: active
    tags: []
    tenant: null
    url: http://192.168.0.19:28080/api/virtualization/virtual-machines/7/
    vcpus: null

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

(a2.10) [zaki@cloud-dev netbox (master)]$ 

以下についてもRESTのときとまったく同じ要領。
ただし前述の通り、IDでなく名前を指定する。

インベントリ変数やvarsなどで変数化しておけば、Primary設定のための「VM情報の更新」を行うnetbox_virtual_machineモジュール再実行も上記リンク先のサンプルのように簡単にできるはず。

Clusterの作成

せっかくなのでVM作成のパラメタのClusterも作成してみる。
Clusterの作成にはCluster Typeが必須なので、これもセットで作る。

作成に使用するパラメタをYAMLで変数定義しておく。

    cluster: vm network
    cluster_type:
      name: ESXi
      slug: esxi

これに対して、「Cluster Type作成 (netbox_cluster_type)」と「Cluster作成 (netbox_cluster)」が順に実行されるようにplaybookを書けばOK。

    - name: create cluster-type
      netbox.netbox.netbox_cluster_type:
        netbox_url: "{{ netbox_url }}"
        netbox_token: "{{ netbox_token }}"
        data:
          name: "{{ cluster_type.name }}"
          slug: "{{ cluster_type.slug }}"

    - name: create cluster
      netbox.netbox.netbox_cluster:
        netbox_url: "{{ netbox_url }}"
        netbox_token: "{{ netbox_token }}"
        data:
          name: "{{ cluster }}"
          cluster_type: "{{ cluster_type }}"

こんな感じ。

環境

NetBoxのバージョンはv2.10.3 (構築時と同じ)

Ansibleは以下の2パターンで確認。

Ansible 2.10

ansible 2.10.2
  config file = /home/zaki/src/ansible-sample/netbox/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/a2.10/lib64/python3.6/site-packages/ansible
  executable location = /home/zaki/src/ansible-sample/venv/a2.10/bin/ansible
  python version = 3.6.8 (default, Nov 16 2020, 16:55:22) [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)]
  • pynetbox: 5.3.0
  • netbox.netbox collections: 1.1.0

Ansible 2.9

ansible 2.9.16
  config file = /home/zaki/src/ansible-sample/netbox/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/2.9/lib64/python3.6/site-packages/ansible
  executable location = /home/zaki/src/ansible-sample/venv/2.9/bin/ansible
  python version = 3.6.8 (default, Nov 16 2020, 16:55:22) [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)]
  • pynetbox: 5.3.1
  • netbox.netbox collections: 2.0.0

あれ?いつのまにか2.0.0になってた。

サンプルコード

github.com


今回は「基本的に」が多いな。。触ってみたモジュールはだいたい同じ使い勝手だったけど、使い方が全く違うものが有るかもしれないし無いかもしれない(^^;


RESTを使った場合はこちら。

zaki-hmkc.hatenablog.com

[Ansible] 複数roleのhandlerとpost_tasks/pre_tasksの処理順序

特定のtaskに変更があったときだけ連動して動く処理を定義できるhandlerですが、処理の実行順序で思い違いしがちなところがあったので確認してみました。
(handlerを使ったroleがあるのを忘れて無邪気に次に処理されるroleを書き足したら期待しない実行順序になって処理がコケたというヤラカシがあったのでまとめ)

notify/handlerそのものについてはこちら参照。

zaki-hmkc.hatenablog.com

handlerを実装したrole

.
├── playbook.yml
└── roles
    ├── sample1
    │   ├── handlers
    │   │   └── main.yml
    │   └── tasks
    │       └── main.yml
    └── sample2
        └── tasks
            └── main.yml

roleは以下の通り。

  • sample1: taskshandlersを実装(taskの結果によってhandlerの中身が起動する)
  • sample2: tasksのみ

playbookは以下の通りで、sample1 -> sample2 の順に実行。

---
- hosts: localhost
  gather_facts: false

  roles:
  - sample1
  - sample2

ディレクトリ構造のイメージから、

  1. sample1のtask
  2. sample1のhandler
  3. sample2

の順序で実行されそう(roles/sample1で定義された全処理が終わったらroles/sample2)だけど、実際は、

  1. sample1のtask
  2. sample2のtask
  3. sample1のhandler

となり、「全rolesのtaskが処理されてからhandler」の順序で処理されます。

github.com

post_tasks

じゃあ「とあるrole/taskでhandlerを使いつつ、その処理後に通常のtask処理を行いたい」場合にどうすればよいかというと、post_tasksを使います。

  post_tasks:
  - name: this is post_tasks
    debug:
      msg: gochisousama desita.

  roles:
  - sample1
  - sample2

post_tasksに記述したtaskは、playbook内の記述順に関係なく、rolesの後に処理されます。

  1. roles/sample1のtask
  2. roles/sample2のtask
  3. roles/sample1のhandler
  4. post_tasksのtask

※ ↑のplaybookは処理順が決まってることを確認するためにpost_tasksを最初に書いているけど、実際は最後に書いた方が直観的に処理順序が分かりやすい。

pre_tasks

postがあるならpreもある。

  post_tasks:
  - name: this is post_tasks
    debug:
      msg: gochisousama desita.

  roles:
  - sample1
  - sample2

  pre_tasks:
  - name: this is pre_tasks
    debug:
      msg: itadakimasu

post_tasksと同じように、playbook内の記述順に関係なく、rolesの前に処理されます。

  1. pre_tasksのtask
  2. roles/sample1のtask
  3. roles/sample2のtask
  4. roles/sample1のhandler
  5. post_tasksのtask

※ ↑のplaybookは処理順が決まってることを確認するためにpost_tasksの例同様pre_tasksを最後に書いているけど(ry

post_tasks/pre_tasksでhandler使用

  post_tasks:
  - name: this is post_tasks
    debug:
      msg: gochisousama desita.
  - name: exec at post_task
    command: ls
    notify: post_notify

  roles:
  - sample1
  - sample2

  handlers:
  - name: pre_notify
    debug:
      msg: handler at pre_notify
    listen: pre_notify
  - name: post_notify
    debug:
      msg: handler at post_notify
    listen: post_notify

  post_tasks:
  - name: this is post_tasks
    debug:
      msg: gochisousama desita.
  - name: exec at pre_task
    command: ls
    notify: pre_notify

これはrolesのディレクトリ構造の勘違いと違って、

  1. pre_tasksのtask
  2. pre_tasksのhandler (ここでpre_tasksの全処理が完了)
  3. rolesのsample1, sample2, rolesのhandler (ここでrolesの全処理が完了)
  4. post_tasksのtask
  5. post_tasksのhandler

「一連のtaskの中で複数回notifyされてもhandlerのtaskが処理されるのは1回」が基本動作だけど、post_taskspre_tasksのhandlerはそれぞれpost_tasksのhandler、pre_tasksのhandlerとして独立して1回ずつ処理されます。

post_tasks/pre_tasksのnotify先が同じtaskの場合

これは、handlerでnotifyで通知されたtaskはpost_tasks/pre_tasksそれぞれで処理されます。
tasksを追加しても同様に個別に処理されます。 「post_tasks内で複数回notifyされても処理は1回」だけど「post_tasks,pre_tasksそれぞれでnotifyされた場合は、それぞれで処理される」(テキストだと説明難しいな。。)

  post_tasks:
  - name: exec at post_task
    command: ls
    notify: post_notify

  pre_tasks:
  - name: exec at pre_task
    command: ls
    notify: pre_notify

  tasks:
  - name: exec at task
    command: ls
    notify: task_notify

  handlers:
  - name: handler_at_task
    debug:
      msg: handler at task
    listen: task_notify
  - name: pre_notify
    debug:
      msg: handler at pre_notify
    listen: pre_notify
  - name: post_notify
    debug:
      msg: handler at post_notify
    listen: post_notify
  - name: all notify task
    debug:
      msg: curry!
    listen:
    - task_notify
    - pre_notify
    - post_notify

handlersセクションがちょっとゴチャゴチャしてるけど、この場合の処理順序は以下の通り。

  1. pre_tasksのtask
  2. pre_tasksのhandlerでpre_notify
  3. pre_tasksのhandlerでall notify task
  4. tasks
  5. tasksのhandlerでhandler_at_task
  6. tasksのhandlerでall notify task
  7. post_tasksのtask
  8. post_tasksのhandlerでpre_notify
  9. post_tasksのhandlerでall notify task

(参考) tasksとrolesの併用

例題でよく出てくると思うけど、playの中でtasksrolesを両方定義した場合の処理順序。

  tasks:
  - name: this task by "tasks" in playbook
    debug:
      msg: hello
  - name: exec at task
    command: ls
    notify: task_notify

  roles:
  - sample1
  - sample2

  handlers:
  - name: handler_at_task
    debug:
      msg: handler at task
    listen: task_notify

この場合は、記述順に関係なく、

  1. rolesの全てのtasks
  2. playの全てのtasks
  3. handlers
    • rolesのhandlers
    • playのhandlers

の順序。

github.com

(参考) tasks/roles/pre_tasks/post_tasksの併用

roles+tasksにさらにpre_tasks/post_tasksを組み込むとroles/tasksの前後に処理されます。

  1. pre_tasksの全処理
  2. rolesの全tasks
  3. tasksの全tasks
  4. roles/tasksのhandlers
  5. post_tasksの全処理

github.com


処理順について書かれたドキュメント

docs.ansible.com

  • Any pre_tasks defined in the play.
  • Any handlers triggered by pre_tasks.
  • Each role listed in roles:, in the order listed. Any role dependencies defined in the role’s meta/main.yml run first, subject to tag filtering and conditionals. See Using role dependencies for more details.
  • Any tasks defined in the play.
  • Any handlers triggered by the roles or tasks.
  • Any post_tasks defined in the play.
  • Any handlers triggered by post_tasks.

https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_roles.html


docs.ansible.com

  • handlers notified within pre_tasks, tasks, and post_tasks sections are automatically flushed at the end of section where they were notified.
  • handlers notified within roles section are automatically flushed at the end of tasks section, but before any tasks handlers.
  • handlers are play scoped and as such can be used outside of the role they are defined in.

https://docs.ansible.com/ansible/latest/user_guide/playbooks_handlers.html

2.9だとこの記述は以下のURL。

docs.ansible.com

pre_tasks(task -> handler) -> roles -> tasks -> handler(roles & tasks) -> post_tasks(task -> handler) の順序は明記されてるけど、rolesのhandlerとtasksのhandlerは特に明記されてない気がする。
(実際に動かしてみると、rolesのhandler -> tasksのhandlerの順になった)

処理順まとめ

  1. pre_tasksのtask
  2. pre_tasksによるhandler
  3. 全rolesのtask
  4. 全tasks
  5. rolesによる全handler
  6. tasksによる全handler
  7. post_tasksのtask
  8. post_tasksによるhandler

確認環境

(a2.10) [zaki@cloud-dev pre_post_tasks (master)]$ ansible-playbook --version
ansible-playbook 2.10.2
  config file = /etc/ansible/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/a2.10/lib64/python3.6/site-packages/ansible
  executable location = /home/zaki/src/ansible-sample/venv/a2.10/bin/ansible-playbook
  python version = 3.6.8 (default, Nov 16 2020, 16:55:22) [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)]

微妙に古いね…

[Ansible] acme_certificateモジュールでLet's Encryptから証明書を発行しApacheでHTTPSサーバー構築

httpdパッケージでApacheをインストール・HTTPS対応し、Let's Encryptの証明書の発行と設定までAnsibleで設定、できるのかな?と思って試したらうまくいった。

Let's Encryptで証明書を作成するには、ACMEプロトコルを利用し、このACMEプロトコルを操作するためのacme_certificateモジュールがAnsibleには(バージョン2.10ではcommunity.cryptoコレクションに)用意されている。

docs.ansible.com

証明書発行部分は外部からのHTTPアクセスに対応できれば良いので、特にApacheである必要はなくNginxでも何でもよい。

環境

(a2.10) [zaki@cloud-dev acme (master)]$ cat /etc/centos-release
CentOS Linux release 7.9.2009 (Core)
(a2.10) [zaki@cloud-dev acme (master)]$ ansible --version
ansible 2.10.2
config file = /home/zaki/src/ansible-sample/acme/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/a2.10/lib64/python3.6/site-packages/ansible
executable location = /home/zaki/src/ansible-sample/venv/a2.10/bin/ansible
python version = 3.6.8 (default, Nov 16 2020, 16:55:22) [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)]
  • ターゲットノード(webサーバー)
[root@instance-20200127-2223 ~]# cat /etc/oracle-release 
Oracle Linux Server release 7.9

ドメインは別途設定。

ターゲットノードはOracle CloudのAlways Free枠のインスタンス
グローバルIPとAレコードを設定したFQDNがあれば仕組み的に特に環境は問わない。
外部からの80/TCP443/TCPは全許可。
(証明書発行が済んだら制限して良い)

内容はRPM系のLinux OSになってるので、APT系の場合はコマンドやパスが異なる場合は読み替えること。

playbook

実際に動作確認したものをGitHubにあげていますので急いでる方はこちらをどうぞ。

github.com

流れ

基本的な流れはacme_certificateモジュールのExamplesを見れば良かった。

httpdのインストールとHTTPS対応

Yum系のLinuxの場合は、mod_sslパッケージを入れればlocalhost用証明書付きで/etc/httpd/conf.d/ssl.conf/etc/httpd/conf.modules.d/00-ssl.confも作成される。

証明書がダミーという点を除いて、起動すれば443/TCPでListenする。

なおapache2_moduleモジュールも存在するが、このモジュールで必要なa2enmodAPT系でapache2をインストールすると付属するが、Yumhttpdには含まれてなかった。

Let's Encryptで証明書発行

ExamplesとPlaybookを見た方が早いですが、大まかな流れは以下の通り。

  1. Let's Encrypt用のアカウントキーを作成
  2. サーバー証明書秘密鍵を作成
  3. サーバー証明書秘密鍵から証明書署名要求(CSR)を作成
  4. 作成したアカウントキーとCSRでLet's EncryptにACMEのチャレンジ要求
  5. チャレンジ要求のレスポンスの内容でトークンファイルをwebサーバー上に作成
    • 62-71行目
    • レスポンスはregisterを使えば簡単に参照できる
    • デフォルトのHTTP-01チャレンジに対するトークンファイルは http://<YOUR_DOMAIN>/.well-known/acme-challenge/<TOKEN> に作成する
    • 平たく言うと、Let's Encryptが「このファイルをあなたのサーバー上で用意してね。後からそれが設置されてるか見に行く(HTTPアクセスする)よ。設定されてたら証明書作ってあげるね」と言っているので、その通りにする
  6. チャレンジの検証を要求し、証明書を取得する
    • 73-81行目
    • パラメタはチャレンジ要求時とほぼ同じだがdataに最初のレスポンス内容を追加している
  7. webサーバーで鍵のパスを設定する
    • 83-92行目
    • 証明書(crtファイル)が作成されたので、Apacheの設定のSSLCertificateFileSSLCertificateKeyFileの設定で証明書ファイルのパスを設定する

要点

f:id:zaki-hmkc:20210118073427p:plain

f:id:zaki-hmkc:20210118073437p:plain

[NetBox] 「VMを登録してIPアドレス割り当て」をREST APIでお試し

NetBoxのREST APIをざっくりと使って「VMを登録してIPアドレスを割り当てる」をやってみる。
APIについては、NetBoxの画面下部の{} APIのリンク先を開くと、Swaggerが表示されるので、エンドポイントとHTTPリクエストメソッド一覧や必要なパラメタ、レスポンス等を確認できる。
NetBoxにおけるSwaggerの操作はこの辺を参照。

f:id:zaki-hmkc:20210113090328p:plain

netbox.readthedocs.io


環境

本環境ではNetBoxは http://192.168.0.19:28080/ で稼働している。
下記参照。

zaki-hmkc.hatenablog.com

また、記事内のHTTPアクセスの内容は基本的にVS CodeのREST Client拡張を使って確認したときのもの。
curlを使う場合はヘッダ指定の-Hや、リクエストBodyを指定するための-dを使用する。

qiita.com

トーク

APIを実行した操作にはトークンが必要。 Docker ComposeでデプロイしたNetBoxは、デフォルトでは 0123456789abcdef0123456789abcdef01234567 になっている。

トークンの値はAdministration画面のTokensから確認できる。

f:id:zaki-hmkc:20210113090553p:plain

HTTPアクセス時には Authorization: Token ${TOKEN値} という書式のリクエストヘッダを付与すれば良い。

netbox.readthedocs.io

Cluster IDを確認

VM情報の登録にはCluster IDが必須項目なので、登録済みCluster IDを確認する。
エンドポイントは /virtualization/clusters/ を使用。

全Clusterを取得するには、特にQuery String無し。

GET http://192.168.0.19:28080/api/virtualization/clusters/
Authorization: Token {{ token }}

Cluster名を指定してフィルタするには以下の通り。(スペースは+エスケープ)

GET http://192.168.0.19:28080/api/virtualization/clusters/?name=vm+network
Authorization: Token {{ token }}

レスポンスは以下の通りで、これから使いたい"vm network"Clusterはid=3`であることが分かる。

HTTP/1.1 200 OK
Server: nginx
Date: Tue, 12 Jan 2021 22:40:09 GMT
Content-Type: application/json
Content-Length: 537
Connection: close
Vary: Accept, Cookie, Origin
Allow: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
API-Version: 2.10
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
X-Frame-Options: SAMEORIGIN
P3P: CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"

{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "id": 3,
      "url": "http://192.168.0.19:28080/api/virtualization/clusters/3/",
      "name": "vm network",
      "type": {
        "id": 2,
        "url": "http://192.168.0.19:28080/api/virtualization/cluster-types/2/",
        "name": "ESXi",
        "slug": "esxi"
      },
      "group": null,
      "tenant": null,
      "site": {
        "id": 1,
        "url": "http://192.168.0.19:28080/api/dcim/sites/1/",
        "name": "自宅",
        "slug": "home"
      },
      "comments": "",
      "tags": [],
      "custom_fields": {},
      "created": "2021-01-11",
      "last_updated": "2021-01-11T06:02:43.351913Z",
      "device_count": 0,
      "virtualmachine_count": 4
    }
  ]
}

VM情報の登録

使用するCluster IDが3わかったのでVMを登録する。
エンドポイントは /virtualization/virtual-machines/ を使用。

必須パラメタはnameclusterになっているので、ひとまずそれだけ指定する。
ここでは client-dev というVMを登録する。

POST http://192.168.0.19:28080/api/virtualization/virtual-machines/
Authorization: Token {{ token }}
Content-Type: application/json

{
    "name": "client-dev",
    "cluster": 3
}

レスポンスは以下。

HTTP/1.1 201 Created
Server: nginx
Date: Tue, 12 Jan 2021 22:45:46 GMT
Content-Type: application/json
Content-Length: 642
Connection: close
Location: http://192.168.0.19:28080/api/virtualization/virtual-machines/6/
Vary: Accept, Cookie, Origin
Allow: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
API-Version: 2.10
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
X-Frame-Options: SAMEORIGIN
P3P: CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"

{
  "id": 6,
  "url": "http://192.168.0.19:28080/api/virtualization/virtual-machines/6/",
  "name": "client-dev",
  "status": {
    "value": "active",
    "label": "Active"
  },
  "site": {
    "id": 1,
    "url": "http://192.168.0.19:28080/api/dcim/sites/1/",
    "name": "自宅",
    "slug": "home"
  },
  "cluster": {
    "id": 3,
    "url": "http://192.168.0.19:28080/api/virtualization/clusters/3/",
    "name": "vm network"
  },
  "role": null,
  "tenant": null,
  "platform": null,
  "primary_ip": null,
  "primary_ip4": null,
  "primary_ip6": null,
  "vcpus": null,
  "memory": null,
  "disk": null,
  "comments": "",
  "local_context_data": null,
  "tags": [],
  "custom_fields": {},
  "config_context": {},
  "created": "2021-01-12",
  "last_updated": "2021-01-12T22:45:46.414175Z"
}

画面で見てもVMが登録されたことを確認できる。

f:id:zaki-hmkc:20210113090842p:plain

インタフェース作成

IPアドレスの設定のためにはインタフェースを作成する。
上記のVM作成時のレスポンスでVMのIDは6だとわかっているので、それを使用する。
エンドポイントは /virtualization/interfaces/ を使用する。

必須項目は virtual_machine(integer)と、name(string)の二つ

POST http://192.168.0.19:28080/api/virtualization/interfaces/
Authorization: Token {{ token }}
Content-Type: application/json

{
    "virtual_machine": 6,
    "name": "ens192"
}

レスポンスは以下の通り。

HTTP/1.1 201 Created
Server: nginx
Date: Tue, 12 Jan 2021 22:50:55 GMT
Content-Type: application/json
Content-Length: 334
Connection: close
Location: http://192.168.0.19:28080/api/virtualization/interfaces/8/
Vary: Accept, Cookie, Origin
Allow: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
API-Version: 2.10
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
X-Frame-Options: SAMEORIGIN
P3P: CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"

{
  "id": 8,
  "url": "http://192.168.0.19:28080/api/virtualization/interfaces/8/",
  "virtual_machine": {
    "id": 6,
    "url": "http://192.168.0.19:28080/api/virtualization/virtual-machines/6/",
    "name": "client-dev"
  },
  "name": "ens192",
  "enabled": true,
  "mtu": null,
  "mac_address": null,
  "description": "",
  "mode": null,
  "untagged_vlan": null,
  "tagged_vlans": [],
  "tags": []
}

VMの詳細画面を開くと、インタフェースが追加されたことを確認できる。

f:id:zaki-hmkc:20210113091026p:plain

IPアドレスの作成とインタフェースへのアサイ

作成したインタフェースのIDは上記で8になっている。
IPアドレスを作成し、このインタフェースへ割り当てを行う。 (割り当ては行わずにIPアドレス作成のみも可能)

エンドポイントは /ipam/ip-addresses/ を使用。

必須項目は address(string) で、インタフェースへの割り当てにはオプションの assigned_object_type(string) と assigned_object_id(integer) を指定する。
あとAPIドキュメントだと nat_outside にも必須(required)のマークが付いているが、無くても動く。

assigned_object_typeに何を指定するかドキュメントでちょっと確認できなかったが、「作成済みIPアドレス一覧を取得(/ipam/ip-addresses/にGET)」すると、以下のようになっていたのでそれを指定。

オブジェクト種別 assigned_object_typeの値
Device "dcim.interface"
Virtual Machine "virtualization.vminterface"

ということでリクエストは以下の通り。

POST http://192.168.0.19:28080/api/ipam/ip-addresses/
Authorization: Token {{ token }}
Content-Type: application/json

{
    "address": "192.168.0.17/24",
    "assigned_object_type": "virtualization.vminterface",
    "assigned_object_id": 8
}

レスポンスは以下の通り。

HTTP/1.1 201 Created
Server: nginx
Date: Tue, 12 Jan 2021 23:00:55 GMT
Content-Type: application/json
Content-Length: 685
Connection: close
Location: http://192.168.0.19:28080/api/ipam/ip-addresses/48/
Vary: Accept, Cookie, Origin
Allow: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
API-Version: 2.10
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
X-Frame-Options: SAMEORIGIN
P3P: CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"

{
  "id": 48,
  "url": "http://192.168.0.19:28080/api/ipam/ip-addresses/48/",
  "family": {
    "value": 4,
    "label": "IPv4"
  },
  "address": "192.168.0.17/24",
  "vrf": null,
  "tenant": null,
  "status": {
    "value": "active",
    "label": "Active"
  },
  "role": null,
  "assigned_object_type": "virtualization.vminterface",
  "assigned_object_id": 8,
  "assigned_object": {
    "id": 8,
    "url": "http://192.168.0.19:28080/api/virtualization/interfaces/8/",
    "virtual_machine": {
      "id": 6,
      "url": "http://192.168.0.19:28080/api/virtualization/virtual-machines/6/",
      "name": "client-dev"
    },
    "name": "ens192"
  },
  "nat_inside": null,
  "nat_outside": null,
  "dns_name": "",
  "description": "",
  "tags": [],
  "custom_fields": {},
  "created": "2021-01-12",
  "last_updated": "2021-01-12T23:00:55.356643Z"
}

これでインタフェースにIPアドレスが割り当て状態となる。

f:id:zaki-hmkc:20210113091135p:plain

IPアドレスの詳細はこの通り。

f:id:zaki-hmkc:20210113091235p:plain

Primary IPに設定

f:id:zaki-hmkc:20210113091336p:plain

IPアドレスの割り当てはこれで完了したが、「Primary IPアドレス設定」になっていないため、このままだと一覧で表示されない。
Primary IPアドレスVMの設定で行う。

エンドポイントは /virtualization/virtual-machines/{id}/ を使用。

ここまでの作成手順で、VMのIDは6、作ったIPアドレスのID(IPじゃないよ)は48なので、リクエストは以下の通り。
(ついでなのでroleも指定)

PUT http://192.168.0.19:28080/api/virtualization/virtual-machines/6/
Authorization: Token {{ token }}
Content-Type: application/json

{
    "name": "client-dev",
    "cluster": 3,
    "primary_ip4": 48,
    "role": 2
}

レスポンスは以下の通り。

HTTP/1.1 200 OK
Server: nginx
Date: Tue, 12 Jan 2021 23:42:42 GMT
Content-Type: application/json
Content-Length: 945
Connection: close
Vary: Accept, Cookie, Origin
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
API-Version: 2.10
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
X-Frame-Options: SAMEORIGIN
P3P: CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"

{
  "id": 6,
  "url": "http://192.168.0.19:28080/api/virtualization/virtual-machines/6/",
  "name": "client-dev",
  "status": {
    "value": "active",
    "label": "Active"
  },
  "site": {
    "id": 1,
    "url": "http://192.168.0.19:28080/api/dcim/sites/1/",
    "name": "自宅",
    "slug": "home"
  },
  "cluster": {
    "id": 3,
    "url": "http://192.168.0.19:28080/api/virtualization/clusters/3/",
    "name": "vm network"
  },
  "role": {
    "id": 2,
    "url": "http://192.168.0.19:28080/api/dcim/device-roles/2/",
    "name": "client",
    "slug": "client"
  },
  "tenant": null,
  "platform": null,
  "primary_ip": {
    "id": 48,
    "url": "http://192.168.0.19:28080/api/ipam/ip-addresses/48/",
    "family": 4,
    "address": "192.168.0.17/24"
  },
  "primary_ip4": {
    "id": 48,
    "url": "http://192.168.0.19:28080/api/ipam/ip-addresses/48/",
    "family": 4,
    "address": "192.168.0.17/24"
  },
  "primary_ip6": null,
  "vcpus": null,
  "memory": null,
  "disk": null,
  "comments": "",
  "local_context_data": null,
  "tags": [],
  "custom_fields": {},
  "config_context": {},
  "created": "2021-01-12",
  "last_updated": "2021-01-12T23:42:42.492869Z"
}

Primary IPアドレスが設定できました。

f:id:zaki-hmkc:20210113091421p:plain


UpdateせずにCreateだけでPrimary IPを設定する方法はちょっとわからなかった。
とはいえ、手動でなく自動処理するのであれば、この点はそこまでこだわらなくても良い気もする。

  • Primary IPの設定はVMの設定項目
  • ただしIPアドレスがインタフェースへ設定済みでないとエラー
  • IPアドレスは単体で作成できるがインタフェース割り当てはインタフェースを作成しておく必要がある
  • インタフェースを作成するにはVMを作成しておく必要がある