💬

Raspberry Pi with ベアメタル Kubernetes クラスタに OpenProject と Grafana を構築する

に公開

はじめに

こんにちは、クラウドエース株式会社の三原と申します。

最近、加齢と共に認知能力が衰退していくのを感じる今日この頃です。
加齢の一方で、日常的なタスクの増加に拍車が掛かっており、脳内でタスクを管理するのに限界を感じております。なので、日常でもタスク管理ツールを導入しようという課題がありました。ただ、SaaS に頼るのも面白くないと感じました。どうせなら、セルフホスト型のものを好きに使いたいと思ったわけです。

そして、昨今のコンテナオーケストレーションの技術選定において、Amazon EKS や Google Kubernetes Engine 等のマネージド Kubernetes(以下、k8s) を選択する事が多い中で、敢えてオンプレミスで k8s クラスタを構築する事で得られるものがあるのではないか、とも思ったわけです。

そうした経緯から Raspberry Pi 5 (公式 SSD 512 GB) × 2台購入して k8s クラスタを構築する試みをしたので、その事跡を共有していきます。

この記事でつくるもの

Ubuntu Server 25.04 を書き込んだ SSD を積んだ Raspberry Pi 5 二台を組み立てて、マスターノード1台、ワーカーノード1台の最小構成ベアメタル k8s クラスタを構築します。

k8sクラスタの中で、 OpenProject と Grafana (とそれらで使う PostgreSQL や Memcached 等)を構築します。

そして、クライアント(192.168.11.*)が k8s 上 のロードバランサーが払い出した IP アドレス宛(192.168.11.200 - 192.168.11.250)に HTTP リクエストする事で、url パスに応じ Ingress が対応するバックエンドサービスにルーティングします。

/openproject であれば OpenProject

/grafana であれば Grafana にアクセスができます。

念のため、OpenProject と Grafana に関して触れておきます
(記事のボリュームの関係上、詳細な説明は省きます。)

OpenProject is 何

軽く説明すると OpenProject は OSS のプロジェクト管理ツールとして歴史の長い Redmine から fork された、より高機能なツールです。

UI も Redmine に比べてリッチですが、高機能故に習熟に時間を要する点で個人レベルでの運用になると持て余す機能が多いように思います。

Grafana is 何

ログ等のデータからグラフィカルなダッシュボードを作成・共有する事が出来る OSS ツールです。
日常的な運用では クレカ やPayPay の明細をデータソースとして使う事で、支出をグラフィカルなグラフに出したりできます。


コンビニに入るたびに無意味にハイボール缶買うとこういうグラフになるんですね...

構築編

構築の章立てとして以下の順で説明していきます。

  • (1) Raspberry Pi 組み立て編
    • Raspberry Pi とか諸々を買う
    • Raspberry Pi 組み立て
    • SSD に OS 焼きこみ
    • Raspberry Pi の IP アドレスの固定化
  • (2) k8s クラスタ構築編
    • ランタイムやプラグインのインストールと有効化
    • IPv4フォワーディング有効化と Swap の無効化
    • 必要なツール,エージェントのインストール
    • CGROUPS_MEMORY の有効化,クラスタの構築
    • Flannel の 適用
  • (3) k8s の諸々のオブジェクト定義
    • MetalLB
    • Ingress NGINX Controller
    • Grafana
    • PostgreSQL + Memcached
    • OpenProject

(1) Raspberry Pi 組み立て編

Raspberry Pi とか諸々を買う

アマゾンと秋月電子さんでめちゃくちゃ買いました。

買ったものリスト

商品名 単価 数量 合計
Raspberry Pi5 8G ¥13,600 2 ¥27,200
Raspberry Pi SSD Kit 512GB ¥9,980 2 ¥19,960
Raspberry Pi 5 用公式アクティブクーラー ¥900 2 ¥1,800
Raspberry Pi 5 電源 5.1V 5.0A 27W USB Type-C ¥1,850 2 ¥3,700

その他諸々ケーブル類だったりハブだったり、必要ないのに無意味に買ってしまったりしたもので計7万ぐらい吹き飛んでます。

