Kotlin: Kotlinプロジェクトデプロイ詳解

最終更新:2026-08-26

コードは書かれた — いよいよラストマイルです。CharlieはOrderProcessorをコンテナ化し、CI/CDパイプラインを構築し、監視とアラートを設定して、git pushから本番までのデプロイを完全自動化します。

1. 学べること


2. 本物の建築家の物語

(1) 課題:手動デプロイパイプライン

Charlieのチームは手動デプロイを行っていました:SSHでサーバーに接続 → git pullgradle buildjava -jarsystemctl restart。1回のデプロイに30分かかり、月に2〜3回のヒューマンエラーが発生していました。

(2) 完全自動化CI/CDソリューション

TEXT 📖 参照専用
Before: git push → SSH → build → deploy (30 min, error-prone)
After:  git push → GitHub Actions → Docker build → deploy (5 min, zero-touch)

コンテナ化 + CI/CD = デプロイが30分の手作業から5分の完全自動パイプラインへ。


3. Dockerマルチステージビルド

(1) Dockerfile

DOCKERFILE
# Stage 1: Build
FROM gradle:8.5-jdk17 AS builder
WORKDIR /app
COPY build.gradle.kts settings.gradle.kts ./
COPY gradle ./gradle
COPY src ./src
RUN gradle bootJar --no-daemon -x test

# Stage 2: Runtime (lightweight JRE)
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar

# Non-root user for security
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget -qO- http://localhost:8080/actuator/health || exit 1

ENTRYPOINT ["java", "-jar", "app.jar"]

(2) マルチステージ vs シングルステージ比較

項目 シングルステージ マルチステージ
イメージサイズ 約800MB(JDK + ソース) 約150MB(JREのみ)
セキュリティ ソースがイメージに含まれる ランタイムイメージにソースなし
ビルドキャッシュ レイヤー化なし ステージごとの独立したレイヤーキャッシュ
ビルド時間 毎回フルビルド 依存関係レイヤーのキャッシュ再利用

(1) ▶ サンプル

マルチステージビルドのイメージサイズ削減効果をシミュレーションする例です。JDK入りイメージとJREのみイメージのサイズを比較します。

KOTLIN
data class DockerImage(val name: String, val sizeMB: Int, val layers: Int)

fun main() {
    val singleStage = DockerImage("order-processor:single", 820, 12)
    val multiStage  = DockerImage("order-processor:multi",  148, 6)

    println("=== Image Size Comparison ===")
    println("  Single-stage: ${singleStage.sizeMB}MB (${singleStage.layers} layers)")
    println("  Multi-stage:  ${multiStage.sizeMB}MB (${multiStage.layers} layers)")
    val reduction = 100.0 * (singleStage.sizeMB - multiStage.sizeMB) / singleStage.sizeMB
    println("  Reduction:    ${"%.1f".format(reduction)}% smaller")
    println("\n  Security: Multi-stage has NO source code in final image")
}

出力:

TEXT 📖 参照専用
=== Image Size Comparison ===
  Single-stage: 820MB (12 layers)
  Multi-stage:  148MB (6 layers)
  Reduction:    81.9% smaller

  Security: Multi-stage has NO source code in final image

4. Docker Composeオーケストレーション

(1) docker-compose.yml

YAML
version: '3.8'

services:
  order-processor:
    build: .
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - SPRING_DATASOURCE_URL=r2dbc:postgresql://postgres:5432/orderdb
      - SPRING_DATASOURCE_USERNAME=order_user
      - SPRING_DATASOURCE_PASSWORD=order_pass
      - SPRING_REDIS_HOST=redis
      - MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE=health,info,prometheus
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - order-net

  postgres:
    image: postgres:16-alpine
    environment:
      - POSTGRES_DB=orderdb
      - POSTGRES_USER=order_user
      - POSTGRES_PASSWORD=order_pass
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U order_user -d orderdb"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - order-net

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
    networks:
      - order-net

  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"
    networks:
      - order-net

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    depends_on:
      - prometheus
    networks:
      - order-net

volumes:
  pgdata:

networks:
  order-net:
    driver: bridge

(2) ワンクリックデプロイ

BASH
# Start all services
docker-compose up -d

# Check status
docker-compose ps

# View logs
docker-compose logs -f order-processor

# Stop all
docker-compose down

5. CI/CDパイプライン

(1) GitHub Actions

