📦

Kind × Scaffold で手軽に継続的な開発を体験したい

2024/07/22に公開

2024 年 6 月に行われた OpenTelemetry Meetup 2024-06 に参加していて、Istio と OpenTelemetry が連携できることを知りました。試したいなーと思っていたのですが、Kubernetes 環境作るの面倒いなぁと思っていたこちらの記事に出会いました。

https://syu-m-5151.hatenablog.com/entry/2024/06/21/135855

Kubernetes in Docker という技術を知り、ローカルで Kubernetes 環境が作れるならすぐにでも試したい!となり触ってみたところ Skaffold と合わせて開発しやすい環境に感動しました。 Skaffold の良さはこちらの記事にまとまっていました。

https://zenn.dev/kojake_300/articles/11945f2047b22b

二番煎じにならぬように、最近検証していた OpenTelemetry や Istio の要素を交えたリポジトリを用意したので、手軽に試せるようにまとめていきたいと思います!

(スリーシェイクのエンジニアのみなさんの記事は本当にいつも参考にさせてもらっています。)

この記事のターゲット

  • ローカル環境で手軽に Kubernetes 環境を構築したい方
  • Skaffold と合わせて継続的な開発を体験したい方
  • OpenTelemetry や Istio を試したい方

最終像

環境準備

今回は Google Cloud の Compute Engine の Ubuntu(22.04 LTS) 上で行っています、各種パッケージは Homebrew でインストールを行っています。また、こちらのリポジトリを扱っていきます。

https://github.com/hayashit6239/kind-istio-otel-sample

Terminal
git clone https://github.com/hayashit6239/kind-istio-otel-sample.git

Ubuntu に Linuxbrew をインストール

Linuxbrew のインストール手順はこちらです。真っさらな Compute Engine を想定しているので細かめに記載しています。

Terminal
terminal
# brew をインストール
sudo apt-get update
sudo apt-get install -y build-essential procps curl file git
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
user=`whoami`
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >> /home/${user}/.profile
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
sudo apt-get install build-essential
brew install gcc

# docker がなければインストール
sudo apt-get install -y docker.io
sudo systemctl start docker

# sudo なしで実行可能にする
sudo gpasswd -a ${user} docker

# 一度 exit して、再度 SSH で入り直す
Terminal
# 各種バージョンを確認
brew --version

Homebrew 4.3.9
##
# docker も sudo なしで実行できれば OK
docker --version

Docker version 27.0.3, build 7d4bcd863a

各種パッケージを Linuxbrew でインストール

Terminal
brew install kind skaffold kubectl istioctl

# 各種バージョンを確認
kind version

kind v0.23.0 go1.22.3 linux/amd64
##
skaffold version

v2.13.0

Kind で環境構築

今回はこちらの構成でクラスターを構築したいので、下記の Kind の Config ファイルを用意しています。アプリケーションに合わせて、ポートマッピングとボリュームマウントを利用します。(ポートマッピングやボリュームマウントはサンプルアプリのデプロイに必要なためです。)

kind/cluster-config.yaml
kind/cluster-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  extraPortMappings:
  - containerPort: 30000
    hostPort: 8080
  - containerPort: 30001
    hostPort: 8081
  - containerPort: 30002
    hostPort: 8082
  - containerPort: 30003
    hostPort: 4317
  - containerPort: 30004
    hostPort: 5080
- role: worker
  extraMounts:
  - containerPath: /mnt
    hostPath: ./
- role: worker
  extraMounts:
  - containerPath: /mnt
    hostPath: ./

下記のコマンドで環境を構築します。

Terminal
cd kind-istio-otel-sample
kind create cluster --config kind/cluster-config.yaml --name kind-cluster

構築後に自動で kubeconfig に反映されるで、kubectl で確認することができます。

Terminal
kubectl get node

NAME                         STATUS   ROLES           AGE   VERSION
kind-cluster-control-plane   Ready    control-plane   2d    v1.30.0
kind-cluster-worker          Ready    <none>          2d    v1.30.0
kind-cluster-worker2         Ready    <none>          2d    v1.30.0

