konchangakita

KPSを一番楽しんでいたブログ 会社の看板を背負いません 転載はご自由にどうぞ

NKPアプリケーションカタログ 削除方法

NKPのアプリカタログ実装するときにハマったことメモ

NKPカタログ一覧に表示されない場合の確認と削除方法

カタログ登録に失敗した後、間違って登録された情報を削除しないと
修正して正しい設定をいれてもカタログに表示されない。。。

<原因>
・metadata.yamlやhelmrelease.yamlの内容が間違っている

<対処方法>
NKPマネージメントクラスタ上のocirepositoryを手動で削除する

WorkspaceのNamespaceを確認し(ややこしい)
この場合、”hoihoi-workspace-97m66-v6hv9” の方

$ nkp get ws
NAME                    NAMESPACE                    
default-workspace       kommander-default-workspace 
hoihoi-workspace-97m66  hoihoi-workspace-97m66-v6hv9
kommander-workspace     kommander                   


ocirepocitoryのアプリカタログを確認し、

$ kubectl get  ocirepository -n hoihoi-workspace-97m66-v6hv9 nkp-catalog-loghoihoi
NAME                    URL                                                 READY   STATUS                                                                                                       AGE
nkp-catalog-loghoihoi   oci://ghcr.io/konchangakita/nkp-catalog/loghoihoi   True    stored artifact for digest '0.0.1@sha256:3784397b0b2baec9d44d1e26b89ac1954daaf1589e75a6659a93df8b998a8659'   19h


deleteする

$ kubectl delete  ocirepository -n hoihoi-workspace-97m66-v6hv9 nkp-catalog-loghoihoi
ocirepository.source.toolkit.fluxcd.io "nkp-catalog-loghoihoi" deleted

ログほいほいを NKPアプリケーションカタログしてみる

この記事は、Nutanix Advent Calendar 2025の11日目として書きました。

ログほいほいHelmチャート化したので、これを活用してNKPアプリケーションカタログに登録してみます
NKPがなんのかというのは、ここでは説明は飛ばして
とにかくどうやって自作WebアプリをNKPのカタログ化させるのかという方法を紹介です
https://github.com/konchangakita/loghoihoi
Helm化したソースはココです


前提知識

まず、やるべきことはここに全部書いてあります
blog.ntnx.jp

NTNX>日記様いつも超すぺしゃるさんくすです!
というわけで、この手順にのっとって試してみましょう

準備

Helm パッケージを作成

$ helm package ./helm/loghoihoi
Successfully packaged chart and saved it to: /home/nutanix/konchangakita/loghoihoi/loghoihoi-0.1.0.tgz

Chart.yamlの Versionに合わせて、"loghoihoi-0.1.0.tgz" な感じのファイルが作成されます

OCI レジストリ(GHCR)へ push

$ export HELM_EXPERIMENTAL_OCI=1
$ helm push loghoihoi-0.1.0.tgz oci://ghcr.io/konchangakita/

※GHCRへのpushには、githubのTokenを先に作っておいたりの準備が必要になります

Github上でパッケージ確認

Package Settingから Publicにして、認証無しでダウンロードできるようにしておきます



NKPカタログ用のファイル作成

Catalog Repository(Git)を準備

NKPカタログには Git 管理されたカタログリポジトリを必要とします
NKPカタログ用のディレクトリを作って、nkpコマンドでテンプレート一式を作成します

nkp generate catalog-repository --apps <アプリ名>=<バージョン> --repo-dir ./

アプリ名を"loghoihoi"(Helmパッケージと同じ名前)、"0.0.1"で作成してみます

$ mkdir nkp-catalog
$ cd nkp-catalog
$ nkp generate catalog-repository --apps loghoihoi=0.0.1 --repo-dir ./
Successfully initialized application layout for loghoihoi-0.0.1
Catalog layout generated at /home/nutanix/konchangakita/nkp-catalog

こんな感じの構造で一式が作成されます

.
├── .bloodhound.yml
└── applications
    └── loghoihoi
        └── 0.0.1
            ├── helmrelease
            │   ├── cm.yaml
            │   ├── helmrelease.yaml
            │   └── kustomization.yaml
            ├── helmrelease.yaml
            ├── kustomization.yaml
            └── metadata.yaml


設定ファイルたち

アイコンの準備

※アイコンはなくても実装可能です
NKPアプリカタログ上で表示したいアイコンの設定できます
svg形式のファイルを用意して base64エンコードします

$ cat hoihoi.svg | base64 -w0
PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcm ~~~~~~~~~超長い~~~~~~~~~~~~~


metadata.yaml

metadata.yamlでは、NKPアプリカタログで表示される項目設定できます
descriptionとicon(エンコードした文字列)、supportLinkを追記しました、その他はそのまま

nkp-catalog/applications/loghoihoi/0.0.1/metadata.yaml

schema: catalog.nkp.nutanix.com/v1/application-metadata
allowMultipleInstances: true
category:
- general
description: "Nutanix LogHoihoi is a tool that helps you manage your logs dayo."
displayName: loghoihoi
icon: "PHN2ZyB4bWxucz0iaH~~~さっきのやつ~~~~~^"
licensing:
- Pro
- Ultimate
overview: ""
scope:
- project
supportLink: "https://x.com/konchangakita"
---

