Insight 마케팅·문서 포털 인프라 — Pulumi IaC와 GitLab CI/CD 통합

사내 마케팅 사이트(Insight v2)와 외부 제품 문서 포털의 인프라·CI/CD 트랙을 맡았습니다. 두 빌드 산출물을 단일 Pulumi(TypeScript) stack으로 AWS에 정의하고, GitLab CI 기반 S3 배포와 CloudFront 무효화 흐름을 표준화했으며, MR마다 자기 호스트를 갖는 리뷰 환경을 엣지 함수 라우팅으로 구성했습니다. v1→v2 전환은 도메인·인증서·DNS를 바꾸지 않고 CloudFront 오리진만 교체하는 방식으로 진행했습니다.

Architecture

동기

마케팅 사이트는 Docusaurus 로 만든 v1 에서 Next.js 로 만든 v2 로 옮겨 가는 중이었고, 같은 시기에 외부 제품 네 종의 한글 문서를 모은 별도 포털이 새로 만들어지고 있었습니다. 두 산출물은 만드는 사람도 빌드 스택도 다른데, 올라가는 자리와 배포되는 방식은 같아야 관리가 됩니다. 그 자리를 한 벌로 정의하는 것이 이 카드의 일입니다.

내가 담당한 트랙

인프라와 CI/CD 가 제 쪽입니다 — Pulumi 로 두 포털의 AWS 자원을 정의하고, 배포 파이프라인과 리뷰 환경을 만들고, 전환 때 두 사이트가 같이 살아 있게 하는 부분입니다. 항목별 내역은 오른쪽 「주요 업무」에 있습니다.

트랙경계
프레임워크 포팅(Docusaurus → Next.js) · 컴포넌트 재설계 · 콘텐츠 모델다른 사람 몫입니다. v2 앱 저장소 커밋의 대부분이 그쪽이고, 제 커밋은 CI 와 배포 경로에 몰려 있습니다
다중 에이전트 문서 번역 스킬다른 트랙입니다. 문서 포털의 번역 산출물이 그쪽에서 나옵니다

두 포털을 한 stack 에

TypeScript 파일 하나에 두 포털의 자원을 다 적었습니다. 버킷 여섯 개, CloudFront 배포 네 개, 엣지 함수 세 개, 인증서 두 장입니다. 둘이 공유하는 것과 각자 갖는 것을 코드에서 갈라 두고, 나머지는 한 화면에서 봅니다. 나눠 두면 어느 쪽이 최신인지를 사람이 기억해야 하고, 새 포털이 생길 때마다 그 기억이 하나씩 늘어납니다.

시작은 권한에서 막혔습니다. 손에 있던 자격이 읽기 전용이라 버킷을 만들 수도 CloudFront 설정을 바꿀 수도 없었습니다. 운영 계정에 이 stack 전용 배포 역할을 만들고, 관리 계정에서 그 역할로 들어오는 경로만 열었습니다. 로컬에 두는 자격은 그 역할을 가리키는 프로필 하나이고, Pulumi 상태 파일은 전용 버킷에 둡니다.

역할의 범위는 리소스 단위로 못 박았습니다. S3 는 이 stack 이 소유한 버킷의 ARN 으로, Route53 은 호스팅 존 하나로 한정해 같은 계정의 다른 자산에는 손이 닿지 않습니다. CloudFront 와 인증서는 리소스 단위로 좁히기 어려워 할 수 있는 동작을 열거하는 데까지만 갔습니다.

도메인을 안 건드리고 v1 에서 v2 로

전환 방식이 두 갈래였습니다. v2 용 CloudFront 배포를 새로 만들어 도메인을 그쪽으로 옮기는 길은 같은 도메인을 두 배포에 동시에 걸 수 없다는 제약에 걸립니다. 이전 배포에서 떼고 새 배포에 붙이는 사이 몇 분간 접속이 안 되고, 그래서 계획서의 완화책이 트래픽 적은 시간대에 하는 것이었습니다. 다른 길은 이미 도메인을 들고 있는 배포의 오리진만 v2 버킷으로 바꾸는 것입니다.

뒤쪽을 택했습니다. 배포 ID 도 도메인도 인증서도 DNS 레코드도 그대로 두고 바뀐 것은 오리진과 엣지 함수, 그리고 v1 이 쓰던 SPA 폴백 에러 응답을 지운 것뿐입니다. 되돌리는 것도 같은 한 수라 v1 버킷은 전환 뒤에도 지우지 않고 남겨 뒀습니다.

MR 마다 자기 주소를 주기

