안녕하세요. IT 엘도라도 에 오신 것을 환영합니다.
글을 쓰는 것은 귀찮지만 다시 찾아보는 것은 더 귀찮습니다.
완전한 나만의 것으로 만들기 위해 지식을 차곡차곡 저장해 보아요.   포스팅 둘러보기 ▼

AWS (Amazon Web Service)

[AWS] Docker 기반의 ECS로 Django, Gunicorn, Nginx 배포하기

피그브라더 2021. 11. 21. 15:41

근무 중인 회사에서 서버 배포 방식을 변경하는 중대한 업무를 맡게 되면서, Docker를 공부하고 또 이를 활용한 배포 서비스인 ECS에 대해서도 공부하게 되었다. 물론 Docker 기반의 ECS 서비스에 실제로 현재 회사의 서비스를 배포하는 작업은 수차례의 삽질을 필요로 했지만, 그 과정에서 얻은 것도 많고 뿌듯함도 크기에 이번 포스팅에 해당 내용을 최대한 상세히 기록해두고자 한다. 아무래도 ECS에 대한 글이 아직까지는 그리 많은 편이 아닌 듯하니, 이번 포스팅이 다른 사람들에게 많은 도움이 될 수 있기를 바란다.

 

1. Docker 설치

Docker를 설치하는 방법에 대해서는 이번 포스팅에서 다루지 않는다. 운영체제별로 다르기도 하고, 공식 문서에 아주 잘 나와 있으며, 공식 문서가 아니더라도 이미 수많은 포스팅들이 작성되어 있기 때문이다. 다만 개인적으로는 공식 문서의 내용을 통해 Docker를 설치하기를 권장한다. 기본적으로 Docker는 Linux 기반의 기술이기 때문에 macOS나 Windows에서 Docker를 사용하려면 Linux 커널을 설치하는 과정이 필요한데, 공식 문서가 그 과정을 세심하게 잘 설명해주고 있기 때문이다.

 

▼ Docker 설치 방법 관련 공식 문서

 

Get Docker

 

docs.docker.com

 

2. 프로젝트 구조, Dockerfile, docker-compose 설정 파일

기본적인 프로젝트 구조는 다음과 같다. eldorado는 프로젝트 이름이다. 필자의 경우 Dockerfile과 docker-compose 설정 파일을 실서버 전용과 로컬 전용으로 분리했기 때문에 파일의 이름에 .production과 .local이 각각 들어가 있다. 다만 여기서는 실제 배포 과정에 대해 다룰 것이기 때문에 실서버 전용 파일만 보여주도록 한다.

 

 

다음으로 배포에 필요한 각종 설정 파일의 내용을 살펴보도록 하자. 단, 이번 포스팅의 목적이 Dockerfile과 docker-compose 설정 파일을 작성하는 방법을 다루는 것은 아니기 때문에, 배포와 관련된 부분만 설명하며 나머지는 생략하도록 한다.

 

▼ eldorado/django/Dockerfile.production

FROM python:3.6

# Set Environment Variables
ENV ...

# Copy Source Codes
COPY . /django
WORKDIR /django

# Install Python Packages
RUN pip3 install --upgrade pip && pip3 install -r requirements/production.txt

# Copy Entrypoint Script
COPY docker-entrypoint.production.sh /docker-entrypoint.production.sh
RUN chmod +x /docker-entrypoint.production.sh
eldorado/django/requirements/production.txt에는 Django와 Gunicorn을 포함한 Python 패키지들이 나열되어 있다. 그리고 eldorado/django/docker-entrypoint.production.sh는 Django 컨테이너가 생성될 때 해당 컨테이너에서 실행될 스크립트이다.

 

▼ eldorado/django/docker-entrypoint.production.sh

#!/bin/bash

# Migrate Database
python3 manage.py migrate --noinput

# Collect Staticfiles
python3 manage.py collectstatic --noinput

# Run Gunicorn (WSGI Server)
gunicorn --bind 0.0.0.0:8000 config.wsgi:application
Gunicorn 실행 시 옵션으로 지정하는 config.wsgi는 eldorado/django/config/wsgi.py 파일을 가리키는 경로이므로, 갖고 있는 wsgi.py 파일의 경로에 맞춰 적절히 지정하면 된다.

 

▼ eldorado/nginx/Dockerfile.production

FROM nginx:1.21.4

# Configure Nginx
COPY nginx.conf /etc/nginx/nginx.conf

# Copy Script to Wait for Gunicorn
COPY wait-for-it.sh /wait-for-it.sh

# Copy Entrypoint Script
COPY docker-entrypoint.production.sh /docker-entrypoint.production.sh
RUN chmod +x /docker-entrypoint.production.sh
eldorado/nginx/wait-for-it.sh는 Django 컨테이너가 준비될 때까지, 즉 Gunicorn 서버가 실행될 때까지 기다리도록 하는 스크립트이다. 이 파일은 https://github.com/vishnubob/wait-for-it에서 다운로드한다. 그리고 eldorado/nginx/docker-entrypoint.production.sh는 Nginx 컨테이너가 생성될 때 해당 컨테이너에서 실행될 스크립트이다.

 

▼ eldorado/nginx/nginx.conf

events {
    worker_connections 1024;
}