・Raspberry Pi 5 8GB

Raspberry Pi 本体です。メモリは2 GB から16 GB モデルまであります。
今回のシリーズから PCIe が搭載された事により、NVMe SSD を直接接続できるようになりました。これにより、高い IOPS が要求されるシナリオにおいて、NVMe SSD の高速なデータ転送能力を最大限に活用することが可能になります。

余談ですが USB-C 電力仕様も従来のシリーズから変更された関係か、発熱しやすいようでより冷却回りが重要になったと言えます。

https://www.raspberrypi.com/products/raspberry-pi-5/

・Raspberry Pi SSD Kit 512GB

Raspberry Pi M.2 HAT+ と SSD が予め組み立てられている公式セットです。256 GB と 512 GB のものが販売されています。

microSD ではなく SSD を選定したのは、今後 水温センサーや pH センサー等からのストリーミングデータ処理を Raspberry Pi に担わせる展望があり、高い IOPS が要求されるような気がしたからです。

https://www.raspberrypi.com/documentation/accessories/ssd-kit.html#about

Raspberry Pi 組み立て

基本的には 公式ドキュメントの M.2 HAT+ に記載の手順に沿って構築していきます。

アクティブクーラーを付けて

Raspberry Pi M.2 HAT+取り付ける作業を2台分実施します。

この工程で重要なのは 必ずリボンケーブルをしっかり取り付ける 事です。

というのも、しっかり取り付けないと PCIe 接続が確立せずに SSD がデバイスとして認識されない為です。

例えば以下は RasberryPi リポジトリで報告されている、PCIe 接続に問題がある旨の Issue です。

https://github.com/raspberrypi/linux/issues/6511

この Issue はハードウェア障害が認められ、交換対応という形で落着していますが、カーネルから検出される以下の PCIe が接続できない旨のエラーメッセージはリボンケーブルが正常に接続されていなくても出ます。

[ 0.681085] brcm-pcie 1000110000.pcie: link down

Lift the ribbon cable holder from both sides, then insert the cable with the copper contact points facing inward, towards the USB ports. With the ribbon cable fully and evenly inserted into the PCIe port, push the cable holder down from both sides to secure the ribbon cable firmly in place.

Raspberry Pi 公式ドキュメントの記載を見るとそこまで難しい手順は書いてないのですが、慣れていないと意外と正常にセッティングできなかったりするかもしれません。

同様の製品で再現されただけに、自分はハードウェア障害を疑ってブート順の制御を変えたり、交換対応の窓口を探したりしてリボンケーブルがただしっかり接続されていないだけという事実に気づくまでに、半日吹き飛びました。

組み立て終わったら AC ケーブルと接続したり LAN ケーブルを差したり します。

SSD に OS 焼きこみ

任意の作業用 PC の Raspberry Pi Imager で 適当な microSD に Raspberry Pi OS(64) を焼きこみます。

その後に、OS を焼きこんだ microSD を Raspberry Pi に差して OS に プリインストールされている Raspberry Pi Imager を使用して、SSD に Ubuntu Server 25.04 を書き込みます。(二台分やります)

Raspberry Pi の IP アドレスの固定化

それぞれ以下の表に準じてIPを固定します。

ノード種別 ホスト名 IPアドレス
マスターノード p1 192.168.11.10/24
ワーカーノード p2 192.168.11.11/24

マスターノード(1個目の Raspberry Pi )

pyuser@p1:~$ cat /etc/netplan/01-net.yaml 
network:
  version: 2
  ethernets:
    eth0:
       dhcp4: no
       addresses:
        - 192.168.11.10/24

ワーカーノード(2個目の Raspberry Pi )

pyuser@p2:~$ cat /etc/netplan/01-net.yaml 
network:
   version: 2
   ethernets:
    eth0:
      dhcp4: no
      addresses:
        - 192.168.11.11/24

(2) k8s クラスタ構築編

以下から、必要なパッケージをインストールしたりカーネルパラメータを調整したりしていきます。まずは、マスターノードに相当する一台目の Raspberry Pi から作業していきます。

ランタイムやプラグインのインストールと有効化

