Raspberry PiでKubernetes The Hard Way(v1.21.0)をやる(失敗)

はじめに

最近、Raspberry Piを買ってKubeadmを使ってKubernetesクラスタを構築してみたので今度は、「Kubernetes The Hard Way」をやってみようと思います。

Kubernetes The Hard Way自体は、VirutalBoxを使ってやったみたことはあったのですが、その当時はほぼ手順をなぞっただけで終わってしまったので今回はきちんと理解を整理しながらやってみようかと思います。

....と思ったのですが、最終的には失敗してしまいました。 クラスターのネットワークの設定にflannelを使ったのですが、Podのネットワークにサブネットが割当されず。
クラスター側のサブネットとかぶってないかとか諸々確認したのですが、うまく行かず。一旦は断念しました。 とはいえせっかくまとめたメモとかを、そのまま消してしまうのももったいないので、最後に雑にまとめて投稿だけしようと思います。
ちょっと別の理由で環境を壊してしまった都合上、問題の分析なども行えていない(本当に、どういうログが出てて困ったのかすらも載せてない)中途半端なブログになっているので、もし何かを期待してみられる方がいるのであればおそらくこのブログは期待に添えるものではありません。

やってみる

環境構築

クライアントマシン

Kubernetesクラスタはラズパイ上に構築しますが、それぞれのラズパイにsshしたり必要なリソースを作成したりするためにThinkPadのPCを用います。
環境は以下のとおりです。

$ uname -srvmpio
Linux 5.4.0-72-generic #80-Ubuntu SMP Mon Apr 12 17:35:00 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
LSB Version:    core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.2 LTS
Release:    20.04
Codename:   focal

Raspberry Piともろもろ

ラズベリーパイ含め今回は以下のものを使って物理的な環境を構築します。
もともと家にあったのもありますが、PC以外で全部で3万円強ぐらい だったと思います。
Aamazonのリンクを貼っているものは実際に自分が購入したものです。

名前 説明 個数
Raspberry Pi 3 Model B+ 1GのRAMを持つラズパイです。秋葉原でリフレッシュ品を2つ適当に購入しました。こちらはサブNodeとして使おうと思います。 2
Raspberry Pi 4 ModelB 4GB 4GのRAMを持つラズパイです。Amazonで購入しました。こちらはMasterのNodeとして使おうと思います。 1
Micro SD 32G ラズパイのディスクとして使用します。Aamazonで購入しました。 3
ルータ親機 ラズパイをWiFiに接続したかったので、Aamazonで購入しました。 1
ロジテック スイッチングハブ 5ポート ラズパイでネットワークを構築するためにAamazonで買いました。 1
AUKEY USB充電器 50W/10A ACアダプター 5ポート ラズパイと諸々の電源として使います。 1
Raspberry Pi 4 ケース 4とありますが、3でも使えます。4段構成で、1つの段は電源を置くために使います。
Micro-USBケーブル Raspberry Pi 3、スイッチングパブ、ルータ親機の電源コードとして使います。配線が邪魔にならないように短めのものを使います。 4
USB type C Raspberry Pi 4の電源コードとして使います。配線が邪魔にならないように短めのものを使います 1
LANケーブル ラズパイとハブ、ハブとルータ親機をそれぞれつなぎます。 4
HDMIケーブル ディスプレイとラズパイをつなぎます。 1
Micro HDMI変換コネクタ Raspberry Pi 4を画面につなぐために使いました 1
ThinkPad T490 もともと持ってたPCです。Micro SDにOSのイメージを焼いたり、sshしてラズパイに諸々インストールしたりするのに使います。 1
ディスプレイ もともと持っていたものを使います 1

配線する

詳細に説明はしませんが、最終的には以下のように配線します。

f:id:yuya_hirooka:20210505115247p:plain

ラズパイ⇨電源、スイッチングハブ スイッチングハブ⇨電源、ルータ親機 ルータ親機⇨電源 ThinkPadスイッチングハブ

また、ルータ親機に関しては説明書を読みつつ自宅の無線LANに接続できるようにしておきます。
画像では電源もすでに入ってますが、この段階では大本の電源のケーブルを抜いておき、まだ電源を入れないようにしておきます。
Micro SDカードをラズパイに入れてから電源を指します。

Micro SDにUbuntuをインストールする

配線ができたら次にMicro SDにOSのイメージを焼いていきます。 今回はOSはUbuntuを使いたいと思います。Raspberry Pi 3には「Ubuntu Server 18.04.5 Arm64」のイメージ、Raspberry Pi 4には「Ubuntu Server 20.04.2 Arm64」を利用します。
それぞれ以下のページからダウンロードしてきます。

ダウンロードしてきたイメージをSDカードに焼きます。Micro SDの場合は/dev/mmcblk0にイメージを焼けば良さそうです。 以下のコマンドをそれぞれのSDカードをPCの組み込みSDカードドライブに差し込んでから実行します。

$ xzcat ubuntu-18.04.5-preinstalled-server-arm64+raspi3.img.xz | sudo dd bs=4M of=/dev/mmcblk0
0+318958 レコード入力
0+318958 レコード出力
2653289472 bytes (2.7 GB, 2.5 GiB) copied, 29.1715 s, 91.0 MB/s

Raspberry Pi 4に利用するSDカードにはubuntu-20.04.2-preinstalled-server-arm64+raspi.img.xzという名前のイメージがダウンロードされると思うので、同じコマンドを実行してください。

3枚のMicro SDカードにイメージを焼き終わったら、ラズパイに差し込んで電源を入れます。
また、ディスプレイと 電源を入れるとUbuntuのインストールがはじまります。
しばらく待つと初期ユーザネーム/パスワードを聞かれるので、ubuntu/ubuntuと入力してください。
初回のパスワード変更では任意のものを入力してください。 同様の作業をすべのSDカードで行ってください「Ubuntu20+Raspberry Pi 4」でも同様のセットアップを行ってください。

Ubuntuが起動したら、この後、諸々わかりやすくするためにホスト名を変更します。
それぞれのラズパイで/etc/hostnameを編集してホスト名を以下のように変更し、rebootコマンドで再起動します。

ホスト名 ラズパイ
leader-01 Raspberry Pi 4
worker-01 Raspberry Pi 3
worker-02 Raspberry Pi 3

今後の作業はsshして行なうため、hostname -Iコマンド等を使って、それぞれのIPも調べておきます。
僕の環境では以下のような対応付になりました。

ホスト名 ip
leader-01 192.168.13.5
worker-01 192.168.13.2
worker-02 192.168.13.3

これで、基本のところのセットアップは完了です。

クライアントツールのインストール

ThinkPadの方でクライアントツールをインストールしておきます。
インストールするのは以下のツールです。

  • kubectl
    • いわずもがな
  • cfssl、cfssljson

kubectlのインストール

まずはkubectlをインストールします。
以下のコマンドを実行してください。

$ wget https://storage.googleapis.com/kubernetes-release/release/v1.21.0/bin/linux/amd64/kubectl
$ chmod +x kubectl
$ sudo mv kubectl /usr/local/bin/

これで、インストールはOKです。
ちなみに諸事上で、僕の環境ではkubectlはv1.20.5のものを使います。

$ kubectl version --client
Client Version: version.Info{Major:"1", Minor:"20", GitVersion:"v1.20.5", GitCommit:"6b1d87acf3c8253c123756b9e61dac642678305f", GitTreeState:"clean", BuildDate:"2021-03-18T01:10:43Z", GoVersion:"go1.15.8", Compiler:"gc", Platform:"linux/amd64"}

ドキュメントによるとkubectlは2つのバージョンがサポートされるようです。

cfssl、cfssljsonのインストール

cfsslとcfljsonはPKI公開鍵暗号基盤)を構築するために利用します。

$ wget -q --show-progress --https-only --timestamping \
  https://storage.googleapis.com/kubernetes-the-hard-way/cfssl/1.4.1/linux/cfssl \
  https://storage.googleapis.com/kubernetes-the-hard-way/cfssl/1.4.1/linux/cfssljson

$ chmod +x cfssl cfssljson

正しくインストールができていれば以下のコマンドでそれぞれバージョンが表示されるはずです。

$ cfssl version
Version: 1.4.1
Runtime: go1.12.12

$ cfssljson --version
Version: 1.4.1
Runtime: go1.12.12

これでクライアントツールのインストールは完了です。

CAのプロビジョニングとTLS証明書の発行

etcd、kube-apiserver、kubelet、kube-proxyのコンポーネント間の通信で利用されるTSL証明書の作成などを行います。

CAのプロビジョンを行なうために、CAの設定ファイルを作成します。

cat > ca-config.json <<EOF
{
  "signing": {
    "default": {
      "expiry": "8760h"
    },
    "profiles": {
      "kubernetes": {
        "usages": ["signing", "key encipherment", "server auth", "client auth"],
        "expiry": "8760h"
      }
    }
  }
}
EOF

次に、CAのための証明書署名リクエストのJsonを作成します。

cat > ca-csr.json <<EOF
{
  "CN": "Kubernetes",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "US",
      "L": "Portland",
      "O": "Kubernetes",
      "OU": "CA",
      "ST": "Oregon"
    }
  ]
}
EOF

そして、CSの証明書と秘密鍵を作成します。

$ cfssl gencert -initca ca-csr.json | cfssljson -bare ca

すると以下のファイルが作成されているのを確認します。

  • ca-key.pem
  • ca.pem

クライアントとサーバの証明書発行

Kubernetesの各コンポーネントとクライアント(adminユーザ)のための証明書を発行します。

Admin クライアントの証明書の発行

Adminクライアントのための証明書署名リクエストのJsonを作成します。

cat > admin-csr.json <<EOF
{
  "CN": "admin",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "US",
      "L": "Portland",
      "O": "system:masters",
      "OU": "Kubernetes The Hard Way",
      "ST": "Oregon"
    }
  ]
}
EOF

cfssl gencertコマンドを用いて、Adminクライアントの秘密鍵と証明書を発行します。

$ cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -profile=kubernetes \
  admin-csr.json | cfssljson -bare admin

その際に前に使っているCAの証明書および秘密鍵を利用します。
実行すると以下のファイルが作成されているのを確認します。

  • admin-key.pem
  • admin.pem

kubeletの証明書の発行

Kubernetesはkubeletからのリクエストの認可を行なうためにNode Authorizerと呼ばれるような特別な認可モードを利用します(詳細はこちら)。
werkerノードがNode Authriozerのリクエストを発行するための証明書を作成します。
今回はwerkerノードが2つなので以下のコマンドで証明書を発行します。

$ cat > worker-01-csr.json <<EOF
{
  "CN": "system:node:worker-01",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "US",
      "L": "Portland",
      "O": "system:nodes",
      "OU": "Kubernetes The Hard Way",
      "ST": "Oregon"
    }
  ]
}
EOF


$  cfssl gencert \
   -ca=ca.pem \
   -ca-key=ca-key.pem \
   -config=ca-config.json \
   -hostname=worker-01,192.168.13.2 \
   -profile=kubernetes \
   worker-01-csr.json | cfssljson -bare worker-01

ここで、Node Authriozerでリクエストを送るためには、system:node:worker-01worker-01の部分はノードのホスト名にする必要があります。
同様のことをworker-02でも行います。worker-01の部分を単純置換してcfssl genecertコマンドの-hostnameオプションに渡すIPは 192.168.13.3になります(手元の環境でそれぞれのIPは確認してください)。
以下の4つのファイルができると思います。

  • worker-01-key.pem
  • worker-01.pem
  • worker-02-key.pem
  • worker-02.pem

Controller Managerの証明書の発行

kube-controller-managerにも証明書を発行します。

$ cat > kube-controller-manager-csr.json <<EOF
{
  "CN": "system:kube-controller-manager",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "US",
      "L": "Portland",
      "O": "system:kube-controller-manager",
      "OU": "Kubernetes The Hard Way",
      "ST": "Oregon"
    }
  ]
}
EOF

$ cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -profile=kubernetes \
  kube-controller-manager-csr.json | cfssljson -bare kube-controller-manager

kube-proxyの証明書の発行

今までと同様の方法でkube-proxyにも証明書を発行します。

$ cat > kube-proxy-csr.json <<EOF
{
  "CN": "system:kube-proxy",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "US",
      "L": "Portland",
      "O": "system:node-proxier",
      "OU": "Kubernetes The Hard Way",
      "ST": "Oregon"
    }
  ]
}
EOF

$ cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -profile=kubernetes \
  kube-proxy-csr.json | cfssljson -bare kube-proxy

以下のファイルが作成されます。

  • kube-proxy-key.pem
  • kube-proxy.pem

Scheduler Clientの証明書の発行

Schedulerの証明書を発行します。

$ cat > kube-scheduler-csr.json <<EOF
{
  "CN": "system:kube-scheduler",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "US",
      "L": "Portland",
      "O": "system:kube-scheduler",
      "OU": "Kubernetes The Hard Way",
      "ST": "Oregon"
    }
  ]
}
EOF

$ cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -profile=kubernetes \
  kube-scheduler-csr.json | cfssljson -bare kube-scheduler

以下のファイルが作成されます。

  • kube-scheduler-key.pem
  • kube-scheduler.pem

Kubernetes APIサーバの証明書の発行

APIサーバの証明書を発行します。 APIサーバはleader-01にデプロイするため、IPなどは、そちらのものを利用します。

$ cat > kubernetes-csr.json <<EOF
{
  "CN": "kubernetes",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "US",
      "L": "Portland",
      "O": "Kubernetes",
      "OU": "Kubernetes The Hard Way",
      "ST": "Oregon"
    }
  ]
}
EOF

$ export KUBERNETES_PUBLIC_ADDRESS=192.168.13.5

