멋진 신세계, 컨테이너: Google App Engine에 Docker 애플리케이션 배포하기

Premist (Minku Lee)
Making Shakr
Published in
20 min readNov 20, 2017

--

마이크로소프트웨어 390호, <오픈의 꿈>에 투고한 글을 옮겼습니다.

Docker를 이용하면 어떤 종류의 애플리케이션이라도 컨테이너 형태로 제작하여 구동할 수 있지만, 많은 기업이 웹 애플리케이션을 혹은 마이크로서비스 등의 구성요소를 운영할 때 주로 Docker를 이용하여 프로덕션 환경을 구축하곤 한다. 어떤 기술에 대한 설명을 무작정 듣기보다는 실제로 사용해 보면서 얻는 지식과 경험이 크기 때문에, 실제로 있을만한 상황을 가정하여 Docker 사용 방법에 대해 설명해 보려고 한다. 간단한 웹 애플리케이션을 제작하여 Docker를 활용해 보고, Docker 이미지를 생성하여 컨테이너 레지스트리에 등록하고, Google App Engine에 애플리케이션을 배포해보자.

예제 소개

Docker를 사용하는 애플리케이션을 Google App Engine에 배포하는 과정을 설명하기 위해 Translator라는 간단한 웹 애플리케이션을 제작하였다. Translator는 빠르고 간단하게 웹 개발을 할 수 있는 Ruby on Rails 최신 버전인 5.1을 사용하고 있고, 프론트엔드 구현에는 Vue.jswebpack을 사용하였다. Translator의 소스 코드는 GitHub에 공개하여, 누구나 자유롭게 사용할 수 있다.

예제 배포에 Google App Engine을 선택한 이유는 여러 가지가 있지만, 대표적인 이유를 꼽아보자면 다음과 같다.

  • Snapchat, Nintendo, Niantic 등 많은 회사가 이미 프로덕션 환경에서 문제 없이 사용하고 있다.
  • 작년 초 출시된 Flexible Environment를 사용하면 언어에 관계 없이 모든 Docker 기반 애플리케이션을 구동할 수 있다.
  • 최소한의 설정으로 로드 밸런싱, 오토 스케일링, 애플리케이션 헬스 체크, 스플릿 테스트, 애플리케이션 성능 모니터링(APM) 등을 모두 제공한다.
  • 쉬운 배포를 위한 커맨드 라인 도구(Google Cloud SDK)를 제공한다.

준비

Git과 Docker가 설치된 컴퓨터

Git은 macOS나 Linux에는 기본적으로 설치되어 있는 경우가 많고, 설치되어 있지 않다면 패키지 매니저를 통해 쉽게 설치가 가능하다. Windows 환경에서는 Git 웹사이트에서 직접 다운로드하여 설치하자.

Docker는 Linux(Ubuntu 14.04, Debian 7.7, CentOS 7 이상), macOS(버전 10.11 이상), Windows(10 Pro, 10 Enterprise, 10 Education) 기반의 64비트 컴퓨터에 다음 링크에서 다운로드하여 설치할 수 있다. Linux에서는 네이티브로 동작하고, macOS와 Windows에서는 운영 체제의 가상 환경 프레임워크(macOS의 경우 Hypervisor.framework, Windows의 경우 Hyper-V)를 통해 동작한다.

Google Cloud Platform 계정

예제 애플리케이션을 Google App Engine으로 배포할 예정이고, 애플리케이션에서 Google API를 사용하기 때문에 Google Cloud Platform계정이 필요하다. Google API나 Google App Engine은 유료 서비스이지만, GCP 가입 시 $300 크레딧을 제공하므로 이를 사용하여 튜토리얼을 충분히 완료할 수 있다.

Translator 프로젝트 받기

앞서 간단하게 설명한 Translator 프로젝트를 컴퓨터에 준비하자. 쉘에서 Git으로 쉽게 소스 코드를 받아올 수 있다.

