코드 한 줄 고치지 않고 앱을 클라우드 스토리지용으로 개조하기
최근에 서버 중 하나에 미디어 공유 앱을 설치했습니다. 설치 자체는 간단했지만, 장기적인 유지보수 측면에서는 교묘한 함정이 숨어 있었습니다.
사용자가 파일을 업로드할 때마다 웹 앱은 로컬 파일 시스템에 저장했습니다. 만약 서버를 날리고 다시 구축해야 한다면 모든 파일을 수동으로 백업하고 복원해야 했습니다. 더 나은 구조는 앱이 파일을 별도의 스토리지 서버에 쓰도록 하는 것이지만, 그러자고 몇 달씩 들여 앱을 다시 작성하고 싶지는 않았습니다.
Docker와 Google Cloud Storage, 그리고 gcsfuse 유틸리티를 이용해 앱 코드 한 줄도 수정하지 않고 이러한 분리를 구현했습니다.
이 튜토리얼에서는 소스 코드 수정을 최소화하면서 레거시 앱을 Google Cloud Storage에 맞게 개조하는 방법을 보여드리겠습니다.
용어 정리
Google Cloud Platform이 익숙하지 않다면, 이 튜토리얼에서 알아두면 좋을 약어들을 정리했습니다.
| 약어 | 원래 명칭 | 설명 | 유사 서비스 |
|---|---|---|---|
| GCS | Google Cloud Storage | Google의 클라우드 스토리지 서비스 | Amazon S3 |
| GCE | Google Compute Engine | Google의 주문형 가상 머신 서비스 | Amazon EC2 |
| GCR | Google Container Registry | Google의 Docker 이미지 호스팅 서비스 | Docker Hub |
| GCP | Google Cloud Platform | Google의 클라우드 컴퓨팅 플랫폼(GCS, GCE, GCR은 모두 GCP의 구성 요소입니다) | Amazon Web Services 또는 Microsoft Azure |
Docker는 개발자가 어디서든 실행할 수 있는 독립적인 애플리케이션 환경을 구축할 수 있게 해줍니다.
- Docker 이미지는 운영체제와 모든 서드파티 의존성을 포함해 앱을 실행하는 데 필요한 모든 파일의 집합입니다.
- Docker 컨테이너는 Docker 이미지가 실행되는 라이브 환경입니다.
이 튜토리얼에서는 엄밀히 말하면 사실이 아니지만, Docker 컨테이너를 가벼운 가상 머신이라고 생각해도 무방합니다.
사전 준비물
제 예제를 따라 하려면 다음이 필요합니다.
- Google Cloud SDK
- Docker(무료 Community Edition이면 충분합니다)
예제 앱 소개
이 튜토리얼을 위해 예제 프로젝트를 만들었습니다. Flask 프레임워크의 업로드 문서를 기반으로 한 아주 단순한 웹 앱입니다.
실행하려면 다음 명령을 입력하세요.
git clone https://github.com/mtlynch/flask_upload_demo.git
cd flask_upload_demo
sudo pip install -r requirements.txt
gunicorn \
demo.app:app \
--bind 0.0.0.0:5000앱에서 파일을 선택해 업로드할 수 있습니다.

flask-upload-demo의 업로드 페이지
그런 다음 파일을 http://[server address]:5000/uploads/[filename] URL에서 영구적으로 제공합니다.