k8s のコンテナランタイムの containerd、及び 低レベルランタイムの runc、コンテナネットワーク構成に必要な CNI plugin をインストールします。containerdのセットアップ手順に沿って作業します。詳細は公式ドキュメントをご参照ください。

containerd

containerd は OSS の高レベルのコンテナランタイムで、後述の kubelet と連携してコンテナの状態管理を担います。

$ wget https://github.com/containerd/containerd/releases/download/v2.0.5/containerd-2.0.5-linux-arm64.tar.gz
$ sudo tar Cxzvf /usr/local containerd-2.0.5-linux-arm64.tar.gz
$ sudo mkdir -p /usr/local/lib/systemd/system
$ sudo wget https://raw.githubusercontent.com/containerd/containerd/main/containerd.service -O /usr/local/lib/systemd/system/containerd.service
$ sudo mkdir -p /etc/containerd
$ sudo containerd config default | sudo tee /etc/containerd/config.toml

runc

runcOCI Runtime Spec に準じた低レベルコンテナランタイムで、先述の上流レイヤーに相当する containerd からのコンテナに関する諸々のコマンドに対する I/F を提供します。

runc は コンテナ毎のメモリやらCPU等のシステムリソースの制御に Linux のカーネル機能の cgroup を利用しています。そして、cgroup はバージョン分けされており、 v1 と v2 が存在しています
そして、Ubuntu 21.10 以降はデフォルトで cgroup v2を使用されており、cgroup v2 を使用する場合は systemd cgroupドライバーの利用を推奨されております

$ wget https://github.com/opencontainers/runc/releases/download/v1.2.6/runc.arm64
$ sudo install -m 755 runc.arm64 /usr/local/sbin/runc

上述の通り、systemd cgroup ドライバーを使うようにする為に、containerd の設定ファイルの内容を置換します。

$ sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
$ systemctl daemon-reload
$ systemctl enable --now containerd

CNI plugin

CNI プラグインはコンテナネットワークの I/F を提供するバイナリ型実行ファイルで、後述の kubeletFlannel がコンテナのネットワーク構成を制御するのに使用する為、/opt/cni/bin に配置する必要があります。

$ sudo mkdir -p /opt/cni/bin
$ wget https://github.com/containernetworking/plugins/releases/download/v1.7.1/cni-plugins-linux-arm64-v1.7.1.tgz
$ sudo tar Cxzvf /opt/cni/bin cni-plugins-linux-arm64-v1.7.1.tgz 

IPv4フォワーディング有効化と Swap の無効化

k8s クラスタを構築する上で、IPv4 フォワーディングの有効化と Swap の無効化は重要な要件となります。

IPv4 フォワーディングの有効化

コンテナランタイムの動作に必要な IPv4 フォワーディングの設定を有効化します。

$ sudo modprobe br_netfilter

$ cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
EOF

$ sudo sysctl --system

Swapの無効化

k8s の コンテナ管理を担う kubelet で、リソース管理のために Swap を無効化することを要件としているのでので Swap を無効化します。

$ sudo swapoff -a

$ sudo sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab

必要なツール,エージェントのインストール

kubeadm、kubelet、kubectl(マスターノードのみ) をそれぞれインストールします。

それぞれ説明すると、以下になります。

  • kubectl は k8s クラスタに対してオブジェクトや pod のデプロイやログ表示等のコマンドを実行できる CLI ツール
  • kubelet は k8s クラスタの各ノードで実行されるコンテナの状態管理を担うエージェント
  • kubeadm は k8s クラスタを構築したり、ノードを追加する為の CLI ツール

詳細なインストール手順については公式ドキュメントをご参照ください。

$ sudo apt-get update

$ sudo apt-get install -y apt-transport-https ca-certificates curl gpg

$ curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.33/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg

$ echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.33/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list

$ sudo apt-get update
$ sudo apt-get install -y kubelet kubeadm kubectl
$ sudo apt-mark hold kubelet kubeadm kubectl

CGROUPS_MEMORY の有効化

kubeadm init コマンドで k8s クラスタを作成しますが、エラーが発生します。

