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?

Warikaアプリケーション デプロイマニュアル

Last updated at Posted at 2025-09-04

Warikaアプリケーション デプロイマニュアル

📋 概要

Tailscaleをリレーサーバーとして使用し、Kubernetes上でGoアプリケーションとPostgreSQLを連携させるアーキテクチャのデプロイ手順

🏗️ アーキテクチャ図

[クライアント] → [Tailscale] → [Kubernetes Service] → [GoアプリPod] → [PostgreSQL]

📁 ディレクトリ構造

warika-app/
├── go-app/
│   ├── main.go
│   ├── go.mod
│   ├── go.sum
│   ├── handlers/
│   ├── database/
│   └── models/
├── kubernetes/
│   ├── postgres/
│   │   ├── deployment.yaml
│   │   ├── service.yaml
│   │   └── pvc.yaml
│   ├── app/
│   │   ├── leader-deployment.yaml
│   │   ├── reader-deployment.yaml
│   │   ├── leader-service.yaml
│   │   └── reader-service.yaml
│   └── configmap.yaml
├── docker/
│   └── app/
│       └── Dockerfile
└── scripts/
    └── deploy.sh

🚀 デプロイ手順

1. 前提条件の確認

# 必須ツールの確認
docker --version
kubectl version --client
kind version
go version

# Kindクラスターの確認
kind get clusters

2. プロジェクトのセットアップ

# リポジトリのクローンまたはディレクトリ作成
mkdir -p ~/warika-app/{go-app,kubernetes,scripts,docker}
cd ~/warika-app

3. Goアプリケーションの準備

# go-app/main.go
cat > go-app/main.go << 'EOF'
package main

import (
    "database/sql"
    "fmt"
    "log"
    "net/http"
    "os"
    "time"
    _ "github.com/lib/pq"
)

func main() {
    // データベース接続(リトライ付き)
    var db *sql.DB
    var err error
    
    connStr := "host=postgres-service user=warika password=warika dbname=warika sslmode=disable"
    
    for i := 0; i < 10; i++ {
        log.Printf("Connecting to database (attempt %d): %s", i+1, connStr)
        
        db, err = sql.Open("postgres", connStr)
        if err != nil {
            log.Printf("Database open failed: %v", err)
            time.Sleep(2 * time.Second)
            continue
        }
        
        err = db.Ping()
        if err != nil {
            log.Printf("Database ping failed: %v", err)
            db.Close()
            time.Sleep(2 * time.Second)
            continue
        }
        
        log.Println("Database connection successful!")
        break
    }
    
    if err != nil {
        log.Fatal("Database connection failed after retries:", err)
    }
    defer db.Close()

    // テーブル存在確認
    var exists bool
    err = db.QueryRow("SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = $1)", "logs").Scan(&exists)
    if err != nil {
        log.Fatal("Table check failed:", err)
    }
    log.Printf("Table 'logs' exists: %v", exists)

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello from Warika App!\nPod: %s\n", os.Getenv("POD_NAME"))
    })

    http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        fmt.Fprintf(w, "Healthy\n")
    })

    http.HandleFunc("/log", func(w http.ResponseWriter, r *http.Request) {
        if r.Method != "POST" {
            http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
            return
        }
        
        message := "Test log message"
        _, err := db.Exec("INSERT INTO logs (message, pod_type, pod_name) VALUES ($1, $2, $3)", 
            message, "leader", os.Getenv("POD_NAME"))
        
        if err != nil {
            log.Printf("Insert failed: %v", err)
            http.Error(w, "Database error", http.StatusInternalServerError)
            return
        }
        
        w.WriteHeader(http.StatusCreated)
        fmt.Fprintf(w, "Log created\n")
    })

    http.HandleFunc("/logs", func(w http.ResponseWriter, r *http.Request) {
        rows, err := db.Query("SELECT id, message, pod_type, pod_name, created_at FROM logs ORDER BY created_at DESC")
        if err != nil {
            log.Printf("Query failed: %v", err)
            http.Error(w, "Database error", http.StatusInternalServerError)
            return
        }
        defer rows.Close()
        
        for rows.Next() {
            var id int
            var message, podType, podName string
            var createdAt time.Time
            
            err := rows.Scan(&id, &message, &podType, &podName, &createdAt)
            if err != nil {
                log.Printf("Scan failed: %v", err)
                continue
            }
            
            fmt.Fprintf(w, "ID: %d, Message: %s, Pod: %s/%s, Time: %s\n", 
                id, message, podType, podName, createdAt.Format("2006-01-02 15:04:05"))
        }
    })

    port := os.Getenv("PORT")
    if port == "" {
        port = "3000"
    }

    log.Printf("Server starting on port %s", port)
    log.Fatal(http.ListenAndServe(":"+port, nil))
}
EOF

# go.modの設定
cd go-app
go mod init warika-app
go get github.com/lib/pq
go mod tidy
cd ..

4. Dockerfileの作成

