본문 바로가기
Cloud

Spring Boot 애플리케이션을 AWS ECS Fargate로 배포하기 - 실전 삽질기

by AlbertIm 2025. 11. 26.

사이드 프로젝트 “Real Money Real Taste”를 AWS에 배포하면서 겪은 경험과 삽질기를 공유합니다.

들어가며

개인 프로젝트로 진행 중인 음식 리뷰 애플리케이션 RMRT(Real Money Real Taste)를 드디어 AWS에 배포했습니다. 로컬에서 Docker로 잘 돌아가던 Spring Boot 앱을 실제 클라우드 환경에 올리는 건 생각보다 고려할 게 많더라고요.

 

이 글에서는 VPC 구성부터 ECS Fargate 배포, GitHub Actions를 통한 CI/CD 파이프라인 구축까지 전체 과정을 정리했습니다. 특히 실제로 겪었던 에러들과 해결 과정을 상세히 담았으니 비슷한 문제를 겪는 분들께 도움이 되길 바랍니다.


전체 아키텍처

배포 완료 후 최종 아키텍처는 다음과 같습니다:

[사용자]
    ↓
[Application Load Balancer] (퍼블릭 서브넷)
    ↓
[ECS Fargate Task] (프라이빗 서브넷)
    ↓
[RDS MySQL] (프라이빗 서브넷)

 

핵심 설계 원칙은 애플리케이션과 데이터베이스를 프라이빗 서브넷에 배치하고 ALB만 퍼블릭에 노출시키는 것이었습니다.


1단계: VPC 네트워크 설계

왜 직접 VPC를 구성했나?

AWS 기본 VPC를 사용할 수도 있었지만 직접 구성한 이유가 있습니다:

  1. 보안: RDS를 프라이빗 서브넷에 격리
  2. 학습: 네트워크 구조를 확실히 이해하고 싶었음
  3. 확장성: 나중에 서비스가 커져도 유연하게 대응 가능

VPC CIDR: 10.0.0.0/16을 선택한 이유

VPC의 IPv4 CIDR 블록으로 10.0.0.0/16을 선택했습니다.

 

왜 10.0.0.0/16인가?

  1. 사설 IP 대역: RFC 1918에서 정의한 사설 IP 대역(10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) 중 하나입니다. 인터넷에서 라우팅되지 않아서 내부 네트워크에 안전하게 사용할 수 있습니다.
  2. 충분한 IP 개수: /16 서브넷 마스크는 65,536개의 IP 주소를 제공합니다. 사이드 프로젝트치고는 과한 것 같지만 나중에 서브넷을 추가하거나 서비스를 확장할 때 IP 부족 걱정이 없습니다.
  3. 서브넷 분할 용이: /16 블록을 /24 단위로 쪼개면 256개의 서브넷을 만들 수 있습니다. 퍼블릭/프라이빗 가용영역별로 넉넉하게 분할할 수 있죠.
  4. AWS 권장 사례: AWS에서도 VPC 생성 시 기본적으로 10.0.0.0/16을 많이 사용합니다. 다른 VPC나 온프레미스 네트워크와 연결할 때 충돌만 피하면 됩니다.
VPC CIDR: 10.0.0.0/16
├── 퍼블릭 서브넷: 10.0.1.0/24, 10.0.2.0/24
└── 프라이빗 서브넷: 10.0.11.0/24, 10.0.12.0/24

퍼블릭은 1~10번대, 프라이빗 11~20번대로 구분해서 나중에 서브넷이 늘어나도 헷갈리지 않게 했습니다.

AWS 콘솔에서 VPC 생성하기

1. VPC 생성

  1. AWS Management Console → VPC 접속
  2. “VPC 생성” 클릭
  3. 설정값 입력:
    • 이름 태그: rmrt-vpc
    • IPv4 CIDR 블록: 10.0.0.0/16

2. 서브넷 생성 (4개)

VPC → 서브넷 → “서브넷 생성”에서 아래 4개를 각각 생성합니다:

서브넷 CIDR 가용영역 용도
rmrt-public-subnet-1 10.0.1.0/24 ap-northeast-2a ALB, NAT Gateway
rmrt-public-subnet-2 10.0.2.0/24 ap-northeast-2c ALB (고가용성)
rmrt-private-subnet-1 10.0.11.0/24 ap-northeast-2a ECS, RDS
rmrt-private-subnet-2 10.0.12.0/24 ap-northeast-2c ECS, RDS (고가용성)

3. 인터넷 게이트웨이 생성

퍼블릭 서브넷이 인터넷과 통신하려면 인터넷 게이트웨이가 필요합니다.

  1. VPC → 인터넷 게이트웨이 → “인터넷 게이트웨이 생성”
  2. 이름 태그: rmrt-igw
  3. 생성 후 “작업” → “VPC에 연결” → rmrt-vpc 선택

4. NAT 게이트웨이 생성

프라이빗 서브넷에서 인터넷으로 나가는 트래픽(예: 패키지 다운로드)을 위해 필요합니다.

  1. VPC → NAT 게이트웨이 → “NAT 게이트웨이 생성”
  2. 이름: rmrt-nat-gw
  3. 서브넷: rmrt-public-subnet-1 선택 (퍼블릭 서브넷에 배치!)
  4. 연결 유형: 퍼블릭
  5. “탄력적 IP 할당” 클릭 → 새 EIP 생성

라우팅 테이블 설정

퍼블릭 라우팅 테이블 생성

  1. VPC → 라우팅 테이블 → “라우팅 테이블 생성”
  2. 이름: rmrt-public-rt
  3. VPC: rmrt-vpc
  4. 생성 후 “라우팅” 탭 → “라우팅 편집”
    • 대상: 0.0.0.0/0 → 대상: rmrt-igw (인터넷 게이트웨이)
  5. “서브넷 연결” 탭 → “서브넷 연결 편집”
    • rmrt-public-subnet-1, rmrt-public-subnet-2 선택

프라이빗 라우팅 테이블 생성

  1. “라우팅 테이블 생성”
  2. 이름: rmrt-private-rt
  3. VPC: rmrt-vpc
  4. “라우팅 편집”
    • 대상: 0.0.0.0/0 → 대상: rmrt-nat-gw (NAT 게이트웨이)
  5. “서브넷 연결 편집”
    • rmrt-private-subnet-1, rmrt-private-subnet-2 선택

퍼블릭 라우팅 테이블 (rmrt-public-rt)

대상: 0.0.0.0/0 → rmrt-igw (인터넷 게이트웨이)

프라이빗 라우팅 테이블 (rmrt-private-rt)

대상: 0.0.0.0/0 → rmrt-nat-gw (NAT 게이트웨이)

2단계: 보안 그룹 설정

보안 그룹은 AWS의 가상 방화벽입니다. 최소 권한 원칙을 적용해서 필요한 포트만 열었습니다.

AWS 콘솔에서 보안 그룹 생성하기

1. ECS 보안 그룹 생성

  1. EC2 → 보안 그룹 → “보안 그룹 생성”
  2. 기본 세부 정보:
    • 이름: rmrt-ecs-sg
    • 설명: Security group for ECS tasks
    • VPC: rmrt-vpc
  3. 인바운드 규칙 추가:
    • 유형: HTTP, 포트: 80, 소스: 0.0.0.0/0
    • 유형: HTTPS, 포트: 443, 소스: 0.0.0.0/0
    • 유형: 사용자 지정 TCP, 포트: 8080, 소스: 0.0.0.0/0
  4. 아웃바운드 규칙: 기본값 유지 (모든 트래픽 허용)

2. RDS 보안 그룹 생성

  1. “보안 그룹 생성”
  2. 기본 세부 정보:
    • 이름: rmrt-rds-sg
    • 설명: Security group for RDS
    • VPC: rmrt-vpc
  3. 인바운드 규칙 추가:
    • 유형: MySQL/Aurora, 포트: 3306
    • 소스: rmrt-ecs-sg (보안 그룹 선택 - 중요!)

ECS 보안 그룹 (rmrt-ecs-sg)

인바운드:
- HTTP (80) ← 0.0.0.0/0
- HTTPS (443) ← 0.0.0.0/0
- TCP 8080 ← 0.0.0.0/0 (Spring Boot 기본 포트)