$ export KUBERNETES_HOSTNAMES=kubernetes,kubernetes.default,kubernetes.default.svc,kubernetes.default.svc.cluster,kubernetes.svc.cluster.local

$ cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -hostname=10.32.0.1,10.240.0.10,10.240.0.11,10.240.0.12,${KUBERNETES_PUBLIC_ADDRESS},127.0.0.1,${KUBERNETES_HOSTNAMES} \
  -profile=kubernetes \
  kubernetes-csr.json | cfssljson -bare kubernetes

次のファイルが作成されると思います。

Service Accountの証明書を発行します。

Service Accountの証明書を発行します。

$ cat > service-account-csr.json <<EOF
{
  "CN": "service-accounts",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "US",
      "L": "Portland",
      "O": "Kubernetes",
      "OU": "Kubernetes The Hard Way",
      "ST": "Oregon"
    }
  ]
}
EOF


$ cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -profile=kubernetes \
  service-account-csr.json | cfssljson -bare service-account

}

作成した証明書のNodeへの配置

作成した証明書を各ノードに配置しておきます。

$ scp ca.pem ca-key.pem kubernetes.pem kubernetes-key.pem  ubuntu@192.168.13.5:~/

$ scp ca.pem ca-key.pem worker-01-key.pem worker-01.pem ubuntu@192.168.13.2:~/

$ scp ca.pem ca-key.pem worker-02-key.pem worker-02.pem ubuntu@192.168.13.3:~/

認証のためのkubeconfigsの作成

KubernetesクライアントがKubernetes API Serverを発見し、認証を行なうためのkubeconfigsファイルを作成する必要があります。
kubeconfigはkubeletkube-proxyscheduleradminユーザcontroller manager用に作成します。

Kubeletのkubeconfigファイルを作成する

以下のコマンドでそれぞれのworker Nodeで動くkubeletのためのconfigファイルを作成します。
kubeletが正しく認可されるためにはkubeconfigファイルの作成にはNodeのホスト名を利用する必要があります。
また、KUBERNETES_PUBLIC_ADDRESSにはAPIサーバが動くノードのIPをセットしておきます(今回は前の工程ですでにexportしているのでそのまま利用します)。

$ for instance in worker-01 worker-02 ; do
  kubectl config set-cluster kubernetes-the-hard-way \
    --certificate-authority=ca.pem \
    --embed-certs=true \
    --server=https://${KUBERNETES_PUBLIC_ADDRESS}:6443 \
    --kubeconfig=${instance}.kubeconfig

  kubectl config set-credentials system:node:${instance} \
    --client-certificate=${instance}.pem \
    --client-key=${instance}-key.pem \
    --embed-certs=true \
    --kubeconfig=${instance}.kubeconfig

  kubectl config set-context default \
    --cluster=kubernetes-the-hard-way \
    --user=system:node:${instance} \
    --kubeconfig=${instance}.kubeconfig

  kubectl config use-context default --kubeconfig=${instance}.kubeconfig
done

以下のファイルが作成されたと思います。

  • worker-01.kubeconfig
  • worker-02.kubeconfig

kube-proxyのkubeconfigファイルを作成する

次はkube-proxyのファイルを作成します。
以下のコマンドを実行します。

$ kubectl config set-cluster kubernetes-the-hard-way \
    --certificate-authority=ca.pem \
    --embed-certs=true \
    --server=https://192.168.13.5:6443 \
    --kubeconfig=kube-proxy.kubeconfig

 $ kubectl config set-credentials system:kube-proxy \
    --client-certificate=kube-proxy.pem \
    --client-key=kube-proxy-key.pem \
    --embed-certs=true \
    --kubeconfig=kube-proxy.kubeconfig

 $ kubectl config set-context default \
    --cluster=kubernetes-the-hard-way \
    --user=system:kube-proxy \
    --kubeconfig=kube-proxy.kubeconfig

 $ kubectl config use-context default --kubeconfig=kube-proxy.kubeconfig

以下のファイルが作成されたと思います。

  • kube-proxy.kubeconfig

kube-controller-managerのkubeconfigファイルを作成する

次はkube-controller-managerのファイルを作成します。
以下のコマンドを実行します。

$ kubectl config set-cluster kubernetes-the-hard-way \
    --certificate-authority=ca.pem \
    --embed-certs=true \
    --server=https://127.0.0.1:6443 \
    --kubeconfig=kube-controller-manager.kubeconfig

$ kubectl config set-credentials system:kube-controller-manager \
    --client-certificate=kube-controller-manager.pem \
    --client-key=kube-controller-manager-key.pem \
    --embed-certs=true \
    --kubeconfig=kube-controller-manager.kubeconfig

$ kubectl config set-context default \
    --cluster=kubernetes-the-hard-way \
    --user=system:kube-controller-manager \
    --kubeconfig=kube-controller-manager.kubeconfig

$ kubectl config use-context default --kubeconfig=kube-controller-manager.kubeconfig

以下のファイルが作成されたと思います。

  • kube-controller-manager.kubeconfig

kube-scheduler.kubeconfigのkubeconfigファイルを作成する

次はkube-schedulerのファイルを作成します。
以下のコマンドを実行します。

$ kubectl config set-cluster kubernetes-the-hard-way \
    --certificate-authority=ca.pem \
    --embed-certs=true \
    --server=https://192.168.13.5:6443 \
    --kubeconfig=kube-scheduler.kubeconfig

$ kubectl config set-credentials system:kube-scheduler \
    --client-certificate=kube-scheduler.pem \
    --client-key=kube-scheduler-key.pem \
    --embed-certs=true \
    --kubeconfig=kube-scheduler.kubeconfig

$ kubectl config set-context default \
    --cluster=kubernetes-the-hard-way \
    --user=system:kube-scheduler \
    --kubeconfig=kube-scheduler.kubeconfig

$ kubectl config use-context default --kubeconfig=kube-scheduler.kubeconfig

以下のファイルが作成されました。

  • kube-scheduler.kubeconfig

adminのkubeconfigファイルを作成する

次はadminのファイルを作成します。
以下のコマンドを実行します。

$ kubectl config set-cluster kubernetes-the-hard-way \
    --certificate-authority=ca.pem \
    --embed-certs=true \
    --server=https://127.0.0.1:6443 \
    --kubeconfig=admin.kubeconfig

$ kubectl config set-credentials admin \
    --client-certificate=admin.pem \
    --client-key=admin-key.pem \
    --embed-certs=true \
    --kubeconfig=admin.kubeconfig

$ kubectl config set-context default \
    --cluster=kubernetes-the-hard-way \
    --user=admin \
    --kubeconfig=admin.kubeconfig

$ kubectl config use-context default --kubeconfig=admin.kubeconfig

作成したkubeconfigのNodeへの配置

以下のコマンドを実行して、Nodeへkubeconfigへ配置します。

$ scp admin.kubeconfig kube-controller-manager.kubeconfig kube-scheduler.kubeconfig ubuntu@192.168.13.5:~/

$ scp worker-01.kubeconfig kube-proxy.kubeconfig ubuntu@192.168.13.2:~/

$ scp worker-02.kubeconfig kube-proxy.kubeconfig  ubuntu@192.168.13.3:~/

データ暗号化の設定と鍵を作成する

Kubernetesクラスターの状態、アプリケーションコンフィグ、Sercretとなどの様々なデータをストアしています。
Kubernetesクラスターのデータを利用されていないときに暗号化して保持する機能を持っています。
ここでは、暗号化の設定と鍵を作成します。

まずは暗号化するための鍵を作成します。

$ ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)

次に設定ファイルを作成ます。

$ cat > encryption-config.yaml <<EOF
kind: EncryptionConfig
apiVersion: v1
resources:
  - resources:
      - secrets
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: ${ENCRYPTION_KEY}
      - identity: {}
EOF

作ったencryption-config.yamlをleaderのNodeに配置します。

$ scp encryption-config.yaml ubuntu@192.168.13.5:~/

Bootstrapping the etcdの起動

諸々の設定ファルの作成とその配置ができたので、コンポーネントを起動していきます。
まずは、etcdからです。
etcdは分散型のkey-valueストアでKubernetesのすべてのクラスターの情報の保存場所として利用されています。
通常はクラスターを組むと思いますが、今回はleaderのNodeは1つのためひとつだけ起動します。
leaderのNodeにsshします。

$ ssh ubuntu@192.168.13.5

ログインに成功したらセットアップを行っていきます。
まずは、etcdのバイナリをダウンロードします。

$ wget -q --show-progress --https-only --timestamping   \
   "https://github.com/etcd-io/etcd/releases/download/v3.4.15/etcd-v3.4.15-linux-arm64.tar.gz"

cpuアーキテクチャはarm64を選択するように注意しましょう。
ダウンロードしたバイナリを回答して、/usr/local/binに配置します。

 $ tar -xvf etcd-v3.4.15-linux-arm64.tar.gz
 $ sudo mv etcd-v3.4.15-linux-arm64/etcd* /usr/local/bin/

etcdサーバのセットアップを行います。

$ sudo mkdir -p /etc/etcd /var/lib/etcd
$ sudo chmod 700 /var/lib/etcd
$ sudo cp ca.pem kubernetes-key.pem kubernetes.pem /etc/etcd/

systemdで管理するためにユニットファイルを作成します。
以下のコマンドを実行します。

cat <<EOF | sudo tee /etc/systemd/system/etcd.service
[Unit]
Description=etcd
Documentation=https://github.com/coreos

[Service]
Type=notify
ExecStart=/usr/local/bin/etcd \\
  --name leader-01 \\
  --cert-file=/etc/etcd/kubernetes.pem \\
  --key-file=/etc/etcd/kubernetes-key.pem \\
  --peer-cert-file=/etc/etcd/kubernetes.pem \\
  --peer-key-file=/etc/etcd/kubernetes-key.pem \\
  --trusted-ca-file=/etc/etcd/ca.pem \\
  --peer-trusted-ca-file=/etc/etcd/ca.pem \\
  --peer-client-cert-auth \\
  --client-cert-auth \\
  --initial-advertise-peer-urls https://192.168.13.5:2380 \\
  --listen-peer-urls https://192.168.13.5:2380 \\
  --listen-client-urls https://192.168.13.5:2379,https://127.0.0.1:2379 \\
  --advertise-client-urls https://192.168.13.5:2379 \\
  --initial-cluster-token etcd-cluster-0 \\
  --initial-cluster leader-01=https://192.168.13.5:2380 \\
  --initial-cluster-state new \\
  --data-dir=/var/lib/etcd
Restart=on-failure
RestartSec=5
Environment="ETCD_UNSUPPORTED_ARCH=arm64"

[Install]
WantedBy=multi-user.target
EOF

systemdでetcdを起動します。

$ sudo systemctl daemon-reload
$ sudo systemctl enable etcd
$ sudo systemctl start etcd

起動に成功していれば以下のコマンドでリストが取得できると思います。

$ sudo ETCDCTL_API=3 etcdctl member list \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/etcd/ca.pem \
  --cert=/etc/etcd/kubernetes.pem \
  --key=/etc/etcd/kubernetes-key.pem

6ceeb31f89f1fb0d, started, leader-01, https://192.168.13.5:2380, https://192.168.13.5:2379, false

Kubernetes Control Planeの起動

Control Planeを起動しいきます。

Control Planeとは

  • kube-apiserver
  • kube-scheduler
    • Podに対してNodeが割り当てられているか監視し、当てられていなかった場合はNodeの割当を行います。
  • kube-controller-manager
    • 各コントローラーのプロセスを実行します。コントローラーはコントロールループの中でクラスターの状態を監視し必要に応じて変更をくわえたり、要求したりしクラスターが望ましい状態になるように調整を行います。代表的なコントローラーには以下のようなものがあります。
      • Deploymentコントローラー
      • ReplicaSetコントローラー
      • CronJobコントローラー

コントロールプレーンの各コンポーネントのより詳細の情報に関してはこちらを確認ください。

バイナリダウンロードと配置

それではインストールしていきます。 etcd同様この作業もleaderのNodeで行います。
まずは、kubernetesの設定を配置するディレクトリを作成します。

$ sudo mkdir -p /etc/kubernetes/config

次にそれぞれのバイナリソースを取得します。
Control Planeとkubectlもインストールします。

wget -q --show-progress --https-only --timestamping \
  "https://storage.googleapis.com/kubernetes-release/release/v1.21.0/bin/linux/arm64/kube-apiserver" \
  "https://storage.googleapis.com/kubernetes-release/release/v1.21.0/bin/linux/arm64/kube-controller-manager" \
  "https://storage.googleapis.com/kubernetes-release/release/v1.21.0/bin/linux/arm64/kube-scheduler" \
  "https://storage.googleapis.com/kubernetes-release/release/v1.21.0/bin/linux/arm64/kubectl"

ダウンロードしたバイナリに実行権限を与え、適当なところに配置します。

$ chmod +x kube-apiserver kube-controller-manager kube-scheduler kubectl
$ sudo mv kube-apiserver kube-controller-manager kube-scheduler kubectl /usr/local/bin/

Kubernetes API Serverの設定を行なう

API Serverを設定していきます。

まずは証明書を配置するディレクトリを作成して、証明書をいどうさせます。

$ sudo mkdir -p /var/lib/kubernetes/

$ sudo mv ca.pem ca-key.pem kubernetes-key.pem kubernetes.pem \
   service-account-key.pem service-account.pem \
   encryption-config.yaml /var/lib/kubernetes/

API Serverをsystemdで起動したいので、ユニットファイルを作成します。

cat <<EOF | sudo tee /etc/systemd/system/kube-apiserver.service
[Unit]
Description=Kubernetes API Server
Documentation=https://github.com/kubernetes/kubernetes

