Kotlin: Kotlinプロジェクトデプロイ詳解
最終更新:2026-08-26
コードは書かれた — いよいよラストマイルです。CharlieはOrderProcessorをコンテナ化し、CI/CDパイプラインを構築し、監視とアラートを設定して、git pushから本番までのデプロイを完全自動化します。
1. 学べること
- Dockerマルチステージビルド:
gradle:jdk→ 軽量JREイメージ - Docker Compose:OrderProcessor + PostgreSQL + Redis
- CI/CD:GitHub Actionsによるビルド、テスト、イメージプッシュ
- 監視:Micrometer + Prometheus + Grafanaメトリクスダッシュボード
- Charlieの実践:ワンクリックデプロイ + ヘルスチェック + 本番準備チェックリスト
2. 本物の建築家の物語
(1) 課題:手動デプロイパイプライン
Charlieのチームは手動デプロイを行っていました:SSHでサーバーに接続 → git pull → gradle build → java -jar → systemctl 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パイプライン図
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を追加。📖 まとめ
- Dockerマルチステージビルド:ビルド段階はJDK、ランタイム段階はJRE — イメージが800MBから150MBに縮小
- Docker Composeワンクリックオーケストレーション:アプリ + データベース + キャッシュ + 監視
- CI/CDパイプライン:git push → テスト → ビルド → プッシュ → デプロイ、5分で完全自動
- Micrometer + Prometheus + Grafana:メトリクス収集、保存、可視化の三位一体
- 本番準備チェックリスト:セキュリティ、信頼性、オブザーバビリティ、パフォーマンス、デプロイ — 8つの必須チェック
- ヘルスチェックは本番のベースライン:
/actuator/healthエンドポイント + Docker HEALTHCHECK
📝 練習問題
- 初級 (⭐):OrderProcessorのDockerfileを書いてください(シングルステージで可)、
eclipse-temurin:17-jreベースで。ヒント:COPY build/libs/*.jar app.jar - 中級 (⭐⭐):OrderProcessor + PostgreSQLを含むdocker-compose.ymlを、ヘルスチェック設定付きで書いてください。ヒント:
depends_onにcondition: service_healthy - 上級 (⭐⭐⭐):ビルド、テスト、Dockerプッシュ、デプロイステップを含む完全なGitHub Actions CI/CDパイプラインを書いてください。ヒント:
on: push: branches: [main]