$ sudo kubeadm init \
  --apiserver-advertise-address=192.168.11.10 \
  --pod-network-cidr=10.244.0.0/16 \
  --service-cidr=10.96.0.0/12
[init] Using Kubernetes version: v1.33.0
[preflight] Running pre-flight checks
[preflight] The system verification failed. Printing the output from the verification:
KERNEL_VERSION: 6.14.0-1005-raspi
...
OS: Linux
CGROUPS_CPU: enabled
CGROUPS_CPUSET: enabled
CGROUPS_DEVICES: enabled
CGROUPS_FREEZER: enabled
CGROUPS_MEMORY: missing
CGROUPS_PIDS: enabled
CGROUPS_HUGETLB: enabled
CGROUPS_IO: enabled
error execution phase preflight: 
[preflight] Some fatal errors occurred:
[ERROR SystemVerification]: missing required cgroups: memory
[preflight] If you know what you are doing, you can make a check non-fatal with `--ignore-preflight-errors=...`
To see the stack trace of this error execute with --v=5 or higher

上述の通り、runc でメモリや CPU 等のリソースの制御に Linux カーネル機能の cgroup を参照するのですが、Ubuntu Server 25.04 のデフォルトで cgroup の サブシステムの memory がカーネル上で有効になっていない為、クラスタを構築する条件に合わずにエラーになっています。

$ cat /proc/cgroups 
#subsys_name    hierarchy       num_cgroups     enabled
cpu     0       296     1
cpuacct 0       296     1
blkio   0       296     1
devices 0       296     1
freezer 0       296     1
net_cls 0       296     1
perf_event      0       296     1
net_prio        0       296     1
hugetlb 0       296     1
pids    0       296     1
rdma    0       296     1
misc    0       296     1
dmem    0       296     1

なので、Raspberry Pi がブート中のカーネルパラメータを決定する際に参照する /boot/firmware/cmdline.txt の内容に cgroup_enable=memory cgroup_memory=1 を追加してオーバーライドする必要があります。

参考:
Configuring the /boot/firmware/cmdline.txt File on Raspberry Pi: https://fleetstack.io/blog/raspberry-pi-boot-firmware-cmdline-txt-file

$ cat /boot/firmware/cmdline.txt
console=serial0,115200 multipath=off dwc_otg.lpm_enable=0 console=tty1 root=LABEL=writable rootfstype=ext4 rootwait fixrtc cfg80211.ieee80211_regdom=GB cgroup_enable=memory cgroup_memory=1

$ sudo reboot

reboot(再起動) 後に再度 kubeadm init する事でクラスタが構築され、ワーカーノードがクラスタに加わる際のトークンが払い出されます。

$ sudo kubeadm init --apiserver-advertise-address=192.168.11.10 --pod-network-cidr=10.244.0.0/16   --service-cidr=10.96.0.0/12

[init] Using Kubernetes version: v1.33.0
...

Your Kubernetes control-plane has initialized successfully!

Alternatively, if you are the root user, you can run:

  export KUBECONFIG=/etc/kubernetes/admin.conf

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
  https://kubernetes.io/docs/concepts/cluster-administration/addons/

kubeadm join 192.168.11.10:6443 --token iayppt.******************* \
        --discovery-token-ca-cert-hash sha256:******************************************************

その後、ワーカーノードに相当する二個目の Raspberry Pi で上述の諸々のパッケージやブート設定を完了させた後に以下のコマンドを実行してクラスタに join します。

kubeadm join 192.168.11.10:6443 --token iayppt.******************* \
        --discovery-token-ca-cert-hash sha256:******************************************************

kubectl の get nodes でノードの一覧を表示します。p2 がクラスタに join できている事がわかると思います。

$ kubectl get nodes 
NAME   STATUS   ROLES           AGE    VERSION
p1     Ready    control-plane   4d9h   v1.33.0
p2     Ready    <none>          4d9h   v1.33.0

p2 に Role が付与されてないので、p2 が明示的にワーカーノードである事を示すために、Node オブジェクトの node-role.kubernetes.io/<role> ラベル を変更して worker ロールを付与します。

