본문 바로가기
개발/웹

Flask Microservice 구축 - Zappa로 AWS Lambda에 Flask Docker 띄우기

by pandatta 2023. 2. 25.

안녕하세요, 저번 Flask Microservice 구축 - Zappa로 AWS Lambda에 Flask 띄우기 에 이어, 이번에는 플라스크(Flask)가 담긴 도커(Docker) 컨테이너를 AWS 람다(Lambda)에 띄워보도록 하겠습니다. 저번 포스트에서는 마이크로서비스에 대한 설명을 곁들이느라 포스트가 길어졌는데, 이번에는 간단하게 끝나지 않을까 싶습니다. 한글로 된 설명서는 딱히 없어서, 설명이 잘 되어있는 영어 포스트를 참고했습니다.

도커가 필요한 이유는?

저번 포스트에서도 말씀드렸지만, AWS 람다의 함수 서비스 하나는 최대 250MB의 용량을 담을 수 있습니다. 하지만 람다를 소스 코드가 아니라 도커 이미지로 띄우게 되면, 최대 10GB 용량의 서비스를 띄울 수 있습니다. 파이썬(Python) 웹 서비스가 종종 텐서플로우(Tensorflow)나 케라스(Keras), 파이토치(PyTorch)처럼 대규모 딥러닝 라이브러리를 동반하기 때문에, 도커 이미지를 만들어서 람다에 띄우는건 필수라고 할 수도 있겠습니다.

도커(Docker)는 저번 포스트에서 나왔던 가상환경(virtual environment)처럼, 운영체제 위 별도의 환경 위에서 프로그램들이 동작하게끔 하는 플랫폼입니다. 가상환경과는 다르게 운영체제 영역까지 건드리기 때문에, 아예 운영체제를 새로 깔 수 있어서 서버의 구조와 관계 없이 모든 소프트웨어를 균일한 환경에서 구동할 수 있습니다. 도커로 운영체제와 각종 프로그램을 합쳐 돌아가는 환경을 도커 컨테이너(container)라 하고, 이를 한 번의 실행으로 구동할 수 있게 압축시켜놓은 일종의 원터치 캡처본을 도커 이미지(image)라 합니다.

그럼 도커를 띄워봅시다

도커를 공식 홈페이지에서 설치하면, 커맨드라인에서 docker라는 명령어로 도커를 실행할 수 있습니다. Dockerfile이라는 이름의 도커파일을 만들어 그 안에 이미지 첫 실행 시 필요한 명령어들을 도커만의 문법으로 작성한 뒤, docker builddocker run 명령어를 실행하면 도커 이미지를 만들고 실행할 수 있습니다. 첫 번째 도커파일을 앞서 만들었던 flask 폴더에 작성해봅시다.

FROM python:3.8.16  # 파이썬 이미지를 가져옵니다
WORKDIR /app  # 도커 실행 시 작업환경 경로를 설정합니다
COPY . /app  # 도커 실행 시 작업환경에 도커 외부의 현재 폴더의 파일들을 복사해둡니다
RUN pip3 install Flask  # 도커에 플라스크를 새로 설치합니다
ENV FLASK_APP flask  # 플라스크 애플리케이션을 환경변수로 지정합니다
ENTRYPOINT flask run --host 0.0.0.0  # 플라스크를 모든 호스트에 대해 실행합니다

처음 작성하는 도커 문법이니 특이한 것이 많습니다. 짚고 넘어가야할 점이 몇 가지 있습니다.

  1. 도커 이미지는 여러 다른 이미지를 쌓아서 만들 수 있습니다. 이미지는 도커 공식 저장소인 도커 허브(Docker Hub)에서 내려받습니다. 이 이미지를 다른 우리가 만들 이미지에 넣어서 배포하므로, 이미지 사용자는 어디서나 도커 이미지를 실행하면 균일한 파이썬 버전을 실행할 수 있습니다.
  2. 도커 이미지 실행 시 작업환경 경로를 설정합니다. 우리가 직접 도커 이미지 안에 들어가서 실행하는 것이 아니라 정해진 순서를 따라 컨테이너가 실행되므로 작업환경 시작 위치가 필요합니다. 보통은 /app 위치에서 시작합니다.
  3. 현재 폴더의 파일들, 즉 flask 폴더 위에 있는 모든 파일들을 /app 위로 복사합니다.
  4. 가상환경과 마찬가지로 도커도 별도의 환경이므로 필요한 라이브러리를 새로 설치해야합니다.
  5. 플라스크를 실행할 때 --host라는 파라미터가 추가되었습니다. --host 0.0.0.0, 즉 모든 호스트(host)에 대한 접속을 허용한다는 파라미터가 필요한 이유는, 도커의 격리된 환경에서 별도로 포트를 연 다음, 우리의 브라우저에서 특정 포트로 접속했을 때 도커의 포트로 다시 연결을 해주는 방식으로 도커 내 플라스크에 접근하기 때문입니다. 이를 포트 포워딩(port forwarding)이라고 합니다.

이제 도커 이미지를 만들고, 도커에 담긴 플라스크 애플리케이션을 실행해 접속해보도록 합시다.

$ docker build -t flask .  # 이 폴더 위 Dockerfile로 flask라는 이름의 이미지를 만듭니다
$ docker run -p 5000:5000 flask  # 도커 이미지를 실행하고, 5000번 포트와 도커 5000번 포트를 연결합니다
$ docker ps  # 현재 실행 중인 도커 컨테이너를 찾습니다
$ docker stop {컨테이너ID}  # 컨테이너ID를 입력하면 해당 컨테이너를 종료합니다

