まだプログラマーですが何か?

プログラマーネタとアスリートネタ中心。たまに作成したウェブサービス関連の話も http://twitter.com/dotnsf

タグ:curl

Docker Desktop ショックがあって以来、なるべく少ない制約の下で docker を動かせる環境を色々調べています。そんな中で見つけた1つの方法が Docker in Docker(以下、DinD)です。

DinD はその名前の通りで、コンテナクラスタ(親)の中で動くコンテナ(子)として Docker サーバー&クライアントを動かす、というものです。多くの場合、「親コンテナ=Kubernetes」となることが多いので、正確には "Docker in Kubernetes" と表現すべきかもしれませんが、広い意味(?)での "Docker in Docker" ということだと思います。

単なる運用環境の観点だと「Kubernetes が使えるなら Docker は要らないのでは?」と思うかもしれませんが、特に開発段階だとコマンドとしての docker CLI を使いたいことがあったり、プロダクション環境とは別の小さな開発環境を Docker で作っておけると便利なことも多くあります。そういった意味で Kubernetes があってもそれとは別に Docker 環境が欲しくなることがあるのでした。


IBM Cloud からも IKS(IBM Kubernetes Services)ROKS(Redhat Openshift Kubernetes Services) が提供されていて、これらの環境でも DinD を使うことができます。特に今回は IKS の 30 日無料版を使って DinD 環境を作って使う手順を紹介します。なお、IKS 30 日無料版の制約事項や環境準備手順についてはこちらの過去記事を参照ください:
http://dotnsf.blog.jp/archives/1079287640.html


↑この記事最後の "$ kubectl get all" コマンドが成功するまでになれば準備完了です。なお Kubernetes クラスタに対して "$ kubectl get all" コマンドが実行できるようになっていれば IKS 以外の他の Kubernetes クラスタ環境でも以下同様にして DinD 環境を作ることができると思います。


【(IKS に) DinD の Pod を作る】
では早速 DinD 環境を作ります。DinD 環境といっても大それたものではなく、コンテナ的に言えば「DinD の Pod を1つ作る」ことになります。そしてその1つの Pod の中に「Docker デーモンのコンテナ」と「Docker クライアントのコンテナ」を1つずつ作ります(つまり1つの Pod の中で2つのコンテナを動かします)。

実際の作成に関しても、以下の内容のマニフェストファイルを用意するだけです(この内容を dind.yml という名前で保存してください):
apiVersion: v1
kind: Pod
metadata:
  name: dind
spec:
  containers:
    - name: docker
      image: docker:19.03
      command: ["docker", "run", "nginx:latest"]
      env:
        - name: DOCKER_HOST
          value: tcp://localhost:2375
    - name: dind-daemon
      image: docker:19.03-dind
      env:
        - name: DOCKER_TLS_CERTDIR
          value: ""
      resources:
        requests:
          cpu: 20m
          memory: 512Mi
      securityContext:
        privileged: true

そして、kubectl コマンドで以下を実行してマニフェストを適用します:
$ kubectl apply -f dind.yml

※または dind.yml を用意しなくても、このコマンドでも同じ結果になります:
$ kubectl apply -f https://raw.githubusercontent.com/dotnsf/dind_iks/main/dind.yml


初回のみイメージのダウンロードで少し時間がかかりますが、しばらく待つと dind という名前の Pod が1つ(コンテナは docker dind-daemon の2つ)起動します:
$ kubectl get pods

NAME READY STATUS RESTARTS AGE dind 2/2 Running 2 101m $ kubectl describe pod dind Name: dind Namespace: default Priority: 0 Node: 10.144.222.147/10.144.222.147 Start Time: Sun, 20 Feb 2022 21:52:46 +0900 Labels: Annotations: cni.projectcalico.org/containerID: 0c110243107d25c9e27b82202871504047d9ac691ad294d8f89bb1a9b114ca5e cni.projectcalico.org/podIP: 172.30.216.78/32 cni.projectcalico.org/podIPs: 172.30.216.78/32 kubernetes.io/psp: ibm-privileged-psp Status: Running IP: 172.30.216.78 IPs: IP: 172.30.216.78 Containers: docker: Container ID: containerd://0dbd05bc4171f96caaa7601a10e1b2f853511b4ed7087ab3958c016024c65f1c Image: docker:19.03 Image ID: docker.io/library/docker@sha256:ea1f0761c92b600417ad14bc9b2b3a30abf8e96e94895fee6cbb5353316f30b0 Port: Host Port: Command: docker run nginx:latest State: Running Started: Sun, 20 Feb 2022 22:53:02 +0900 Last State: Terminated Reason: Completed Exit Code: 0 Started: Sun, 20 Feb 2022 21:53:08 +0900 Finished: Sun, 20 Feb 2022 22:53:01 +0900 Ready: True Restart Count: 2 Environment: DOCKER_HOST: tcp://localhost:2375 Mounts: /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-gz5fb (ro) dind-daemon: Container ID: containerd://5a6a15eefa9ebdf8a0dcc825f1c87fa4a5a37daab005a1d83e5df8f9a33ff7bb Image: docker:19.03-dind Image ID: docker.io/library/docker@sha256:c85365ad08c7f6e02ac962a8759c4a5b8512ea5c294d3bb9ed25fca52e9e22e5 Port: Host Port: State: Running Started: Sun, 20 Feb 2022 21:53:07 +0900 Ready: True Restart Count: 0 Requests: cpu: 20m memory: 512Mi Environment: DOCKER_TLS_CERTDIR: Mounts: /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-gz5fb (ro) Conditions: Type Status Initialized True Ready True ContainersReady True PodScheduled True Volumes: kube-api-access-gz5fb: Type: Projected (a volume that contains injected data from multiple sources) TokenExpirationSeconds: 3607 ConfigMapName: kube-root-ca.crt ConfigMapOptional: DownwardAPI: true QoS Class: Burstable Node-Selectors: Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 600s node.kubernetes.io/unreachable:NoExecute op=Exists for 600s Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Created 43m (x3 over 103m) kubelet Created container docker Normal Started 43m (x3 over 103m) kubelet Started container docker Normal Pulled 43m (x2 over 103m) kubelet Container image "docker:19.03" already present on machine d