$ git clone https://github.com/premist/translator.git
Cloning into 'translator'...
remote: Counting objects: 279, done.
remote: Compressing objects: 100% (178/178), done.
remote: Total 279 (delta 112), reused 228 (delta 78), pack-reused 0
Receiving objects: 100% (279/279), 104.74 KiB | 599.00 KiB/s, done.
Resolving deltas: 100% (112/112), done.
$ cd translator

Dockerfile 톺아보기

받아온 소스 코드를 살펴보면 루트 디렉터리에 Dockerfile이라는 파일이 존재하는데, 이 Dockerfile은 Docker가 어떻게 이미지를 어떤 단계를 거쳐 생성해야 할 지 기술한다.

FROM ruby:2.4.1

ENV RAILS_ENV production
ENV RAILS_LOG_TO_STDOUT true
ENV RAILS_SERVE_STATIC_FILES true
ENV PORT 8080

# Install apt-https-transport and lsb-release for Node.js and Yarn
RUN apt-get update && apt-get install -y apt-transport-https && apt-get clean

# Add Node.js and Yarn source
RUN curl -s https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add -
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://deb.nodesource.com/node_8.x jessie main\ndeb-src https://deb.nodesource.com/node_8.x jessie main" | tee /etc/apt/sources.list.d/nodesource.list
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list

ADD . /opt/translator
WORKDIR /opt/translator

# Install Ruby dependencies
RUN bundle install

# Install Node.js and Yarn, install NPM packages via Yarn,
# run asset precompilation (which runs webpack), and remove Node.js and Yarn.
RUN apt-get update && apt-get install -y nodejs yarn \
&& yarn install && rake assets:precompile && apt-get remove -y yarn nodejs \
&& apt-get clean && rm -rf node_modules

EXPOSE 8080

RUN ["./bin/rails", "server"]

처음 Dockerfile 포맷을 보면 어떤 부분이 어떤 역할을 하는지 감이 오지 않을 수 있는데, 각 단계별로 천천히 살펴보자.

FROM ruby:2.4.1

모든 Dockerfile은 FROM 구문으로 시작한다. 이 이미지의 기반 이미지를 지정하는 것인데, 여기서는 Docker가 제공하는 공식 Ruby 이미지를 사용하였다. 사용하는 언어 런타임을 설치하는 과정부터 직접 수행하고 싶다면, 이 FROM 구문에 debian:stretchcentos:7 와 같은 OS 이미지를 지정하여 Dockerfile을 작성할 수 있다.

ENV RAILS_ENV production

ENV 구문을 사용하면 컨테이너 내에 환경 변수를 할당할 수 있다. 가령 레일즈 애플리케이션의 경우 RAILS_ENV 라는 환경 변수로 현재 애플리케이션의 환경을 정의해줄 수 있는데, 여기서는 프로덕션 환경에서 사용할 이미지이므로 프로덕션으로 지정해주었다. 여기서 지정된 환경 변수이더라도 이미지를 실행할 때 -e MY_ENV_VAR=OVERRIDE_VALUE 와 같이 스위치를 지정해 주는 것으로 오버라이드가 가능하다.

RUN apt-get update && apt-get install -y apt-transport-https && apt-get clean

RUN 구문을 사용하여 이미지 빌드 단계에서 명령어를 실행할 수 있다. 위의 명령어는 APT 패키지 관리자를 이용하여 apt-transport-https 패키지를 설치하는 명령어이다. apt-get updateapt-get install, apt-get clean 명령을 단일 RUN 구문으로 실행하여 가독성이 낮게 느껴질 수도 있는데, 그럼에도 불구하고 이렇게 RUN 구문을 작성한 이유는 Docker가 이미지를 빌드하는 특성을 감안해서이다. 각 명령어가 독립적인 레이어 형태로 저장이 되므로, 위 명령어를 3개로 나누어 실행하게 되면 apt-get clean 에 의해 삭제될 파일도 모두 저장되기 때문에 단일 명령어로 묶어주면 레이어가 보다 간결해지고 사이즈도 작게 유지할 수 있다. 파일 아래 부분에서도 이와 같은 이유로 Node.js와 Yarn을 설치하고, 패키지를 받아와 webpack 빌드를 실행하는 구문을 단일 명령어를 묶어서 RUN 구문으로 작성하였다.

