k3sとprefectでワークフロー基盤を構築する

概要

PrefectはPythonでワークフローを記述可能なオーケストレーションツールです。本記事では、軽量Kubernetesであるk3s上にPrefectServerを構築し、冗長構成のワークフロー基盤を構築する方法を紹介します。

環境設計

  • コンピューティング: k3s Node (3台)
    • OS: Ubuntu Server 24.04 LTS
    • CPU: 2CPU
    • MEM: 8GB
  • ネットワーキング
    • k3s Node同士が同一のL2ネットワークで疎通可能な環境
    • k3s Nodeがそれぞれインターネットに疎通可能な環境
    • k3s NodeがそれぞれIPアドレスを1個ずつ持っていること
    • MetalLB用に代表IPアドレスを1個用意すること
      • 代表IPアドレスに対してドメインを割り当てること

構成図

Whoamiサービス(接続テスト用)

flowchart TB
    Client["Client<br/>Browser"]
    VIP["代表IP/32<br/>MetalLB VIP"]
    subgraph K3s["K3s Cluster(3 Nodes)"]
        subgraph MetalLB["ns:metallb-system"]
            Pool["IPAddressPool<br/>代表IP/32"]
        end
        subgraph EnvoyGWSystem["ns:envoy-gateway-system"]
            GC["GatewayClass<br/>envoy"]
        end
        subgraph Public["ns:public"]
            Secret["Secret<br/>whoami-tls<br/>tls.crt / tls.key"]
            GW["envoy-gateway<br/>spec.addresses:代表IP"]
            Route["HTTPRoute Host/Path Any<br/>parentRefs: envoy-gateway"]
            SVC["whoami Service"]
            subgraph Pods["DaemonSet"]
                Pod1["whoami Pod1 :80"]
                Pod2["whoami Pod2 :80"]
                Pod3["whoami Pod3 :80"]
            end
        end
    end
    Client -->|"HTTPS :443"| VIP
    Pool -.->|"IP Pool allocation"| EnvoyGWSystem
    Pool -.->|"IP Pool advertisement"| VIP
    VIP -->|"HTTPS :443"| GW
    GC -.->|"gatewayClassName: envoy"| GW
    Secret -.->|"certificateRefs"| GW
    GW -->|"HTTP :80"| Route
    Route -->|"backendRef: svc:80"| SVC
    SVC --> Pod1
    SVC --> Pod2
    SVC --> Pod3

Prefect基盤

flowchart TB
    Client["Client<br/>Browser"]
    VIP["代表IP/32<br/>MetalLB VIP"]
    DNS["DNS"]
    subgraph K3s["K3s Cluster(3 Nodes)"]
        subgraph MetalLB["ns:metallb-system"]
            Pool["IPAddressPool<br/>代表IP/32"]
        end
        subgraph EnvoyGWSystem["ns:envoy-gateway-system"]
            GC["GatewayClass<br/>envoy"]
        end
        subgraph Public["ns:public"]
            Secret["Secret<br/>whoami-tls<br/>tls.crt / tls.key"]
            GW["envoy-gateway<br/>spec.addresses:代表IP"]
            Route["HTTPRoute<br/>Host: PrefectUIホスト名<br/>Path: Any<br/>parentRefs: envoy-gateway"]
            SVC["Prefect Service"]
            subgraph Pods["ReplicaSet"]
                Pod1["Prefect Server Pod :80"]
                Pod2["Prefect Worker Pod :80"]
            end
        end
    end
    Client -->|"HTTPS :443"| VIP
    Pool -.->|"IP Pool allocation"| EnvoyGWSystem
    Pool -.->|"IP Pool advertisement"| VIP
    DNS -.->|"Resolve<br/>prefect.example.local<br/>代表IP"| VIP
    VIP -->|"HTTPS :443"| GW
    GC -.->|"gatewayClassName: envoy"| GW
    Secret -.->|"certificateRefs"| GW
    GW -->|"HTTP :80"| Route
    Route -->|"backendRef: svc:80"| SVC
    SVC --> Pod1
    SVC --> Pod2

環境構築

k3sクラスタの作成

まず、初めに3台のうち適当な1台のサーバでk3sクラスタを作成します。この1台で最初にクラスタを作り、他の2台をクラスタに参加させます。

Warning

- 全部rootで実行します。 - NW環境で10.0.0.0/8,172.16.0.0/12が使用済みであったのでクラスタの内部ネットワークはCIDRを192.168.0.0/16の範囲におさめます。(--cluster-cidr,--service-cidr) - k3sのLBに今回はmetallbを使うため、デフォルトのlb(servicelb)を無効化(--disable)します。 - k3sのingressに今回はenvoyを使い、gatewayを使用するため、デフォルトのingress(traefik)を無効化(--disable)します。 - 各サーバ筐体に複数のインターフェースがある為、念の為、外部I/Fをeth0に指定しいます。(--flannel-iface)

