Docker 이미지 GCP에 배포 자동화하기

서비스를 docker image로 말아 클라우드에 배포해보고 더 나아가 코드를 푸쉬했을 때 이미지를 자동으로 다시 말아 배포하는 것을 만들어보자.

예시로 사용할 것으로는 streamlt과 FastAPI로 이루어진 간단한 서비스를 사용하겠다. (부스트캠프 마지막 프로젝트에서 일부만 가져옴)
클라우드는 GCP, 자동 배포는 github action을 사용

#프론트
"""
앞뒤생략
"""
def main():

    st.set_page_config(page_title = "PM 위법행위 탐지 시스템", 
    page_icon=":scooter:")
    st.title("PM 위법행위 감지 시스템")
    st.write("영상에서 헬멧 미착용, 승차인원 초과행위를 탐지하는 시스템 입니다.")


    uploaded_file = st.file_uploader("사진을 선택하세요", type=['png', 'jpg'])

    if uploaded_file:
        bytes_data = uploaded_file.getvalue()
        response = requests.post('http://localhost:8000/image', files={'image': bytes_data})
        st.image(response.content, caption='Result')

main()
#백엔드

@app.post('/image')
async def image_detect(image: UploadFile = File(...), width: int = 1280, height: int = 960):
    image_bytes = await image.read()
    pil_image = Image.open(io.BytesIO(image_bytes))
    image = ImageOps.exif_transpose(pil_image)
    result = ProcessImage(image, model, confidence_threshold=0.9, width=width, height=height)
    converted_img = Image.fromarray(result)
    bytes_io = io.BytesIO()
    converted_img.save(bytes_io, format="PNG")
    return Response(bytes_io.getvalue(), media_type="image/png")

프론트는 사용자가 업로드한 파일을 받아 백엔드에 사진을 request하고 백엔드는 미리 만들어진 yolor 모델로 사진 속에서 킥보드를 노헬멧이나 동승을 탐지하여 response한다.


Docker image로 배포하는 이유??


개발 환경에서 만든 프로그램이 다른 사람의 pc나 서버에서 작동이 되지 않는 경우는 정말 많다.
환경마다 설치된 라이브러리, 파이썬 버전부터 시작해서 운영체제도 다를 수 있고 운영체제가 같아도 환경 변수가 달라 실행이 되지 않을 수 있다.
설정(뿌려야?)하는 서버가 한두 개면 일일히 설정을 해주며 동작하게 만들 수는 있지만 서버가 몇십 개라면 매우 품이 많이 들 것이다.

특정 소프트웨어 환경을 정하고 개발과 production 서버에서 통일하여 동일한 컴퓨팅 환경을 사용하는 가상화가 해결책이 되었는데,
docker가 나타나기 전엔 VM(virtual machine)으로 이것을 구현해냈다.

하지만 VM은 OS 위에서 OS를 하나 더 얹어 사용하기 것이라 리소스가 매우 많이 사용된다.

이것을 경량화시켜 가상화를 구현시킨 것이 container인데 docker는 이 container를 쉽게 사용할 수 있게 해주는 오픈소스이다.

docker image : 컨테이너를 사용할 수 있게 해주는 템플릿
docker container : image를 활용해 돌리는 인스턴스

개발 서버에서 만든 것을 도커 이미지(잘 동작함이 확인된)로 말아 다른 인스턴스에서 돌리면 같은 환경에서 돌아가는 것과 마찬가지이므로 라이브러리나 운영체제에 따른 에러가 일어나지 않는다.

클라우드에서 compute engine을 생성하여 도커 이미지를 받아 실행시킬 수도 있지만 최근에는 GCP나 AWS에서 도커 이미지 위치만 지정해주면 알아서 돌려주는 서버리스 서비스도 제공한다고 한다.
이 글에서는 둘 다 해볼 것이다.


준비