※descriptionやiconはなくてもよいけど、supportLinkは何か入れとかないとエラー出ます

helmrelease.yaml

helmrelease.yamlでは、OCIレジストリにプッシュしたOCI アーティファクトの URL のみ指定
"url: oci://ghcr.io/konchangakita/loghoihoi" と "tag: 0.1.0"を追記

nkp-catalog/applications/loghoihoi/0.0.1/helmrelease/helmrelease.yaml

apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
  name: ${releaseName}-chart-source
  namespace: ${releaseNamespace}
spec:
  interval: 6h0m0s
  ref:
    tag: 0.1.0
  url: oci://ghcr.io/konchangakita/loghoihoi
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: loghoihoi
  namespace: ${releaseNamespace}
spec:
  chartRef:
    kind: OCIRepository
    name: ${releaseName}-chart-source
    namespace: ${releaseNamespace}
  install:
    crds: CreateReplace
    createNamespace: true
    remediation:
      retries: 30
  interval: 15s
  targetNamespace: ${releaseNamespace}
  upgrade:
    crds: CreateReplace
    remediation:
      retries: 30
  valuesFrom:
  - kind: ConfigMap
    name: ${releaseName}-config-defaults
---

その他のファイルはデフォルトのまま

NKPアプリカタログの実装

Catalog Bundle の生成
nkp create catalog-bundle --repo-dir <ディレクトリ指定>
$ nkp create catalog-bundle --repo-dir .
Bundling 1 application(s) (airgapped : false)
 ✓ Validating metadata.yaml for loghoihoi/0.0.1
 ✓ Building OCI artifact nkp-catalog/loghoihoi:0.0.1
Processing application loghoihoi/0.0.1
 ✓ K8s v1.33.0: parsing resources 
 ✓ K8s v1.33.0: validating 
 ✓ Pulling requested images [====================================>1/1] (time elapsed 00s) 
 ✓ Saving application bundle to /home/nutanix/konchangakita/nkp-catalog/loghoihoi-0.0.1.tar

Run the following to push the artifact to your registry:
        nkp push bundle --bundle /home/nutanix/konchangakita/nkp-catalog/loghoihoi-0.0.1.tar --to-registry <your-registry-url>


Run the following command to create catalog artifact(s) after pushing them:
        nkp create catalog-application --url oci://<registry-url>/nkp-catalog/loghoihoi --tag 0.0.1 --workspace kommander-workspace


Bundle を GHCR へ Push
nkp push bundle \
  --bundle <バンドルファイル> \
  --to-registry <OCIレジストリ>
$ nkp push bundle \
  --bundle loghoihoi-0.0.1.tar \
  --to-registry oci://ghcr.io/konchangakita

 ✓ Creating temporary directory
 ✓ Extracting bundle configs from "loghoihoi-0.0.1.tar"
 ✓ Parsing image bundle config
 ✓ Starting temporary Docker registry
 ✓ Pushing bundled images [====================================>1/1] (time elapsed 02s) 


Github上のパッケージ確認

パッケージをpublicにして、認証無しでダウンロード可能に
github上のパッケージ名を確認、自動的にディレクトリ名の"nkp-catalog"が付与されている

先程と同じくパッケージをpublicにして、認証無しでダウンロード可能にしておく



NKP Workspace の Applications にカタログを登録

NKP の Workspace Name を確認

NKP Managementクラスタに対して、コマンドを実行
実行例)

$ nkp get ws
NAME                    NAMESPACE                    
default-workspace       kommander-default-workspace 
hoihoi-workspace-xbjfn  hoihoi-workspace-xbjfn-k9xgv
kommander-workspace     kommander                   


カタログ登録

先程プッシュした Catalog Bundle を指定し、カタログ登録したい workspace name "hoihoi-workspace-xbjfn" を指定

nkp create catalog-application \
  --url <OCIリポジトリ> \
  --tag <バージョン> \
  --workspace <ワークスペース名> \
  --skip-oci-registry-patches

実行例)

$ nkp create catalog-application \
  --url oci://ghcr.io/konchangakita/nkp-catalog/loghoihoi \
  --tag 0.0.1 \
  --workspace hoihoi-workspace-xbjfn \
  --skip-oci-registry-patches

Catalog application nkp-catalog-loghoihoi created. Use 'nkp edit ocirepository -n testest-workspace-mpjsh-2jwlt nkp-catalog-loghoihoi' to change its configuration if needed.
Note that the OCIRepository is not patched by NKP with credentials due to custom configuration. Make sure to configure the url and credentials according to your cluster networking capabilities.



NKP上でWorkspaceを選択して、Applicationをみると追加されている!


カタログから ログほいほいを実装

特にオプションなど必要ないのでEnableするだけ


loghoihoiのネームスペースが自動的に作成されて、アプリケーションが実装されているのが分かります

$ kubectl get all -n loghoihoi
NAME                                   READY   STATUS    RESTARTS   AGE
pod/elasticsearch-546d769b69-b7sxr     1/1     Running   0          9h
pod/kibana-6d6778c97b-gtwj8            1/1     Running   0          9h
pod/loghoi-backend-74c797cd79-nvhgm    1/1     Running   0          9h
pod/loghoi-frontend-6d57c96cfd-7nbg5   1/1     Running   0          9h
pod/loghoi-syslog-555bff66d9-27vcq     1/1     Running   0          9h

