왜 시작인 docker container 라고 생각하는 사람이 있을 것 같다. 결론부터 말하자면 파이썬 외부 라이브러리를 이용하기 위해서 이다. requests, pillow 같은 라이브러리는 python 설치시 같이 포함되는 기본 라이브러리가 아니다. 이러한 라이브러를 이용하기 위한 가장 정석적인 방법이 docker를 이용하는 방법이다.  AWS Lambda 가 사용하는 docker image 를 개발 PC에서 실행시켜 해당 환경을 docker container에서 동작시켜 파이썬 라이브러리를 특정 경로에 설치하고 이를 zip 로 만들어서 aws lambda 에 업로드 하는 방법이다. 
 (이것외에 남이 만든 zip 파일을 이용하는 방법도 있는 것 같다. 그런 것을 원한다면 https://github.com/keithrozario/Klayers?tab=readme-ov-file  에서 찾아 보길 바란다. 다만 내가 원하는 라이브러리가 존재하지 않을 수도 있다. ) 

딱히 docker  명령어에 대해 자세히 설명하지 않겠다. 이번에 처음으로 docker를 실무에 이용해봤지 나는 docker 사용에 익숙하지 못하다. 따라서 내가 docker에 대해 설명하는 것은 틀릴 수 있다. 그리고 docker 는 설치되어 있다고 가정하고 진행하겠다. 


 AWS Lambda가 사용하는  docker 이미지에 대한 정보는 https://gallery.ecr.aws/lambda/python  에서 찾을 수 있다. 이렇게 만들어진 docker 이미지는 AWS 에서만든 linux 배포판을  기반으로 만들어졌다. 여기에 대한 정보는 https://docs.aws.amazon.com/ko_kr/lambda/latest/dg/python-image.html  에서 확인 할 수 있다. 내가 사용하려는 python3.11 은 Amazon Linux 2 을 기반으로 한다. python3.12 부터는 Amazon Linux 2023 을 기반으로 한다. 
Amazon Linux 2 은 redhat 을 기반으로 한 것 같다. (yum 을 이용해서 패키지를 설치한다. yum 은 ubuntu 에서의 apt 같은 것이다.) 

내 경우는 python3.11 이기 때문에  public.ecr.aws/lambda/python:3.11  을 다운받았다. 아래 과정은 docker 이미지를 설치컨테이너를 실행하는 명령어 이다. 

AWS Lambda 에 맞는  docker container 다운로드 및 설정

# 다운로드
docker pull public.ecr.aws/lambda/python:3.11

# 다운로드 확인
docker images

# 컨테이너 생성
# 시작 command 를 /bin/bash 로 변경하면서 생성한다. 
docker create -i  --name aws_py_311  public.ecr.aws/lambda/python:3.11 /bin/bash

# 컨테이너 실행
docker start aws_py_311

# 컨테이너 접속
docker exec -it aws_py_311 /bin/bash

# 컨테이너 빠져나오기
exit

위에서 시작 command 를 변경하는 것이 있는데, 이것이 좀 중요하다. 기본적으로 생성되는 docker container 가 파이썬 특정 경로의 lambda_handler 를 실행하는 것으로 설정되어 있다. 이것을 변경하지 않고 docker conainer 를 실행하면 그냥 에러를 내면서 종료되어 버린다. 


docker container 에 필요 파이썬 라이브러리 설치

이제 aws lambda 를 개발할 때 필요한 파이썬 라이브러리를 설치해보겠다. 그런데 이 파이썬 라이브러리를 특정 경로에 설치해야 한다. 그리고 이 파일들을 zip 파일로 만들어야 한다. 

cd ~
mkdir python
cd python

# /root/python 디렉토리에 파이썬 라이브러리 설치
pip3.11 install requests -t .
pip3.11 install boto3 -t .

# pillow-simd 가 아닌 pillow 를 설치하려면
pip3.11 install pillow -t .


# zip 프로그램 설치
yum install zip

# python.zip 으로 압축
cd ..
zip -r python.zip python/


컨테이너 안의 파일을 다운받기 위해서는 container shell 이 아닌 내 개발 PC의 shell 에서 

docker cp aws_py_311:/root/python.zip .

하면 된다. 

pillow-simd 를 설치하는 경우

pillow-simd 를 이용하는 경우 좀 복잡하다. 이 경우 컴파일이 필요하다. 그리고 추가적인 리눅스 패키지를 다운받아야 한다. 그리고 설치한 리눅스 패키지 때문에 shared library(so 파일, 윈도우로 치면 dll 같은 것)을 같이 zip 에 포함해야 한다. 
내 개인적인 경험으로 pillow 보다 pillow-simd 를 사용하면 40%는 빨라지는 것 같다.  pillow-simd 는 인텔 CPU의 avx2 명령어를 사용한다. 따라서 x86 전용이다. https://github.com/uploadcare/pillow-simd/issues/43  이 이슈가 open 상태인 것으로 보아 arm 은 지원하지 않는것 같다. 

내가 오늘 기준으로 컴파일 해봤을 때 pillow-simd==v8.4.0.post0 버전만 사용이 가능했다. 어째든 다음 과정을 거치면 된다. 

# pillow-simd 컴파일 준비
yum install -y \
    freetype-devel \
    gcc \
    ghostscript \
    lcms2-devel \
    libffi-devel \
    libimagequant-devel \
    libjpeg-devel \
    libraqm-devel \
    libtiff-devel \
    libwebp-devel \
    make \
    openjpeg2-devel \
    rh-python311 \
    rh-python311-python-virtualenv \
    sudo \
    tcl-devel \
    tk-devel \
    tkinter \
    which \
    xorg-x11-server-Xvfb \
    zlib-devel \
    && yum clean all
    
# 디렉토리에 파이썬 라이브러리 설치
cd ~
mkdir python
cd python

# pillow-simd 컴파일, 최신 버전은 에러가 발생했음
CC="cc -mavx2"  pip install -U --force-reinstall   -t . --compile pillow-simd==v8.4.0.post0 

# shared library 한 곳에 모우기
# /root/python/lib에 필요한 파일을 모운다.
mkdir lib
cp /usr/lib64/libtiff.so.5 lib/libtiff.so.5
cp /usr/lib64/libjpeg.so.62 lib/libjpeg.so.62
cp /usr/lib64/libjbig.so.2.0 lib/libjbig.so.2.0
cp /usr/lib64/libopenjp2.so.7 lib/libopenjp2.so.7
cp /usr/lib64/libxcb.so.1 lib/libxcb.so.1
cp /usr/lib64/liblcms2.so.2 lib/liblcms2.so.2
cp /usr/lib64/libpng15.so.15 lib/libpng15.so.15
cp /usr/lib64/libwebp.so.4 lib/libwebp.so.4
cp /usr/lib64/libwebpmux.so.0 lib/libwebpmux.so.0


# zip 프로그램 설치
yum install zip

# python.zip 으로 압축
cd ..
zip -r python.zip python/

python 버전이 다르거나 다른 pillow-simd 버전을 사용하는 경우 다른 so 파일로 변경될 수 있다. 이 경우 
AWS Lambda로 thumbnail 만들기(3) - 트러블 슈팅  
를 참고해서 어떤 파일의 so 파일이 필요한지 확인하기 바란다. 

이 경우도 동일하게  아래처럼 python.zip 파일을 내 개발 PC로 옮길 수 있다. 

docker cp aws_py_311:/root/python.zip .

 

다음에는 AWS lambda 를 설정하는 방법을 설명하겠다. 

 며칠전 https://yiunsr.tistory.com/922 이런 사고도 있기는 했지만 aws lambda python 으로 thumnail 만드는 것이 어느 정도 끝났다. 언제나 그렇지만 내가 하려는 것은 꼭 인터넷에 그대로 있지는 않았다. 있는 것을 여러개 조합해서 해야하고 기존 방법이 변경사항도 꽤 있었다. 보통 이런것은 정리하지 않는 편인데, 이건 내가봐도 너무 일을 잘 처리한 것 같아서 남기고 싶었다.  정리해서 글을 올린다. 누군가에게는 도움이 될길 바란다. 

 참고한 글 
* AWS Lambda Image Resize 도입기(올리브영 테크블로그) https://oliveyoung.tech/2023-05-19/aws-lambda-resize/   
    - 기본 개념을 배우기는 좋은 글이다.
* 자습서: Amazon S3 트리거를 사용하여 썸네일 이미지 생성 https://docs.aws.amazon.com/ko_kr/lambda/latest/dg/with-s3-tutorial.html  
    - 이것도 기본 개념을 배우기 좋은 글이다. 
* Creating faster AWS Lambda functions with AVX2  https://aws.amazon.com/ko/blogs/compute/creating-faster-aws-lambda-functions-with-avx2/
    - 나처럼 속도를 개선하고 싶어서 pillow-simd 라이브러리를 사용하는 사람들에게 유용한 글이다. 다만 좀 오래되어서 현재 사항에 안맞는 사항이 있다. 

 이 내용은 
1. AWS Lambda로 thumbnail 만들기(1) - docker container 사용하기 (https://yiunsr.tistory.com/924)
2. AWS Lambda로 thumbnail 만들기(2) - Lambda 설정하기 (https://yiunsr.tistory.com/925)
3. AWS Lambda로 thumbnail 만들기(3) - 트러블 슈팅 (https://yiunsr.tistory.com/926)
로 나눈다. 참고로 python 코드는 다루지 않겠다. 이건 찾아보면 많은 코드를 참고 할 수 있으리라 생각된다. 파이썬 코드를 만들 때 pillow 와 pillow-simd 차이는 없다. 다만 pillow-simd 가 pillow 보다는 버전업이 느린편이다. (이것 어쩔 수 없겠지.)

우선 내 개발환경은 intel mac 이라는 것을 밝힌다. 그리고 사용한 파이썬 버전은 3.11 이다. pillow-simd 를 사용하기 위해서는 x86-64 환경이어야 한다. 

다음 글에서 계속 이어 나가도록 하겠다. 

 

 이 때까지 uwsgi 를 이용한 배포를 사용했었다. 그러다 개인 서버를 ubunt22.04 로 업그래이드 하고 이번에는 gunicorn 을 사용해보기로 했다. 개인 서버이기 때문에 여러 flask 가 배포될 수 있는 환경이다. 그래서 virtualenv 환경은 나에게 필수이다. 그리고 프로세스가 죽지 않도록 관리하기 위해 systemd 를 적용했다. 그러다 보니 일반적은 gunicorn 환경보다는 좀 복잡한 편이다.
전체 환경은
flask + gunicorn + Nginx  그리고 systemd 설정이다. 
Ubuntu 22.04
Ngin 1.22.0
gunicorn 20.1.0
Flask 2.2.2
Python  3.10.6

참고 사이트
* https://docs.gunicorn.org/en/stable/deploy.html#using-virtualenv
* https://stackoverflow.com/questions/42219633/running-two-instances-of-gunicorn/42230370#42230370

nginx 를 설치한다든지 파이썬 가상환경을 설치하는 등의 과정은 생략했다. 인터넷에 나보다 더 잘 설명해둔 자료가 많기에 어렵지 않을 것이다. 다만 gunicorn 으로 배포시 systemd target 을 등록하는 과정은 찾기 어려워서 기록을 남겨둔다. 

0.
여러개의 flask 가 동작하는 환경이기 때문에 프로젝트명을 통일하는게 좋다. 여기서는 flask_labstoo  이다. 
1. virtualenv 환경 구축
  내 경우 이런 환경은 /opt/py_envs/  아래 폴더에 구축하는 편이다.  /opt/py_envs/flask_labstoo  디렉토리에 virtualenv 환경을 구축했다. 
2. gunicorn 설치
 gunicorn 을 apt 로 설치할 수도 있고, 파이썬으로도 설치할 수 있다. virtualenv 환경을 사용해야하는 경우라면 꼭 해당 virtualenv 가 active 된 상태에서 pip instal gunicorn  으로 설치해야한다.
이 경우 gunicorn 위치가 /opt/py_envs/flask_labstoo/bin/gunicorn  에 존재하게 된다.
3. 파이썬 소스코드 받기
 내 소스코드의 위치는 /user/share/nginx/flask_labstoo   이다. 
4. gunicorn 설정 테스트
/usr/share/nginx/flask_labstoo 디렉토리에서 아래 코드를 실행시킨다. 
/opt/py_envs/flask_labstoo/bin/gunicorn --workers 2 --bind 0.0.0.0:8000  main:app
여기서 main 은 flask 가 동작하는 wsgi 파일명이다. 내 경우는 main.py 에 flask 를 동작하는 코드가 아래 처럼 들어있다.

======== main.py ========
from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "<p>Hello, World!</p>"

if __name__ == '__main__':
    PORT = os.getenv('PORT') or 9000
    if len(sys.argv) == 1 or sys.argv[1] == "runserver":
        app.run(host='0.0.0.0', port=PORT, debug=True)

================================

이렇게 해서 웹브라우저에서 8000 포트로 접속이 가능한지 테스트 해 본다. 경우에 따라서 리눅스 서버의 포트 open 이 안되어 있거나 클라우드 서버의 port 가 안 열려 있으면 접속이 안되는 경우도 있다. 
 working 디렉토리 상관없이 gunicorn 을 실행하기 위해서는 아래와 같이 사용해야 한다. systemd 설정 할 때는 아래 코드를 사용하게 될 것이다. 
/opt/py_envs/flask_labstoo/bin/gunicorn --workers 2 --bind unix:/var/run/flask_labstoo_gunicorn.sock --chdir /usr/share/nginx/flask_labstoo main:app
--chdir 를 사용해서 

5. sytemd 설정
systemd 의 여러 서비스들의 묶음을 target 이라고 한다. gunicorn 이 여러개가 실행될 때, 이 서비스를 묶기위해 guicorn target 을 설정하게 된다. 그리고 systemd 서비스를 등록하게 하기위해 서비스 템플릿을 사용하게 된다. 

sudo vim /etc/systemd/system/gunicorn.target
==== gunicorn.target ====
[Unit]
Description=Gunicorn

[Install]
WantedBy=multi-user.target
========


서비스 템플릿파일 생성한다.   %i 를 잘 사용해야 한다.

sudo vim /etc/systemd/system/gunicorn@.service
==== gunicorn@.service ====
[Unit]
Description=gunicorn daemon
After=network.target
PartOf=gunicorn.target
# Since systemd 235 reloading target can pass through
ReloadPropagatedFrom=gunicorn.target


[Service]
User=root
Group=root
# WorkingDirectory가 동작하지 않음.
# WorkingDirectory=/user/share/nginx/%i

ExecStart=/opt/py_envs/%i/bin/gunicorn --workers 2 --bind unix:/var/run/%i_gunicorn.sock  --chdir /usr/share/nginx/%i  main:app

[Install]
WantedBy=gunicorn.target

========


우선 처음에 타켓서비스를 등록하고 서비스를 등록하고, 서비스를 시작해야 한다. 

sudo systemctl start gunicorn.target
sudo systemctl enable  gunicorn@flask_labstoo
sudo systemctl start gunicorn@flask_labstoo

sudo systemctl enable  gunicorn@flask_labstoo 하는 과정에서  gunicorn@.service 파일의 %i 부분에 flask_labstoo  가 들어가서 등록되게 된다. (이 말은 추가적으로 flask 서버를 배포할 때는 소스파일, virtualenv 등 이름을 잘 정해야 한다는 것을 의미한다.

잘된다면 이것으로 끝나야 한다. 잘 동작하는지 확인은 sudo systemctl status gunicorn@flask_labstoo 으로 확인가능하다. 잘동작한다면  Active: active (running)   이 보여야 한다. 잘 안된다면 sudo  journalctl -u gunicorn@flask_labstoo  명령어도 더 자세한 에러 상태를 알 수 있다. 키워드을 찾아 인터넷에 검색해 가면서 해결해야 한다. 그리고 여기서 위에서 설정한 것을 변경할 필요가 있다면 변경 후 시스템을 재로딩한 후 서비스를 다시 동작시켜야 한다. 
sudo systemctl daemon-reload
sudo systemctl restart gunicorn@flask_labstoo

systemd 설정 명령어

#### systemd 설정 명령어 ####

# taret 등록과 부팅시 자동시작(stop 은 제거)
sudo systemctl start gunicorn.target
sudo systemctl stop gunicorn.target

# 서비스 추가 삭제
# 위의 gunicorn@.service 에서 %i 대신 flask_labstoo  가 들어가게된다. 
sudo systemctl enable  gunicorn@flask_labstoo
sudo systemctl disable gunicorn@flask_labstoo

# 서비스 시작, 종료
sudo systemctl start gunicorn@flask_labstoo
sudo systemctl stop gunicorn@flask_labstoo
sudo systemctl restart gunicorn@flask_labstoo

# 시스템 재로딩시
# 모든 변경 사항을 적용한다. 위의 파일을 수정하고 나서 적용해야 한다.
sudo systemctl daemon-reload

# 시스템 오류 확인
sudo systemctl status gunicorn.target

# 서비스 오류 확인
sudo systemctl status gunicorn@flask_labstoo
# 좀 더 상세한 오류 확인
sudo journalctl -u gunicorn@flask_labstoo

잘 동작한다면 /var/run/flask_labstoo_gunicorn.sock 이라는 파일이 생성되어야 한다. 이 파일을 통해 nginx 와 통신하게 한다. 


6. Nginx 설정 
/etc/nginx/sites-available 폴더에 적당한 파일 생성. 여기서는 labstoo 라는 이름이다. 

======== labstoo ========
server {
    listen 80;
    server_name labstoo.com;
    return 302 https://labstoo.co$request_uri;
}

server {

    listen 443 ssl http2;
    ssl_certificate /etc/letsencrypt/live/labstoo.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/labstoo.com/privkey.pem;

    server_name labstoo.com;
    client_max_body_size 100M;
    keepalive_timeout  0;
    
    # GET, POSST, PUT, DELETE, PATCH 만 가능
    if ($request_method !~ ^(GET|POST|PUT|DELETE|PATCH)$ ) {
        return 444;
   }
   

    root /var/www/flask_labstoo/;
    access_log /var/log/nginx/flask_labstoo_access.log;
    access_log /var/log/nginx/flask_labstoo_request.log   post_log;
    error_log /var/log/nginx/flask_labstoo_error.log;

    location /static/ {
        alias /usr/share/nginx/flask_labstoo/static/;
    }

    location /{
        include proxy_params;
        proxy_pass http://unix:/var/run/flask_labstoo_gunicorn.sock;
    }

}
================================

내 경우에는 letsencrypt 인증서를 설정했다. 
가장 중요한 부븐은 proxy_pass 부분으로 해당 sock 파일의 위치가 gunicorn 에서 생성한 sock 파일위치와 동일해야한다. 

이 설정 후 nginx 를 재시작시키면 된다. 


추가 필요작업
* gunicorn 에서 로그를 남기는 과정을 설정하지 않은 것 같다. flask 에러가 발생히 확인을 위해 로그를 남기는 세팅이 필요하다. 
* systemd 설정에서 그냥 root 권한으로 gunicorn을 실행하도록 했는데, 이 부분을 www-data 같은 것으로 실행하도록 설정해야 해킹이 발생할 때 대비할 수 있을 것 같다.