Saltar al contenido principal

🚀 Despliegue

📖 ¿Qué es el Despliegue?

El despliegue es el proceso de ejecutar su aplicación desarrollada en un entorno de producción real. ¡Con Docker puede ejecutarla de manera idéntica en cualquier lugar!

💡 Construcción de JAR

Configuración de build.gradle.kts

plugins {
application
kotlin("jvm") version "1.9.0"
id("io.ktor.plugin") version "2.3.5"
}

application {
mainClass.set("com.example.ApplicationKt")
}

ktor {
fatJar {
archiveFileName.set("app.jar")
}
}

Comando de Construcción

# JAR 파일 생성
./gradlew buildFatJar

# 실행
java -jar build/libs/app.jar

🎯 Configuración de Entorno

application.conf

ktor {
deployment {
port = 8080
port = ${?PORT} # 환경 변수 우선
}

application {
modules = [ com.example.ApplicationKt.module ]
}
}

database {
url = "jdbc:postgresql://localhost:5432/mydb"
url = ${?DATABASE_URL}
user = "admin"
user = ${?DATABASE_USER}
password = "password"
password = ${?DATABASE_PASSWORD}
}

Uso de Variables de Entorno

fun Application.module() {
val dbUrl = environment.config.property("database.url").getString()
val dbUser = environment.config.property("database.user").getString()
val dbPassword = environment.config.property("database.password").getString()

Database.connect(
url = dbUrl,
user = dbUser,
password = dbPassword
)

routing {
get("/health") {
call.respondText("OK")
}
}
}

🐳 Despliegue con Docker

Dockerfile

# 1단계: 빌드
FROM gradle:8.4-jdk17 AS build
WORKDIR /app

COPY build.gradle.kts settings.gradle.kts ./
COPY src ./src

RUN gradle buildFatJar --no-daemon

# 2단계: 실행
FROM openjdk:17-jdk-slim
WORKDIR /app

COPY --from=build /app/build/libs/app.jar ./app.jar

EXPOSE 8080

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

docker-compose.yml

version: '3.8'

services:
app:
build: .
ports:
- "8080:8080"
environment:
- DATABASE_URL=jdbc:postgresql://db:5432/mydb
- DATABASE_USER=admin
- DATABASE_PASSWORD=secret
depends_on:
- db

db:
image: postgres:15
environment:
- POSTGRES_DB=mydb
- POSTGRES_USER=admin
- POSTGRES_PASSWORD=secret
volumes:
- db-data:/var/lib/postgresql/data
ports:
- "5432:5432"

volumes:
db-data:

Ejecución

# 빌드 및 실행
docker-compose up -d

# 로그 확인
docker-compose logs -f app

# 중지
docker-compose down

🔧 Configuración de Producción

Registro de Logs

import org.slf4j.LoggerFactory

val logger = LoggerFactory.getLogger("Application")

fun Application.module() {
logger.info("애플리케이션 시작")

routing {
get("/") {
logger.info("/ 요청")
call.respondText("Hello!")
}
}
}

Configuración de CORS

import io.ktor.server.plugins.cors.routing.*

fun Application.module() {
install(CORS) {
allowHost("myapp.com", schemes = listOf("https"))
allowHeader(HttpHeaders.ContentType)
allowMethod(HttpMethod.Options)
allowMethod(HttpMethod.Get)
allowMethod(HttpMethod.Post)
allowCredentials = true
}
}

Compresión

import io.ktor.server.plugins.compression.*

fun Application.module() {
install(Compression) {
gzip {
priority = 1.0
}
deflate {
priority = 10.0
minimumSize(1024)
}
}
}

🛡️ Seguridad

Configuración de HTTPS

embeddedServer(Netty, environment = applicationEngineEnvironment {
connector {
port = 8080
}

sslConnector(
keyStore = KeyStore.getInstance("PKCS12").apply {
load(FileInputStream("keystore.p12"), "password".toCharArray())
},
keyAlias = "myalias",
keyStorePassword = { "password".toCharArray() },
privateKeyPassword = { "password".toCharArray() }
) {
port = 8443
}
}).start(wait = true)

Limitación de Tasa (Versión Simple)

class RateLimiter {
private val requests = mutableMapOf<String, MutableList<Long>>()
private val limit = 100 // 100 requests
private val windowMs = 60_000L // per minute

fun isAllowed(key: String): Boolean {
val now = System.currentTimeMillis()
val userRequests = requests.getOrPut(key) { mutableListOf() }

// 오래된 요청 제거
userRequests.removeIf { it < now - windowMs }

return if (userRequests.size < limit) {
userRequests.add(now)
true
} else {
false
}
}
}