ADD . /opt/translator
WORKDIR /opt/translator

ADD 구문을 사용하면 현재 작업 디렉터리 내의 파일이나 폴더를 지정하여 이미지 내에 추가할 수 있고, WORKDIR 구문으로 Dockerfile의 각 명령어가 실행되는 작업 디렉터리를 정의할 수 있다.

EXPOSE 8080

EXPOSE 구문에 포트를 지정해주면, 컨테이너를 실행할 때 해당 포트에서 연결을 받을 수 있다. 이렇게 지정된 포트는 동일한 Docker 네트워크 내에서 다른 컨테이너가 접근할 수 있다. 다만 EXPOSE 구문 만으로는 해당 포트가 호스트에 노출되지는 않으므로, 호스트에 해당 포트를 노출시키고 싶다면 이미지를 실행할 때 -p 컨테이너-포트:호스트-포트 스위치를 지정해 주어야 한다.

RUN ["./bin/rails"]

RUN 구문으로 해당 이미지가 기본으로 실행할 명령어를 지정할 수 있다. 이렇게 지정한 명령어는 명령어 지정 없이 docker run를 실행하였을 때 기본적으로 사용되며, 필요한 경우 명령어를 지정해주는 것으로 오버라이드도 가능하다.

.dockerignore 파일

Git 버전 관리 소프트웨어를 사용해 본 독자라면 .gitignore 파일에 익숙할 것이다. .gitignore 파일에는 Git이 무시할 파일을 지정하는데, 이를 사용하면 중요한 정보를 저장하고 있는 설정 파일이나 크기가 지나치게 방대한 의존성 디렉터리가 Git 저장소에 포함되는 것을 미연에 방지할 수 있다. .dockerignore 파일도 이와 비슷한데, 이 파일에 Docker 이미지 빌드 시 무시할 파일과 폴더를 지정해 주면 이미지가 필요 이상으로 커지는 것을 방지할 수 있다.

개발 환경에서 이미지 빌드하고 실행하기

Docker 이미지 명세를 담고 있는 Dockerfile을 살펴보았으니, 이제 이 Dockerfile을 이용하여 이미지를 빌드해보자. 커맨드 라인에서 다음과 같은 명령어로 이미지를 빌드할 수 있다. 이미지를 빌드할 때는 docker build 명령어를 사용하는데, Dockerfile이 위치한 디렉터리에서 다음과 같은 명령어를 실행하면 된다.

$ docker build -t image-name .

해당 명령어를 실행하면, Dockerfile에 작성한 순서대로 각 단계를 거쳐 이미지가 완성되는 것을 확인할 수 있다.

$ docker build -t translator .
Sending build context to Docker daemon 3.723MB
Step 1/16 : FROM ruby:2.4.1
---> 3630c02d3d1b
Step 2/16 : ENV RAILS_ENV production
---> 2a12ea7c6eef
(중략)
Step 13/16 : RUN bundle install
---> Running in f2a52936b83d
Fetching gem metadata from https://rubygems.org/.........
(중략)
---> 1f57192986de
Step 16/16 : CMD ./bin/rails server
---> Running in b8b2bab7504c
---> dc51ead245a0
Removing intermediate container b8b2bab7504c
Successfully built dc51ead245a0
Successfully tagged translator:latest

이렇게 생성된 이미지를 이용해 컨테이너를 실행하기 위해서는 docker run 명령어를 사용한다.