[Service]
ExecStart=/usr/local/bin/kube-apiserver \\
  --advertise-address=192.168.13.5 \\
  --allow-privileged=true \\
  --apiserver-count=3 \\
  --audit-log-maxage=30 \\
  --audit-log-maxbackup=3 \\
  --audit-log-maxsize=100 \\
  --audit-log-path=/var/log/audit.log \\
  --authorization-mode=Node,RBAC \\
  --bind-address=0.0.0.0 \\
  --client-ca-file=/var/lib/kubernetes/ca.pem \\
  --enable-admission-plugins=NamespaceLifecycle,NodeRestriction,LimitRanger,ServiceAccount,DefaultStorageClass,ResourceQuota \\
  --etcd-cafile=/var/lib/kubernetes/ca.pem \\
  --etcd-certfile=/var/lib/kubernetes/kubernetes.pem \\
  --etcd-keyfile=/var/lib/kubernetes/kubernetes-key.pem \\
  --etcd-servers=https://127.0.0.1:2379 \\
  --event-ttl=1h \\
  --encryption-provider-config=/var/lib/kubernetes/encryption-config.yaml \\
  --kubelet-certificate-authority=/var/lib/kubernetes/ca.pem \\
  --kubelet-client-certificate=/var/lib/kubernetes/kubernetes.pem \\
  --kubelet-client-key=/var/lib/kubernetes/kubernetes-key.pem \\
  --runtime-config='api/all=true' \\
  --service-account-key-file=/var/lib/kubernetes/service-account.pem \\
  --service-account-signing-key-file=/var/lib/kubernetes/service-account-key.pem \\
  --service-account-issuer=https://192.168.13.5:6443 \\
  --service-cluster-ip-range=10.32.0.0/24 \\
  --service-node-port-range=30000-32767 \\
  --tls-cert-file=/var/lib/kubernetes/kubernetes.pem \\
  --tls-private-key-file=/var/lib/kubernetes/kubernetes-key.pem \\
  --v=2
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

Kubernetes Controller Managerの設定を行なう

Controller Managerの設定を行っていきます。

kube-controller-manager.kubeconfig/var/lib/kubernetesに配置します。

sudo mv kube-controller-manager.kubeconfig /var/lib/kubernetes/

Controller Managerのユニットファイルを作成します。

$ cat <<EOF | sudo tee /etc/systemd/system/kube-controller-manager.service
[Unit]
Description=Kubernetes Controller Manager
Documentation=https://github.com/kubernetes/kubernetes

[Service]
ExecStart=/usr/local/bin/kube-controller-manager \\
  --bind-address=0.0.0.0 \\
  --cluster-cidr=10.200.0.0/16 \\
  --cluster-name=kubernetes \\
  --cluster-signing-cert-file=/var/lib/kubernetes/ca.pem \\
  --cluster-signing-key-file=/var/lib/kubernetes/ca-key.pem \\
  --kubeconfig=/var/lib/kubernetes/kube-controller-manager.kubeconfig \\
  --leader-elect=true \\
  --root-ca-file=/var/lib/kubernetes/ca.pem \\
  --service-account-private-key-file=/var/lib/kubernetes/service-account-key.pem \\
  --service-cluster-ip-range=10.32.0.0/24 \\
  --use-service-account-credentials=true \\
  --v=2
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

Kubernetes Schedulerの設定を行なう

Schedulerの設定を行っていきます。
kube-scheduler.kubeconfig/var/lib/kubernetesに配置します。

$ sudo mv kube-scheduler.kubeconfig /var/lib/kubernetes/

次に、kube-scheduler.yamlを作成します。

$ sudo mkdir -p /etc/kubernetes/config/

$ cat <<EOF | sudo tee /etc/kubernetes/config/kube-scheduler.yaml
apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration
clientConnection:
  kubeconfig: "/var/lib/kubernetes/kube-scheduler.kubeconfig"
leaderElection:
  leaderElect: true
EOF

systemdのユニットファイルを作成します。

cat <<EOF | sudo tee /etc/systemd/system/kube-scheduler.service
[Unit]
Description=Kubernetes Scheduler
Documentation=https://github.com/kubernetes/kubernetes

[Service]
ExecStart=/usr/local/bin/kube-scheduler \\
  --config=/etc/kubernetes/config/kube-scheduler.yaml \\
  --v=2
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

各Control Planeの起動

コンポーネントのユニットファイルは作成したので、読み込んで起動します。

$ sudo systemctl daemon-reload
$ sudo systemctl enable kube-apiserver kube-controller-manager kube-scheduler
$ sudo systemctl start kube-apiserver kube-controller-manager kube-scheduler

起動が正常にできているか以下のコマンドで確認します。

$ kubectl cluster-info --kubeconfig admin.kubeconfig

Kubernetes control plane is running at https://127.0.0.1:6443

Kubernetes control plane is runningというのが出ていればOKっぽいです。

KubeletへのRBACの設定を行なう

API Serverが各Workerで動くKubeletへのアクセスをするためのRBACの設定を行います。
API Serverはメトリクスの収集、ログの取得、Pod内でのコマンド実行などのためにkubeletにアクセスします。

以下のコマンドを実行して、system:kube-apiserver-to-kubelet ClusterRoleを作成します。

$ cat <<EOF | kubectl apply --kubeconfig admin.kubeconfig -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  annotations:
    rbac.authorization.kubernetes.io/autoupdate: "true"
  labels:
    kubernetes.io/bootstrapping: rbac-defaults
  name: system:kube-apiserver-to-kubelet
rules:
  - apiGroups:
      - ""
    resources:
      - nodes/proxy
      - nodes/stats
      - nodes/log
      - nodes/spec
      - nodes/metrics
    verbs:
      - "*"
EOF

API ServerはKubuletにkubernetesユーザとして認証します。なので、先程作ったsystem:kube-apiserver-to-kubelet ClusterRolekubernetesユーザにバインドします。

cat <<EOF | kubectl apply --kubeconfig admin.kubeconfig -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: system:kube-apiserver
  namespace: ""
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: system:kube-apiserver-to-kubelet
subjects:
  - apiGroup: rbac.authorization.k8s.io
    kind: User
    name: kubernetes
EOF

Worker Nodeの起動

2つのworker Nodeを起動します。
Worker Nodeでは以下のようなソフトウェアが起動します。

  • runc
    • OCI Specificationに沿って、コンテナの生成、実行を行なうCLIツール
  • container networking plugins
  • containerd
  • kubelet
    • 各ノードで動作する主要なNodeエージェント。API Serverにホスト名等を使ってNodeを登録する。PodSpecのセットを取得し、PodSpecに記載されているコンテナが正常に動作している状態を保証する
  • kube-proxy
    • 各ノードで動作するネットワークプロキシ。Nodeのネットワークルールをメンテナンスする。

Worker Nodeをプロビジョニングする

以下の操作は各Worker Nodeで実行します。なお、説明ではworker-01でセットアップを行い02のセットアップは省略しますが基本的に同じようなセットアップを行います(別途、別の操作が必要な場合は補足します)。

最初にworker-01にアクセスします。

$ ssh ubuntu@192.168.13.2

必要な依存を取得してきます。

$ sudo apt-get update
$ sudo apt-get -y install socat conntrack ipset

socatkubectl port-forwardコマンドのサポートを行なうために必要なようです。

デフォルトではkubeletはswapが有効化されていると起動しません。
以下のコマンドを実行した際に出力がなにもなければswapは有効化されていません。

$ sudo swapon --show

もし、有効化されていた場合は以下のコマンドで、無効にしてください。

$ sudo swapoff -a

無効化が確認できたら、必要なバイナリをインストールします。

まずはバイナリをダウンロードします。

$ wget -q --show-progress --https-only --timestamping \
  https://github.com/kubernetes-sigs/cri-tools/releases/download/v1.21.0/crictl-v1.21.0-linux-arm64.tar.gz \
  https://github.com/containernetworking/plugins/releases/download/v0.9.1/cni-plugins-linux-arm64-v0.9.1.tgz \
  https://storage.googleapis.com/kubernetes-release/release/v1.21.0/bin/linux/arm64/kubectl \
  https://storage.googleapis.com/kubernetes-release/release/v1.21.0/bin/linux/arm64/kube-proxy \
  https://storage.googleapis.com/kubernetes-release/release/v1.21.0/bin/linux/arm64/kubelet

次に必要なディレクトリを作成しておきます。

$ sudo mkdir -p \
  /etc/cni/net.d \
  /opt/cni/bin \
  /var/lib/kubelet \
  /var/lib/kube-proxy \
  /var/lib/kubernetes \
  /var/run/kubernetes

バイナリを各ディレクトリに移動 or 解凍します。

$ tar -xvf crictl-v1.21.0-linux-arm64.tar.gz
$ sudo tar -xvf cni-plugins-linux-arm64-v0.9.1.tgz -C /opt/cni/bin/
$ chmod +x crictl kubectl kube-proxy kubelet  
$ sudo mv crictl kubectl kube-proxy kubelet /usr/local/bin/

containerdとruncに関してはarm64アーキテクチャでのインストールにはひと工夫が必要です。 まず、containerdですが、このIssueによるとdownload.docker.comで提供されているものがあるみたいだったのでそちらを利用させていただこうと思います。

以下のコマンドでインストールします。

$ wget https://download.docker.com/linux/ubuntu/dists/focal/pool/test/arm64/containerd.io_1.4.3-2_arm64.deb
$ sudo apt install ./containerd.io_1.4.3-2_arm64.deb
$ containerd -v
containerd containerd.io 1.4.3 269548fa27e0089a8b8278fc4fc781d7f65a939b

続いて、runcですが、こちらは公式での提供が内容だったので(少なくとも僕が調べる限りでは)自分でビルドしようと思います。
ビルドは1番スペックがよいmaster-01で行って、実行ファイルをcspでそれぞれのworkerに配布しようと思います。

$ ssh ubuntu@192.168.13.5

まず、ビルドにはgo が必要なようなので、goのインストールを行います。

$ wget https://golang.org/dl/go1.16.3.linux-arm64.tar.gz
$ sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.16.3.linux-arm64.tar.gz 
$ export PATH=$PATH:/usr/local/go/bin
$ go version
go version go1.16.3 linux/arm6

次にビルドに必要な依存をとってきます。

$ sudo apt install build-essential
$ sudo apt install pkg-config
$ sudo apt install -y libseccomp-dev

最後にソースコードを取得して、ビルドを行います。

$ git clone https://github.com/opencontainers/runc
$ cd runc
$ make
$ sudo make install

ビルドが完了したらできた実行ファイルをそれぞれのworkerに配布します。

$ scp /usr/local/sbin/runc ubuntu@192.168.13.2:~/
$ scp /usr/local/sbin/runc ubuntu@192.168.13.3:~/

それぞれのworkerで適当なディレクトリにruncを移動します。

$ sudo mv runc /usr/local/bin/
$ runc -v
runc version 1.0.0-rc93+dev
commit: 2965ffc7e327dc3dc33a9b308ba8396e60e5bb58
spec: 1.0.2-dev
go: go1.16.3
libseccomp: 2.4.3

(versionの指定をちゃんとしてなかったので、devがインストールされてしまってますね...一旦ここでは先に進みます)

CNIの設定を行なう

bridgeネットワークの設定ファイルを作成します。

$ cat <<EOF | sudo tee /etc/cni/net.d/10-bridge.conf
{
    "cniVersion": "0.4.0",
    "name": "bridge",
    "type": "bridge",
    "bridge": "cnio0",
    "isGateway": true,
    "ipMasq": true,
    "ipam": {
        "type": "host-local",
        "ranges": [
          [{"subnet": "10.200.0.0/24"}]
        ],
        "routes": [{"dst": "0.0.0.0/0"}]
    }
}
EOF

loopbackのネットワーク設定を記述します。

$ cat <<EOF | sudo tee /etc/cni/net.d/99-loopback.conf
{
    "cniVersion": "0.4.0",
    "name": "lo",
    "type": "loopback"
}
EOF

kubeletの設定を行なう

kubeletのための設定や証明書を必要なディレクトリに配置します。

$ sudo mv ${HOSTNAME}-key.pem ${HOSTNAME}.pem /var/lib/kubelet/
$ sudo mv ${HOSTNAME}.kubeconfig /var/lib/kubelet/kubeconfig
$ sudo mv ca.pem /var/lib/kubernetes/

kubelet-config.yamlを作成します。

$ cat <<EOF | sudo tee /var/lib/kubelet/kubelet-config.yaml
kind: KubeletConfiguration
apiVersion: kubelet.config.k8s.io/v1beta1
authentication:
  anonymous:
    enabled: false
  webhook:
    enabled: true
  x509:
    clientCAFile: "/var/lib/kubernetes/ca.pem"
authorization:
  mode: Webhook
clusterDomain: "cluster.local"
clusterDNS:
  - "10.32.0.10"
podCIDR: "10.200.0.0/24"
resolvConf: "/run/systemd/resolve/resolv.conf"
runtimeRequestTimeout: "15m"
tlsCertFile: "/var/lib/kubelet/${HOSTNAME}.pem"
tlsPrivateKeyFile: "/var/lib/kubelet/${HOSTNAME}-key.pem"
EOF

kubuletのユニットファイルを作成します。

cat <<EOF | sudo tee /etc/systemd/system/kubelet.service
[Unit]
Description=Kubernetes Kubelet
Documentation=https://github.com/kubernetes/kubernetes
After=containerd.service
Requires=containerd.service

[Service]
ExecStart=/usr/local/bin/kubelet \\
  --config=/var/lib/kubelet/kubelet-config.yaml \\
  --container-runtime=remote \\
  --container-runtime-endpoint=unix:///var/run/containerd/containerd.sock \\
  --image-pull-progress-deadline=2m \\
  --kubeconfig=/var/lib/kubelet/kubeconfig \\
  --network-plugin=cni \\
  --register-node=true \\
  --v=2
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

containerdの設定を行なう