NAME                              TYPE           CLUSTER-IP       EXTERNAL-IP    PORT(S)                         AGE
service/elasticsearch-service     ClusterIP      10.106.170.124   <none>         9200/TCP                        9h
service/kibana-service            ClusterIP      10.98.136.116    <none>         5601/TCP                        9h
service/loghoi-backend-service    ClusterIP      10.105.120.228   <none>         7776/TCP                        9h
service/loghoi-frontend-service   ClusterIP      10.109.64.227    <none>         7777/TCP                        9h
service/loghoi-syslog-service     LoadBalancer   10.99.214.63     10.55.80.138   7515:31508/TCP,5066:31476/TCP   9h

NAME                              READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/elasticsearch     1/1     1            1           9h
deployment.apps/kibana            1/1     1            1           9h
deployment.apps/loghoi-backend    1/1     1            1           9h
deployment.apps/loghoi-frontend   1/1     1            1           9h
deployment.apps/loghoi-syslog     1/1     1            1           9h

NAME                                         DESIRED   CURRENT   READY   AGE
replicaset.apps/elasticsearch-546d769b69     1         1         1       9h
replicaset.apps/kibana-6d6778c97b            1         1         1       9h
replicaset.apps/loghoi-backend-74c797cd79    1         1         1       9h
replicaset.apps/loghoi-frontend-6d57c96cfd   1         1         1       9h
replicaset.apps/loghoi-syslog-555bff66d9     1         1         1       9h
$ kubectl get ingress -n loghoihoi
NAME             CLASS               HOSTS   ADDRESS        PORTS   AGE
loghoi-ingress   kommander-traefik   *       10.55.80.137   80      9h

ingressIPアドレスへブラウザから接続すれば
ログほいほいのWeb UIが表示されます


おまけ:カタログから削除するには

やはりここを参照
blog.ntnx.jp


Management クラスタに向けて、Namespaceを指定する

$ kubectl get apps.apps.kommander.d2iq.io -n hoihoi-workspace-xbjfn-k9xgv
NAME               APP ID       APP VERSION   SOURCE                  AGE
loghoihoi-0.0.1    loghoihoi    0.0.1         nkp-catalog-loghoihoi   10m
nutanix-ai-2.4.0   nutanix-ai   2.4.0         nutanix-ai-2.4.0        18m
$ kubectl delete apps.apps.kommander.d2iq.io -n hoihoi-workspace-xbjfn-k9xgv loghoihoi-0.0.1 
app.apps.kommander.d2iq.io "loghoihoi-0.0.1" deleted


さいごに

ここまでログほいほいを育ててきました
現状すべてのログを網羅しているわけではないですが、対象のログを増やすことはそんなに難しくないので
ここまできたらちょっとした改良もCI/CDで〜みたいこともやってみたいですね

ログほいほいをHelmでパッケージ化する

この記事は、Nutanix Advent Calendar 2025の2日目として書きました。

ログほいほいの機能自体は、前回の記事で概ね開発完了したわけですが、
ここから次の目標として、Nutanix Kubernetes Platform(NKP)アプリケーションカタログにチャレンジしたいと思います
まず最初の壁として必ず出てくるのが Helm 化です

ここからは、新しい本番用(?)リポジトリを切り出して実装していきます
github.com
とにかくすぐ実装したい人はGithub上を参照しよう

なんで NKP カタログって Helm チャート必須なの?

NKP アプリケーションカタログが Helm チャートを必須としている理由は、「アプリを標準化された形式で配布する」ためです。
NKP は内部的に FluxCD の HelmRelease を使っており、アプリを登録・Enable する仕組み自体が Helm を前提に設計されています。
単なる YAML の集合では、環境差分・アップグレード・Rollback を統一できず、バージョン管理も困難で「アプリ」として扱うには不十分です。
Helm なら values による上書き、OCI レジストリ配布、SemVer によるバージョン管理が可能になり、NKP が UI でのインストール/更新/削除を正確に制御できます。
つまり、NKP カタログで流通する“アプリ”の単位は Helm チャートであり、
これはプラットフォームとしての安定性と互換性を担保するための必須要件なのです。

Helmって何?

Helm は Kubernetes でアプリケーションを配布・管理するための“パッケージマネージャー” というものです
多数の YAML を1つの「チャート」としてまとめ、インストール・アップグレード・削除を統一的に扱えるようになります
values.yaml による変数のように設定変更が可能で、環境ごとに柔軟にパラメータを上書きできます
また、チャートにバージョンを付けて OCI レジストリに公開したり、依存コンポーネントをまとめて管理することもできます
これにより、Kubernetes でのアプリ展開が再現性高く、標準化された形で行えるようになるのが Helm の最大の利点です

Helmでインストールするとこんなイメージ

helm install loghoihoi ./helm/loghoihoi


Helm使うための前提条件

1. Helm CLI がインストールされていること
2. Kubernetes クラスタにアクセスできること
3. デプロイ先の namespace に権限があること
4. OCI レジストリ利用権限