kubeconfig に反映されていない場合は、下記で取得したものを ~/.kube/config に転記します。

Terminal
kind get kubeconfig --name kind-cluster

extraPortMappings

"追加のポートマッピングを使用して、Kind ノードにポートフォワーディングすることができます。これは、Kind クラスタにトラフィックを転送するためのクロスプラットフォームオプションです。"
公式ドキュメントより抜粋

各種アプリケーションは 8000, 8001, 8002, 4317, 5080 で NodePort で公開する想定です。それぞれを Node Container の 30000, 30001, 30002, 30003, 3004 にフォワーディングしています。containerPort は後ほどアプリケーションのマニフェストでポイントとなってきます。

Terminal
docker ps

CONTAINER ID   IMAGE                  COMMAND                  CREATED      STATUS      PORTS                                                                                                                           NAMES
ba823ebfa85d   kindest/node:v1.30.0   "/usr/local/bin/entr…"   2 days ago   Up 2 days   127.0.0.1:40093->6443/tcp, 0.0.0.0:8080->30000/tcp, 0.0.0.0:8081->30001/tcp, 0.0.0.0:8082->30002/tcp, 0.0.0.0:4317->30003/tcp, 0.0.0.0:5080->30004/tcp   kind-cluster-control-plane
bfa3ed6934b2   kindest/node:v1.30.0   "/usr/local/bin/entr…"   2 days ago   Up 2 days                                                                                                                                   kind-cluster-worker2
e723da0b24d2   kindest/node:v1.30.0   "/usr/local/bin/entr…"   2 days ago   Up 2 days                                                                                                                                   kind-cluster-worker

extraMounts

"追加のマウントを使用して、ホスト上のストレージを Kind ノードにパススルーし、データを永続化したり、コードを通してマウントしたりできます。"
公式ドキュメントより抜粋

ホストのリポジトリを Node Container の /mnt にマウントするための設定です。さらにアプリケーションから /mnt の各種ディレクトリをマウントすることでホストのリポジトリを Kubernetes 上のアプリケーションから参照させています。

Skaffold でアプリのデプロイ

Kubernetes 環境ができたので、サンプルアプリケーションをデプロイします。マニフェストファイルはこちらです。Serviceaccount, Deployment, Service を各アプリケーションごとにデプロイします。

manifests/service-backend-for-frontend.yaml
manifests/service-backend-for-frontend.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: bff-serviceaccouunt
  namespace: default
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-for-frontend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: backend-for-frontend
  template:
    metadata:
      labels:
        app: backend-for-frontend
    spec:
      containers:
      - name: backend-for-frontend
        image: backend-for-frontend
        ports:
        - containerPort: 8080
        volumeMounts:
        - mountPath: /app
          name: src-volume
      volumes:
      - name: src-volume
        hostPath:
          path: /mnt/service-backend-for-frontend
          type: Directory
      serviceAccountName: bff-serviceaccouunt
---
apiVersion: v1
kind: Service
metadata:
  name: service-backend-for-frontend
spec:
  type: NodePort
  ports:
  - port: 8080
    targetPort: 8080
    nodePort: 30000
  selector:
    app: backend-for-frontend

いつも通りなら kubectl でデプロイするところを冒頭の記事に従って、下記のパイプラインを定義したファイルを用意してあるので scaffold dev コマンドを実行します。

skaffold.yaml
skaffold.yaml
apiVersion: skaffold/v2beta26
kind: Config

build:
  artifacts:
  - image: backend-a
    context: .
    docker:
      dockerfile: containers/service-backend-a/Dockerfile
  - image: backend-b
    context: .
    docker:
      dockerfile: containers/service-backend-b/Dockerfile
  - image: backend-for-frontend
    context: .
    docker:
      dockerfile: containers/service-backend-for-frontend/Dockerfile
  # - image: otel-collector
  #   context: .
  #   docker:
  #     dockerfile: containers/service-otel-collector/Dockerfile
  # - image: openobserve
  #   context: .
  #   docker:
  #     dockerfile: containers/service-openobserve/Dockerfile