YAML
# .github/workflows/deploy.yml
name: Build and Deploy

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Cache Gradle
        uses: actions/cache@v3
        with:
          path: ~/.gradle/caches
          key: gradle-${{ hashFiles('**/*.gradle.kts') }}

      - name: Run tests
        run: ./gradlew test

      - name: Build JAR
        run: ./gradlew bootJar

      - name: Build Docker image
        run: docker build -t order-processor:${{ github.sha }} .

      - name: Push to registry
        if: github.ref == 'refs/heads/main'
        run: |
          docker tag order-processor:${{ github.sha }} registry.example.com/order-processor:latest
          docker push registry.example.com/order-processor:latest

      - name: Deploy
        if: github.ref == 'refs/heads/main'
        run: |
          ssh deploy@prod-server "docker pull registry.example.com/order-processor:latest && docker-compose up -d"

(1) ▶ サンプル

CI/CDパイプラインの各ステージの成否をシミュレーションする例です。テスト失敗時にデプロイがブロックされることを確認します。

KOTLIN
data class StageResult(val stage: String, val success: Boolean, val durationMs: Long)

class PipelineSimulator {
    private val results = mutableListOf<StageResult>()

    fun run(stage: String, durationMs: Long, success: Boolean): PipelineSimulator {
        results.add(StageResult(stage, success, durationMs))
        val icon = if (success) "PASS" else "FAIL"
        println("  [$icon] $stage (${durationMs}ms)")
        return this
    }

    fun isDeployable(): Boolean = results.all { it.success }
}

fun main() {
    println("=== CI/CD Pipeline (Success) ===")
    val pass = PipelineSimulator()
        .run("Checkout", 5, true)
        .run("Test", 120, true)
        .run("Build JAR", 45, true)
        .run("Docker Build", 60, true)
    println("  Deploy: ${if (pass.isDeployable()) "PROCEED" else "BLOCKED"}")

    println("\n=== CI/CD Pipeline (Test Fail) ===")
    val fail = PipelineSimulator()
        .run("Checkout", 5, true)
        .run("Test", 120, false)
    println("  Deploy: ${if (fail.isDeployable()) "PROCEED" else "BLOCKED"}")
}

出力:

TEXT 📖 参照専用
=== CI/CD Pipeline (Success) ===
  [PASS] Checkout (5ms)
  [PASS] Test (120ms)
  [PASS] Build JAR (45ms)
  [PASS] Docker Build (60ms)
  Deploy: PROCEED

=== CI/CD Pipeline (Test Fail) ===
  [PASS] Checkout (5ms)
  [FAIL] Test (120ms)
  Deploy: BLOCKED

(2) CI/CDパイプライン図

100%
flowchart TD
    A[git push] --> B[GitHub Actions]
    B --> C[Checkout Code]
    C --> D[Setup JDK 17]
    D --> E[Cache Gradle]
    E --> F[Run Tests]
    F --> G{Tests Pass?}
    G -->|Yes| H[Build JAR]
    G -->|No| I[Notify Team]
    H --> J[Build Docker Image]
    J --> K{Main Branch?}
    K -->|Yes| L[Push to Registry]
    K -->|No| M[Stop]
    L --> N[Deploy to Production]
    N --> O[Health Check]
    O --> P{Healthy?}
    P -->|Yes| Q[Live]
    P -->|No| R[Rollback]

6. 監視

(1) Spring Boot Actuator設定

KOTLIN
// application.yml
// management:
//   endpoints:
//     web:
//       exposure:
//         include: health,info,prometheus,metrics
//   metrics:
//     export:
//       prometheus:
//         enabled: true
//   endpoint:
//     health:
//       show-details: always

(2) Micrometerカスタムメトリクス

KOTLIN
import io.micrometer.core.instrument.Counter
import io.micrometer.core.instrument.MeterRegistry
import io.micrometer.core.instrument.Timer

class OrderMetrics(registry: MeterRegistry) {
    private val ordersCreated = Counter.builder("orders.created.total")
        .description("Total orders created")
        .register(registry)

    private val orderProcessingTime = Timer.builder("orders.processing.time")
        .description("Order processing time")
        .register(registry)

    fun recordOrderCreated() { ordersCreated.increment() }

    fun <T> recordProcessingTime(block: () -> T): T {
        return orderProcessingTime.recordCallable { block() } ?: block()
    }
}

(3) 監視メトリクス

メトリクス タイプ アラート閾値
orders_created_total Counter
orders_processing_time Timer P99 > 2s
jvm_memory_used_bytes Gauge > 80%
db_connection_pool_active Gauge > 90%
http_server_requests_seconds Timer P99 > 5s
disk_free_bytes / disk_total_bytes Gauge 空き < 10%

(1) ▶ サンプル

Micrometerカスタムメトリクスの記録とダッシュボード用出力をシミュレーションする例です。CounterとTimerの基本的な使い方を示します。

KOTLIN
class SimpleCounter(private val name: String) {
    private var value = 0.0
    fun increment() { value += 1.0 }
    fun count(): Double = value
}