ログほいほいをHelmで実装

helm実装の構造
helm/
└── loghoihoi
    ├── Chart.yaml
    ├── templates
    │   ├── backend-deployment.yaml

*** 各種 yamlファイル

    │   └── syslog-deployment.yaml
    └── values.yaml
Helm実装に必要なファイル達

・Chart.yaml
パッケージ情報、チャートのメタデータ(名前、バージョン、依存関係など)

・values.yaml
テンプレートに渡す デフォルト値を定義

・templates/ ディレクト

Helmを使ってインストール

なんやかんやで実装してみまして、helm/配下必要なファイルは配置してあるので
gitcloneしてきて、これだけです

helm install loghoihoi ./helm/loghoihoi


Helmインストール後の状況
$ kubectl get all -n loghoihoi
NAME                                   READY   STATUS    RESTARTS   AGE
pod/elasticsearch-546d769b69-vfl64     1/1     Running   0          10h
pod/kibana-6d6778c97b-zzw88            1/1     Running   0          10h
pod/loghoi-backend-74c797cd79-nzj65    1/1     Running   0          10h
pod/loghoi-frontend-6d57c96cfd-66kgz   1/1     Running   0          10h
pod/loghoi-syslog-555bff66d9-zbhlg     1/1     Running   0          10h

NAME                              TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)                         AGE
service/elasticsearch-service     ClusterIP      10.102.83.105   <none>          9200/TCP                        10h
service/kibana-service            ClusterIP      10.98.76.110    <none>          5601/TCP                        10h
service/loghoi-backend-service    ClusterIP      10.107.56.61    <none>          7776/TCP                        10h
service/loghoi-frontend-service   ClusterIP      10.104.45.80    <none>          7777/TCP                        10h
service/loghoi-syslog-service     LoadBalancer   10.105.21.6     10.42.153.138   7515:32239/TCP,5066:32397/TCP   10h

NAME                              READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/elasticsearch     1/1     1            1           10h
deployment.apps/kibana            1/1     1            1           10h
deployment.apps/loghoi-backend    1/1     1            1           10h
deployment.apps/loghoi-frontend   1/1     1            1           10h
deployment.apps/loghoi-syslog     1/1     1            1           10h

NAME                                         DESIRED   CURRENT   READY   AGE
replicaset.apps/elasticsearch-546d769b69     1         1         1       10h
replicaset.apps/kibana-6d6778c97b            1         1         1       10h
replicaset.apps/loghoi-backend-74c797cd79    1         1         1       10h
replicaset.apps/loghoi-frontend-6d57c96cfd   1         1         1       10h
replicaset.apps/loghoi-syslog-555bff66d9     1         1         1       10h


Web UIにアクセスするURLは下記で確認できます

INGRESS_IP=$(kubectl get ingress -n loghoihoi -o jsonpath='{.items[0].status.loadBalancer.ingress[0].ip}')

echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🌐 WebブラウザでアクセスするURL"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "フロントエンド:     https://${INGRESS_IP}/"
echo "API ドキュメント:   https://${INGRESS_IP}/docs"
echo "API ドキュメント:   https://${INGRESS_IP}/redoc"
echo "Kibana:            https://${INGRESS_IP}/kibana"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"


Helmでアンインストール

クリーンアップもこれだけで、Namespaceごと削除してくれます

helm uninstall loghoihoi


とても楽ちんな気がする

さいごに

今回のはじめて、Helm実装にチャレンジしたわけですが、実装にコツは必要にはなるけど、慣れるとデプロイや削除こっちがとても楽ですね
Kebernetesで実装のためには、たっくさんの yaml を書いて用意するわけですが、後でちょっとした値の変更などであっちこっちのファイルのメンテが大変でした
バラバラの YAML をまとめてinstall / upgrade / delete を一括で実行できるので、アプリを「1つの単位」で扱える感覚です

ログほいほい Kubernetes化へ ! 、、、ついに完成なのか

Nutanix ログほいほいがついにKubernetes化に対応です
Kubernetes化に合わせて、大きくリファクタリングも行いました
変更点をピックアップします

  • リファクタリング方針(Tidy First)で基盤整備を優先し、以降の機能追加を速く安全にする
  • バックエンドAPIの変更 Flask から FastAPI へ
  • Kubernetes化の実装(Dockerfile分離、StorageClass戦略、Recreate戦略、Traefik/MetalLB、ヘルスチェック)
  • 認証用SSH鍵の管理方式を統一K8sはSecret、composeはホストパス)


いつもの今回のブログの内容はココ
起動の仕方などもGithub上に書いてます、実装しながら試した人はまず Github
github.com


リファクタリング方針:Tidy First で「掃除してから進む」

Tidy First は、「まず掃除してから進めよう」という開発方針です
いきなり新機能を足す前に、コードや設定を少し整理しておくことで、後の変更を楽にしようというものみたいです
たとえば変数名をわかりやすくしたり、ファイルを整理したり、不要なコードを消したりなどなど

このあたりきっと元ネタです
Software Design: Tidy First? | Kent Beck | Substack

まぁ、キレイに書きたいって思ってても正解がわからず、勝手にコードは汚くなっていくわけですが
最近は、AIにコード書かせるのもだいぶこなれてきたので、そのAIにTidy Firstを意識させるのがよいのでしょう