DinD 環境はあっけなく完成しました。


【(IKS の) DinD 内の docker を操作する】
次に完成した DinD 環境を実際に CLI で使ってみます。そのためにまずは Docker クライアントが使える環境のシェルにアタッチする必要があります。Docker クライアントは docker という名前のコンテナで動いていることが分かっているので、以下のコマンドを実行します:
$ kubectl exec -it dind -c docker -- /bin/sh

/ #

プロンプトが "/ #" という記号に変わればアタッチ成功です。ここからは docker CLI コマンドが実行できます。試しに "docker version" コマンドを実行するとクライアント&サーバー双方の docker バージョン情報(下の例ではどちらも "19.03.15")を確認できます:
/ # docker version

Client: Docker Engine - Community
 Version:           19.03.15
 API version:       1.40
 Go version:        go1.13.15
 Git commit:        99e3ed8
 Built:             Sat Jan 30 03:11:43 2021
 OS/Arch:           linux/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.15
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       99e3ed8
  Built:            Sat Jan 30 03:18:13 2021
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          v1.3.9
  GitCommit:        ea765aba0d05254012b0b9e595e995c09186427f
 runc:
  Version:          1.0.0-rc10
  GitCommit:        dc9208a3303feef5b3839f4323d9beb36df0a9dd
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683


実際にサーバーイメージをデプロイして動作確認してみましょう。というわけで、まずは nginx イメージを 8000 番ポートでデプロイしてみます:
/ # docker run -d --name ningx -p 8000:80 nginx

成功したら早速アクセスして動作確認を・・・と思ったのですが、この docker コンテナには HTTP クライアントが curl 含めてインストールされていないようでした。というわけで動作確認用のコマンドもインストールしておきます。とりあえず curl と w3m あたりでいいですかね。。:
/ # apk add --update curl

/ # apk add --update w3m

インストールが成功したら、まずは curl で http://localhost:8000/ にアクセスしてみます:
/ # curl http://localhost:8000/

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

うぉっ、と。少なくとも HTTP サーバーとして動いているらしいことは確認できたのですが、これだとちょっと見にくいですね。というわけで、先程一緒にインストールした w3m で確認してみます:
/ # w3m http://localhost:8000/

Welcome to nginx!

If you see this page, the nginx web server is successfully installed and working. Further configuration is required.

For online documentation and support please refer to nginx.org.
Commercial support is available at nginx.com.

Thank you for using nginx.

2022022101


かろうじて Nginx のトップ画面らしきものが確認できました。というわけで一応動いていると思います。


【まとめ】
というわけで DinD が IBM Cloud の 30 日無料版 Kubernetes クラスタ環境でも動かせることが確認できました。

とはいえ、もともとは Docker Desktop の代替になるような Docker 環境を探していたことに立ち返ると、この(クライアント側のような) CLI だけの Docker 環境はどうしても使い道が限られてしまうように感じます。ある程度、Docker を理解している人向けに、CLI だけで完結する使いみちであればなんとか、といったところでしょうか。


本当はここで作った Pod を外部に EXPOSE できるといいんですが、自分で試行錯誤している限りではまだうまく行ってません。もし方法をご存じの方がいらっしゃったら是非教えてください。


(2022/02/21 追記ここから)
試行錯誤の中で外部公開する方法がわかりました。

上述の "$ kubectl apply -f ..." コマンドを実行する箇所の内容を以下のように変更してください:
$ kubectl apply -f https://raw.githubusercontent.com/dotnsf/dind_iks/main/dind_expose.yml

なお、ここで指定している dind_expose.yml の内容は以下のようなものです。Pod を Deployment に書き換えた上で Service オブジェクトを追加して、8000 番ポートでの待受けを 30800 番ポートから転送するように(この後の作業で 8000 番ポートで待ち受けるアプリケーションをデプロイする想定で)あらかじめ公開しています:
apiVersion: v1
kind: Service
metadata:
  name: dind
spec:
  selector:
    app: dind
  ports:
  - port: 8000
    name: port8000
    protocol: TCP
    targetPort: 8000
    nodePort: 30800
  type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dind
spec:
  selector:
    matchLabels:
      app: dind
  replicas: 1
  template:
    metadata:
      labels:
        app: dind
    spec:
      containers:
        - name: docker
          image: docker:19.03
          command: ["docker", "run", "nginx:latest"]
          env:
            - name: DOCKER_HOST
              value: tcp://localhost:2375
        - name: dind-daemon
          image: docker:19.03-dind
          env:
            - name: DOCKER_TLS_CERTDIR
              value: ""
          resources:
            requests:
              cpu: 20m
              memory: 512Mi
          securityContext:
            privileged: true

このコマンドの後、"$ kubectl get all" を実行すると以下のような結果になります:
$ kubectl get all
NAME READY STATUS RESTARTS AGE pod/dind-7d8546bc8-fxbhw 2/2 Running 1 19s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/dind NodePort 172.21.8.233 8000:30800/TCP 21s service/kubernetes ClusterIP 172.21.0.1 443/TCP 3d10h NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/dind 1/1 1 1 20s NAME DESIRED CURRENT READY AGE replicaset.apps/dind-7d8546bc8 1 1 1 21s

以前の方法では Pod 名は "dind" 固定だったのですが、ここでは "dind-" に続けてランダムな文字列が付与されています(上例では "dind-7d8546bc8-fxbhw" となっています)。この Pod 名を指定して以下を実行して docker コンテナのシェルに接続します:
$ kubectl exec -it dind-7d8546bc8-fxbhw -c docker -- /bin/sh

/ #

この後は以前の方法と同様にして docker コンテナ内で NGINX を 8000 番ポートで起動します:
/ # docker run -d --name ningx -p 8000:80 nginx

