date
slug
author
status
tags(최대 3개)
summary
type
thumbnail
category
updatedAt
TL;DR
- 오랜 기간 쌓인 흔적을 걷어내고 운영 데이터와 유사한 조건에서 검증할 수 있는 stage 환경을 구축했습니다.
- 레거시 PHP(EC2)와 프론트엔드, 백엔드의 마이크로서비스인 EKS, ECS 등 3가지의 운영 환경을 분석해 stage에 담았습니다.
- AI Native 방식으로 운영 복제 대신 설정과 흔적을 분리해 빌드, 배포 주입 구조로 재구성했습니다.
무한 로딩의 원인
새로 구축한 stage 환경의 첫 화면은 200초 동안 로딩했습니다. 코드나 서버 사양 문제는 아니었습니다.
원인은 공용 서버 이미지 안에 하드코딩되어 있던 운영 세션의 저장소 주소였습니다.
DB와 캐시 주소는 배포 시점에 stage 값으로 덮어쓰고 있었지만 세션 주소만 그 목록에서 빠졌습니다. stage 망에서는 운영 세션 저장소 endpoint로 통신할 수 없었고, 앱은 세션을 시작할 때마다 타임아웃까지 기다렸습니다.
이 값을 이미지에서 걷어내 배포 시점에 주입하도록 바꾸자, 메인 페이지 응답은 0.04초로 돌아왔습니다.