今回のTidy例:
  • Dockerfileの用途分離(`dockerfile`=開発、`Dockerfile.k8s`=本番)
  • ストレージとデプロイ戦略の統一(HostPath=Recreate、RWXがない前提を明文化)
  • ドキュメントの相対パス化(複製先でもデプロイ可能に)


フォルダ構造刷新と汎用コンポーネントの独立

Kubernetes対応にむけて、役割ごとにフォルダを明確に分離させる
コンポーネント間の境界が明確になり、CI/CDやローカル開発の導線も一本化する

  • scripts/
    • `k8s/deploy.sh` や `build-and-push.sh` など、環境準備・デプロイを自動化するスクリプト群を集約
    • `init-ssh-keys.sh` や `get-host-ip.sh` のような開発向けユーティリティもここに配置し、再利用性・権限管理を一本化

  • shared/
    • `shared/gateways/` に Prism API クライアントや Elasticsearch ラッパーなど、バックエンド複数機能で使い回すドメインサービスを格納
    • FastAPIアプリ本体(`backend/fastapi_app`)からは `import shared.gateways...` するだけで利用でき、依存関係が明示的に
    • テストや将来のジョブワーカー追加時にも、同じ shared モジュールを読み込めばロジックを重複させずに済む


バックエンドAPI をFlask から FastAPIへ変更

`Swagger UI` と `ReDoc` の実装(FastAPI標準機能)

API を作るときに、自動でドキュメント(説明書)を生成してくれるのです
このドキュメントを表示する画面が Swagger UI と ReDoc です

  • Swagger UIとは: OpenAPI仕様をGUIで操作できるインタラクティブドキュメント。エンドポイントのリクエスト/レスポンス例を見ながら、その場で試し打ちができる
    1. コードを書くだけで自動生成(docstring不要)
    2. 試し打ち(Try it out)でAPIテストができる
    3. JSON形式のリクエストやレスポンスも確認できる
    4. デフォルトで /docs に用意される

Swagger UI


  • ReDocとは: 同じOpenAPI仕様をもとにした静的ドキュメントビューア。情報密度が高く、階層化されたメニューで仕様を読み込みやすい
    1. 同じく FastAPI が自動生成する 別のスタイルのドキュメント画面
    2. こちらは 読み物としてわかりやすい構成
    3. 見やすいUIで、主に「API仕様書」として利用される

ReDoc


FastAPIでの提供方法

基本的には自動生成してくれるので、バックエンドのIPアドレス`/docs`、`/redoc`にアクセスするだけで楽チン
http://バックエンドIP/docs → Swagger UI
http://バックエンドIP/redoc → ReDoc

Kubernetes 化の実装ポイント

  • Dockerfile分離
    • 開発: `backend/dockerfile`, `frontend/dockerfile`, `syslog/dockerfile`
    • 本番: `*/Dockerfile.k8s`
    • 誤運用を防ぎつつ、CI/CDとローカル開発の両立を容易に
  • イメージレジストリ: GHCR(`ghcr.io/konchangakita/*`)
    • Docker hubに比べて Pull安定性、公開設定で認証レス Pull
  • ストレージ戦略
    • デフォルトは HostPath `manual`(開発・検証向け)
    • PVCはRWOのため、Elasticsearch/Backendは `strategy: Recreate` を採用
    • 本番用には `STORAGE_CLASS` を環境変数で差し替えて(NKP等のCSIに対応)
  • Ingress / ネットワーク
    • IngressClass: `kommander-traefik`
    • ルーティング: `/api`(API), `/socket.io`(WebSocket), `/docs` `/redoc`(API UI), `/kibana`(UI), `/`(FE)
    • MetalLB で LoadBalancer IP を割当
    • `/docs` `/redoc` の`pathType: Prefix`化で静的アセットまで確実にバックエンドへ転送し、`/api`付き経路やTraefik Middlewareを整理
  • ヘルスチェック
    • Backend: `/health`(liveness), `/ready`(readiness)
  • 自動化スクリプト
    • `k8s/deploy.sh` が Namespace、Traefik検出/インストール、PV/PVC、各種Manifest、Ingressまでを自動化


Ingressルーティング図


docker-compose 開発環境の維持

docker-compose開発環境とKubernetes本番環境のように、同一コードでそのまま使えるようにしたく
開発と本番で「ファイル/環境変数/ストレージ/起動方法」が明確に分離され、相互の混入を防止

認証用SSH鍵の管理方式(K8s=Secret、compose=ホストパス)

  • 共通方針: 鍵はGit管理せず、ホストディレクトリ `config/.ssh/` に生成・保持
  • Kubernetes
    • `deploy.sh` が `config/.ssh/` の鍵を検査し、なければ生成→Secret `loghoi-secrets` を作成
    • Deploymentで `/app/config/.ssh` にマウント、`SSH_KEY_PATH`/`SSH_PUBLIC_KEY_PATH` で参照
    • 初回は公開鍵をPrism Element(Cluster Lockdown)へ登録
  • docker-compose
    • `./config/.ssh` をコンテナにマウントし、環境変数で鍵パスを指定
    • 初回は `scripts/init-ssh-keys.sh` で鍵生成→公開鍵をUI/ファイルで確認
  • フロント連携: SSH認証失敗を検知したら、UIで自動的に公開鍵表示を促す