これで以前と同様に k8s のワーカーノード内で(NodePort サービスを使って) NGINX が 8000 番ポートで起動します。ただ今回は Service オブジェクトで 8000 番リクエストを 30800 番ポートで外部公開しているので、 http://(ワーカーノードのパブリックIPアドレス):30800/ にアクセスすればクラスタ外部からでもこの NGINX に接続できるようになっています。


ワーカーノードのパブリック IP アドレスは IBM Cloud の IKS 環境であれば、IBM Cloud ダッシュボード画面から確認することができます(この例では "169.51.206.71" となっています):
2022022102


というわけで、改めてウェブブラウザで http://169.51.206.71:30800/ にアクセスしてみると、、、期待通りの画面が表示されました! IBM Cloud の30日無料版 Kubernetes クラスタ環境で構築した DinD のコンテナを外部に公開することができることが確認できました:
2022022100


というわけで、30 日間無料の IBM Cloud Kubernetes 環境を使って、Docker および Kubernetes クラスタの実行環境を構築することができました。

(2022/02/21 追記ここまで)
 

まず最初に、自分がもともと今回勝利する技術に興味を持ったきっかけは GIGAZINE に掲載されたこのネタでした:


これに近いことを自分でもやろうとするとどうすればよいか、、と考えました。Kubernetes (以下 "k8s")には API サーバーが存在していて、kubectl はこの API サーバー経由でリクエスト/レスポンスを使って動く、ということは知っていたので、うまくやれば REST API でなんとかなるんじゃないか、、、という発想から色々調べてみたのでした。

調べてみるとわかるのですが、k8s の API サーバーを直接利用するには kubectl コマンドの裏で行われている認証などの面倒な部分も意識する必要がありました:
k8s_api



本ブログエントリでは、認証などの面倒な指定を回避して利用できるよう k8s の proxy 機能を使って k8s クラスタの外部から k8s API を実行してクラスタの状態を確認できる様子を紹介します。 なおここで紹介する k8s の環境としては IBM Cloud の 30 日無料 k8s クラスタ環境を使って紹介します。


【k8s クラスタの準備】
まずは k8s クラスタを用意します。手元で用意できる環境があればそれを使ってもいいのですが、ここでは IBM Cloud から提供されている 30 日間無料のシングルワーカーノードクラスタ環境を使うことにします。この環境を入手するまでの具体的な手順については以前に記載したこちらの別エントリを参照してください:


次にこのシングルワーカーノードクラスタ環境をカレントコンテキストにして kubectl から利用できる状態にします。

まず、今回紹介する機能は CLI コマンドを使います。最低限必要なのは kubectl コマンドで、こちらはまだインストールできていない場合はこちらなどを参考にインストールしてください。また IBM Cloud の k8s を使う場合は kubectl に加えて ibmcloud コマンドも必要です。ibmcloud コマンドのインストールについてはこちらのページ等を参照して、自分の環境に合わせた ibmcloud コマンドをインストールしてください:


CLI の用意ができたら IBM Cloud のダッシュボード画面から作成したクラスタを選択し、画面上部の「ヘルプ」と書かれた箇所をクリックします:
2022011401


すると画面右側にヘルプメニューが表示されます。ここで「クラスターへのログイン」と書かれた箇所を展開して、その中に書かれている2つのコマンド(ログインコマンドとカレントコンテキストを指定するコマンド)をターミナルなどから続けて実行することで、CLI からも IBM Cloud にログインし、k8s クラスタをカレントコンテキストに切り替えることができます:
2022011402


$ ibmcloud login

$ ibmcloud ks cluster config -c XXXX(一意のクラスタID)XXXX

ここまで実行することで CLI 環境のカレントコンテキストが IBM Cloud 上の k8s になり、kubectl コマンドも IBM Cloud の k8s に対して実行できるようになります(以下は実行例です):
$ kubectl get all

NAME                            READY   STATUS      RESTARTS   AGE
pod/hostname-7b4f76fd59-c8b2l   1/1     Running     0          4d22h

NAME                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
service/hostname     NodePort    172.21.105.49           8080:30080/TCP   4d22h
service/kubernetes   ClusterIP   172.21.0.1              443/TCP          4d23h

NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/hostname   1/1     1            1           4d22h

NAME                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/hostname-7b4f76fd59   1         1         1       4d22h

k8s 環境の準備段階としてここまでが必要です。IBM Cloud 以外の k8s クラスタ環境を利用する場合でもここまでと同様の準備ができていれば、以下の Proxy 実行から続けて行うことができるはずです。


【k8s Proxy の起動】
この状態で k8s Proxy を起動します:
$ kubectl proxy
Starting to serve on 127.0.0.1:8001

↑8001 番ポートで Proxy が起動した状態になります。Proxy を終了するにはこの画面で Ctrl+C を押します。

ここまでできていれば localhost:8001 を経由して k8s REST API を実行し、その結果を参照することができるようになっています。


【k8s REST API の実行】
いくつか k8s REST API を実行してみることにします。今回は(別のターミナルから)curl コマンドを使って REST API を実行してみます。

例えばクラスタ上で稼働している Pods の一覧を取得してみます。kubectl であれば
$ kubectl get pods

NAME                        READY   STATUS    RESTARTS   AGE
hostname-7b4f76fd59-c8b2l   1/1     Running   0          4d22h
というコマンドを実行するところです。今回は上記のように hostname という名前の Pod が1つだけ存在している状態であるとします。

一方、REST API ではバージョンや namespace を指定して以下のように実行します。また結果もこのように取得できます:
$ curl -X GET http://localhost:8001/api/v1/namespaces/default/pods