1代目の設定

#!/bin/bash
# encoding: utf-8
openssl rand -hex 32 > /tmp/k3s_token.txt
curl -sfL https://get.k3s.io | K3S_TOKEN=$(cat /tmp/k3s_token.txt) sh -s - server \
    --cluster-init \
    --cluster-cidr=192.168.0.0/17 \
    --service-cidr=192.168.128.0/17 \
    --disable servicelb \
    --disable traefik \
    --flannel-iface="L2ネットワークインターフェース名"
# /tmp/k3s_token.txt の内容はクラスタに参加する際に必要なので、他の2台のサーバにコピーしておきます。

2台目,3台目の設定

#!/bin/bash
# encoding: utf-8
curl -sfL https://get.k3s.io | K3S_TOKEN="1台目で設定したk3s_tokent.txtの内容" sh -s - server \
    --server https://"1台目のIP":6443 \
    --cluster-cidr=192.168.0.0/17 \
    --service-cidr=192.168.128.0/17 \
    --disable servicelb \
    --disable traefik \
    --flannel-iface="L2ネットワークインターフェース名"

Warning

- k3sのアンインストールをするには `/usr/local/bin/k3s-uninstall.sh` を実行します。ただし、ローカルにPersistent Volume (PV)を作っている場合はそれらの手動削除(rm)が必要です。


以後のコマンドは全て1台目のサーバで行います

Helmのインストール

HelmはKubernetesのパッケージマネージャです。 Helmを使うことでKubernetes上にアプリケーションを簡単にデプロイできます。 以下のコマンドでHelmをインストールします。

#!/bin/bash
# encoding: utf-8
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-4 | bash

MetalLBのインストール

MetalLBはL2ネットワークでKubernetesのLoadBalancerを実現するためのプラグインです。 ActiveなNodeがクラスタの代表IPアドレスを名乗る事により冗長化を実現します。

#!/bin/bash
# encoding: utf-8
helm repo add metallb https://metallb.github.io/metallb
helm repo update
helm pull --untar metallb/metallb
helm upgrade --install metallb ./metallb --namespace metallb-system --create-namespace

MetalLBの設定

MetalLBが代表するIPアドレスプールを設定します。 以下のYAMLファイルを作成し、kubectl apply -fで適用します。

# metallb-envoy-public.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: metallb-public-ip-pool
  namespace: metallb-system
spec:
  addresses:
    - "代表IPアドレス/32"
  serviceAllocation:
    priority: 10
    namespaces:
      - envoy-gateway-system
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: envoy-gateway-system-public-l2-advertisement
  namespace: metallb-system
spec:
  ipAddressPools:
    - metallb-public-ip-pool
#!/bin/bash
# encoding: utf-8
kubectl apply -f metallb-envoy-public.yaml

これで Namespace が envoy-gateway-system のものに対して MetalLBが代表IPアドレスプールを割り当てるようになります。

Warning

- MetalLBが割り当てるのはアドレスプールのみである事に注意してください。実際に割り当てるIPアドレスはServiceのspec.addressesで指定する必要があります。

Envoy Gatewayのインストール

Envoy GatewayはKubernetes上で動作するL7ロードバランサです。MetaLBのIPアドレスプールを制御してインストールします。 MetalLB側で指定したnamespaceをここで作成します。

#!/bin/bash
# encoding: utf-8
helm pull --untar oci://docker.io/envoyproxy/gateway-helm --version v0.0.0-latest
helm install envoygateway ./gateway-helm -n envoy-gateway-system --create-namespace --set deployment.replicas=2

Gatewayの作成

代表IPアドレスを指定してGatewayを作成します。 代表IPアドレスを利用可能なNamespaceを “public” とします。

# public-envoy-gateway.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: public
---
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: envoy
spec:
  controllerName: gateway.envoyproxy.io/gatewayclass-controller
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: envoy-gateway
  namespace: public
spec:
  gatewayClassName: envoy
  addresses:
    - type: IPAddress
      value: "代表IPアドレス"
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      allowedRoutes:
        namespaces:
          from: Same
#!/bin/bash
# encoding: utf-8
kubectl apply -f public-envoy-gateway.yaml

テスト用HTTPRouteの作成

代表IPアドレスのGatewayで受けた通信のHTTPRouteを作成します。 whoamiというテスト用サーバーにルーティングするように設定します。

# whoami-http-route.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: whoami-http-route
  namespace: public
spec:
  parentRefs:
    - name: envoy-gateway
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: whoami
          port: 80