containerdの設定ファイルを作成します。
以下のコマンドを実行します。

cat << EOF | sudo tee /etc/containerd/config.toml
[plugins]
  [plugins.cri.containerd]
    snapshotter = "overlayfs"
    [plugins.cri.containerd.default_runtime]
      runtime_type = "io.containerd.runtime.v1.linux"
      runtime_engine = "/usr/local/bin/runc"
      runtime_root = ""
EOF

Kubernetes Proxyの設定を行なう

kube-proxyの設定を行います。
まずは、必要な設定ファイルを適当なディレクトリに配置します。

$ sudo mv kube-proxy.kubeconfig /var/lib/kube-proxy/kubeconfig

次にkube-proxy-config.yamlを作成します。

$ cat <<EOF | sudo tee /var/lib/kube-proxy/kube-proxy-config.yaml
kind: KubeProxyConfiguration
apiVersion: kubeproxy.config.k8s.io/v1alpha1
clientConnection:
  kubeconfig: "/var/lib/kube-proxy/kubeconfig"
mode: "iptables"
clusterCIDR: "10.200.0.0/16"
EOF

例のごとく、ユニットファイルを作成します。

cat <<EOF | sudo tee /etc/systemd/system/kube-proxy.service
[Unit]
Description=Kubernetes Kube Proxy
Documentation=https://github.com/kubernetes/kubernetes

[Service]
ExecStart=/usr/local/bin/kube-proxy \\
  --config=/var/lib/kube-proxy/kube-proxy-config.yaml
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

コンポーネントの起動

設定が完了したので、最後にコンポーネントを起動します。
以下のコマンドを実行します。

$ sudo systemctl daemon-reload
$ sudo systemctl enable containerd kubelet kube-proxy
$ sudo systemctl start containerd kubelet kube-proxy

正常に起動が完了すると、leader-01からNodeが認識されているのが確認できます。

$ kubectl get nodes --kubeconfig admin.kubeconfig
NAME        STATUS   ROLES    AGE     VERSION
worker-01   Ready    <none>   8m2s    v1.21.0
worker-02   Ready    <none>   8m59s   v1.21.0

リモートからkubectlでアクセスできるようにする

kubectlの設定を行って毎回--kubeconfig等でファイルを指定しなくても良いようにします。
ThinkPadのクライアントPCで以下のコマンドを実行します。

$ kubectl config set-cluster kubernetes-the-hard-way \
   --certificate-authority=ca.pem \
   --embed-certs=true \
   --server=https://192.168.13.5:6443

$ kubectl config set-credentials admin \
   --client-certificate=admin.pem \
   --client-key=admin-key.pem

$ kubectl config set-context kubernetes-the-hard-way \
   --cluster=kubernetes-the-hard-way \
   --user=admin

$ kubectl config use-context kubernetes-the-hard-way

kubectl get nodeでNodeの一覧を取得できるようになってます。

$ kubectl version
Client Version: version.Info{Major:"1", Minor:"20", GitVersion:"v1.20.5", GitCommit:"6b1d87acf3c8253c123756b9e61dac642678305f", GitTreeState:"clean", BuildDate:"2021-03-18T01:10:43Z", GoVersion:"go1.15.8", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"21", GitVersion:"v1.21.0", GitCommit:"cb303e613a121a29364f75cc67d3d580833a7479", GitTreeState:"clean", BuildDate:"2021-04-08T16:25:06Z", GoVersion:"go1.16.1", Compiler:"gc", Platform:"linux/arm64"}


$ kubectl get node
NAME        STATUS   ROLES    AGE   VERSION
worker-01   Ready    <none>   16m   v1.21.0
worker-02   Ready    <none>   17m   v1.21.0

Podネットワークルールのプロビジョニングを行なう

Pod間の通信を行なうためにflannelクラスタにインストールします。
flannelはkubernetesのlayer 3でのネットワーク設定を行ってくれます。

$ kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

このあと失敗した

ここまでは、いろいろ苦難しつつもできたのですが、前述の通りflannelがうまく動かず失敗しました。
また、時間が立ってから再度やってみようと思います。

Raspberry pi 3 model b+にUbuntu18をインストールする

はじめに

Twitterとかでにk8sクラスターをラズパイ上に構築しているのをよく見かけていたのですが、自分もGWでちょっとやってみようかと思い。
その仮定で、ラズパイにUbuntuをインストールする必要があるみたいなのでメモとして残しておこうと思います。 ラズパイを触るのは今回はじめてです。

やってみる

環境

Ubuntuをインストールするためのホストマシンの環境はこんな感じです。

$ lsb_release -a
LSB Version:    core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.2 LTS
Release:    20.04
Codename:   focal

$ uname -srvmpio
Linux 5.4.0-72-generic #80-Ubuntu SMP Mon Apr 12 17:35:00 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

そして、今回今回購入したのが以下の3つです。

f:id:yuya_hirooka:20210501182532j:plain

micro USBコネクター今回持ってなかったので購入しましたが、持っていれば買う必要はないです。
マウスやキーボードは自宅のすでに持っているものを追加います。 MicroSDとラズパイは秋葉原で購入しましたが大体2つで7千円弱ぐらいでした。

MircoSDにイメージを焼く

PCの組み込みMicroSDドライバーがあったので差し込みます。
MicroSDmmcblk0という名前でマウントされるようです。

$ dmesg | grep mmcblk0
[    1.769416] mmcblk0: mmc0:5048 SD32G 28.9 GiB 
[    1.788078]  mmcblk0: p1

次にイメージを焼いていくのですが、
Raspberry pi 3 model b+のBroadcom BCM2837B0, Cortex-A53はARMv8が蔡陽されたCPUのアーキテクチャのようで、ARM64のものを使えそうです。
wgetでイメージを取得して、ddコマンドでイメージを焼きます。

$ wget http://cdimage.ubuntu.com/releases/bionic/release/ubuntu-18.04.5-preinstalled-server-arm64+raspi3.img.xz

$  xzcat ubuntu-18.04.5-preinstalled-server-arm64+raspi3.img.xz | sudo dd bs=4M of=/dev/mmcblk0
0+318958 レコード入力
0+318958 レコード出力
2653289472 bytes (2.7 GB, 2.5 GiB) copied, 29.1715 s, 91.0 MB/s

これで、イメージを焼くことができました。

Ubuntuをインストールする

MiroiSDをPCの組み込みの読み込み機に差し込み、micro-usbでつないで電源を入れます。
ディスプレイをつなぐとUbuntunのインストールが自動的に始まり、パスワードの入力が求められます。
ここはubuntu/ubuntuと入力すると新しいパスワードの設定を求められるので任意のものを入れてください。

これでインストールは完了です。

JITWatchを動かしてみる

はじめに

ちょっと前のどこかのイベント(おそらくJJUG)でJitWatchというのが紹介されていて、ちょっと気になってました。 このブログではJITWatchを動かして見ようかと思います。
紹介動画とかみつつ、自分の理解をまとめています。正確な情報は公式のGitHub紹介動画スライドWiki等をご覧ください。
間違え等があればご指摘いただけると嬉しいです。

JitWatchとは

HotSpot JIT compilerのLogの解析と可視化を行なうためのツールです。
AdoptOpenJDKのプロジェクトで、Hotspotのログファイルを読み込んで、解析を実行することが可能なようです。 具体的には、以下のような機能を提供します。

動かしてみる

環境

$ java --version
openjdk 11.0.10 2021-01-19
OpenJDK Runtime Environment 18.9 (build 11.0.10+9)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.10+9, mixed mode)


$ mvn --version
Apache Maven 3.6.3
Maven home: /usr/share/maven
Java version: 11.0.10, vendor: Oracle Corporation, runtime: /home/someone/.sdkman/candidates/java/11.0.10-open
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-72-generic", arch: "amd64", family: "unix"

まずは起動してみる

インストールとJITWatchの起動は、GitHubからプロジェクトをクローンしてきて、Exec Maven Pluginの実行コマンド叩くだけでGUIが立ち上がります。
ただし、注意点としてJITWatchのGUIJavaFXを利用するためもし含まれていないJDKを利用している場合はここの手順を参考にセットアップして利用するか、JDKを別のものに買えてください。
それでは、起動してみます。

$  git clone https://github.com/prometheus-operator/kube-prometheus.git

$ cd jitwatch

$ mvn clean compile test exec:java

実行すると以下のような画面が表示されます。

f:id:yuya_hirooka:20210430151602p:plain

さくっと起動ができました。

Sandboxを動かしてみる

JITWatchの左上にsandbox文字が見れるのでおもむろに押してみます。
すると以下のようなエディターが立ち上がり、サンプルコードを実行できるようになっているみたいです。

f:id:yuya_hirooka:20210430153356p:plain

Configure Sandboxの設定からクラスパス等を設定するといろいろできそうですが、一旦はこのままRunコマンドを実行してみます。
すると以下のようなウィンドウが起動され実行結果が出力されます。

f:id:yuya_hirooka:20210430155416p:plain

これはTriViewと呼ばれるもので、実際のソースコードと対応するバイドコード、そしてアセンブリのコードが出力されるようになっているみたいです。
AssemblyのタブではAssembly not found. Was -XX:+PrintAssembly option used?という出力がされていますが、これはJITWatchがアセンブリコードを表示するためにいろいろ設定をしてやる必要があるからのようです。これらの設定等は後ほど補足しますが、ここではスルーします。

JITWatchを起動した際に、1番最初に表示された画面を確認してみると、少し表示が変わっているのがわかります。

f:id:yuya_hirooka:20210430174743p:plain

ここでは、Compile Treeが表示されており、パッケージとそこに属するクラスを選択することができ、それぞれにメソッド対する解析結果を見ることができます。

画面上部にはTimelineHistoToplistCacheなどのボタンが用意されていますが、このボタンを押すとそれぞれの解析結果を見れるようです。 先程表示されていた、TriViewも上部にあります。 主要なものの説明を以下にまとめます。

  • Config: クラスパスなどの設定。
  • Timeline: コンパイルのタイムライン
  • Histo: コンパイル時間、メソッドサイズ、インラインメソッドサイズなどのヒストグラム
  • Cache: 時間軸に対するコードキャッシュのフリースペース
  • Threads: コンパイラのスレッドアクティビティ
  • TriView: ソースコード、バイドコード、アセンブリのView
  • Sugest: 予想が難しい分岐とホットメソットのインラインのサジェスト

ログを読み込んで解析を行なう

アプリの実行ログから解析を行ないます。
Spring BootのWebアプリを作成して、解析をしてみようと思います。

プロジェクトを作成して、hotspotのログを取得する

プロジェクトはSpring Initializrを使って、以下の設定で作成します。

f:id:yuya_hirooka:20210430192206p:plain

作成したプロジェクトをダウンロードして解凍し以下のコントローラーひとつだけ作成しておきます。

@RestController
public class HelloController {

    Logger logger = LoggerFactory.getLogger(HelloController.class);

    @GetMapping("/hello")
    public String hello() {
        for (int i = 0; i < 1000; i++) logger.info(String.format("exec adjustNum %s times", adjustNum(i)));
        return "hello";
    }

    private int adjustNum(int a) {
        return a + 1;
    }
}

以下のコマンドでビルドします。

$ mvn clean package

$ ls target/
classes  demo-0.0.1-SNAPSHOT.jar  demo-0.0.1-SNAPSHOT.jar.original  generated-sources  generated-test-sources  maven-archiver  maven-status  surefire-reports  test-classes

jarができたので実行をします。

この際にJITWatchが必要とするログを出力するため以下のオプションを追加します。

  • -XX:+UnlockDiagnosticVMOptions
    • JDKの解析系のオプションをアンロックする
  • -XX:+LogCompilation
  • -XX:+TraceClassLoading
    • クラスロードされたクラスの追跡を有効にする

今回の場合は以下のように実行します。

$ java  -XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation -XX:+TraceClassLoading -jar target/demo-0.0.1-SNAPSHOT.jar 

すると、hotsopt_pid[アプリケーションのpid].logという名前のログファイルが出力されます。

$ ls
HELP.md  demo.iml  hotspot_pid41577.log  mvnw  mvnw.cmd  pom.xml  src  target

今回の場合はhotspot_pid41577.logというのがそれですね。

JITWatchの設定を行なう

作成したログファイルをJITWatchに読み込ませますが、その前に以下のような設定をJITWatch側で行なう必要があります。

  • src.zip(Javaプラットフォームのソース・コードを含むアーカイブ)を指定
  • アプリケーションのsrcディレクト
  • アプリケーションのclassファイルのロケーション

これらはJITWatchのConfig(最初に起動する画面上部の右から五番目あたり)から行なうことができます。
Config画面は以下のようになっています。

f:id:yuya_hirooka:20210430214748p:plain

画面上部の段にはsrc.zipとアプリケーションのsrcデレクトリ、下の段にはクラスファイルのロケーションを指定します。

src.zipに関してはAdd JDK srcボタンを押すと(おそらくJITWatchを起動している)JDKのzipが自動的に設定され、その他の2つはAdd Folderで設定します。

この状態で、Open log から先程のログ読み込み、startボタンを押します。 すると以下のようにadjustNum()関数がC1コンパイルされたことが確認できます。

f:id:yuya_hirooka:20210430233641p:plain

アセンブリコードの生成(補足)

今回は試しませんが、JITWatchのTriViewでは生成されたアセンブリのコードも見ることができます。
そのためにはhsdisをビルドして、JDKの適当なディレクトリ(8以前と以降で配置するディレクトリが違います)に配置し、プログラム実行時に-XX:+PrintAssemblyオプションを指定してやる必要があります。
詳細はここを確認してください。

感想

今回、Spring Bootのアプリの解析をやってみましあんまり意味は無かったかもしれません、ちなみにjarを適切に指定してやるとSpringのクラスの解析も行えそうでした。