$ docker run -p 8080:8080 -e SECRET_KEY_BASE=XXX translator:latest
=> Booting Puma
=> Rails 5.1.3 application starting in production on http://0.0.0.0:8080
=> Run `rails server -h` for more startup options
Puma starting in single mode...

앞서 설명한 -p 스위치를 이용하여 컨테이너의 80 포트를 호스트의 8080 포트에 매핑하고, -e 스위치를 이용하여 컨테이너에 환경 변수를 설정해주었다.

Google App Engine 배포를 위한 app.yaml 작성

이제 Google App Engine에 Translator 애플리케이션을 배포할 차례이다. Google App Engine에 배포를 위해서는 app.yaml을 작성해야 하는데, Translator 애플리케이션의 app.yaml을 보며 어떤 부분을 설정할 수 있는지 살펴보자.

runtime: custom
env: flex

skip_files:
- .env
- tmp/
- node_modules

resources:
cpu: 1
memory_gb: 1
disk_size_gb: 10

health_check:
enable_health_check: true
check_interval_sec: 10
timeout_sec: 5
unhealthy_threshold: 2
healthy_threshold: 2

automatic_scaling:
min_num_instances: 1
max_num_instances: 10
cool_down_period_sec: 120
cpu_utilization:
target_utilization: 0.6

env_variables:
SECRET_KEY_BASE: 'XXX'
GOOGLE_CLOUD_KEYFILE_JSON: '{...}'

먼저 runtime: customenv: flex 는 이 Google App Engine 애플리케이션은 Flexible Environment 에서 Custom Runtime 을 이용한다는 것을 알려준다. skip_files 의 경우에는 Google App Engine이 이미지를 빌드하기 위해 파일을 업로드할 때 업로드 하지 않아도 되는 파일을 정의할 수 있는데, Docker 이미지에 포함이 될 필요가 없거나 포함되어서는 안 되는 파일을 추가해 줄 수 있다.

다음은 resources 블록인데, 여기서 이 애플리케이션의 리소스 사용량을 정의할 수 있다. CPU 코어 갯수의 경우 1 혹은 2부터 32까지의 짝수 숫자를 사용할 수 있고, 메모리의 경우에는 (CPU 코어 수 x 0.9 - 0.4) 부터 (CPU 코어 수 x 6.5 - 0.4) 기가바이트까지 지정이 가능하다. 이 외에도 베이스 디스크의 크기나 추가적으로 마운트할 디스크의 크기를 지정할 수 있다.

health_check 블록에서는 애플리케이션의 헬스 체크 조건을 설정할 수 있다. 헬스 체크를 수행할 간격과 최대 요청 시간, 그리고 애플리케이션이 사용 가능한 상태인지를 판단할 헬스 체크의 최소 갯수를 정의할 수 있다. 헬스 체크의 경우 Google App Engine에서 애플리케이션의 /_ah/health 엔드포인트로 HTTP GET 요청을 보내게 되므로, 이 엔드포인트에서 애플리케이션이 사용 가능한 상태인지를 확인하여 응답을 보내도록 코드를 작성하면 된다.

Google App Engine은 기본적으로 CPU 사용량에 따라 자동으로 인스턴스의 갯수를 늘이고 줄이는데, 이에 대한 자세한 설정을 automatic_scaling 블록에서 할 수 있다. 위 설정에서는 CPU의 사용량이 60%(0.6)를 초과하였을 때 인스턴스를 최대 10개까지 생성하도록 설정하였다. 필요한 경우 automatic_scaling 대신 manual_scaling 블록을 사용하여 인스턴스 갯수를 정의할 수도 있다.

애플리케이션 구동에 필요한 환경 변수(Environment Variable)이 있다면 env_variables 블록에 정의해줄 수 있다. 다만 환경 변수의 특성상 민감한 정보를 가지고 있을 수 있는데, 이러한 경우에는 app.yaml.gitignore 파일과 같은 버전 관리 소프트웨어의 예외 목록에 추가하자.