deploy:
  kubectl:
    manifests:
    - manifests/service-backend-for-frontend.yaml
    - manifests/service-backend-a.yaml
    - manifests/service-backend-b.yaml
    # - manifests/service-otel-collector.yaml
    # - manifests/service-openobserve.yaml

シンプルにビルドとデプロイを定義しています。イメージはアプリケーションそれぞれに Dockerfile を用意しているのでそこを参照してビルドします。デプロイは、上述のマニフェストを参照しています。

Terminal
skaffold dev --port-forward --no-prune --cache-artifacts=false

3 つの FastAPI アプリケーションごとに DeploymentService が作成されます。このコマンドではアプリケーションごとに下図のようなログが出力されます。 uvicorn running on http://0.0.0.0:8080 あたりでアプリケーションが実行されているログだということがわかるかと思います。

※ Skaffold のログの中でも Loading images into kind cluster nodes.. というビルドしたイメージを Kind 上にデプロイするフェーズで時間がかかるかもしれません。

Terminal
kubectl get pod

kubectl get po -A
NAMESPACE            NAME                                                 READY   STATUS    RESTARTS   AGE
default              backend-a-74f9b95768-5mppk                           1/1     Running   0          2m57s
default              backend-b-78ff79765-vwdqq                            1/1     Running   0          2m56s
default              backend-for-frontend-77df6df69d-kk482                1/1     Running   0          2m57s

ここまででマイクロサービスっぽく構成したサンプルアプリを Kubernetes 上にデプロイすることができました!下記で API を叩くと ture のみの結果が返ってくる & ログも出力されます。

Terminal
curl localhost:8080/micro

叩ける API は localhost:8080/docs で確認することができます。

port-forward

"Skaffold は、dev、debug、deploy、または run モードで実行しているときに、クラスタ上の公開された Kubernetes リソースからローカルマシンにポートを転送するための組み込みのサポートを提供します。"
公式ドキュメントより抜粋

こちらもいつもなら kubectl port-foward を実行するところですが、--portforward をつけて実行することで同様の機能を利用できます。

no-prune & cache-artifacts

"Skaffold でビルドされ、ローカル Docker デーモンに保存されたイメージは簡単に積み重なり、大量のディスクスペースを消費する可能性があります。これを回避するために、ユーザーはイメージのプルーニングを有効にすることができます。これは、skaffold dev および skaffold debug から SIGTERM を受け取ると、Skaffold によってビルドされたイメージを削除します。"
公式ドキュメントより抜粋

skaffold dev コマンドを実行していると、Dockerfile やアプリケーションコードの変更を検知して自動で設定したパイプラインを実行してくれます。その際にイメージが高い頻度で作成されてしまうのを、こちらのオプションでコマンド終了時に削除できます。

ここまでだと冒頭で紹介した記事の FastAPI バージョンとなってしまうので、次は OpenTelemetry を絡めたいと思います。その中でホットリロードを良さ体験していきます

オブザーバビリティの検証

先ほどの curl で飛ばしたリクエストがどのようにサービスを介しているかを見れるようにしたいと思います。OpenTelemetry による計装はすでにコードに含まれているのでいくつか手順を踏んで、トレースを可視化できるようにします。

ここでは OpenTelemetry という技術を用いてテレメトリーを送信できるようにします。その中でアプリケーションからオブザーバビリティバックエンドにテレメトリを中継する Otel Collector を利用します。

Otel Collector のデプロイ

まずは、リポジトリに含まれている Otel Collector の設定ファイルと Kubernetes のマニフェストファイルの説明になります。オブザーバビリティバックエンドツールは Google Cloud 環境がある方向けの Cloud Trace と環境がない方向けの OpenObserve の設定を用意しています。

  • Google Cloud の環境がある方は下記の otel-collector.yamlproject_id を利用している環境の Project ID に書き換えた上で、実行しているホストに Cloud Trace 関連の権限を付与してください。

  • Google Cloud の環境がない方は下記の otel-collector.yamlpipelines の中の exportersgooglecloud を含む行をコメントアウトして、otlphttp を含む行のコメントアウトをはずしてください。