ストレージ比較表

環境 StorageClass アクセスモード デプロイ戦略 備考
開発・検証 `manual` (HostPath) `ReadWriteOnce` `Recreate` ノードローカルに永続化、手軽さ優先
本番 `STORAGE_CLASS` 環境変数で指定 (CSI) クラスタ標準のアクセスモード `RollingUpdate` (Elasticsearchは環境依存) NKPなどのクラスタストレージに合わせて差し替え

さいごに

このみちのりは相当大変でした、、、もともと kubernetes のよくわからないまま開発を続け
そこからの kubernetes化は、ある意味いちから作り直しに近かったのです
もうそこは紆余曲折ありすぎて、ログほいほいからははずれるので書きませんが
今度何かを新しく作る時は、kubernetes化をはじめから意識しておけば、そんなに大変でないはず
Kubernetes化までできたので、一旦開発日記ブログ的なものはここまですが
クラウドネイティブ Webアプリケーションとしては、もう少しチャレンジしてみたいことはあるので
新しくブランチを切り出して、CI/CDやhelm-chartなど開発側勉強に取り組んでみたいと思います

FastAPI で SocketIO


Socket.IOは、リアルタイム双方向通信を実現するJavaScriptライブラリなのですが、FlaskとFastAPIでは実装がまた別でここも作り直しになります

時間のない人向けの最終形態は github
github.com

最小の実装

バージョン情報

バックエンドの requirement.txt
Pythonバージョン: 3.11

fastapi==0.104.1
uvicorn[standard]==0.24.0
python-socketio==4.3.1
python-engineio==3.9.0
python-multipart==0.0.6

フロントエンドの package.json
Node.jsバージョン: 18

{
  "dependencies": {
    "next": "^14.0.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "socket.io-client": "^4.7.2"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.0",
    "@types/socket.io-client": "^1.4.36",
    "typescript": "^5.0.0",
    "eslint": "^8.0.0",
    "eslint-config-next": "^14.0.0"
  }
}


FastAPIでの実装のとっかかり

インスタンスの作り方からして違うのね

from fastapi import FastAPI
import socketio

sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*")
sio_app = socketio.ASGIApp(sio)

app = FastAPI()
app.mount("/socket.io", sio_app)  # クライアントは /socket.io に接続

@sio.event
async def connect(sid, environ):
    await sio.emit("welcome", {"msg": "hello"}, to=sid)

if __name__ == "__main__":
    uvicorn.run(socket_app, host="0.0.0.0", port=8000)


SocketIO接続テスト環境の実装

最小限のテスト環境として、フロントエンドとバックエンドでSocketIOで接続と切断だけを行う簡単なテスト

バックエンド側の実装

main.py

'use client'

import React, { useState } from 'react'
import io from 'socket.io-client'

type Socket = ReturnType<typeof io>

export default function Home() {
  const [socket, setSocket] = useState<Socket | null>(null)
  const [connected, setConnected] = useState(false)

  const connectSocket = () => {
    if (socket && connected) {
      console.log('既に接続されています')
      return
    }

    console.log('SocketIO接続を開始します...')
    const newSocket = io(`${window.location.protocol}//${window.location.hostname}:8000`)
    
    newSocket.on('connect', () => {
      console.log('サーバーに接続しました')
      setConnected(true)
    })

    newSocket.on('disconnect', () => {
      console.log('サーバーから切断されました')
      setConnected(false)
    })

    newSocket.on('connect_error', (error: any) => {
      console.error('接続エラー:', error)
      setConnected(false)
    })

    setSocket(newSocket)
    console.log('SocketIOオブジェクトを設定しました')
  }

  const disconnectSocket = () => {
    if (socket) {
      socket.close()
      setSocket(null)
      setConnected(false)
      console.log('手動で切断しました')
    }
  }

  return (
    <div className="container">
      <h1>SocketIO テスト環境 (Next.js App Router)</h1>
      
      <div className="card">
        <h2>接続状態</h2>
        <div className={`status ${connected ? 'connected' : 'disconnected'}`}>
          {connected ? '接続中' : '切断中'}
        </div>
        <div style={{ marginTop: '10px' }}>
          <button 
            onClick={connectSocket} 
            className="button connect" 
            disabled={connected}
          >
            接続
          </button>
          <button 
            onClick={disconnectSocket} 
            className="button disconnect" 
            disabled={!connected}
          >
            切断
          </button>
        </div>
      </div>
    </div>
  )
}
フロントエンド

page.tsx

'use client'

import React, { useState } from 'react'
import io from 'socket.io-client'

type Socket = ReturnType<typeof io>

