Kind × Scaffold で手軽に継続的な開発を体験したい
2024 年 6 月に行われた OpenTelemetry Meetup 2024-06 に参加していて、Istio と OpenTelemetry が連携できることを知りました。試したいなーと思っていたのですが、Kubernetes 環境作るの面倒いなぁと思っていたこちらの記事に出会いました。
Kubernetes in Docker という技術を知り、ローカルで Kubernetes 環境が作れるならすぐにでも試したい!となり触ってみたところ Skaffold と合わせて開発しやすい環境に感動しました。 Skaffold の良さはこちらの記事にまとまっていました。
二番煎じにならぬように、最近検証していた OpenTelemetry や Istio の要素を交えたリポジトリを用意したので、手軽に試せるようにまとめていきたいと思います!
(スリーシェイクのエンジニアのみなさんの記事は本当にいつも参考にさせてもらっています。)
この記事のターゲット
- ローカル環境で手軽に Kubernetes 環境を構築したい方
- Skaffold と合わせて継続的な開発を体験したい方
- OpenTelemetry や Istio を試したい方
最終像
環境準備
今回は Google Cloud の Compute Engine の Ubuntu(22.04 LTS) 上で行っています、各種パッケージは Homebrew でインストールを行っています。また、こちらのリポジトリを扱っていきます。
git clone https://github.com/hayashit6239/kind-istio-otel-sample.git
Ubuntu に Linuxbrew をインストール
Linuxbrew のインストール手順はこちらです。真っさらな Compute Engine を想定しているので細かめに記載しています。
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 で入り直す
# 各種バージョンを確認
brew --version
Homebrew 4.3.9
##
# docker も sudo なしで実行できれば OK
docker --version
Docker version 27.0.3, build 7d4bcd863a
各種パッケージを Linuxbrew でインストール
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
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: ./
下記のコマンドで環境を構築します。
cd kind-istio-otel-sample
kind create cluster --config kind/cluster-config.yaml --name kind-cluster
構築後に自動で kubeconfig に反映されるで、kubectl
で確認することができます。
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
に転記します。
kind get kubeconfig --name kind-cluster
extraPortMappings
"追加のポートマッピングを使用して、Kind ノードにポートフォワーディングすることができます。これは、Kind クラスタにトラフィックを転送するためのクロスプラットフォームオプションです。"
公式ドキュメントより抜粋
各種アプリケーションは 8000, 8001, 8002, 4317, 5080 で NodePort で公開する想定です。それぞれを Node Container の 30000, 30001, 30002, 30003, 3004 にフォワーディングしています。containerPort
は後ほどアプリケーションのマニフェストでポイントとなってきます。
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
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
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 を用意しているのでそこを参照してビルドします。デプロイは、上述のマニフェストを参照しています。
skaffold dev --port-forward --no-prune --cache-artifacts=false
3 つの FastAPI
アプリケーションごとに Deployment
と Service
が作成されます。このコマンドではアプリケーションごとに下図のようなログが出力されます。 uvicorn running on http://0.0.0.0:8080
あたりでアプリケーションが実行されているログだということがわかるかと思います。
※ Skaffold のログの中でも Loading images into kind cluster nodes..
というビルドしたイメージを Kind 上にデプロイするフェーズで時間がかかるかもしれません。
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 のみの結果が返ってくる & ログも出力されます。
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.yaml
のproject_id
を利用している環境の Project ID に書き換えた上で、実行しているホストに Cloud Trace 関連の権限を付与してください。 -
Google Cloud の環境がない方は下記の
otel-collector.yaml
のpipelines
の中のexporters
のgooglecloud
を含む行をコメントアウトして、otlphttp
を含む行のコメントアウトをはずしてください。
containers/service-otel-collector/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
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
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
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
FROM public.ecr.aws/zinclabs/openobserve:latest
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
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
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 でトレースを確認することができます。
curl localhost:8080/micro
先ほどの構成図の通り、リクエストが service-backend-for-frontend → [service-backend-a, service-backend-b] というふうに流れていることをそれぞれのオブザーバビリティバックエンドツールで確認できます。
- Cloud Trace
- OpenObserve
- ログイン画面で必要なメールアドレスとパスワードは
service-openobserve.yaml
で環境変数として渡している値になります。
- ログイン画面で必要なメールアドレスとパスワードは
OpenTelemetry for Python での計装に興味がある方は、こちらにまとめているのでご一読いただけると幸いです。
サービスメッシュの検証
最後に Istio をデプロイして、マイクロサービスチックなサンプルアプリのトラフィックを Kiali で可視化したいと思います。
Istio のデプロイ
Istioctl で Istio 関連のコンポーネントを Ambient モードでデプロイしていきます。Ambient モードについて知りたい方は、こちらの記事で軽く触れているのでご一読いただければ幸いです。
公式ドキュメントの通りに下記のコマンドを実行します。Istio の Ambient モードには、istio-cni, istiod, ztunnel という 3 つのコンポーネントが必要なようです。
istioctl install --set profile=ambient --skip-confirmation
下記のコマンドでデプロイされたコンポーネントを確認します。
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
を指定します。
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 というコンポーネントをデプロイする必要があるようです。
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 にアクセスできる状態にしてみます。
istioctl dashboard kiali
http://localhost:20001/kiali
Failed to open browser; open http://localhost:20001/kiali in your browser.
こちらのコマンドによって localhost:20001/kiali
でアクセスできるようになるので、実際にアクセスするとこのような画面になります。
実際にトラフィックが流れているところを見たいので、適当に複数回 curl
で叩いてみます。
for i in $(seq 1 20); do curl -s http://localhost:8080/micro; done
いい感じにアニメーションでトラフィックを可視化することができました!!
さいごに
今回は Kind と Scaffold による開発を体験すべく、FastAPI のサンプルアプリケーションのデプロイから OpenTelemetry によるトレースの可視化、Istio と Kiali によるトラフィックの可視化を行いました。
Otel Collector や OpenObserve に関しては Dockerfile
と manifests/hoge.yaml
を用意して skaffold.yaml
を変更するだけで、変更検知して自動でビルド & デプロイが完了するというところで docker コマンドや kubectl コマンドを意識しない開発を体験できました。ビルドとデプロイに関して、考えることが少なくなりコードに集中できる感じが非常に嬉しいポイントでした。
本来やりたかった OpenTelmetry と Istio の連携は別の記事としたいと思います。
参考
- LinuxBrew
- Kind
- Scaffold
- OpenObserve
- Kiali
Discussion