containers/service-otel-collector/otel-collector.yaml
otel-collector.yaml
receivers:
  otlp:
    protocols:
      grpc:

processors:
  batch: {}
  resourcedetection:
    detectors: [env, gcp]
    timeout: 40s
    override: false

exporters:
  googlecloud:
    project: ${project_id}
  otlphttp:
    endpoint: "http://service-openobserve.default.svc.cluster.local:5080/api/default"
    # Basic 認証用の情報。こちらを参照: https://openobserve.ai/docs/ingestion/traces/
    headers:
      Authorization: Basic cm9vdEBleGFtcGxlLmNvbTpDb21wbGV4cGFzcyMxMjMK

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch, resourcedetection]
      exporters: [googlecloud]
      # exporters: [otlphttp]
containers/service-otel-collector/Dockerfile
containers/service-otel-collector/Dockerfile
FROM otel/opentelemetry-collector-contrib:0.90.1

COPY ./containers/service-otel-collector/otel-collector.yml /etc/otel-collector.yml

CMD ["--config=/etc/otel-collector.yml"]
manifests/service-otel-collector.yaml
manifests/service-otel-collector.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: otel-collector
spec:
  replicas: 1
  selector:
    matchLabels:
      app: otel-collector
  template:
    metadata:
      labels:
        app: otel-collector
    spec:
      containers:
      - name: otel-collector
        image: otel-collector
        ports:
        - containerPort: 4317
---
apiVersion: v1
kind: Service
metadata:
  name: service-otel-collector
spec:
  type: NodePort
  ports:
  - port: 4317
    targetPort: 4317
    nodePort: 30003
  selector:
    app: otel-collector

ここでホットリロードの良さを体感するのですが、先ほどの skaffold.yaml を下記のように Otel Collector 関連の行のコメントアウトを外します。

skaffold.yaml
skaffold.yaml
apiVersion: skaffold/v2beta26
kind: Config

build:
  artifacts:
  - image: backend-a
    context: .
    docker:
      dockerfile: containers/service-backend-a/Dockerfile
  - image: backend-b
    context: .
    docker:
      dockerfile: containers/service-backend-b/Dockerfile
  - image: backend-for-frontend
    context: .
    docker:
      dockerfile: containers/service-backend-for-frontend/Dockerfile
  - image: otel-collector
    context: .
    docker:
      dockerfile: containers/service-otel-collector/Dockerfile
  # - image: openobserve
  #   context: .
  #   docker:
  #     dockerfile: containers/service-openobserve/Dockerfile

deploy:
  kubectl:
    manifests:
    - manifests/service-backend-for-frontend.yaml
    - manifests/service-backend-a.yaml
    - manifests/service-backend-b.yaml
    - manifests/service-otel-collector.yaml
    # - manifests/service-openobserve.yaml

コメントアウトを外して保存すると Skaffold のログが動きます。Otel Collector がビルド & デプロイされます。

OpenObserve のデプロイ

ここでも、リポジトリに含まれている各種ファイルの説明になりますが、こちら特に難しいことがないので割愛します。Google Cloud 環境がある方はこちらのデプロイ項目はスキップしてください。

containers/service-openobserve/Dockerfile
containers/service-openobserve/Dockerfile
FROM public.ecr.aws/zinclabs/openobserve:latest
manifests/service-openobserve.yaml
manifests/service-openobserve.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: openobserve
spec:
  replicas: 1
  selector:
    matchLabels:
      app: openobserve
  template:
    metadata:
      labels:
        app: openobserve
    spec:
      containers:
      - name: openobserve
        image: openobserve
        env:
        - name: ZO_DATA_DIR
          value: "/data"
        - name: ZO_ROOT_USER_EMAIL
          value: "root@example.com"
        - name: ZO_ROOT_USER_PASSWORD
          value: "Complexpass#123"
        ports:
        - containerPort: 5080