아웃바운드:
- 모든 트래픽 → 0.0.0.0/0 (중요!)

RDS 보안 그룹 (rmrt-rds-sg)

인바운드:
- MySQL (3306) ← rmrt-ecs-sg (보안 그룹 참조)

 

포인트: RDS 보안 그룹의 소스를 0.0.0.0/0이 아닌 ECS 보안 그룹으로 지정했습니다. 이렇게 하면 ECS 태스크에서만 DB에 접근할 수 있어서 훨씬 안전합니다.


3단계: RDS 데이터베이스 설정

Free Tier로 시작하기

사이드 프로젝트니까 비용을 최소화하는 게 중요했습니다:

  • 인스턴스: db.t4g.micro (Free Tier)
  • 스토리지: 20GiB gp2
  • 엔진: MySQL 8.0

AWS 콘솔에서 RDS 생성하기

1. DB 서브넷 그룹 생성

RDS가 사용할 서브넷을 먼저 지정해야 합니다.

  1. RDS → 서브넷 그룹 → “DB 서브넷 그룹 생성”
  2. 설정:
    • 이름: rmrt-db-subnet-group
    • 설명: Subnet group for RMRT RDS
    • VPC: rmrt-vpc
    • 가용 영역: ap-northeast-2a, ap-northeast-2c
    • 서브넷: rmrt-private-subnet-1, rmrt-private-subnet-2 추가

2. RDS 인스턴스 생성

  1. RDS → 데이터베이스 → “데이터베이스 생성”
  2. “표준 생성” 선택
  3. 엔진 옵션:
    • 엔진 유형: MySQL
    • 엔진 버전: MySQL 8.0
    • 템플릿: 프리 티어 (중요!)
  4. 설정:
    • DB 인스턴스 식별자: rmrt-db
    • 마스터 사용자 이름: rmrt
    • 마스터 암호: 안전한 비밀번호 입력
  5. 연결:
    • VPC: rmrt-vpc
    • 서브넷 그룹: rmrt-db-subnet-group
    • 퍼블릭 액세스: 아니요 (중요!)
    • VPC 보안 그룹: 기존 항목 선택 → rmrt-rds-sg
  6. 추가 구성:
    • 초기 데이터베이스 이름: realmoneyrealtaste
  7. 백업: 자동 백업 활성화, 보유 기간: 1일
  8. 암호화: 활성화
  9. 삭제 보호: 활성화 (중요!)
  10. “데이터베이스 생성” 클릭

주요 설정

DB 인스턴스 식별자: rmrt-db
마스터 사용자: rmrt
데이터베이스 이름: realmoneyrealtaste
퍼블릭 액세스: 아니요 ← 중요!

삭제 보호는 꼭 활성화하세요. 실수로 DB 날리면… 생각만 해도 아찔합니다.


4단계: ECR에 Docker 이미지 푸시

AWS 콘솔에서 ECR 리포지토리 생성하기

  1. ECR → 리포지토리 → “리포지토리 생성”
  2. 설정:
    • 리포지토리 이름: rmrt-app
    • 이미지 태그 변경 가능성: 변경 가능
    • 푸시 시 스캔: 활성화 (보안 취약점 자동 체크)
  3. “리포지토리 생성” 클릭

ECR(Elastic Container Registry)에 rmrt-app 리포지토리를 생성했습니다. 이미지 스캔 기능을 활성화해서 보안 취약점도 자동으로 체크하도록 설정했습니다.

첫 이미지 푸시

# ECR 로그인
aws ecr get-login-password --region ap-northeast-2 | \
  docker login --username AWS --password-stdin \
  [ACCOUNT_ID].dkr.ecr.ap-northeast-2.amazonaws.com

# 이미지 빌드
docker build -t rmrt-app .

# 태깅
docker tag rmrt-app:latest \
  [ACCOUNT_ID].dkr.ecr.ap-northeast-2.amazonaws.com/rmrt-app:latest

# 푸시
docker push [ACCOUNT_ID].dkr.ecr.ap-northeast-2.amazonaws.com/rmrt-app:latest

5단계: ECS 클러스터 및 태스크 정의

Fargate를 선택한 이유