문제는 설정값 하나지만, 그 안에는 13년 간의 흔적이 있었습니다.
이처럼 긴급 대응, 수동 변경, 임시 설정이 서버 이미지와 설정 파일 곳곳에 쌓였고, 아무도 히스토리를 자세히 알고 있는 사람이 없었습니다.
한 달동안 어떤 방식으로 AI와 운영 환경을 분석했고, 검증 가능한 stage 환경을 구축했는지 알아보겠습니다.
운영 데이터를 검증할 환경의 필요성
위험도가 높은 변경을 운영에 곧바로 넣는 일은 늘 부담이 큽니다.
앞서 인프라팀은 트래픽이 몰릴 때
writer DB 부하로 가용성이 흔들리던 지점을 캐싱과 읽기, 쓰기로 분리해 급한 부담부터 덜었습니다.앞선 작업이 현재 운영 DB를 더 안정적으로 쓰기 위한 개선이었다면, 다음 단계는 분산형 DB 도입과 마이그레이션이라는 더 큰 변경을 미리 검증하는 일이었습니다.
개발 환경은 있지만 데이터 규모가 운영과 비교할 수 없이 작았습니다.
실제 규모의 데이터와 데이터 간의 관계, 그 위를 흐르는 진짜 요청이 있어야 비로소 보이는 문제들이 있었습니다. 운영에 적용하기 전에 기존 사용자 흐름이 그대로 동작하는지 확인할 수 있는 운영에 가까운 환경이 필요했습니다.
그래서 stage 환경을 구축했습니다.
운영의 문제도 같이 복제
보통 stage 환경이라고 하면 운영 환경과 똑같은 환경을 하나 더 만드는 일로 생각하기 쉽지만, 조직의 상황은 많이 달랐습니다.
아임웹의 웹 서버는 트래픽 성격에 따라 여러 그룹(일반, 이벤트, 관리자, API 등)으로 나뉘어 운영되고 있습니다.
본질적으로는 같은 웹 서버인데, 운영환경의 인스턴스에 직접 들어가 전수 조사해 보니 그룹마다 PHP 설정 파일이 제각각이었습니다. OS 튜닝 값(파일 핸들 한도, TCP Connection 재사용 정책 등)도 서로 달랐습니다. 게다가 어떤 서버는 설정 파일의 symlink를 끊고 파일을 직접 덮어쓴 흔적까지 있었습니다.
더 큰 문제는 기준의 부재였습니다. 설정의 원본인 저장소는 실제 서버와 비동기된 지 오래였습니다.
급할 때마다 서버에 직접 접속해 수정해온 이력이 쌓인 것, 지금 서버에 있는 설정과 저장소에 있는 설정 중 어느 쪽도 정답이 아니었습니다.
그러다 보니 차이는 코드가 아니라 긴급 대응과 수동 변경, 임시 설정, 곧 운영의 흔적에서 생겼습니다. 이 상태에서 운영 서버를 그대로 복제하면 stage는 운영의 불확실성을 그대로 옮겨놓은 또 하나의 환경이 될 뿐이었습니다. 이는 안전한 실험실이 될 수 없습니다.
결국 stage 구축은 운영을 복사하는 일이 아니라, 실제로 필요한 기준을 다시 선별하고 최적의 값으로 환경을 구성하는 일입니다.
4가지 설정 분류
가장 먼저 운영 서버가 실제로 어떤 설정으로 동작하고 있는지 확인했습니다. 운영 서버에 남아 있는 모든 설정이 곧 정답은 아니었습니다.
당장 필요한 설정, 이전 방식의 흔적, 특정 장애나 긴급 대응 과정에서 적용된 임시 값이 혼재되어 있었습니다.
분류 | 설명 | 예시 |
이미지에 포함 | 모든 서버에 공통으로 필요한 실행 기반 설정 | OS, 언어 런타임, 웹 서버, 필수 패키지, agent 등 |
배포 시 주입 | 환경별로 달라지는 값 (→ 이미지에 포함하면 안 되는 값) | DB 접속 정보, secret, 세션 저장소, 환경별 도메인·경로 등 |
기존 흔적 | 예전 방식의 흔적 또는 더 이상 기준으로 삼기 어려운 설정 | 이전 버전 설정 파일, 사용하지 않는 모듈, 주석 처리된 값 등 |
검증 필요 | 실제 운영에서 사용 중인지 확인이 필요한 설정 | 특정 옵션, 로깅 설정, 보안 정책, 네트워크 설정, 스크립트 등 |
AI와 함께 작업을 분류했습니다. AI는 그룹별로 덤프한 설정을 비교해 구분하고, 그 차이가 의도된 것인지 흔적인지 최종 판단은 사람이 맡습니다.
3개의 파이프라인, 새로 구축한 것과 확장한 것
오래된 서비스인만큼 레거시 모놀리식부터 최신 마이크로서비스까지 성격이 다른 워크로드가 공존하고 있었고 stage는 이 세 갈래를 전부 담아야 했습니다.
워크로드 | 배포 방식 | 최종 타겟 |
레거시 모놀리식 (PHP) | Packer Golden Image + CodeDeploy | EC2 / 트래픽 성격별 ASG 7개 그룹 / ALB |
front-end 마이크로 서비스(ECS) | CloudFormation + ECR + ECS | ECS 클러스터 2개 |
Back-end 마이크로서비스(EKS) | GitOps (Helm PR) + ArgoCD | EKS (중앙 관리 클러스터와 hub-spoke) |
파이프라인 외에도 환경 전체를 새로 세웠습니다.
- 네트워크: stage 전용 VPC(용도별 서브넷 분리)와 도메인 체계 신규 구성
- IaC(Infrastructure as Code): Terraform Cloud + GitHub Actions, OIDC 기반 인증으로 정적 자격증명 0개
- DB: 운영과 유사한 규모·관계를 갖춘 stage 전용 RDS 3개 + 주기적 데이터 동기화 (접근은 내부 통제망으로 제한)
- 캐시·메시징: 캐시 클러스터 20여 개, Kafka 기반 메시지 브로커
- secret: 외부 시크릿 저장소에서 자동 주입 (External Secrets + Pod Identity)
- 배포 규격: 운영과 동일한 blue-green 무중단 배포 + 이벤트 기반 오토스케일링
PHP의 낡은 배포 구조로 처음부터 구축해야 했지만, ECS와 EKS는 개발과 운영 환경에서 이미 잘 동작하는 파이프라인이 있었습니다.
그렇기 때문에 기존 구조는 그대로 사용하고, stage 분기만 더해 개발자 입장에서는 환경이 늘었어도 배포 경험은 유지됩니다.
rsync가 하던 일을 둘로 나누기
AS-IS: 부팅할 때마다 중앙 서버 한 대에서 rsync
기존 운영 환경은 새 서버가 부팅하면 중앙 배포 서버 한 대에서
app source/library/config를 rsync로 당겨오는 방식입니다. 이 구조에서는 중앙 배포 서버가 죽으면 배포도, 장애 대응을 위한 긴급 증설도 전부 멈춥니다. 이것이 단일 장애점(SPOF, Single Point of Failure)입니다.서버 이미지 안에는 실제 설정 파일 대신 껍데기 symlink만 있고 알맹이는 부팅 후 rsync가 채웠습니다. 이미지만으로는 서버를 재현할 수 없고, 부팅 직후의 서버는 불완전한 상태였습니다.
즉, rsync는 파일을 복사할 뿐이라 배포 시점에 환경별로 값을 맞추거나 검증이나 롤백 단계를 끼워 넣지도 못합니다.
TO-BE: 굽는 건 Packer, 채우는 건 CodeDeploy
핵심은 무엇을 이미지에 고정하고 무엇을 배포 시점에 채워 넣을지, 그 경계를 “어떻게 구분하는가”입니다.
그 경계를 따라 이미지를 굽는 Build time은 Packer가, 값을 끼워 넣는 Runtime은 CodeDeploy가 맡습니다.
Packer란?
Packer는 HashiCorp의 오픈소스 머신 이미지 빌드 도구로, 코드로 쓴 템플릿 하나로 AMI 같은 서버 이미지를 자동으로 구워냅니다.
Build time에서 활용한 Packer는 서버에 올릴 기준 이미지(Golden Image)를 코드로 정의합니다.
이때 기준으로 삼은 것은 저장소의 설정 파일이 아니라 서버에서 실제로 돌고 있는 설정입니다.
트래픽 성격별로 나뉜 서버 그룹의 운영 EC2에 일일이 들어가 실제 설정을 전부 덤프합니다. 이 전수 조사는 서버 그룹별로 AI 세션을 나눠 병렬로 진행하고, 의도한 차이만 남긴 공통 이미지 하나로 통합했습니다.
저장소에 Merge되면 GitHub Actions의 matrix 전략으로 수정된 템플릿만 병렬로 굽고 결과는 Slack으로 전송합니다.
CodeDeploy란?
CodeDeploy는 AWS의 관리형 배포 서비스입니다.
배포 패키지를 EC2·ASG(Auto Scaling Group)에 내려보내고, 수명주기 hook 단계마다 스크립트를 실행해 설치와 검증을 자동화합니다. 또, 배포가 실패하면 마지막 정상 버전으로 자동 롤백합니다.
Runtime에는 AWS CodeDeploy를 활용했습니다.
앱을 빌드해 배포 패키지를 만들어 올리면 CodeDeploy가 각 서버 그룹(ASG)별로 배포합니다.
배포는
BeforeInstall(설치 전 준비) → AfterInstall(스토리지 마운트/환경값 렌더링) → ApplicationStart(문법 검증 후 재기동) → ValidateService(프로세스/헬스 검증)의 단계별 lifecycle hook으로 진행됩니다.배포하는 순간 파라미터 저장소에서 도메인, 세션, 캐시, DB 등 27종의 환경값을 읽어 설정을 렌더링합니다. 승인된 키만 주입되도록 allowlist로 통제해 정의되지 않은 값이 조용히 흘러드는 것을 차단합니다. 또한, 배포가 실패하면 마지막 정상 버전을 자동으로 재배포(rollback)합니다.
한눈에 보이는 배포
파이프라인을 새로 구축하면서 배포가 보이는 구조도 함께 만들었습니다.
알림 구조를 잡기 전에 방식부터 선택 했습니다.
첫 번째는 단계마다 새 메시지를 보내는 방식입니다. 구현은 가장 쉽지만, 배포할 때 마다 채널이 도배됩니다.
두 번째는 첫 메시지에 스레드로 누적하는 방식입니다. 채널은 깔끔하지만, 스레드 내에서 메시지가 휘발됩니다.
세 번째는 채택한 방식으로 메시지 하나를 계속 갱신하는 방식입니다.
GitHub workflow가 CI를 시작하고 각 단계(진행, 성공, 실패, 롤백)마다 설정해 놓은
EventBridge를 거쳐 Lambda가 받아 슬랙 메시지를 수정합니다. 메시지 생성과 갱신의 주체를 분리했기 때문에 동시 수정 충돌이 없고 알림이 실패해도 배포 자체에는 영향이 없게끔 동작합니다.관측, 비용 모니터링
배포가 보이니 환경 자체의 상태와 비용도 보이게 만들었습니다.
가장 먼저 보이게 만든 것은 빌드 시간입니다. 이미지 빌드는 로그 각 줄에 timestamp를 남겨 설치 구간별 소요시간을 재고 오래 걸린 순으로 정렬해 알림 스레드에 붙였습니다. 이로 인해 50분짜리 빌드에서 어디가 병목인지 스레드만 열면 알 수 있습니다.
서버, DB, 컨테이너 지표를 모으는 stage 모니터링 대시보드를 구성해 어느 리소스가 어떻게 움직이는지 한눈에 확인합니다.
마지막으로 비용도 감보다 실측으로 접근했습니다. stage 고정 비용을 리소스 단위로 분석해보니 최대 비용원은 VPC Endpoint와 NAT 였습니다. 이를 정리하고 추가로 stage가 평일에만 쓰인다는 점을 이용해 주말에는 워크로드를 자동으로 줄이는 스케줄링도 걸었습니다.
가장 큰 난관은 수많은 고객사 도메인
아임웹은 고객사가 연결한 도메인이 수십만 건에 이릅니다.
일반적으로 운영에서는 요청의 Host(도메인)를 보고 어느 서버 그룹으로 보낼지 분기하기 때문에 stage에서도 이 흐름을 그대로 검증해야 합니다. 하지만 문제는 고객사 도메인의 DNS가 당연히 운영을 가리켰고, stage에서 운영 도메인 그대로 접근할 방법이 필요했습니다.
고민 끝에 세 가지 안을 비교했습니다.
도메인 치환
고객 도메인 뒤에 stage 도메인을 붙여 접근(예:
shop-a.com.stage.example)하고, 프록시가 접미사를 떼어 원 도메인을 복원합니다. 하지만 wildcard 인증서 특성상 상위 몇 개 TLD(최상위 도메인) 패턴만 커버할 수 있습니다. 수십만 건 전체를 담을 수 없고 접근 도메인이 달라지는 순간 도메인 기반 분기가 운영과 미묘하게 달라집니다.내부 DNS 정책과 TLS 재서명 프록시 설정 방안
내부 검증망에서 운영 도메인을 그대로 입력하면 DNS가 stage proxy로 유도하고 proxy가 내부 CA로 해당 도메인의 인증서를 즉석 발급해 HTTPS까지 완전히 재현합니다. 재현 완성도는 가장 높지만 내부 CA가 사실상 중간자(MITM) 권한을 갖게 되고, CA 키 보관, 발급 감사, 단말 통제 같은 보안 통제 비용이 검증 환경 하나에 걸기에는 너무 컸습니다.
채택: 내부망 전용 DNS Proxy
내부 검증망에서 운영 도메인을 질의하면, DNS Proxy가 도메인 매핑 정보를 조회해 stage ALB 주소로 응답합니다. HTTPS 대신 HTTP로 제한되지만 사용자는 운영 도메인 그대로 접근하고 도메인 기반 분기 로직도 운영과 동일하게 검증합니다. 추가 보안 통제도 필요 없었습니다.
AI를 동료로 삼은 방법