export default function Home() {
  const [socket, setSocket] = useState<Socket | null>(null)
  const [connected, setConnected] = useState(false)

  const connectSocket = () => {
    if (socket && connected) {
      console.log('既に接続されています')
      return
    }

    console.log('SocketIO接続を開始します...')
    const newSocket = io(`${window.location.protocol}//${window.location.hostname}:8000`)
    
    // 接続成功時の処理
    newSocket.on('connect', () => {
      console.log('サーバーに接続しました')
      setConnected(true)
    })

    // 切断時の処理
    newSocket.on('disconnect', () => {
      console.log('サーバーから切断されました')
      setConnected(false)
    })

    // エラー時の処理
    newSocket.on('connect_error', (error: any) => {
      console.error('接続エラー:', error)
    })

    setSocket(newSocket)
    console.log('SocketIOオブジェクトを設定しました')
  }

  const disconnectSocket = () => {
    if (socket) {
      socket.close()
      setSocket(null)
      setConnected(false)
      console.log('手動で切断しました')
    }
  }

  return (
    <div className="container">
      <h1>SocketIO チャットルーム (Next.js App Router)</h1>
      
      <div className="card">
        <h2>接続状態</h2>
        <div className={`status ${connected ? 'connected' : 'disconnected'}`}>
          {connected ? '接続中' : '切断中'}
        </div>
        <div style={{ marginTop: '10px' }}>
          <button 
            onClick={connectSocket} 
            className="button connect" 
            disabled={connected}
          >
            接続
          </button>
          <button 
            onClick={disconnectSocket} 
            className="button disconnect" 
            disabled={!connected}
          >
            切断
          </button>
        </div>
      </div>

    </div>
  )
}


接続テスト

ブラウザでhttp://localhost:3000へ接続

「接続」ボタンをクリックし、状態が「接続中」に変わることを確認

「切断」ボタンをクリックし、状態が「切断中」に変わることを確認

socketio バージョンによって、テストがうまくいかないことがあるので
うまくいかない時は、フロントエンドとバックエンドのバージョンを変えてテストを色々試してみるとよいです
今回の構成では、python-socketio 5.13.0(Engine.IO 4.xプロトコル)とsocket.io-client 4.7.2(Engine.IO 4.xプロトコル)を使用しており、互換性がある


チャットルーム実装

SocketIOでは双方向通信できるので、クライアント同士を「部屋(room)」に参加させて、同じ部屋にいる人だけにメッセージを届けるちょっとしたチャットルームを作ることができます

チャットルーム基本の流れ

1.クライアントが接続する
接続時にユーザーを特定の「room」に参加させる

2.メッセージを受信する
受け取ったメッセージを「同じroom」にいる人だけにブロードキャストする

3.退出処理する
切断時や退出時にsocketIO切断

実装は

実装のステップバイステップはこちらにまとめてあるので参照ください
github.com


おわりに

まだFastAPIによりパフォーマンス向上の実感はないですが、バックエンドをリファクタリングしつつ
テスト用プログラムも実装していきたい。。。
Kubernetes化への道のりは、まだもう少し時間がかかりそう

FastAPIを試してみる


今までなんとなくFlask使っていましたが、どうやら遅いらしい
というわけで、簡単なAPIを作るならFastAPIが良いというのを聞いて早速お試し

いつもどおり、Docker Composeを使ってフロントエンド(Next.js、ポート7777)とバックエンド(FastAPI、ポート7776)を構築し、簡単なCRUDアプリケーションを作成してみる

時間がない人むけに まずgithub おいておきます
github.com


FastAPIとは

FastAPIが選ばれる理由はこんなとこらしい
1. **自動生成されるAPIドキュメント**: Swagger UIとReDocが自動で生成される
2. **型安全性**: Pydanticを使った型ヒントによるデータ検証
3. **高性能**: 他のPythonフレームワークと比較して高速
4. **現代的な設計**: async/await対応、OpenAPI準拠

とりあえずDockerで作ってみる

必要最低限でFastAPI作ってみる

どうやらFlaskとほぼ同じ構文でいけるっぽい、これは楽
main.py

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import List, Optional
import uvicorn
from datetime import datetime

# FastAPIアプリケーションのインスタンスを作成
app = FastAPI(
    title="FastAPI Test API",
    description="FastAPIのテスト用API",
    version="1.0.0"
)

@app.get("/")
async def root():
    return {"message": "FastAPI Test API はじめました", "status": "running"}

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=7776)


Dockerイメージの準備

requirements.txt

fastapi==0.104.1
uvicorn[standard]==0.24.0
pydantic==2.5.0
python-multipart==0.0.6


Dockerfile

FROM python:3.11-slim

WORKDIR /app

# 依存関係をインストール
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# アプリケーションファイルをコピー
COPY . .

# ポート7776を公開
EXPOSE 7776

# FastAPIアプリケーションを起動
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7776", "--reload"]


Docker イメージの作成
docker build -t fastapi-test-backend .
docker run -p 7776:7776 fastapi-test-backend


ブラウザから確認

httphttp://localhost:7776/へアクセスしてみる
メッセージが確認できればOK


これだけだと速さはわからんけど。。。とりあえず動いた!


フロントエンドも実装してそれっぽくしてみる

フロントエンドは個人的定番のNext.js+Tailwindでサクッとな
docker-composeで一気に起動できるいつも検証構成です

実装のステップ・バイ・ステップの様子はこちらにおいておきます
github.com


では、テストの最終段階の実装方法です