{
  "kind": "PodList",
  "apiVersion": "v1",
  "metadata": {
    "resourceVersion": "92526"
  },
  "items": [
    {
      "metadata": {
        "name": "hostname-7b4f76fd59-c8b2l",
        "generateName": "hostname-7b4f76fd59-",
        "namespace": "default",
        "uid": "3753ac40-6d2c-428a-86ac-4181dfc0cce9",
        "resourceVersion": "2162",
        "creationTimestamp": "2022-01-09T07:27:25Z",
        "labels": {
          "app": "hostname",
          "pod-template-hash": "7b4f76fd59"
        },
        "annotations": {
          "cni.projectcalico.org/containerID": "1e1592caee1c1a76426c1fca88a70ddb681fae650301cd0cbe3985f0b0975d45",
          "cni.projectcalico.org/podIP": "172.30.153.142/32",
          "cni.projectcalico.org/podIPs": "172.30.153.142/32",
          "kubernetes.io/psp": "ibm-privileged-psp"
        },
        "ownerReferences": [
          {
            "apiVersion": "apps/v1",
            "kind": "ReplicaSet",
            "name": "hostname-7b4f76fd59",
            "uid": "9160ac38-3e4e-4a01-80a1-affba620fa9c",
            "controller": true,
            "blockOwnerDeletion": true
          }
        ],
        "managedFields": [
          {
            "manager": "kube-controller-manager",
            "operation": "Update",
            "apiVersion": "v1",
            "time": "2022-01-09T07:27:25Z",
            "fieldsType": "FieldsV1",
            "fieldsV1": {"f:metadata":{"f:generateName":{},"f:labels":{".":{},"f:app":{},"f:pod-template-hash":{}},"f:ownerReferences":{".":{},"k:{\"uid\":\"9160ac38-3e4e-4a01-80a1-affba620fa9c\"}":{".":{},"f:apiVersion":{},"f:blockOwnerDeletion":{},"f:controller":{},"f:kind":{},"f:name":{},"f:uid":{}}}},"f:spec":{"f:containers":{"k:{\"name\":\"hostname\"}":{".":{},"f:image":{},"f:imagePullPolicy":{},"f:name":{},"f:ports":{".":{},"k:{\"containerPort\":8080,\"protocol\":\"TCP\"}":{".":{},"f:containerPort":{},"f:protocol":{}}},"f:resources":{},"f:terminationMessagePath":{},"f:terminationMessagePolicy":{}}},"f:dnsPolicy":{},"f:enableServiceLinks":{},"f:restartPolicy":{},"f:schedulerName":{},"f:securityContext":{},"f:terminationGracePeriodSeconds":{}}}
          },
          {
            "manager": "calico",
            "operation": "Update",
            "apiVersion": "v1",
            "time": "2022-01-09T07:27:26Z",
            "fieldsType": "FieldsV1",
            "fieldsV1": {"f:metadata":{"f:annotations":{"f:cni.projectcalico.org/containerID":{},"f:cni.projectcalico.org/podIP":{},"f:cni.projectcalico.org/podIPs":{}}}}
          },
          {
            "manager": "kubelet",
            "operation": "Update",
            "apiVersion": "v1",
            "time": "2022-01-09T07:27:36Z",
            "fieldsType": "FieldsV1",
            "fieldsV1": {"f:status":{"f:conditions":{"k:{\"type\":\"ContainersReady\"}":{".":{},"f:lastProbeTime":{},"f:lastTransitionTime":{},"f:status":{},"f:type":{}},"k:{\"type\":\"Initialized\"}":{".":{},"f:lastProbeTime":{},"f:lastTransitionTime":{},"f:status":{},"f:type":{}},"k:{\"type\":\"Ready\"}":{".":{},"f:lastProbeTime":{},"f:lastTransitionTime":{},"f:status":{},"f:type":{}}},"f:containerStatuses":{},"f:hostIP":{},"f:phase":{},"f:podIP":{},"f:podIPs":{".":{},"k:{\"ip\":\"172.30.153.142\"}":{".":{},"f:ip":{}}},"f:startTime":{}}}
          }
        ]
      },
      "spec": {
        "volumes": [
          {
            "name": "kube-api-access-4qr4h",
            "projected": {
              "sources": [
                {
                  "serviceAccountToken": {
                    "expirationSeconds": 3607,
                    "path": "token"
                  }
                },
                {
                  "configMap": {
                    "name": "kube-root-ca.crt",
                    "items": [
                      {
                        "key": "ca.crt",
                        "path": "ca.crt"
                      }
                    ]
                  }
                },
                {
                  "downwardAPI": {
                    "items": [
                      {
                        "path": "namespace",
                        "fieldRef": {
                          "apiVersion": "v1",
                          "fieldPath": "metadata.namespace"
                        }
                      }
                    ]
                  }
                }
              ],
              "defaultMode": 420
            }
          }
        ],
        "containers": [
          {
            "name": "hostname",
            "image": "dotnsf/hostname",
            "ports": [
              {
                "containerPort": 8080,
                "protocol": "TCP"
              }
            ],
            "resources": {

            },
            "volumeMounts": [
              {
                "name": "kube-api-access-4qr4h",
                "readOnly": true,
                "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount"
              }
            ],
            "terminationMessagePath": "/dev/termination-log",
            "terminationMessagePolicy": "File",
            "imagePullPolicy": "Always"
          }
        ],
        "restartPolicy": "Always",
        "terminationGracePeriodSeconds": 30,
        "dnsPolicy": "ClusterFirst",
        "serviceAccountName": "default",
        "serviceAccount": "default",
        "nodeName": "10.144.214.171",
        "securityContext": {

        },
        "imagePullSecrets": [
          {
            "name": "all-icr-io"
          }
        ],
        "schedulerName": "default-scheduler",
        "tolerations": [
          {
            "key": "node.kubernetes.io/not-ready",
            "operator": "Exists",
            "effect": "NoExecute",
            "tolerationSeconds": 600
          },
          {
            "key": "node.kubernetes.io/unreachable",
            "operator": "Exists",
            "effect": "NoExecute",
            "tolerationSeconds": 600
          }
        ],
        "priority": 0,
        "enableServiceLinks": true,
        "preemptionPolicy": "PreemptLowerPriority"
      },
      "status": {
        "phase": "Running",
        "conditions": [
          {
            "type": "Initialized",
            "status": "True",
            "lastProbeTime": null,
            "lastTransitionTime": "2022-01-09T07:27:25Z"
          },
          {
            "type": "Ready",
            "status": "True",
            "lastProbeTime": null,
            "lastTransitionTime": "2022-01-09T07:27:36Z"
          },
          {
            "type": "ContainersReady",
            "status": "True",
            "lastProbeTime": null,
            "lastTransitionTime": "2022-01-09T07:27:36Z"
          },
          {
            "type": "PodScheduled",
            "status": "True",
            "lastProbeTime": null,
            "lastTransitionTime": "2022-01-09T07:27:25Z"
          }
        ],
        "hostIP": "10.144.214.171",
        "podIP": "172.30.153.142",
        "podIPs": [
          {
            "ip": "172.30.153.142"
          }
        ],
        "startTime": "2022-01-09T07:27:25Z",
        "containerStatuses": [
          {
            "name": "hostname",
            "state": {
              "running": {
                "startedAt": "2022-01-09T07:27:36Z"
              }
            },
            "lastState": {

            },
            "ready": true,
            "restartCount": 0,
            "image": "docker.io/dotnsf/hostname:latest",
            "imageID": "docker.io/dotnsf/hostname@sha256:e96808f33e747004d895d8079bc05d0e98010114b054aea825a6c0b1573c759e",
            "containerID": "containerd://7cc6b4eafd0723f46f78fc933a43ddefc6d4ddc75796608548b34a9aaae77f17",
            "started": true
          }
        ],
        "qosClass": "BestEffort"
      }
    }
  ]
}