http {
    upstream django {
        ip_hash;
        server django:8000;
    }

    server {
        listen 80;

        server_name eldorado.com;

        location / {
            proxy_pass http://django/;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto https;
        }
    }

    server {
        listen 80 default_server;

        server_name _;

        location / {
            return 404;
        }

        location = /health-check {
            access_log off;
            return 200;
        }
    }
}
첫 번째 server 블록에서는 80번 포트로 들어오는 트래픽을 Django 컨테이너에서 8000번 포트로 열려 있는 Gunicorn 서버에 전달하는 리버스 프록시 서버의 역할을 수행하도록 설정한다. 이때 server_name에는 자신의 서비스에 사용할 도메인을 지정하면 된다. 그리고 두 번째 server 블록에서는 로드 밸런서의 상태 검사 요청에 대한 200 응답을 줄 수 있도록 설정한다. 로드 밸런서의 상태 검사 요청에 대해서는 뒤에서 설명한다.

 

▼ eldorado/nginx/docker-entrypoint.production.sh

#!/bin/bash

# Wait for Gunicorn
chmod +x /wait-for-it.sh
/wait-for-it.sh django:8000 --timeout=0 -- nginx -g 'daemon off;'
앞서 말했듯, wait-for-it.sh는 Django 컨테이너가 준비될 때까지, 즉 Gunicorn 서버가 실행될 때까지 기다리도록 한다.

 

▼ eldorado/docker-compose.production.yml

version: '3'
services:
  django:
    build:
      context: ./django
      dockerfile: ./Dockerfile.production
    image: django-image
    container_name: django-container
    entrypoint:
      - /docker-entrypoint.production.sh
  nginx:
    build:
      context: ./nginx
      dockerfile: ./Dockerfile.production
    image: nginx-image
    container_name: nginx-container
    ports:
      - "80"
    depends_on:
      - django
    links:
      - django
    entrypoint:
      - /docker-entrypoint.production.sh
ports 설정에 "80:80"을 지정하지 않고 "80"을 지정한 것은 동적 포트 매핑을 위함이다. 동적 포트 매핑에 대해서는 뒤에서 자세히 설명한다. depends_on 설정은 Django 컨테이너가 실행된 다음 Nginx 컨테이너가 실행되도록 한다. links 설정의 경우 로컬에서 할 때는 없어도 괜찮았는데, ECS에 배포할 때는 넣어줘야 정상 작동하는 것을 확인했다. links 설정이 없으면 작업 정의에 서로 다른 컨테이너가 통신할 수 있도록 하는 설정이 포함되지 않는 듯싶다. 작업 정의에 대해서는 뒤에서 설명한다. 참고로 image 설정으로 지정되는 django-image라는 값과 nginx-image라는 값은 이따가 Docker 이미지를 ECR 리포지토리에 푸시하기 위해 그 리포지토리의 경로로 바꿔줄 예정이다. 이 또한 뒤에서 다시 설명한다.

 

3. AWS CLI 설치 및 설정

AWS CLI(Command Line Interface)란 커맨드 라인으로 AWS의 각종 기능에 접근하기 위한 프로그램을 의미한다. AWS CLI는 운영체제별로 설치 방법이 다른데, AWS 공식 문서에서 이를 아주 잘 설명하고 있으니 읽고 그대로 따라 하면 된다.

 

▼ AWS CLI 설치 방법 관련 공식 문서

 

Installing or updating the latest version of the AWS CLI - AWS Command Line Interface

To update your current installation of the AWS CLI, add your existing symlink and installer information to construct the install command with the --update parameter. $ sudo ./aws/install --bin-dir /usr/local/bin --install-dir /usr/local/aws-cli --update To

docs.aws.amazon.com

 

AWS CLI를 설치하고 나면, 이를 이용하기 위한 자격 증명(Credential) 설정을 해야 한다. 쉽게 말해서, AWS CLI를 이용하여 AWS의 각종 기능에 접근할 수 있는 권한이 있음을 증명하기 위한 설정을 하는 것이다. 이를 위해서는 먼저 IAM Console에 들어간 뒤 다음과 같은 과정을 통해 IAM 사용자를 추가하여 자격 증명 정보를 발급받아야 한다. (이미 있는 경우 그것을 사용하되, 정책 부분만 바꿔주면 된다.)

 

  • [사용자] 탭으로 간 후 [사용자 추가] 버튼을 클릭한다.
  • 사용자 이름을 입력하고, 액세스 유형으로 [프로그래밍 방식 액세스]를 선택한다.
  • [기존 정책 직접 연결] 버튼을 클릭하여 다음과 같은 정책들을 추가한 뒤 [다음] 버튼을 클릭한다.
    • AmazonEC2ContainerRegistryFullAccess
    • AmazonECS_FullAccess
    • AmazonEC2FullAccess
    • IAMFullAccess
  • 태그 추가 단계는 [다음] 버튼을 클릭하여 건너뛴다.
  • [사용자 만들기] 버튼을 클릭하여 IAM 사용자 추가를 완료한다.
  • 자격 증명 정보가 담긴 CSV 파일을 다운로드한다. (이때만 다운로드 가능하니 주의)

 

이제 발급받은 자격 증명 정보를 AWS CLI에 설정해야 한다. 이를 위해 다음 명령어를 실행한다.

aws configure
# AWS Access Key ID [None]: ???
# AWS Secret Access Key [None]: ???
# Default region name [None]: ap-northeast-2
# Default output format [None]: json
AWS Access Key IDAWS Secret Access Key에 지정해야 하는 값은 앞서 다운로드한 CSV 파일을 열어보면 확인할 수 있다. Default region name의 경우, 우리는 서울 리전을 사용할 것이므로 ap-northeast-2를 지정하면 된다. Default output format의 경우, 무엇으로 해도 상관없으니 그냥 json으로 지정하면 된다.

 