Google App Engine에 배포하기

먼저 Google Cloud Platform 콘솔에 접속하여, 새로운 프로젝트를 제작한다. App Engine 애플리케이션의 경우 한 프로젝트당 하나만 생성하고 배포할 수 있기 때문에, 애플리케이션 별로 프로젝트를 생성해 주어야 한다. 이 과정에서 프로젝트의 고유 ID가 할당되는데, 이 ID를 기억해 두자.

GCP 프로젝트 생성

다음으로 Google Cloud SDK를 로컬 컴퓨터에 설치한다. 모든 배포 작업을 커맨드라인으로 진행하기 때문에, Google Cloud SDK의 gcloud 명령어를 많이 사용하게 된다. macOS를 사용하고 있고 컴퓨터에 Homebrew가 설치되어 있다면, brew cask install google-cloud-sdk 로도 설치가 가능하다. Google Cloud SDK를 설치한 이후에는 gcloud auth login 명령어로 인증 정보를 저장할 수 있다.

SDK 설치까지 끝마쳤으면 App Engine 애플리케이션을 생성해 줄 차례이다. 이 단계에서 App Engine 애플리케이션이 어느 리전에 배포될 지를 결정하는데, 한번 결정하면 이 프로젝트에서는 변경이 불가능하니 신중하게 결정해야 한다. 대한민국과 가장 가까운 리전은 일본 도쿄에 위치한 asia-northeast1 이므로, 대부분의 사용자는 이 리전을 사용하면 빠른 반응 속도를 기대할 수 있다. 쉘에서 다음 명령어를 실행하자.

$ gcloud app create
You are creating an app for project [project-id].
WARNING: Creating an App Engine application for a project is irreversible and the region
cannot be changed. More information about regions is at
https://cloud.google.com/appengine/docs/locations.

Please choose the region where you want your App Engine application
located:

[1] us-central (supports standard and flexible)
(중략)
[6] asia-northeast1 (supports standard and flexible)
(중략)
[9] cancel
Please enter your numeric choice: 6

Creating App Engine application in project [project-id] and region [asia-northeast1]....done.
Success! The app is now created. Please use `gcloud app deploy` to deploy your first app.

App Engine 애플리케이션을 생성했다면 이제 Translator 애플리케이션을 배포할 차례이다! 앞서 받아온 Translator 폴더로 이동하여, gcloud app deploy 명령어로 배포를 시작하자. gcloud app deploy를 실행하면 애플리케이션의 소스 코드를 업로드하여 클라우드에서 Docker 이미지 빌드를 수행하고, 빌드된 버전을 레지스트리에 저장하고 실행한 후, 앞서 app.yaml에서 지정해 준 헬스 체크가 통과할 때 활성 버전으로 승격시킨다. 애플리케이션을 처음 배포할 때는 비교적 긴 시간이 걸릴 수 있으니, 여유를 가지고 기다리자.

$ gcloud app deploy
Services to deploy:

descriptor: [/Users/premist/dev/translator/app.yaml]
source: [/Users/premist/dev/translator]
target project: [sample-project-id]
target service: [default]
target version: [20170822t013314]
target url: [https://sample-project-id.appspot.com]
Do you want to continue (Y/n)? y
If this is your first deployment, this may take a while...done.
Beginning deployment of service [default]...
(중략)
Updating service [default]...done.
Deployed service [default] to [https://sample-project-id.appspot.com]

You can stream logs from the command line by running:
$ gcloud app logs tail -s default

To view your application in the web browser run:
$ gcloud app browse

모든 과정이 완료되면 Google Cloud Platform 콘솔에서 애플리케이션이 정상적으로 배포된 것을 확인할 수 있다.

Translator 애플리케이션을 무사히 배포했다

https://프로젝트-ID.appspot.com 에 방문하면, Translator 애플리케이션을 문제 없이 사용할 수 있다.

--

--