#!/bin/bash
# encoding: utf-8
kubectl apply -f whoami-http-route.yaml

テスト用サーバー(DaemonSet)のデプロイ

テスト用サーバーをデプロイします。 whoamiサービスはDaemonSetでデプロイすることで クラスタ内の全てのNodeにwhoami Podが配置され 代表IPアドレスにアクセスした際にどのNodeのPodに ルーティングされるかを確認することができます。

# whoami-daemonset.yaml
apiVersion: v1
kind: Service
metadata:
  name: whoami
  namespace: public
spec:
  type: ClusterIP
  selector:
    app: whoami
  ports:
    - name: http
      port: 80
      targetPort: 80
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: whoami
  namespace: public
spec:
  selector:
    matchLabels:
      app: whoami
  template:
    metadata:
      labels:
        app: whoami
    spec:
      containers:
        - name: whoami
          image: traefik/whoami
          ports:
            - containerPort: 80
#!/bin/bash
# encoding: utf-8
kubectl apply -f whoami-daemonset.yaml

確認

#!/bin/bash
# encoding: utf-8
curl "http://<代表IPアドレス>/"
# 3回ほど実行してwhoamiのPodが3台のNodeに分散していることを確認。

補足: TLS終端(自己署名証明書)

ワイルドカード証明書設定

# openssl.cnf
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = req_distinguished_name
x509_extensions = v3_ca

[req_distinguished_name]
C = JP
ST = Tokyo
L = Shinjuku
O = MyOrg
OU = MyOrgUnit
CN = *.example.local

[v3_req]
basicConstraints = critical, CA:false
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectKeyIdentifier = hash
subjectAltName = @alt_names

[alt_names]
DNS.1 = *.example.local
DNS.2 = example.local
IP.1 = 代表IPアドレス

証明書の作成とSecretの作成

#!/bin/bash
# encoding: utf-8
openssl genrsa -out tls.key 2048
openssl req \
  -x509 \
  -new \
  -key tls.key \
  -sha256 \
  -days 3650 \
  -out tls.crt \
  -config openssl.cnf \
  -extensions v3_req
kubectl create secret tls public-tls \
  -n public \
  --cert=tls.crt \
  --key=tls.key

TLS終端Gatewayの作成

# public-envoy-gateway.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: public
---
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: envoy
spec:
  controllerName: gateway.envoyproxy.io/gatewayclass-controller
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: envoy-gateway
  namespace: public
spec:
  gatewayClassName: envoy
  addresses:
    - type: IPAddress
      value: "代表IPアドレス"
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      allowedRoutes:
        namespaces:
          from: Same
    - name: https
      protocol: HTTPS
      port: 443
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: public-tls

CloudNative-PG(PostgreSQL)のインストール

CloudNative-PGはKubernetes上で動作するPostgreSQLのOperatorです。冗長構成のPostgreSQLクラスタを簡単に構築できます。

helm repo add cnpg https://cloudnative-pg.github.io/charts
helm repo update
helm pull --untar cnpg/cloudnative-pg
helm upgrade --install cnpg ./cloudnative-pg --namespace cnpg-system  --create-namespace 

CloudNative-PGクラスタの設定

今回はPrefect用にそれぞれ64GiBのPersistent Volumeを3台のNodeに配置するように設定します。

# cloudnative-pg.yaml
--- # Create Namespace cnpg
kind: Namespace
apiVersion: v1
metadata:
    name: cnpg-system
--- # Create Secret for Initial Database
apiVersion: v1
data:
  username: ZGVmYXVsdA== # default
  password: ZGVmYXVsdA== # default
kind: Secret
metadata:
  name: default-postgres-secret
  namespace: cnpg-system
type: kubernetes.io/basic-auth
--- # Create Secret for prefect
apiVersion: v1
data:
  username: cHJlZmVjdA== # prefect
  password: cHJlZmVjdA== # prefect
kind: Secret
metadata:
  name: prefect-postgres-secret
  namespace: cnpg-system
type: kubernetes.io/basic-auth
--- # create Cluster and Initial Database and Additional Roles
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: cnpgcluster
  namespace: cnpg-system
spec:
  instances: 3
  storage:
    size: 64Gi
  bootstrap:
    initdb:
      database: default
      owner: default
      secret:
        name: default-postgres-secret
  managed:
    roles:
    - name: prefect
      ensure: present
      comment: PrefectServer
      login: true
      passwordSecret:
        name: prefect-postgres-secret
--- # Create prefect Database
apiVersion: postgresql.cnpg.io/v1
kind: Database
metadata:
  name: prefect-db
  namespace: cnpg-system
spec:
  name: prefect
  owner: prefect
  cluster:
    name: cnpgcluster

Prefect Serverのインストール