EC2 기반 ECS와 Fargate 중 고민했는데 Fargate를 선택했습니다:

  1. 서버 관리 불필요: 인스턴스 패치, 스케일링 신경 안 써도 됨
  2. 비용 효율: 사용한 만큼만 과금 (사이드 프로젝트에 적합)
  3. 빠른 시작: 설정이 훨씬 간단함

AWS 콘솔에서 ECS 설정하기

1. ECS 클러스터 생성

  1. ECS → 클러스터 → “클러스터 생성”
  2. 클러스터 이름: rmrt-cluster
  3. 인프라: AWS Fargate (서버리스) 선택
  4. 모니터링: Container Insights 활성화 (선택)
  5. “생성” 클릭

2. 태스크 정의 생성

  1. ECS → 태스크 정의 → “새 태스크 정의 생성”
  2. 기본 정보:
    • 태스크 정의 패밀리: rmrt-task
    • 시작 유형: AWS Fargate
  3. 태스크 크기:
    • CPU: 0.25 vCPU (256)
    • 메모리: 0.5 GB (512)
  4. 태스크 역할: ecsTaskExecutionRole (없으면 새로 생성)
  5. 컨테이너 정의 → “컨테이너 추가”:
    • 컨테이너 이름: rmrt-container
    • 이미지 URI: [ACCOUNT_ID].dkr.ecr.ap-northeast-2.amazonaws.com/rmrt-app:latest
    • 포트 매핑: 컨테이너 포트 8080
    • 로깅:
      • 로그 드라이버: awslogs
      • 로그 그룹: /ecs/rmrt (자동 생성)
      • 로그 스트림 접두사: ecs
    • 환경 변수 (키-값 쌍으로 추가):
SPRING_PROFILES_ACTIVE=prod
SPRING_DATASOURCE_URL=jdbc:mysql://rmrt-db.xxxx.ap-northeast-2.rds.amazonaws.com:3306/realmoneyrealtaste?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul
SPRING_DATASOURCE_USERNAME=rmrt
SPRING_DATASOURCE_PASSWORD=rmrtpassword123!
SPRING_MAIL_HOST=smtp.gmail.com
SPRING_MAIL_PORT=587
APP_BASE_URL=https://your-domain.com
ADMIN_PASSWORD=secure-admin-password

6. “생성” 클릭

태스크 정의

태스크 이름: rmrt-task
CPU: 256 (0.25 vCPU)
메모리: 512MB

환경 변수 설정

민감한 정보는 환경 변수로 주입했습니다:

SPRING_PROFILES_ACTIVE=prod
SPRING_DATASOURCE_URL=jdbc:mysql://rmrt-db.xxx.ap-northeast-2.rds.amazonaws.com:3306/realmoneyrealtaste
SPRING_DATASOURCE_USERNAME=rmrt
SPRING_DATASOURCE_PASSWORD=***

Tip: 프로덕션에서는 AWS Secrets Manager나 Parameter Store를 사용하는 게 더 안전합니다.

 


6단계: Application Load Balancer

ALB가 하는 일

  1. 외부 트래픽을 받아서 ECS 태스크로 전달
  2. 헬스 체크로 정상 태스크만 트래픽 라우팅
  3. (선택) HTTPS 종료 처리

AWS 콘솔에서 ALB 생성하기

1. 대상 그룹 먼저 생성

  1. EC2 → 대상 그룹 → “대상 그룹 생성”
  2. 기본 구성:
    • 대상 유형: IP (Fargate는 IP 타입 필수!)
    • 대상 그룹 이름: rmrt-tg
    • 프로토콜: HTTP
    • 포트: 8080
    • VPC: rmrt-vpc
  3. 상태 검사:
    • 상태 검사 경로: /actuator/health
    • 상태 검사 포트: 트래픽 포트
    • 정상 임계값: 2
    • 비정상 임계값: 3
    • 제한 시간: 5초
    • 간격: 30초
  4. “다음” → 대상 등록은 건너뛰기 (ECS 서비스에서 자동 등록)
  5. “대상 그룹 생성”