$ kubectl label nodes p2 node-role.kubernetes.io/worker=worker

$ kubectl get nodes 

NAME   STATUS   ROLES           AGE     VERSION
p1     Ready    control-plane   4d10h   v1.33.0
p2     Ready    worker          4d9h    v1.33.0

Flannel の 適用

Flannelは、オーバーレイネットワークを作成し、先ほど /opt/cni/bin にインストールした CNI プラグインの機能の一部を使って k8s クラスタ内のコンテナ間通信を可能にするサービスです。

kubectl apply で GitHub にホスティングされたマニフェストを基にデプロイします。

$ kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
namespace/kube-flannel created
clusterrole.rbac.authorization.k8s.io/flannel created
clusterrolebinding.rbac.authorization.k8s.io/flannel created
serviceaccount/flannel created
configmap/kube-flannel-cfg created
daemonset.apps/kube-flannel-ds created

$ kubectl get pods,svc -o wide -n kube-flannel
NAME                        READY   STATUS    RESTARTS       AGE     IP              NODE   NOMINATED NODE   READINESS GATES
pod/kube-flannel-ds-j5mnd   1/1     Running   2 (4h8m ago)   4d13h   192.168.11.11   p2     <none>           <none>
pod/kube-flannel-ds-qv9v7   1/1     Running   2 (4h8m ago)   4d13h   192.168.11.10   p1     <none>

(3) k8s の諸々のオブジェクト定義

構築した k8s クラスタに以下の namespace でオブジェクトを定義していきます。

  • metallb-system: MetalLB のコントローラーやスピーカーなどの Pod が配置されます。
  • ingress-nginx: Ingress リソースを管理し、外部からの HTTP トラフィックをクラスタ内の適切なサービスにルーティングするための NGINX Ingress Controller を配置します。
  • monitoring: Grafana を配置します。
  • openproject: OpenProject を配置します。
  • data: Grafana と OpenProject等、別のnamespaceから参照される PostgreSQL やMemcached などのデータストアを配置します。

MetalLB

MetalLBは、ベアメタル k8s クラスタ向けのロードバランサーです。

主要なオブジェクトを以下のマニフェストを利用してデプロイし、

$ kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.9/config/manifests/metallb-native.yaml

IPAddressPool と L2Advertisement は以下のように定義します。IPAddressPool でロードバランサーが使用可能な IP アドレスの範囲を定義し、L2Advertisement でその IP アドレスプールを L2 モードでアドバタイズする設定を行います。

apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: default
  namespace: metallb-system
spec:
  addresses:
  - 192.168.11.200-192.168.11.250
  autoAssign: true
---

apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: l2-advert
  namespace: metallb-system
spec:
  ipAddressPools:
  - default
  interfaces:
  - eth0

Ingress NGINX Controller

Ingress NGINX Controller は、k8s クラスタ内で Ingress リソースを処理するコントローラーです。主に HTTP / HTTPS トラフィックのルーティングを管理し、クラスタ外からのサービスへのアクセスを可能にします。

今回は以下のマニフェストを使用して主要なオブジェクトを以下のコマンドでデプロイし、

kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.6.4/deploy/static/provider/cloud/deploy.yaml

Ingress は以下のように定義します。この設定により、外部からの HTTP リクエストを/openproject/grafanaのパスに基づいて、それぞれのバックエンドサービスにルーティングします。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx-ingress
  namespace: ingress-nginx
spec:
  ingressClassName: nginx
  rules:
  - http:
      paths:
      - path: /openproject
        pathType: Prefix
        backend:
          service:
            name: openproject-ingress
            port:
              number: 8080

      - path: /grafana
        pathType: Prefix
        backend:
          service:
            name: grafana-ingress
            port:
              number: 3000

---
apiVersion: v1
kind: Service
metadata:
  name: nginx-ingress
  namespace: ingress-nginx
spec:
  type: ExternalName
  externalName: nginx.openproject.svc.cluster.local
---
apiVersion: v1
kind: Service
metadata:
  name: openproject-ingress
  namespace: ingress-nginx