# Dockerfile
cat > Dockerfile << 'EOF'
FROM golang:1.21-alpine

WORKDIR /app

# 依存関係を先にコピー
COPY go-app/go.mod go-app/go.sum ./
RUN go mod download

# ソースコードをコピー
COPY go-app/ .

RUN go build -o main .

EXPOSE 3000

CMD ["./main"]
EOF

5. Kubernetesマニフェストの作成

# PostgreSQL Deployment
mkdir -p kubernetes/postgres
cat > kubernetes/postgres/deployment.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres-deployment
  labels:
    app: postgres
    tier: database
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
        tier: database
    spec:
      containers:
      - name: postgres
        image: postgres:15-alpine
        env:
        - name: POSTGRES_DB
          value: warika
        - name: POSTGRES_USER
          value: warika
        - name: POSTGRES_PASSWORD
          value: warika
        - name: PGDATA
          value: /var/lib/postgresql/data/pgdata
        ports:
        - containerPort: 5432
        volumeMounts:
        - name: postgres-storage
          mountPath: /var/lib/postgresql/data
        livenessProbe:
          exec:
            command:
            - sh
            - -c
            - exec pg_isready -U warika -d warika
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          exec:
            command:
            - sh
            - -c
            - exec pg_isready -U warika -d warika
          initialDelaySeconds: 5
          periodSeconds: 5
      volumes:
      - name: postgres-storage
        persistentVolumeClaim:
          claimName: postgres-pvc
EOF

# PostgreSQL Service
cat > kubernetes/postgres/service.yaml << 'EOF'
apiVersion: v1
kind: Service
metadata:
  name: postgres-service
  labels:
    app: postgres
    tier: database
spec:
  selector:
    app: postgres
  ports:
    - protocol: TCP
      port: 5432
      targetPort: 5432
  type: ClusterIP
EOF

# PostgreSQL PVC
cat > kubernetes/postgres/pvc.yaml << 'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
  labels:
    app: postgres
    tier: database
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  storageClassName: standard
EOF

# Leader App Deployment
mkdir -p kubernetes/app
cat > kubernetes/app/leader-deployment.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: leader-app-deployment
  labels:
    app: warika-app
    tier: leader
spec:
  replicas: 1
  selector:
    matchLabels:
      app: warika-app
      tier: leader
  template:
    metadata:
      labels:
        app: warika-app
        tier: leader
    spec:
      containers:
      - name: warika-app
        image: warika-app:latest
        imagePullPolicy: Never
        env:
        - name: POD_TYPE
          value: "leader"
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: PORT
          value: "3000"
        - name: DB_HOST
          value: "postgres-service"
        - name: DB_USER
          value: "warika"
        - name: DB_PASSWORD
          value: "warika"
        - name: DB_NAME
          value: "warika"
        ports:
        - containerPort: 3000
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 5
EOF

# Reader App Deployment
cat > kubernetes/app/reader-deployment.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: reader-app-deployment
  labels:
    app: warika-app
    tier: reader
spec:
  replicas: 2
  selector:
    matchLabels:
      app: warika-app
      tier: reader
  template:
    metadata:
      labels:
        app: warika-app
        tier: reader
    spec:
      containers:
      - name: warika-app
        image: warika-app:latest
        imagePullPolicy: Never
        env:
        - name: POD_TYPE
          value: "reader"
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: PORT
          value: "3000"
        - name: DB_HOST
          value: "postgres-service"
        - name: DB_USER
          value: "warika"
        - name: DB_PASSWORD
          value: "warika"
        - name: DB_NAME
          value: "warika"
        ports:
        - containerPort: 3000
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 5
EOF

# Leader Service
cat > kubernetes/app/leader-service.yaml << 'EOF'
apiVersion: v1
kind: Service
metadata:
  name: leader-app-service
  labels:
    app: warika-app
    tier: leader
spec:
  selector:
    app: warika-app
    tier: leader
  ports:
    - protocol: TCP
      port: 3000
      targetPort: 3000
  type: ClusterIP
EOF

# Reader Service
cat > kubernetes/app/reader-service.yaml << 'EOF'
apiVersion: v1
kind: Service
metadata:
  name: reader-app-service
  labels:
    app: warika-app
    tier: reader
spec:
  selector:
    app: warika-app
    tier: reader
  ports:
    - protocol: TCP
      port: 3000
      targetPort: 3000
  type: ClusterIP
EOF

6. デプロイスクリプトの作成

# scripts/deploy.sh
cat > scripts/deploy.sh << 'EOF'
#!/bin/bash

set -e

echo "=== Warikaアプリケーション デプロイ開始 ==="

# 1. Kindクラスターの作成または確認
echo "=== Kindクラスターの確認 ==="
if ! kind get clusters | grep -q "kind"; then
    echo "Kindクラスターを作成します..."
    kind create cluster --name kind
else
    echo "既存のKindクラスターを使用します"
fi

kubectl config use-context kind-kind
kubectl get nodes