도커 공식 홈페이지(https://docs.docker.com/engine/install/)에서 자신의 운영체제에 맞는 설치파일을 다운받아 설치한다.

윈도우에서 도커 엔진은 wsl 위에서 돌아가는 것 같다.

잘 설치됐는지 확인하려면 터미널 창에서 docker를 입력해보면 된다.


도커 이미지 제작

이미지를 만들기 전에 필요한 파이썬 라이브러리들이 담긴 requirements.txt를 작성해주자.
가상환경을 새로 판 다음 리눅스 커맨드 '>' 를 이용해

$ pip freeze > requirements.txt

를 해주면 편하다.

이미지를 만들기 위해선 이미지에 대한 정보가 잠긴 Dockerfile이라는 것을 작성해주어야 한다.

# 백엔드에 대한 Dockerfile
FROM python:3.8-slim-buster

RUN mkdir /backend
COPY . /backend
WORKDIR /backend

RUN apt-get -y update
RUN apt-get -y upgrade
RUN apt-get install ffmpeg libsm6 libxext6  -y

RUN pip install -r requirements.txt

CMD ["python", "server.py"]

Dockerfile 작성 명령어

Dockerfile에서 자주 사용되는 명령어들은 다음과 같다.

  • FROM : 이미 만들어진 이미지 중 어떤 것을 베이스로 만들 지 선택. 파이썬 기반의 FastAPI니까 python:3.8-slim-buster를 사용했는데, dockerhub에 다른 이미지들도 많음
    옛날에 nodejs 서버 이미지를 만들 땐 node로 시작하는 베이스 이미지를 사용했던 것 같음

  • COPY : 컨테이너는 자체적인 파일 시스템을 가지기 때문에 우리가 만든 파일들을 컨테이너 파일 시스템 안으로 복사해주어야 한다. 현재 디렉토리의 파일 전부(.)를 위에서 만들어준 backend 디렉토리로 옮겨준 모습

  • WORKDIR : working directory. 작업 경로를 변경

  • RUN : 실행할 리눅스 명령어. 서비스를 시작하기 전에 실행할 것들, 보통 apt-get update 를 한다.
    현재 백엔드는 python-opencv라는 라이브러리를 사용하는데, 얘는 ffmpeg 를 설치하지 않으면 에러가 일어나서 설치해주는 명령어를 한 줄 더 추가해주었다.

  • CMD : docker run을 할 때 실행되는 명령어. 띄어쓰기 없고 리스트 안에 넣어 쓴다.

Dockerfile을 작성했으면 터미널에

$ docker build -t "이미지 이름" 

을 입력하면 이미지가 build된다.

사실 docker-compose로 두 개의 이미지를 한꺼번에 만들었다.
docker-compose는 여러 개의 컨테이너로 이루어진 서비스를 만들 때 각각의 컨테이너 이미지 빌드, 배포를 더 편하게 해주는 것이다.
docker-compose.yml 파일을 만들어주면 됨

docker image를 build할 때 에러가 정말 많이 일어났는데 빌드하는 데에 시간도 많이 들어서 안타까운 일이 아닐 수 없다.

제대로 이미지가 만들어졌는지 docker images로 확인해볼 수 있다.

docker 이미지 배포

도커 이미지를 dockerhub같은 곳에 공개적으로 배포해도 되지만 보통 기업 내에서는 비밀스런 container registry에 저장한다.

AWS, GCP 다른 이름으로 서비스가 존재함

GCP를 사용하기로 했으니 로컬에 gcloud를 설치(https://cloud.google.com/sdk/docs/install)하고 인증을 하자

첫 사용이라면 GCP 콘솔에 GCR로 들어가 API를 활성화해주면 된다.

GCR에 이미지를 push하려면 이미지 이름을 "gcr.io/프로젝트 이름/컨테이너 이름" 으로 양식을 맞춰 주어야 한다.

docker tag로 이름을 바꿔주자.
그다음 프론트엔드, 백엔드 이미지 둘 다 push해주면 된다.

$ docker push gcr.io/프로젝트ID/이미지이름 

제대로 push가 됐다면 GCR에 이렇게 뜬다.

아까 이미지를 GCP에 배포하는 방법엔 두 가지가 있다 했는데 하나는 cloud run을 통해 서버리스로 배포하는 것이고 나머지 하나는 compute engine을 만들어 배포하는 것이다.

백엔드는 첫번째 방법으로 배포하고
streamlit은 cloud run으로 배포하면 잘 안 되는 이슈가 있어 프론트는 두번째 방법을 사용하면서 겸사겸사 git push시 이미지 빌드, 배포 자동화를 해보기로 했다.

cloud run

cloud run은 매우 간편하다.
container registry에서 원하는 이미지에서 ...을 누르고 cloud run에 배포를 선택하면 된다.

혹은 GCP 전체 메뉴에서 cloud run에 들어간 뒤 서비스 만들기를 해도 된다.

이 단계에서 에러가 몇 번 났는데 알고보니 메모리가 부족해서 그런 것이었다. 어차피 공짜니 넉넉하게 설정해주자.

정상적으로 배포가 되었다면 만들어진 url로 들어가 작동을 확인할 수 있다.

build, deploy 자동화

우선 초기 인스턴스를 만든 후 docker image를 설정해 초기 배포를 해주어야 한다.

그런 다음 github action gcp docker deploy와 같이 구글에 검색해 이미 만들어진 github action을 찾아보자.

웬만하면(?) 보통 양식이 채워져 있을 것이다.

name: Docker image build and push to gcp

on:
  push:
    branches: [ main ]
    
env:
  PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }}
  DOCKER_IMAGE_NAME: streamlit
  GCE_INSTANCE: ${{ secrets.GCE_INSTANCE }}
  GCE_INSTANCE_ZONE: ${{ secrets.GCE_INSTANCE_ZONE}}

