just1s AWS Elastic Container Service 기반 배포 아키텍쳐 정리
2020년 07월 16일

서론

우선 프로젝트 시작할 때부터 인프라에 Docker를 꼭 써보고 싶었습니다. 따라서 비즈니스 로직이나 외부 요인이 아닌 Hype Driven Development 기반으로 도입했습니다. 좋은지 안좋은지는 직접 적용해봐야 직성이 풀릴 것으로 판단해서 였습니다.

컨테이너 종류는 Vue.js 기반 프론트 페이지, API 서버 두 종류로 구성되어 있으며 DB는 제외하기로 했습니다.

로컬 환경에서는 docker-compose를 활용해 모든 컨테이너를 빌드 및 실행할 수 있도록 구성 후, 배포는 그 이후에 생각하기로 했습니다.

# Dockerfile in backend FROM node:lts-alpine WORKDIR /usr/src/app COPY . . RUN npm install && npm run build CMD ["npm", "run", "start:prod"] EXPOSE 3000
# Dockerfile in frontend FROM node:lts-alpine as build-stage WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build FROM nginx:stable-alpine COPY ./default.conf /etc/nginx/conf.d/default.conf COPY --from=build-stage /app/dist /usr/share/nginx/html EXPOSE 8080 CMD ["nginx", "-g", "daemon off;"]

Vue.js 컨테이너 같은 경우는 백엔드와 다르게 결과물이 static한 web page이기 때문에 multi-stage build를 활용했습니다. 먼저 빌드 후 그 결과물을 nginx를 통해 serve하는 방식이다. 마지막 stage에 있는 내용들만 빌드 결과물에 포함되고 이전 stage에서 복사되지 않는 것들은 버리기 때문에 컨테이너 경량화를 할 수 있습니다.

Container를 어떻게 배포할 것인가

docker-compose로 로컬에서 한번에 실행시키는 것 정도까지는 해보았습니다. 하지만 단순히 이것 가지고는 부족했습니다. 한번 build된 image를 기반으로 container를 매우 flexible하게 실행, 제거 및 적재적소에 배치할 수 있어야 의미가 있습니다. 이런 것을 관리해주는 것을 Container Orchestration 이라 하는데 가장 유명한 라이브러리는 google에서 제공하는 kubernetes가 있습니다.

AWS ECS - Elastic Container Service

ECS vs EKS - kubernetes는 google에서만 제공하는 줄 알았는데 AWS에서도 제공하고 있었습니다. 그래도 AWS를 이용하는데 ECS를 이용하면 연동할때 마찰이 적지 않을까 하여 ECS로 진행하기로 했습니다. 큰 이유가 있는 것은 아니었습니다.

ECS Service | EC2 | Amazon Web Services

Cluster - 컨테이너가 배포될 기본적인 단위. 정도로 우선 이해하고 넘어갔습니다.

Task Definition - 컨테이너를 어떻게 실행시킬 것인가에 대한 명세를 json으로 관리합니다. 컨테이너를 어느 저장소에서 받아올 것인가에서부터, 포트 맵핑, 시작 커맨드, 주입할 환경 변수, 호스트 내에 cpu 나 ram등 자원은 얼마나 할당할 것인가 등 다양한 옵션들이 있습니다.

Service - Task definition은 명세고, 실제 실행은 이 service로 한다.scale-out, 목표 task의 수, healthy한 task 수의 허용 최소, 최대 비율, 배포 전략, 로드 밸런싱 등을 관리합니다. 원하는 task 수(desired count)를 지정하면 가능한 상황 내에서 지정된 배포 전략을 통해 배포합니다. 자원이 한정되어 있다면 동작이 보장되지 않습니다.

Container Repository

Amazon ECR | Amazon Web Services

Docker Hub

처음에는 AWS에서 제공하는 Amazon Elastic Container Registry(ECR)를 저장소로 사용할까 하다가 dockerhub에 public으로 배포하면 무료인 것을 알게되어 Dockerhub를 사용했습니다.

배포

aws-cli를 통한 배포

# EC2 생성 시 user-data를 통해 주입하여 해당 cluster의 container들이 배포될 EC2라는 것을 지정 echo ECS_CLUSTER=ecs-cluster-01 >> /etc/ecs/ecs.config
# Task Definition 등록, 업데이트 개념이 없고 새 revision을 추가하는 방식 aws ecs register-task-definition --cli-input-json <json file>
# Service 정의 { "cluster": "ecs-cluster-01", "service": "ecs-service-web", "deploymentConfiguration": { "maximumPercent": 100, "minimumHealthyPercent": 0 }, "desiredCount": 1 } # Service 등록 aws ecs create-service --cluster=ecs-cluster-01 \ --cli-input-json <json file> # Service 업데이트 aws ecs update-service \ --cluster=ecs-cluster-01 \ --service=ecs-service-web \ --cli-input-json <json file>

github action을 통한 배포 자동화

name: Deploy on: [push] jobs: build_docker: name: Build Docker Image runs-on: ubuntu-latest steps: - uses: actions/checkout@master - name: Publish to Registry uses: elgohr/Publish-Docker-Github-Action@master with: name: kimjbstar/just1s-backend username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} dockerfile: Dockerfile update-service: name: Update AWS ESC Service needs: build_docker runs-on: ubuntu-latest steps: - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ap-northeast-2 - name: force update ecs run: aws ecs update-service --cluster=ecs-cluster-01 --service=service-back-1 --force-new-deployment

외부 접근

배포는 성공했고, 이제 www.just1s.xyz 도메인을 통해 완벽히 실행되는 것을 다음 목표로 잡았습니다.

결론

배포 프로세스

  1. git을 통해 github으로 코드 push
  2. github action에서 빌드 및 docker push를 통해 dockerhub 저장소에 배포
  3. aws ecs update
  4. aws ecs는 dockerhub에서 지정된 container의 latest 버전을 fetch하여 반영

노출 원리

  • Application Load Balancer를 서비스에 연결, 이 internal 로드밸런서에는 고정 아이피를 연결할 수 없다.
  • Network Load Balancer를 생성한다. 이 로드밸런스에는 Elastic IP 고정 아이피, Route 53을 통한 도메인을 연결할 수 있다.
  • 두 로드밸런서를 연결해주는 스케줄형 lambda function을 둔다.
    • populate_NLB_TG_with_ALB
    • ALB에서 사용하는 IP 주소 들을 DNS 쿼리로 주기적으로 받아 NLB target group에 popluate(적용)
    • ALB의 IP주소는 S3 Bucket에 저장한다.

위 구조를 도식화하면 다음과 같습니다.

결론, 한계점

전반적으로 한번에 많은 것을 배우다보니 깊게 배우지 못하고 넘어간 것들도 많은 것 같습니다. 인프라 관련 된 부분은 사실 실제 운영하면서 이슈가 더 많이 나기 때문에 이쪽에 경험이 많은 사람이 있었으면 더 좋았을텐데 독학으로 하다보니 한계점이 보였습니다.

사실 가장 원하던 그림은 동적 포트 할당을 이용해 클러스터 내 EC2에선 자유롭게 Container를 배포하는 것이었는데 이 케이스 같은 경우는 두 종류의 다른 컨테이너가 "포트"를 통해 구분되기 때문에 동적 포트를 사용하지 못했습니다. 결국 EC2를 작은 사이즈로 잡고 한번에 두 종류의 컨테이너만 들어가게끔 구성, EC2의 갯수는 max(백엔드, 프론트엔드)로 했습니다. 사실 아예 클러스터를 분리했으면 가능했을 수도 있습니다.

또한 ECS가 자동으로 repository에서 fetch하는 게 아니라 force update를 통해야 한다는 점이 마음에 걸렸습니다. 컨테이너를 배포할 때 tag를 latest가 아닌 배포 때마다 증가하는 값으로 넣어줬으면 자동으로 서비스가 fetch할 것 같기도 한데 테스트 해보지는 못했습니다.

무엇보다 배포 소요 시간이 큽니다. ECS Service의 minmum healthy를 0%로 과감히 낮추고 load balancer target group의 deregistration delay로 0초로 셋하여 불필요한 딜레이는 감소시켰지만 그래도 여전히 느린 편이었습니다.

이 방식은 배포 전략을 통해 안정적 무중단 배포를 할 때는 유용한 것 같습니다. 하지만 개발 단계에서 빠르게 테스트할 때는 적절치 않은 것으로 판단됩니다. 어쨋든 Docker랑 Orchestration에 대해 조금이나마 공부해 볼 수 있는 계기였습니다.

또한 간단한 서비스인데 Load Balancer가 2개나 들어가는 것이 걸렸습니다. Load Balancer는 구축만 해 놓아도 고정비가 나갑니다.

참고 링크

두 아티클을 많이 참고했습니다.

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

https://aws.amazon.com/ko/blogs/korea/using-static-ip-addresses-for-application-load-balancers/