이번 글은 만든 서버를 실제로 운영하기 위한 배포 계획과, 그 첫 단계인 AWS 인프라 구축 과정을 정리한다. 운영과 배포는 이번 부트캠프에서 개념으로만 접했고 구체적인 절차를 몰랐기 때문에, 코드를 올리기에 앞서 무엇을 어떤 순서로 준비해야 하는지부터 계획으로 세웠다. 계획의 결정 사항, 코드 쪽 준비, 계정 정리부터 EC2, RDS, ECR 구축과 검증까지의 과정, 그리고 앞으로의 단계를 남긴다.
main에 병합하면 테스트 → 빌드 → 배포가 자동으로 이루어지는 CI/CD까지 적용한다.개발할 때는 내 PC에서 내가 직접 켜고 끄고, 문제가 생기면 화면을 보고 고친다. 운영은 이 전제가 모두 깨진다.
| 개발 | 운영 |
|---|---|
| 내가 켜고 끈다 | 아무도 지켜보지 않아도 24시간 돌아야 한다 |
| 내 PC 환경에서 돈다 | 남의 서버에서 돈다. 환경이 다르다 |
비밀번호를 .env에 둬도 된다 | 비밀값이 새면 사고다 |
| 죽으면 다시 켠다 | 스스로 살아나야 하고, 죽었다는 것을 알아야 한다 |
| 고치고 껐다 켜면 된다 | 사용자가 쓰는 중에 새 버전으로 교체해야 한다 |
그래서 운영 환경에서는 다음 질문에 답을 할 수 있어야 한다.
| 항목 | 현재 상태 | 배포에서 알아야 할 점 |
|---|---|---|
| 런타임 | Java 21, Spring Boot 4.1.1, Gradle Wrapper | 실행 가능한 jar 하나로 빌드된다 |
| DB | MySQL 8.4, Flyway, ddl-auto=validate | RDS도 8.4로 맞춘다. 스키마는 서버가 시작할 때 Flyway가 적용한다 |
| 설정 | DB_URL, DB_USERNAME, DB_PASSWORD, JWT_SECRET 환경변수로 덮어쓰기 가능 | 코드 수정 없이 주입만 하면 된다 |
| 게임 콘텐츠 | resources/content/가 jar에 포함되고, 시작할 때 검증한다 | 콘텐츠가 잘못되면 서버가 시작하지 않는다 |
| CI | Checkstyle만 실행 | 빌드·테스트 CI는 "후속 단계"로 남겨 둔 상태였다 |
| 헬스체크 | 없음 | 배포 직후 정상 기동을 확인할 수단이 필요하다 |
계획을 세우면서 갈림길마다 정한 것들이다.
| 항목 | 결정 | 근거 |
|---|---|---|
| 배포 방식 | Docker 이미지 + ECR | 실행 환경이 어디서든 같고, 이미지 태그로 되돌릴 수 있다 조금 더 간단한 방법도 있는 듯했지만, 학습 목적으로 이 방식을 택했다 |
| 접속 방식 | GitHub OIDC + SSM (SSH 포트·장기 키 없음) | 열린 로그인 경로와 관리할 키를 없앤다 |
| HTTPS | 도메인 + Nginx + Let's Encrypt | 아래 참고 |
| 배포 트리거 | main 푸시 | 기본 브랜치는 dev지만 배포 기준은 main이다 |
| 리전 / 환경 | 서울 / 운영 1개 | 데모 및 평가 용도에는 충분하다 |
| CPU 아키텍처 | x86_64 | 아래 참고 |
| 비용 | 월 예산 알림 + 크레딧 잔액 확인 | 크레딧 기반의 무료 플랜 계정이다 |
학습할 때는 ARM(Graviton) 인스턴스가 동급 x86보다 성능이 좋고 시간당 약 20% 저렴하다고 배웠다. 이 서버는 순수 Java라서 ARM에서도 돌아가고, 소형 인스턴스 1대 기준으로는 월 몇 달러 차이다. 그렇지만 첫 배포는 x86으로 정했고, 근거는 다음과 같다.
exec format error로 시작조차 못 한다. 처음 구축하는 파이프라인에서 원인 찾기가 어려운 오류다.클라이언트가 웹 주소를 입력해서 접속하는 구조가 아니니 IP로 바로 연결하면 되지 않느냐는 의문이 있었다. 기술적으로는 가능하지만 도메인을 쓰기로 했다.
정식 도메인은 팀 논의 후 구매하기로 하고, 그동안은 임시 무료 도메인(DuckDNS) 으로 HTTPS까지 검증한다. 클라이언트의 서버 주소는 하드코딩되지 않는 설정값이라, 도메인을 바꿔도 설정 한 줄만 바꾸면 된다. 전환 기간에는 두 도메인을 함께 서비스한다.
Unity 클라이언트
│ HTTPS (443)
▼
[EC2: Nginx(리버스 프록시) → Docker 컨테이너(Spring Boot, 8080)] ← public subnet
│ MySQL (3306, 앱 서버의 보안그룹에서 오는 연결만 허용)
▼
[RDS MySQL 8.4] ← private subnet, 외부 접근 불가
GitHub Actions: main 푸시 → 테스트 → 이미지 빌드 → ECR 푸시 → (SSM) EC2에서 pull·교체 → 헬스체크
원칙은 세 가지다.
학습 중에 쓰던 계정(무료 크레딧이 제공되는 무료 플랜)을 그대로 쓰기로 했는데, 실습 때 만든 리소스가 남아 있었다. 새로 만들기 전에 먼저 계정을 정리했다. 이 부분은 이번 프로젝트와 직접적인 연관은 없지만, 추후 참고할 수 있도록 과정을 남겨둔다.
실행 중인 RDS가 가장 먼저 크레딧을 소모하므로 RDS부터 지웠다. 남길 데이터가 없어서 최종 스냅샷과 자동 백업 보존도 만들지 않았다.
VPC는 RDS가 완전히 사라진 뒤에야 지울 수 있다. RDS가 VPC 안에 네트워크 인터페이스를 가지고 있기 때문이다. 그래서 RDS가 "삭제 중"인 동안에는 기다렸다가 확인한 뒤 VPC를 삭제했다. 삭제는 항목마다 삭제 직전 화면을 확인하고 진행했다.
IAM 사용자 s3-camp-health-user는 S3 권한만 있었고 대상 버킷이 이미 없어서 액세스 키를 비활성화한 뒤 정책과 함께 삭제했다. 그런데 이때까지 IAM 역할, RDS 서브넷 그룹, Parameter Store 파라미터는 점검 대상에 넣지 않았다. 새로 만든 역할 목록에서 이전 실습의 역할(test-ec2-ssm-readonly)이, 서브넷 그룹 목록과 Parameter Store에서 이전 실습의 서브넷 그룹과 /webcraft/prod/*가 보였다. 비용은 없지만 정리가 덜 된 상태라 미정리 항목으로 계획서에 기록해 두었다. 특히 /webcraft/prod/DB_PASSWORD는 평문 String으로 저장되어 있어서(삭제된 DB의 값이긴 하다) 지우는 것이 맞다.
정리할 때는 "컴퓨팅·네트워크·DB"만 보지 말고 IAM, 설정 저장소, 서브넷 그룹까지 목록을 넓혀야 한다는 것을 배웠다.
루트는 계정을 만든 이메일 로그인으로 권한에 제한이 없다. 평소 작업은 IAM 사용자로 하고 루트는 잠가 두는 것이 기본이다. 루트 자격이 유출되면 계정과 결제 전체가 위험하지만, IAM 사용자는 정책 범위와 삭제로 피해를 막을 수 있다.
| 신분 | 쓰는 쪽 | 방식 | 이번 계획에서 |
|---|---|---|---|
| IAM 사용자 | 나(콘솔 작업) | 비밀번호 + MFA | 0~4단계 |
| EC2 인스턴스 역할 | 서버(EC2) | 역할을 인스턴스에 부여, 키 없음 | 2단계 |
| GitHub Actions용 역할 | CI/CD | OIDC로 임시 자격, 키 없음 | 6단계 |
사람의 신분과 기계의 신분을 나누고, 기계에는 장기 액세스 키를 주지 않는다는 것이 이 계획의 핵심이다.
gwangmin을 만들고 그룹(admins)에 소속시켰다. 사용자에 정책을 직접 붙이지 않고 그룹에 붙이는 편이, 나중에 조회 전용 팀원이 생겨도 viewers 그룹만 추가하면 되어 확장하기 쉽다. 콘솔 로그인과 가상 MFA를 설정했고, 액세스 키는 만들지 않았다.권한은 개인 학습 계정이라 AdministratorAccess로 두었다. 실무라면 필요한 서비스만 허용하는 정책으로 줄이는 것이 맞고, 선택 과제로 남겨 두었다.
AWS에 올리기 전에 코드 쪽에서 준비한 것들이다. 작업 단위로 나눠 커밋했다.
배포한 뒤 새 서버가 정말 정상인지 기계가 물어볼 창구가 필요하다. GET /actuator/health를 추가했다.
503 DOWN을 반환한다.show-details=never로 DB 종류, 디스크 같은 내부 정보를 숨기고, 웹에는 health 하나만 노출한다. Actuator에는 환경변수나 설정을 보여 주는 위험한 경로가 있어서다./actuator/health/**만 토큰 없이 열었다. 배포 스크립트는 토큰이 없기 때문이다. 나머지 경로는 그대로 토큰이 필요하다./actuator/health는 200이고 상세 정보가 없다. /actuator/env는 401이다).server.shutdown=graceful, 종료 대기 20초: 배포는 곧 컨테이너를 종료하고 교체하는 것이다. 종료 신호를 받으면 처리 중인 요청을 마친 뒤 내려가서 결산 저장 같은 요청이 중간에 끊기지 않는다.server.forward-headers-strategy=framework: 앞단의 Nginx가 넘기는 X-Forwarded-* 헤더로 원래 클라이언트 정보를 복원한다. 앱 포트(8080)를 외부에 직접 열지 않는 것이 전제다. 열려 있으면 헤더를 위조할 수 있기 때문이다..dockerignoreFROM eclipse-temurin:21-jdk AS build # 빌드: JDK + bootJar
...
FROM eclipse-temurin:21-jre # 실행: JRE + jar만
RUN useradd --system ... blackhole
USER blackhole
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=70", "-XX:+ExitOnOutOfMemoryError", "-jar", "/app/app.jar"]
.dockerignore로 .env가 빌드 컨텍스트에 들어가지 않게 했다.MaxRAMPercentage=70은 컨테이너 메모리의 70%까지만 힙으로 쓰게 한다. ExitOnOutOfMemoryError는 메모리가 바닥나면 어정쩡하게 살아 있지 않고 즉시 죽게 해서, 재시작 정책이 다시 띄우게 한다. 반쯤 살아 있는 서버가 가장 위험하다.java가 PID 1이 되도록 exec 형식을 써서, 종료 신호(SIGTERM)를 직접 받아 graceful shutdown이 동작한다.bootJar의 파일명을 app.jar로 고정해서 버전이 바뀌어도 Dockerfile을 고치지 않게 했다.설정이 맞아 보여도 돌려 보기 전에는 모른다. AWS에 올리기 전에 내 PC에서 운영 환경과 비슷하게 확인했다.
./gradlew build가 통과했다(테스트 361개).UP을 반환했다. 이미지 안에 .env가 없고 비루트로 실행됨을 확인했다.503 DOWN이 나왔고, 다시 켜면 자동으로 UP이 됐다. 다만 DB가 끊겼을 때 응답까지 약 30초가 걸렸다(DB 연결 대기 기본값). 6단계의 배포 스크립트는 헬스체크 타임아웃을 40초 이상으로 잡아야 한다.콘솔에서 하나씩 직접 만들었다. 각 구성요소의 역할을 이해하려는 목적이다. 리소스 이름은 blackhole- 접두사로 통일했다.
서울 리전에는 기본 VPC가 없어서 VPC를 새로 만들었다. 콘솔의 "VPC 등" 마법사로 만들었다.
| 항목 | 값 |
|---|---|
| 범위 | 10.0.0.0/16 |
| 가용 영역 | 2개 |
| 서브넷 | 퍼블릭 2 + 프라이빗 2 |
| 게이트웨이 | 인터넷 게이트웨이 + S3 게이트웨이 엔드포인트(무료) |
| NAT 게이트웨이 | 없음 |
NAT 게이트웨이는 시간당 요금이 커서 만들지 않았다. 앱 서버는 퍼블릭 서브넷에 두어 퍼블릭 IP로 인터넷에 나가고, 인터넷에서 직접 접근할 필요가 없는 DB만 프라이빗 서브넷에 둔다. 프라이빗 서브넷이 외부로 나갈 일이 없으므로 NAT가 필요하지 않다.
보안그룹은 인스턴스 앞의 가상 방화벽이다.
| 이름 | 허용하는 인바운드 | 비고 |
|---|---|---|
blackhole-sg-app | 80, 443 (전체) | SSH(22)는 열지 않았다 |
blackhole-sg-db | 3306, 출처가 blackhole-sg-app | IP가 아니라 보안그룹을 출처로 지정 |
DB의 출처를 IP 대역이 아니라 앱 서버의 보안그룹으로 지정했다. 그러면 앱 서버의 IP가 바뀌거나 서버가 늘어나도 규칙을 고칠 필요가 없고, 그 보안그룹이 붙은 인스턴스만 DB에 닿는다.
먼저 DB 서브넷 그룹을 만들었다. RDS를 어느 서브넷들에 둘지 정하는 것으로, 서로 다른 AZ의 서브넷 2개가 필요하다. 마법사가 자동으로 만드는 서브넷 그룹은 퍼블릭 서브넷까지 포함할 수 있어서, 프라이빗 서브넷 2개만 골라 직접 만들었다.
| 항목 | 값 |
|---|---|
| 엔진 | MySQL 8.4.11, 단일 AZ |
| 인스턴스 | db.t4g.micro (RDS는 관리형이라 앱 이미지의 CPU 아키텍처와 무관) |
| 스토리지 | gp2 20GiB, 암호화 |
| 네트워크 | 위 VPC·서브넷 그룹, 퍼블릭 액세스 끔, 보안그룹은 blackhole-sg-db만 |
| 초기 DB | feedablackhole |
| 보호 | 삭제 방지 켬 |
무료 플랜 계정이라 몇 가지가 정해져 있었다. 템플릿은 "프리 티어"와 단일 AZ로 고정되고(Aurora와 일부 구성은 선택할 수 없다), 자동 백업 보존 기간은 1일까지만 허용됐다. 계획대로 7일로 만들려다가 생성이 실패했고(The specified backup retention period exceeds the maximum available to free tier customers) 1일로 낮춰 다시 만들었다.
별도의 파라미터 그룹은 만들지 않았다. 기본 default.mysql8.4가 이미 문자셋 utf8mb4, 시간대 UTC이기 때문이다. 나중에 직접 확인해서 이 가정이 맞는 것도 확인했다.
마스터 비밀번호는 RDS가 생성 후에는 보여 주지 않으므로 20자 무작위 값을 직접 만들어 입력하고 비밀번호 관리자에 저장했다.
이미지 저장소(feedablackhole-server)를 만들었다.
EC2가 쓸 역할(blackhole-ec2-role)을 만들었다. 서버가 AWS를 호출할 때 키 없이 임시 자격을 받는 신분이다.
| 권한 | 목적 |
|---|---|
AmazonEC2ContainerRegistryReadOnly | ECR에서 이미지를 받는다 |
AmazonSSMManagedInstanceCore | SSM으로 서버를 관리하고 명령을 받는다 |
blackhole-read-parameters (직접 작성) | Parameter Store의 /blackhole 경로 파라미터를 읽는다 |
직접 만든 정책은 읽기 동작(GetParameter, GetParameters, GetParametersByPath)을 blackhole 경로 아래 리소스로만 제한했다. 이 정책은 나중에 한 번 고쳤다(8장).
| 항목 | 값 | 이유 |
|---|---|---|
| AMI | Amazon Linux 2023 (x86_64) | 위의 아키텍처 결정 |
| 유형 | t3.small (2vCPU, 2GiB) | t3.micro(1GiB)는 JVM과 Docker에 빠듯하다 |
| 네트워크 | 퍼블릭 서브넷, 퍼블릭 IP, blackhole-sg-app | |
| 키 페어 | 없음 | 접속은 SSM으로만 한다 |
| 스토리지 | 20GiB gp3, 암호화 | 이미지 10개와 로그를 고려 |
| 인스턴스 프로파일 | blackhole-ec2-role | |
| 보호 | 종료 방지, IMDSv2만 허용 | 실수로 인한 종료 방지, 메타데이터 탈취 공격 방지 |
| 크레딧 사양 | 표준 | 아래 참고 |
| 사용자 데이터 | Docker 설치, 로그 크기 제한, 부팅 시 자동 시작 |
blackhole-sg-app을 선택했다.daemon.json으로 컨테이너 로그를 10MB × 3개로 제한하는 설정을 넣었다. 제한하지 않으면 로그가 디스크를 채울 수 있다.탄력적 IP도 할당해 연결했다. 자동 할당 퍼블릭 IP는 인스턴스를 중지했다가 시작하면 바뀌지만, 탄력적 IP는 고정이다. 지금은 퍼블릭 IPv4 주소 자체에 요금이 붙어서 둘의 비용은 거의 같다. 단 인스턴스를 삭제할 때 탄력적 IP를 반납하지 않으면 계속 과금되니 정리할 때 확인해야 한다.
앱이 환경변수로 받을 값을 SecureString으로 저장했다(암호화 키는 기본 aws/ssm).
| 이름 | 값 | 비고 |
|---|---|---|
/blackhole/DB_URL | RDS 엔드포인트와 sslMode=REQUIRED | 연결 암호화 |
/blackhole/DB_USERNAME | 앱 전용 계정 | |
/blackhole/DB_PASSWORD | 앱 전용 계정의 비밀번호 | 직접 입력 |
/blackhole/JWT_SECRET | 32자 이상 무작위 문자열 | 직접 입력. 로컬 .env와 다른 값 |
비밀값 두 개는 화면을 보고 있는 도구가 아니라 내가 직접 입력했다. 비밀번호 관리자에도 저장했다. JWT_SECRET은 바꾸면 발급된 액세스 토큰이 모두 무효가 되므로 한 번 정하면 함부로 바꾸지 않는다.
마스터 계정을 앱이 그대로 쓰면 앱이 뚫렸을 때 DB 서버 전체가 넘어간다. 그래서 마스터는 관리용으로만 쓰고, 앱 전용 계정 blackhole_app 을 따로 만들었다.
CREATE USER IF NOT EXISTS 'blackhole_app'@'10.0.%' IDENTIFIED BY '...';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, DROP, INDEX, REFERENCES
ON feedablackhole.* TO 'blackhole_app'@'10.0.%';
10.0.%로 한정해서 VPC 안에서만 접속할 수 있다.feedablackhole 스키마에 한정했고 서버 전체 권한은 없다. Flyway가 앱 계정으로 테이블을 만들기 때문에 DML뿐 아니라 CREATE, ALTER, DROP, INDEX, REFERENCES도 포함했다. 실제로 이 권한으로 충분한지는 3단계에서 앱을 띄워 보면 드러난다.read -s로 화면에 보이지 않게 받아 환경변수로만 썼다. 그래서 명령 기록에는 값이 아니라 변수 이름만 남는다.EC2에는 SSH 대신 Session Manager로 접속했다. 서버 안의 SSM 에이전트가 SSM 서비스로 먼저 연결을 맺어 두고, 브라우저가 IAM 권한으로 그 연결을 통해 셸을 얻는 방식이다. 그래서 서버에 들어오는 포트를 열 필요도 키 파일도 없고, 접속 이력은 CloudTrail에 남는다. 이 서버는 퍼블릭 서브넷에 퍼블릭 IP가 있어서 NAT 없이도 에이전트가 SSM에 닿는다.
확인한 것은 다음과 같다.
| 확인 | 결과 | 의미 |
|---|---|---|
docker --version, systemctl is-active docker | 25.0.14, active | 사용자 데이터가 성공했고 부팅 시 자동 시작된다 |
aws sts get-caller-identity | assumed-role/blackhole-ec2-role | 서버가 액세스 키 없이 역할로 호출한다 |
| ECR 로그인 | Login Succeeded | ECR 읽기 권한과 네트워크 경로가 정상이다 |
| 파라미터 이름 조회 | 4개 | 정책이 적용됐다(값은 출력하지 않는다) |
| 마스터로 접속해 계정 생성 | SHOW GRANTS 확인 | EC2 → RDS 경로(보안그룹 규칙)가 정상이고 권한 범위가 의도대로다 |
| 앱 계정으로 접속 | blackhole_app@10.0.%, utf8mb4, UTC, 8.4.11 | SSM의 비밀번호가 실제 DB 계정과 일치한다. 문자셋·시간대 기본값도 맞다 |
내 PC에서는 반대 방향도 확인했다. RDS 주소는 프라이빗 IP로 풀리고 3306에 접속되지 않으며, EC2의 22, 8080, 3306도 닫혀 있다. 열어 둔 것은 80/443뿐이고 아직 그 포트에서 응답하는 프로세스는 없다.
| 문제 | 원인 | 해결 |
|---|---|---|
| RDS 생성이 실패했다 | 무료 플랜은 자동 백업 보존이 1일까지만 허용된다 | 1일로 낮춰 다시 생성. 보존 기간이 짧아 복구 가능 시점이 하루뿐이라는 점은 인지해 둔다 |
get-parameters-by-path가 거부됐다 | 이 호출은 parameter/blackhole(경로 자체)를 리소스로 검사하는데, 정책에는 parameter/blackhole/*만 허용했다 | 리소스에 경로 자체를 추가한 새 버전을 기본으로 지정했다. 배포 스크립트도 이 호출을 쓸 수 있어서 계획 단계에서 잡은 것이 다행이다 |
| EC2 마법사 기본값 | 새 보안그룹이 22번을 전체 허용하고, T3는 "무제한" 크레딧이 기본이다 | 기존 보안그룹을 선택하고 크레딧을 "표준"으로 변경 |
| 보안그룹 출처에 이름을 입력해도 오류 | 출처는 CIDR 또는 보안그룹 ID만 받는다 | sg-… ID로 검색해 목록에서 선택 |
| IAM 정책 설명에서 오류 | 설명에는 / 같은 문자를 쓸 수 없다 | 허용되는 문자만 사용 |
| 세션 기록에 "종료 중"이 남았다 | 팝업 창을 종료하지 않고 닫았다 | 활성 세션은 없고 종료 시각도 기록되어 있어 이미 끝난 세션의 표시 문제다. 연결이 끊긴 세션도 유휴 시간이 지나면(기본 20분) 자동 종료된다 |
docker login 경고 | 인증 정보가 config.json에 암호화 없이 저장된다는 안내 | 저장되는 것은 12시간짜리 읽기 전용 ECR 토큰이다. 배포 스크립트에서 배포할 때마다 로그인하고 끝나면 로그아웃하게 한다 |
| 단계 | 내용 | 검증 |
|---|---|---|
| 3. 수동 배포 | 내 PC에서 이미지를 빌드해 ECR에 올린다(커밋 SHA 같은 고유 태그). EC2에서 Parameter Store 값으로 env 파일을 만들어 컨테이너를 실행한다. 새 태그로 교체하고 이전 태그로 되돌리는 것을 손으로 해 본 뒤 deploy.sh <태그>로 정리한다 | 컨테이너가 Flyway를 적용하고 헬스체크가 UP이다. EC2를 재부팅해도 컨테이너가 다시 뜬다. 앱 계정의 권한(DDL)이 충분한지도 여기서 확인된다 |
| 4. 도메인과 HTTPS | DuckDNS 서브도메인을 탄력적 IP에 연결한다. Nginx로 443 → 8080을 프록시하고, certbot으로 인증서를 발급·자동 갱신한다. 앱 포트는 127.0.0.1에만 바인딩한다. 정식 도메인은 구매 후 같은 IP로 연결하고 두 도메인을 함께 서비스하다가 전환한다 | curl https://도메인/actuator/health 성공. 클라이언트의 게스트 로그인이 동작한다 |
| 5. CI 확장 | PR에서 ./gradlew build(테스트 포함)를 실행하는 워크플로를 추가한다. Gradle 캐시를 적용하고, main 브랜치 보호(Checkstyle·빌드 통과 필수, 리뷰 1명)를 건다 | 테스트가 깨지는 PR은 병합이 막힌다 |
| 6. CD | AWS에 GitHub OIDC 공급자와 배포용 역할(저장소·브랜치로 신뢰 범위 제한, 최소 권한)을 만든다. main 푸시 시 테스트 → 이미지 빌드 → ECR 푸시 → SSM Run Command로 EC2에서 교체 → 헬스체크(타임아웃 40초 이상) → 실패 시 이전 태그로 자동 롤백한다. 동시 실행을 막는다 | 사소한 변경을 병합하면 사람 손 없이 배포되고, 일부러 헬스체크가 실패하게 하면 롤백된다 |
| 7. 운영 점검 | 로그 확인법, CloudWatch 알람(EC2 상태, RDS 저장공간·연결 수), 백업 복원 시험, 운영 런북(배포·롤백·비밀값 교체·DB 접속), 비용 확인 주기 | 복원 절차를 한 번 실제로 해 본다 |
함께 남은 일들이다.
| 항목 | 선택 | 이유 |
|---|---|---|
| 배포 방식 | Docker 이미지 + ECR | 환경이 어디서든 같고 태그로 롤백한다 |
| 비밀값 | 이미지에 넣지 않고 Parameter Store(SecureString)에서 실행할 때 주입 | 이미지는 여러 곳에 저장되고 공유된다 |
| 접속 | SSH 없이 SSM Session Manager, 장기 키 없음 | 로그인 경로와 관리할 키를 없앤다 |
| 네트워크 | DB는 프라이빗 서브넷, 보안그룹을 출처로 3306 허용 | 앱 서버만 DB에 닿는다 |
| DB 계정 | 마스터는 관리용, 앱은 스키마 한정 계정 | 앱이 뚫려도 서버 전체가 넘어가지 않는다 |
| 아키텍처 | x86 | 빌드하는 곳과 실행하는 곳의 CPU를 맞춰 변수를 줄인다 |
| 도메인 | 임시 DuckDNS → 정식 도메인 구매 후 전환 | HTTPS 인증서와 모바일 클라이언트의 HTTP 제한 |