fun Application.module() {
val rateLimiter = RateLimiter()

routing {
intercept(ApplicationCallPipeline.Call) {
val ip = call.request.origin.remoteHost

if (!rateLimiter.isAllowed(ip)) {
call.respond(HttpStatusCode.TooManyRequests)
finish()
}
}

get("/api/data") {
call.respondText("데이터")
}
}
}

🎯 Monitoreo

Verificación de Estado

fun Application.module() {
routing {
get("/health") {
val dbHealth = try {
transaction {
exec("SELECT 1") {}
}
"ok"
} catch (e: Exception) {
"error"
}

call.respond(
mapOf(
"status" to "ok",
"database" to dbHealth,
"timestamp" to System.currentTimeMillis()
)
)
}
}
}

Métricas

class Metrics {
private val requestCount = AtomicInteger(0)
private val errorCount = AtomicInteger(0)

fun incrementRequests() = requestCount.incrementAndGet()
fun incrementErrors() = errorCount.incrementAndGet()

fun getMetrics() = mapOf(
"requests" to requestCount.get(),
"errors" to errorCount.get()
)
}

fun Application.module() {
val metrics = Metrics()

routing {
intercept(ApplicationCallPipeline.Monitoring) {
metrics.incrementRequests()
try {
proceed()
} catch (e: Exception) {
metrics.incrementErrors()
throw e
}
}

get("/metrics") {
call.respond(metrics.getMetrics())
}
}
}

🔥 Ejemplo de Despliegue en Producción

Configuración Completa

// Application.kt
fun main(args: Array<String>) {
EngineMain.main(args)
}

fun Application.module() {
// 설정 로드
val config = environment.config

// 로깅
install(CallLogging) {
level = Level.INFO
}

// CORS
install(CORS) {
allowHost(config.property("cors.host").getString())
allowCredentials = true
}

// 압축
install(Compression)

// JSON
install(ContentNegotiation) {
json()
}

// 에러 처리
install(StatusPages) {
exception<Throwable> { call, cause ->
log.error("Unhandled exception", cause)
call.respond(
HttpStatusCode.InternalServerError,
mapOf("error" to "Internal server error")
)
}
}

// 데이터베이스
configureDatabases(config)

// 라우팅
configureRouting()
}

fun Application.configureDatabases(config: ApplicationConfig) {
val url = config.property("database.url").getString()
val user = config.property("database.user").getString()
val password = config.property("database.password").getString()

Database.connect(
url = url,
user = user,
password = password
)
}

🤔 Preguntas Frecuentes

P1. ¿Dónde debo desplegar?

R: ¡Se recomiendan servicios en la nube!

# AWS (Elastic Beanstalk, ECS)
# Google Cloud (Cloud Run)
# Azure (App Service)
# DigitalOcean (App Platform)
# Heroku

# Docker만 있으면 어디든 가능!

P2. ¿Qué hay del despliegue sin tiempo de inactividad?

R: ¡Despliegue Blue-Green o Rolling Update!

# docker-compose.yml (Rolling Update)
version: '3.8'

services:
app:
image: myapp:latest
deploy:
replicas: 3
update_config:
parallelism: 1
delay: 10s

P3. ¿Cómo gestiono configuraciones específicas de cada entorno?

R: ¡Use perfiles!

// application-dev.conf
ktor {
deployment {
port = 8080
}
}

// application-prod.conf
ktor {
deployment {
port = 80
}
}

// 실행
java -jar app.jar -config=application-prod.conf

🎬 Conclusión

¡Para un despliegue seguro y eficiente!

핵심 정리:
✅ JAR 빌드로 실행 파일
✅ Docker로 환경 통일
✅ 환경 변수로 설정 분리
✅ Health Check로 모니터링
✅ 보안 설정 필수

¡Felicitaciones! ¡Ha completado la serie Backend y toda la documentación de Kotlin! 🎉

¡Ahora puede hacer desarrollo full-stack con Kotlin!

Revisión de toda la serie:

  • Basics: Fundamentos de Kotlin
  • Practical: Aplicación práctica (funciones de extensión, excepciones, archivos, expresiones regulares, DSL)
  • Coroutines: Programación asíncrona
  • Testing: Escritura de pruebas
  • Backend: Desarrollo de servidores y despliegue