spec:
  type: ExternalName
  externalName: openproject.openproject.svc.cluster.local
---
apiVersion: v1
kind: Service
metadata:
  name: grafana-ingress
  namespace: ingress-nginx
spec:
  type: ExternalName
  externalName: grafana.monitoring.svc.cluster.local

Grafana

Grafana はデフォルトでは SQLite をデータストアとして利用しますが、この構成ではdata namsespace に配置する PostgreSQL を利用します。

apiVersion: v1
kind: Namespace
metadata:
  name: monitoring
  labels:
    name: monitoring 
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: grafana
  namespace: monitoring
spec:
  replicas: 1
  selector:
    matchLabels:
      app: grafana
  template:
    metadata:
      labels:
        app: grafana
    spec:
      securityContext:
        fsGroup: 472
        supplementalGroups:
          - 0
      containers:
      - name: grafana
        image: grafana/grafana:latest
        ports:
        - containerPort: 3000
          name: http-grafana
          protocol: TCP
        resources:
          limits:
            memory: 1Gi
            cpu: 1000m
          requests:
            memory: 500Mi
            cpu: 500m

        env:
        - name: GF_SECURITY_ADMIN_PASSWORD
          value: "admin"
        - name: GF_USERS_ALLOW_SIGN_UP
          value: "false"
        - name: GF_DATABASE_TYPE
          value: "postgres"
        - name: GF_DATABASE_HOST
          value: "postgresql.data.svc.cluster.local"
        - name: GF_DATABASE_NAME
          value: "grafana"
        - name: GF_DATABASE_USER
          value: "grafana"
        - name: GF_DATABASE_PASSWORD
          value: "grafana"
        - name: GF_DATABASE_SSL_MODE
          value: "disable"
        - name: GF_DATABASE_INSTRUMENT_QUERIES
          value: "false"
        - name: GF_SERVER_ROOT_URL
          value: "%(protocol)s://%(domain)s/grafana"
        - name: GF_SERVER_SERVE_FROM_SUB_PATH
          value: "true"
---
apiVersion: v1
kind: Service
metadata:
  name: grafana
  namespace: monitoring
spec:
  type: ClusterIP
  ports:
  - port: 3000
    targetPort: 3000
  selector:
    app: grafana 

PostgreSQL + Memcached

data namespace に配置する PostgreSQL と Memcached は、他の namespace のアプリケーションから利用される共有データストアとして機能します。

PostgreSQL は OpenProject, Grafana で使い、
Memcached は OpenProject のキャッシュサーバーとして使います。

初期化スクリプトとして、ConfigMapに DDL を連番振って定義しています。

apiVersion: v1
kind: Namespace
metadata:
  name: data
---
apiVersion: v1
kind: Secret
metadata:
  name: postgresql-secret
  namespace: data
type: Opaque
data:
  POSTGRES_PASSWORD: cG9zdGdyYXBo
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: postgresql-pv
  namespace: data
spec:
  capacity:
    storage: 10Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Recycle
  claimRef:
    namespace: data
    name: postgresql-pvc
  hostPath:
    path: "/mnt/data/postgresql"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgresql-pvc
  namespace: data
spec:
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  resources:
    requests:
      storage: 10Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgresql
  namespace: data
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgresql
  template:
    metadata:
      labels:
        app: postgresql
    spec:
      containers:
      - name: postgresql
        image: postgres:13
        env:
        - name: POSTGRES_DB
          value: postgres
        - name: POSTGRES_USER
          value: postgres
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: postgresql-secret
              key: POSTGRES_PASSWORD
        ports:
        - containerPort: 5432
        volumeMounts:
        - name: postgresql-data
          mountPath: /var/lib/postgresql/data
        - name: init-scripts
          mountPath: /docker-entrypoint-initdb.d
      volumes:
      - name: postgresql-data
        persistentVolumeClaim:
          claimName: postgresql-pvc
      - name: init-scripts
        configMap:
          name: postgresql-init-scripts

---
apiVersion: v1
kind: Service
metadata:
  name: postgresql
  namespace: data
spec:
  type: ClusterIP
  ports:
  - port: 5432
  selector:
    app: postgresql

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: postgresql-init-scripts
  namespace: data