次に API で Pod を作成してみます。Pod そのものはシンプルな Hello World 出力コンテナとして、この Pod を API で作成するには以下のように指定します(YAML 部分をファイルで指定する方法がよくわからないので、ご存じの人がいたら教えてください):
$ curl -X POST -H 'Content-Type: application/yaml' http://localhost:8001/api/v1/namespaces/default/pods -d '
apiVersion: v1
kind: Pod
metadata:
  name: pod-example
spec:
  containers:
  - name: ubuntu
    image: ubuntu:trusty
    command: ["echo"]
    args: ["Hello World"]
  restartPolicy: Never
'

{
  "kind": "Pod",
  "apiVersion": "v1",
  "metadata": {
    "name": "pod-example",
    "namespace": "default",
    "uid": "79d0f086-6c6f-445a-9819-69ae8630710a",
    "resourceVersion": "92782",
    "creationTimestamp": "2022-01-14T06:25:25Z",
    "annotations": {
      "kubernetes.io/psp": "ibm-privileged-psp"
    },
    "managedFields": [
      {
        "manager": "curl",
        "operation": "Update",
        "apiVersion": "v1",
        "time": "2022-01-14T06:25:25Z",
        "fieldsType": "FieldsV1",
        "fieldsV1": {"f:spec":{"f:containers":{"k:{\"name\":\"ubuntu\"}":{".":{},"f:args":{},"f:command":{},"f:image":{},"f:imagePullPolicy":{},"f:name":{},"f:resources":{},"f:terminationMessagePath":{},"f:terminationMessagePolicy":{}}},"f:dnsPolicy":{},"f:enableServiceLinks":{},"f:restartPolicy":{},"f:schedulerName":{},"f:securityContext":{},"f:terminationGracePeriodSeconds":{}}}
      }
    ]
  },
  "spec": {
    "volumes": [
      {
        "name": "kube-api-access-mc7tz",
        "projected": {
          "sources": [
            {
              "serviceAccountToken": {
                "expirationSeconds": 3607,
                "path": "token"
              }
            },
            {
              "configMap": {
                "name": "kube-root-ca.crt",
                "items": [
                  {
                    "key": "ca.crt",
                    "path": "ca.crt"
                  }
                ]
              }
            },
            {
              "downwardAPI": {
                "items": [
                  {
                    "path": "namespace",
                    "fieldRef": {
                      "apiVersion": "v1",
                      "fieldPath": "metadata.namespace"
                    }
                  }
                ]
              }
            }
          ],
          "defaultMode": 420
        }
      }
    ],
    "containers": [
      {
        "name": "ubuntu",
        "image": "ubuntu:trusty",
        "command": [
          "echo"
        ],
        "args": [
          "Hello World"
        ],
        "resources": {

        },
        "volumeMounts": [
          {
            "name": "kube-api-access-mc7tz",
            "readOnly": true,
            "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount"
          }
        ],
        "terminationMessagePath": "/dev/termination-log",
        "terminationMessagePolicy": "File",
        "imagePullPolicy": "IfNotPresent"
      }
    ],
    "restartPolicy": "Never",
    "terminationGracePeriodSeconds": 30,
    "dnsPolicy": "ClusterFirst",
    "serviceAccountName": "default",
    "serviceAccount": "default",
    "securityContext": {

    },
    "imagePullSecrets": [
      {
        "name": "all-icr-io"
      }
    ],
    "schedulerName": "default-scheduler",
    "tolerations": [
      {
        "key": "node.kubernetes.io/not-ready",
        "operator": "Exists",
        "effect": "NoExecute",
        "tolerationSeconds": 600
      },
      {
        "key": "node.kubernetes.io/unreachable",
        "operator": "Exists",
        "effect": "NoExecute",
        "tolerationSeconds": 600
      }
    ],
    "priority": 0,
    "enableServiceLinks": true,
    "preemptionPolicy": "PreemptLowerPriority"
  },
  "status": {
    "phase": "Pending",
    "qosClass": "BestEffort"
  }
}

kubectl コマンドで Pods 一覧を見ると、Pods が追加/実行され、終了していることが確認できます:
$ kubectl get pods

NAME                        READY   STATUS      RESTARTS   AGE
hostname-7b4f76fd59-c8b2l   1/1     Running     0          4d23h
pod-example                 0/1     Completed   0          3m