---
apiVersion: v1
kind: Service
metadata:
  name: service-openobserve
spec:
  type: NodePort
  ports:
  - port: 5080
    targetPort: 5080
    nodePort: 30004
  selector:
    app: openobserve

Dockerfile と Kubernetes マニフェストファイルを確認できたら、先ほどと同様に scaffold.yaml のコメントアウトを外します。

skaffold.yaml
skaffold.yaml
apiVersion: skaffold/v2beta26
kind: Config

build:
  artifacts:
  - image: backend-a
    context: .
    docker:
      dockerfile: containers/service-backend-a/Dockerfile
  - image: backend-b
    context: .
    docker:
      dockerfile: containers/service-backend-b/Dockerfile
  - image: backend-for-frontend
    context: .
    docker:
      dockerfile: containers/service-backend-for-frontend/Dockerfile
  - image: otel-collector
    context: .
    docker:
      dockerfile: containers/service-otel-collector/Dockerfile
  - image: openobserve
    context: .
    docker:
      dockerfile: containers/service-openobserve/Dockerfile

deploy:
  kubectl:
    manifests:
    - manifests/service-backend-for-frontend.yaml
    - manifests/service-backend-a.yaml
    - manifests/service-backend-b.yaml
    - manifests/service-otel-collector.yaml
    - manifests/service-openobserve.yaml

コメントアウトを外して保存すると、またパイプラインが動き出し自動でビルドとデプロイが行われます。

トレースの確認

最後に各アプリケーションに Otel Collector のエンドポイントを環境変数で渡します。service-backend-for-frontend.yaml を編集して、OTEL_EXPORTER_OTLP_ENDPOINT の値を Otel Collector のエンドポイントにします。(例として backend-for-frontend について示しています、backend-a, backend-b にも編集します。)

manifests/service-backend-for-frontend.yaml
manifests/service-backend-for-frontend.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: bff-serviceaccouunt
  namespace: default
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-for-frontend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: backend-for-frontend
  template:
    metadata:
      labels:
        app: backend-for-frontend
    spec:
      containers:
      - name: backend-for-frontend
        image: backend-for-frontend
        env:
        - name: OTEL_EXPORTER_OTLP_ENDPOINT
        # OpenTelemetry を検証する際に value に値を入れる
          value: "http://service-otel-collector.default.svc.cluster.local:4317"
          # value: ""
        ports:
        - containerPort: 8080
        volumeMounts:
        - mountPath: /app
          name: src-volume
      volumes:
      - name: src-volume
        hostPath:
          path: /mnt/service-backend-for-frontend
          type: Directory
      serviceAccountName: bff-serviceaccouunt
---
apiVersion: v1
kind: Service
metadata:
  name: service-backend-for-frontend
spec:
  type: NodePort
  ports:
  - port: 8080
    targetPort: 8080
    nodePort: 30000
  selector:
    app: backend-for-frontend

変更が自動で検知されて、正常にデプロイができていると下記のコマンドにより Cloud Trace もしくは OpneObserve でトレースを確認することができます

Terminal
curl localhost:8080/micro

先ほどの構成図の通り、リクエストが service-backend-for-frontend → [service-backend-a, service-backend-b] というふうに流れていることをそれぞれのオブザーバビリティバックエンドツールで確認できます。

  • Cloud Trace

  • OpenObserve
    • ログイン画面で必要なメールアドレスとパスワードは service-openobserve.yaml で環境変数として渡している値になります。

OpenTelemetry for Python での計装に興味がある方は、こちらにまとめているのでご一読いただけると幸いです。

https://zenn.dev/t_hayashi/articles/bf11a89d34fcc0

サービスメッシュの検証

最後に Istio をデプロイして、マイクロサービスチックなサンプルアプリのトラフィックを Kiali で可視化したいと思います。

Istio のデプロイ

Istioctl で Istio 関連のコンポーネントを Ambient モードでデプロイしていきます。Ambient モードについて知りたい方は、こちらの記事で軽く触れているのでご一読いただければ幸いです。

