0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

KubernetesクラスタをVMware Workstationでオンプレ想定で構築してみる (RHEL9) - (4) 自作アプリのディプロイ/結合テスト編 -

Last updated at Posted at 2025-05-14

はじめに

Kubernetesクラスタに自分で開発したアプリケーションをディプロイする際に行う基本的な作業の備忘録。対象のディプロイ環境はオンプレ想定で構成してみる。

ここでは(これまでの記事で実施してきた作業内容のレビューを兼ねて)ディプロイした各コンポーネントを利用したテストアプリケーションを作成し結合テストをしてみる。

具体的にはGoで開発したアプリケーションからPodmanでコンテナイメージを作成し、プライベートレジストリ (Harbor)へPushする。その後、Kubernetesクラスタ環境でプライベートレジストリからイメージをPullしディプロイする。

以下のような作業観点で実施する。

  • 永続ボリューム (Ceph File System)の利用
  • ConfigMap (設定情報)の利用
  • Secret (認証情報や証明書関連)の利用
  • Service (LoadBalancerタイプ)の利用
  • プライベートレジストリ (Harbor)の利用
  • RGW/Amazon S3 API (Ceph Object Store)連携
  • Podmanでのコンテナビルド
  • Pod間のAPI呼び出し連携
  • リソース定義でのディプロイ
  • Helmチャートでのインストール
  • Helmチャートでの更新 (例: サーバ証明書だけの更新)

利用するソフトウェア

  • VMware Workstation 17 Pro (Windows 11 / X86_64)
  • RHEL 9.5 (VM)
  • Dnsmasq (2.79)
  • HA-Proxy (1.8.27)
  • Kubernetes (v1.32)
  • CRI-O (v1.32)
  • Calico (v3.29.3)
  • MetalLB (v0.14.9)
  • Helm (v3.17.3)
  • Harbor (2.13.0Rook-Ceph (v1.17.1)
  • Ceph (v19.2.2)
  • amazon/aws-cli

(参考) テストアプリケーションの作成環境

  • go (1.23.6)
  • gin-gonic (v1.10.0)
  • Podman (5.2.2)
  • Visual Studio Code (1.100.0)
  • Visual Studio Code拡張: Go (0.46.1)
  • Visual Studio Code拡張: Remote Development (0.26.0)
  • Visual Studio Code拡張: Remote Explorer (0.5.0)
  • Visual Studio Code拡張: Dev Containers (0.413.0)
  • Visual Studio Code拡張: YAML (1.18.0)
  • コンテナ: ubi9/ubi-minimal (latest)
  • コンテナ: ubi9/go-toolset (1.23)
  • コンテナ: ubi9 (latest)
  • コンテナ: busybox (latest)
  • ChatGPT (GPT-4o/o4-mini-high)

※ goのバージョンはコンテナビルド時に利用する「ubi9/go-toolset」イメージのgoのバージョンに(マイナーバージョン含めて)合わせたほうがよさげ。

Kubernetesクラスタ構成

以下で構築済みの環境を利用する。

テスト環境の構成

これまでセットアップしたKubernetesクラスタ環境に簡単なテストアプリとしてAPIサービスとAPIゲートウェイサービスをディプロイして結合テストを実施してみる。

test_apps1.png

開発するテストアプリケーション

テストアプリ名 API 説明
k8s-test-app 前回作成したテストアプリケーション。単純なAPIをエキスポートする
GET /ping ping要求に対してpong応答(json)を返すAPI
k8s-test-appgw 各コンポーネントアクセスへのAPIゲートウェイ
GET /ping k8s-test-appアプリのAPI (/ping)へ中継する
GET /s3-buckets オブジェクトストアのバケット一覧応答(json)を返すAPI。Ceph RGWへのAPI要求を中継する
GET /json-from-cephfs CephFSに格納されているローカルファイル内容を応答(json)として返すAPI

テストアプリケーションの構成定義

k8s-test-appの構成定義

項目 タイプ Volumeマウント先
TLS: サーバ証明書 Secret /app/certs/tls.crt
TLS: サーバ秘密鍵 Secret /app/certs/tls.key
API-KEY Secret /app/secret/testapp-secret.yaml
アクセスログ PersistentVolumeClaim /mnt/cephfs/k8s-test-app/log/history.log
本来は同時書き込みを防ぐためPod毎に用意すべきだがテスト目的なので割愛

k8s-test-appgwの構成定義

項目 タイプ Volumeマウント先
k8s-test-app URL ConfigMap /app/config/app-config.yaml
RGWエンドポイント ConfigMap /app/config/app-config.yaml
RGWリージョン ConfigMap /app/config/app-config.yaml
読み出し先のjsonファイルパス ConfigMap /app/config/app-config.yaml
TLS: PrivateCA証明書 ConfigMap /app/certs/ca.crt
rook-ceph:rgw-ca-certからコピーする
TLS: サーバ証明書 Secret /app/certs/tls.crt
TLS: サーバ秘密鍵 Secret /app/certs/tls.key
API-KEY Secret /app/secret/auth-secret.yaml
RGW アクセスID Secret /app/secret/s3-secret.yaml
RGW シークレットアクセスキー Secret /app/secret/s3-secret.yaml
読み出し先のjsonファイル PersistentVolumeClaim /mnt/cephfs/k8s-test-appgw/data.json
アクセスログ PersistentVolumeClaim /mnt/cephfs/k8s-test-appgw/log/history.log
本来は同時書き込みを防ぐためPod毎に用意すべきだがテスト目的なので割愛

(参考) Podへのパラメータ情報の渡し方の備忘録

以下はdeployment定義内で構成できる。

(1) 環境変数として定義しておく (envフィールド)
(2) ConfigMap定義への参照を定義し環境変数として受け取る (configMapRef)
(3) ConfigMap定義をボリューム定義しマウントされた設定ファイルとして受け取る (volumes/volumeMountsフィールド)
(4) Secret定義への参照を定義し環境変数として受け取る (secretKeyRef)
(5) Secret定義をボリューム定義しマウントされた設定ファイルとして受け取る (volumes/volumeMountsフィールド)
(6) アプリケーション実行時の引数として渡す (command/argsフィールド)

テストアプリケーションの作成とディプロイ

k8s-test-appアプリのTLS/API-KEY認証対応 (エンハンス)

※ 小さいテストプログラムなので前回作成した元コードをChatGPTに修正内容を入力して対応。

コード対応
[mng ~]$ cd k8s-test-app

[mng k8s-test-app]$ go mod tidy

[mng k8s-test-app]$ go build

[mng k8s-test-app]$ ls
go.mod  go.sum  main.go  test-app
ソースコード (main.go)
main.go
package main

import (
	"crypto/tls"
	"fmt"
	"log"
	"net/http"
	"os"
	"time"

	"github.com/gin-gonic/gin"
	"gopkg.in/yaml.v2"
)

type TestAppSecret struct {
	APIKey string `yaml:"api_key"`
}

var (
	validAPIKey string
)

func main() {
	if err := loadSecret("/app/secret/testapp-secret.yaml"); err != nil {
		log.Println("ERROR: failed to load API key:", err)
		return
	}
	log.Println("INFO: API key successfully loaded")

	r := gin.Default()
	r.Use(authMiddleware)

	r.GET("/ping", handlePing)

	srv := &http.Server{
		Addr:    ":8443",
		Handler: r,
		TLSConfig: &tls.Config{
			MinVersion: tls.VersionTLS12,
		},
	}

	log.Println("INFO: Starting HTTPS server on :8443")
	if err := srv.ListenAndServeTLS("/app/certs/tls.crt", "/app/certs/tls.key"); err != nil {
		log.Println("ERROR: failed to start HTTPS server:", err)
		return
	}
}

func loadSecret(path string) error {
	data, err := os.ReadFile(path)
	if err != nil {
		log.Println("ERROR: failed to read secret file:", err)
		return fmt.Errorf("read error: %w", err)
	}

	var secret TestAppSecret
	if err := yaml.Unmarshal(data, &secret); err != nil {
		log.Println("ERROR: failed to unmarshal secret YAML:", err)
		return fmt.Errorf("unmarshal error: %w", err)
	}

	if secret.APIKey == "" {
		log.Println("ERROR: API key in secret is empty")
		return fmt.Errorf("API key is empty")
	}

	validAPIKey = secret.APIKey
	return nil
}

func authMiddleware(c *gin.Context) {
	expected := fmt.Sprintf("TestApp %s", validAPIKey)
	header := c.GetHeader("Authorization")
	if header != expected {
		log.Println("WARN: unauthorized request")
		c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
		return
	}
	c.Next()
}

func handlePing(c *gin.Context) {
	log.Println("INFO: /ping request")
	appendAccessLog("/mnt/cephfs/k8s-test-app/log/history.log", c.FullPath())
	c.JSON(http.StatusOK, gin.H{"message": "pong"})
}

func appendAccessLog(path string, route string) {
	f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		log.Println("ERROR: log open error:", err)
		return
	}
	defer f.Close()
	fmt.Fprintf(f, "%s %s\n", time.Now().Format(time.RFC3339), route)
}