2. Application Load Balancer 생성

  1. EC2 → 로드 밸런서 → “로드 밸런서 생성”
  2. Application Load Balancer 선택
  3. 기본 구성:
    • 이름: rmrt-alb
    • 체계: 인터넷 경계
    • IP 주소 유형: IPv4
  4. 네트워크 매핑:
    • VPC: rmrt-vpc
    • 매핑: rmrt-public-subnet-1, rmrt-public-subnet-2 (퍼블릭 서브넷!)
  5. 보안 그룹: rmrt-ecs-sg
  6. 리스너:
    • 프로토콜: HTTP, 포트: 80
    • 기본 작업: rmrt-tg로 전달
  7. “로드 밸런서 생성”

대상 그룹 설정

대상 유형: IP (Fargate는 IP 타입 필수)
프로토콜: HTTP
포트: 8080
헬스 체크 경로: /actuator/health

 

Spring Boot Actuator의 /actuator/health 엔드포인트를 헬스 체크에 활용했습니다. application.yml에서 actuator를 활성화해둬야 합니다:

management:
  endpoints:
    web:
      exposure:
        include: health

7단계: ECS 서비스 생성

모든 준비가 끝났으니 서비스를 생성합니다.

AWS 콘솔에서 ECS 서비스 생성하기

  1. ECS → 클러스터 → rmrt-cluster → “서비스 생성”
  2. 컴퓨팅 구성:
    • 시작 유형: Fargate
    • 플랫폼 버전: LATEST
  3. 배포 구성:
    • 애플리케이션 유형: 서비스
    • 태스크 정의: rmrt-task
    • 서비스 이름: rmrt-service
    • 원하는 태스크 수: 1
  4. 네트워킹:
    • VPC: rmrt-vpc
    • 서브넷: rmrt-private-subnet-1, rmrt-private-subnet-2 (프라이빗!)
    • 보안 그룹: rmrt-ecs-sg
    • 퍼블릭 IP: (프라이빗 서브넷이므로)
  5. 로드 밸런싱:
    • 로드 밸런서 유형: Application Load Balancer
    • “기존 로드 밸런서 사용” 선택
    • 로드 밸런서: rmrt-alb
    • 리스너: 80:HTTP
    • 대상 그룹: rmrt-tg
  6. “서비스 생성” 클릭

서비스 설정 요약

서비스 이름: rmrt-service
원하는 작업 수: 1
서브넷: rmrt-private-subnet-1, rmrt-private-subnet-2
퍼블릭 IP: 사용 안 함
로드 밸런서: rmrt-alb

배포 확인

서비스 생성 후 “태스크” 탭에서 상태를 확인합니다. RUNNING이 되면 성공!

ALB DNS 이름으로 접속 테스트:

http://[ALB-DNS-NAME]
http://[ALB-DNS-NAME]/actuator/health

8단계: GitHub Actions CI/CD

수동 배포는 귀찮으니까~ main 브랜치에 푸시하면 자동 배포되도록 설정했습니다.

CI/CD 파이프라인 구조

테스트 → 빌드            → Docker 빌드 & ECS 배포
     → SonarQube 분석  →

4단계 파이프라인으로 구성했습니다. 테스트가 실패하면 빌드 단계로 넘어가지 않고 main 브랜치 푸시일 때만 실제 배포가 진행됩니다.

GitHub Actions용 IAM 사용자 생성

GitHub Actions에서 AWS에 접근하려면 자격증명이 필요합니다.

1. IAM 사용자 생성

  1. AWS Console → IAM → 사용자 → “사용자 생성”
  2. 사용자 이름: github-actions-rmrt
  3. “다음” 클릭

2. 권한 정책 연결

다음 정책들을 연결합니다:

  • AmazonEC2ContainerRegistryFullAccess
  • AmazonECS_FullAccess
  • CloudWatchLogsFullAccess
  • ElasticLoadBalancingReadOnly

3. Access Key 발급

  1. 생성된 사용자 → “보안 자격 증명” 탭
  2. “액세스 키 만들기” → “CLI” 선택
  3. Access Key ID와 Secret Access Key 안전하게 저장

GitHub Secrets 등록

  1. GitHub Repository → Settings → Secrets and variables → Actions
  2. “New repository secret”으로 하나씩 추가:
AWS_ACCOUNT_ID: 123456789012
AWS_ACCESS_KEY_ID: AKIA...
AWS_SECRET_ACCESS_KEY: ...
AWS_REGION: ap-northeast-2
DB_ENDPOINT: rmrt-db.xxx.ap-northeast-2.rds.amazonaws.com
DB_PORT: 3306
DB_PASSWORD: [RDS 비밀번호]
SPRING_MAIL_USERNAME: your-email@gmail.com
SPRING_MAIL_PASSWORD: [앱 비밀번호]
SPRING_MAIL_HOST: smtp.gmail.com
SPRING_MAIL_PORT: 587
APP_BASE_URL: http://[ALB-DNS-NAME]
SONAR_TOKEN: [SonarQube 토큰]

워크플로우 전체 코드

https://gist.github.com/AlbertImKr/f22817268aebd48b5df733138e298f79

워크플로우 핵심 포인트

1. 강제 재배포 방식

aws ecs update-service \  
    --cluster rmrt-cluster \  
    --service rmrt-service-1 \  
    --task-definition $TASK_DEFINITION_ARN \  
    --force-new-deployment

새로운 태스크 정의를 매번 만들지 않고 기존 태스크 정의를 재사용하면서 --force-new-deployment 옵션으로 새 이미지를 배포합니다. 태스크 정의가 latest 태그를 참조하고 있기 때문에 가능한 방식입니다.

2. 배포 완료 대기

aws ecs wait services-stable \  
    --cluster rmrt-cluster \  
    --services rmrt-service-1

이 명령어가 없으면 배포 요청만 보내고 GitHub Actions가 끝나버립니다. wait services-stable을 사용하면 새 태스크가 정상적으로 실행될 때까지 기다립니다.


🔥 트러블슈팅: 진짜 삽질은 여기서부터

배포하면서 만난 에러들과 해결 과정입니다. 이 부분이 진짜 핵심입니다.

1. exec format error - Docker 아키텍처 불일치

ECS 태스크가 시작되자마자 바로 종료되길래 CloudWatch 로그를 확인했더니:

exec /bin/sh: exec format error

원인: Apple Silicon(M1/M2) Mac에서 빌드한 이미지를 x86_64 기반 AWS Fargate에서 실행하려고 해서 발생

  • M1/M2 Mac: ARM64 (arm64) 아키텍처
  • AWS Fargate: x86_64 (amd64) 아키텍처

로컬에서 그냥 docker build하면 ARM64 이미지가 만들어지는데 Fargate는 x86_64만 지원합니다.

 

해결 방법 1: Dockerfile에 플랫폼 명시

# 빌드 단계FROM 
--platform=linux/amd64 eclipse-temurin:21-jdk-alpine AS build
# ... 빌드 과정 ...# 실행 단계
FROM --platform=linux/amd64 eclipse-temurin:21-jre-alpine

해결 방법 2: 빌드 시 플랫폼 옵션 추가

# 기존 (ARM64로 빌드됨)
docker build -t rmrt-app .
# 변경 (x86_64로 빌드)
docker build --platform linux/amd64 -t rmrt-app .

 

저는 Dockerfile에 --platform=linux/amd64를 명시하는 방법을 선택했습니다. 이렇게 하면 어디서 빌드하든 항상 x86_64 이미지가 만들어집니다.

 

아키텍처 확인하는 방법:

# 이미지 아키텍처 확인
docker inspect rmrt-app | grep Architecture

2. ECR 연결 실패 - ResourceInitializationError

배포 후 ECS 콘솔에서 이벤트 탭을 확인했더니 이런 에러가 떠있었습니다:

ResourceInitializationError: unable to pull secrets or registry auth:
The task cannot pull registry auth from Amazon ECR:
There is a connection issue between the task and Amazon ECR.
Check your task network configuration.

operation error ECR: GetAuthorizationToken, exceeded maximum number of attempts, 3,
https response error StatusCode: 0, RequestID: , request send failed,
Post "https://api.ecr.ap-northeast-2.amazonaws.com/": dial tcp 54.180.184.238:443: i/o timeout

 

원인: ECR 프라이빗 리포지토리에 대한 접근 권한이 없음