그러고 나면, 이제 앞서 등록한 자격 증명 정보에 해당하는 IAM 사용자에 설정된 권한만큼의 기능을 AWS CLI로 수행할 수 있게 된다.

 

4. ECS CLI 설치

ECS CLI(Command Line Interface)란 커맨드 라인으로 ECS의 각종 기능에 접근하기 위한 프로그램을 의미한다. 마찬가지로 ECS CLI도 운영체제별로 설치 방법이 다른데, AWS 공식 문서에서 이를 아주 잘 설명하고 있으니 읽고 그대로 따라 하면 된다.

 

▼ ECS CLI 설치 방법 관련 공식 문서

 

Installing the Amazon ECS CLI - Amazon Elastic Container Service

You must configure the Amazon ECS CLI with your AWS credentials, an AWS Region, and an Amazon ECS cluster name before you can use it.

docs.aws.amazon.com

 

5. ECR 리포지토리 생성

ECR(Elastic Container Registry)이란 AWS가 제공하는 Docker Registry를 의미하는 것으로, AWS만의 Docker Hub라고 생각하면 된다. 즉, Docker 이미지를 저장할 수 있는 원격 서버이다. 우리는 총 두 개의 Docker 이미지를 사용하므로, ECR에도 총 두 개의 리포지토리를 생성할 것이다. 이를 위해 다음 명령어를 실행한다.

aws ecr create-repository --repository-name eldorado-django
aws ecr create-repository --repository-name eldorado-nginx
--repository-name 옵션은 생성할 리포지토리의 이름을 지정하면 된다. 이는 해당 Docker 이미지를 다운로드할 때 사용할 ECR 리포지토리의 경로를 결정짓는다.

※ IAM 사용자에 AmazonEC2ContainerRegistryFullAccess 정책 등록 필요 (ecr:CreateRepository 권한의 확보를 위함)

 

이제 AWS Console의 ECR에 들어가 보면, ECR 리포지토리가 두 개 생성되어 있는 걸 확인할 수 있을 것이다.

 

6. Docker 이미지 생성

이제 앞서 생성한 두 개의 ECR 리포지토리에 푸시할 두 개의 Docker 이미지를 생성하자. 먼저, docker-compose 설정 파일에서 image 설정들을 해당하는 ECR 리포지토리의 경로로 변경해준다. 각 ECR 리포지토리의 경로는 AWS Console에서 ECR에 들어가 보면 URI 컬럼에서 확인할 수 있다.

version: '3'
services:
  django:
    ...
    image: <???>.dkr.ecr.ap-northeast-2.amazonaws.com/eldorado-django
    ...
  nginx:
    ...
    image: <???>.dkr.ecr.ap-northeast-2.amazonaws.com/eldorado-nginx
    ...

 

이제 수정한 docker-compose 설정 파일을 이용하여 두 개의 Docker 이미지를 생성하자. 이를 위해 다음 명령어를 실행한다.

docker-compose -f docker-compose.production.yml build
docker-compose.production.yml 파일을 참조할 수 있어야 하기 때문에, eldorado 프로젝트로 이동한 다음 실행해야 한다.

 

7. Docker 이미지 푸시

먼저 다음 명령어를 실행하여 ECR 리포지토리에 푸시할 수 있는 권한을 획득한다. (= Docker 로그인)

aws ecr get-login-password --region ap-northeast-2 | \
  docker login --username AWS --password-stdin \
  <???>.dkr.ecr.ap-northeast-2.amazonaws.com
Docker 이미지를 ECR 리포지토리에 푸시하려면 docker login 명령어를 실행하여 ECR 리포지토리에 대한 권한을 획득해야 한다. 이때 --username 옵션을 AWS로 지정하면서 ECR 인증 토큰도 함께 전달해줘야 한다. ECR 인증 토큰은 aws ecr get-login-password 명령어를 실행함으로써 얻는 매우 긴 임의의 문자열이다.

 

이제 다음 명령어를 실행하여 앞서 생성한 두 개의 Docker 이미지를 각각의 ECR 리포지토리에 푸시한다.

docker-compose -f docker-compose.production.yml push
이름이 원격 리포지토리의 경로인 Docker 이미지가 위 명령어의 대상이 된다.

 

8. ECS 클러스터 생성

ECS에서 클러스터(Cluster)란 컨테이너 인스턴스(Container Instance)의 묶음을 의미한다. 여기서 컨테이너 인스턴스란 쉽게 말해서 EC2를 의미한다. 즉, 클러스터는 EC2의 묶음이다. 우리는 총 두 개의 EC2로 이뤄진 하나의 클러스터를 사용할 것이다.

 

먼저, ECS CLI에서 사용할 클러스터 설정을 정의해줘야 한다. 이를 위해 다음 명령어를 실행한다.

ecs-cli configure \
  --config-name eldorado-ecs-cluster-conf \
  --cluster eldorado-ecs-cluster \
  --default-launch-type EC2 \
  --region ap-northeast-2