세 갈래의 서버 운영 환경, 오랜 기간 누적된 설정, 수많은 도메인을 혼자 다루기엔 너무 넓었습니다. 개발팀도 각자의 우선순위가 있어 상시 지원을 기대하기 어려웠습니다.
그래서 접근 방식을 바꿨습니다.
필요할 때만 AI를 호출하는 방식이 아니라, 조사, 구축, 검증까지 모든 단계를 AI와 함께 진행하는 구조로 일을 다시 설계했습니다. 단순한 코드 생성이 아니라 운영 환경을 함께 읽고 판단을 보조하는 동료로 두는 AI Native 방식이었습니다.
실제로 AI를 코드 작성보다 읽는데 많이 썼습니다. 13년치 흔적이 남은 서버와 설정을 사람이 파악하기에는 물리적인 한계가 있기 때문입니다.
물론 AI에게 그대로 맡겨두면 오히려 작업이 흔들린다는 것도 일찍 체감했습니다.
AI를 많이 쓰는 것보다 중요한 건, AI가 읽고 판단하는 범위를 사람이 검증할 수 있는 흐름으로 설계하는 것이었습니다.
AI를 동료로 쓰는 4가지 원칙
탐색은 AI, 판단은 사람
설정 파일 비교, 로그 탐색, 원인 정리 등은 AI에게 맡겼습니다. 대신 어떤 결과를 기준으로 삼을지, 어떤 변경을 실제로 반영할지는 사람이 판단했습니다.
영역을 나눠 병렬로 조사
서버 이미지, 배포 파이프라인, 애플리케이션 의존성이 각각 별도의 세션이었습니다. 혼자 순서대로 봤다면 오래 걸렸을 내용을 훨씬 짧은 시간 안에 구조화 했습니다.
추측은 반드시 실측으로
AI가 그럴듯한 가설을 내놓아도 그대로 믿지 않았습니다. 로그, 설정, 배포 결과, 실제 응답으로 사실로 바꾼 뒤에야 다음 단계로 넘어갔습니다.
목표 고정과 반복 검증
빌드 실패, 누락된 패키지, 권한 문제처럼 새 이슈가 계속 드러나는 상황에서 AI를 눈앞의 문제만 따라가게 두면 목표가 흐려지고 시간과 토큰만 소모합니다. 그래서 “운영에서 실제로 필요한 설정을 분석하고, stage에서 재현 가능한 기준을 만든다”는 최종 목표를 세션 자체에 걸어두고, AI가 그 목표를 향해 탐색과 검증을 반복하게 했습니다.
/goal로 시작하는 루프 엔지니어링
여기서 제 역할은 매 단계 지시를 내리는 것이 아니라 AI가 도는 루프 자체를 설계하는 것, 말하자면 루프 엔지니어링이었습니다.
/goal 로 세션에 최종 목표를 박아두면 AI는 탐색 → 실행 → 검증 → 목표 재정렬의 루프를 스스로 돕니다. 눈앞의 이슈가 몇 번을 바뀌어도 매 반복마다 지금 하는 일이 목표에서 얼마나 벗어났는지를 스스로 되짚는, 일종의 자기 교정 루프입니다.