ECR 리포지토리를 프라이빗으로 생성했는데 ECS 태스크 실행 역할(ecsTaskExecutionRole)에 해당 리포지토리 접근 권한이 없었습니다.

 

해결 방법: ECR 리포지토리 정책에 권한 추가

  1. ECR → 리포지토리 → rmrt-app 선택
  2. "권한" 탭 → "정책 JSON 편집"
  3. 다음 정책 추가:
{
  "Sid": "AllowECSTaskExecution",
  "Effect": "Allow",
  "Principal": {
    "AWS": "arn:aws:iam::[ACCOUNT_ID]:role/ecsTaskExecutionRole"
  },
  "Action": [
    "ecr:GetDownloadUrlForLayer",
    "ecr:BatchGetImage",
    "ecr:BatchCheckLayerAvailability"
  ]
}

 

주요 Action 설명:

  • ecr:GetDownloadUrlForLayer: 이미지 레이어 다운로드 URL 획득
  • ecr:BatchGetImage: 이미지 매니페스트 조회
  • ecr:BatchCheckLayerAvailability: 레이어 존재 여부 확인

이렇게 Principal을 사용해서 특정 IAM 역할에만 ECR 접근 권한을 부여하면 프라이빗 리포지토리를 안전하게 사용할 수 있습니다.

정책 추가 후 ECS 서비스를 강제 재배포하니 해결되었습니다!

3. 프로덕션 프로필 설정 문제

애플리케이션이 시작되긴 하는데 설정이 이상하게 적용되는 문제가 있었습니다.

 

문제의 설정:

# application-prod.yml
spring:  
    profiles:    
        active: prod  # 이게 문제!

 

application-prod.yml 파일 자체가 prod 프로필인데 그 안에서 또 profiles.active: prod를 설정하면 순환 참조 같은 문제가 생길 수 있습니다.

 

해결: 불필요한 profiles.active 제거

4. JPA DDL 설정

처음에 ddl-auto: validate로 설정했다가 테이블이 없어서 에러가 났습니다.

# 변경 전
jpa:
  hibernate:
    ddl-auto: validate

# 변경 후
jpa:
  hibernate:
    ddl-auto: update

주의: update는 개발 단계에서만 사용하고 프로덕션에서는 Flyway나 Liquibase 같은 마이그레이션 도구를 사용하는 게 좋습니다.

5. 헬스 체크 실패

증상: 대상 그룹에서 계속 unhealthy

확인 사항:

  • /actuator/health 엔드포인트가 활성화되어 있는지
  • 보안 그룹에서 8080 포트가 열려있는지
  • 애플리케이션 시작 시간이 헬스 체크 유예 기간보다 긴지

해결: SecurityConfig에서 actuator 엔드포인트 허용

.requestMatchers("/actuator/**").permitAll()

마치며

처음 AWS ECS 배포할 때는 서비스가 너무 많아서 막막했는데 하나씩 구성하다 보니 각 서비스의 역할이 명확해졌습니다.

 

특히 ECR 연결 문제를 해결하면서 VPC 엔드포인트의 역할을 확실히 이해하게 됐습니다. 프라이빗 서브넷은 말 그대로 “프라이빗”이라서 AWS 서비스에 접근하려면 별도의 경로(NAT Gateway 또는 VPC Endpoint)가 필요하다는 걸 직접 경험으로 배웠네요.

이번 배포에서 배운 것들

  1. 네트워크 설계의 중요성: VPC, 서브넷, 라우팅 테이블을 제대로 이해해야 함
  2. VPC 엔드포인트: 프라이빗 서브넷에서 AWS 서비스 접근 시 필수
  3. 로그 확인의 중요성: CloudWatch Logs와 ECS 이벤트 탭을 항상 확인
  4. 리소스 이름 일관성: AWS 콘솔에서 생성한 이름을 정확히 확인

다음 개선 과제

  • HTTPS 적용 (ACM + Route 53)
  • Secrets Manager로 민감 정보 관리
  • Auto Scaling 설정
  • CloudFront CDN 추가
  • Flyway로 DB 마이그레이션 관리

질문이나 피드백은 댓글로 남겨주세요! 🙌


Reference

댓글