--config-name 옵션은 정의할 클러스터 설정의 이름, --cluster 옵션은 생성할 클러스터의 이름을 지정하면 된다. --default-launch-type 옵션은 시작 유형을 지정하는 것으로, 우리는 EC2를 사용할 것이므로 EC2를 지정하면 된다. --region 옵션은 클러스터를 생성할 리전을 지정하는 것으로, 우리는 서울 리전을 사용할 것이므로 ap-northeast-2를 지정하면 된다.

 

다음으로, 앞서 정의한 클러스터 설정을 기반으로 클러스터를 생성하면 된다. 이를 위해 다음 명령어를 실행한다.

ecs-cli up \
  --cluster-config eldorado-ecs-cluster-conf \
  --size 2 \
  --instance-type t3.medium \
  --vpc vpc-XXXXXXXX \
  --subnets subnet-XXXXXXXX,subnet-XXXXXXXX,subnet-XXXXXXXX,subnet-XXXXXXXX \
  --keypair eldorado-key-pair \
  --capability-iam
--cluster-config 옵션은 클러스터를 생성하기 위해 사용할 클러스터 설정의 이름을 지정하는 것으로, 앞서 정의한 설정의 이름을 지정하면 된다. --size 옵션은 생성할 EC2의 개수를 지정하는 것으로, 우리는 두 개의 EC2를 사용할 것이므로 2를 지정하면 된다. --instance-type 옵션은 생성할 EC2의 성능을 지정하면 된다. --vpc 옵션은 사용할 VPC의 ID를 지정하는 것으로, 지정하지 않으면 자동으로 생성되지만 혼란을 막기 위해 디폴트 VPC의 ID를 지정해주는 것을 권장한다. --subnets 옵션은 --vpc 옵션을 지정한 경우 필수적으로 지정해야 하는 옵션으로, 해당 VPC에 속한 서브넷들의 ID를 지정하면 된다. --keypair 옵션은 이 클러스터의 생성에 의해 자동으로 생성될 EC2들에 추후 SSH로 접속하기 위해 사용할 키 페어 파일의 이름을 지정하는 것으로, 가지고 있는 키 페어 파일(홈 디렉토리의 .ssh 디렉토리에 위치해야 함)의 이름을 지정하면 된다. 만약 아직 키 페어 파일이 없다면, 여기를 참조하여 키 페어 파일을 발급받도록 하자.

※ IAM 사용자에 AmazonECS_FullAccess, AmazonEC2FullAccess, IAMFullAccess 정책 등록 필요

 

이제 AWS Console의 ECS에 들어가 보면, 클러스터가 하나 생성되어 있는 걸 확인할 수 있을 것이다.

 

9. ECS 작업 정의 생성

ECS에서 작업 정의(Task Definition)란 하나 혹은 여러 개의 컨테이너를 어떻게 실행할 것인가에 대한 설정을 담은 JSON 파일을 의미한다. 일반적으로는 하나의 필수 컨테이너로 구성되지만, 필요한 경우 여러 개의 부가적인 컨테이너까지 함께 담는다. 작업(Task)이란 이러한 작업 정의에 의해 실행되는 대상을 말하며, ECS의 기본 관리 단위에 해당한다. 우리는 Django 컨테이너와 Nginx 컨테이너로 이뤄진 작업을 정의해볼 것이다.

 

다음과 같이 ecs-cli compose 명령어를 실행하면 docker-compose 설정 파일을 읽어서 작업 정의를 자동으로 생성한다.

ecs-cli compose \
  -f docker-compose.production.yml
  -p eldorado-ecs-td \
  create \
  --cluster-config eldorado-ecs-cluster-conf
-f 옵션은 작업 정의를 생성하기 위해 필요한 docker-compose 설정 파일의 경로를 지정하는 것으로, 이 명령어를 실행하는 위치에 맞춰 적절히 지정하면 된다. -p 옵션은 생성할 작업 정의의 이름을 지정하면 된다. --cluster-config 옵션은 사용할 클러스터 설정의 이름을 지정하는 것으로, 앞서 정의한 설정의 이름을 지정하면 된다.

 

이제 AWS Console의 ECS에 들어가 보면, 작업 정의가 하나 생성되어 있는 걸 확인할 수 있을 것이다.

 

10. 보안 그룹 설정

보안 그룹(Security Group)이란 방화벽 규칙을 의미한다. 같은 보안 그룹에 속한 리소스들은 해당 보안 그룹에서 지정한 인바운드 규칙과 아웃바운드 규칙을 적용받는다. 여기서는 로드 밸런서, EC2, RDS, Redis를 위한 보안 그룹을 각각 어떻게 설정할지 알아볼 것이다. 단, RDS와 Redis의 경우 사용하지 않는다면 건너뛰어도 괜찮다. 다음 표를 참고하자.

 

보안 그룹 인바운드 규칙 아웃바운드 규칙
로드 밸런서 보안 그룹
HTTP, TCP, 80, 0.0.0.0/0
HTTPS, TCP, 443, 0.0.0.0/0
모든 TCP, TCP, 0 - 65535, EC2 보안 그룹
EC2 보안 그룹
모든 TCP, TCP, 0 - 65535, 로드 밸런서 보안 그룹
(필요한 경우) SSH, TCP, 22, 특정 IP
모든 트래픽, 전체, 전체, 0.0.0.0/0
RDS 보안 그룹 (선택)
PostgreSQL, TCP, 5432, EC2 보안 그룹 모든 트래픽, 전체, 전체, 0.0.0.0/0
Redis 보안 그룹 (선택)
사용자 지정 TCP, TCP, 6379, EC2 보안 그룹 모든 트래픽, 전체, 전체, 0.0.0.0/0
1. 로드 밸런서 보안 그룹 (생성 필요)
로드 밸런서의 경우, 인바운드 규칙에서는 모든 IP로부터의 80(HTTP), 443(HTTPS) 트래픽을 허용하도록 한다. 그리고 아웃바운드 규칙에서는 EC2로의 모든 TCP 트래픽을 허용하도록 한다. 포트가 0부터 65535까지인 이유는 로드 밸런서가 트래픽을 임의의 동적 포트로 EC2에 전달할 것이기 때문이다. 이에 대해서는 뒤에서 자세히 설명한다.