업로드된 이미지를 제공하는 flask-upload-demo
서버 내부에서 보면 앱이 파일을 로컬 파일 시스템의 demo/uploads 폴더에 저장한 것을 확인할 수 있습니다.
$ ls -l demo/uploads/
-rw-rw-r-- 1 mike mike 230720 Nov 24 21:45 Space_Duck_Desktop_RGB_PNG.png예제 앱 Docker 이미지로 만들기
이 앱은 의존성이 몇 가지뿐이라 Docker 이미지를 만들기가 쉽습니다.
Dockerfile
FROM debian:stretch
RUN apt-get update
RUN apt-get install --yes \
git-core \
python \
python-pip \
python-virtualenv
# Create demo user system account.
ARG APP_USER="demo-user"
ARG APP_GROUP="demo-user"
ARG APP_HOME_DIR="/home/demo-user"
RUN set -x && \
groupadd "$APP_GROUP" && \
useradd \
--comment "Demo app system account" \
--home-dir "$APP_HOME_DIR" \
--create-home \
--system \
--gid "$APP_GROUP" \
"$APP_USER"
# Create directory for app source code.
ARG APP_ROOT="/srv/demo-app"
RUN mkdir --parents "$APP_ROOT" && \
chown \
--no-dereference \
--recursive \
"${APP_USER}:${APP_GROUP}" "$APP_ROOT"
USER "$APP_USER"
WORKDIR "$APP_ROOT"
# Install demo app.
ARG DEMO_APP_REPO="https://github.com/mtlynch/flask_upload_demo"
RUN set -x && \
git clone "$DEMO_APP_REPO" . && \
virtualenv VIRTUAL && \
. VIRTUAL/bin/activate && \
pip install --requirement requirements.txt
EXPOSE 5000
# Run demo app.
ENV FLASK_APP "demo/app.py"
CMD virtualenv VIRTUAL && \
. VIRTUAL/bin/activate && \
gunicorn \
demo.app:app \
--bind 0.0.0.0:5000 \
--log-level info위 Dockerfile은 이미지를 준비하기 위해 다음과 같은 작업을 수행합니다.
- Git, Python 및 관련 패키지를 설치합니다
- 제한된 권한으로 앱을 실행하기 위한 시스템 계정(
demo-user)을 생성합니다 - 앱 소스 저장소를 로컬에 클론합니다
- 포트 5000에서 데모 앱을 시작하는
CMD를 추가합니다
제 저장소를 클론한 뒤 로컬에서 Docker 컨테이너를 빌드해 이 Dockerfile을 테스트할 수 있습니다.
cd ~
git clone https://github.com/mtlynch/docker-flask-upload-demo.git
cd docker-flask-upload-demo
docker build \
--tag demo-app-image \
.
docker run \
--detach \
--publish 80:5000 \
--name demo-app \
demo-app-image브라우저에서 http://localhost/에 접속하면 데모 앱이 보일 것입니다.
조금 더 현실적인 Docker 이미지
대부분의 웹 앱은 브라우저의 트래픽을 직접 받지 않습니다. 대신 Nginx나 Apache 같은 HTTP 서버가 로드 밸런싱, 정적 파일 제공 등 공통 작업을 처리하고, 백엔드가 앱 고유의 로직을 담당합니다.
실제 서비스에서 사용할 법한 Docker 이미지를 보여주기 위해 Docker 저장소에 nginx 브랜치를 추가했습니다. 주요 변경 사항은 다음과 같습니다.
# Install nginx.
ARG NGINX_GROUP="www-data"
COPY nginx.conf /etc/nginx/sites-enabled/nginx.conf
RUN set -x && \
apt-get install --yes \
nginx \
sudo && \
rm /etc/nginx/sites-enabled/default && \
usermod --append --groups "$NGINX_GROUP" "$APP_USER" && \
echo "$APP_USER ALL=(ALL:ALL) NOPASSWD: /usr/sbin/nginx" >> /etc/sudoersNginx는 일반적으로 80번과 443번 같은 특권 HTTP 포트에서 대기하기 위해 root로 실행됩니다. 따라서 Dockerfile에서는 sudo를 사용해 데모 앱 사용자가 Nginx만 root로 실행하고, 나머지 작업은 제한된 권한으로 수행하도록 했습니다.
마지막으로 Nginx 설정 파일을 컨테이너에 복사합니다.
nginx.conf
server {
listen 80;
server_name example.org; # Replace with your server's domain name
client_max_body_size 20m;
# Serve static resources directly (bypass backend).
location /uploads/ {
alias /srv/demo-app/demo/uploads/;
}
# Forward all other requests to the application backend.
location / {
proxy_pass http://127.0.0.1:5000;
}
}location /uploads { ... } 블록은 요청을 flask-upload-demo 백엔드로 전달하는 대신 Nginx가 /uploads 폴더의 파일을 직접 제공하도록 합니다. Nginx가 앱 백엔드보다 정적 파일을 더 빠르고 효율적으로 처리하기 때문에 흔히 사용하는 설정입니다.
아래 명령으로 flask-upload-demo의 Nginx 버전을 실행할 수 있습니다.
cd ~/docker-flask-upload-demo
git checkout nginx
docker build \
--tag demo-app-image \
.
docker run \
--detach \
--publish 80:80 \
--name demo-app \
demo-app-image앱은 다시 http://localhost/에서 확인할 수 있습니다. 동작은 이전 버전과 동일하지만, 파일 업로드를 제공하는 데 리소스를 더 적게 사용합니다.
GCP 프로젝트 준비하기
앱을 로컬에 배포하는 것도 괜찮지만, 모든 사용자가 접근할 수 있도록 클라우드에 게시하는 것이 훨씬 흥미롭습니다. GCP에 배포하려면 몇 가지 설정을 해야 합니다.
먼저 gcloud에서 GCP 프로젝트 이름을 지정하세요.
PROJECT_ID="ENTER-YOUR-PROJECT-ID-HERE"
gcloud config set project "$PROJECT_ID"다음으로 GCP 웹 콘솔에서 서비스 계정을 생성하고 owner 역할을 부여하세요.
주의: GCP의 버그로 보이는 문제 때문에 루트 GCP 계정(예: @gmail.com 계정)이나 gcloud로 생성한 서비스 계정을 사용하면 gcr.io로의 Docker 이미지 푸시(다음 섹션 참조)가 실패합니다.