といった感じで、k8s Proxy を使うことで簡単に k8s API を利用できる REST API サーバーを作ることができそうでした。もともとこの技術に興味を持った GIGAZINE に掲載されたこのネタも、REST API の Proxy を経由して API を実行して結果を GUI で視覚化して・・・ というのも、そこまで難しくない気がしています。

なお k8s の REST API についてはここから一覧を参照できます:
https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/


(参照)
https://qiita.com/iaoiui/items/36e86d173e451a7b18be

IBM Cloudant のバイナリ・アタッチメント機能を使って、簡易的なファイルサーバー代わりに使う方法を紹介します。具体的には curl コマンドを使って手元の画像ファイル(でなくてもよい)をアップロードし、URL を指定して画像を表示できるようにする、というものです。

とりあえず以下の作業を行うには IBM Cloudant のデータベースと curl コマンドが必要です。前者は IBM Cloud のライトアカウントを使って無料で入手することも可能です。IBM Cloud にログイン後、IBM Cloudant のインスタンスを(無料であれば Lite プランで)作成しておいてください:
2018101601


また、以下のコマンド実行時に必要になるため、この Cloudant サービスのアクセス情報を確認しておきます。サービスを選択し、「サービス資格情報」と書かれたタブを選んで、「資格情報の表示」をクリックします(「資格情報の表示」がなかったら新規に作成してからクリックします):
2018101603


すると以下のような表示が画面下に出てきます:
2018101604


この中の username の値と、password の値を後でコマンド実行する際に指定することになります。どこかにメモしておくか、コピー&ペーストできるようにしておいてください。


また curl コマンドは使う方のシステムにあわせて適宜入手してください。Linux や MacOS であればたいてい標準で入っているはずですが、Windows の場合は別途インストールが必要です。私はこちらのをダウンロードしてインストールしました:
https://curl.haxx.se/download.html

実際に以下の作業を行う前に、IBM Cloudant 内に今回の作業の対象となるデータベースを用意します。今回は testdb という名称のデータベースを1つ用意しました。このデータベースを簡易ファイルサーバー代わりに使う方法を紹介します:
2018101602


まずはファイルを保存する手順です。今回は仮に cloudant.png という名前の以下のような画像ファイルを保存することにします:
cloudant1


コマンドプロンプト(Linux や MacOS の場合はターミナル)を開き、この cloudant.png が存在しているフォルダに移動して、以下のコマンドを入力します:
$ curl https://(username の値):(password の値)@(username の値).cloudant.com/testdb/(作成するドキュメントの _id)/(作成するアタッチメントの名称) -X PUT -H "Content-Type: image/png" --data-binary @cloudant.png

仮に username の値が "user1"、password の値が "pass1"、ドキュメントIDを "doc001"、アタッチメント名を "att001" とすると以下のようなコマンドになります:
$ curl https://user1:pass1@user1.cloudant.com/testdb/doc001/att001 -X PUT -H "Content-Type: image/png" --data-binary @cloudant.png

このコマンドが成功すると以下のような JSON が返されます:
{ "ok": true, "id": "doc001", "rev": "1-xxxxxxxx" }


コマンド実行が成功すると同じディレクトリにある cloudant.png ファイルを Cloudant の testdb にアップロードします。なお Content-Type の異なるファイルをアップロードする場合は -H オプションで指定する Content-Type を変えて指定してください。


Cloudant に保存したファイルを取り出す場合は以下の URL をブラウザで指定します:
https://(username の値):(password の値)@(username の値).cloudant.com/testdb/(作成するドキュメントの _id)/(作成するアタッチメントの名称)

同様に username の値が "user1"、password の値が "pass1"、ドキュメントIDを "doc001"、アタッチメント名を "att001" とすると以下のような URL になります:
https://user1:pass1@user1.cloudant.com/testdb/doc001/att001

ブラウザのアドレス欄に指定すると、こんな感じで表示できます:
2018101605



Cloudant のアタッチメント機能を使った簡易的なファイル保存のやりかたを紹介しました。Node-RED でウェブページを作る際の静的画像をどうするか、という問題を比較的簡単に解決する方法の1つだと思っています。


これらの記事の続きです(シリーズとしては今回が最終回です):
Cloudant の便利な API (1) : バルクインサート
Cloudant の便利な API (2) : View Design Document
Cloudant の便利な API (3) : List Design Document

DBaaS である IBM Cloudant の便利で特徴的な API を紹介しています。前回までは Design Document の1つである View Design Document と List Design Document を紹介して、データベース内の特定の条件を満たすドキュメントデータだけを「ビュー」としてまとめ、かつそのビューの UI も格納ドキュメントの一部として定義する、という内容を紹介しました(47都道府県のドキュメントデータから海なし県だけを取り出して HTML の UI で表示する、というところまでを作りました):
2017100602

↑ https://username.cloudant.com/mydb/_design/nosea/_list/nosea/nosea にアクセスした結果(username は Cloudant インスタンスに接続するための username です)

最終回である今回は、この一覧からクリックした各ドキュメントも HTML で表示するための Show Design Document と、その API を紹介します。

前回のおさらいとして、このビューで表示される各ドキュメントをクリックすると、以下の URL に移動します(現時点ではエラーになります):
https://username.cloudant.com/mydb/_design/nosea/_show/nosea/(doc.id)

これは doc.id で示される id 値を持つドキュメントデータを nosea という Show Design Document の設計内容を使って表示する際の URL です(そして現時点では nosea という Show Design Document を定義していないためエラーになります)。