2. EC2 보안 그룹 (클러스터 생성 과정에서 자동으로 생성)
EC2의 경우, 인바운드 규칙에서는 로드 밸런서로부터의 모든 TCP 트래픽과 특정 IP로부터의 22(SSH) 트래픽을 허용하도록 한다. 여기서 특정 IP란 해당 EC2에 SSH로 접속하고자 하는 IP를 의미한다. 그리고 아웃바운드 규칙에서는 외부로의 모든 트래픽을 허용하도록 한다.

3. RDS 보안 그룹 (생성 필요)
RDS의 경우, 인바운드 규칙에서는 EC2로부터의 5432(PostgreSQL) 트래픽을 허용하도록 한다. 그리고 아웃바운드 규칙에서는 외부로의 모든 트래픽을 허용하도록 한다.

4. Redis 보안 그룹 (생성 필요)
Redis의 경우, 인바운드 규칙에서는 EC2로부터의 6379 트래픽을 허용하도록 한다. 그리고 아웃바운드 규칙에서는 외부로의 모든 트래픽을 허용하도록 한다.

 

11. 대상 그룹, 로드 밸런서, ECS 서비스 개념

이번 섹션에서는 대상 그룹, 로드 밸런서, ECS 서비스의 개념을 알아볼 것이다. 만약 이에 대한 이해가 이미 있다면 다음 섹션으로 바로 넘어가도 되지만, ECS 배포를 처음 하는 사람이라면 제대로 이해하고 있기가 어려운 내용이므로 잘 읽어보기 바란다. 다음 그림은 우리가 최종적으로 구축할 환경의 구조를 보여준다.

 


11-1. 로드 밸런서와 대상 그룹

로드 밸런서(Elastic Load Balancer, ELB)는 자신에게 부착된 대상 그룹(Target Group)을 통해 여러 개의 EC2에 트래픽을 분산시키는 역할을 수행한다. 사용자는 로드 밸런서에 80(HTTP) 혹은 443(HTTPS) 포트로 접근하는데, 이때 80(HTTP) 포트로 접근한 경우에는 로드 밸런서가 301 리다이렉트 응답을 반환하여 사용자가 다시 443(HTTPS) 포트로 접근하게 한다(이를 어떻게 하는지는 뒤에서 설명). 결론적으로 로드 밸런서는 443(HTTP) 포트로만 트래픽을 받는다고 생각하면 되며, 이때 로드 밸런서는 해당 트래픽을 그대로 대상 그룹에 전달한다. 실제로 사용자들의 트래픽은 로드 밸런서를 통해 EC2에 도달하는 것이기 때문에, 도메인 및 HTTPS 인증서 등의 설정은 전부 로드 밸런서를 대상으로 하게 된다.


11-2. 대상 그룹의 기본 동작 (≠ ECS에서의 실제 동작)

기본적으로 대상 그룹은 자신만의 포트 및 프로토콜이 지정되어 있어, 로드 밸런서로부터 트래픽을 전달받으면 해당 포트 및 프로토콜로 자신에게 등록된 임의의 EC2에 해당 트래픽을 전달한다. 하지만 로드 밸런서를 ECS의 서비스에서 사용할 때는 조금 다른 동작을 보인다. 이를 이해하기 위해, 먼저 ECS에서 서비스라는 것이 무엇인지부터 알아보도록 하자.


11-3. ECS 서비스 (Service)

ECS에서 서비스(Service)란 여러 개의 작업을 여러 개의 EC2에 적절히 분산하여 배치 및 실행시키고 이들의 실행 상태를 관리하는 단위를 의미하며, 특정 작업 정의 및 클러스터를 기반으로 생성된다. 즉, 특정 클러스터에 속한 여러 개의 EC2에 특정 작업 정의를 기반으로 (지정된 개수만큼의) 작업들을 분산하여 배치 및 실행시키는 것이다. 서비스의 생성 과정은 뒤에서 설명한다.


11-4. 정적/동적 포트 매핑 (Static/Dynamic Port Mapping)

작업 정의를 생성하기 위한 docker-compose 설정 파일에는 각 컨테이너에 대해 포트 매핑 설정을 할 수 있다. 만약 Nginx 컨테이너의 포트 매핑 설정을 "8080:80"으로 지정했다면, 이는 컨테이너 포트 80을 호스트 포트 8080에 매핑한다는 의미이다. 이때 호스트 포트가 8080으로 고정되기 때문에 이를 정적 포트 매핑(Static Port Mapping)이라고 한다.

 

반면, 포트 매핑 설정을 "80"으로 지정했다면, 이는 곧 "0:80"을 의미하는 것으로 컨테이너 포트 80을 임의의 호스트 포트에 동적으로 매핑한다는 의미이다. 호스트 포트가 매번 동적으로 결정되기 때문에 이를 동적 포트 매핑(Dynamic Port Mapping)이라고 한다. 우리의 docker-compose 설정 파일을 보면 Nginx 컨테이너의 포트 매핑 설정이 이러함을 볼 수 있다.

 