class SimpleTimer(private val name: String) {
    private val samples = mutableListOf<Long>()
    fun record(ms: Long) { samples.add(ms) }
    fun p99(): Long = samples.sorted().let { it[(it.size * 99 / 100).coerceAtMost(it.size - 1)] }
    fun avg(): Long = if (samples.isEmpty()) 0 else samples.sum() / samples.size
}

fun main() {
    val orderCounter = SimpleCounter("orders.created.total")
    val processTimer = SimpleTimer("orders.processing.time")

    // Simulate 5 orders
    val times = listOf(120L, 95L, 310L, 88L, 150L)
    times.forEach { t ->
        orderCounter.increment()
        processTimer.record(t)
    }

    println("=== Metrics Dashboard ===")
    println("  ${orderCounter.name}: ${orderCounter.count().toInt()} orders")
    println("  ${processTimer.name}:")
    println("    avg = ${processTimer.avg()}ms")
    println("    p99 = ${processTimer.p99()}ms")
    println("    alert (p99 > 2000ms)? ${if (processTimer.p99() > 2000) "YES - ALERT" else "No"}")
}

出力:

TEXT 📖 参照専用
=== Metrics Dashboard ===
  orders.created.total: 5 orders
  orders.processing.time:
    avg = 152ms
    p99 = 310ms
    alert (p99 > 2000ms)? No

7. ヘルスチェックと本番準備

(1) ヘルスチェックエンドポイント

KOTLIN
// Spring Boot Actuator health endpoint
// GET /actuator/health
// {
//   "status": "UP",
//   "components": {
//     "db": { "status": "UP" },
//     "redis": { "status": "UP" },
//     "diskSpace": { "status": "UP" }
//   }
// }

(2) 本番準備チェックリスト

カテゴリ チェック項目 ステータス
セキュリティ 非rootユーザーで実行
セキュリティ シークレットのハードコードなし
セキュリティ HTTPS設定済み
信頼性 ヘルスチェックエンドポイント
信頼性 グレースフルシャットダウン(SIGTERM)
信頼性 データベース接続プール設定済み
オブザーバビリティ 構造化ログ出力
オブザーバビリティ Prometheusメトリクス公開
オブザーバビリティ アラートルール設定済み
パフォーマンス JVMヒープサイズ設定済み
パフォーマンス GC戦略選択済み
デプロイ Dockerイメージ < 200MB
デプロイ CI/CDパイプライン
デプロイ ロールバック戦略

8. 完成例:ワンクリックデプロイデモ

KOTLIN
// ============================================
// OrderProcessor - Deployment Simulation
// Feature: Docker + CI/CD + Health check demo
// ============================================

import kotlin.system.measureTimeMillis

data class DeployResult(val service: String, val status: String, val time: Long)

class DeploySimulator {
    private val services = mutableListOf<DeployResult>()
    private var deployed = false

    fun build(): DeploySimulator {
        print("  Building Docker image...")
        val time = measureTimeMillis { Thread.sleep(800) }
        println(" Done (${time}ms)")
        return this
    }

    fun test(): DeploySimulator {
        print("  Running tests...")
        val time = measureTimeMillis { Thread.sleep(300) }
        println(" Passed (${time}ms)")
        return this
    }

    fun push(): DeploySimulator {
        print("  Pushing to registry...")
        val time = measureTimeMillis { Thread.sleep(500) }
        println(" Done (${time}ms)")
        return this
    }

    fun deploy(service: String, port: Int): DeploySimulator {
        print("  Deploying $service on port $port...")
        val time = measureTimeMillis { Thread.sleep(400) }
        services.add(DeployResult(service, "RUNNING", time))
        println(" Running (${time}ms)")
        return this
    }

    fun healthCheck(): DeploySimulator {
        print("  Health check...")
        val time = measureTimeMillis { Thread.sleep(200) }
        val allHealthy = services.all { it.status == "RUNNING" }
        println(if (allHealthy) " ALL HEALTHY" else " UNHEALTHY DETECTED")
        return this
    }

    fun summary() {
        println("\n=== Deployment Summary ===")
        services.forEach { s ->
            println("  ${s.service}: ${s.status} (${s.time}ms)")
        }
        println("\n  Total services: ${services.size}")
        println("  Health: ${if (services.all { it.status == "RUNNING" }) "ALL GREEN" else "ISSUES DETECTED"}")
        deployed = true
    }

    fun isDeployed() = deployed
}

