AWS ECS 개발기 1
NOWIL
by NOWIL
3 min read

Categories

Tags

Series

  1. aws-ecs-개발기 - intro
  2. aws-ecs-개발기 - 1
  3. aws-ecs-개발기 - 2
  4. aws-ecs-개발기 - 3

1부

NestJS 프로젝트 생성

Untitled

Untitled

Untitled

Untitled

Untitled

간단한 환경 변수 설정하기

$npm install @nestjs/config
#.env

AUTHOR=NOWIL
//app.module.ts

import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { ConfigModule } from "@nestjs/config";

@Module({
  imports: [ConfigModule.forRoot({ isGlobal: true })],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";

@Injectable()
export class AppService {
  constructor(private config: ConfigService) {}

  getHello(): string {
    const author = this.config.get("AUTHOR");
    return `Hello World! By ${author}`;
  }
}
npm run start:dev

Untitled

Untitled

Dockerfile & Docker Compose 설정하기

목표 구조

Untitled

폴더 구조

$tree . -L 2
.
├── app
│   ├── Dockerfile
│   ├── README.md
│   ├── dist
│   ├── ecosystem.config.js
│   ├── nest-cli.json
│   ├── node_modules
│   ├── package-lock.json
│   ├── package.json
│   ├── src
│   ├── test
│   ├── tsconfig.build.json
│   └── tsconfig.json
├── docker-compose.yml
└── nginx
    ├── Dockerfile
    ├── logrotate.conf
    └── nginx.conf

7 directories, 12 files

sample app의 Dockerfile

# Base image
FROM node:18

#간단한 환경변수 설정을 위해 저자의 이름을 추가하였습니다.
ARG AUTHOR_ARG
ENV AUTHOR=$AUTHOR_ARG

ENV TZ=Asia/Seoul

# Bundle APP files
WORKDIR /example-server

COPY . .

RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN npm cache clean --force
# 앱의 무중단 운영 및 프로세스 관리를 위해 Node.js의 프로세스 매니저인 PM2를 사용합니다.
RUN npm install -g pm2
RUN npm install -g dotenv-cli

# 앱의 의존성 패키지들을 설치합니다.
RUN npm install

# 프로덕션 빌드로 "dist" 폴더를 생성합니다.
RUN npm run build

# 앱의 리스닝 포트를 노출합니다.
EXPOSE 3000

# ecosystem.config.js 설정 파일을 통해 앱을 PM2로 실행합니다.
CMD ["sh", "-c", "pm2-runtime start ecosystem.config.js --env production"]

reverse-proxy(NGINX)의 Dockerfile

FROM nginx:latest
COPY nginx.conf /etc/nginx/nginx.conf
COPY logrotate.conf /etc/logrotate.conf

RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

nginx의 log rotate에 대한 자세한 설명은 아래 링크를 참고해주세요. https://www.lesstif.com/system-admin/nginx-log-rotate-logrotate-75956229.html

docker-compose.yml

version: "3"

networks:
  server:
    driver: bridge

services:
  reverse-proxy-container:
    platform: linux/amd64
    build:
      context: ./nginx
    ports:
      - 80:80
    restart: always
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/logrotate.conf:/etc/logrotate.conf
    environment:
      TZ: Asia/Seoul
    depends_on:
      example-app-container:
        condition: service_healthy
    networks:
      - server

  example-app-container:
    platform: linux/amd64
    build:
      context: ./app
    # app env file을 사용
    env_file:
      - ./app/.env
    ports:
      - 3000:3000
    healthcheck:
      test: curl -f http://localhost:3000
      interval: 1s
      timeout: 3s
      retries: 20
    networks:
      - server

참고 : docker compose 파일에서 reverse-proxy 처리하기

docker에서 reverse-proxy를 구현하는 방법은 여러가지 있습니다. NGINX config 파일에 앱서버의 주소와 포트를 하드코딩하여 입력하는 방법도 있죠. 하지만 이는 보안적으로나 여러가지로 매우 위험한 방법입니다. 이를 위해 docker에서는 Service Discovery를 제공합니다. Docker 네트워크 상에 내부 DNS 서버를 갖고 있고 같은 Docker 네트워크 상에 있다면 각 컨테이너에서는 서비스 이름만으로도 접속할 수 있죠.

위 docker-compose.yml을 보면 example-app-container와 reverse-proxy-container 모두 server라는 네트워크 상에 있습니다. 그리고 그들은 bridge 방식으로 연결되어 있죠. 그리고 bridge 네트워크는 private ip를 할당받으며 컨테이너 이름을 통해 통신을 가능하게 해줍니다. 하지만 host로 들어오는 트래픽을 직접 받을 수 없기 때문에 -D 옵션을 이용하여 포트 포워딩을 수행해야 합니다.

참고자료

그리고 이 내부 DNS 서버를 NGINX의 config에서 그대로 사용할 수 있습니다.

...
http {
...
upstream app {
    server example-app-container:3000;
  }
  ...
  server {
    listen 80;
    ...
# 지정한 패턴으로 시작
    location /api/ {
      proxy_pass http://app/;
      ...
    }
  }
}

example-app-container가 reverse-proxy-container보다 먼저 시작되어야 합니다. 이를 위해 depends_on 설정을 사용합니다. 하지만 depends_on의 가장 큰 단점은 후순위 컨테이너가 선순위 컨테이너의 완료 여부를 체크하지 않는다는 점입니다. 이를 해결하기 위해 condition 옵션을 주어 선순위 컨테이너가 test를 통과해야 다음 컨테이너가 실행되도록 컨트롤합니다.

reverse-proxy-container:
---
depends_on:
  example-app-container:
    #example-app-container의 healthcheck가 통과되면 reverse-proxy-container가 실행됩니다.
    condition: service_healthy
---
example-app-container:
---
#example-app-container가 시작되고 localhost:3000로 통신이 가능한지 체크합니다.
healthcheck:
  test: curl -f http://localhost:3000
  interval: 1s
  timeout: 3s
  retries: 20

Docker compose 빌드 및 실행

docker compose build

Untitled

docker compose up

Untitled

Untitled

이제 docker compose를 통해 앱에 필요한 이미지들을 빌드하였습니다. 하지만 ecs에서 docker compose로 빌드한 이미지들을 사용하기 위해선 어떻게 해야 할까요?

그건 AWS ECR을 사용하면 됩니다.

ECR 이미지 생성하기

Untitled

Untitled

Untitled

이제 ECR 이미지들을 생성하였으니 docker compose 파일에 해당 이미지들을 사용한다고 선언해줍니다.

version: "3"

networks:
  server:
    driver: bridge

services:
  reverse-proxy-container:
    image: 010920087890.dkr.ecr.ap-northeast-2.amazonaws.com/reverse-proxy:latest
    platform: linux/amd64
    build:
      context: ./nginx
    ports:
      - 80:80
    restart: always
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/logrotate.conf:/etc/logrotate.conf
    environment:
      TZ: Asia/Seoul
    depends_on:
      example-app-container:
        condition: service_healthy
    networks:
      - server

  example-app-container:
    image: 010920087890.dkr.ecr.ap-northeast-2.amazonaws.com/ecs-sample-app:latest
    platform: linux/amd64
    build:
      context: ./app
    # app env file을 사용
    env_file:
      - ./app/.env
    ports:
      - 3000:3000
    healthcheck:
      test: curl -f http://localhost:3000
      interval: 1s
      timeout: 3s
      retries: 20
    networks:
      - server

그 후 전 과정에서 똑같이 빌드를 해준 후 push를 한번 해보겠습니다.

docker compose build
docker compose push

Untitled

push를 하려고 하니 권한이 없다고 하네요.

이건 간단하게 AWS 계정의 자격 증명을 설정하면 됩니다.

Untitled

그 외 에러가 발생한다면 아래 링크를 참고해주세요. https://docs.aws.amazon.com/ko_kr/AmazonECR/latest/userguide/common-errors-docker.html

Untitled

Untitled

Untitled

두 이미지 모두 정상적으로 push된 것을 확인할 수 있습니다.

이제 ECS의 작업 정의에서 위 이미지들을 사용한다고 선언해주면 됩니다.