우리가 동적 포트 매핑을 사용하는 이유는 무엇일까? 정적 포트 매핑을 사용하는 경우, 호스트 포트의 충돌로 인해 하나의 EC2에서 여러 개의 작업을 실행하는 것이 불가능하기 때문이다. 그런데 우리는 두 개의 EC2에 두 개의 작업을 배치 및 실행시킬 것이다. 그렇다면 정적 포트 매핑을 사용해도 되지 않을까? 그렇지 않다. 우리는 안정적인 배포를 위해, 기존 두 개의 작업을 그대로 둔 채 추가로 두 개의 새 작업을 실행시킨 다음 기존 두 개의 작업을 중지시키는 방식을 택할 것이다. 이 방식을 사용하면 순간적으로 하나의 EC2에서 여러 개의 작업이 실행되는 시점이 존재하게 된다. 따라서 정적 포트 매핑이 아닌 동적 포트 매핑을 사용해야 한다.


11-5. ECS에서 로드 밸런서와 대상 그룹의 실제 동작

서비스에는 선택적으로 로드 밸런서를 설정해주는 것이 가능하다. 이때, 로드 밸런서와 대상 그룹의 동작이 기본 동작과 조금 달라진다. 서비스에 의해 EC2에서 작업이 하나 실행될 때마다 대상 그룹에는 (해당 EC2, 해당 작업의 호스트 포트) 매핑 정보가 자동으로 등록된다. 하나의 매핑 정보는 곧 하나의 대상(Target)이 된다. 예를 들어, 포트 매핑 설정이 "81:80"으로 지정된 Nginx 컨테이너 하나와 포트 매핑 설정이 없는 Django 컨테이너 하나로 이뤄진 작업을 이름이 A인 EC2와 이름이 B인 EC2에서 각각 실행시켰다면, 대상 그룹에는 (A, 81), (B, 81) 이렇게 네 개의 매핑 정보가 자동으로 등록된다. 그리고 대상 그룹은 자신에게 지정된 포트 및 프로토콜을 무시한 채, 로드 밸런서로부터 전달받는 트래픽을 각각의 매핑 정보대로 EC2에 전달하게 된다. 위 예시의 경우, A의 81 포트 혹은 B의 81 포트로 트래픽을 전달하는 것이다. 참고로 이러한 동작은 ALB(Application Load Balancer) 유형의 로드 밸런서를 사용하는 경우에만 해당한다.

포트 매핑 설정이 "81:80"으로 지정된 Nginx 컨테이너 하나와 포트 매핑 설정이 "8001:8000"으로 지정된 Django 컨테이너 하나로 이뤄진 작업을 이름이 A인 EC2와 이름이 B인 EC2에서 각각 실행시켰다면 어떻게 될까? 즉, 하나의 작업에서 호스트로 포트가 개방된 컨테이너가 두 개 이상인 경우이다. 이 경우, (뒤에서 알아보겠지만) 로드 밸런서에서 트래픽을 어떠한 컨테이너로 전달할지 선택하게 된다. 만약 Nginx 컨테이너만 선택한다면 A의 81 포트 혹은 B의 81 포트로 트래픽을 전달할 것이고, Django 컨테이너까지 선택한다면 A의 81 포트, A의 8001 포트, B의 81 포트, B의 8001 포트 중 하나로 트래픽을 전달할 것이다.

11-6. 로드 밸런서의 상태 검사 (Health Check) 요청

로드 밸런서는 대상 그룹에 등록된 각각의 대상(= 매핑 정보)에 대해 상태 검사(Health Check) 요청을 주기적으로 보낸다. 미리 설정되어 있는 경로, 포트, 프로토콜을 통해 각 대상에 요청을 보내보고, 200 응답이 오는지 확인하는 것이다. 이때, 우리의 경우 고정된 포트가 아닌 '트래픽 포트'라는 것을 이용할 것이기 때문에, 로드 밸런서가 대상 그룹을 통해 대상에게 트래픽을 전달할 때 사용하는 포트, 즉 각 EC2에서 개방되어 있는 동적 포트들을 이용하여 상태 검사 요청이 이뤄질 것이다.

 

12. 대상 그룹 생성

대상 그룹은 다음과 같은 명령어를 실행하여 생성한다.

aws elbv2 create-target-group \
  --target-type instance \
  --name eldorado-ecs-elb-tg \
  --protocol HTTP \
  --port 80 \
  --vpc-id vpc-XXXXXXXX \
  --protocol-version HTTP1 \
  --health-check-protocol HTTP \
  --health-check-path /health-check \
  --health-check-port traffic-port
--name 옵션은 생성할 대상 그룹의 이름을 지정하면 된다. --protocol 옵션과 --port 옵션은 대상 그룹에 지정되는 프로토콜과 포트를 의미하는데, 앞서 설명했듯이 이건 어차피 무시되기 때문에 무엇을 지정해도 상관없다. --vpc-id 옵션은 사용할 VPC의 ID를 지정하는 것으로, 앞서 클러스터를 생성할 때 지정한 VPC의 ID를 지정하면 된다. --health-check-path는 상태 검사 요청에 사용할 경로를 지정하는 것으로, 우리의 Nginx 설정 파일대로라면 /health-check를 지정하면 된다. --health-check-port는 상태 검사 요청에 사용할 포트를 지정하는 것으로, 우리의 경우 동적 포트 매핑을 사용할 것이므로 고정 포트가 아닌 '트래픽 포트'를 지정하기 위해 traffic port를 지정하면 된다. '트래픽 포트'의 개념에 대해서는 위에서 설명했다.

※ 참고로, 이 단계에서 굳이 대상 그룹에 대상을 등록할 필요는 없다. 어차피 서비스 생성 과정에서 자동으로 등록되기 때문이다.

 

13. 로드 밸런서 생성

로드 밸런서는 다음과 같은 명령어를 실행하여 생성한다.

aws elbv2 create-load-balancer \
  --type application \
  --name eldorado-ecs-elb \
  --scheme internet-facing \
  --ip-address-type ipv4 \
  --subnets subnet-XXXXXXXX subnet-XXXXXXXX subnet-XXXXXXXX subnet-XXXXXXXX \
  --security-groups <로드 밸런서 보안 그룹 ID>
--type 옵션은 로드 밸런서 유형을 지정하는 것으로, 우리는 동적 포트 매핑을 위해 Application Load Balancer를 사용할 것이므로 application을 지정하면 된다. --name 옵션은 생성할 로드 밸런서의 이름을 지정하면 된다. --subnets 옵션은 앞서 클러스터를 생성할 때 지정한 VPC에 속한 서브넷들의 ID를 지정하면 된다. --security-groups 옵션은 앞서 생성한 로드 밸런서 보안 그룹의 ID를 지정하면 된다.

 

그리고 다음과 같은 명령어를 실행하여 80(HTTP) 및 443(HTTPS) 포트에 대한 리스너 규칙을 추가한다.

# 80(HTTP) 리스너 규칙 : 443(HTTPS)으로의 리다이렉트
aws elbv2 create-listener \
  --load-balancer-arn <로드 밸런서 ARN> \
  --protocol HTTP --port 80 \
  --default-actions '[{\"Type\": \"redirect\", \"RedirectConfig\": {\"Protocol\": \"HTTPS\", \"Port\": \"443\", \"StatusCode\": \"HTTP_301\"}}]'

# 443(HTTPS) 리스너 규칙 : 대상 그룹으로 전달
aws elbv2 create-listener \
  --load-balancer-arn <로드 밸런서 ARN> \
  --protocol HTTPS --port 443 \
  --default-actions '[{\"Type\": \"forward\", \"ForwardConfig\": {\"TargetGroups\": [{\"TargetGroupArn\": \"<대상 그룹 ARN>\"}]}}]' \
  --certificates CertificateArn=<인증서 ARN>
--load-balancer-arn 옵션은 리스너 규칙을 추가할 로드 밸런서의 ARN을 지정하면 된다. ARN 정보는 AWS Console에서 확인할 수 있다. --protocol 옵션과 --port 옵션은 로드 밸런서가 Listen 할 프로토콜 및 포트를 지정하는 것으로, 80(HTTP)과 443(HTTPS)을 지정하면 된다. 80(HTTP) 포트로의 접근은 443(HTTPS) 포트로 다시 접근하도록 301 리다이렉트 응답을 반환하게 한다. 그리고 443(HTTPS) 포트로의 접근은 앞서 생성한 대상 그룹으로 트래픽을 전달하게 한다. 이때 HTTPS를 사용하기 위해 HTTPS 인증서를 지정하는데, AWS에서 인증서는 ACM(Amazon Certificate Manager)에서 발급받을 수 있다. ACM에서 HTTPS 인증서를 발급받는 방법은 여기를 참조하자.

※ Windows PowerShell이 아니라면 \" 대신 "를 사용하자. Windows PowerShell에서만 이스케이프가 필요한 것 같다.

 

14. ECS 서비스 생성

ECS에서 서비스(Service)란 여러 개의 작업을 여러 개의 EC2에 적절히 분산하여 배치 및 실행시키고 이들의 실행 상태를 관리하는 단위를 의미하며, 특정 작업 정의 및 클러스터를 기반으로 생성된다. 따라서 서비스의 생성은 곧 각 EC2에 작업들이 적절히 배치 및 실행되는 것을 의미한다. 다음은 서비스를 생성하기 위한 명령어이다.

aws ecs create-service \
  --service-name eldorado-ecs-service \
  --launch-type EC2 \
  --task-definition eldorado-ecs-td \
  --cluster eldorado-ecs-cluster \
  --desired-count 2 \
  --deployment-controller type=ECS \
  --deployment-configuration minimumHealthyPercent=100,maximumPercent=200 \
  --health-check-grace-period-seconds 600 \
  --scheduling-strategy REPLICA \
  --load-balancers '[{\"targetGroupArn\": \"<대상 그룹 ARN>\", \"containerName\": \"nginx\", \"containerPort\": 80}]'
--service-name 옵션은 생성할 서비스의 이름을 지정하면 된다. --launch-type 옵션은 시작 유형을 지정하는 것으로, EC2를 지정하면 된다. --task-definition 옵션은 앞서 생성한 작업 정의의 이름을 지정하면 된다. --cluster 옵션은 앞서 생성한 클러스터의 이름을 지정하면 된다. --desired-count 옵션은 실행할 작업들의 개수를 지정하는 것으로, 우리의 경우 두 개의 작업을 배치 및 실행시킬 것이므로 2를 지정하면 된다.

--deployment-controller 옵션은 배포 유형을 지정하는 것으로, 우리는 롤링 업데이트를 사용할 것이므로 ECS를 지정하면 된다. minimumHealthyPercent 옵션은 최소 정상 상태 백분율을 지정하는 것으로, 최소 두 개의 작업은 RUNNING 상태를 유지하도록 하기 위해 100을 지정하면 된다. maximumPercent 옵션은 최대 백분율을 지정하는 것으로, 최대 네 개의 작업까지만 RUNNING 또는 PENDING 상태를 유지하도록 하기 위해 200을 지정하면 된다. --health-check-grace-period-seconds 옵션은 서비스 스케쥴러가 몇 초 동안은 로드 밸런서 상태 검사를 통과하지 못한 대상에 해당하는 작업을 중지시키지 않도록 할지 지정하는 것으로, 우리의 경우 Django 컨테이너가 migrate 명령어와 collectstatic 명령어를 수행할 때까지 Nginx 컨테이너가 기다려야 하기 때문에 넉넉히 600초(= 10분)를 지정하면 된다. --schedule-strategy 옵션은 서비스 유형을 지정하는 것으로, 우리의 경우 REPLICA를 지정하면 된다.

--load-balancers 옵션은 서비스에 설정할 로드 밸런서에 관한 설정을 지정한다. targetGroupArn 옵션은 대상 그룹을 지정하는 것으로, 로드 밸런서가 어떤 대상 그룹으로 트래픽을 전달할지, 동시에 클러스터에 속한 EC2에 대한 매핑 정보들을 어떤 대상 그룹에 자동으로 등록할지를 명시한다. containerName 옵션은, EC2에서 실행할 각 작업이 여러 개의 컨테이너를 가질 수도 있기 때문에, 그중에 어떤 컨테이너로 트래픽을 전달할지 지정하는 것이며, containerPort 옵션은 해당 컨테이너의 어떤 포트로 트래픽을 전달할지 지정하는 것이다. 따라서 containerPort 옵션에 지정되는 포트는 실제로 해당 컨테이너가 가지는 컨테이너 포트와 일치해야 한다.

※ 공식 문서를 보면 Application Load Balancer의 경우에는 로드 밸런서의 이름을 명시적으로 지정하지 말라고 되어 있는데, 지정한 대상 그룹을 통해 자동으로 알아낼 수 있어서인지, 그 이유는 아직 정확히 파악하지 못했다.

 

15. 도메인 DNS 레코드 설정 편집 (AWS Route 53)

자신의 서비스에 사용할 도메인이 앞서 생성한 로드 밸런서를 가리키도록 DNS 레코드 설정을 편집해야 한다. AWS Route 53에서 도메인을 발급받는 방법에 대해서는 여기를 참조하자. AWS Console의 Route 53에 들어가 보면, 발급받은 도메인에 대한 DNS 레코드 설정을 편집할 수 있다. 여기서 A 레코드가 앞서 생성한 로드 밸런서를 별칭으로 가리킬 수 있도록 편집해주자. 이후 어느 정도의 시간을 기다린 후 해당 도메인으로 접속을 시도해보자. 성공했길 바란다.

 

16. 변경 사항을 ECS에 배포하기

만약 소스 코드나 배포 관련 설정이 변경되어 ECS에 다시 배포해야 하는 상황이 된다면, 다음과 같은 과정을 거치면 된다. 이는 곧 CI/CD 설정에서 CD 설정에 해당하는 스크립트를 작성할 때 필요한 명령어들에 해당한다.

 

16-1. Docker 이미지 생성

docker-compose -f docker-compose.production.yml build
새로운 Docker 이미지를 생성하는 과정이다.

 

16-2. Docker 이미지 푸시

aws ecr get-login-password --region ap-northeast-2 | \
  docker login --username AWS --password-stdin \
  <???>.dkr.ecr.ap-northeast-2.amazonaws.com
docker-compose -f docker-compose.production.yml push
새로운 Docker 이미지를 푸시하는 과정이다. 이때 로그인이 풀렸을 수도 있으므로 Docker 로그인도 다시 해준다.

 

16-3. 작업 정의 생성

ecs-cli compose \
  -f docker-compose.production.yml
  -p eldorado-ecs-td \
  create \
  --cluster-config eldorado-ecs-cluster-conf
새로운 버전(Revision)의 작업 정의를 생성하는 과정이다.

 

16-4. 서비스 업데이트

aws ecs update-service \
  --service eldorado-ecs-service \
  --task-definition eldorado-ecs-td \
  --cluster eldorado-ecs-cluster \
  --force-new-deployment
서비스를 업데이트하는 과정이다. 즉, 새로운 Docker 이미지로 작업들을 새로 실행하고, 기존의 작업들은 중지시킨다. --force-new-deployment 옵션은 서비스 정의가 변경되지 않은 경우에도 서비스를 업데이트하도록 강제한다. 일반적으로 서비스 정의는 변경되지 않았지만 Docker 이미지가 변경된 경우에 사용한다.

 

 

 

 

 

 

본 글은 아래 링크의 내용을 참고하여 학습한 내용을 나름대로 정리한 글임을 밝힙니다.

https://dev.to/sfrancavilla/deploy-web-apps-nginx-to-ecs-with-docker-198i

https://www.44bits.io/ko/post/container-orchestration-101-with-docker-and-aws-elastic-container-service