middlemoon
AWS ECS( Elastic Container Service) 로 배포하기 본문
먼저 ECS를 사용하기 앞서, ECS 개념을 조금 더 짚고 넘어가보자 합니다.
ECS는 AWS에서 제공하는 컨테이너 오케스트레이션 서비스입니다.
주요 특징으로는
- 서버 관리 불필요 (Fargate): 유저님이 사용하신 Fargate 모드는 실제 서버(EC2)를 직접 관리하거나 패치할 필요 없이, 컨테이너를 실행할 CPU와 메모리 사양만 정해주면 AWS가 알아서 인프라를 할당합니다.
- 유연한 확장성: 트래픽이 늘어나면 컨테이너 개수(Task)를 자동으로 늘려 서비스 중단을 막아줍니다.
- AWS 서비스와의 완벽한 통합: ECR에서 이미지를 땡겨오고, IAM으로 권한을 관리하며, 곧 만드실 로드밸런서(ALB)와 유기적으로 연결됩니다.
이전 시간 AWS ECR 생성을 이어서, ECS 쪽 관련된 설정부분을 확인하고 있습니다.

로그를 보니 내 앱이 아니라, 도커 설치 확인용 샘플인 hello-world 이미지로 확인이 되어지고 있네요.

ECS의 태스크를 확인해보니 ECS 서비스는 컨테이너가 항상 켜진다는 조건을 전제하에 태스크가 실행되곤하는데,
이 hello-world라는 이미지는 바로 꺼져버리니 ECS 입장에서는
"어? 앱이 죽었네? 다시 살려야지!" 하고 무한 반복을 하다가 지쳐서 포기하게 된것이라 보면됩니다.
이제 저는 hello-world에서 저희 ECS 서비스에 들어갈 실제 이름으로 넣어서 진행하려 합니다.
과정은 다음과 같습니다.
1단계: 도커 이미지 준비 (Dockerfile)
2단계: 이미지 빌드 및 ECR 업로드
3단계: 인프라 자동화 (Python SDK - Boto3 사용)
4단계: 트러블슈팅 (SRE의 핵심 업무)
5단계: 최종 확인
1단계: 도커 이미지 준비 (Dockerfile)
FROM python:3.9-slim
WORKDIR /app
# 8080 포트에서 무한 대기하는 서버 예시
CMD ["python", "-m", "http.server", "8080"]
Dockerfile 작성: 가볍고 관리가 쉬운 python:3.9-slim 이미지를 기반으로, 테스트용 웹 서버가 중단되지 않고 돌 수 있도록 설정했습니다.
이미지 빌드: docker build -t mun-test .
[root@ip-192-168-100-19 ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
890742611047.dkr.ecr.ap-northeast-2.amazonaws.com/mun-test latest d182794e6c0e 5 hours ago 122MB
mun-test latest d182794e6c0e 5 hours ago 122MB
890742611047.dkr.ecr.ap-northeast-2.amazonaws.com/hello-world latest 1b44b5a3e06a 5 months ago 10.1kB
890742611047.dkr.ecr.ap-northeast-2.amazonaws.com/mun-test/hello-world latest 1b44b5a3e06a 5 months ago 10.1kB
890742611047.dkr.ecr.ap-northeast-2.amazonaws.com/mun-test 20260106-0654 1b44b5a3e06a 5 months ago 10.1kB
hello-world latest 1b44b5a3e06a 5 months ago 10.1kB
2단계: 이미지 빌드 및 ECR 업로드
docker images를 통해 기존 hello-world에서 mun-test로 변경될 수 있도록 build 부분을 지정해줍니다.
태그 설정: ECR 주소를 포함한 이름표를 붙였습니다. (docker tag ...)
docker tag mun-test:latest 890742611047.dkr.ecr.ap-northeast-2.amazonaws.com/mun-test:latest
이미지 푸시: docker push를 통해 AWS로 전송했습니다.
docker push 890742611047.dkr.ecr.ap-northeast-2.amazonaws.com/mun-test:latest
3단계: 인프라 자동화 (Python SDK - Boto3 사용)
- create_cluster.py: 컨테이너가 담길 논리적 그룹인 클러스터를 생성했습니다.
- register_task.py: 어떤 이미지를 쓰고, CPU/메모리는 얼마나 사용할지 정의하는 '작업 정의'를 등록했습니다.
- create_service.py: 정의된 작업을 실제로 실행하고 유지하는 '서비스'를 생성했습니다.
- update_service.py: 기존 subnet, sg에서 신규로 들어오는 파라미터 값을 등록하기 위해 생성했습니다.
create_cluster.py
import boto3
from botocore.exceptions import ClientError
def create_ecs_cluster(cluster_name):
ecs = boto3.client('ecs', region_name='ap-northeast-2')
try:
print(f" ECS Cluster 시작: {cluster_name}...")
response = ecs.create_cluster(
clusterName=cluster_name,
capacityProviders=['FARGATE', 'FARGATE_SPOT'],
settings=[
{
'name' : 'containerInsights',
'value' : 'enabled'
},
]
)
#클러스터 정보를 가져오도록 한다.
cluster_info = response['cluster']
print(f" 클러스터 생성 완료!")
print(f" 이름: {cluster_info['clusterName']}")
print(f" 상태: {cluster_info['status']}")
print(f" ARN: {cluster_info['clusterArn']}")
#return 결과값은 Arn중에 하나 값으로 가져와서 처리해준다.
return cluster_info['clusterArn']
#예외처리 메세지 남김
except ClientError as e:
print(f"'예외처리' : {e}")
if __name__ == "__main__":
YourCluster = "bizbee_Cluster"
create_ecs_cluster(YourCluster)
register_task.py
import boto3
#뭐가 필요할까?
def register_bizbee_task(execution_role_arn, image_uri):
#boto3 import 불러오기
#클라이언트는 ecs, 지역이름 한국
ecs = boto3.client('ecs', region_name = 'ap-northeast-2')
#설계도 이름
#Fargate 필수
#CPU
#GB
#IAM에서 복사한 ARN
response = ecs.register_task_definition(
family ='bizbee-task-def', # 설계도 그룹 이름
networkMode ='awsvpc', # aws vpc
requiresCompatibilities = ['FARGATE'], # Fargate로 인스턴스
cpu ='256', # CPU 용량 체크
memory ='512',
executionRoleArn=execution_role_arn, # 실행 Role Alarm
containerDefinitions=[
{
#컨테이너 이름 정의
#컨테이너 이미지 정의
'name' : 'bizbee-app-container',
'image' : image_uri, # 1단계에서 만든 ECR URI
'portMappings' : [
{
'containerPort': 8080,
'hostPort': 8080,
'protocol': 'tcp'
}
],
#Aws logs
'logConfiguration' : {
'logDriver' : 'awslogs',
'options' : {
'awslogs-group' : '/ecs/bizbee-logs', #로그 저장소 이름
'awslogs-region' : 'ap-northeast-2', #aws 리전
'awslogs-stream-prefix' : 'ecs', #로그 형태
'awslogs-create-group' : 'true' #그룹 형태
}
}
}
]
)
task_arn = response['taskDefinition']['taskDefinitionArn']
print(f"✅ 설계도 등록 성공! ARN: {task_arn}")
return task_arn
if __name__ == "__main__":
# 1. IAM에서 복사한 ARN을 넣으세요
MY_ROLE_ARN = "arn:aws:iam::890742611047:role/ecsTaskExecutionRole"
# 2. 1단계에서 만든 ECR URI를 넣으세요
MY_ECR_URI = "890742611047.dkr.ecr.ap-northeast-2.amazonaws.com/mun-test:latest"
register_bizbee_task(MY_ROLE_ARN, MY_ECR_URI)
create_service.py
import boto3
REGION = 'ap-northeast-2'
CLUSTER_NAME = 'bizbee_Cluster' # 2단계에서 만든 이름과 정확히 일치해야 함
TASK_DEFINITION = 'bizbee-task-def:1' # 방금 성공한 설계도 이름과 버전
def create_bizbee_service():
ecs = boto3.client('ecs', region_name=REGION)
try:
response = ecs.create_service(
cluster=CLUSTER_NAME,
serviceName='bizbee-service',
taskDefinition=TASK_DEFINITION,
desiredCount=1, # 컨테이너를 1개 띄우겠다
launchType='FARGATE',
networkConfiguration={
'awsvpcConfiguration': {
# ⚠️ 본인의 Subnet ID와 Security Group ID를 넣어야 합니다!
'subnets': ['subnet-0e4cac7d0f6d1d29a'],
'securityGroups': ['sg-0422d33a812db84fe'],
'assignPublicIp': 'ENABLED' # 인터넷 연결을 위해 필수
}
}
)
print("🚀 서비스 생성 요청 성공!")
return response
except Exception as e:
print(f"❌ 에러 발생: {e}")
if __name__ == "__main__":
create_bizbee_service()
update_service.py
import boto3
REGION = 'ap-northeast-2'
CLUSTER_NAME = 'bizbee_Cluster'
SERVICE_NAME = 'bizbee-service' # 기존 서비스 이름
def update_bizbee_service():
ecs = boto3.client('ecs', region_name=REGION)
try:
# create_service 대신 update_service를 사용합니다.
response = ecs.update_service(
cluster=CLUSTER_NAME,
service=SERVICE_NAME,
# 새로운 퍼블릭 서브넷 ID가 적용된 네트워크 설정을 넣습니다.
networkConfiguration={
'awsvpcConfiguration': {
'subnets' : ['subnet-0e4cac7d0f6d1d29a'],
'securityGroups': ['sg-0422d33a812db84fe'],
'assignPublicIp': 'ENABLED'
}
},
forceNewDeployment = True
)
print(" 서비스 업데이트 및 서브넷 교체 성공!")
return response
except Exception as e:
print("에러발생 {e}")
if __name__ == "__main__":
update_bizbee_service()
4단계: 트러블슈팅 (SRE의 핵심 업무)
배포 후 접속이 안 되는 문제를 해결하며 네트워크 구조를 파악했습니다.
이미지 불일치 해결: docker images 명령어로 이미지 ID를 대조하여 구버전 이미지가 배포된 것을 확인하고 최신본으로 갱신했습니다.
보안 그룹(Security Group) 설정: 외부에 대문 격인 8080 포트를 열어주어 접속 허용을 설정했습니다.
네트워크 경로 수정: 인터넷 통로(Internet Gateway)가 없는 프라이빗 서브넷 대신 퍼블릭 서브넷으로 교체하여 외부 접속을 성공시켰습니다.
멱등성 에러 해결: 이미 존재하는 서비스를 수정할 때는 update_service를 사용해야 한다는 것을 배웠습니다.
5단계: 최종 확인
결과: 퍼블릭 IP와 8080 포트를 통해 브라우저에서 화면을 확인하며 배포를 완료했습니다.

마무리 하면서
"도커 빌드부터 ECR 푸시, 그리고 ECS Fargate 배포까지. 쉽지 않은 여정이었지만 하나씩 문제를 해결해 가며 인프라 자동화(IaC)의 기초를 다질 수 있었습니다.
특히 토큰 만료나 이미지 지문(Digest) 불일치 같은 실제 운영 현장에서 만날 법한 에러들을 직접 겪고 해결해 본 것이 큰 자산이 되었습니다. 다음 포스팅에서는 고정된 주소와 안정적인 트래픽 분산을 위한 로드밸런서 구축기로 돌아오겠습니다."

'DevOps > AWS' 카테고리의 다른 글
| AWS ECR(Elastic Container Registry) 생성하기 (0) | 2026.01.06 |
|---|---|
| AWS Lambda, API Gateway 를 활용하여 비대칭키 발급 구현 (0) | 2025.12.04 |
| AWS Bastion Host의 연결과 Jenkins 파이프라인 설계 - 프로젝트 (2) (0) | 2025.10.21 |
| AWS 클라우드의 VPC 구성부터 EC2 생성까지 - 프로젝트(1) (1) | 2025.10.15 |
| AWS - EC2 인스턴스 생성해서 SSH에 AWS 연결하기(1) (0) | 2022.12.28 |