https://zenn.dev/t_hayashi/articles/ad115c602203da

公式ドキュメントの通りに下記のコマンドを実行します。Istio の Ambient モードには、istio-cni, istiod, ztunnel という 3 つのコンポーネントが必要なようです。

Terminal
istioctl install --set profile=ambient --skip-confirmation

下記のコマンドでデプロイされたコンポーネントを確認します。

Terminal
kubectl get pod -n istio-system

NAMESPACE      NAME                  READY    STATUS   RESTARTS   AGE
istio-system  istio-cni-node-4z26d    1/1     Running   0          24s
istio-system  istio-cni-node-hb7gn    1/1     Running   0          24s
istio-system  istio-cni-node-k4nc9    1/1     Running   0          24s
istio-system  istiod-67d49c6d97-jv42x 1/1     Running   0          27s
istio-system  ztunnel-7ggtr           1/1     Running   0          20s
istio-system  ztunnel-cxmww           1/1     Running   0          20s
istio-system  ztunnel-dzd6w           1/1     Running   0          20s

想定通りにデプロイできていました。 Ambient Mesh を構成するためにサンプルアプリがデプロイされている Namespace にラベルを付与します。今回は default を利用しているので、対象の Namespace に default を指定します。

Terminal
kubectl label namespace default istio.io/dataplane-mode=ambient
kubectl get namespace default -o yaml

apiVersion: v1
kind: Namespace
metadata:
  creationTimestamp: "2024-07-14T16:09:07Z"
  labels:
    istio.io/dataplane-mode: ambient
    kubernetes.io/metadata.name: default
  name: default
  resourceVersion: "80698"
  uid: b1e8f546-b025-4956-80fb-bd01b682467d
spec:
  finalizers:
  - kubernetes
status:
  phase: Active

Kiali のデプロイ

こちらも公式通りにコマンドを実行します。可視化にあたって、Prometheus と Kiali というコンポーネントをデプロイする必要があるようです。

Terminal
kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.22/samples/addons/prometheus.yaml
kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.22/samples/addons/kiali.yaml
kubectl get pod -n istio-system

NAME                          READY   STATUS    RESTARTS   AGE
istio-cni-node-4z26d          1/1     Running   0          22m
...
istiod-67d49c6d97-jv42x       1/1     Running   0          22m
kiali-5446b88647-79tkh        1/1     Running   0          2d5h
prometheus-777db476b6-l24q9   2/2     Running   0          2d5h
...
ztunnel-dzd6w                 1/1     Running   0          22m

こちらも想定通りにデプロイできていそうです。

トラフィックの可視化

下記のコマンドで Kiali Dashboard にアクセスできる状態にしてみます。

Terminal
istioctl dashboard kiali

http://localhost:20001/kiali
Failed to open browser; open http://localhost:20001/kiali in your browser.

こちらのコマンドによって localhost:20001/kiali でアクセスできるようになるので、実際にアクセスするとこのような画面になります。

実際にトラフィックが流れているところを見たいので、適当に複数回 curl で叩いてみます。

Terminal
for i in $(seq 1 20); do curl -s http://localhost:8080/micro; done

いい感じにアニメーションでトラフィックを可視化することができました!!

さいごに

今回は Kind と Scaffold による開発を体験すべく、FastAPI のサンプルアプリケーションのデプロイから OpenTelemetry によるトレースの可視化、Istio と Kiali によるトラフィックの可視化を行いました。

Otel Collector や OpenObserve に関しては Dockerfilemanifests/hoge.yaml を用意して skaffold.yaml を変更するだけで、変更検知して自動でビルド & デプロイが完了するというところで docker コマンドや kubectl コマンドを意識しない開発を体験できました。ビルドとデプロイに関して、考えることが少なくなりコードに集中できる感じが非常に嬉しいポイントでした。

本来やりたかった OpenTelmetry と Istio の連携は別の記事としたいと思います。

参考

Discussion