https://www.inforum24.com/memos/1309
이전 Dockerize에 이어서 다음 문제들을 해결해보자.
Dockerfile을 수정하여 도커 캐시를 활용해 빌드 시간을 최적화한다.
ECS를 사용하여 EC2로 배포하되, Task 는 Uvicorn 하나로 설정하고, 하나의 EC2의 vCPU + 1 개의 테스크를 실행해야한다.
로깅은 도커의 마운트 기능을 이용하여 호스트 EC2에 파일을 놓고, AWS CloudWatch Log 에이전트 이미지를 실행시켜 로그 파일을 수집하도록 한다.
이전 도커파일에서 로깅을 위한 명령어를 추가하고, 애플리케이션에 필요한 다른 폴더들도 COPY 하였다.
FROM python:3.12.2-slim
RUN pip install poetry
COPY .env poetry.lock poetry.toml pyproject.toml logconfig.py ./
COPY ./app ./app
COPY ./utils ./utils
COPY ./to_search ./to_search
RUN poetry install --no-root
RUN mkdir logs /logs/access_log /logs/debug_log /logs/user_log
ARG BUILD_DATE
ENV OUTPUT_FILE="${BUILD_DATE}.log"
ENV OUTPUT_LATEST_FILE=latest.log
RUN touch "logs/${OUTPUT_FILE}"
RUN ln -sf "${OUTPUT_FILE}" "logs/${OUTPUT_LATEST_FILE}"
ENTRYPOINT ["sh", "-c", "poetry run gunicorn -w 2 -k uvicorn.workers.UvicornH11Worker --bind 0.0.0.0:8800 --log-config logconfig.py app.main:app >> logs/${OUTPUT_FILE} 2>&1"]
먼저, 위 도커파일을 최적화해보자.
Docker는 image를 통해 애플리케이션을 실행하는데, 이 때 내부적으로 Layer를 쌓으면서 실행된다.
명령어 한 줄 마다 Layer가 쌓인다.
Layer 에서 변한 것이 없다면, 기존의 Layer 를 재사용하여 build 과정에서 속도를 높이고, 중복된 Layer 를 방지하여 저장공간의 효율을 높일 수 있다.
캐싱 여부는 ADD, COPY 를 제외하면 도커파일의 String 형태만 확인하고, ADD, COPY 는 String 형태 + 내용의 변화까지 확인하여 캐싱한다.
한 번 캐싱할 수 없는 구간이 발생하면, 그 아래 모든 Layer도 캐싱할 수 없다.
따라서, 위 Dockerfile 을 실행에는 영향이 없되, 변화가 빈번한 파일에 대한 접근을 뒤로 빼고, 변화가 자주 일어나지 않는 명령어를 위로 올려 캐싱하자.
FROM python:3.12.2-slim
RUN pip install poetry
COPY poetry.lock poetry.toml pyproject.toml ./
RUN poetry install --no-root
RUN mkdir logs /logs/access_log /logs/debug_log /logs/user_log
COPY .env logconfig.py ./
COPY ./to_search ./to_search
COPY ./utils ./utils
COPY ./app ./app
ARG BUILD_DATE
ENV OUTPUT_FILE="${BUILD_DATE}.log"
ENV OUTPUT_LATEST_FILE=latest.log
RUN touch "logs/${OUTPUT_FILE}"
RUN ln -sf "${OUTPUT_FILE}" "logs/${OUTPUT_LATEST_FILE}"
ENTRYPOINT ["sh", "-c", "poetry run gunicorn -w 2 -k uvicorn.workers.UvicornH11Worker --bind 0.0.0.0:8800 --log-config logconfig.py app.main:app >> logs/${OUTPUT_FILE} 2>&1"]
이전 Dockerize 시도에서 확인했듯이 pip install poetry
, poetry install
은 빌드 시간의 대부분을 차지한다.
따라서, 가능한 가장 상단 Layer 에 위치 시켜 poetry를 통한 의존 라이브러리가 바뀌지 않는 이상 캐싱을 활용할 수 있도록 한다.
이후 명령어들은 대부분 수행시간이 0.1s 이므로 캐싱이 큰 의미는 없으나, 로깅 파일은 빌드시간을 받아 작성하므로 String이 달라져 항상 캐싱이 되지 않으므로 하위로 뺀다.
변경이 자주 일어나는 지점이 최대한 아래있는 것이 캐싱에 도움이 되므로, 가장 변경이 적은 파일부터 많은 순으로 COPY 명령어를 작성한다.
이제 코드를 변경하고 다시 빌드해보자.
9번 /app COPY 부터 캐싱이 적용되지 않는다. (변경지점)
10, 11번은 빌드 시간으로 명령어를 수행하므로 어차피 캐싱이 되지 않는 지점이다.
이전 글에서 보았듯이 스케일 아웃의 단위는 작을수록 좋다. (https://www.inforum24.com/memos/1310)
따라서, 단일 프로세스로 컨테이너를 실행시킬 수 있도록 명령어를 수정해보자.
ENTRYPOINT ["sh", "-c", "poetry run gunicorn -w 2 -k uvicorn.workers.UvicornH11Worker --bind 0.0.0.0:8800 --log-config logconfig.py app.main:app >> logs/${OUTPUT_FILE} 2>&1"]
ENTRYPOINT ["sh", "-c", "poetry run uvicorn --host 0.0.0.0 --port 8800 --log-config logconfig.py app.main:app >> logs/${OUTPUT_FILE} 2>&1"]
uvicorn 으로 실행시키도록 uvicorn의 명령어를 사용한다.
host, port, log-config 를 gunicorn 과 동일하게 설정한다.
지금까지는 docker cp 명령어를 통해 컨테이너 내부에 저장하고 있는 파일을 직접 호스트 OS로 가져와 로그 등을 확인해야했다.
로그 수집을 자동화하기 위해 도커의 마운트 기능을 이용해 컨테이너의 특정 파일을 호스트 OS의 파일 시스템에서 직접 확인할 수 있도록 수정해보자.
다음과 같이 수정할 수 있다.
docker run -dp 8800:8800 -v $(pwd)/dockerlogs:/logs -v $(pwd)/dockerlogs/access_log:/logs/access_log -v $(pwd)/dockerlogs/debug_log:/logs/debug_log -v $(pwd)/dockerlogs/user_log:/logs/user_log --name=fastapi fastapi-img
-v 옵션을 통해 컨테이너 내부의 파일을 호스트 파일 시스템에서 확인할 수 있도록 경로를 설정해준다.
access, user, debug, root 4개의 로그 폴더를 가져올 수 있도록 각각 마운트해준다.
호스트에서 로그를 직접 확인할 수 있다.
컨테이너를 멈추고 재실행시키면 어떻게 될까? -> 컨테이너가 특정 부분만 호스트의 파일 시스템을 이용하는 개념이므로 마운트된 파일은 여전히 남는다.
같은 경로에 있는 파일에 추가로 작성된다.
빌드시간에 지정한 타임스탬프로 로그 파일 이름을 정하기 때문에 컨테이너를 실행시킬때도 동일한 파일명을 사용한다.
로그의 마운트는 의도대로 잘 동작하는 것 같다.