GCP 웹 콘솔에서 새 서비스 계정 만들기

서비스 계정에 역할 할당하기
비공개 키를 key.json으로 다운로드하세요.

서비스 계정의 비공개 키 다운로드하기
마지막으로 방금 만든 서비스 계정으로 gcloud 인증을 수행하세요.
gcloud auth activate-service-account --key-file key.json인증을 마쳤다면, 튜토리얼의 나머지 과정을 위해 몇 가지 명령을 실행해 프로젝트를 준비해야 합니다.
# Enable APIs you'll need for this workflow.
gcloud services enable \
cloudresourcemanager.googleapis.com \
compute.googleapis.com \
containerregistry.googleapis.com \
iam.googleapis.com
# Enable gcloud to provide credentials for Docker image pushes.
gcloud auth configure-docker --quietGoogle Container Registry에 이미지 업로드하기
Docker 이미지를 GCE에 배포하기 전에 Docker 이미지 호스팅 서비스에 게시해야 합니다. GCR은 Google Cloud에 통합된 Docker 이미지 호스팅 서비스이므로 가장 간편한 선택지입니다.
Docker 이미지를 GCR에 업로드하려면 제 예제 저장소의 nginx 브랜치를 체크아웃하세요.
cd ~/docker-flask-upload-demo
git checkout nginx이제 Docker 이미지를 로컬에서 빌드하고 GCR로 푸시하세요.
LOCAL_IMAGE_NAME="flask-upload-demo-image"
docker build --tag "$LOCAL_IMAGE_NAME" .
GCR_HOSTNAME="gcr.io"
GCR_IMAGE_PATH="${GCR_HOSTNAME}/${PROJECT_ID}/flask-demo-app"
docker tag "$LOCAL_IMAGE_NAME" "$GCR_IMAGE_PATH"
docker push "$GCR_IMAGE_PATH"Docker 컨테이너 배포하기
컨테이너 배포는 약간의 간접 과정을 거칩니다. GCP에서는 Docker 이미지를 직접 배포할 수 없습니다. 대신 GCE로 전체 가상 머신(VM)을 띄우고, 그 VM에서 Docker를 실행한 뒤, GCE가 해당 VM 안의 컨테이너에서 Docker 이미지를 실행하는 방식입니다. 다행히 GCP 도구가 이 과정을 단순화해 줍니다.
GCE VM은 기본적으로 인바운드 HTTP 트래픽을 차단합니다. 이를 허용하려면 포트 80(평문 HTTP 트래픽의 표준 포트)에서 TCP 연결을 수락하는 방화벽 규칙을 만들어야 합니다.
가장 쉬운 방법은 http-server 태그가 붙은 모든 VM에 이 규칙을 적용하는 것입니다.
VM_TAGS="http-server"
gcloud compute \
--project="$PROJECT_ID" \
firewall-rules create default-allow-http \
--direction=INGRESS \
--action=ALLOW \
--rules=tcp:80 \
--target-tags="$VM_TAGS"그런 다음 http-server 태그를 지정해 GCE VM을 배포하세요.
VM_NAME="flask-demo-app-vm"
MACHINE_TYPE="n1-standard-1"
ZONE=us-east1-b
gcloud compute \
--project="$PROJECT_ID" \
instances create-with-container "$VM_NAME" \
--zone="$ZONE" \
--machine-type="$MACHINE_TYPE" \
--network-tier=STANDARD \
--metadata=google-logging-enabled=true \
--maintenance-policy=MIGRATE \
--scopes=https://www.googleapis.com/auth/cloud-platform \
--tags="$VM_TAGS" \
--image-family=cos-stable \
--image-project=cos-cloud \
--boot-disk-size=10GB \
--boot-disk-type=pd-standard \
--boot-disk-device-name="$VM_NAME" \
--container-image="$GCR_IMAGE_PATH" \
--container-restart-policy=on-failure주목할 만한 플래그는 다음과 같습니다.
--tags="$VM_TAGS" \--tags 플래그는 방화벽 규칙용으로 만든 태그를 사용해 VM을 시작합니다. 이 플래그 덕분에 VM이 포트 80에서 HTTP 트래픽을 수신할 수 있습니다.
--image-family=cos-stable \
--image-project=cos-cloud \이 플래그들은 GCE가 컨테이너를 Container-Optimized OS에서 실행하도록 합니다. Container-Optimized OS는 Google이 Docker 컨테이너 실행을 위해 만든 경량 Linux OS입니다. --image-family=cos-stable은 gcloud가 Container-Optimized OS의 최신 안정 버전을 사용하도록 지정합니다.
--container-image="$GCR_IMAGE_PATH" \위 플래그는 앞서 만든 GCR URL을 사용해 GCE VM 안에서 실행할 Docker 이미지를 지정합니다.
명령이 완료되면 다음과 같은 출력이 표시됩니다.
Created [https://www.googleapis.com/compute/v1/projects/flask-upload-demo-2018-11-26/zones/us-east1-b/instances/flask-demo-app-vm].
NAME ZONE MACHINE_TYPE PREEMPTIBLE INTERNAL_IP EXTERNAL_IP STATUS
flask-demo-app-vm us-east1-b n1-standard-1 10.142.0.2 35.211.106.214 RUNNING브라우저에 EXTERNAL_IP의 주소를 입력하면 로컬 버전과 동일하게 동작합니다.

GCE에서 실행 중인 flask-upload-demo

GCE에서 이미지 파일을 제공하는 flask-upload-demo
문제는 해당 VM을 종료하고 같은 Docker 이미지로 새 VM을 시작하면 업로드한 파일이 더 이상 존재하지 않는다는 점입니다.

새 GCE VM은 파일이 이전 VM에 저장되어 있었기 때문에 해당 파일을 제공할 수 없습니다
물론 이것이 이 튜토리얼을 시작하게 된 문제입니다. 컨테이너는 파일을 내부 파일 시스템에 저장합니다. 호스트 VM을 종료하면 모든 파일이 사라집니다.
이를 해결하려면 Docker 컨테이너가 모든 영구 데이터를 Google Cloud Storage(GCS) 버킷에 저장하도록 설정해야 합니다.
GCS를 고려한 아키텍처 설계하기
이 문제를 해결하는 목표 아키텍처는 다음과 같습니다.

Flask 앱을 Google Cloud Platform에 배포하기 위한 아키텍처
- 웹 브라우저는 모든 프론트엔드 요청을 조율하는 웹 서버인 Nginx와만 통신합니다.
- 웹 브라우저가 파일을 요청하면 Nginx는 GCS 버킷을 파일 시스템의 폴더처럼 마운트해 주는 gcsfuse라는 유틸리티를 통해 GCS에서 파일을 가져옵니다.
- 그 외의 모든 요청은 Nginx가 flask-upload-demo 앱으로 전달합니다.
- flask-upload-demo 역시 gcsfuse 유틸리티를 통해 GCS에 새 파일을 쓸 수 있습니다.
이 아키텍처는 글 서두에서 정의한 목표를 충족합니다. 영구적인 상태는 모두 GCS에 저장되므로 VM 안의 모든 것은 버릴 수 있습니다. 모든 앱 코드는 Docker 컨테이너 안에 있어 배포가 간단하고 원자적으로 이루어집니다.
GCS 버킷 만들기(선택 사항)
아직 GCS 버킷이 없다면 다음 gcloud 명령으로 만들 수 있습니다.
STORAGE_LOCATION="us-east1"
GCS_BUCKET="${PROJECT_ID}-storage"
gsutil mb \
-p "$PROJECT_ID" \
-l "$STORAGE_LOCATION" \
"gs://${GCS_BUCKET}"이미 버킷이 있다면 GCS_BUCKET 환경 변수를 해당 GCS 버킷 이름으로 설정하세요.
Docker 컨테이너에 GCS 접근 권한 부여하기
gcsfuse 유틸리티를 통합하려면 Docker 이미지를 수정해야 합니다. 다행히 제가 대신 해 두었습니다. 전체 Dockerfile은 제 GitHub 저장소의 gcsfuse 브랜치에서 확인할 수 있지만, 주요 변경 사항은 다음과 같습니다.
# Install gcsfuse.
ARG GCSFUSE_REPO="gcsfuse-stretch"
ARG GCS_MOUNT_ROOT="/mnt/gcsfuse"
RUN set -x && \
apt-get install --yes --no-install-recommends \
ca-certificates \
curl && \
echo "deb http://packages.cloud.google.com/apt $GCSFUSE_REPO main" \
| tee /etc/apt/sources.list.d/gcsfuse.list && \
curl https://packages.cloud.google.com/apt/doc/apt-key.gpg \
| apt-key add -
RUN apt-get update
RUN set -x && \
apt-get install --yes gcsfuse && \
echo 'user_allow_other' > /etc/fuse.conf && \
mkdir --parents "$GCS_MOUNT_ROOT" && \
chown \
--no-dereference \
"${APP_USER}:${NGINX_GROUP}" "$GCS_MOUNT_ROOT"여기서 주목할 부분은 두 가지입니다.
echo 'user_allow_other' > /etc/fuse.conf위 라인은 gcsfuse의 -o allow_other 옵션을 사용할 수 있게 합니다. 앱 시스템 계정과 Nginx 시스템 계정 모두 GCS 폴더에 접근해야 하므로 필요합니다. 설정 파일에 user_allow_other 라인이 없으면 단 하나의 계정만 GCS 폴더에 접근할 수 있습니다.
주의: 두 개 이상의 시스템 계정이 GCS 버킷에 접근해야 한다면 /etc/fuse.conf 파일에 user_allow_other 라인이 반드시 포함되어야 합니다.
mkdir --parents "$GCS_MOUNT_ROOT" && \
chown \
--no-dereference \
"${APP_USER}:${NGINX_GROUP}" "$GCS_MOUNT_ROOT"gcsfuse는 실행 사용자가 쓰기 권한을 가진 기존 디렉터리가 필요합니다. 일반 사용자는 /mnt 디렉터리에 쓰기 권한이 없으므로, Dockerfile에서는 root 사용자로 /mnt/gcsfuse 디렉터리를 만들고 chown으로 소유권을 Nginx 및 데모 앱 시스템 계정에 부여합니다.
또 다른 주요 변경 사항은 컨테이너의 런타임 동작을 정의하는 Dockerfile의 CMD 부분에 있습니다.
ENV GCS_BUCKET "REPLACE-WITH-YOUR-GCS-BUCKET-NAME"
ENV GCS_MOUNT_ROOT "$GCS_MOUNT_ROOT"
ENV APP_UPLOADS_DIR "${APP_ROOT}/demo/uploads"
CMD set -x && \
sudo nginx && \
gcsfuse \
-o nonempty \
-o allow_other \
--implicit-dirs \
"$GCS_BUCKET" "$GCS_MOUNT_ROOT" && \
if [ ! -d "$APP_UPLOADS_DIR" ]; then \
ln --symbolic "$GCS_MOUNT_ROOT" "$APP_UPLOADS_DIR"; \
fi && \
virtualenv VIRTUAL && \
. VIRTUAL/bin/activate && \
gunicorn \
demo.app:app \
--bind 127.0.0.1:5000 \
--log-level info다시 한번 흥미로운 코드 조각별로 살펴보겠습니다.
gcsfuse \
-o nonempty \
-o allow_other \
--implicit-dirs \
"$GCS_BUCKET" "$GCS_MOUNT_ROOT"위의 user_allow_other 옵션과 마찬가지로, -o allow_other는 마운트된 폴더에 여러 사용자가 접근할 수 있게 합니다. Nginx(www-data 사용자로 실행)와 데모 앱(demo-user로 실행) 모두 접근이 필요하기 때문입니다.
주의: 여러 사용자 계정이 GCS 마운트의 파일에 접근한다면 gcsfuse에 -o allow_other 플래그가 필요합니다.
--implicit-dirs 플래그가 없으면 gcsfuse는 GCS 버킷의 하위 폴더에 있는 파일에 접근할 수 없습니다.
주의: GCS 버킷에 하위 폴더가 있다면 gcsfuse에 --implicit-dirs 플래그가 필요합니다.
if [ ! -d "$APP_UPLOADS_DIR" ]; then \
ln --symbolic "$GCS_MOUNT_ROOT" "$APP_UPLOADS_DIR"; \
fi앱은 업로드된 파일을 demo/uploads 디렉터리에 씁니다. 위 블록은 demo/uploads에서 /mnt/gcsfuse로 향하는 심볼릭 링크를 만듭니다. 이렇게 하면 앱이 demo/uploads 경로에 쓰는 것처럼 보여도 실제로는 GCS 버킷에 쓰게 됩니다. if/then 블록은 컨테이너 재시작 시처럼 이 단계를 두 번 이상 수행하지 않도록 보호합니다.
GCS 접근 권한이 있는 서비스 계정 만들기
이 이미지를 GCE에 배포하기 전에 한 단계가 더 필요합니다. 기본적으로 GCE 인스턴스는 표준 GCE 서비스 계정의 컨텍스트에서 실행됩니다. 해당 계정은 GCS에 대해 읽기 전용 권한만 가지고 있어, 앱이 GCS에 새 파일을 쓰는 데 실패합니다.
이를 해결하려면 다음 두 가지 역할을 가진 커스텀 서비스 계정을 만드세요.
storage.objectAdmin: VM의 프로세스가 GCS의 객체를 읽고 쓸 수 있게 합니다.logging.logWriter: 앱의 로그 출력이 GCP 로깅 인터페이스에 표시될 수 있게 합니다.
주의: 커스텀 서비스 계정으로 시작하지 않으면 GCE 인스턴스는 GCS 버킷에 쓸 수 없습니다.
다음 명령으로 필요한 권한을 가진 서비스 계정을 만들 수 있습니다.
SERVICE_ACCOUNT_NAME=flask-demo-app-service-account
SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"
gcloud iam service-accounts create "$SERVICE_ACCOUNT_NAME"
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member "serviceAccount:${SERVICE_ACCOUNT_EMAIL}" \
--role roles/storage.objectAdmin
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member "serviceAccount:${SERVICE_ACCOUNT_EMAIL}" \
--role roles/logging.logWriterGCS를 인식하는 컨테이너 배포하기
docker-flask-upload-demo 저장소의 클론으로 돌아가 gcsfuse 브랜치를 체크아웃하세요.
cd ~/docker-flask-upload-demo
git checkout gcsfuse이제 Docker 이미지를 다시 빌드해 GCR로 푸시하세요.
LOCAL_IMAGE_NAME="flask-upload-demo-image"
docker build --tag "$LOCAL_IMAGE_NAME" .
GCR_HOSTNAME="gcr.io"
GCR_IMAGE_PATH="${GCR_HOSTNAME}/${PROJECT_ID}/flask-demo-app"
docker tag "$LOCAL_IMAGE_NAME" "$GCR_IMAGE_PATH"
docker push "$GCR_IMAGE_PATH"참고: 이 이미지를 로컬에서 실행하면 실패합니다. gcsfuse 버전의 Dockerfile은 실행 환경이 이미 gcloud에 인증되어 있다고 가정합니다(GCE에서 실행되는 컨테이너의 경우 해당합니다). GCE 외부에서 실행하면서도 GCS 버킷을 마운트하도록 Dockerfile을 조정하는 것도 가능하지만, 이는 독자의 몫으로 남겨두겠습니다.
새로운 커스텀 GCE 서비스 계정이 준비되었으니, 이제 GCS를 인식하는 컨테이너를 배포할 차례입니다.
VM_NAME="flask-demo-app-vm-gcsfuse"
MACHINE_TYPE="n1-standard-1"
ZONE=us-east1-b
gcloud compute \
--project="$PROJECT_ID" \
instances create-with-container "$VM_NAME" \
--zone="$ZONE" \
--machine-type="$MACHINE_TYPE" \
--network-tier=STANDARD \
--metadata=google-logging-enabled=true \
--maintenance-policy=MIGRATE \
--scopes=https://www.googleapis.com/auth/cloud-platform \
--service-account="$SERVICE_ACCOUNT_EMAIL" \
--tags="$VM_TAGS" \
--image-family=cos-stable \
--image-project=cos-cloud \
--boot-disk-size=10GB \
--boot-disk-type=pd-standard \
--boot-disk-device-name="$VM_NAME" \
--container-image="$GCR_IMAGE_PATH" \
--container-restart-policy=on-failure \
--container-privileged \
--container-env="GCS_BUCKET=$GCS_BUCKET"이 명령은 두 가지 플래그가 추가된 것을 제외하면 이전 배포 명령과 동일합니다.
--container-privileged \--container-privileged 플래그는 Docker 컨테이너가 FUSE 파일 시스템을 마운트할 수 있도록 하는 데 필요합니다(Docker는 더 세분화된 방법도 제공하지만, GCE에서는 아직 지원하지 않습니다).
주의: --container-privileged 플래그를 지정해 VM을 배포하지 않으면 gcsfuse가 GCE에서 GCS 버킷을 마운트할 수 없습니다.
--container-env="GCS_BUCKET=$GCS_BUCKET"Docker 이미지는 런타임 전까지 GCS 버킷 이름에 구애받지 않도록 의도적으로 설계했습니다. --container-env 플래그를 사용하면 배포 시점에 GCS_BUCKET 환경 변수를 지정할 수 있습니다.
영속성의 보상은… 영속성
이제 상태를 GCS에 영구 저장하는 Docker 컨테이너가 완성되었습니다. 배포된 앱에 파일을 업로드해 테스트해 보세요.

GCS의 영구 저장소에서 파일을 제공하는 flask-upload-demo
GCS 버킷을 확인하면 방금 업로드한 파일이 보일 것입니다.

GCS 버킷 안의 이미지 파일
진짜 시험은 이 상태가 서로 다른 VM 사이에서도 유지되는지 여부입니다. VM을 완전히 종료한 뒤 재배포해 확인할 수 있습니다. 이전 VM의 이미지 URL이 새 서버에서도 그대로 접근됩니다.

VM을 삭제하고 다시 만들었는데도 계속 파일을 제공하는 flask-upload-demo
보너스: 로깅 인터페이스
이 솔루션의 또 다른 장점은 GCP가 앱 로그를 확인할 수 있는 깔끔한 웹 인터페이스를 제공한다는 점입니다.
VM에 SSH로 접속한 뒤 docker logs를 실행해 언제든 로그를 직접 확인할 수 있습니다.
$ docker logs klt-flask-demo-app-vm-gcsfuse-qnnf
...
[2018-11-26 21:28:54 +0000] [33] [INFO] Starting gunicorn 19.9.0
[2018-11-26 21:28:54 +0000] [33] [INFO] Listening at: http://127.0.0.1:5000 (33)
[2018-11-26 21:28:54 +0000] [33] [INFO] Using worker: sync
[2018-11-26 21:28:54 +0000] [37] [INFO] Booting worker with pid: 37
[2018-11-26 21:32:59 +0000] [37] [INFO] Saving uploaded file "zestful-logo.png" to "/srv/demo-app/demo/uploads/zestful-logo.png"더 쉬운 방법은 GCP의 Stackdriver Logging 인터페이스를 여는 것입니다. 거기서 기능이 풍부한 웹 인터페이스로 모든 로그를 확인할 수 있습니다.

GCP Stackdriver 인터페이스에 표시된 앱의 로그 출력.
새 릴리스 배포하기
이 아키텍처 덕분에 새 릴리스를 쉽게 푸시할 수 있습니다. 앱이나 의존성을 업데이트하고 싶을 때마다 새 이미지를 빌드해 GCR로 푸시하면 됩니다.
docker build --tag "$LOCAL_IMAGE_NAME" .
docker tag "$LOCAL_IMAGE_NAME" "$GCR_IMAGE_PATH"
docker push "$GCR_IMAGE_PATH"그런 다음 update-container 명령으로 실행 중인 GCE 인스턴스의 Docker 이미지를 업데이트하세요.
gcloud compute \
--project="$PROJECT_ID" \
instances update-container "$VM_NAME" \
--container-image="$GCR_IMAGE_PATH"주의: 고정 IP를 할당하지 않았다면 이 명령이 완료된 후 VM의 외부 IP 주소가 변경됩니다.
잘못된 릴리스를 푸시했더라도 update-container 명령으로 이전의 정상 이미지로 롤백할 수 있습니다.
한계
이 솔루션은 gcsfuse 유틸리티 및 GCS 자체와 동일한 한계를 안고 있습니다. gcsfuse는 GCS 버킷을 일반 파일 시스템 폴더처럼 보이게 하려고 최선을 다하지만, 추상화는 크게 두 가지 측면에서 무너집니다.
- GCS는 잠금을 지원하지 않으므로 앱이 파일 잠금을 시도하면 문제가 생길 수 있습니다.
- 지연 시간이 길며, 특히 대용량 파일에 대해 작고 무작위한 읽기/쓰기를 수행할 때 더욱 심합니다.
특히 sqlite는 gcsfuse 마운트에 위치한 데이터베이스를 가리키도록 하면 금방 실패한다는 것을 알게 되었습니다.
결론
이 튜토리얼에서는 앱 자체를 수정하지 않고 앱의 데이터를 클라우드 스토리지로 리다이렉트하는 방법을 배웠습니다. 예제 앱은 Google Cloud Platform을 전혀 인식하지 못했지만, Google Compute Engine에 배포하고 영구 데이터를 Google Cloud Storage로 리다이렉트했습니다.
소스 코드
- flask-upload-demo: 로컬 파일 시스템에 상태를 유지하는 Flask 예제 앱.
- docker-flask-upload-demo: flask-upload-demo용 Docker 설정, 세 가지 버전으로 제공됩니다.
- master 브랜치 - 앱의 기본 패키징을 보여줍니다
- nginx 브랜치 - nginx가 앱의 트래픽을 프록시하는 좀 더 현실적인 실제 아키텍처를 보여줍니다
- gcsfuse 브랜치 - Docker 컨테이너 내부에서 Google Cloud Storage 버킷을 마운트하는 방법을 보여줍니다(컨테이너가 Google Cloud Storage에 대한 읽기/쓰기 권한을 가진 Google Compute Engine VM에서 실행된다고 가정합니다).
- mediagoblin-docker(gcsfuse 브랜치) - 동일한 기법을 사용해 앱의 영구 데이터를 Google Cloud Storage로 리다이렉트한 실제 미디어 공유 앱용 Docker 설정입니다.
소프트웨어 아키텍처 다이어그램: Loraine Yow
글을 무작위로 읽기