ではこの nosea Show Design Document を定義します。前回までに紹介した View Design Document と List Design Document を含む JSON の内容に赤字部分を加えて以下のようなドキュメント(nosea_show.json)を作ります:
{
 "language": "javascript",
 "views": {
  "nosea": {
   "map": "function(doc){ if( doc.code && [9,10,11,19,20,21,25,29].indexOf(doc.code) > -1 ){ emit( doc._id, {code:doc.code,prefecture:doc.prefecture,capital:doc.capital,lat:doc.lat,lng:doc.lng} ); } }"
  }
 },
 "lists": {
  "nosea": "function( head, row ){ start( { 'headers': { 'content-type': 'text/html' } } ); send( '<ul>' ); var row; while( row = getRow() ){  var url = '../../_show/nosea/';  send( ' <li><a href=\"' + url + row.id + '\">' + row.value.prefecture + '(' + row.value.capital + ')</a></li>' ); } send( '</ul>' );}"
 },
 "shows": {
  "nosea": "(function( doc, req ){ if( doc ){  var str = '<h2>' + doc.prefecture + '</h2><h3>' + doc.capital + '</h3><hr/>緯度: ' + doc.lat + '<br/>経度: ' + doc.lng;  return str; }else{  return 'empty'; }})"
 }
}

赤字部分が Show Desgin Document に相当する部分です。この中では同ファイル内の上位部分で定義された nosea ビューからクリックされた各ドキュメントが表示する際に実行される処理が記載されています。上記例では理解しやすさを優先して、ドキュメントの各属性(県名、県庁所在地名、緯度、経度)を単純に HTML で表示する内容にしています。

なお、この内容と同じファイル(nosea_show.json)をこちらからダウンロードできるよう用意しました:
https://raw.githubusercontent.com/dotnsf/samples/master/nosea_show.json


前回同様に、この JSON ファイルを指定して Design Document を更新します。まずは API で現在の Design Document を確認し、現在のリビジョンを確認します:
$ curl -u "username:password" -XGET "https://username.cloudant.com/mydb/_design/nosea"

{"_id":"_design/nosea","_rev":"YYYY..YYYY","language":"javascript", ... }

この例の場合の YYYY..YYYY 部分("_rev" の値)が現在のリビジョン ID なので、この値をダウンロードした nesea_show.json ファイルに追加します:
{
 "_rev": "YYYY..YYYY",
 "language": "javascript",
 "views": {
  "nosea": {
   "map": "function(doc){ if( doc.code && [9,10,11,19,20,21,25,29].indexOf(doc.code) > -1 ){ emit( doc._id, {code:doc.code,prefecture:doc.prefecture,capital:doc.capital,lat:doc.lat,lng:doc.lng} ); } }"
  }
 },
 "lists": {
  "nosea": "function( head, row ){ start( { 'headers': { 'content-type': 'text/html' } } ); send( '<ul>' ); var row; while( row = getRow() ){  var url = '../../_show/nosea/';  send( ' <li><a href=\"' + url + row.id + '\">' + row.value.prefecture + '(' + row.value.capital + ')</a></li>' ); } send( '</ul>' );}"
 },
 "shows": {
  "nosea": "(function( doc, req ){ if( doc ){  var str = '<h2>' + doc.prefecture + '</h2><h3>' + doc.capital + '</h3><hr/>緯度: ' + doc.lat + '<br/>経度: ' + doc.lng;  return str; }else{  return 'empty'; }})"
 }
}

これで更新用の nosea_show.json ファイルが完成しました。改めてこのファイルを指定して Design Document を更新します:
$ curl -u "username:password" -XPUT -H "Content-Type: application/json" "https://username.cloudant.com/mydb/_design/nosea" -d@nosea_show.json