githubからクローン
git clone https://github.com/konchangakita/fastapi-test.git
fastapi-test/
├── backend/
│   ├── main.py              # FastAPIアプリケーション
│   ├── requirements.txt     # Python依存関係
│   └── Dockerfile          # バックエンド用Dockerfile
├── frontend/
│   ├── app/                # Next.js App Router
│   ├── components/         # Reactコンポーネント
│   ├── lib/               # APIクライアント
│   ├── types/             # TypeScript型定義
│   ├── package.json       # Node.js依存関係
│   └── Dockerfile         # フロントエンド用Dockerfile
└── docker-compose.yml     # Docker Compose設定


Docker Composeで起動

一気に起動してしまう

docker-compose -f "docker-compose.yml" up --build -d
> docker ps
CONTAINER ID   IMAGE                   COMMAND                   CREATED             STATUS                         PORTS                                         NAMES
19b1082e51ff   fastapi-test-frontend   "docker-entrypoint.s…"   About an hour ago   Up About an hour               0.0.0.0:7777->7777/tcp, [::]:7777->7777/tcp   fastapi-frontend
016840bdb614   fastapi-test-backend    "uvicorn main:app --…"   About an hour ago   Up About an hour (unhealthy)   0.0.0.0:7776->7776/tcp, [::]:7776->7776/tcp   fastapi-backend


ブラウザで接続

起動後は、ブラウザから接続するだけ
そこそこそれっぽいWebアプリになっています
http://localhost:7777


テスト用なのでストレージ、DBなんかは使わずに、Pythonのリストを利用したメモリ内で追加削除を実装しています
いつものelasticを使うのもアリですね
あと、テスト用なのでどこからでもバックエンドに接続できるようにCORS設定はワイルドカード["*"]にしています

さいごに

あいかわらずぼちぼちログほいほいしているわけですが、長期かかりすぎているので
ちょっとづづバージョンなどを見直していたら気になるものをみつけたので、勢いで検証してみました
リファクタリングしながら、ログほいほいにも実装してみようと思います
いずれKubernetes化にむけて

【Nutanix ログほいほい】コレクトログ ほいほい

Nutanix Advent Calendar 2024 21日目の記事です!

Nutanix Advent Calendar 2024
adventar.org

さて、みなさん待望のログほいほいの続報です
前の記事から随分と間隔もあいてしまい、ログほいほいも長期プロジェクト化してきました
いまさらログほいほいがなんなのかは説明はしませんが
去年のAdvent Calendarもログほいほい関連でしたし、2年連続です
なんとなく今年のAdevent Calendarあたり完成するだろうとは思っていたのですが
ようやくやりたいことのカタチには近づいてきました、完全体まではまだもう少し掛かりそうです
飽きてさわっていないのではなく、単純にNutanix機能が増えすぎて他のことやっているうちに時間が経ってしまいました
増えすぎた機能は、他の方 Advent Calenarを要チェックです!



というわけで、今回は最後の目玉機能コレクトログです



計画段階ではこんな絵でした

当初は、至ってシンプルなイメージだけでした
実際に作り始めたら、色々盛り込みたい機能があって捗ってしまいました

コレクトログとは

やりたいことは、トラブルシューティングに必要そうな「ログ」と「CVMでコマンド実行結果」をまとめてZIPでダウンロード、なんなら、ダウンロードせずともログの中身をGUI上でも表示
トラブル時に実際に自分で集めたり中身を確認するにはまぁまぁ手間のかかることなので
実現できたらなかなか便利そうな機能だと思ってます

今回の仕組み


今回は、外部のツールを使うことなくログはバックエンドサーバにシンプルに保管する方法を採用してます
収集するログは代表的なものを選んでみたけど、バックエンドコンテナ上に jsonファイル で定義しているので、そこをいじれば自由に取得対象を増やすこともできます
Nutanix的には、ログ保管場所に Objects とか使っても面白いかもなので、いつかやってみたい

取得するログ設定ファイル

この json ファイルで設定してます
実行するコマンドを指定する json
取得するログファイルを指定する json

ログコレクトの利用イメージ

ソースコードは、コチラgithub から入手できます
起動方法は、Github上にも記載があるので参照すること
今回は特にNutanixクラスタ側では準備の必要はないです
github.com

コレクトログの画面

トップのメニュー画面に「Collect Log」のメニューを追加
メニューも充実してきました

ログ収集

まずは何も表示されていない状態です
「START COLLECT LOG」でログ収集を開始

けっこうお気に入りな Loading画面が表示され、1分くらい待つ
(いつかはちゃんとプログレスバーとかつけてみたい)


zipファイルの中身をみる

取得完了すると、zipファイル名を選択できるようになります


zipファイルを選択すると、取得したログ一覧が表示される


ログビューワー

ログを選択すると、Collect Log Viewに表示されます
ダウンロードせずに表示できる、わりとこだわりです


ダウンロード

もちろんzipファイルのダウンロードもできます


まとめ

リアルタイムログシスログコレクトログと作り続けて、ようやくここまで辿り着きました
あともう少しづつ修正を加えながら、最終目的としては Nutanixマーケットプレイスでワンクリックデプロイとか、Kunbernetes化させて、NKPのアプリ登録とかしたらおもしろそうかなと思ってみたりしてます
そのためには本格的にデータの置き場所とかも考えないといけないので、ちょうどよい勉強にもなりそう