이 때 이미지 이름을 flask가 아니라 flask:latest 처럼 별도의 버전 태그를 달 수 있습니다(기본은 latest로 설정됩니다). 브라우저에서 똑같이 127.0.0.1:5000에 접속하면, 5000번 포트를 통해 도커 컨테이너 내부의 5000번 포트로 연결돼서 플라스크 애플리케이션에 접근할 수 있습니다. 도커 컨테이너에 임의의 컨테이너 ID가 부여되므로, 따로 커맨드라인을 통해 실행 중인 컨테이너를 종료할 수 있습니다.

람다에 맞게 도커파일을 고쳐야한다

하지만 우리는 람다에 이 도커를 띄워야합니다. 람다는 파이썬 코드를 실행할 뿐인데, 도커 이미지는 그럼 어떻게 실행할 수 있을까요? AWS는 AWS ECR(Elastic Container Registry)이라는, 도커 이미지를 업로드해서 실행해주는 서비스를 별도로 마련해두고 있습니다. 플라스크를 사용하는 우리들에겐 구세주나 다름없는 자파(Zappa)는, 도커 이미지를 ECR에 업로드해주고 이를 람다와 연결해주는 방법 또한 갖추고 있습니다.

우선 람다 환경에 걸맞게 도커파일을 수정해봅시다.

FROM amazon/aws-lambda-python:3.8  # 람다에서 제공하는 이미지로 바꿔주면 더 간단합니다
ARG FUNCTION_DIR="/var/task/"  # 람다는 /var/task를 작업환경으로 사용합니다
COPY ./ ${FUNCTION_DIR}  # 똑같이 작업환경에 모든 코드를 복사해줍니다
RUN pip3 install flask  # 똑같이 플라스크를 설치합니다
RUN pip3 install zappa  # 자파도 설치해줍니다
RUN ZAPPA_HANDLER_PATH=$( \  # 자파의 핸들러를 작업환경으로 빼주어야 람다가 인식해줍니다
    python -c "from zappa import handler; print (handler.__file__)" \
    ) \
    && echo $ZAPPA_HANDLER_PATH \
    && cp $ZAPPA_HANDLER_PATH ${FUNCTION_DIR}
CMD [ "handler.lambda_handler" ]  # 플라스크 대신 이 핸들러를 실행해줍니다

조금 달라진 점이 몇 가지 있습니다.

  1. 파이썬 이미지를 람다에서 제공하는 공식 이미지로 바꾸었습니다. 다른 파이썬 이미지를 사용하면 작업환경 경로를 맞춰주는 등 복잡한 작업이 더 필요합니다.
  2. 람다는 임의의 작업환경을 사용하면 안되고, 도커 컨테이너 내의 /var/task에 있는 파일만 실행할 수 있습니다.
  3. 람다가 작업환경의 자파 핸들러(handler)에 event를 전달하면, 핸들러가 플라스크 애플리케이션에 이를 전달해줍니다.

자파야 ECR에 있는 도커를 람다로 서비스해줘

이제 자파가 담긴 도커 이미지를 AWS ECR에 배포해봅시다.

$ zappa save-python-settings-file flask  # zappa_settings.py라는 파일이 별도로 필요하므로 만들어줍니다
$ docker build -t flask .  # 똑같이 도커 이미지를 만들어줍니다
$ aws ecr create-repository --repository-name flask  # flask라는 이름의 ECR 저장소를 만듭니다
{
    "repository": {
        ...
        "repositoryName": "flask",
        "repositoryUri": "1234567890.dkr.ecr.ap-northeast-2.amazonaws.com/flask",
        ...
    }
}
$ docker tag flask 1234567890.dkr.ecr.ap-northeast-2.amazonaws.com/flask  # 도커 태그를 ECR 저장소로 바꿉니다
$ aws ecr get-login-password | docker login --username AWS --password-stdin 1234567890.dkr.ecr.ap-northeast-2.amazonaws.com/flask
$ docker push 1234567890.dkr.ecr.ap-northeast-2.amazonaws.com/flask  # 도커가 직접 태그 URL이 가리키는 ECR에 이미지를 업로드해줍니다

도커가 AWS CLI(CommandLine Interface)와 소통하여 ECR에 이미지를 업로드해주었습니다. AWS 콘솔을 통해 ECR로 들어가면 이미지가 업로드되어있는 것을 확인할 수 있습니다. 이제 자파에게 람다를 통해 ECR에 있는 이미지를 배포해달라는 요청만 하면 끝입니다.

$ zappa deploy flask -d 1234567890.dkr.ecr.ap-northeast-2.amazonaws.com/flask:latest

성공하였다면, 저번 포스트와 같이 AWS API Gateway에서 배정해준 AWS 람다 서비스의 URL이 출력됩니다. 접속해서 확인해보면 Hello, World!가 출력될 것입니다. 우리는 파이썬 코드 한 줄 바꾸지 않고, 도커 이미지에 담은 플라스크 웹 애플리케이션을 AWS 서버리스 서비스로 만들었습니다!

아직도 갈 길이 멉니다. DB로 사용되는 AWS RDS나 DynamoDB도 마이크로서비스로 연결해주어야하고, 이미지 파일 저장소로 사용되는 AWS S3도 연결해주어야 하고, 언제까지 AWS API Gateway에서 알려주는 URL로 접속할 수는 없으니 도메인도 AWS Route53으로 연결해주어야 합니다. 이제는 파이썬 코드를 바꿔서 출력 외의 기능이 있는 애플리케이션도 만들어나가야겠죠. 다음 포스트에서 이들 중 하나로 찾아뵙겠습니다.

댓글