# 2. Dockerイメージのビルド
echo "=== Dockerイメージのビルド ==="
docker build -t warika-app .

# 3. Kindへのイメージロード
echo "=== Kindへのイメージロード ==="
kind load docker-image warika-app:latest --name kind

# 4. PostgreSQLのデプロイ
echo "=== PostgreSQLのデプロイ ==="
kubectl apply -f kubernetes/postgres/

# 5. データベースの起動待機
echo "=== データベースの起動待機 ==="
kubectl wait --for=condition=ready pod -l app=postgres --timeout=120s

# 6. データベースの初期化
echo "=== データベースの初期化 ==="
kubectl exec -it deployment/postgres-deployment -- psql -U warika -d warika -c "
CREATE TABLE IF NOT EXISTS logs (
    id SERIAL PRIMARY KEY,
    message TEXT NOT NULL,
    pod_type VARCHAR(50) NOT NULL,
    pod_name VARCHAR(100) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_logs_created_at ON logs(created_at);
CREATE INDEX IF NOT EXISTS idx_logs_pod_type ON logs(pod_type);
"

# 7. アプリケーションのデプロイ
echo "=== アプリケーションのデプロイ ==="
kubectl apply -f kubernetes/app/

# 8. アプリケーションの起動待機
echo "=== アプリケーションの起動待機 ==="
kubectl wait --for=condition=ready pod -l app=warika-app --timeout=120s

# 9. デプロイ完了確認
echo "=== デプロイ完了確認 ==="
kubectl get all

echo "=== デプロイ完了 ==="
echo "Leader Service: leader-app-service"
echo "Reader Service: reader-app-service"
echo "PostgreSQL Service: postgres-service"

echo "=== テスト方法 ==="
echo "1. ポートフォワード: kubectl port-forward svc/leader-app-service 3000:3000 &"
echo "2. 接続テスト: curl http://localhost:3000"
echo "3. ヘルスチェック: curl http://localhost:3000/health"
echo "4. ログ追加: curl -X POST http://localhost:3000/log"
echo "5. ログ確認: curl http://localhost:3000/logs"
EOF

chmod +x scripts/deploy.sh

7. デプロイ実行

# デプロイスクリプトの実行
./scripts/deploy.sh

# 手動でポートフォワード開始
kubectl port-forward svc/leader-app-service 3000:3000 &

# アプリケーションテスト
curl http://localhost:3000
curl http://localhost:3000/health
curl -X POST http://localhost:3000/log
curl http://localhost:3000/logs

8. テストスクリプトの作成

# scripts/test.sh
cat > scripts/test.sh << 'EOF'
#!/bin/bash

set -e

echo "=== Warikaアプリケーション テスト開始 ==="

# ポートフォワード開始
echo "=== ポートフォワード開始 ==="
pkill -f "kubectl port-forward" || true
kubectl port-forward svc/leader-app-service 3000:3000 &
sleep 3

# 基本テスト
echo "=== 基本接続テスト ==="
curl http://localhost:3000
curl http://localhost:3000/health

# ログ機能テスト
echo "=== ログ機能テスト ==="
curl -X POST http://localhost:3000/log
curl http://localhost:3000/logs

# データベース確認
echo "=== データベース確認 ==="
kubectl exec -it deployment/postgres-deployment -- psql -U warika -d warika -c "SELECT COUNT(*) as total_logs FROM logs;"

# Readerサービステスト
echo "=== Readerサービステスト ==="
kubectl port-forward svc/reader-app-service 3001:3000 &
sleep 2
curl http://localhost:3001/health

echo "=== テスト完了 ==="
EOF

chmod +x scripts/test.sh

🧹 クリーンアップ手順

# リソースの削除
kubectl delete -f kubernetes/app/
kubectl delete -f kubernetes/postgres/

# Kindクラスターの削除
kind delete cluster --name kind

# ポートの解放
pkill -f "kubectl port-forward"
sudo kill -9 $(sudo lsof -t -i:3000) 2>/dev/null || true
sudo kill -9 $(sudo lsof -t -i:3001) 2>/dev/null || true

📊 モニタリングとトラブルシューティング

ログ確認

# アプリケーションログ
kubectl logs -l tier=leader
kubectl logs -l tier=reader

# データベースログ
kubectl logs -l app=postgres

# 詳細なログ
kubectl describe pods -l app=warika-app

状態確認

# すべてのリソース状態
kubectl get all

# イベント確認
kubectl get events --sort-by=.metadata.creationTimestamp

# サービス詳細
kubectl describe svc postgres-service
kubectl describe svc leader-app-service

🎯 正常性確認ポイント

  1. ✅ すべてのPodがRunning状態
  2. ✅ データベース接続が成功
  3. ✅ HTTPリクエストに正常応答
  4. ✅ ログの追加と取得が可能
  5. ✅ サービス間でデータの一貫性が保たれている

これでWarikaアプリケーションのデプロイは完了です!

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?