시작하며: 문제 및 필요성
초기 서비스 단계에서는 관리의 직관성과 환경의 단순성을 이유로 EC2와 같은 가상 머신(VM) 기반의 인프라를 선택하는 경우가 많습니다. 이는 빠른 배포와 소규모 운영에는 일정 수준의 유효성을 지니지만, 점차 서비스 트래픽이 증가하고 배포 주기가 짧아질수록 인프라 운영의 비효율성과 확장성의 한계가 드러납니다.
가장 큰 제약 중 하나는 수평 확장의 어려움입니다. VM 기반 서버는 물리적 또는 논리적 자원을 기준으로 스케일링해야 하며, 신규 인스턴스를 프로비저닝하는 데 시간이 소요되고, 설정과 이미지 관리도 번거롭습니다. 무중단 배포 또한 복잡한 로직과 헬스체크 전략을 요구하며, 배포 실패 시 롤백 관리가 어렵습니다. 이로 인해 운영 리스크가 누적되며, 배포 자동화 파이프라인의 안정성 확보 또한 어렵습니다.
이러한 한계를 해결하기 위해 컨테이너 기반의 오케스트레이션 환경이 필요하며, Amazon ECS는 그 대안으로써 명확한 이점을 제공합니다. 특히 AWS Fargate 기반의 ECS는 인프라 자원 관리 없이 컨테이너 단위로 애플리케이션을 정의하고 실행할 수 있게 해 주며, Auto Scaling 및 롤링 배포가 기본적으로 내장되어 있습니다. 결과적으로 ECS는 서비스의 민첩성과 확장성을 동시에 확보할 수 있게 해주는 현실적인 대안이자, MSA 아키텍처로의 전환에도 적합한 기반 인프라로 작용합니다.
저희가 최종적으로 구축할 인프라는 위와 같은 형태가 될 예정입니다.
전제 조건
이 글의 범위는 클라우드 인프라 구성에 한정되며, 애플리케이션 레벨에서 발생할 수 있는 문제(예: 동시성 이슈, 리더 선출, graceful shutdown 등)는 이미 해결되었다고 가정합니다. 다시 말해, 이 포스팅에서는 ECS 기반 배포 환경 구성에만 집중합니다.
또한 VPC 설정은 별도 심화 주제로 다루지 않으며, 본 글에서는 기본적으로 Public Subnet만 사용하는 구조를 전제로 합니다. Private Subnet을 활용하고자 하는 경우, 예를 들어 Secrets Manager 등 외부 서비스와의 통신이 필요한 환경에서는 NAT Gateway 설정이 필수적이며, 이 경우 비용 및 복잡도가 증가합니다. 따라서 이러한 아키텍처를 설계를 원하실 경우, 본 포스팅 이외로도 조사를 철저히 하시길 권합니다.
ACM 및 도메인 인증서에 대해서도 다루지 않습니다. 사전에 도메인을 구매, ACM에 등록해두시기 바랍니다.
ECS 런타임 - Fargate vs EC2
ECS는 EC2 기반과 Fargate 기반이라는 두 가지 런타임 모드를 제공합니다. 본 포스팅에서는 Fargate 기반 ECS를 전제로 하며, 이는 단순히 인프라 관리의 편의성 때문만이 아니라, 자원 회수 구조의 근본적인 효율성 차이 때문입니다.
EC2 기반 ECS는 무중단 배포(Rolling Update)를 위해 신규 버전의 태스크를 실행할 때, 기존 태스크와의 공존을 위해 일시적으로 더 많은 리소스를 필요로 합니다. 이 과정에서 다음과 같은 문제가 발생합니다:
- 새로운 태스크를 수용할 수 있는 여유 자원이 부족할 경우, ECS는 오토스케일링 그룹을 통해 추가 EC2 인스턴스를 생성 합니다.
- 배포가 완료되고 기존 태스크가 중지되더라도, 인스턴스 레벨의 리소스는 즉시 회수되지 않습니다.
- 이는 특히 태스크 단위 리소스 요구량이 작고, 배포 주기가 짧은 서비스 일수록 인스턴스가 부분적으로만 활용된 채 계속 유지 되며 자원 낭비 로 이어집니다.
예를 들어, 한 EC2 인스턴스가 4개의 태스크를 수용할 수 있는데, 배포 과정에서 잠시 2개 태스크가 더 필요해 인스턴스가 하나 추가되었다면, 이후 기존 태스크가 종료되더라도 그 신규 인스턴스는 일부 리소스만 사용된 채 계속 떠 있는 구조가 됩니다. ECS는 인스턴스가 부분적으로 비워져도 이를 자동으로 정리하지 않기 때문에, 리소스 릴리즈가 비효율적으로 작동합니다.
반면 Fargate는 태스크 단위로 리소스를 생성하고, 종료 시 해당 자원을 완전히 릴리즈합니다. 인프라 수준의 리소스를 사용자 측에서 직접 관리하지 않기 때문에,
- 배포 중 리소스가 잠시 증가하더라도,
- 배포 완료 후에는 불필요한 자원이 자동으로 제거 됩니다.
이는 자원이 부분적으로 유휴 상태로 남아있는 상황을 방지하고, 실제 사용한 만큼만 과금되도록 함으로써, 운영 비용 최적화에 있어 구조적으로 유리합니다.
대상그룹 & 로드 밸런서 세팅하기
먼저 ECS를 생성하기 전에 로드 밸런서를 생성하겠습니다.
ECS 생성 페이지에서 로드 밸런서를 만드는 방식도 있지만, 해당 방식으로 만든 경우 서비스 삭제 시 로드 밸런서가 삭제된다던가, 정밀한 제어를 못한다던가 여러 면에서 조금 귀찮아지기 때문에 먼저 필요한 인프라들을 만들어놓고 가겠습니다.
대상 그룹 생성
EC2 >대상 그룹에서 IP 주소를 대상 그룹으로 하여 대상그룹을 만들어줍니다.
세팅에서 주의하실 점은 대상 그룹의 포트설정입니다. 추후 ECS에서 실행할 서버에 대한 정의(task-definition)을 설정할텐데, Fargate 기반의 서비스는 컨테이너의 실행 포트와 호스트의 포트가 같아야
합니다. 즉 서버를 3000번 포트로 띄운다고 하면, 기존 도커에서 하던 것 처럼 443
나머지는 쭉 만들되, 헬스체크는 서버 측에서 엔드포인트 하나를 설정해주시기 바랍니다.
고급 상태 검사 설정 쪽을 보시면 성공 코드가 명시되어있습니다. 여기서 만약 아무 설정 없이 대상그룹을 만든다면 기본값이 /
(루트), 200
이 될텐데 보통 서버앱의 root path는 사용하지 않으실테니 404가 뜰 겁니다. 정상적으로 서버가 부팅되어도 실패하죠. 따라서 서버 부팅을 확인할 수 있고, 200 응답을 주는 엔드포인트 하나를 개설한 뒤에 대상 그룹에 연결해주세요.
IP 지정 및 포트 정의는 빈 값으로 둡니다.
생성이 완료되셨으면 대상 그룹의 속성 탭으로 가서 등록 취소 지연을 줄여줍니다.
CICD 때 시간을 잡아먹는 주범입니다. 이 지연 시간이 끝나야 기존 인스턴스가 내려가는데, 수십초 이상의 연산이 필요한 작업을 수행하는 게 아닌 이상 60초 아래로 설정하셔도 좋습니다. 보통 graceful shutdown과 트랜잭션을 잘 구현해두셨다면 지연이 필요 없는 경우가 많습니다.
로드 밸런서 생성
이제 로드 밸런서를 생성해줍니다. HTTPS를 활용할테니 Application Load Balancer를 설정합니다.
이름이나 서브넷 쭉 설정해주시고 리스너 및 라우팅에서 아까 만든 대상그룹을 지정해줍니다.
ACM에서 만들어둔 인증서 가져와줍니다. 안 만들어두신 분들은 잠깐 구글링 하셔서 만드시고 오세요! 이 글에선 범위가 넓어져 다루지 않습니다.
보안그룹은 80(선택), 443열어두시고, elb임을 식별할 수 있는 고유한 보안그룹도 하나 할당해주세요. 추후 ecs에서 로드 밸런서 한정으로 포트를 열 때 사용합니다.
80포트 열어두실거면 아래의 리스너 설정을 추가로 해주세요.
우선 이대로 생성을 해줍니다.
(선택)로드 밸런서: 리스너 설정
생성이 완료 되셨으면 이제 리스너들 설정을 좀 더 해줍니다. dev서버와 prod서버를 하나의 alb로 처리하고 싶으신 분, http로 접속 시 https로 리디렉션 시키고 싶으신 분은 이 단락을 따라와주세요.
호스트 헤더로 조건 분기하기
먼저 앞서 만든 것과 동일한 방식으로 dev 혹은 staging, monitoring 등 원하는 다른 대상 그룹을 만들어 줍니다. 글에서는 dev 서버 배포를 예시로 들겠습니다.
아까만든 HTTPS
리스너에 들어가서 리스너 규칙을 위와 같이 추가해줍니다. 호스트헤더로 규칙을 설정해주시면 되며 이 경우 기본값은 503으로 바꿔줍니다.HTTP -> HTTPS로 리디렉션
리스너 추가를 누르시고 URL로 리디렉션 선택 하신 후 생성하시면 됩니다.
OIDC용 IAM Role 만들기
이제 ecs에 올릴 도커 이미지를 준비해야 합니다. github action을 사용해 cd 파이프라인을 구축하겠습니다.
먼저 github에서 aws에 잘 접속할 수 있게 OIDC를 이용하겠습니다. OIDC는 관련이 없는 두 애플리케이션이 사용자 자격 증명을 손상시키지 않고 사용자 프로필 정보를 공유할 수 있도록 하는 데 사용되는 ID 인증 프로토콜인 OpenID Connect 프로토콜을 의미합니다.
aws <-> github간의 OIDC에 대한 자세한 내용은 아래를 참조해주세요!
https://token.actions.githubusercontent.com
sts.amazonaws.com
우선 IAM에 들어가서 역할 생성 -> 웹 자격 증명을 선택하고, ID 제공업체 및 Audience에 다음과 같이 입력합니다.
하단의 조직, 리포지토리, 브랜치 옵션은 본인의 서버 배포 환경에 맞게 설정해주세요.
권한은 최소권한정책을 유지하면서 설정해주시면 좋습니다. 자세한 설정은 각각 OIDC의 사용처마다 다르기 때문에 찾아보시기 바랍니다.
일단 따라오시기 편하게 저는 FullAccess 때리고 가겠습니다. 실제 사용하시는 서비스면 이렇게 하면 안됩니다..!
https://github.com/aws-actions/amazon-ecs-deploy-task-definition?tab=readme-ov-file#permissions
ecs 관련 role 설정은 위 레포 README의 permissions 부분 참조해주세요.
ECR repository 생성하기
Amazon ECR로 들어가서 repo를 만들어줍니다. 이름만 입력하시고 기본값 그대로 두시고 생성하시면 됩니다.
생성된 ecr은 github action을 통해 빌드한 서버의 이미지를 푸시할 저장소로 사용됩니다.
ECS 설정
이제 ecs 생성 이전 모든 작업을 마쳤습니다. 마지막으로 ecs내에서 어떤 이미지를 배포할 것인지에 따라 요구조건이 조금 바뀌는데, 만약 ecs 내 서비스간의 통신을 할 생각이라면 네임스페이스 생성을 먼저 진행해주세요. msa gateway <-> services 통신이라던가, 내부에 redis / kafka 등을 직접 구축해서 사용하신다던가, 모니터링 서버를 띄우고 각각 서버의 메트릭을 수집한다던가 하시면 네임스페이스 생성을 해야합니다.
(선택) 네임스페이스 생성
CloudMap에서 네임스페이스 생성을 누릅니다. docker compose에서 컨테이너 이름을 통해 컨테이너간 통신을 했던 것과 유사한 서비스라 생각하면 됩니다. ecs 내부 여러 task들은 ip가 아닌 네임스페이스를 통해 각자 통신할 수 있습니다.
사진과 같이 세팅하시고 생성을 합니다.
클러스터 생성
ECS -> 클러스터 생성을 누르고 Fargate로 클러스터를 생성해줍니다. 네임스페이스를 생성해두셨으면 선택해줍니다.
Task Definition 정의
task는 ecs에서 실행할 컨테이너 개별 단위라 보시면 좋습니다. task definition을 통해 어떤 이미지를 어떤 인프라 요구사항으로 배포할 것인 지 정의해둘 수 있습니다.
Fargate로 생성합니다.
OS 및 CPU, 메모리는 운용하시는 서버의 요구사항에 맞춰서 생성하시면 됩니다. 최근에 Fargate에서 ARM64를 지원해주기 시작하여 저는 ARM64 환경을 사용하겠습니다. 저처럼 ARM64를 사용하실 분은 Docker 이미지 빌드 시에 —platform을 명시해주셔야 합니다.
task 실행역할은 이미 aws에서 정의해준 TaskExecutionRole을 사용해줍니다.
만약 로깅을 하실 분들은 위와 같이 설정을 하고 생성을 했을 때 오류가 뜰수도 있습니다. ecsTaskExecutionRole에 CreateLogGroup 정책을 하나 만들어서 넣어줍니다.
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": "logs:CreateLogGroup",
"Resource": "*"
}
로그를 따지 않으실 분은 넘어가도 좋습니다.
아까 생성한 ecr의 URI 복사해서 넣습니다. 어차피 ci/cd하면서 덮어쓸 값인데 우선 초기값 설정은 해둡시다.
컨테이너 포트는 서버가 실행되는 포트입니다. 서버가 3000에서 실행된다고 하면, 해당 값을 그대로 넣어주시면 됩니다. 도커의 80
으로 매핑하는 건 fargate에선 지원해주지 않습니다.환경변수를 추가해야 하는데, 문제가 저희는 이 task-definition 자체를 cd를 위해 repository에 저장해두고 사용할 예정입니다. 따라서 여기에 환경변수를 설정해두시면 그대로 repository에 노출됩니다.
이를 위해 Secrets Manager를 사용합니다.
일단 비워두시고 생성합니다. 나중에 CICD때 수정할 예정입니다.
Secrets Manager로 환경변수 관리
Secrets Manager에 들어가서 보안 암호를 생성합니다. 이렇게 생성해둔 암호는 task-definition에서
arn:aws:secretsmanager:ap-northeast-2:381492248147:secret:test-api-rDw6Wm:ADMIN_CLIENT_URL::
위와 같이 설정하면 읽어올 수 있습니다.
service 생성
시작 유형 FARGATE로 service를 생성해줍니다.
앞서 정의해둔 task definition을 가져와주고, 원하는 테스크 수를 정해줍니다. 이 수만큼 컨테이너가 올라갑니다.
상태 검사 유예 기간을 넉넉하게 잡아줍니다. 서버가 부팅되는 동안 헬스체크가 진행되어 배포 실패로 처리되는 것을 막아줍니다.
무중단 배포 옵션을 선택할 수 있습니다.
기본 값으로 두면 배포 시 task 수가 일시적으로 2배로 늘어나고, 새 task가 헬스체크를 통과하면 기존 task를 수거합니다.
보안그룹을 설정해줍시다. 네트워크 탭은 서비스 최초 생성 시를 제외하고는 변경이 불가능하니 신중하게 설정해주세요.
퍼블릭 IP는 secrets manager를 사용하려면 필요합니다.
만약 클러스터 내부 서비스 간의 통신을 원한다면 서비스 연결을 켜야합니다. 인바운드 요청을 수신할 필요가 있으면 클라이언트 및 서버로 설정해주세요.
만약 서버인 경우, 포트 매핑에서 설정한 DNS 값을 타 서비스에서 통신할 때 HOST로 넣어주면 됩니다.
가령 redis를 띄운다고 했을 때, DNS 이름을 redis.test.internal 로 설정해주시고, 외부 서비스 환경변수에 REDIS_HOST = redis.test.internal
로 넣어서 연결합니다.
여러 인스턴스가 띄워져 있는 경우 여러 IP를 반환합니다. 별도의 복잡한 로드밸런싱이 필요 없는 경우, 라운드 로빈이나 랜덤한 IP를 엑세스 하는 방식으로 내부 통신을 쉽게 구현할 수 있습니다.
서버 등 외부에서 접근이 필요한 경우, 로드 밸런서를 꼭 연결해줍니다. 여기서 대상 그룹을 지정해두면, ecs task들을 자동으로 대상 그룹에 올려줍니다.
CD: github action -> ecr -> ecs
이제 github action과 연동하여 cd를 구축해보겠습니다. 다음과 같은 파이프라인을 거칩니다.
- repo 기반으로 docker image build
- ecr에 push
- ecr에 push 된 이미지를 기반으로 task-definition 새 version 생성
- 새 version을 이용해서 ecs 서비스 배포
task-definition 추출
생성된 task definition의 json 파일을 다운받아 줍니다.
[
"taskDefinitionArn",
"revision",
"status",
"registeredAt",
"deregisteredAt",
"registeredBy",
"requiresAttributes",
"compatibilities",
];
위 필드들은 ecs에서 자동으로 생성하는 값들로 지워줍니다.
프로젝트 내부에 위치시켜 줍니다. 저는 바로 루트에 넣었습니다.
json의 secrets에 환경변수를 모두 넣어줍니다.
github action yml 만들기
Creating the workflow 탭에 보시면 CD 가이드 코드가 있는데 그대로 긁어와서 환경변수나 배포 조건만 원하시는 대로 커스텀 해줍니다.
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ env.AWS_OIDC_ROLE_ARN }}
role-session-name: "github-actions-${{ github.run_id }}"
aws-region: ${{ env.AWS_REGION }}
aws credentials 방식은 access key 대신 더 안전하게 OIDC를 활용하게 해줍니다.
배포하기
여기까지 잘 따라오셨으면 github action을 트리거하면 자동으로 배포가 될 겁니다.
혹시 에러가 뜨는 분들은 메세지를 잘 확인하고 고쳐주시길 바랍니다. 대부분은 권한 문제, 환경변수 문제, 보안 그룹 이나 네트워크 설정 문제 중 하나입니다.
이제 초기 기획했던 아키텍처의 구성을 완료하였습니다. 마지막으로 도메인에 ALB의 CNAME을 넣어주시면 클라이언트 측에서 접속 가능합니다.
마무리하며: 보완할 포인트들
다음은 추가적으로 보완하면 좋은 포인트들입니다.
- OIDC 권한 최소화
: GitHub OIDC Role에 최소 권한만 부여 (
sts:AssumeRoleWithWebIdentity
+ 필수 ECS 권한만) - Redis 등 stateful 서비스는 EBS 또는 EFS로 볼륨 유지 : task 재시작/이동에도 데이터 보존
- 컨테이너 이미지 서명 및 스캔 활성화 : ECR 이미지에 취약점 스캐닝 적용
- Security Group 최소화 : 서비스 간 통신만 허용하는 VPC 보안 그룹 설계
- 서브넷 분리 구성 : 퍼블릭/프라이빗 서브넷 분리로 외부 노출 최소화
- 내부 로그 정비 & CloudWatch 로그 유지 기간 및 알람 설정 : 로그 비용 및 모니터링 효율화
- Auto Scaling 정책 설정 : CPU/메모리 기반으로 태스크 수 자동 조정