KtorのアプリからMicrometer+PrometheusでJVMのメトリクスを取得する

はじめに

Ktorのドキュメント呼んでいたらMicrometerに対応してそうだというのを見かけてちょっと動かしてみようかと思います。
基本的にはドキュメントに書かれた流れを沿う感じでやろうかと思います。
あと、PrometheusはDockerを用いて起動します。

やってみる

環境

$ lsb_release -a
LSB Version:    core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.2 LTS
Release:        20.04
Codename:       focal

$ uname -srvmpio
Linux 5.4.0-72-generic #80-Ubuntu SMP Mon Apr 12 17:35:00 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux


$ docker version
Client: Docker Engine - Community
 Version:           20.10.6
 API version:       1.41
 Go version:        go1.13.15
 Git commit:        370c289
 Built:             Fri Apr  9 22:47:17 2021
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.6
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       8728dd2
  Built:            Fri Apr  9 22:45:28 2021
  OS/Arch:          linux/amd64
  Experimental:     true
 containerd:
  Version:          1.4.4
  GitCommit:        05f951a3781f4f2c1911b05e61c160e9c30eaa8e
 runc:
  Version:          1.0.0-rc93
  GitCommit:        12644e614e25b05da6fd08a38ffa0cfe1903fdec
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0


$ java --version
openjdk 16 2021-03-16
OpenJDK Runtime Environment (build 16+36-2231)
OpenJDK 64-Bit Server VM (build 16+36-2231, mixed mode, sharing)

プロジェクトの作成

プロジェクトはIntelliJKtorプラグインを使って作成します。
設定は以下のようにします。

f:id:yuya_hirooka:20210421214635p:plain

f:id:yuya_hirooka:20210421214439p:plain

依存の追加のところでMicrometerを追加することもできますが、今回ははRoutingだけを選択します。

出来上がったプロジェクトのPomの抜粋を以下に示します。

    <properties>
        <ktor_version>1.5.3</ktor_version>
        <kotlin.code.style>official</kotlin.code.style>
        <kotlin_version>1.4.32</kotlin_version>
        <logback_version>1.2.3</logback_version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <kotlin.compiler.incremental>true</kotlin.compiler.incremental>
        <main.class>hirooka.dev.ApplicationKt</main.class>
    </properties>
      <dependencies>
        <dependency>
            <groupId>io.ktor</groupId>
            <artifactId>ktor-server-core</artifactId>
            <version>${ktor_version}</version>
        </dependency>
        <dependency>
            <groupId>io.ktor</groupId>
            <artifactId>ktor-server-netty</artifactId>
            <version>${ktor_version}</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>${logback_version}</version>
        </dependency>
        <dependency>
            <groupId>io.ktor</groupId>
            <artifactId>ktor-server-tests</artifactId>
            <version>${ktor_version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

Ktorのバージョンは1.4.32が選択されているのと、依存にはテスト関連の者Lockbackそして、ktor-server-coreが追加されていました。

Prometheus形式のメトリクスを取得できるようにする

まずは以下の依存をPomに追加する必要があります。

        <dependency>
            <groupId>io.ktor</groupId>
            <artifactId>ktor-metrics-micrometer</artifactId>
            <version>${ktor_version}</version>
        </dependency>
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-registry-prometheus</artifactId>
            <version>1.6.6</version>
        </dependency>

1つ目はKtorのMicrometerのサポートを提供してくれるプロジェクトで、2つ目はPrometheusのレジストリーです。

次にApplication.ktを以下のように修正します。

import io.ktor.server.engine.*
import io.ktor.server.netty.*
import hirooka.dev.plugins.*
import io.ktor.application.*
import io.ktor.metrics.micrometer.*

fun main() {
    val prometheusMeterRegistry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT)
    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
        install(MicrometerMetrics) {
            registry = prometheusMeterRegistry
        }
        configureRouting()
    }.start(wait = true)
}

追加したのはinstall(MicrometerMetrics)のところです。
KtorでMicromerを使う場合installでMicrometerMwetricsのクラスをインストールする必要があります。
次にJVMのメトリクスを公開するために更に以下のような修正をくわえます。

fun main() {
    val prometheusMeterRegistry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT)
    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
        install(MicrometerMetrics) {
            registry = prometheusMeterRegistry

            meterBinders = listOf(
                JvmMemoryMetrics(),
                JvmGcMetrics(),
                ProcessorMetrics(),
            )
        }
        configureRouting()
    }.start(wait = true)
}

最後のProcessorMetrics()はシステムのメトリクスなので今回はいらないかもしれませんが一応追加しておきます。
最後に取得したメトリクスを公開するRoutingを記述します。

fun main() {
    val prometheusMeterRegistry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT)
    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
        install(MicrometerMetrics) {
            registry = prometheusMeterRegistry
        }

        routing {
            get("/metrics") {
                call.respond(prometheusMeterRegistry.scrape())
            }
        }
        configureRouting()
    }.start(wait = true)
}

Mainの頭のところでイニシャライズしたprometheusMeterRegistryを使う必要があったため、今回はApplication.ktにRoutingを記述しました。
具体的には/metricsというPathに対してprometheusMeterRegistry.scrape()で取得できるメトリクスを返すようにしています。

これでアプリケーション側の設定は完了です。
アプリケーションを起動して、cURLでメトリクスを取得してみます。