コンテナイメージを再作成

ビルド
[mng k8s-test-app]$ podman build -t k8s-test-app:latest -f Containerfile .

[mng k8s-test-app]$ podman images
REPOSITORY                                       TAG         IMAGE ID      CREATED         SIZE
localhost/k8s-test-app                           latest      3a2c94adf05e  26 seconds ago  229 MB
<none>                                           <none>      0c95e1761eb6  48 seconds ago  1.53 GB
<none>                                           <none>      cadb179cb435  11 minutes ago  229 MB
<none>                                           <none>      9d4640aa3552  11 minutes ago  1.53 GB
harbor.test.k8s.local/k8s-test-app/k8s-test-app  latest      1aba92ed3d43  28 hours ago    229 MB
<none>                                           <none>      ff57c1ac1fe0  28 hours ago    1.53 GB
<none>                                           <none>      3932eb3d7723  28 hours ago    1.38 GB
<none>                                           <none>      384b325ddbb4  29 hours ago    1.13 GB
registry.access.redhat.com/ubi9/go-toolset       1.23        d8663eae6e1a  7 days ago      1.13 GB
registry.access.redhat.com/ubi9                  latest      18ac20acd5ec  2 weeks ago     217 MB
Containerfile
# Stage 1: Build
FROM registry.access.redhat.com/ubi9/go-toolset:1.23 AS builder

USER root
WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o app

# Stage 2: Runtime
FROM registry.access.redhat.com/ubi9

WORKDIR /app
COPY --from=builder /app/app .

EXPOSE 8443
CMD ["./app"]

k8s-test-app用のサーバ証明書の作成

k8s-test-app用のサーバ証明書を発行する。

# 証明書設定の作成
[mng k8s-test-app]$ vi openssl.cnf

# 秘密鍵の作成
[mng k8s-test-app]$ openssl genpkey -algorithm ec -pkeyopt ec_paramgen_curve:P-256 -out tls.key

# CSRの作成
[mng k8s-test-app]$ openssl req -new -key tls.key -out tls.csr -config openssl.cnf

# 証明書作成
[mng k8s-test-app]$ openssl x509 -req -in tls.csr -CA private_ca.crt -CAkey private_ca.key -CAcreateserial -out tls.crt -days 365 -sha256 -extfile openssl.cnf -extensions server-cert

[mng k8s-test-app]$  openssl x509 -in tls.crt -text -noout
Certificate:
    Data:
        Version: 3 (0x2)
...
        Issuer: CN=private_ca
...
        Subject: C=JP, O=k8s, OU=test, CN=k8s-test-app
...
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment, Key Agreement
            X509v3 Extended Key Usage:
                TLS Web Server Authentication
            X509v3 Subject Alternative Name:
                DNS:k8s-test-app.test.k8s.local, DNS:k8s-test-app.default.svc.cluster.local, DNS:k8s-test-app, IP Address:10.0.0.64, IP Address:10.100.133.21, IP Address:127.0.0.1
...

※ tls.crt(証明書)とtls.key(秘密鍵)というファイルが生成される。

SAN(alt_name)をService(MetalLB)の(前回に割り当てられた)情報へ合わせて指定しておく。

openssl.cnf
[server-cert]
keyUsage = critical, digitalSignature, keyEncipherment, keyAgreement
extendedKeyUsage = serverAuth
subjectAltName = @alt_name

[req]
distinguished_name = dn
prompt = no

[dn]
C = JP
O = k8s
OU = test
CN = k8s-test-app

[alt_name]
DNS.1 = k8s-test-app.test.k8s.local # 外部アクセス用のホスト名
DNS.2 = k8s-test-app.default.svc.cluster.local # クラスタ内のホスト名
DNS.3 = k8s-test-app # クラスタ内のホスト名 (同一ネームスペース)
IP.1 = 10.0.0.64 # 外部アクセス用のIP
IP.2 = 10.100.133.21 # ClusterIP
IP.3 = 127.0.0.1 # ローカルホストからのアクセス用

ローカルテスト

ディレクトリツリー
/home/hoge/k8s-test-app/podman/
├── certs/                  # TLS証明書(読み取り専用でマウント)
│   ├── tls.crt             # サーバ証明書(PEM形式)
│   └── tls.key             # 秘密鍵(PEM形式)
│
├── secret/                 # 認証用APIキー(読み取り専用でマウント)
│   └── testapp-secret.yaml # APIキー定義ファイル
│
└── cephfs/
    └── k8s-test-app/
        └── log/     # アクセスログ保存先(書き込み可能でマウント)
コンテナ起動
[mng k8s-test-app]$ podman run -d \
  -p 8443:8443 \
  -v /home/hoge/k8s-test-app/podman/certs:/app/certs:ro \
  -v /home/hoge/k8s-test-app/podman/secret:/app/secret:ro \
  -v /home/hoge/k8s-test-app/podman/cephfs/k8s-test-app/log:/mnt/cephfs/k8s-test-app/log \
  k8s-test-app
8c874bf93a201a4ebc9449586d0ad99fd5f84725397304d9282bcb218ee91aa1
起動確認
[mng k8s-test-app]$ podman ps
CONTAINER ID  IMAGE                          COMMAND     CREATED         STATUS         PORTS                             NAMES
8c874bf93a20  localhost/k8s-test-app:latest  ./app       26 seconds ago  Up 26 seconds  0.0.0.0:8443->8443/tcp, 8443/tcp  cranky_lewin

[mng k8s-test-app]$ ss -lnt
State         Recv-Q    Send-Q    Local Address:Port    Peer    Address:Port    Process
...
LISTEN        0          128      0.0.0.0:8443                   0.0.0.0:*
...
APIアクセステスト
[mng k8s-test-app]$ echo 'api_key: "my-super-secret-key"' > ./podman/secret/testapp-secret.yaml

[mng k8s-test-app]$ curl -k -H 'Authorization: TestApp my-super-secret-key' https://localhost:8443/ping |jq
{
  "message": "pong"
}
ログ確認
[mng k8s-test-app]$ podman logs 8c874bf93a20
2025/05/04 01:51:08 INFO: API key successfully loaded
...
[GIN-debug] GET    /ping                     --> main.handlePing (4 handlers)
2025/05/04 01:51:08 INFO: Starting HTTPS server on :8443
2025/05/04 01:52:05 INFO: /ping request
[GIN] 2025/05/04 - 01:52:05 | 200 |     727.947µs |       127.0.0.1 | GET      "/ping"

[mng k8s-test-app]$ cat podman/cephfs/k8s-test-app/log/history.log
2025-05-04T01:40:16Z /ping
コンテナ停止&削除
[mng k8s-test-app]$ podman ps
CONTAINER ID  IMAGE                          COMMAND     CREATED        STATUS        PORTS                             NAMES
8c874bf93a20  localhost/k8s-test-app:latest  ./app       9 minutes ago  Up 9 minutes  0.0.0.0:8443->8443/tcp, 8443/tcp  cranky_lewin

[mng k8s-test-app]$ podman stop 8c874bf93a20
8c874bf93a20

[mng k8s-test-app]$ podman rm 8c874bf93a20
8c874bf93a20

[mng k8s-test-app]$ podman ps -a
CONTAINER ID  IMAGE       COMMAND     CREATED     STATUS      PORTS       NAMES

コンテナイメージをレジストリ(Harbor)へPush