fun main() {
    println("=== OrderProcessor CI/CD Pipeline ===\n")

    println("[1/6] Build Stage:")
    DeploySimulator()
        .build()
        .test()

    println("\n[2/6] Push Stage:")
    DeploySimulator().push()

    println("\n[3/6] Deploy Stage:")
    val deployer = DeploySimulator()
        .deploy("postgres", 5432)
        .deploy("redis", 6379)
        .deploy("order-processor", 8080)
        .deploy("prometheus", 9090)
        .deploy("grafana", 3000)

    println("\n[4/6] Health Check:")
    deployer.healthCheck()

    println("\n[5/6] Smoke Test:")
    println("  GET /actuator/health -> 200 OK")
    println("  GET /api/v1/orders -> 200 OK")

    println("\n[6/6] Production Ready Checklist:")
    val checks = listOf(
        "Non-root user" to true,
        "No hardcoded secrets" to true,
        "Health endpoint exposed" to true,
        "Prometheus metrics enabled" to true,
        "Graceful shutdown configured" to true,
        "Docker image < 200MB" to true,
        "CI/CD pipeline active" to true,
        "Rollback strategy defined" to true
    )
    checks.forEach { (item, passed) ->
        println("  ${if (passed) "✅" else "❌"} $item")
    }

    val passCount = checks.count { it.second }
    println("\n  Result: $passCount/${checks.size} checks passed")

    if (passCount == checks.size) {
        println("\n  🚀 OrderProcessor is LIVE!")
    }
}

出力:

TEXT 📖 参照専用
=== OrderProcessor CI/CD Pipeline ===

[1/6] Build Stage:
  Building Docker image... Done (804ms)
  Running tests... Passed (301ms)

[2/6] Push Stage:
  Pushing to registry... Done (502ms)

[3/6] Deploy Stage:
  Deploying postgres on port 5432... Running (401ms)
  Deploying redis on port 6379... Running (401ms)
  Deploying order-processor on port 8080... Running (401ms)
  Deploying prometheus on port 9090... Running (401ms)
  Deploying grafana on port 3000... Running (401ms)

[4/6] Health Check:
  Health check... ALL HEALTHY

[5/6] Smoke Test:
  GET /actuator/health -> 200 OK
  GET /api/v1/orders -> 200 OK

[6/6] Production Ready Checklist:
  ✅ Non-root user
  ✅ No hardcoded secrets
  ✅ Health endpoint exposed
  ✅ Prometheus metrics enabled
  ✅ Graceful shutdown configured
  ✅ Docker image < 200MB
  ✅ CI/CD pipeline active
  ✅ Rollback strategy defined

  Result: 8/8 checks passed

  🚀 OrderProcessor is LIVE!

❓ よくある質問

Q Dockerマルチステージビルドのメリットは?
A 最終イメージにはランタイム依存関係(JRE)のみが含まれ、ソースコードやビルドツールは含まれません。イメージが800MBから150MBに縮小し、攻撃面が小さく、起動も高速になります。
Q ゼロダウンタイムデプロイはどう実現する?
A ブルーグリーンデプロイまたはローリングアップデートを使います。ブルーグリーンは2つの環境を維持し切り替えます。ローリングアップデートはインスタンスを1つずつ置き換えます。Kubernetesはローリングアップデートをネイティブにサポートしています。
Q デプロイ失敗時のロールバック方法は?
A 各DockerイメージビルドにGit SHAでタグを付け、docker run registry.example.com/order-processor:<previous-sha>でロールバックします。自動CI/CDロールバックがさらに良いです。
Q シークレットの管理方法は?
A 環境変数や設定ファイルにシークレットを保存しないでください。Vault、AWS Secrets Manager、またはKubernetes Secretsを使用します。CI/CDがシークレットマネージャーから注入します。
Q JVMアプリケーションの監視方法は?
A Spring Boot Actuator + Micrometer + Prometheus + Grafanaが標準的な組み合わせです。JVM固有のメトリクス:ヒープメモリ、GC回数/時間、スレッド数。
Q 本番のJVMパラメータの設定方法は?
A -Xms-Xmxを同じ値に設定します(ヒープリサイズのオーバーヘッドを回避)。G1GC(-XX:+UseG1GC)を使用。コンテナ環境では-XX:+UseContainerSupportを追加。

📖 まとめ


📝 練習問題

  1. 初級 (⭐):OrderProcessorのDockerfileを書いてください(シングルステージで可)、eclipse-temurin:17-jreベースで。ヒント:COPY build/libs/*.jar app.jar
  2. 中級 (⭐⭐):OrderProcessor + PostgreSQLを含むdocker-compose.ymlを、ヘルスチェック設定付きで書いてください。ヒント:depends_oncondition: service_healthy
  3. 上級 (⭐⭐⭐):ビルド、テスト、Dockerプッシュ、デプロイステップを含む完全なGitHub Actions CI/CDパイプラインを書いてください。ヒント:on: push: branches: [main]

← 前へ | 次へ →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%