jobs:
  build:
    name: Setup, Build, Publish, and Deploy
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      # Setup gcloud CLI
      - uses: 'google-github-actions/setup-gcloud@v0'
        with:
          service_account_key: ${{ secrets.SERVICE_ACCOUNT_KEY }}
          project_id: ${{ secrets.GCP_PROJECT_ID }}
          export_default_credentials: true
          
      - run: |-
          gcloud --quiet auth configure-docker
          
      # Build docker image
      - name: Build
        run: |-
          docker build . --tag "gcr.io/$PROJECT_ID/$DOCKER_IMAGE_NAME:$GITHUB_SHA"
      # Configure docker to use the gcloud command-line tool as a credential helper
      
      - run: |
          gcloud auth configure-docker -q
      # Push image to Google Container Registry
      - name: Push
        run: |-
          docker push "gcr.io/$PROJECT_ID/$DOCKER_IMAGE_NAME:$GITHUB_SHA"
     
      - name: Deploy
        run: |-
          gcloud compute instances update-container "$GCE_INSTANCE" \
          --zone "$GCE_INSTANCE_ZONE" \
          --container-image "gcr.io/$project_ID/$DOCKER_IMAGE_NAME:$GITHUB_SHA"

필요한 secret들을 찾아 등록해주면 된다.
SERVICE_ACCOUNT_KEY는 GCP에서 서비스 계정을 만든 후 key를 json으로 발급받아 붙여넣어주면 된다.

문제가 없으면 잘 배포가 된다.

사실 지금의 프론트엔드는 백엔드 request 주소를 localhost로 해놓아서 배포하면 동작이 안 된다.
프론트엔드의 request 하는 주소를 localhost에서 아까 배포한 백엔드의 cloud run 주소로 바꾸고 커밋, 푸쉬를 해보자.

# before
response = requests.post('http://localhost:8000/image', files={'image': bytes_data})

# after
response = requests.post('https://fastapi-{대충이름}-du.a.run.app/image, files={'image': bytes_data})

좋은 웹페이지 즐겨찾기