タグ付け
[mng k8s-test-app]$ podman tag k8s-test-app:latest harbor.test.k8s.local/k8s-test-app/k8s-test-app:latest
ログイン
[mng k8s-test-app]$ podman login harbor.test.k8s.local
Username: admin
Password:
Login Succeeded!
Push
[mng k8s-test-app]$ podman push k8s-test-app harbor.test.k8s.local/k8s-test-app/k8s-test-app:latest

DeploymentとPodを一旦削除

削除
[mng k8s-test-app]$ kubectl get deployment
NAME           READY   UP-TO-DATE   AVAILABLE   AGE
k8s-test-app   0/3     3            0           14h

[mng k8s-test-app]$ kubectl delete deployment k8s-test-app
deployment.apps "k8s-test-app" deleted

[mng k8s-test-app]$ kubectl get deployment
No resources found in default namespace.

[mng k8s-test-app]$ kubectl get pods
No resources found in default namespace.

k8s-test-app用サーバ証明書のSecretを作成

Secret定義を作成後に適用
# --dry-runで生成
[mng k8s-test-app]$ kubectl create secret tls k8s-test-app-tls --cert=tls.crt --key=tls.key --namespace=default --dry-run=client -o yaml > k8s-test-app-tls-secret.yaml

[mng k8s-test-app]$ kubectl apply -f k8s-test-app-tls-secret.yaml
k8s-test-app-tls-secret.yaml
apiVersion: v1
data:
  tls.crt: xxx...
  tls.key:  yyy...
kind: Secret
metadata:
  creationTimestamp: null
  name: k8s-test-app-tls
  namespace: default
type: kubernetes.io/tls

※ tls.crtやtls.keyはそれぞれtls.crtファイルとtls.keyファイル内のPEM形式データをさらにBase64エンコードしたものになる点へ留意。「kubectl create secret --dry-run」で自動的にエンコードされる。

k8s-test-app用API-KEYのSecretを作成

適用
[mng k8s-test-app]$ kubectl apply -f k8s-test-app-api-key.yaml
k8s-test-app-api-key.yaml
apiVersion: v1
kind: Secret
metadata:
  name: k8s-test-app-api-key
type: Opaque
stringData:
  testapp-secret.yaml: |
    api_key: "xxx..."

k8s-test-appアプリケーションのPodをクラスタへ再ディプロイ

適用
[mng k8s-test-app]$ kubectl apply -f k8s-test-app-deploy.yaml
k8s-test-app-deploy.yaml (ChatGPTによる解説)
apiVersion: apps/v1               # DeploymentリソースのAPIバージョン(v1)
kind: Deployment                  # リソースの種類:Deployment
metadata:
  name: k8s-test-app              # Deployment名
spec:
  replicas: 3                     # 起動するPodのレプリカ数(3つ)
  selector:
    matchLabels:
      app: k8s-test-app           # 対象のPodを識別するラベル(matchLabelsとtemplateのラベルが一致する必要がある)
  template:
    metadata:
      labels:
        app: k8s-test-app         # Podに付与するラベル(selectorと一致する必要あり)
    spec:
      imagePullSecrets:
        - name: harbor-creds      # イメージプル時の認証情報として使うSecret名(Harborの認証)

      initContainers:
        - name: k8s-test-app-init         # Initコンテナ名
          image: busybox                  # 使用するコンテナイメージ(軽量なBusyBox)
          command:                        # 実行するコマンド
            - sh
            - -c
            - |
              echo "[INIT] Creating log directory...";  # ログ出力(開始通知)
              if mkdir -p /mnt/cephfs/k8s-test-app/log; then  # ログディレクトリ作成
                chmod -R 777 /mnt/cephfs/k8s-test-app;        # 書き込み権限の付与(全ユーザーにフルアクセス)
              else
                echo "[ERROR] Failed to create /mnt/cephfs/k8s-test-app/log" >&2;  # エラーログ
                exit 1;                        # エラー時に終了
              fi
          volumeMounts:
            - name: history-vol              # マウントするボリューム名(PVCと紐づけられる)
              mountPath: /mnt/cephfs         # コンテナ内でマウントされるパス(CephFS全体)

      containers:
        - name: k8s-test-app                 # メインアプリケーションコンテナ名
          image: harbor.test.k8s.local/k8s-test-app/k8s-test-app:latest  # 使用するイメージ(Harborから取得)
          imagePullPolicy: Always            # 常に新しいイメージをPullする設定
          ports:
            - containerPort: 8443            # コンテナ内の待受ポート(HTTPSなどで使われる)
          volumeMounts:
            - name: certs
              mountPath: /app/certs          # TLS証明書格納パス
              readOnly: true                 # 読み取り専用
            - name: api-key
              mountPath: /app/secret         # APIキー格納パス
              readOnly: true
            - name: history-vol
              mountPath: /mnt/cephfs         # CephFSをアプリコンテナにもマウント(ログ出力などで使用)

      volumes:
        - name: certs
          secret:
            secretName: k8s-test-app-tls     # TLS証明書用のSecret名
        - name: api-key
          secret:
            secretName: k8s-test-app-api-key # APIキー用のSecret名
        - name: history-vol
          persistentVolumeClaim:
            claimName: cephfs-pvc            # CephFSのPVC名(共有ストレージ)

k8s-test-appアプリケーションのService(MetalLB)を更新

更新
[mng k8s-test-app]$ kubectl apply -f k8s-test-app-svc.yaml
k8s-test-app-svc.yaml
apiVersion: v1
kind: Service
metadata:
  name: k8s-test-app
spec:
  type: LoadBalancer
  selector:
    app: k8s-test-app
  ports:
    - protocol: TCP # TLSポートへ変更
      port: 443
      targetPort: 8443
確認
[mng k8s-test-app]$ kubectl get svc -o wide
NAME           TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)         AGE   SELECTOR
k8s-test-app   LoadBalancer   10.100.133.21   10.0.0.64     443:32278/TCP   24h   app=k8s-test-app

※ 割り当てられたVIPおよびCLUSTER-IPは変更なし。

テスト実施

外部クライアントからアクセス疎通確認 (管理端末)

※ 事前にlb.test.k8s.localのdnsmasq設定に「k8s-test-app.test.k8s.local: 10.0.0.64」の名前解決設定を追加しておく。