data:
  01_create_users.sql: |
    CREATE USER openproject WITH PASSWORD 'openproject';
    CREATE USER grafana WITH PASSWORD 'grafana';

  02_create_databases.sql: |
    CREATE DATABASE openproject;
    CREATE DATABASE grafana;
    
  03_grant_privileges.sql: |
    GRANT ALL PRIVILEGES ON DATABASE openproject TO openproject;
    GRANT ALL PRIVILEGES ON DATABASE grafana TO grafana;
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: memcached
  namespace: data
spec:
  replicas: 1
  selector:
    matchLabels:
      app: memcached
  template:
    metadata:
      labels:
        app: memcached
    spec:
      containers:
      - name: memcached
        image: memcached:1.6
        args: ["-m", "64"]
        ports:
        - containerPort: 11211

---
apiVersion: v1
kind: Service
metadata:
  name: memcached
  namespace: data
spec:
  type: ClusterIP
  ports:
  - port: 11211
  selector:
    app: memcached 

OpenProject

OpenProject の定義された環境変数を基に BasePath や DB 接続文字列を定義します。

apiVersion: v1
kind: Namespace
metadata:
  name: openproject
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: openproject-config
  namespace: openproject
data:
  OPENPROJECT_RAILS__RELATIVE__URL__ROOT: "/openproject"
  OPENPROJECT_HTTPS: "false"
  OPENPROJECT_DEFAULT__LANGUAGE: "ja"
  DATABASE_URL: "postgresql://openproject:openproject@postgresql.data.svc.cluster.local:5432/openproject"
  RAILS_CACHE_STORE: "memcache"
  OPENPROJECT_CACHE__MEMCACHE__SERVER: "memcached.data.svc.cluster.local:11211"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: openproject
  namespace: openproject
spec:
  replicas: 1
  selector:
    matchLabels:
      app: openproject
  template:
    metadata:
      labels:
        app: openproject
    spec:
      containers:
      - name: openproject
        image: openproject/openproject@sha256:17721da6b733d7ebedcbbdf1912e1a17a02a9c6891541fd6e21f57ee40a93ce8
        envFrom:
        - configMapRef:
            name: openproject-config
        ports:
        - containerPort: 8080

        resources:
          requests:
            memory: "2Gi"
            cpu: "1000m"
          limits:
            memory: "4Gi"
            cpu: "2000m"

---
apiVersion: v1
kind: Service
metadata:
  name: openproject
  namespace: openproject
spec:
  type: ClusterIP
  ports:
  - port: 8080
    targetPort: 8080
  selector:
    app: openproject

それぞれ マニフェストを apply 後に podの状態を確認します。

$ kubectl get pods,svc -o wide --all-namespaces

