はじめに
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ゲートウェイサービスをディプロイして結合テストを実施してみる。
開発するテストアプリケーション
テストアプリ名 | 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)
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
# 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)の(前回に割り当てられた)情報へ合わせて指定しておく。
[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:*
...
[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!
[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を作成
# --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
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
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
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
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"
}
[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からアクセス疎通確認
[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
[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)
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
# 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については不明。後ほど証明書を更新する。
[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!
[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で一括インストールしてみる。
[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
テンプレートをベースに以下のように作成する。
[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
apiVersion: v2
name: k8s-test-appgw
version: 0.1.0
description: A Helm chart for k8s-test-appgw
appVersion: "1.0.0"
[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フィールド
# 起動する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
# アプリケーション設定
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 }}
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
# 認証情報を設定
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 }} # シークレットアクセスキー
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
# 本アプリケーションのサーバ証明書と秘密鍵データ設定
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 }}
{{- 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」の名前解決設定を追加しておく。
[mng ~]$ curl --cacert tls/private_ca.crt -H 'Authorization: TestApp xxx...'
https://k8s-test-appgw.test.k8s.local/ping | jq .
{
"message": "pong"
}
[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"
}
[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からアクセス疎通確認
[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
[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へ合わせて指定しておく。
[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
[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定義ファイルを作成
[mng k8s-test-appgw]$ echo "crt: \"$(base64 -w 0 tls.crt)\""
crt: "LS0tL..."
tls:
# 新しいTLS証明書(PEM形式をBase64エンコード)
crt: "LS0tL..."
key: "LS..." # 秘密鍵は変更なし。前回と同じものを指定する
※ サーバ証明書だけ更新するのでtlsフィールドだけ定義する。秘密鍵フィールド(tls.key)にも(変更はしていないが)指定しないとSecretの変更と認識されない模様なので両方指定しておく。
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が更新されていないので新しい証明書が適用されていない模様。
[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
[mng k8s-test-appgw]$ kubectl rollout restart deployment k8s-test-appgw
接続確認テスト (2) - 成功
[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を指定して確認する。
[mng ~]$ curl --cacert tls/private_ca.crt -H 'Authorization: TestApp xxx...' https://10.0.0.65/ping | jq .
{
"message": "pong"
}
[mng ~]$ curl --cacert ../tls/private_ca.crt -H 'Authorization: TestApp xxx...' https://10.0.0.65/json-from-cephfs | jq .
{
"message": "Hello, world"
}
[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"
}
]