Skip to content

refactor(terraform): draw the foundation/service seam - #7

Merged
iflov merged 1 commit into
mainfrom
iflov/foundation-service-seam
Sep 1, 2026
Merged

iflov merged 1 commit into
mainfrom
iflov/foundation-service-seam

Conversation

@iflov

@iflov iflov commented Sep 1, 2026

Copy link
Copy Markdown
Owner

왜

결정 문서가 "terraform 사본 하나당 소비자 하나"를 전제했는데 그 전제가 틀렸다. frontend와 다른 서비스가 같은 인프라를 나눠 쓴다. 소비자가 여럿이면 리소스가 변경 빈도와 blast radius가 전혀 다른 두 층으로 갈리는데, 코드는 둘을 섞고 있었다.

저장소 경계보다 이 seam이 먼저다. state 경계는 저장소가 아니라 backend key가 정한다 — 저장소를 옮겨도 key가 같으면 같은 state이고, 안 옮겨도 key를 나누면 나뉜다.

공유 비용은 하나가 아니라 셋이다

ap-northeast-2, 730h, AWS 공식 Price List (storage·LCU 제외):

자원 월 공유하려면 치를 값
VPC·subnet·IGW·ECS 클러스터 $0 없음
ALB $16.43 listener rule priority 거버넌스
Valkey $14.02 prefix 주입 + ACL/TLS — 앱 코드 변경 필요
RDS $18.25 별도 SQL provider + VPC 안 특권 runner

공유 RDS의 최소권한은 hashicorp/aws만으로 불가능하다. CREATE ROLE/GRANT는 SQL catalog 작업이라 cyrilgdn/postgresql 같은 provider가 필요한데, RDS는 data.tf publicly_accessible = false에 private subnet이고 data SG는 app SG만 허용해서 GitHub 호스티드 runner가 닿지 못한다. $18/월을 아끼려고 영구적인 특권 CI 표면을 사는 거래다. 안 사면 대안은 모든 서비스에 master credential 배포다.

→ 데이터 플레인은 서비스별, foundation은 공짜인 것들 + ALB만 공유.

변경

항목 이전 → 이후
이름 축 local.name 하나가 VPC부터 target group까지 전부의 이름 → foundation_name / service_name 분리, var.service_name 신규
SG rule inline ingress(authoritative) → aws_vpc_security_group_*_rule. 층이 갈리면 foundation apply가 service의 rule을 지운다
outputs split에 필요한 식별자 0개 → vpc_id, subnet ids, ALB SG id, listener arn, cluster arn, db subnet group
Redis 네임스페이스 앱엔 축이 있는데 terraform이 안 넘겨서 서비스 둘이면 app:/db 0 충돌 → service name에서 유도
이름 길이 substr(..., 0, 32) 조용한 절단 → lifecycle.precondition으로 plan에서 실패
backend key foundation/service key 규약 문서화

검증

실제 AWS 대상 plan이 처음으로 통과했다:

Plan: 33 to add, 0 to change, 0 to destroy.

validation/precondition 4종 발화 확인:

입력 결과
service_name=API must be lowercase alphanumeric with hyphens...
redis_db=16 must be an integer between 0 and 15.
project_name=verylongprojectnamehere + environment=production ALB name '...' exceeds 32 characters
service_name=averyveryverylongservicename Target group name '...' exceeds 32

뒤 둘이 이전엔 조용히 잘리던 것들이다.

그 외: fmt -check -recursive / init -backend=false / validate, check:scaffold(typeorm + postgres), check:observability, typecheck, lint, pnpm test → Test Files 35 passed (35) / Tests 317 passed (317).

미검증 / 남은 것

  • apply는 하지 않았다. plan 통과는 "AWS가 요청을 받아들일 것 같다"이지 "앱이 부팅한다"가 아니다. task 기동·health check·env 계약 정합은 미검증
  • ALB 라우팅(host-based / path-based)이 미결정. 지금 listener default action이 이 서비스의 target group을 직접 forward해서 서비스가 둘이 되면 성립하지 않는다. rule priority는 listener 안에서 유일해야 하므로(PriorityInUse) 대역 배정 규약이 함께 필요하다
  • 공유 Redis는 buildRedisOptions(src/config/redis.config.ts)가 password만 받고 username/TLS가 없어 ElastiCache RBAC를 못 쓴다 — 앱 변경 선행

교차검증: Claude(주 세션)와 Codex(orca 워크트리 infra-seam-audit, gpt-5.6-terra high) 2라운드. 읽기 전용.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DrsDBRXt7sYCYxt5gmFMVy

The boundary doc assumed one terraform copy per consumer. That premise is
wrong: frontend and other services share this infrastructure. With more
than one consumer, resources split into two layers with very different
change rates and blast radius, and the code mixed them.

Sharing cost is not one number. In ap-northeast-2 at 730h, VPC/subnets/
ECS cluster are free, ALB is $16.43, Valkey $14.02 and RDS $18.25 per
month. Complexity differs by an order of magnitude in the other
direction: a shared RDS with least-privilege users needs a SQL provider
that must reach a private-subnet instance, so it buys a permanent
privileged runner inside the VPC to save $18/month. The data plane stays
per-service; the foundation shares what is free plus the ALB.

- name axis: local.name was the name of everything from the VPC to the
  target group, so two services would collide on the ECS cluster name
  alone. Split into local.foundation_name and local.service_name, with a
  new service_name input.
- security groups: inline ingress/egress blocks are authoritative, so
  once the layers split a foundation apply would delete rules a service
  attached to a shared group. Moved to aws_vpc_security_group_*_rule.
- outputs: none of the identifiers a service root needs existed. Added
  vpc_id, subnet ids, ALB security group id, listener arn, cluster arn
  and db subnet group name as the foundation contract.
- Redis namespaces: the app already has REDIS_DB, REDIS_KEY_PREFIX and
  BULLMQ_PREFIX; terraform never passed them, so two services on one
  Valkey would both use 'app:' on db 0. Derived from service_name.
- names silently truncated at 32 characters via substr(); two long
  service names could collide and only show it at apply. Replaced with
  lifecycle preconditions that fail during plan.
- backend.hcl.example documents the foundation/service key convention.
  State boundaries come from the backend key, not from the repository.
- removed the unused local.redis_on.

Verified against real AWS: terraform plan reports 33 to add, 0 to
change, 0 to destroy — the first successful plan of this config. Not
applied, so task startup, health checks and the env contract remain
unverified.

ALB routing is still undecided. The listener default action currently
forwards straight to this service's target group, which stops working
with a second service.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DrsDBRXt7sYCYxt5gmFMVy
@iflov
iflov merged commit cd00b92 into main Sep 1, 2026
2 checks passed
@iflov
iflov deleted the iflov/foundation-service-seam branch September 1, 2026 01:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant