Series
1부
NestJS 프로젝트 생성
간단한 환경 변수 설정하기
$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
Dockerfile & Docker Compose 설정하기
목표 구조
폴더 구조
$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 옵션을 이용하여 포트 포워딩을 수행해야 합니다.
참고자료
- ecs-nginx-reverse-proxy/tree/master/reverse-proxy
- [Docker 기본(8/8)] Docker의 Network
- [docker] network bridge mode
- Nginx Reverse Proxy 구성하기 (feat. Docker)
그리고 이 내부 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
docker compose up
이제 docker compose를 통해 앱에 필요한 이미지들을 빌드하였습니다. 하지만 ecs에서 docker compose로 빌드한 이미지들을 사용하기 위해선 어떻게 해야 할까요?
그건 AWS ECR을 사용하면 됩니다.
ECR 이미지 생성하기
이제 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
push를 하려고 하니 권한이 없다고 하네요.
이건 간단하게 AWS 계정의 자격 증명을 설정하면 됩니다.
그 외 에러가 발생한다면 아래 링크를 참고해주세요. https://docs.aws.amazon.com/ko_kr/AmazonECR/latest/userguide/common-errors-docker.html
두 이미지 모두 정상적으로 push된 것을 확인할 수 있습니다.
이제 ECS의 작업 정의에서 위 이미지들을 사용한다고 선언해주면 됩니다.