리뷰 환경은 처음에 경로로 갈랐습니다. 리뷰 버킷의 /<MR번호>/ 아래에 올리고 같은 접두를 URL 로 쓰는데, 그러면 산출물 안의 절대 경로가 전부 어긋납니다. 그래서 빌드에 기준 경로를 넣어 다시 굽고, 그러고도 남는 /img/... 같은 참조는 CI 가 결과물의 HTML·CSS·JS 를 훑어 문자열로 고쳐 쓰고 있었습니다.

지금은 호스트가 가릅니다. 리뷰어는 review-<MR번호> 로 시작하는 주소를 열고, 엣지 함수가 요청이 들어온 단계에서 Host 헤더를 읽어 MR 번호를 뽑아 오리진 요청 경로 앞에 붙입니다. 패턴이 안 맞으면 404 로 끊고, 옛 경로 형태로 들어온 요청은 접두를 떼어 호스트 주소로 301 을 보냅니다. 리뷰 호스트 전체를 덮는 와일드카드 인증서와 DNS 레코드를 코드로 발급해 두어서 MR 이 늘어도 인프라에 손댈 일이 없습니다.

바뀐 것은 산출물을 안 건드리게 됐다는 점입니다. 같은 빌드 결과가 어느 MR 에서든 그대로 동작하고, 결과물을 문자열로 고쳐 쓰던 단계가 CI 에서 사라졌습니다.

정리는 두 겹입니다. MR 이 닫히면 그 번호의 접두를 바로 지우고, 남는 것은 30일 만료 규칙이 받습니다.

매번 다시 설치하던 것들

프로덕션 배포 잡은 실행할 때마다 파이썬 패키지 관리자로 AWS CLI 를 새로 받고 있었고, 매번 2분에서 3분이 여기서 갔습니다. 배포판이 제공하는 패키지로 바꾸니 2초쯤에 끝납니다. 여기에 의존성 캐시를 붙이고, 매번 달라질 수 있는 이미지 태그를 고정하고, 서로 기다릴 이유가 없는 잡을 병렬로 돌리고, 두 배포가 겹치지 않도록 잠금을 걸었습니다. 준비 단계에 3분에서 4분 걸리던 것이 38초가 됐습니다.

같이 고친 것이 하나 더 있습니다. 배포가 끝나면 알림이 오게 돼 있었는데 몇 주째 안 오고 있었습니다. 알림을 보내는 쪽이 잡 이름으로 대상을 찾는데 파이프라인을 손보면서 그 이름이 바뀌어 있었습니다. 찾는 이름이 없으니 실패가 아니라 계속 기다리다 두 시간 뒤에 타임아웃으로 끝났고, 그래서 배포는 되는데 아무 말이 없는 상태가 유지됐습니다.

계정 비용을 뜯어보기

운영 계정 전체의 비용을 한 번 훑었습니다. Cost Explorer 와 CloudWatch, 버킷 오브젝트, 클러스터를 직접 재서 어디에 얼마가 나가는지 항목별로 정리하고 보고서와 발표 자료로 만들었습니다.

가장 확실했던 것은 노드 루트 디스크의 IOPS 였습니다. 기본값의 세 배가 넘게 잡혀 있는데 실측 피크가 두 자리였습니다. 값을 낮추는 것 자체는 코드 한 줄인데, 그 한 줄을 근거 있게 만드는 데 필요한 것이 실제 사용량이었습니다. IaC 를 고쳐 노드가 교체되는 것까지 확인했고 월 $140 이 빠졌습니다.

나머지는 목록으로 남겼습니다. 확장 지원 기간에 들어가 붙는 과금, 약정 할인 미적용, 설정은 돼 있는데 실제로는 안 지워지고 있던 로그 보존 같은 것들입니다. 각각 금액과 위험을 붙여 두었고 아직 손대지 않았습니다.

배운 점

  • 격리를 어느 계층에서 하느냐가 그 뒤 손댈 곳의 수를 정했습니다. 경로로 가르면 산출물이 자기 경로를 알아야 해서 빌드와 CI 후처리까지 리뷰 환경의 사정을 알게 되는데, 호스트로 가르니 그 지식이 엣지 한 곳으로 모였습니다.
  • 배포가 되는데 아무 말이 없는 상태가 몇 주 갔습니다. 알림이 실패로 끝났으면 바로 알았을 텐데, 찾는 대상이 없어 기다리다 타임아웃이 났습니다. 파이프라인을 고칠 때 거기에 무엇이 결합돼 있는지 같이 봐야 했습니다.
  • 비용을 줄이는 조치는 대개 설정 한 줄이었고, 오래 걸린 것은 그 한 줄을 눌러도 된다는 근거를 만드는 쪽이었습니다. 근거는 실제 사용량을 재는 데서 나왔습니다.