アクセス疎通確認
[mng k8s-test-app]$ curl -v --cacert ../tls/private_ca.crt -H 'Authorization: TestApp xxx...' https://k8s-test-app.test.k8s.local/ping | jq .
*   Trying 10.0.0.64:443...
* Connected to k8s-test-app.test.k8s.local (10.0.0.64) port 443 (#0)
...
*  CAfile: ../tls/private_ca.crt
...
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
...
> GET /ping HTTP/2
> Host: k8s-test-app.test.k8s.local
> user-agent: curl/7.76.1
> accept: */*
> authorization: TestApp xxx...
...
< HTTP/2 200
...
{
  "message": "pong"
}
Podのログ確認
[mng k8s-test-app]$ kubectl get pods -o wide
NAME                            READY   STATUS    RESTARTS   AGE   IP               NODE          NOMINATED NODE   READINESS GATES
k8s-test-app-58dfd654cc-h4cqf   1/1     Running   0          44m   172.30.126.28    k8s-worker2   <none>           <none>
k8s-test-app-58dfd654cc-h5r46   1/1     Running   0          45m   172.23.229.145   k8s-worker0   <none>           <none>
k8s-test-app-58dfd654cc-t5l5w   1/1     Running   0          44m   172.20.194.97    k8s-worker1   <none>           <none>

[mng k8s-test-app]$ kubectl logs k8s-test-app-58dfd654cc-h5r46
ログ出力
Defaulted container "k8s-test-app" out of: k8s-test-app, k8s-test-app-init (init)
2025/05/03 02:42:33 INFO: API key successfully loaded
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /ping                     --> main.handlePing (4 handlers)
2025/05/03 02:42:33 INFO: Starting HTTPS server on :8443
2025/05/03 02:43:59 INFO: /ping request
[GIN] 2025/05/03 - 02:43:59 | 200 |  211.126456ms |      10.0.0.157 | GET      "/ping"

他のPodからアクセス疎通確認

テスト用Podの実行
[mng k8s-test-app]$ kubectl run testpod --image=registry.access.redhat.com/ubi9/ubi-minimal --restart=Never -it -- sh

sh-5.1# microdnf install vim-minimal

# PrivateCA証明書 (PEM形式) をコピペし保存
sh-5.1# vi /tmp/private_ca.crt

# Service名でアクセス疎通
sh-5.1# curl -v --cacert /tmp/private_ca.crt -H 'Authorization: TestApp xxx...' https://k8s-test-app/ping
*   Trying 10.100.133.21:443...
* Connected to k8s-test-app (10.100.133.21) port 443 (#0)
...
{"message":"pong"}

sh-5.1# exit
テスト用Podを削除
[mng k8s-test-app]$ kubectl get pods
NAME                            READY   STATUS      RESTARTS   AGE
...
testpod                         0/1     Completed   0          13m

[mng k8s-test-app]$ kubectl delete pod testpod
pod "testpod" deleted

k8s-test-appgwアプリの作成 (新規)

※ こちらも小さいテストプログラムなのでChatGPTに機能内容を入力して作成。

コード対応
[mng ~]$ mkdir k8s-test-appgw

[mng ~]$ cd k8s-test-appgw

[mng k8s-test-app]$ go mod init test.k8s.local/test-appgw

[mng k8s-test-appgw]$ go mod tidy

[mng k8s-test-appgw]$ go build

[mng k8s-test-appgw]$ ls
go.mod  go.sum  main.go  test-appgw
ソースコード (main.go)
main.go
package main

import (
	"crypto/tls"
	"crypto/x509"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/gin-gonic/gin"
	"gopkg.in/yaml.v2"
)

type Config struct {
	TestAppURL     string `yaml:"testapp_url"`
	CephFSJSONPath string `yaml:"cephfs_json_path"`
	S3             struct {
		Endpoint string `yaml:"endpoint"`
		Region   string `yaml:"region"`
	} `yaml:"s3"`
}

type AuthSecret struct {
	APIKey string `yaml:"api_key"`
}

type S3Secret struct {
	AccessKey       string `yaml:"access_key"`
	SecretAccessKey string `yaml:"secret_access_key"`
}

type TestAppSecret struct {
	APIKey string `yaml:"api_key"`
}

var (
	cfg           Config
	authSecret    AuthSecret
	s3Secret      S3Secret
	testAppSecret TestAppSecret
	caCertPool    *x509.CertPool
	s3Session     *session.Session
)

func main() {
	if err := loadConfig("/app/config/app-config.yaml"); err != nil {
		log.Println("ERROR: config error:", err)
		return
	}
	log.Println("INFO: config loaded")

	if err := loadAuthSecret("/app/secret/auth-secret.yaml"); err != nil {
		log.Println("ERROR: auth secret error:", err)
		return
	}
	log.Println("INFO: auth secret loaded")

	if err := loadS3Secret("/app/secret/s3-secret.yaml"); err != nil {
		log.Println("ERROR: s3 secret error:", err)
		return
	}
	log.Println("INFO: s3 secret loaded")

	if err := loadTestAppSecret("/app/secret/testapp-secret.yaml"); err != nil {
		log.Println("ERROR: testapp secret error:", err)
		return
	}
	log.Println("INFO: testapp secret loaded")

	if err := loadCACert("/app/ca/ca.crt"); err != nil {
		log.Println("ERROR: CA cert error:", err)
		return
	}
	log.Println("INFO: CA cert loaded")

	if err := initS3Session(); err != nil {
		log.Println("ERROR: S3 session error:", err)
		return
	}
	log.Println("INFO: S3 session initialized")

	r := gin.Default()
	r.Use(authMiddleware)

	r.GET("/ping", handlePing)
	r.GET("/json-from-cephfs", handleJsonFromCephfs)
	r.GET("/s3-buckets", handleS3Buckets)

	log.Println("INFO: Starting HTTPS server on :8443")
	err := r.RunTLS(":8443", "/app/certs/tls.crt", "/app/certs/tls.key")
	if err != nil {
		log.Println("ERROR: RunTLS error:", err)
		return
	}
}

func loadConfig(path string) error {
	data, err := os.ReadFile(path)
	if err != nil {
		log.Println("ERROR: read error:", err)
		return fmt.Errorf("read error: %w", err)
	}
	if err := yaml.Unmarshal(data, &cfg); err != nil {
		log.Println("ERROR: config unmarshal error:", err)
		return fmt.Errorf("unmarshal error: %w", err)
	}
	return nil
}

func loadAuthSecret(path string) error {
	data, err := os.ReadFile(path)
	if err != nil {
		log.Println("ERROR: read error:", err)
		return fmt.Errorf("read error: %w", err)
	}
	if err := yaml.Unmarshal(data, &authSecret); err != nil {
		log.Println("ERROR: auth secret unmarshal error:", err)
		return fmt.Errorf("unmarshal error: %w", err)
	}
	return nil
}

func loadS3Secret(path string) error {
	data, err := os.ReadFile(path)
	if err != nil {
		log.Println("ERROR: read error:", err)
		return fmt.Errorf("read error: %w", err)
	}
	if err := yaml.Unmarshal(data, &s3Secret); err != nil {
		log.Println("ERROR: s3 secret unmarshal error:", err)
		return fmt.Errorf("unmarshal error: %w", err)
	}
	return nil
}

func loadTestAppSecret(path string) error {
	data, err := os.ReadFile(path)
	if err != nil {
		log.Println("ERROR: read error:", err)
		return fmt.Errorf("read error: %w", err)
	}
	if err := yaml.Unmarshal(data, &testAppSecret); err != nil {
		log.Println("ERROR: testapp secret unmarshal error:", err)
		return fmt.Errorf("unmarshal error: %w", err)
	}
	return nil
}

func loadCACert(caPath string) error {
	caCertPool = x509.NewCertPool()
	caCert, err := os.ReadFile(caPath)
	if err != nil {
		log.Println("ERROR: read error:", err)
		return fmt.Errorf("read error: %w", err)
	}
	if ok := caCertPool.AppendCertsFromPEM(caCert); !ok {
		log.Println("ERROR: CA cert append error")
		return fmt.Errorf("failed to append CA cert")
	}
	return nil
}

func initS3Session() error {
	sess, err := session.NewSession(&aws.Config{
		Region:           aws.String(cfg.S3.Region),
		Endpoint:         aws.String(cfg.S3.Endpoint),
		S3ForcePathStyle: aws.Bool(true),
		Credentials: credentials.NewStaticCredentials(
			s3Secret.AccessKey,
			s3Secret.SecretAccessKey,
			"",
		),
		HTTPClient: &http.Client{
			Transport: &http.Transport{
				TLSClientConfig: &tls.Config{RootCAs: caCertPool},
			},
		},
	})
	if err != nil {
		log.Println("ERROR: session error:", err)
		return fmt.Errorf("session error: %w", err)
	}
	s3Session = sess
	return nil
}

func appendAccessLog(path string, route string) {
	f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		log.Println("ERROR: log open error:", err)
		return
	}
	defer f.Close()
	fmt.Fprintf(f, "%s %s\n", time.Now().Format(time.RFC3339), route)
}

func authMiddleware(c *gin.Context) {
	header := c.GetHeader("Authorization")
	expected := fmt.Sprintf("TestApp %s", authSecret.APIKey)
	if header != expected {
		log.Println("WARN: unauthorized request")
		c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
		return
	}
	c.Next()
}

func handlePing(c *gin.Context) {
	log.Println("INFO: /ping request")
	appendAccessLog("/mnt/cephfs/k8s-test-appgw/log/history.log", c.FullPath())

	client := &http.Client{
		Transport: &http.Transport{
			TLSClientConfig: &tls.Config{RootCAs: caCertPool},
		},
	}

	req, err := http.NewRequest("GET", cfg.TestAppURL, nil)
	if err != nil {
		log.Println("ERROR: /ping create request error:", err)
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create request"})
		return
	}

	req.Header.Set("Authorization", fmt.Sprintf("TestApp %s", testAppSecret.APIKey))

	resp, err := client.Do(req)
	if err != nil {
		log.Println("ERROR: /ping forwarding error:", err)
		c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
		return
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), body)
}

func handleJsonFromCephfs(c *gin.Context) {
	log.Println("INFO: /json-from-cephfs request")
	appendAccessLog("/mnt/cephfs/k8s-test-appgw/log/history.log", c.FullPath())

	data, err := os.ReadFile(cfg.CephFSJSONPath)
	if err != nil {
		log.Println("ERROR: /json-from-cephfs read error:", err)
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read file"})
		return
	}
	var parsed interface{}
	if err := json.Unmarshal(data, &parsed); err != nil {
		log.Println("ERROR: /json-from-cephfs unmarshal error:", err)
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid json"})
		return
	}
	c.JSON(http.StatusOK, parsed)
}

func handleS3Buckets(c *gin.Context) {
	log.Println("INFO: /s3-buckets request")
	appendAccessLog("/mnt/cephfs/k8s-test-appgw/log/history.log", c.FullPath())

	svc := s3.New(s3Session)
	out, err := svc.ListBuckets(&s3.ListBucketsInput{})
	if err != nil {
		log.Println("ERROR: /s3-buckets list error:", err)
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusOK, out.Buckets)
}

コンテナイメージを作成

ビルド
[mng k8s-test-appgw]$ podman build -t k8s-test-appgw:latest -f Containerfile .

[mng k8s-test-appgw]$ podman images
REPOSITORY                                       TAG         IMAGE ID      CREATED         SIZE
localhost/k8s-test-appgw                         latest      434e548425a4  14 minutes ago  235 MB
<none>                                           <none>      441699e1d862  14 minutes ago  1.95 GB
<none>                                           <none>      9cf798a71325  34 minutes ago  1.13 GB
localhost/k8s-test-app                           latest      3a2c94adf05e  14 hours ago    229 MB
harbor.test.k8s.local/k8s-test-app/k8s-test-app  latest      3a2c94adf05e  14 hours ago    229 MB
<none>                                           <none>      0c95e1761eb6  14 hours ago    1.53 GB
<none>                                           <none>      cadb179cb435  14 hours ago    229 MB
<none>                                           <none>      9d4640aa3552  14 hours ago    1.53 GB
<none>                                           <none>      1aba92ed3d43  42 hours ago    229 MB
<none>                                           <none>      ff57c1ac1fe0  42 hours ago    1.53 GB
<none>                                           <none>      3932eb3d7723  42 hours ago    1.38 GB
<none>                                           <none>      384b325ddbb4  42 hours ago    1.13 GB
registry.access.redhat.com/ubi9/go-toolset       1.23        d8663eae6e1a  7 days ago      1.13 GB
registry.access.redhat.com/ubi9                  latest      18ac20acd5ec  2 weeks ago     217 MB
Containerfile
# Stage 1: Build
FROM registry.access.redhat.com/ubi9/go-toolset:1.23 AS builder

USER root
WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o app

# Stage 2: Runtime
FROM registry.access.redhat.com/ubi9

WORKDIR /app
COPY --from=builder /app/app .

EXPOSE 8443
CMD ["./app"]

k8s-test-appgw用のサーバ証明書の作成

[mng k8s-test-appgw]$ openssl genpkey -algorithm ec -pkeyopt ec_paramgen_curve:P-256 -out tls.key

[mng k8s-test-appgw]$ openssl req -new -key tls.key -out tls.csr -config openssl.cnf

[mng k8s-test-appgw]$  openssl x509 -req -in tls.csr -CA ../tls/private_ca.crt -CAkey ../tls/private_ca.key -CAcreateserial -out tls.crt -days 365 -sha256 -extfile openssl.cnf -extensions server-cert
Certificate request self-signature ok
subject=C=JP, O=k8s, OU=test, CN=k8s-test-appgw

[mng k8s-test-appgw]$  openssl x509 -in tls.crt -text -noout
Certificate:
    Data:
        Version: 3 (0x2)
...
        Issuer: CN=private_ca
...
        Subject: C=JP, O=k8s, OU=test, CN=k8s-test-appgw
...
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment, Key Agreement
            X509v3 Extended Key Usage:
                TLS Web Server Authentication
            X509v3 Subject Alternative Name:
                DNS:k8s-test-appgw.test.k8s.local, DNS:k8s-test-appgw.default.svc.cluster.local, DNS:k8s-test-appgw, IP Address:127.0.0.1
...

※ tls.crt(証明書)とtls.key(秘密鍵)というファイルが生成される。

SAN(alt_name)をService(MetalLB)あくせすを想定して合わせてホスト名で指定しておく。ただし、まだディプロイしていないのでVIPとClusterIPについては不明。後ほど証明書を更新する。

openssl.cnf
[server-cert]
keyUsage = critical, digitalSignature, keyEncipherment, keyAgreement
extendedKeyUsage = serverAuth
subjectAltName = @alt_name

[req]
distinguished_name = dn
prompt = no

[dn]
C = JP
O = k8s
OU = test
CN = k8s-test-appgw

[alt_name]
DNS.1 = k8s-test-appgw.test.k8s.local
DNS.2 = k8s-test-appgw.default.svc.cluster.local
DNS.3 = k8s-test-appgw
IP.1 = 127.0.0.1

コンテナイメージをレジストリ(Harbor)へPush

タグ付け
[mng k8s-test-appgw]$ podman tag k8s-test-appgw:latest harbor.test.k8s.local/k8s-test-app/k8s-test-appgw:latest

[mng k8s-test-appgw]$ podman images
REPOSITORY                                         TAG         IMAGE ID      CREATED         SIZE
localhost/k8s-test-appgw                           latest      434e548425a4  15 minutes ago  235 MB
harbor.test.k8s.local/k8s-test-app/k8s-test-appgw  latest      434e548425a4  15 minutes ago  235 MB
...
ログイン
[mng k8s-test-appgw]$ podman login harbor.test.k8s.local
Username: admin
Password:
Login Succeeded!
Push
[mng k8s-test-appgw]$ podman push k8s-test-appgw harbor.test.k8s.local/k8s-test-app/k8s-test-appgw:latest

Helmチャートの作成

k8s-test-appアプリケーションは個別にリソースを適用したが、別のやり方としてこのアプリケーションではHelmで一括インストールしてみる。

Helmチャートのテンプレート生成
[mng k8s-test-appgw]$ helm create k8s-test-appgw-helm

[mng k8s-test-appgw]$ tree k8s-test-appgw-helm
k8s-test-appgw-helm/
 +-- charts
 +-- Chart.yaml
 +-- values.yaml
 +-- templates
      +-- deployment.yaml
      +-- _helpers.tpl
      +-- hpa.yaml
      +-- ingress.yaml
      +-- NOTES.txt
      +-- serviceaccount.yaml
      +-- service.yaml
      +-- tests
           +-- test-connection.yaml

テンプレートをベースに以下のように作成する。

Helmチャートを作成
[mng k8s-test-appgw]$ k8s-test-appgw-helm/
 +-- charts
 +-- Chart.yaml
 +-- values.yaml
 +-- templates
      +-- configmap.yaml
      +-- deployment.yaml
      +-- secret.yaml
      +-- service.yaml
      +-- tls-secret.yaml
      +-- _helpers.tpl
      +-- tests
Chart.yaml
apiVersion: v2
name: k8s-test-appgw
version: 0.1.0
description: A Helm chart for k8s-test-appgw
appVersion: "1.0.0"
サーバ証明書と秘密鍵をBase64エンコード
[mng k8s-test-appgw]$ echo "crt: \"$(base64 -w 0 tls.crt)\""
crt: "LS0tLS1CRUdJ...=="

[mng k8s-test-appgw]$ echo echo "key: \"$(base64 -w 0 tls.key)\"""
key: "LS0...=="

※ 上記エンコードされた値を以下のvalues.yaml内で指定する。

  • tls.crtフィールド
  • tls.keyフィールド
values.yaml (ChatGPTの解説コメント)
# 起動するPodのレプリカ数
replicaCount: 3

image:
  # 使用するアプリケーションのコンテナイメージ(Harborから取得)
  repository: harbor.test.k8s.local/k8s-test-app/k8s-test-appgw
  # イメージの取得ポリシー(Alwaysにより毎回Pull)
  pullPolicy: Always
  # 使用するイメージのタグ(latestなど)
  tag: latest

service:
  # Serviceのタイプ(LoadBalancerによって外部に公開)
  type: LoadBalancer
  # Serviceがリッスンするポート番号
  port: 443

authSecret:
  # 外部アクセス用APIキー(Authorization: TestApp XXX)
  api_key: "xxx..."

testappSecret:
  # 内部連携先(k8s-test-app)へのアクセス用APIキー
  api_key: "xxx..."

s3Secret:
  # Ceph RGW S3アクセスに使用するアクセスキー
  access_key: "xxxxxx..."
  # S3のシークレットキー(アクセスキーとペア)
  secret_access_key: "yyyyyy..."

config:
  # k8s-test-app への疎通確認URL(/pingエンドポイント)
  testapp_url: "https://k8s-test-app/ping"
  # CephFS上のJSONファイルパス(APIが応答する内容)
  cephfs_json_path: "/mnt/cephfs/k8s-test-appgw/data.json"
  s3:
    # S3互換のCeph RGWエンドポイントURL
    endpoint: "https://rgw.test.k8s.local"
    # S3 APIで使うリージョン(Ceph側の設定に合わせる)
    region: "us-east-1"

tls:
  # サーバ証明書(k8s-test-appgw)のBase64エンコード文字列(Helm valuesに埋め込む形式)
  # コマンド例: $ echo "crt: \"$(base64 -w 0 tls.crt)\""
  crt: "xxxx... (PEM形式をBase64エンコード)"
  # サーバ秘密鍵(k8s-test-appgw)のBase64エンコード文字列
  # コマンド例: $ echo "key: \"$(base64 -w 0 tls.key)\""
  key: "yyyy... (PEM形式をBase64エンコード)"

# PrivateCA証明書(PEM形式を直接埋め込む)
caBundle: |
  -----BEGIN CERTIFICATE-----
  MIIBjz... (PEM形式)
  -----END CERTIFICATE-----

persistence:
  # 使用する既存のPersistentVolumeClaim名(CephFS)
  existingClaim: cephfs-pvc

resources:
  limits:
    # 最大CPU使用量(例: 0.5コア)
    cpu: 500m
    # 最大メモリ使用量(例: 256MiB)
    memory: 256Mi
  requests:
    # 最小確保CPU(例: 0.1コア)
    cpu: 100m
    # 最小確保メモリ(例: 128MiB)
    memory: 128Mi
configmap.yaml
# アプリケーション設定
apiVersion: v1
kind: ConfigMap
metadata:
  name: k8s-test-appgw-config
  labels:
    app: k8s-test-appgw
data:
  app-config.yaml: |
    testapp_url: {{ .Values.config.testapp_url | quote }} # 連携先k8s-test-appアプリのURL(APIパス)
    cephfs_json_path: {{ .Values.config.cephfs_json_path | quote }} # API応答するJsonデータファイル
    s3:
      endpoint: {{ .Values.config.s3.endpoint | quote }} # RGW/S3エンドポイント
      region: {{ .Values.config.s3.region | quote }} # RG/S3リージョン ("us-east-1")
---
# PrivateCA証明書を設定
apiVersion: v1
kind: ConfigMap
metadata:
  name: rgw-ca-cert
  labels:
    app: k8s-test-appgw
data:
  ca.crt: |
    {{ .Values.caBundle | nindent 4 }}
deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: k8s-test-appgw
  labels:
    app: k8s-test-appgw
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app: k8s-test-appgw
  template:
    metadata:
      labels:
        app: k8s-test-appgw
    spec:
      imagePullSecrets:
        - name: harbor-creds

      initContainers:
        - name: k8s-test-appgw-init
          image: busybox
          command:
            - sh
            - -c
            - |
              DIR=/mnt/cephfs/k8s-test-appgw
              FILE="$DIR/data.json"
              if [ ! -f "$FILE" ]; then
                if mkdir -p "$DIR"; then
                  echo '{"message": "Hello, world"}' > "$FILE"
                  chmod 644 "$FILE"
                  echo "[INIT] data.json created at $FILE"
                else
                  echo "[ERROR] Failed to create directory $DIR" >&2
                  exit 1
                fi
              else
                echo "[INIT] $FILE already exists. Skipping creation."
              fi
              echo "[INIT] Creating log directory...";
              LOGDIR="$DIR/log"
              if mkdir -p "$LOGDIR"; then
                chmod -R 777 "$DIR";
              else
                echo "[ERROR] Failed to create " "$LOGDIR" >&2;
                exit 1;
              fi
          volumeMounts:
            - name: history-vol
              mountPath: /mnt/cephfs

      containers:
        - name: app
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - containerPort: 8443
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          volumeMounts:
            - name: config-vol
              mountPath: /app/config
              readOnly: true
            - name: secret-vol
              mountPath: /app/secret
              readOnly: true
            - name: tls-vol
              mountPath: /app/certs
              readOnly: true
            - name: history-vol
              mountPath: /mnt/cephfs
            - name: ca
              mountPath: /app/ca/ca.crt
              subPath: ca.crt

      volumes:
        - name: config-vol
          configMap:
            name: k8s-test-appgw-config
        - name: secret-vol
          secret:
            secretName: k8s-test-appgw-secret
        - name: tls-vol
          secret:
            secretName: k8s-test-appgw-tls
        - name: history-vol
          persistentVolumeClaim:
            claimName: {{ .Values.persistence.existingClaim }}
        - name: ca
          configMap:
            name: rgw-ca-cert
            items:
              - key: ca.crt
                path: ca.crt
secret.yaml
# 認証情報を設定
apiVersion: v1
kind: Secret
metadata:
  name: k8s-test-appgw-secret
  labels:
    app: k8s-test-appgw
type: Opaque
stringData:
  # 本アプリケーションのAPI-KEY
  auth-secret.yaml: |
    api_key: {{ .Values.authSecret.api_key | quote }}
  # 連携先k8s-test-appアプリのAPI-KEY
  testapp-secret.yaml: |
    api_key: {{ .Values.testappSecret.api_key | quote }}
  # 連携先RGW/S3 APIの認証情報 (aws-cliのプロファイルど同じ値)
  s3-secret.yaml: |
    access_key: {{ .Values.s3Secret.access_key | quote }} # アクセスID
    secret_access_key: {{ .Values.s3Secret.secret_access_key | quote }} # シークレットアクセスキー
service.yaml
apiVersion: v1
kind: Service
metadata:
  name: k8s-test-appgw
  labels:
    app: k8s-test-appgw
spec:
  type: {{ .Values.service.type }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: 8443
  selector:
    app: k8s-test-appgw
tls-secret.yaml
# 本アプリケーションのサーバ証明書と秘密鍵データ設定
apiVersion: v1
kind: Secret
metadata:
  name: k8s-test-appgw-tls
  labels:
    app: k8s-test-appgw
type: kubernetes.io/tls
data:
  tls.crt: {{ .Values.tls.crt | quote }}
  tls.key: {{ .Values.tls.key | quote }}
_helpers.tpl (未利用)
{{- define "k8s-test-appgw.fullname" -}}
{{ .Release.Name }}
{{- end -}}

Tipsメモ

(1) SecretとConfigMap における証明書などの認証情報の記述形式の違い

リソースタイプ 値の形式 エンコードの要否
Secret(type: kubernetes.io/tls) data: フィールド PEM形式やjson形式からbase64エンコードが必要
Secret(type: Opaque`) data: フィールド PEM形式やjson形式からbase64エンコードが必要
ConfigMap data: フィールド PEM形式やjson形式のままでOK

(2) 他のネームスペースのConfigMap/Secretは参照できないのでコピーするなどが必要

(3) アプリケーション(Deployment定義)間でマウントするボリュームのディレクトリパスやファイルパスがオーバーラップするなど不整合とならないこと。共有ファイルシステムの場合にルートパス名を一致させること。

(4) Podの起動エラー時には以下で確認

$ kubectl describe pod ポッド名
$ kubectl logs pod ポッド名

(5) Helmでアンインストール

$ helm uninstall k8s-test-appgw --namespace default

Helmでインストール

インストール
[mng k8s-test-appgw]$ helm install k8s-test-appgw ./k8s-test-appgw-helm --namespace default --create-namespace
NAME: k8s-test-appgw
LAST DEPLOYED: Wed May 04 20:59:46 2025
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
確認
[mng k8s-test-appgw]$ kubectl get all -l app=k8s-test-appgw -o wide
NAME                                  READY   STATUS    RESTARTS   AGE    IP               NODE          NOMINATED NODE   READINESS GATES
pod/k8s-test-appgw-75d7898466-6bdzn   1/1     Running   0          115s   172.23.229.131   k8s-worker0   <none>           <none>
pod/k8s-test-appgw-75d7898466-jwlgx   1/1     Running   0          115s   172.20.194.102   k8s-worker1   <none>           <none>
pod/k8s-test-appgw-75d7898466-nwc5g   1/1     Running   0          115s   172.30.126.7     k8s-worker2   <none>           <none>

NAME                     TYPE           CLUSTER-IP    EXTERNAL-IP   PORT(S)         AGE    SELECTOR
service/k8s-test-appgw   LoadBalancer   10.98.206.4   10.0.0.65     443:30695/TCP   116s   app=k8s-test-appgw

NAME                             READY   UP-TO-DATE   AVAILABLE   AGE    CONTAINERS   IMAGES                                                     SELECTOR
deployment.apps/k8s-test-appgw   3/3     3            3           116s   app          harbor.test.k8s.local/k8s-test-app/k8s-test-appgw:latest   app=k8s-test-appgw

NAME                                        DESIRED   CURRENT   READY   AGE    CONTAINERS   IMAGES                                                     SELECTOR
replicaset.apps/k8s-test-appgw-75d7898466   3         3         3       116s   app          harbor.test.k8s.local/k8s-test-app/k8s-test-appgw:latest   app=k8s-test-appgw,pod-template-hash=75d7898466

[mng k8s-test-appgw]$ kubectl get secret -l app=k8s-test-appgw -o wide
NAME                    TYPE                DATA   AGE
k8s-test-appgw-secret   Opaque              3      3m6s
k8s-test-appgw-tls      kubernetes.io/tls   2      3m6s

[mng k8s-test-appgw]$ kubectl get cm -l app=k8s-test-appgw -o wide
NAME                    DATA   AGE
k8s-test-appgw-config   1      3m13s
rgw-ca-cert             1      3m13s

※Service (MetalLB)によりVIPは「10.0.0.65」が割り当てられている。またClusterIPは「10.98.206.4」になっている。

テスト実施

外部クライアントからアクセス疎通確認 (管理端末)

※ 事前にlb.test.k8s.localのdnsmasq設定に「k8s-test-appgw.test.k8s.local: 10.0.0.65」の名前解決設定を追加しておく。

APIアクセス確認 (/ping)
[mng ~]$ curl --cacert tls/private_ca.crt -H 'Authorization: TestApp xxx...'
https://k8s-test-appgw.test.k8s.local/ping | jq .
{
  "message": "pong"
}
APIアクセス確認 (/json-from-cephf)
[mng ~]$  curl --cacert ../tls/private_ca.crt -H 'Authorization: TestApp xxx...' https://k8s-test-appgw.test.k8s.local/json-from-cephfs | jq .
{
  "message": "Hello, world"
}
APIアクセス確認 (/s3-buckets)
[mng ~]$ curl --cacert ../tls/private_ca.crt -H 'Authorization: TestApp xxx...' https://k8s-test-appgw.test.k8s.local/s3-buckets | jq .
[
  {
    "CreationDate": "2025-05-03T06:13:39.058Z",
    "Name": "bucket-for-testuser1"
  }
]

他のPodからアクセス疎通確認

テスト用Podの実行
[mng ~]$ kubectl run testpod --image=registry.access.redhat.com/ubi9/ubi-minimal --restart=Never -it -- sh

sh-5.1# microdnf install vim-minimal

# PrivateCA証明書 (PEM形式) をコピペし保存
sh-5.1# vi /tmp/private_ca.crt

# Service名でアクセス疎通
sh-5.1# curl --cacert /tmp/private_ca.crt -H 'Authorization: TestApp xxx...' https://k8s-test-appgw/ping
{"message":"pong"}

# Service名(内部FQDN)でアクセス疎通
sh-5.1# curl --cacert /tmp/private_ca.crt -H 'Authorization: TestApp xxx...' https://k8s-test-appgw.default.svc.cluster.local/json-from-cephfs
{"message":"Hello, world"}

# ホスト名(外部FQND)でアクセス疎通
sh-5.1# curl --cacert /tmp/private_ca.crt -H 'Authorization: TestApp xxx...' https://k8s-test-appgw.test.k8s.local/s3-buckets
[{"CreationDate":"2025-05-09T06:13:39.058Z","Name":"bucket-for-testuser1"}]

sh-5.1# exit
テスト用Podを削除
[mng ~]$ kubectl get pods
NAME                            READY   STATUS      RESTARTS   AGE
...
testpod                         0/1     Completed   0          13m

[mng ~]$ kubectl delete pod testpod
pod "testpod" deleted

Helmでディプロイを更新 (サーバ証明書だけを更新)

有効期限切れなどのタイミングでサーバ証明書だけを更新するシナリオを想定した操作をやってみる。

今回はService(MetalLB)により新たに割り当てられたVIPやClusterIPをSANに追加するためサーバ証明書を更新する作業内容で実施する。

k8s-test-appgw用のサーバ証明書を再作成

[mng k8s-test-appgw]$ openssl req -new -key tls.key -out tls.csr -config openssl.cnf

[mng k8s-test-appgw]$ openssl x509 -req -in tls.csr -CA ../tls/private_ca.crt -CAkey ../tls/private_ca.key -CAcreateserial -out tls.crt -days 365 -sha256 -extfile openssl.cnf -extensions server-cert
Certificate request self-signature ok
subject=C=JP, O=k8s, OU=test, CN=k8s-test-appgw

[mng k8s-test-appgw]$ openssl x509 -in tls.crt -text -noout
Certificate:
    Data:
        Version: 3 (0x2)
...
        Issuer: CN=private_ca
...
        Subject: C=JP, O=k8s, OU=test, CN=k8s-test-appgw
...
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment, Key Agreement
            X509v3 Extended Key Usage:
                TLS Web Server Authentication
            X509v3 Subject Alternative Name: ★
                DNS:k8s-test-appgw.test.k8s.local, DNS:k8s-test-appgw.default.svc.cluster.local, DNS:k8s-test-appgw, IP Address:127.0.0.1, IP Address:10.0.0.65, IP Address:10.98.206.4 ★
...

※ tls.crt(証明書)というファイルが生成される。秘密鍵ファイル(tls.key)は前回作成したものを使い続ける。

SAN(alt_name)をService(MetalLB)で割り当てられたVIPとClusterIPへ合わせて指定しておく。

openssl.cnf
[server-cert]
keyUsage = critical, digitalSignature, keyEncipherment, keyAgreement
extendedKeyUsage = serverAuth
subjectAltName = @alt_name

[req]
distinguished_name = dn
prompt = no

[dn]
C = JP
O = k8s
OU = test
CN = k8s-test-appgw

[alt_name]
DNS.1 = k8s-test-appgw.test.k8s.local
DNS.2 = k8s-test-appgw.default.svc.cluster.local
DNS.3 = k8s-test-appgw
IP.1 = 127.0.0.1
IP.2 = 10.0.0.65 # ★VIP追加
IP.3 = 10.98.206.4 # ★ClusterIP追加

更新前のステータスを確認

更新前のステータス
[mng k8s-test-appgw]$ kubectl get all -l app=k8s-test-appgw -o wide
NAME                                  READY   STATUS    RESTARTS   AGE    IP               NODE          NOMINATED NODE   READINESS GATES
pod/k8s-test-appgw-75d7898466-6bdzn   1/1     Running   0          101m   172.23.229.131   k8s-worker0   <none>           <none>
pod/k8s-test-appgw-75d7898466-jwlgx   1/1     Running   0          101m   172.20.194.102   k8s-worker1   <none>           <none>
pod/k8s-test-appgw-75d7898466-nwc5g   1/1     Running   0          101m   172.30.126.7     k8s-worker2   <none>           <none>

NAME                     TYPE           CLUSTER-IP    EXTERNAL-IP   PORT(S)         AGE    SELECTOR
service/k8s-test-appgw   LoadBalancer   10.98.206.4   10.0.0.65     443:30695/TCP   101m   app=k8s-test-appgw

NAME                             READY   UP-TO-DATE   AVAILABLE   AGE    CONTAINERS   IMAGES                                                     SELECTOR
deployment.apps/k8s-test-appgw   3/3     3            3           101m   app          harbor.test.k8s.local/k8s-test-app/k8s-test-appgw:latest   app=k8s-test-appgw

NAME                                        DESIRED   CURRENT   READY   AGE    CONTAINERS   IMAGES                                                     SELECTOR
replicaset.apps/k8s-test-appgw-75d7898466   3         3         3       101m   app          harbor.test.k8s.local/k8s-test-app/k8s-test-appgw:latest   app=k8s-test-appgw,pod-template-hash=75d7898466

[mng k8s-test-appgw]$ kubectl get secret -l app=k8s-test-appgw -o wide
NAME                    TYPE                DATA   AGE
k8s-test-appgw-secret   Opaque              3      102m
k8s-test-appgw-tls      kubernetes.io/tls   2      102m

[mng k8s-test-appgw]$ kubectl get cm -l app=k8s-test-appgw -o wide
NAME                    DATA   AGE
k8s-test-appgw-config   1      102m
rgw-ca-cert             1      102m
更新前のサーバ証明書Secret
[mng k8s-test-appgw]$ kubectl get secret k8s-test-appgw-tls -o yaml
apiVersion: v1
data:
  tls.crt: LS0tL...==
  tls.key: LS...==
kind: Secret
metadata:
  annotations:
    meta.helm.sh/release-name: k8s-test-appgw
    meta.helm.sh/release-namespace: default
  creationTimestamp: "2025-05-04T11:59:47Z"
  labels:
    app: k8s-test-appgw
    app.kubernetes.io/managed-by: Helm
  name: k8s-test-appgw-tls
  namespace: default
  resourceVersion: "847995"
  uid: 795eed5e-96e6-4a00-9328-00e03bbe530c
type: kubernetes.io/tls

更新する情報だけのvalues定義ファイルを作成

サーバ証明書をBase64エンコード
[mng k8s-test-appgw]$ echo "crt: \"$(base64 -w 0 tls.crt)\""
crt: "LS0tL..."
values-cert-update.yaml
tls:
  # 新しいTLS証明書(PEM形式をBase64エンコード)
  crt: "LS0tL..."
  key: "LS..." # 秘密鍵は変更なし。前回と同じものを指定する

※ サーバ証明書だけ更新するのでtlsフィールドだけ定義する。秘密鍵フィールド(tls.key)にも(変更はしていないが)指定しないとSecretの変更と認識されない模様なので両方指定しておく。

Helmで更新を実行

Helmで更新
[mng k8s-test-appgw]$ helm upgrade k8s-test-appgw ./k8s-test-appgw-helm --namespace default --reuse-values -f values-cert-update.yaml
Release "k8s-test-appgw" has been upgraded. Happy Helming!
NAME: k8s-test-appgw
LAST DEPLOYED: Wed May 04 22:46:23 2025
NAMESPACE: default
STATUS: deployed
REVISION: 2 ★ アップしている
TEST SUITE: None

接続確認テスト (1) - 失敗

管理端末からアクセス確認 (失敗)
[mng k8s-test-appgw]$ openssl s_client -connect 10.0.0.65:443 -showcerts </dev/null |openssl x509 -noout -text
Connecting to 10.0.0.65
...
Certificate:
    Data:
        Version: 3 (0x2)
...
        Issuer: CN=private_ca
...
        Subject: C=JP, O=k8s, OU=test, CN=k8s-test-appgw
...
        X509v3 extensions:
...
            X509v3 Subject Alternative Name: ★更新されていない
                DNS:k8s-test-appgw.test.k8s.local, DNS:k8s-test-appgw.default.svc.cluster.local, DNS:k8s-test-appgw, IP Address:127.0.0.1 ★
...

※ SubjectAltNameが更新されていないので新しい証明書が適用されていない模様。

Helm upgradeしただけではPodは再起動されていない模様
[mng k8s-test-appgw]$ kubectl get all -l app=k8s-test-appgw -o wide
NAME                                  READY   STATUS    RESTARTS   AGE    IP               NODE          NOMINATED NODE   READINESS GATES
pod/k8s-test-appgw-75d7898466-6bdzn   1/1     Running   0          110m   172.23.229.131   k8s-worker0   <none>           <none>
pod/k8s-test-appgw-75d7898466-jwlgx   1/1     Running   0          110m   172.20.194.102   k8s-worker1   <none>           <none>
pod/k8s-test-appgw-75d7898466-nwc5g   1/1     Running   0          110m   172.30.126.7     k8s-worker2   <none>           <none>

NAME                     TYPE           CLUSTER-IP    EXTERNAL-IP   PORT(S)         AGE    SELECTOR
service/k8s-test-appgw   LoadBalancer   10.98.206.4   10.0.0.65     443:30695/TCP   110m   app=k8s-test-appgw

NAME                             READY   UP-TO-DATE   AVAILABLE   AGE    CONTAINERS   IMAGES                                                     SELECTOR
deployment.apps/k8s-test-appgw   3/3     3            3           110m   app          harbor.test.k8s.local/k8s-test-app/k8s-test-appgw:latest   app=k8s-test-appgw

NAME                                        DESIRED   CURRENT   READY   AGE    CONTAINERS   IMAGES                                                     SELECTOR
replicaset.apps/k8s-test-appgw-75d7898466   3         3         3       110m   app          harbor.test.k8s.local/k8s-test-app/k8s-test-appgw:latest   app=k8s-test-appgw,pod-template-hash=75d7898466
Podを再起動を手動で実行
[mng k8s-test-appgw]$ kubectl rollout restart deployment k8s-test-appgw

接続確認テスト (2) - 成功

Podの再起動(再作成)を確認
[mng k8s-test-appgw]$ kubectl get pods -o wide
NAME                              READY   STATUS    RESTARTS   AGE    IP               NODE          NOMINATED NODE   READINESS GATES
...
k8s-test-appgw-c4d4bb56c-qxxs4   1/1     Running   0          29s     172.20.194.84    k8s-worker1   <none>           <none>
k8s-test-appgw-c4d4bb56c-wf6t4   1/1     Running   0          37s     172.30.126.45    k8s-worker2   <none>           <none>
k8s-test-appgw-c4d4bb56c-wh7j9   1/1     Running   0          19s     172.23.229.181   k8s-worker0   <none>           <none>
管理端末からアクセスを再確認 (成功!)
[mng k8s-test-appgw]$ openssl s_client -connect 10.0.0.65:443 -showcerts </dev/null |openssl x509 -noout -text
Connecting to 10.0.0.65
Can't use SSL_get_servername
depth=0 C=JP, O=k8s, OU=test, CN=k8s-test-appgw
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 C=JP, O=k8s, OU=test, CN=k8s-test-appgw
verify error:num=21:unable to verify the first certificate
verify return:1
depth=0 C=JP, O=k8s, OU=test, CN=k8s-test-appgw
verify return:1
DONE
Certificate:
    Data:
        Version: 3 (0x2)
...
        Issuer: CN=private_ca
...
        Subject: C=JP, O=k8s, OU=test, CN=k8s-test-appgw
...
        X509v3 extensions:
...
            X509v3 Subject Alternative Name: ★ VIP/ClusterIPが追加されている
                DNS:k8s-test-appgw.test.k8s.local, DNS:k8s-test-appgw.default.svc.cluster.local, DNS:k8s-test-appgw, IP Address:127.0.0.1, IP Address:10.0.0.65, IP Address:10.98.206.4 ★
...

APIアクセス確認

外部クライアントからアクセス疎通確認 (管理端末)

※ URLのホスト名にVIPを指定して確認する。

APIアクセス確認 (/ping)
[mng ~]$ curl --cacert tls/private_ca.crt -H 'Authorization: TestApp xxx...' https://10.0.0.65/ping | jq .
{
  "message": "pong"
}
APIアクセス確認 (/json-from-cephf)
[mng ~]$ curl --cacert ../tls/private_ca.crt -H 'Authorization: TestApp xxx...' https://10.0.0.65/json-from-cephfs | jq .
{
  "message": "Hello, world"
}
APIアクセス確認 (/s3-buckets)
[mng ~]$ curl --cacert ../tls/private_ca.crt -H 'Authorization: TestApp xxx...' https://10.0.0.65/s3-buckets | jq .
[
  {
    "CreationDate": "2025-05-03T06:13:39.058Z",
    "Name": "bucket-for-testuser1"
  }
]
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?