{"ok":true, "id":_design/nosea", "rev":"ZZZZ..ZZZZ"}

これで Design Document が更新されました。改めてウェブブラウザで https://username.cloudant.com/mydb/_design/nosea/_list/nosea/nosea にアクセスすると、List Design Document で定義された内容に従って nosea ビューが表示されます(ここまでは前回と同様):
2017100602


そしていずれかのドキュメント(県名)をクリックすると、そのドキュメントデータが Show Design Document に定義された内容で表示されます。これで一覧から詳細情報まで表示するアプリケーションとして繋がりました!:
2017100604


先程は Design Document の理解のため比較的シンプルな Show Design Document を使いましたが、少し UI にも凝ったバージョンも用意しました:
https://raw.githubusercontent.com/dotnsf/samples/master/nosea_show2.json

こちらの JSON ファイルをダウンロードして、上記同様にリビジョン ID を調べて追加し、API で PUT すると、少しだけ凝った UI で海なし県の一覧と詳細画面を確認することができます(View Design Document は変更せずに、List Design Document と Show Design Document を変更したものです)。

nosea_show2.json を PUT した場合、ビューはテーブル形式で表示されます。県名または県庁所在地名がクリック可能になっています:
2017100605


クリックすると OpenStreetMap API を使って、その県庁所在地周辺の地図が表示されます。海なし県の海までの遠さを視覚的にも確認できる UI にしました(笑):
2017100606



4回に渡って IBM Cloudant の特徴的な Design Document とその API を中心に紹介してきました。以前にも触れましたが、IBM Cloudant のベースとなった CouchDB は IBM Notes/Domino と似た生まれであり、その設計思想などにも似た部分が多くあると感じています。私自身がノーツ大好きということもあってそんな Cloudant の特徴的な機能をまとめて紹介してみました。


これらの記事の続きです:
Cloudant の便利な API (1) : バルクインサート
Cloudant の便利な API (2) : View Design Document


DBaaS である IBM Cloudant の便利で特徴的な API を紹介しています。前回は Design Document の1つである View Design Document とその API を紹介して、データベース内の特定の条件を満たすドキュメントデータだけを「ビュー」としてまとめる方法を紹介しました。

今回紹介するのはやはり Design Document の1つで、「ビューの見た目を定義する」List Design Document です。前回の「ビュー」がドキュメント集合の条件を定義していたのに対して、今回の「リスト」は「ビューの見た目」を定義します。


一般的にデータベースを使ったウェブアプリケーションを作る場合、データベースサーバーにデータを格納した上で、そのデータを読み/書き/更新/検索して画面に表示する部分はアプリケーションサーバーに実装されることが多いです。しかし(HTTP をベースとした) REST API で動かす Cloudant は、その HTTP 部分を更に活用してデータの見た目に関わる部分までを定義・実装することができます。特に今回紹介する List Design Document ではデータの集合体であるビューの見た目を実装して格納します。個別の文書の見た目を実装する Show Design Document については次回紹介する予定です。


※余談ですが、この「見た目を定義する情報がドキュメントの一部として格納される」という点も「ノーツのビューやフォーム」に近い考え方や設計だと思っています。

このリスト(List)も Design Document の1つなので、ビューと同様に設計文書を用意します。前回紹介した View Design Document の JSON の内容に赤字部分を加えて以下のようなドキュメント(nosea_list.json)を作ります:

{
 "language": "javascript",
 "views": {
  "nosea": {
   "map": "function(doc){ if( 

doc.code && [9,10,11,19,20,21,25,29].indexOf(doc.code) > -1 ){ emit( doc._id, 

{code:doc.code,prefecture:doc.prefecture,capital:doc.capital} ); } }"
  }
 },
 "lists": {
  "nosea": "function( head, row ){ 

start( { 'headers': { 'content-type': 'text/html' } } ); send( '<ul>' ); var row; while( row = getRow() ){  var url = 

'../../_show/nosea/';  send( ' <li><a href=\"' + url + row.id + '\">' + row.value.prefecture + '(' + row.value.capital + ')

</a></li>' ); } send( '</ul>' );}"
 }
}

赤字部分が List Desgin Document に相当する部分です。この中では同ファイル内の上位部分で定義された nosea ビューを表示する際に実行される処理が記載されています。

なお、この内容と同じファイル(nosea_list.json)をこちらからダウンロードできるよう用意しました:
https://raw.githubusercontent.com/dotnsf/samples/master/nosea_list.json


赤字部分について簡単に内容を紹介すると、まず HTTP レスポンスヘッダとして 'Content-Type: text/html' を指定し、全体が HTML として返されるよう宣言しています。次に <ul> と </ul> の間が while でループ処理され、このビューに含まれる各文書(doc)の値を row として取り出します。そして
  <li><a href="../../_show/nosea/(doc.id)">県名(県庁所在地名)</a></li>
という <li> 要素を動的に作って <ul> と </ul> の間に挿入し、最後に全体を返しています。 要するに nosea ビューに含まれる海無し県の一覧を <ul> タグを使ってリスト表示している、というものです。

この JSON ファイルを指定して API を実行すれば List Design Document が作成される、、、のですが、もう1つだけ準備が必要です。今回作成する Design Document は前回作成した Design Document を上書きして保存する必要があります。そのため単にこのままのファイルを指定して API を実行しても(同一 ID への新規作成コマンドと解釈され)コンフリクトを起こしてしまい、更新できません。Cloudant の既存ドキュメントを更新する場合は既存ドキュメントのリビジョン ID を明示して実行する必要があるのでした。

というわけで、まずは API で現在の Design Document を確認し、現在のリビジョンを確認します。前回同様、コマンド内の username, password はそれぞれ Cloudant インスタンスに接続するための username, password で、実行結果を青字で表しています(以下同様):
$ curl -u "username:password" -XGET "https://username.cloudant.com/mydb/_design/nosea"

{"_id":"_design/nosea","_rev":"XXXX..XXXX","language":"javascript","views":{"nosea":{"map":"function(doc){ if( doc.code && [9,10,11,19,20,21,25,29].indexOf(doc.code) > -1 ){ emit( doc._id, {code:doc.code,prefecture:doc.prefecture,capital:doc.capital} ); } }"}}}

この例の場合の XXXX..XXXX 部分("_rev" の値)が現在のリビジョンIDです。nosea_list.json をテキストエディタで開いて、この値を使って以下のように書き換えます:
{
 "_rev": "xxxx..xxxx",
"language": "javascript", "views": { "nosea": { "map": "function(doc){ if( doc.code && [9,10,11,19,20,21,25,29].indexOf(doc.code) > -1 ){ emit( doc._id, {code:doc.code,prefecture:doc.prefecture,capital:doc.capital} ); } }" } }, "lists": { "nosea": "function( head, row ){ start( { 'headers': { 'content-type': 'text/html' } } ); send( '<ul>' ); var row; while( row = getRow() ){ var url = '../../_show/nosea/'; send( ' <li><a href=\"' + url + row.id + '\">' + row.value.prefecture + '(' + row.value.capital + ') </a></li>' ); } send( '</ul>' );}" } }

これで更新用の nosea_list.json ファイルが完成しました。改めてこのファイルを指定して Design Document を更新します:
$ curl -u "username:password" -XPUT -H "Content-Type: application/json" "https://username.cloudant.com/mydb/_design/nosea" -d@nosea_list.json

{"ok":true, "id":_design/nosea", "rev":"YYYY..YYYY"}

これで Design Document が更新されました。この時点でダッシュボードの nosea ビューを確認すると・・・一見、何も変わっていないようにみえます:
2017100601


しかしウェブブラウザで https://username.cloudant.com/mydb/_design/nosea/_list/nosea/nosea にアクセスすると、List Design Document で定義された内容に従って nosea ビューが表示されます。結果としてこのような画面が表示されます:
2017100602


これが Cloudant(CouchDB) の特徴ともいえる Design Document の機能です。通常であれば別途アプリケーションサーバーを用意した上で UI やロジックを実現するのですが、Cloudant の場合は Design Document を使うことで Cloudant の REST API(HTTP) の機能を使って UI を実現することができるようになるのでした。 ただ上のUIはシンプルすぎるので実用段階ではもう少しちゃんとしたほうがいいとは思います(苦笑)。

なお、この結果表示されている画面の各海なし都道府県へのリンクをクリックすると、以下のようなエラーになります:
2017100603


これはビューの UI とリンクまでは作成したのですが、各ドキュメントの内容(UI)を表示する Design Document についてはまだ作成していないために(リンク先が見当たらずに)エラーとなっているのでした。というわけで、次回は各ドキュメントの UI を定義する Show Design Document とその API について紹介し、各ドキュメントの内容まで表示できるようなアプリケーションを完成させる予定です。



このページのトップヘ