Lab 18: ECS 컨테이너 서비스 배포
ECS Fargate로 nginx 컨테이너 하나를 배포합니다. 서버(EC2) 없이 컨테이너만 정의하면 AWS가 실행 환경을 관리해주는 서버리스 컨테이너 패턴으로, 클러스터·태스크 정의·서비스·로그 수집까지 전부 Terraform으로 구성합니다.
ECS Fargate — 서버 없는 컨테이너 실행
컨테이너 오케스트레이션의 핵심 질문은 “컨테이너를 어디서, 몇 개, 어떻게 실행할 것인가"입니다. ECS는 AWS 네이티브 답변이고, Fargate는 그중에서도 EC2 관리까지 AWS에 위임하는 실행 모드입니다.
flowchart TD
subgraph tf["Terraform 정의"]
CLUSTER["aws_ecs_cluster\n(논리적 컨테이너 그룹)"]
TASKDEF["aws_ecs_task_definition\nnginx 이미지, 256 CPU / 512 MB\njsonencode 컨테이너 정의"]
SVC["aws_ecs_service\ndesired_count = 1\n죽으면 자동 재기동"]
end
subgraph net["기본 VPC (data source 조회)"]
SUBNET["퍼블릭 서브넷\nassign_public_ip = true"]
SG["보안그룹\n80 인바운드 허용"]
end
subgraph run["Fargate 런타임 (AWS 관리)"]
TASK["실행 중인 태스크\nnginx 컨테이너"]
end
LOGS["CloudWatch Logs\n/ecs/lab18-nginx"]
USER["브라우저\nhttp://퍼블릭IP"]
CLUSTER --> SVC
TASKDEF --> SVC
SVC -->|"태스크 배치"| TASK
SUBNET --> TASK
SG --> TASK
TASK -->|"awslogs 드라이버"| LOGS
USER -->|"HTTP 80"| TASK
ECS vs EKS vs 자체 운영 Kubernetes
JD들이 요구하는 것은 특정 도구가 아니라 컨테이너 오케스트레이션 스펙트럼에 대한 이해입니다.
| 구분 | ECS (Fargate) | EKS | 자체 운영 Kubernetes |
|---|---|---|---|
| 컨트롤 플레인 | AWS 완전 관리 (무료) | AWS 관리 (시간당 $0.10) | 직접 구축·운영 |
| 학습 곡선 | 낮음 — AWS 개념만 알면 됨 | 중간 — K8s + AWS 통합 | 높음 — K8s 전체 스택 |
| 이식성 | AWS 종속 | K8s 표준 — 매니페스트 재사용 가능 | 완전한 이식성 |
| 생태계 | AWS 서비스 통합 중심 | Helm, Operator 등 K8s 생태계 | K8s 생태계 전체 |
| Terraform 리소스 | aws_ecs_* 몇 개로 완결 | aws_eks_cluster + helm/kubernetes provider | 직접 조합 |
| 적합한 경우 | AWS 단일 클라우드, 소규모 팀 | K8s 표준이 필요한 조직 | 온프레미스, 특수 요구사항 |
이 랩이 입증하는 실무 역량
Mercor — DevSecOps Specialist “Terraform with modular, reusable patterns across VPC, EC2, ECS, EKS, IAM, S3, and SQS”
First Soft Solutions — Cloud Terraform Engineer “Configure and manage Kubernetes environments… Kubernetes, Infrastructure as Code (IaC)”
Information Consulting Services — Junior DevOps Engineer “Deploy and support containerized services using Docker and/or Kubernetes”
| JD 요구사항 | 이 랩에서 다루는 내용 |
|---|---|
| Terraform across VPC, ECS, IAM | 기본 VPC data source 조회 + ECS 클러스터/태스크/서비스 + 태스크 실행 IAM Role을 한 구성으로 |
| Kubernetes environments | ECS ↔ K8s 개념 매핑(태스크 정의=Pod, 서비스=Deployment)으로 오케스트레이션 공통 원리 학습 |
| Deploy containerized services | nginx 컨테이너를 코드로 배포하고 퍼블릭 IP로 서비스 확인 |
| Support (운영) | CloudWatch Logs로 컨테이너 로그 수집 — 배포 후 관측까지가 “지원"의 범위 |
실습 파일 구성
lab18-ecs-container/
├── versions.tf
├── providers.tf
├── variables.tf ← project/environment (기본값 lab18/dev)
├── locals.tf ← name_prefix + common_tags
├── network.tf ← 기본 VPC/서브넷 data source + 보안 그룹(80 인바운드)
├── iam.tf ← ECS 태스크 실행 역할 + AmazonECSTaskExecutionRolePolicy
├── logs.tf ← CloudWatch 로그 그룹 (3일 보존)
├── ecs.tf ← ECS 클러스터, Fargate 태스크 정의, 서비스
└── outputs.tf전체 코드
versions.tf
terraform {
required_version = ">= 1.0.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}providers.tf
provider "aws" {
region = "ap-northeast-2"
}variables.tf
variable "project" {
description = "프로젝트 이름"
type = string
default = "lab18"
}
variable "environment" {
description = "배포 환경"
type = string
default = "dev"
}locals.tf
locals {
# 리소스 이름 접두사 — 클러스터/태스크/서비스 이름을 doc과 동일하게 유지
name_prefix = var.project
common_tags = {
Project = var.project
Environment = var.environment
ManagedBy = "terraform"
}
}network.tf
# 기본 VPC와 퍼블릭 서브넷을 data source로 조회 (새 VPC 생성 없음)
data "aws_vpc" "default" {
default = true
}
data "aws_subnets" "default" {
filter {
name = "vpc-id"
values = [data.aws_vpc.default.id]
}
filter {
name = "default-for-az"
values = ["true"]
}
}
# Fargate 태스크용 보안 그룹 — HTTP 인바운드만 허용
resource "aws_security_group" "nginx" {
name = "${local.name_prefix}-nginx-sg"
description = "Allow HTTP inbound for nginx task"
vpc_id = data.aws_vpc.default.id
ingress {
description = "HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
description = "All outbound (image pull, logs)"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-nginx-sg"
})
}iam.tf
# 태스크 "실행" Role — ECS 에이전트가 이미지 pull, 로그 전송에 사용
# (컨테이너 내부에서 AWS API를 호출하려면 별도의 task_role이 필요)
resource "aws_iam_role" "task_execution" {
name = "${local.name_prefix}-ecs-task-execution-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
Action = "sts:AssumeRole"
}]
})
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-ecs-task-execution-role"
})
}
# AWS 관리형 정책 연결 — ECR pull, CloudWatch Logs 전송 권한 포함
resource "aws_iam_role_policy_attachment" "task_execution" {
role = aws_iam_role.task_execution.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}logs.tf
# 컨테이너 stdout/stderr 로그 수집 대상
resource "aws_cloudwatch_log_group" "nginx" {
name = "/ecs/${local.name_prefix}-nginx"
retention_in_days = 3 # 실습용 — 짧게 보관해 비용 최소화
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-nginx-logs"
})
}ecs.tf
# 논리적 컨테이너 그룹 — 실제 컴퓨팅은 Fargate가 관리
resource "aws_ecs_cluster" "main" {
name = "${local.name_prefix}-cluster"
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-cluster"
})
}
# 태스크 정의 — nginx 컨테이너 스펙 (≈ Kubernetes Pod 스펙)
resource "aws_ecs_task_definition" "nginx" {
family = "${local.name_prefix}-nginx"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc" # Fargate 필수 — 태스크마다 ENI 할당
cpu = 256 # 0.25 vCPU
memory = 512 # 512 MB
execution_role_arn = aws_iam_role.task_execution.arn
# 컨테이너 정의는 JSON — jsonencode로 HCL 안에서 관리
container_definitions = jsonencode([
{
name = "nginx"
image = "public.ecr.aws/nginx/nginx:stable"
essential = true
portMappings = [
{
containerPort = 80
protocol = "tcp"
}
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.nginx.name
"awslogs-region" = "ap-northeast-2"
"awslogs-stream-prefix" = "nginx"
}
}
}
])
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-nginx-taskdef"
})
}
# 서비스 — 태스크 수를 desired_count로 유지 (≈ Kubernetes Deployment)
resource "aws_ecs_service" "nginx" {
name = "${local.name_prefix}-nginx-svc"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.nginx.arn
launch_type = "FARGATE"
desired_count = 1 # 태스크가 죽으면 ECS가 자동으로 재기동
network_configuration {
subnets = data.aws_subnets.default.ids
security_groups = [aws_security_group.nginx.id]
assign_public_ip = true # 퍼블릭 서브넷 + NAT 없이 이미지 pull
}
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-nginx-svc"
})
}outputs.tf
output "cluster_name" {
description = "ECS 클러스터 이름"
value = aws_ecs_cluster.main.name
}
output "service_name" {
description = "ECS 서비스 이름"
value = aws_ecs_service.nginx.name
}
output "log_group" {
description = "컨테이너 로그가 쌓이는 CloudWatch 로그 그룹"
value = aws_cloudwatch_log_group.nginx.name
}terraform destroy를 실행하세요.실행 단계
배포
cd lab18-ecs-container
terraform init
terraform plan # 클러스터, 태스크 정의, 서비스, SG, IAM, 로그 그룹 확인
terraform apply -auto-approve태스크가 RUNNING 될 때까지 대기
CLUSTER=$(terraform output -raw cluster_name)
SERVICE=$(terraform output -raw service_name)
# 서비스가 안정화(태스크 RUNNING)될 때까지 대기 — 보통 1~2분
aws ecs wait services-stable --cluster "$CLUSTER" --services "$SERVICE"
echo "서비스 안정화 완료"퍼블릭 IP 조회 후 nginx 응답 확인
Fargate 태스크의 퍼블릭 IP는 태스크에 붙은 ENI에서 조회합니다.
# 1. 실행 중인 태스크 ARN
TASK_ARN=$(aws ecs list-tasks --cluster "$CLUSTER" \
--service-name "$SERVICE" --query 'taskArns[0]' --output text)
# 2. 태스크의 ENI ID
ENI_ID=$(aws ecs describe-tasks --cluster "$CLUSTER" --tasks "$TASK_ARN" \
--query "tasks[0].attachments[0].details[?name=='networkInterfaceId'].value" \
--output text)
# 3. ENI의 퍼블릭 IP
PUBLIC_IP=$(aws ec2 describe-network-interfaces --network-interface-ids "$ENI_ID" \
--query 'NetworkInterfaces[0].Association.PublicIp' --output text)
echo "http://$PUBLIC_IP"
curl -s "http://$PUBLIC_IP" | grep -o "<title>.*</title>"
# <title>Welcome to nginx!</title>컨테이너 로그 확인
aws logs tail /ecs/lab18-nginx --since 10m
# nginx 기동 로그 + 방금 보낸 curl의 액세스 로그 확인예상 결과 / 검증
Apply complete! Resources: 7 added, 0 changed, 0 destroyed.
Outputs:
cluster_name = "lab18-cluster"
log_group = "/ecs/lab18-nginx"
service_name = "lab18-nginx-svc"| 검증 항목 | 방법 | 기대 결과 |
|---|---|---|
| 태스크 상태 | aws ecs describe-tasks --query 'tasks[0].lastStatus' | RUNNING |
| HTTP 응답 | curl http://$PUBLIC_IP | Welcome to nginx! HTML |
| 자동 재기동 | 콘솔에서 태스크 수동 중지 → 1~2분 대기 | 새 태스크가 자동으로 RUNNING (desired_count 유지) |
| 로그 수집 | aws logs tail /ecs/lab18-nginx | nginx 액세스/에러 로그 스트림 |
aws ecs stop-task --cluster lab18-cluster --task $TASK_ARN으로 태스크를 죽이면, ECS 서비스가 desired_count = 1을 유지하기 위해 새 태스크를 자동으로 띄웁니다. Kubernetes의 ReplicaSet과 같은 원리입니다. 단, 새 태스크는 퍼블릭 IP가 바뀌므로 IP를 다시 조회해야 합니다 — 실무에서 ALB를 앞에 두는 이유입니다.실습 정리
terraform destroy -auto-approveaws ecs list-tasks --cluster lab18-cluster가 클러스터 없음 오류를 반환하면 정상 삭제된 것입니다. Fargate는 태스크가 떠 있는 시간만큼 과금되므로, 삭제 확인까지가 실습의 끝입니다.실무 포인트
execution_role_arn은 ECS 에이전트용(이미지 pull, 로그 전송)입니다. 컨테이너 안의 애플리케이션이 S3, SQS 등 AWS API를 호출해야 한다면 별도의 task_role_arn을 부여합니다. 두 Role의 구분은 ECS 면접 단골 질문입니다.desired_count 고정 대신 Application Auto Scaling으로 태스크 수 자동 조절. 이 랩의 코드가 그 확장의 기반 골격입니다.container_definitions는 Terraform 입장에선 그냥 문자열이라, 키 이름 오타(portMapping vs portMappings)가 apply 시점 또는 태스크 기동 실패로만 드러납니다. 태스크가 STOPPED를 반복하면 aws ecs describe-tasks의 stoppedReason부터 확인하세요.→ 다음 실습: Lab 19 Kubernetes(EKS) 프로비저닝 — 같은 컨테이너 워크로드를 K8s 표준 위에서, 클러스터 프로비저닝부터 파드 배포까지