NAMESPACE        NAME                                        READY   STATUS      RESTARTS      IP              NODE   NOMINATED NODE   READINESS GATES
data             memcached-7f9f7c4c65-x5hsr                  1/1     Running     0             10.244.1.153    p2     <none>           <none>
data             postgresql-7496dc846d-xsgcl                 1/1     Running     0             10.244.1.158    p2     <none>           <none>
ingress-nginx    ingress-nginx-admission-create-tkzdt        0/1     Completed   0             10.244.1.147    p2     <none>           <none>
ingress-nginx    ingress-nginx-admission-patch-dwvjq         0/1     Completed   1             10.244.1.148    p2     <none>           <none>
ingress-nginx    ingress-nginx-controller-554ccfcbf5-jc2cr   1/1     Running     0             10.244.1.149    p2     <none>           <none>
kube-flannel     kube-flannel-ds-szz7m                       1/1     Running     3 (34h ago)   192.168.11.17   p1     <none>           <none>
kube-flannel     kube-flannel-ds-xhdlt                       1/1     Running     3 (34h ago)   192.168.11.18   p2     <none>           <none>
kube-system      coredns-674b8bbfcf-lp57d                    1/1     Running     3 (34h ago)   10.244.0.48     p1     <none>           <none>
kube-system      coredns-674b8bbfcf-qb7dv                    1/1     Running     3 (34h ago)   10.244.0.49     p1     <none>           <none>
kube-system      etcd-p1                                     1/1     Running     6 (34h ago)   192.168.11.17   p1     <none>           <none>
kube-system      kube-apiserver-p1                           1/1     Running     6 (34h ago)   192.168.11.17   p1     <none>           <none>
kube-system      kube-controller-manager-p1                  1/1     Running     6 (34h ago)   192.168.11.17   p1     <none>           <none>
kube-system      kube-proxy-d98bc                            1/1     Running     3 (34h ago)   192.168.11.17   p1     <none>           <none>
kube-system      kube-proxy-x8dwq                            1/1     Running     3 (34h ago)   192.168.11.18   p2     <none>           <none>
kube-system      kube-scheduler-p1                           1/1     Running     6 (34h ago)   192.168.11.17   p1     <none>           <none>
metallb-system   controller-bb5f47665-ncwmh                  1/1     Running     1 (29s ago)   10.244.1.165    p2     <none>           <none>
metallb-system   speaker-4fnq4                               1/1     Running     0             192.168.11.18   p2     <none>           <none>
metallb-system   speaker-qnksl                               1/1     Running     0             192.168.11.17   p1     <none>           <none>
monitoring       grafana-645b8cc7b6-wdf7p                    1/1     Running     0             10.244.1.164    p2     <none>           <none>
openproject      nginx-55d67f7b54-kgdhh                      1/1     Running     0             10.244.1.155    p2     <none>           <none>
openproject      openproject-84d589549-44hk4                 1/1     Running     0             10.244.1.160    p2     <none>           <none>

NAMESPACE        NAME                                 TYPE           CLUSTER-IP       EXTERNAL-IP                                 PORT(S)                      AGE
data             memcached                            ClusterIP      10.96.33.64      <none>                                      11211/TCP                    28h
data             postgresql                           ClusterIP      10.109.171.203   <none>                                      5432/TCP                     28h
default          kubernetes                           ClusterIP      10.96.0.1        <none>                                      443/TCP                      2d20h
ingress-nginx    grafana-ingress                      ExternalName   <none>           grafana.monitoring.svc.cluster.local        <none>                       27h
ingress-nginx    ingress-nginx-controller             LoadBalancer   10.102.34.187    192.168.11.200                              80:30671/TCP,443:32220/TCP   32h
ingress-nginx    ingress-nginx-controller-admission   ClusterIP      10.102.39.108    <none>                                      443/TCP                      32h
ingress-nginx    nginx-ingress                        ExternalName   <none>           nginx.openproject.svc.cluster.local         <none>                       27h
ingress-nginx    openproject-ingress                  ExternalName   <none>           openproject.openproject.svc.cluster.local   <none>                       27h
kube-system      kube-dns                             ClusterIP      10.96.0.10       <none>                                      53/UDP,53/TCP,9153/TCP       2d20h
metallb-system   metallb-webhook-service              ClusterIP      10.100.200.151   <none>                                      443/TCP                      77m
metallb-system   webhook-service                      ClusterIP      10.105.6.6       <none>                                      443/TCP                      2d2h
monitoring       grafana                              ClusterIP      10.106.204.229   <none>                                      3000/TCP                     27h
openproject      nginx                                ClusterIP      10.97.187.33     <none>                                      80/TCP                       28h
openproject      openproject                          ClusterIP      10.103.137.218   <none>                                      8080/TCP                     28h

適切にpodが配置されているのを確認したので、それぞれのサービスにアクセスできるか試します。

http://192.168.11.200/openproject

http://192.168.11.200/grafana

無事正しくルーティングされているようです。

今後

やる前からわかったけど低レイヤまでしっかり見ないとなので、うわ~~~ってなりますね。

わたくしは趣味がアクアリウムなのですが、水温、pH 値等の極端な変動を自動検知できる仕組みが欲しいなと思っております。なので次回、この k8s 上で水温センサの検査値を Grafana でビジュアライズしたり アラートを飛ばしたり等する試みを記事にしたいと思います。

ありがとうございました。

Discussion