빌드 실패를 쫓다 보면 초점이 워크플로 경로 문제로, 다시 인프라 상태 확인으로 옮겨갑니다. 그동안에도 루프는 목표를 놓치지 않고 다음 지점을 계속 좁혀갑니다.
이전 반복의 실패 이력과 판단을 그대로 이어받기 때문에 같은 가설을 두 번 검증하는 낭비도 없습니다.
사람이 개입하는 순간은 루프의 매 바퀴가 아닌 경계를 벗어나려 할 때입니다.

서버 설정을 이미지로 통합한다는 목표 하나에 test → fix → docs 커밋 80개가 쌓였습니다.
겉보기엔 두더지 잡기 같지만, 방향은 한 번도 바뀌지 않았습니다. 하나의 목표를 향한 80바퀴의 루프 기록입니다.
마치며
한 달간의 작업을 전과 후로 정리해봤습니다.
항목 | Before | After |
배포 구조 | 부팅 시 중앙 서버 1대에서 rsync (SPOF) | Golden Image + CodeDeploy 그룹별 배포 |
설정 관리 | 서버마다 다른 13년치 스냅숏, 저장소와 불일치 | 코드로 정의된 공통 이미지 + 배포 시점 주입 |
환경값 변경 반영 | 이미지 재빌드 40분~1시간 | 배포 1회 |
배포 가시성 | 로그 나열, 상태 추적 불가 | Slack 메시지 1개로 생애주기 추적 (롤백 포함) |
상징적 사례 | 메인 페이지 응답 200초 (타임아웃) | 0.04초 |
소요 기간 | 혼자 진행 시 추정 반년~1년+ | 1개월 (1명 + AI) |
다만 이 숫자들은 같은 코드와 DB, 도메인을 붙였다고 운영과 같은 환경이 아님을 말해줍니다.
요청이 어떤 DNS와 로드밸런서를 거쳐 어느 서버 그룹에 도달하는지, 어떤 설정은 이미지에 고정되어 있고 어떤 값은 배포 시점에 주입되는지 알아야 했습니다.
이 흐름을 설명할 수 있어야만, 비로소 운영에 가까운 환경을 만들고 있다고 말할 수 있었습니다.
검증할 수 있는 환경과 지속가능한 방식
이번 stage 구축은 13년 간 아무도 손대지 못한 운영 환경을 설명 가능한 상태로 만드는 일이었습니다.
stage 환경에서 올해 인프라팀의 목표인 분산형 DB 마이그레이션처럼, 운영에서 부담스러웠던 큰 변경들을 검증할 수 있게 되었습니다.
또한 목표를 고정하고, AI가 탐색과 검증의 루프를 돌고, 사람은 그 루프를 설계하는 방식이 함께 남았습니다.
혼자서는 반년 넘게 걸렸을 일이 한 달로 줄어든 것은 도구의 힘이 아니라 방식의 차이였고, 앞으로도 같은 방식으로 진행할 예정입니다.
이호수 DevOps
같은 실수를 두 번 하지 않으려고 경험을 글로 남깁니다.
추측 대신 실측으로 EC2부터 EKS까지 안전하게 인프라를 운영합니다.