$ curl localhost:8080/metrics 
# HELP jvm_buffer_memory_used_bytes An estimate of the memory that the Java virtual machine is using for this buffer pool
# TYPE jvm_buffer_memory_used_bytes gauge
jvm_buffer_memory_used_bytes{id="mapped",} 0.0
jvm_buffer_memory_used_bytes{id="direct",} 3.3562632E7
# HELP system_cpu_usage The "recent cpu usage" for the whole system
# TYPE system_cpu_usage gauge
system_cpu_usage 0.06208306434258662
# HELP jvm_memory_max_bytes The maximum amount of memory in bytes that can be used for memory management
# TYPE jvm_memory_max_bytes gauge
jvm_memory_max_bytes{area="nonheap",id="CodeHeap 'profiled nmethods'",} 1.22908672E8
jvm_memory_max_bytes{area="heap",id="G1 Survivor Space",} -1.0
jvm_memory_max_bytes{area="heap",id="G1 Old Gen",} 8.321499136E9
jvm_memory_max_bytes{area="nonheap",id="Metaspace",} -1.0
jvm_memory_max_bytes{area="nonheap",id="CodeHeap 'non-nmethods'",} 5836800.0
jvm_memory_max_bytes{area="heap",id="G1 Eden Space",} -1.0
jvm_memory_max_bytes{area="nonheap",id="Compressed Class Space",} 1.073741824E9
jvm_memory_max_bytes{area="nonheap",id="CodeHeap 'non-profiled nmethods'",} 1.22912768E8
# HELP jvm_gc_pause_seconds Time spent in GC pause
# TYPE jvm_gc_pause_seconds summary
jvm_gc_pause_seconds_count{action="end of minor GC",cause="Metadata GC Threshold",} 1.0
jvm_gc_pause_seconds_sum{action="end of minor GC",cause="Metadata GC Threshold",} 0.006
# HELP jvm_gc_pause_seconds_max Time spent in GC pause
# TYPE jvm_gc_pause_seconds_max gauge
jvm_gc_pause_seconds_max{action="end of minor GC",cause="Metadata GC Threshold",} 0.006
# HELP jvm_gc_max_data_size_bytes Max size of long-lived heap memory pool
# TYPE jvm_gc_max_data_size_bytes gauge
jvm_gc_max_data_size_bytes 8.321499136E9
# HELP system_load_average_1m The sum of the number of runnable entities queued to available processors and the number of runnable entities running on the available processors averaged over a period of time
# TYPE system_load_average_1m gauge
system_load_average_1m 2.18
# HELP jvm_memory_committed_bytes The amount of memory in bytes that is committed for the Java virtual machine to use
# TYPE jvm_memory_committed_bytes gauge
jvm_memory_committed_bytes{area="nonheap",id="CodeHeap 'profiled nmethods'",} 2686976.0
jvm_memory_committed_bytes{area="heap",id="G1 Survivor Space",} 6291456.0
jvm_memory_committed_bytes{area="heap",id="G1 Old Gen",} 4.5088768E8
jvm_memory_committed_bytes{area="nonheap",id="Metaspace",} 2.5608192E7
jvm_memory_committed_bytes{area="nonheap",id="CodeHeap 'non-nmethods'",} 2555904.0
jvm_memory_committed_bytes{area="heap",id="G1 Eden Space",} 6.291456E7
jvm_memory_committed_bytes{area="nonheap",id="Compressed Class Space",} 2883584.0
jvm_memory_committed_bytes{area="nonheap",id="CodeHeap 'non-profiled nmethods'",} 2555904.0
# HELP jvm_buffer_total_capacity_bytes An estimate of the total capacity of the buffers in this pool
# TYPE jvm_buffer_total_capacity_bytes gauge
jvm_buffer_total_capacity_bytes{id="mapped",} 0.0
jvm_buffer_total_capacity_bytes{id="direct",} 3.3562631E7
# HELP system_cpu_count The number of processors available to the Java virtual machine
# TYPE system_cpu_count gauge
system_cpu_count 8.0
# HELP ktor_http_server_requests_seconds_max  
# TYPE ktor_http_server_requests_seconds_max gauge
ktor_http_server_requests_seconds_max{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",} 0.021426668
# HELP ktor_http_server_requests_seconds  
# TYPE ktor_http_server_requests_seconds histogram
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.001",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.001048576",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.001398101",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.001747626",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.002097151",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.002446676",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.002796201",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.003145726",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.003495251",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.003844776",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.004194304",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.005592405",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.006990506",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.008388607",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.009786708",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.011184809",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.01258291",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.013981011",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.015379112",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.016777216",} 0.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.022369621",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.027962026",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.033554431",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.039146836",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.044739241",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.050331646",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.055924051",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.061516456",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.067108864",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.089478485",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.1",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.111848106",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.134217727",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.156587348",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.178956969",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.20132659",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.223696211",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.246065832",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.268435456",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.357913941",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.447392426",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.5",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.536870911",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.626349396",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.715827881",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.805306366",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.894784851",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="0.984263336",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="1.073741824",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="1.431655765",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="1.789569706",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="2.147483647",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="2.505397588",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="2.863311529",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="3.22122547",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="3.579139411",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="3.937053352",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="4.294967296",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="5.726623061",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="7.158278826",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="8.589934591",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="10.021590356",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="11.453246121",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="12.884901886",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="14.316557651",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="15.748213416",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="17.179869184",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="20.0",} 1.0
ktor_http_server_requests_seconds_bucket{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",le="+Inf",} 1.0
ktor_http_server_requests_seconds_count{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",} 1.0
ktor_http_server_requests_seconds_sum{address="localhost:8080",method="GET",route="/metrics",status="200",throwable="n/a",} 0.021426668
# HELP ktor_http_server_requests_active  
# TYPE ktor_http_server_requests_active gauge
ktor_http_server_requests_active 1.0
# HELP jvm_buffer_count_buffers An estimate of the number of buffers in the pool
# TYPE jvm_buffer_count_buffers gauge
jvm_buffer_count_buffers{id="mapped",} 0.0
jvm_buffer_count_buffers{id="direct",} 6.0
# HELP jvm_memory_used_bytes The amount of used memory
# TYPE jvm_memory_used_bytes gauge
jvm_memory_used_bytes{area="nonheap",id="CodeHeap 'profiled nmethods'",} 2629376.0
jvm_memory_used_bytes{area="heap",id="G1 Survivor Space",} 6291456.0
jvm_memory_used_bytes{area="heap",id="G1 Old Gen",} 0.0
jvm_memory_used_bytes{area="nonheap",id="Metaspace",} 2.4776112E7
jvm_memory_used_bytes{area="nonheap",id="CodeHeap 'non-nmethods'",} 1170432.0
jvm_memory_used_bytes{area="heap",id="G1 Eden Space",} 1.2582912E7
jvm_memory_used_bytes{area="nonheap",id="Compressed Class Space",} 2673248.0
jvm_memory_used_bytes{area="nonheap",id="CodeHeap 'non-profiled nmethods'",} 507008.0
# HELP jvm_gc_live_data_size_bytes Size of long-lived heap memory pool after reclamation
# TYPE jvm_gc_live_data_size_bytes gauge
jvm_gc_live_data_size_bytes 0.0
# HELP process_cpu_usage The "recent cpu usage" for the Java Virtual Machine process
# TYPE process_cpu_usage gauge
process_cpu_usage 9.6815834767642E-4
# HELP jvm_gc_memory_allocated_bytes_total Incremented for an increase in the size of the (young) heap memory pool after one GC to before the next
# TYPE jvm_gc_memory_allocated_bytes_total counter
jvm_gc_memory_allocated_bytes_total 2.3068672E7
# HELP jvm_gc_memory_promoted_bytes_total Count of positive increases in the size of the old generation memory pool before GC to after GC
# TYPE jvm_gc_memory_promoted_bytes_total counter
jvm_gc_memory_promoted_bytes_total 0.0

Promehteusから収集する

最後に公開されているメトリクスをPrometheus側から取得したいと思います。
まずは

global:
  scrape_interval:     15s
  evaluation_interval: 15s

rule_files:

scrape_configs:
  - job_name: prometheus
    static_configs:
      - targets: ['host.docker.internal:8080/metric']

余談ですがDocker 20.10系からLinux--add-host=host.docker.internal:host-gatewayみたいな感じで起動時にオプションを渡してやるとhost.docker.internalでホストマシンにアクセスできるようになったみたいですね。

PrometheusをDockerで起動します。この際に上記の設定ファイルをマウントするようにします。

docker run \
    --add-host=host.docker.internal:host-gateway \
    -p 9090:9090 \
    -v /path/to/four/config/prometheus.yml:/etc/prometheus/prometheus.yml \
    prom/prometheus

これでPrometheusが起動し、localhost:9090でUIに接続できます。

f:id:yuya_hirooka:20210421225443p:plain

ターゲットの一覧を確認するときちんとアプリが認識されているのがわかります。

f:id:yuya_hirooka:20210421231219p:plain

以下のようにjvm_buffer_count_buffersも可視化できました。

f:id:yuya_hirooka:20210421231354p:plain

Docker 20.10.0についてまとめる

はじめに

だいぶ前ですがDokcer 20.10.0がリリースされています。かねてからやろうと思ってやってなかったのですが、リリースノートを眺めて、気になったところをかいつまんでまとめてみようと思います。
ものによっては動かしてみようかと。

やっていく

環境

$ uname -srvmpio
Linux 5.4.0-70-generic #78-Ubuntu SMP Fri Mar 19 13:29:52 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
LSB Version:    core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.2 LTS
Release:    20.04
Codename:   focal


$ docker version
Client: Docker Engine - Community
 Version:           20.10.5
 API version:       1.41
 Go version:        go1.13.15
 Git commit:        55c4c88
 Built:             Tue Mar  2 20:18:20 2021
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.5
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       363e9a8
  Built:            Tue Mar  2 20:16:15 2021
  OS/Arch:          linux/amd64
  Experimental:     true
 containerd:
  Version:          1.4.4
  GitCommit:        05f951a3781f4f2c1911b05e61c160e9c30eaa8e
 runc:
  Version:          1.0.0-rc93
  GitCommit:        12644e614e25b05da6fd08a38ffa0cfe1903fdec
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

今回、使用するDockerは20.10.5ですが、20.10.0の変更点のみをまとめていこうと思います。

API

GET /eventspruneのイベントを取得できる

例えば、以下のようにcurl --unix-socket /var/run/docker.sock http:/v1.41/eventsにアクセスした状態で、docker system pruneコマンドを実行すると、以下のようにイベントを取得することができます。

$ curl --unix-socket /var/run/docker.sock http:/v1.41/events
{"status":"prune","Type":"container","Action":"prune","Actor":{"ID":"","Attributes":{"reclaimed":"0"}},"scope":"local","time":1618107008,"timeNano":1618107008756813704}
{"Type":"network","Action":"prune","Actor":{"ID":"","Attributes":{"reclaimed":"0"}},"scope":"local","time":1618107008,"timeNano":1618107008758330171}
{"status":"prune","Type":"image","Action":"prune","Actor":{"ID":"","Attributes":{"reclaimed":"0"}},"scope":"local","time":1618107008,"timeNano":1618107008798691967}
{"Type":"builder","Action":"prune","Actor":{"ID":"","Attributes":{"reclaimed":"0"}},"scope":"local","time":1618107008,"timeNano":1618107008866211129}

このエンドポイントはcontainernetworkvolumeimageそれぞれに対してreclaimed(回収したバイト数)を取得することができます。

GET /infoに対する変更点

以下のような変更点があります。

  • OSのVersionを取得できるようになった
  • DefaultAddressPoolsを取得できるようになった

Builder

#syntaxなしで、RUN --mountオプションを利用できるようになった

もともとBuildKitのRUN --mount拡張機能を利用したい場合は``のようなコメント行をDockerfileの先頭に追加する必要がありましたが不要になりました。

ARGENVのように複数の値を受け付けるように変更

ARGコマンドは変数を定義してビルド時に--build-arg <varname>=<value>のフラグを利用することによって引数を渡すことができます。このARGコマンドが今回の変更でENVと同じように複数定義を一行でできるように修正さています。

ADDコマンドに--chownパラメーター拡張を渡せるようになった

もともとADD/COPYコマンドでは--chownフラグを指定できファイルオーナーを設定できますしかし、ADDコマンドではARGENVで定義された変数からの指定ができないようになってました。しかし今回の修正でそれが行えるようになったようです。

Clientクライアントでは以下のような修正が行われています

  • -a/--all-tagsフラグですべてのタグをpushできるようになった
  • Kubernetesusername/password認証をサポート
  • runコマンドとcreateコマンドに--pull=missing|always|neverフラグを追加
  • docker exec--env-fileフラグがついかされ環境変数を渡せるようになった
  • log-driverが追加されPrettyオプションが利用可能になった
  • --cgroupnsフラグでネームスペースを指定できようになった
  • docker manifest rmローカルストレージのマニフェストリストドラフト(設定やレイヤーの情報を保持)を削除
  • docker infodocker versionにコンテキストの情報を出力するように変更

Runtime

ランタイムでは以下のような修正が行われています。

  • cgroup2のサポート
  • cgroup2がデフォルトでsystemdのcgroupを利用するようになった
  • 新しいストレージドライバfuse-overlayfs
  • containerdのバイナリを1.4.3にアップデート
  • docker pushのデフォルトがすべてのタグのPushからlatestに変更
  • イメージをPullする際のコネクションロスでリコネクトする回数を指定できるように変更
  • コンテナスタートアップのために、最低のメモリリミットを6Mへ変更。

Networking

  • host.docker.internalLinuxでのサポート

最後の方力尽きましたが、すべての変更に関してはこちらをご確認ください

QuarkusのDevServicesを試す

はじめに

先日、Quarkusの1.13がリリースされので、リリースブログを眺めていたのですがDevServicesという便利そうな機能が追加されていたので試してみようかと思います。

DevServicesとは

DevモードでQuarkusを起動した場合、追加の設定無しでDBを起動してくれてDevモードのコンフィグとバインドしてくれるようです。
例えば、PostgresSQL JDBCの拡張がPomの依存に追加されている場合Testcontainersを使って(もしくはJavaのプロセス内で)自動的にDBを立ち上げてくれます。(今回は試しませんがReactiveクライアントにも対応しているようです)
現在DevSercicesは以下のDBに対応しているようです。

また、DB2MSSQLに関してはライセンスへの同意が必要です。src/main/resources/container-license-acceptance.txtを作成して以下のようなテキストを記述する必要があります。

ibmcom/db2:11.5.0.0a
mcr.microsoft.com/mssql/server:2017-CU12

使ってみる

環境

今回は以下の環境でアプリを動かします。

$ java --version
openjdk 16 2021-03-16
OpenJDK Runtime Environment (build 16+36-2231)
OpenJDK 64-Bit Server VM (build 16+36-2231, mixed mode, sharing)

$ mvn --version
Apache Maven 3.6.3
Maven home: /usr/share/maven
Java version: 16, vendor: Oracle Corporation, runtime: /home/username/.sdkman/candidates/java/16-open
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-70-generic", arch: "amd64", family: "unix"

$ docker version
Client: Docker Engine - Community
 Version:           20.10.5
 API version:       1.41
 Go version:        go1.13.15
 Git commit:        55c4c88
 Built:             Tue Mar  2 20:18:20 2021
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.5
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       363e9a8
  Built:            Tue Mar  2 20:16:15 2021
  OS/Arch:          linux/amd64
  Experimental:     true
 containerd:
  Version:          1.4.4
  GitCommit:        05f951a3781f4f2c1911b05e61c160e9c30eaa8e
 runc:
  Version:          1.0.0-rc93
  GitCommit:        12644e614e25b05da6fd08a38ffa0cfe1903fdec
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

プロジェクトを作成する

今回はPosgresSQLを利用して、DBに保存されたデータを返す簡単なAPIを記述しようと思います。
以下の構成でプロジェクトを作成します。

f:id:yuya_hirooka:20210405140316p:plain

Devモードでアプリケーションを起動する

DevServcesはデフォルトでオンになっており、DB URLやパスワード、ユーザ名が設定されていなければTestcontainersを使ってDBを立ち上げてくれます。
先程、作成したプロジェクトをunzipしてなにも変更せずにDevモードで起動すると以下のようなログを出力してDBも立ち上げてくれます。

$ ./mvnw compile quarkus:dev
[INFO] Scanning for projects...
[INFO] 
[INFO] ----------------------< dev.hirooka:devservices >-----------------------
[INFO] Building devservices 1.0.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- quarkus-maven-plugin:1.13.0.Final:generate-code (default) @ devservices ---
[INFO] 
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ devservices ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 2 resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ devservices ---
[INFO] Nothing to compile - all classes are up to date
[INFO] 
[INFO] --- quarkus-maven-plugin:1.13.0.Final:dev (default-cli) @ devservices ---
Listening for transport dt_socket at address: 5005
2021-04-05 12:13:24,948 INFO  [org.tes.doc.DockerClientProviderStrategy] (build-28) Loaded org.testcontainers.dockerclient.EnvironmentAndSystemPropertyClientProviderStrategy from ~/.testcontainers.properties, will try it first
2021-04-05 12:13:25,299 INFO  [org.tes.doc.DockerClientProviderStrategy] (build-28) Found Docker environment with Environment variables, system properties and defaults. Resolved dockerHost=unix:///var/run/docker.sock
2021-04-05 12:13:25,300 INFO  [org.tes.DockerClientFactory] (build-28) Docker host IP address is localhost
2021-04-05 12:13:25,324 INFO  [org.tes.DockerClientFactory] (build-28) Connected to docker: 
  Server Version: 20.10.5
  API Version: 1.41
  Operating System: Ubuntu 20.04.2 LTS
  Total Memory: 31741 MB
2021-04-05 12:13:25,326 INFO  [org.tes.uti.ImageNameSubstitutor] (build-28) Image name substitution will be performed by: DefaultImageNameSubstitutor (composite of 'ConfigurationFileImageNameSubstitutor' and 'PrefixingImageNameSubstitutor')
2021-04-05 12:13:25,352 INFO  [org.tes.uti.RegistryAuthLocator] (build-28) Failure when attempting to lookup auth config. Please ignore if you don't have images in an authenticated registry. Details: (dockerImageName: testcontainers/ryuk:0.3.1, configFile: /home/yuya-hirooka/.docker/config.json. Falling back to docker-java default behaviour. Exception message: /home/yuya-hirooka/.docker/config.json (そのようなファイルやディレクトリはありません)
2021-04-05 12:13:26,102 INFO  [org.tes.DockerClientFactory] (build-28) Ryuk started - will monitor and terminate Testcontainers containers on JVM exit
2021-04-05 12:13:26,103 INFO  [org.tes.DockerClientFactory] (build-28) Checking the system...
2021-04-05 12:13:26,103 INFO  [org.tes.DockerClientFactory] (build-28) ✔︎ Docker server version should be at least 1.6.0
2021-04-05 12:13:26,185 INFO  [org.tes.DockerClientFactory] (build-28) ✔︎ Docker environment should have more than 2GB free disk space
2021-04-05 12:13:26,295 INFO  [🐳 .6.12]] (build-28) Creating container for image: postgres:9.6.12
2021-04-05 12:13:26,337 INFO  [🐳 .6.12]] (build-28) Starting container with ID: 33b42f67a20ed4d4ff1b2f0135ec06b13c3444e5782567e4d30395e0c5741fd3
2021-04-05 12:13:26,710 INFO  [🐳 .6.12]] (build-28) Container postgres:9.6.12 is starting: 33b42f67a20ed4d4ff1b2f0135ec06b13c3444e5782567e4d30395e0c5741fd3
2021-04-05 12:13:30,167 INFO  [🐳 .6.12]] (build-28) Container postgres:9.6.12 started in PT3.98152593S
__  ____  __  _____   ___  __ ____  ______ 
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ 
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \   
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/   
2021-04-05 12:13:30,588 INFO  [io.quarkus] (Quarkus Main Thread) devservices 1.0.0-SNAPSHOT on JVM (powered by Quarkus 1.13.0.Final) started in 6.064s. Listening on: http://localhost:8080
2021-04-05 12:13:30,589 INFO  [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2021-04-05 12:13:30,589 INFO  [io.quarkus] (Quarkus Main Thread) Installed features: [agroal, cdi, hibernate-orm, jdbc-postgresql, mutiny, narayana-jta, resteasy, smallrye-context-propagation]

docker psコマンドで確認してみるとPostgresとTestcontainersのコンテナがきどうしているのが確認できます。

$ docker ps
CONTAINER ID   IMAGE                       COMMAND                  CREATED         STATUS         PORTS                     NAMES
74541a57e8cd   postgres:9.6.12             "docker-entrypoint.s…"   3 minutes ago   Up 3 minutes   0.0.0.0:49164->5432/tcp   great_blackburn
72f55524eecf   testcontainers/ryuk:0.3.1   "/app"                   3 minutes ago   Up 3 minutes   0.0.0.0:49163->8080/tcp   testcontainers-ryuk-42c2bcf5-1ddc-415d-b694-3f7f311d885e

データを取得してみる

それでは、実際にこの起動したDB利用してみようと思います。
まずは初期データを投入します。
まずはapplication.propertiesを以下のように修正します。

%dev.quarkus.hibernate-orm.log.sql=true
%dev.quarkus.hibernate-orm.database.generation=drop-and-create

quarkus.hibernate-orm.database.generation=drop-and-createを指定することで、Quarkusのアプリは起動時にsqlファイルを読み取ってテーブルを作成してくれるようになります。
また、デフォルトではresourcesimport.sqlという名前のファイルを読み取るようになっているのでそちらも用意しておきます。

drop table if exists message;

create table message(
    id      int,
    content varchar(100)
);

insert into message
values (1, 'hello, dev service1'),
       (2, 'hello, dev service2'),
       (3, 'hello, dev service3');

quarkus.hibernate-orm.log.sqlSQLの実行がわかりやすいように設定しています。また%devはDevモードで起動時に有効になる設定を示します。

この状態でアプリケーションを再起動します。
すると、ここには記述しませんが実行されたSQLのログが出力されるはずです。

ここまでで、起動したDBを利用できていることががある程度確認できたかもしれませんが。
EntityとControllerを作成してmessageを取得してみようと思います。

MessageEntity.java

@Entity
public class MessageEntity {

    @Id
    private int id;

    private String content;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

GreetingResource .java

@Path("/messages")
public class GreetingResource {
    private EntityManager em;

    public GreetingResource(EntityManager em) {
        this.em = em;
    }


    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<MessageEntity> hello() {
        return em.createNativeQuery("select id, content from message", MessageEntity.class).getResultList();
    }
}

コントローラーはデフォルトで作成されたGreetingResource.javaを少し改造して作っています。また、少し筋は悪いかもしれませんがEntityをそのままレスポンスとして返すようにしています。

アプリケーションがリロードされたらcURLを叩いてみます。

$ curl localhost:8080/messages
[{"id":1,"content":"hello, dev service1"},{"id":2,"content":"hello, dev service2"},{"id":3,"content":"hello, dev service3"}]

import.sqlでセットしていた値が帰ってきていることが確認できました。

本番用の設定を記述する

前述したとおりDevServicesはDB URLなどがセットされている場合においてDBを起動しないようになっています。
例えば、以下のように設定を記述してアプリケーションを起動すると、DBが起動されません。

quarkus.datasource.db-kind=postgresql
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/hibernate_orm_test
quarkus.datasource.username=username
quarkus.datasource.password=password

起動すると、import.sqlの実行に失敗したエラーログが出力されたと思います。
application.properitesに本番用の設定値を書きたいが、ローカルでの開発ではDevServicesを利用したい場合は設定のプリフィクスとして%prodを以下のように付与します。

%prod.quarkus.datasource.db-kind=postgresql
%prod.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/hibernate_orm_test
%prod.quarkus.datasource.username=username
%prod.quarkus.datasource.password=password

これで上記の設定はDevモードでは用いられることはなくDevServicesはDBを立ち上げてくれます。

その他の設定値

最後にDevServicesの設定をいくつかまとめておこうと思います。完全なリストはこちら をご覧ください。

設定値 説明 デフォルト値(型)
quarkus.datasource.devservices DevServicesを明示的に有効化するかのフラグ true(boolean)
quarkus.datasource.devservices.image-name 利用するコンテナイメージ名を指定する。もしH2などコンテナを起動しないサービスの場合は影響を及ぼさない (string)

また、ここにはまとめてませんが、名前付きデータソースごとにそれぞれの設定を行なうことも可能なようです。

k8s上のSpring Bootアプリからメトリクスを取得する(Actuator + Prometheus Operator)

はじめに

Kubernetes上でのSpring Bootのアプリのデバックを行なう際にPodのコンテナ内に応じて必要なコマンドとってきて、スレッドダンプやヒープダンプを取得してホストにコピーみたいなことをやっていたのですが、流石に面倒に感じはじめました。
そこでふとActuator入れりゃええやんと思いたち、どうせだったら、Prometeheusでメトリクスの収集までできればいろいろええんじゃないかと思いいろいろ動かしてみようと思います。

Spring Boot Actuatorとは

(まず、前提としてこのブログはBoot 2.4系のドキュメントを確認してます。貼られているリンクもそうなってると思います。)
Spring Boot Actuatorはアプリケーションをモニター、マネージするための追加機能を提供してくれています。HTTPやJMXのエンドポイントを通して操作を選択することができます(JMXを利用する場合はspring.jmx.enabledtrueで設定する必要があります)。HTTPを利用する場合はデフォルトでは/actuator/{id}の形式で情報を取得することが可能で、たとえばhealthの情報を取得したい場合は/actuator/healthのエンドポイントで利用できます。カスタムエンドポイントを公開することもできますが、基本としては以下のようなエンドポイントが用意されています。

ID 説明
health アプリケーションの正常性情報を取得でる。このエンドポイントを通して、実行中のアプリケーション(本体と関連するアプリケーション)ステータスを確認することができる。関連するアプリケーションに関してはHealthContributorRegistryに定義されているすべてのHealthContributorから収集される。(詳細はこちら
httptrace HTTPのトレース情報(リクエスト/レスポンス)を取得できる。デフォルトで最新100個をインメモリに保存。HTTP トレースを有効にするには、アプリケーションの構成で HttpTraceRepository型をBeanとして提供する必要がある
beans アプリケーション内のSpring Beanのリストを取得できる
mappings @RequestMappingのパスのリストを表示します
shutdown アップリケーションをシャットダウンする
threaddump スレッドダンプを取得できる
heapdump hprofヒープダンプを取得できる。アプリケーションがSpring MVCやWebFluxである場合に有効
logfile ログの内容を返す。logging.file.namelogging.file.pathプロパティが設定されている必要がある。アプリケーションがアプリケーションがSpring MVCやWebFluxである場合に有効

詳細なリストに関してはこちらをご覧ください。
また、Prometheusフォーマットで出力してくれるエンドポイントも提供されており今回はこちらを利用して、メトリクスを収集しようと思います。

エンドポイントの公開に関するセキュリティ的な考慮点に付いて

Actuatorでアプリケーションのインフラレイヤーやアプリケーションレイヤーで保護しておく必要があります。アプリケーションレイヤーでは「必要なエンドポイントのみ公開する」のが基本的な戦略となると思います。ActuatorではデフォルトでHTTPのエンドポイントはhealthinfo以外は非公開になっているようです。 これらのエンドポイントを公開非公開を制御する場合は以下のようなincludeもしくはexcluedeプロパティを使用します。例えば、info, health, threaddumpのエンドポイントを公開したい場合は、アプリケーションプロパティで以下のように設定します。

management.endpoints.web.exposure.include=info, health, threaddump

その他のプロパティに関してはこちらをご覧ください。

単純な公開非公開とは別に、Spring Securityを利用している場合、そのコンテンツネゴシエーションの仕組みを利用してエンドポイントを保護することができます。例えば、特定のロールを持つユーザのみアクセス可能と言ったような制御を行なうことができます。

やっていく

環境

今回KubernetesクラスタはMinikubeを用いてローカルに構成します。そのVMはデフォルトのDockerを利用します。
ミドルウェア、OSのバージョンは以下のとおりです。

$ uname -srvmpio
Linux 5.4.0-70-generic #78-Ubuntu SMP Fri Mar 19 13:29:52 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
LSB Version:    core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.2 LTS
Release:    20.04
Codename:   focal


(Minikubeが動いているDocker)
$ docker version

(クライアントは省略)

Server: Docker Engine - Community
 Engine:
  Version:          20.10.5
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       363e9a8
  Built:            Tue Mar  2 20:16:15 2021
  OS/Arch:          linux/amd64
  Experimental:     true
 containerd:
  Version:          1.4.4
  GitCommit:        05f951a3781f4f2c1911b05e61c160e9c30eaa8e
 runc:
  Version:          1.0.0-rc93
  GitCommit:        12644e614e25b05da6fd08a38ffa0cfe1903fdec
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0


$ minikube version
minikube version: v1.16.0
commit: 9f1e482427589ff8451c4723b6ba53bb9742fbb1

$ kubectl version -o yaml
clientVersion:
  buildDate: "2021-03-18T01:10:43Z"
  compiler: gc
  gitCommit: 6b1d87acf3c8253c123756b9e61dac642678305f
  gitTreeState: clean
  gitVersion: v1.20.5
  goVersion: go1.15.8
  major: "1"
  minor: "20"
  platform: linux/amd64
serverVersion:
  buildDate: "2020-12-08T17:51:19Z"
  compiler: gc
  gitCommit: af46c47ce925f4c4ad5cc8d1fca46c7b77d13b38
  gitTreeState: clean
  gitVersion: v1.20.0
  goVersion: go1.15.5
  major: "1"
  minor: "20"
  platform: linux/amd64

環境構築

プロジェクトの作成 & Ptometeusのメトリクスを取得できるように設定

まずは、プロジェクトを作成します。
プロジェクトはSpring Initializrを利用して以下のような構成で作成します。

f:id:yuya_hirooka:20210328124237p:plain

Webの他にActuatorとPrometheusの選択肢を選択肢てます。
出来上がったプロジェクトのPomの依存を確認すると以下の2つが存在することが確認できます。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
    <scope>runtime</scope>
</dependency>

Actuatorの出力をPrometheusの形式で出力する場合micrometer-registry-prometheusの依存を追加する必要があります。今回は初期Initializrで追加しましたが、既存のBootプロジェクトにActuatorとPromtheusを使ってメトリクスの収集をする場合は上記の2つの依存を追加してやれば大丈夫だと思います。

コントローラーを作っておく

GETでリクエストを受けると文字列を返すコントローラーを作っておきます。
コンポーネントスキャン対象のパッケージ配下にいかのようなGreetingControllerを作成します。

@RestController
public class GreetingController {
    @GetMapping("/hello")
    public String hello(){
        return "hello, hello";
    }
}

起動してcURLで作ったコントローラーとActuatorのエンドポイントにアクセスしてみます。

$ curl localhost:8080/hello
hello, hello

$ curl localhost:8080/actuator/health
{"status":"UP"}

Prometheusのエンドポイントの公開設定を行なう

前述の通りActuatorのエンドポイントはhealthinfo以外は非公開になっています。/actuatorにアクセスすると、公開されているエンドポイントの情報を取得することができます。

$ curl localhost:8080/actuator/ | jq
{
  "_links": {
    "self": {
      "href": "http://localhost:8080/actuator",
      "templated": false
    },
    "health": {
      "href": "http://localhost:8080/actuator/health",
      "templated": false
    },
    "health-path": {
      "href": "http://localhost:8080/actuator/health/{*path}",
      "templated": true
    },
    "info": {
      "href": "http://localhost:8080/actuator/info",
      "templated": false
    }
  }

Prometheus用のエンドポイントを公開するには、設定を追加する必要があります。
また、もともとの困りごとだったダンプの取得を行なうエンドポイントも公開しておきます。 アプリケーションプロパティに以下のように設定を行います。

# ついでにPort番号も変えておきます。
server.port=8888

management.endpoints.web.exposure.include=info, health, threaddump, prometheus, heapdump

再度/actuatorにアクセスしてみます。

$ curl localhost:8888/actuator | jq
{
  "_links": {
    "self": {
      "href": "http://localhost:8888/actuator",
      "templated": false
    },
    "health": {
      "href": "http://localhost:8888/actuator/health",
      "templated": false
    },
    "health-path": {
      "href": "http://localhost:8888/actuator/health/{*path}",
      "templated": true
    },
    "info": {
      "href": "http://localhost:8888/actuator/info",
      "templated": false
    },
    "heapdump": {
      "href": "http://localhost:8888/actuator/heapdump",
      "templated": false
    },
    "threaddump": {
      "href": "http://localhost:8888/actuator/threaddump",
      "templated": false
    },
    "prometheus": {
      "href": "http://localhost:8888/actuator/prometheus",
      "templated": false
    }
  }
}
yuya-hirooka@yuya-hirooka:~/source/java/actu

先程と比べて、設定したしたエンドポイントの情報が出力されているのがわかります。
Prometheus形式のメトリクスの取得をしてみます。

$ curl 'http://localhost:8888/actuator/prometheus'
# HELP jvm_memory_committed_bytes The amount of memory in bytes that is committed for the Java virtual machine to use
# TYPE jvm_memory_committed_bytes gauge
jvm_memory_committed_bytes{area="heap",id="G1 Survivor Space",} 0.0
jvm_memory_committed_bytes{area="heap",id="G1 Old Gen",} 5.4525952E7
jvm_memory_committed_bytes{area="nonheap",id="Metaspace",} 3.0539776E7
jvm_memory_committed_bytes{area="nonheap",id="CodeHeap 'non-nmethods'",} 2555904.0
jvm_memory_committed_bytes{area="heap",id="G1 Eden Space",} 4.6137344E7
jvm_memory_committed_bytes{area="nonheap",id="Compressed Class Space",} 4325376.0
jvm_memory_committed_bytes{area="nonheap",id="CodeHeap 'non-profiled nmethods'",} 6815744.0
# HELP tomcat_sessions_alive_max_seconds  
# TYPE tomcat_sessions_alive_max_seconds gauge
tomcat_sessions_alive_max_seconds 0.0
# HELP jvm_threads_states_threads The current number of threads having NEW state
# TYPE jvm_threads_states_threads gauge
jvm_threads_states_threads{state="runnable",} 11.0
jvm_threads_states_threads{state="blocked",} 0.0
jvm_threads_states_threads{state="waiting",} 11.0
jvm_threads_states_threads{state="timed-waiting",} 4.0
jvm_threads_states_threads{state="new",} 0.0
jvm_threads_states_threads{state="terminated",} 0.0
# HELP system_cpu_usage The "recent cpu usage" for the whole system
# TYPE system_cpu_usage gauge
system_cpu_usage 1.0
# HELP jvm_gc_memory_promoted_bytes_total Count of positive increases in the size of the old generation memory pool before GC to after GC
# TYPE jvm_gc_memory_promoted_bytes_total counter
jvm_gc_memory_promoted_bytes_total 9284952.0
# HELP jvm_threads_peak_threads The peak live thread count since the Java virtual machine started or peak was reset
# TYPE jvm_threads_peak_threads gauge
jvm_threads_peak_threads 30.0
# HELP jvm_threads_daemon_threads The current number of live daemon threads
# TYPE jvm_threads_daemon_threads gauge
jvm_threads_daemon_threads 22.0
# HELP tomcat_sessions_active_current_sessions  
# TYPE tomcat_sessions_active_current_sessions gauge
tomcat_sessions_active_current_sessions 0.0
# HELP jvm_buffer_total_capacity_bytes An estimate of the total capacity of the buffers in this pool
# TYPE jvm_buffer_total_capacity_bytes gauge
jvm_buffer_total_capacity_bytes{id="mapped - 'non-volatile memory'",} 0.0
jvm_buffer_total_capacity_bytes{id="mapped",} 0.0
jvm_buffer_total_capacity_bytes{id="direct",} 49152.0
# HELP system_load_average_1m The sum of the number of runnable entities queued to available processors and the number of runnable entities running on the available processors averaged over a period of time
# TYPE system_load_average_1m gauge
system_load_average_1m 2.04
# HELP jvm_memory_used_bytes The amount of used memory
# TYPE jvm_memory_used_bytes gauge
jvm_memory_used_bytes{area="heap",id="G1 Survivor Space",} 0.0
jvm_memory_used_bytes{area="heap",id="G1 Old Gen",} 1.8475864E7
jvm_memory_used_bytes{area="nonheap",id="Metaspace",} 3.016488E7
jvm_memory_used_bytes{area="nonheap",id="CodeHeap 'non-nmethods'",} 1257088.0
jvm_memory_used_bytes{area="heap",id="G1 Eden Space",} 0.0
jvm_memory_used_bytes{area="nonheap",id="Compressed Class Space",} 4117488.0
jvm_memory_used_bytes{area="nonheap",id="CodeHeap 'non-profiled nmethods'",} 6814208.0
# HELP system_cpu_count The number of processors available to the Java virtual machine
# TYPE system_cpu_count gauge
system_cpu_count 8.0
# HELP jvm_buffer_memory_used_bytes An estimate of the memory that the Java virtual machine is using for this buffer pool
# TYPE jvm_buffer_memory_used_bytes gauge
jvm_buffer_memory_used_bytes{id="mapped - 'non-volatile memory'",} 0.0
jvm_buffer_memory_used_bytes{id="mapped",} 0.0
jvm_buffer_memory_used_bytes{id="direct",} 49152.0
# HELP process_uptime_seconds The uptime of the Java virtual machine
# TYPE process_uptime_seconds gauge
process_uptime_seconds 231.285
# HELP tomcat_sessions_rejected_sessions_total  
# TYPE tomcat_sessions_rejected_sessions_total counter
tomcat_sessions_rejected_sessions_total 0.0
# HELP tomcat_sessions_created_sessions_total  
# TYPE tomcat_sessions_created_sessions_total counter
tomcat_sessions_created_sessions_total 0.0
# HELP logback_events_total Number of error level events that made it to the logs
# TYPE logback_events_total counter
logback_events_total{level="warn",} 0.0
logback_events_total{level="debug",} 0.0
logback_events_total{level="error",} 0.0
logback_events_total{level="trace",} 0.0
logback_events_total{level="info",} 7.0
# HELP jvm_classes_unloaded_classes_total The total number of classes unloaded since the Java virtual machine has started execution
# TYPE jvm_classes_unloaded_classes_total counter
jvm_classes_unloaded_classes_total 0.0
# HELP jvm_buffer_count_buffers An estimate of the number of buffers in the pool
# TYPE jvm_buffer_count_buffers gauge
jvm_buffer_count_buffers{id="mapped - 'non-volatile memory'",} 0.0
jvm_buffer_count_buffers{id="mapped",} 0.0
jvm_buffer_count_buffers{id="direct",} 6.0
# HELP process_cpu_usage The "recent cpu usage" for the Java Virtual Machine process
# TYPE process_cpu_usage gauge
process_cpu_usage 0.0
# HELP jvm_gc_live_data_size_bytes Size of long-lived heap memory pool after reclamation
# TYPE jvm_gc_live_data_size_bytes gauge
jvm_gc_live_data_size_bytes 1.8475864E7
# HELP jvm_gc_memory_allocated_bytes_total Incremented for an increase in the size of the (young) heap memory pool after one GC to before the next
# TYPE jvm_gc_memory_allocated_bytes_total counter
jvm_gc_memory_allocated_bytes_total 6.291456E7
# HELP tomcat_sessions_expired_sessions_total  
# TYPE tomcat_sessions_expired_sessions_total counter
tomcat_sessions_expired_sessions_total 0.0
# HELP jvm_threads_live_threads The current number of live threads including both daemon and non-daemon threads
# TYPE jvm_threads_live_threads gauge
jvm_threads_live_threads 26.0
# HELP jvm_gc_pause_seconds Time spent in GC pause
# TYPE jvm_gc_pause_seconds summary
jvm_gc_pause_seconds_count{action="end of major GC",cause="Heap Dump Initiated GC",} 2.0
jvm_gc_pause_seconds_sum{action="end of major GC",cause="Heap Dump Initiated GC",} 0.077
jvm_gc_pause_seconds_count{action="end of minor GC",cause="G1 Evacuation Pause",} 1.0
jvm_gc_pause_seconds_sum{action="end of minor GC",cause="G1 Evacuation Pause",} 0.005
# HELP jvm_gc_pause_seconds_max Time spent in GC pause
# TYPE jvm_gc_pause_seconds_max gauge
jvm_gc_pause_seconds_max{action="end of major GC",cause="Heap Dump Initiated GC",} 0.0
jvm_gc_pause_seconds_max{action="end of minor GC",cause="G1 Evacuation Pause",} 0.0
# HELP http_server_requests_seconds  
# TYPE http_server_requests_seconds summary
http_server_requests_seconds_count{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator/heapdump",} 2.0
http_server_requests_seconds_sum{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator/heapdump",} 0.425517121
http_server_requests_seconds_count{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator",} 1.0
http_server_requests_seconds_sum{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator",} 0.024393608
# HELP http_server_requests_seconds_max  
# TYPE http_server_requests_seconds_max gauge
http_server_requests_seconds_max{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator/heapdump",} 0.0
http_server_requests_seconds_max{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator",} 0.0
# HELP process_start_time_seconds Start time of the process since unix epoch.
# TYPE process_start_time_seconds gauge
process_start_time_seconds 1.616906295082E9
# HELP tomcat_sessions_active_max_sessions  
# TYPE tomcat_sessions_active_max_sessions gauge
tomcat_sessions_active_max_sessions 0.0
# HELP jvm_memory_max_bytes The maximum amount of memory in bytes that can be used for memory management
# TYPE jvm_memory_max_bytes gauge
jvm_memory_max_bytes{area="heap",id="G1 Survivor Space",} -1.0
jvm_memory_max_bytes{area="heap",id="G1 Old Gen",} 8.321499136E9
jvm_memory_max_bytes{area="nonheap",id="Metaspace",} -1.0
jvm_memory_max_bytes{area="nonheap",id="CodeHeap 'non-nmethods'",} 7553024.0
jvm_memory_max_bytes{area="heap",id="G1 Eden Space",} -1.0
jvm_memory_max_bytes{area="nonheap",id="Compressed Class Space",} 1.073741824E9
jvm_memory_max_bytes{area="nonheap",id="CodeHeap 'non-profiled nmethods'",} 2.44105216E8
# HELP jvm_classes_loaded_classes The number of classes that are currently loaded in the Java virtual machine
# TYPE jvm_classes_loaded_classes gauge
jvm_classes_loaded_classes 7304.0
# HELP process_files_open_files The open file descriptor count
# TYPE process_files_open_files gauge
process_files_open_files 60.0
# HELP jvm_gc_max_data_size_bytes Max size of long-lived heap memory pool
# TYPE jvm_gc_max_data_size_bytes gauge
jvm_gc_max_data_size_bytes 8.321499136E9
# HELP process_files_max_files The maximum file descriptor count
# TYPE process_files_max_files gauge
process_files_max_files 1048576.0

JVMの情報とか諸々が出力されてるが見て取れます。

threddump、heapdumpを取得するときは以下のようにします。

$ curl localhost:8888/actuator/threaddump -H 'Accept: text/plain' > threddump.txt
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 30125  100 30125    0     0   668k      0 --:--:-- --:--:-- --:--:--  684k

$ curl 'http://localhost:8888/actuator/heapdump' -O
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 27.6M  100 27.6M    0     0   124M      0 --:--:-- --:--:-- --:--:--  124M

Docker Imageの作成

アプリケーションの準備として最後に作成したアプリをDockerイメージ化しておきます。
ここで、今回はDockerのリポジトリを個別に用意したりDocker HubにイメージをPushのは面倒なので、Minikubeのコンテキストでイメージをビルドします。
具体的には次のコマンドを実行します。

$ eval $(minikube docker-env)
f

$ docker images
REPOSITORY                                TAG        IMAGE ID       CREATED         SIZE
kubernetesui/dashboard                    v2.1.0     9a07b5b4bfac   3 months ago    226MB
k8s.gcr.io/kube-proxy                     v1.20.0    10cc881966cf   3 months ago    118MB
k8s.gcr.io/kube-apiserver                 v1.20.0    ca9843d3b545   3 months ago    122MB
k8s.gcr.io/kube-scheduler                 v1.20.0    3138b6e3d471   3 months ago    46.4MB
k8s.gcr.io/kube-controller-manager        v1.20.0    b9fa1895dcaa   3 months ago    116MB
gcr.io/k8s-minikube/storage-provisioner   v4         85069258b98a   3 months ago    29.7MB
k8s.gcr.io/etcd                           3.4.13-0   0369cf4303ff   7 months ago    253MB
k8s.gcr.io/coredns                        1.7.0      bfe3a36ebd25   9 months ago    45.2MB
kubernetesui/metrics-scraper              v1.0.4     86262685d9ab   12 months ago   36.9MB
k8s.gcr.io/pause                          3.2        80d28bedfe5d   13 months ago   683kB

これで一時的にMinikubeのコンテキストでDocker ビルドできるようになりました。
minikube docker-envコマンドはMinikube組み込みのDockerデーモンをホストマシンから操作できるようにするためのコマンドです。

この状態でイメージをビルドします。

Spring Bootは2.3からBuildpackを利用したイメージのビルドをサポートしています。今回はその機能を利用してイメージを作成したいと思います。
具体的にはプロジェクトルートで以下のコマンドを実行します。

$ ./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=spring/actuator-prometheus

$ docker images | grep spring

$ docker images | grep spring/actuator-prometheus
spring/actuator-prometheus                                         latest                                          2b5438489b00   41 years ago    278MB

なんかCreatedがバグってる気がしますが、一旦はこれでOKです。

クラスターの設定 & 諸々のリソース作成

今回クラスターは以前のブログで作成したものを再利用しようと思います。構成としてはMinikubeで作ったクラスタkube-promtheusを使ってPrometheusとGrafanaをデプロイ&設定を行っています。

PodMonitorを作成

通常、Prohetheusがk8sクラスタ内のサービスを見つける際にはkubernetes_sd_configをう場合が多いみたいですが、今回はkube-prometheusを利用しており、Kubernetes Operatorが利用できます。Kubernetes OperatorではPodMonitorPodMetricsEndpointsというCRDが用意されており、そっちを利用してやってみようと思います。
以下のようなマニフェストファイルを用意します。

apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
  name: spring-app
  labels:
    backend: spring-app
spec:
  selector:
    matchLabels:
      app: spring-app
  podMetricsEndpoints:
  - port: app-port
  - path: /actuator/prometheus

ポイントはpodMetricsEndpointsのところで、ここにはPodで指定するポートの名前と、メトリクスを収集するPathを指定します。Actuatorはデフォルトでは/actuator/prometheusでメトリクスを公開しているので、そのパスを指定します。

作ったマニフェストをApplyしておきます。

$ kubectl apply -f pod-monitor.yaml 
podmonitor.monitoring.coreos.com/spring-app created

$ kubectl get podmonitor
NAME         AGE
spring-app   19s

Doploymentを作成

それでは、Deploymentを作成して、作ったSpringのアプリをKubernetesにデプロイします。
まずは、--dry-runオプションを使ってベースとなるDeploymentを作成します。

$ kubectl create deployment spring-app --image=spring/actuator-prometheus --dry-run=client -o yaml

できたマニフェストを以下のように修正します。

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: spring-app
  name: spring-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: spring-app
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: spring-app
    spec:
      containers:
      - image: spring/actuator-prometheus
        name: actuator-prometheus
        resources: {}
status: {}

このマニフェストを以下のように修正します。

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: spring-app
  name: spring-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: spring-app
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: spring-app
    spec:
      containers:
      - image: spring/actuator-prometheus
        name: actuator-prometheus
        imagePullPolicy: IfNotPresent
        ports:
          - name: spring-app
            containerPort: 8888
        resources: {}
status: {}

具体的には

        imagePullPolicy: IfNotPresent
        ports:
          - name: spring-app
            containerPort: 8888

のところを追記しました。 imagePullPolicy: IfNotPresentはローカルにイメージがある場合はPullしないようの設定と、PodMonitorが利用するようにportに名前を付けています。

それではこのDeploymentをApplyします。

$ kubectl apply -f deployment.yaml 
deployment.apps/spring-app created

Prometheusからメトリクスを確認する

実はここまでで大体設定が終わっていて、すでにPrometheusでのメトリクスの収集が始まってます。
Prometheusのダッシュボードでメトリクスを確認してみます。
まずは、デプロイされているプロメテウスにlocalhostでアクセスするためにポートフォワードを行います。

$ kubectl --namespace monitoring port-forward svc/prometheus-k8s 9090[f:id:yuya_hirooka:20210403140913p:plain]
Forwarding from 127.0.0.1:9090 -> 9090
Forwarding from [::1]:9090 -> 9090

http://localhost:9090にアクセスしてUIから確認すると以下のようにJVMのメトリクスを取得できるようになっています。

f:id:yuya_hirooka:20210403140002p:plain

試しにjvm_memory_used_bytesをグラフ化してみると以下のようにヒープの領域ごとにいい感じにグラフ化できることが確認できました。

f:id:yuya_hirooka:20210403140913p:plain