지난 포스트에서 전체 그림을 봤다. 이번엔 Terraform이 올릴 대상 앱을 만든다. 목적이 인프라 테스트라서 앱은 최대한 단순하게 만든다.
프로젝트 구조
실제로 운영 중인 MSA 프로젝트와 같은 모양으로, Gradle 멀티모듈로 서비스를 나눴다.
1
2
3
4
5
6
7
8
9
10
11
terraform-study/
├── settings.gradle.kts
├── build.gradle.kts
├── apps/
│ ├── gateway/ # 9000
│ ├── chatting/ # 8080
│ ├── session/ # 8081
│ ├── user/ # 8082
│ ├── push/ # 8084
│ └── push-worker/ # 8085
└── infra/terraform/ # 다음 글부터
1
2
3
4
// settings.gradle.kts
rootProject.name = "terraform-study"
include(":apps:gateway", ":apps:chatting", ":apps:session", ":apps:user", ":apps:push", ":apps:push-worker")
각 서비스 안은 헥사고날 구조의 흔적만 남겼다. 컨트롤러는 adapter/in/web 아래에 둔다.
1
2
3
apps/user/src/main/java/com/genesisnest/user/
├── UserApplication.java
└── adapter/in/web/HomeController.java
HomeController: 이름만 돌려준다
서비스 6개가 전부 같은 모양이고 반환값만 다르다.
1
2
3
4
5
6
7
8
@RestController
public class HomeController {
@GetMapping("/")
public String home() {
return "user-service";
}
}
각 서비스 의존성은 딱 두 개다.
1
2
3
4
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-actuator")
}
actuator를 넣은 이유는 헬스체크 때문이다. ALB와 ECS가 /actuator/health를 호출해서 서비스가 살아 있는지 판단한다. 이게 없으면 인프라가 서비스를 정상으로 인식하지 못한다.
gateway: 설정만으로 라우팅
gateway는 Spring Cloud Gateway를 쓴다. 코드는 한 줄도 없고, application.yml에 경로를 적은 게 전부다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
server:
port: 9000
# 업스트림은 환경변수로 덮어쓴다 (ECS Service Connect DNS 등)
gateway:
upstreams:
chatting: ${CHATTING_URL:http://localhost:8080}
session: ${SESSION_URL:http://localhost:8081}
user: ${USER_URL:http://localhost:8082}
push: ${PUSH_URL:http://localhost:8084}
spring:
cloud:
gateway:
server:
webflux:
routes:
- id: user
uri: ${gateway.upstreams.user}
predicates: [Path=/user/**]
filters: ["RewritePath=/user(?<segment>/?.*), /${segment}"]
# chatting, session, push도 같은 모양
이 설정에서 중요한 점은 두 가지다.
1. 업스트림 주소가 환경변수다. 로컬에서는 기본값(localhost:8082)을 쓰고, ECS에서는 환경변수로 서비스 이름(http://user:8082)을 넣어 덮어쓴다. 이걸 빼먹으면 gateway가 자기 자신의 localhost를 호출한다. 기동은 정상으로 되고 라우팅만 조용히 깨지는 최악의 버그다.
2. RewritePath로 접두사를 뗀다. /user로 들어온 요청을 user 서비스의 /로 보낸다. user 서비스에는 /user라는 경로가 없기 때문이다.
로컬에서 gateway, user, chatting을 같이 띄워서 확인했다.
1
2
3
/ → gateway-service
/user → user-service
/chatting → chatting-service
이미지 빌드: Jib
컨테이너 이미지는 Docker 없이 Jib로 만든다. Gradle 플러그인이 이미지 빌드와 레지스트리 push를 한 번에 해준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
jib {
from {
image = "amazoncorretto:25"
platforms {
platform {
architecture = "arm64"
os = "linux"
}
}
}
to.image = "terraform-study/user"
container {
mainClass = "com.genesisnest.user.UserApplication"
format = com.google.cloud.tools.jib.api.buildplan.ImageFormat.OCI
jvmFlags = listOf("-XX:MaxRAMPercentage=75.0")
}
}
- arm64로 빌드한다. Fargate에서 ARM(Graviton)이 더 저렴하고, 뒤에서 만들 ECS 태스크도 ARM64로 맞춘다. 이미지 아키텍처와 태스크 아키텍처가 다르면
exec format error가 난다. mainClass를 직접 지정한 이유는 5편에서 다룬다. 처음엔 빼고 썼다가 빌드가 실패했다.
다음 글
앱이 준비됐으니 이제 이걸 올릴 인프라를 설계한다. 다음 글에서는 Terraform 코드를 어떻게 나눌지 다룬다.
➡️ 다음 글: Terraform 코드 구조 설계 — global, envs, modules를 나눈 이유
➡️ 시리즈 인덱스: Terraform으로 AWS ECS 서비스 배포하기