Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

AWS Lambda에서 사용한 Chrome의 headless_shell 직접 컴파일하기

얼마 전에 Headless Chrome에 대한 글을 올렸는데 Karma 같은 테스트 도구로 로컬에서 테스트를 실행할 때는 자연히 설치된 브라우저를 이용하게 되는데 이외 Headless Chrome을 서버 등에서 이용하려면 크롬이 서버에 설치가 되어 있어야 한다. PhantomJS같은 경우도 초반에는 전역으로 설치해 놓고 사용했지만 이후부터는 미리 빌드된 PhantomJS를 사용해서 필요한 프로젝트에 내장시켜서 사용하는 것이 더 일반적이라고 생각한다.

나 같은 경우도 Headless Chrome을 AWS Lambda에 올려서 사용할 생각이었으므로 빌드된 Headless Chrome이 필요했고 이를 소스코드와 함께 배포해서 사용할 수 있게 해야 했다. 이렇게 사용할 수 있어야 PhantomJS를 활용할 때처럼 사용할 수 있으니까....

Headless Chrome 컴파일

다른 OS를 대상으로도 컴파일할 수 있겠지만 여기서는 AWS Lambda에서 사용할 것이므로 Amazon Linux(amzn-ami-hvm-2016.03.3.x86_64-gp2)를 대상으로 빌드를 하려고 한다. AWS Lambda가 훨씬 제한적인 환경이므로 여기서 사용할 수 있으면 다른 곳에서도 충분히 사용할 수 있다고 생각한다. 노파심에 얘기하자면 나는 Chromium 개발자도 아니고 이런 컴파일에 대해서는 자세히는 모른다. 그냥 문서를 보면서 얕게만 이해하면서 했을 뿐이다.(잘못된 내용이 있으면 댓글로...)

Chrome 컴파일에 관해서는 잘 알지 못하므로 관련 내용을 찾아보니 How to get headless Chrome running on AWS Lambda라는 적절한 글이 있었다. 이 글에서는 단순 Chrome 컴파일이 아니라 Lambda에서 실행할 수 있는 Headless Chrome을 컴파일하는 방법이 잘 나와 있다. 이 글에 나온 대로 AWS에 c4.4xlarge EC2 인스턴스를 띄워서 이 글에 나와 있는 대로 컴파일했다. 나는 여기서 최신 버전인 Amazon Linux AMI 2017.03.0 (HVM)를 사용했다.(Lambda는 2016.03 버전을 사용한다.)

이 글에 나온 방법을 간단히 설명하자면 먼저 필요한 의존성을 설치한다.

$ sudo printf "LANG=en_US.utf-8\nLC_ALL=en_US.utf-8" >> /etc/environment

$ sudo yum install -y git redhat-lsb python bzip2 tar pkgconfig atk-devel \
  alsa-lib-devel bison binutils brlapi-devel bluez-libs-devel bzip2-devel \
  cairo-devel cups-devel dbus-devel dbus-glib-devel expat-devel \
  fontconfig-devel freetype-devel gcc-c++ GConf2-devel glib2-devel \
  glibc.i686 gperf glib2-devel gtk2-devel gtk3-devel java-1.*.0-openjdk-devel \
  libatomic libcap-devel libffi-devel libgcc.i686 libgnome-keyring-devel \
  libjpeg-devel libstdc++.i686 libX11-devel libXScrnSaver-devel libXtst-devel \
  libxkbcommon-x11-devel ncurses-compat-libs nspr-devel nss-devel pam-devel \
  pango-devel pciutils-devel pulseaudio-libs-devel zlib.i686 httpd mod_ssl \
  php php-cli python-psutil wdiff --enablerepo=epel

Chrome을 컴파일하는 관련 스크립트를 다운로드 받고 소스를 받아서 의존성을 설치한다.

$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
$ echo "export PATH=$PATH:$HOME/depot_tools" >> ~/.bash_profile
$ source ~/.bash_profile
$ mkdir Chromium && cd Chromium
$ fetch --no-history chromium
$ cd src

depot_tools은 Chromium 개발을 돕는 도구를 모아놓은 것이다. 그래서 이 도구를 설치한 뒤에 PATH에 추가하고 fetch 명령어로 Chrome 소스를 가져온다. fetch를 하면 Chromium의 소스코드를 가져온 뒤 관련 의존성(아마도 submoule 등)을 같이 가져와서 설정해 준다.

이제 컴파일을 해야 하는데 윗 글에 따르면 src/base/files/file_util_posix.cc 파일에서 임시 디렉토리로 /dev/shm을 사용하는데 Lambda에서는 이를 사용할 수 없으므로 이 부분을 무력화하기 위해 use_dev_shm = false; 코드를 중간에 넣어서 /dev/shm을 사용하지 않도록 한다.

이제 컴파일하기 위해 빌드 플래그를 지정한다.

$ mkdir -p out/Headless
$ echo 'import("//build/args/headless.gn")' > out/Headless/args.gn
$ echo 'is_debug = false' >> out/Headless/args.gn
$ echo 'symbol_level = 0' >> out/Headless/args.gn
$ echo 'is_component_build = false' >> out/Headless/args.gn
$ echo 'remove_webcore_debug_symbols = true' >> out/Headless/args.gn
$ echo 'enable_nacl = false' >> out/Headless/args.gn
$ gn gen out/Headless

다음 명령어로(ninja도 depot_tools에 포함된 명령어다.) Chrome을 컴파일한다.

$ ninja -C out/Headless headless_shell

컴파일이 완료되면 src/out/Headless/headless_shell이 생기게 된다. 이 파일을 가져다 사용하면 Headless Chrome을 사용할 수 있다. 이 컴파일로 알게 됐는데 Chrome이 설치되어 있으면 --headless 플래그를 주어 Headless Chrome을 사용하게 되는데 위처럼 컴파일하면 headless_shell을 따로 뽑아낼 수 있다. 띄어놓고 나중에 가서 받아와서 정확한 시간은 모르겠지만 1시간 정도 걸린 것 같다.

Headless Chrome 컴파일용 Docker 컨테이너

컴파일을 하는 방법을 확인했는데 이렇게 하고 보니 더 하고 싶은 게 있었다. Chrome도 계속 새 버전이 나오므로 시간이 지나면 계속 새로운 Headless Chrome을 컴파일 해야 하므로 어떤 환경을 만들어 두고 필요할 때마다 headless_shell을 컴파일하고 싶었다. Headless mode가 Chrome 59부터 추가됐다고 하는 것처럼 보통 Chrome 버전으로 새 기능 지원 여부를 인지하게 되는데 여기서 컴파일한 Chrome의 버전을 알 수가 없었고 내가 명시적으로 컴파일할 Chrome 버전을 지정해서 컴파일하고 싶었다.

Amazon Linux로 컴파일했으니 같은 방식으로 Docker 컨테이너를 하나 만들어 두면 계속 찍어 낼 수 있을 거로 생각했다. 결론적으로 말하면 이게 삽질의 시작으로 일주일 내내 퇴근하고 이거만 붙잡고 있게 됐다. ㅠ

Amazon에서 Amazon Linux의 Docker 이미지를 제공하고 있다. AWS Lambda가 사용하는 버전은 2016.03 버전이지만 Docker 이미지는 2016.09부터만 있으므로 그냥 2016.09 이미지를 사용했다.(큰 차이는 없는 것 같다.) 그리고 컴파일을 시도해보면서 알게 됐는데 이유는 모르지만, EC2에서 선택하는 Amazone Linux와 같지는 않은 것 같다. 같아야 하지 않나 싶은데 설정이나 의존성을 더 설정해 주어야 하는 것들이 있다.

앞에서 살펴본 컴파일 방법으로 하나하나 수정해 가고 방법을 찾아가면서 Docker 이미지를 만들었다. 처음에는 바로 깨져서 괜찮았지만, 뒤로 갈수록 한번 컴파일하면 2~3시간씩 걸리므로 하루에 몇 번 테스트해보지도 못했지만 결국 headless_shell을 컴파일하는 Docker 이미지를 만들었다.

FROM amazonlinux:2016.09
MAINTAINER Outsider <outsideris@gmail.com>

# ref: https://medium.com/@marco.luethy/running-headless-chrome-on-aws-lambda-fa82ad33a9eb
RUN printf "LANG=en_US.utf-8\nLC_ALL=en_US.utf-8" >> /etc/environment

# install dependencies
RUN yum install epel-release -y
RUN yum install -y \
    git redhat-lsb python bzip2 tar pkgconfig atk-devel \
    alsa-lib-devel bison binutils brlapi-devel bluez-libs-devel \
    bzip2-devel cairo-devel cups-devel dbus-devel dbus-glib-devel \
    expat-devel fontconfig-devel freetype-devel gcc-c++ GConf2-devel \
    glib2-devel glibc.i686 gperf glib2-devel gtk2-devel gtk3-devel \
    java-1.*.0-openjdk-devel libatomic libcap-devel libffi-devel \
    libgcc.i686 libgnome-keyring-devel libjpeg-devel libstdc++.i686 \
    libX11-devel libXScrnSaver-devel libXtst-devel \
    libxkbcommon-x11-devel ncurses-compat-libs nspr-devel nss-devel \
    pam-devel pango-devel pciutils-devel pulseaudio-libs-devel \
    zlib.i686 httpd mod_ssl php php-cli python-psutil wdiff --enablerepo=epel

# ref: https://chromium.googlesource.com/chromium/src.git/+refs
ENV CHROMIUM_VERSION 61.0.3114.0

# install dept_tools
WORKDIR /chrome
RUN git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

ENV PATH="/opt/gtk/bin:${PATH}:/chrome/depot_tools"

WORKDIR /chrome/Chromium
# fetch chromium source code
# ref: https://www.chromium.org/developers/how-tos/get-the-code/working-with-release-branches
RUN git clone https://chromium.googlesource.com/chromium/src.git

WORKDIR /chrome/Chromium/src
# checkout the release tag
RUN git checkout -b build "${CHROMIUM_VERSION}"

ADD .gclient /chrome/Chromium/
WORKDIR /chrome/Chromium
# Checkout all the submodules at their branch DEPS revisions
RUN gclient sync --with_branch_heads --jobs 16
# tweak to disable use of the tmpfs mounted at /dev/shm
RUN sed -e '/if (use_dev_shm) {/i use_dev_shm = false;\n' -i src/base/files/file_util_posix.cc

WORKDIR /chrome/Chromium/src
# specify build flags
RUN mkdir -p out/Headless && \
    echo 'import("//build/args/headless.gn")' > out/Headless/args.gn && \
    echo 'is_debug = false' >> out/Headless/args.gn && \
    echo 'symbol_level = 0' >> out/Headless/args.gn && \
    echo 'is_component_build = false' >> out/Headless/args.gn && \
    echo 'remove_webcore_debug_symbols = true' >> out/Headless/args.gn && \
    echo 'enable_nacl = false' >> out/Headless/args.gn && \
    gn gen out/Headless

# build chromium headless shell
RUN ninja -C out/Headless headless_shell

위는 Dockerfile 파일이다. 여러 부분의 명령어를 합치면 좀 더 단계를 줄여서 만들 수도 있지만 각 단계가 너무 오래 걸려서 여러 단계를 합치기보다는 잘게 나누어서 테스트하다 보니 위처럼 만들어졌다. 하나씩 간단히 살펴보자.

RUN printf "LANG=en_US.utf-8\nLC_ALL=en_US.utf-8" >> /etc/environment

# install dependencies
RUN yum install epel-release -y
RUN yum install -y \
    git redhat-lsb python bzip2 tar pkgconfig atk-devel \
    alsa-lib-devel bison binutils brlapi-devel bluez-libs-devel \
    bzip2-devel cairo-devel cups-devel dbus-devel dbus-glib-devel \
    expat-devel fontconfig-devel freetype-devel gcc-c++ GConf2-devel \
    glib2-devel glibc.i686 gperf glib2-devel gtk2-devel gtk3-devel \
    java-1.*.0-openjdk-devel libatomic libcap-devel libffi-devel \
    libgcc.i686 libgnome-keyring-devel libjpeg-devel libstdc++.i686 \
    libX11-devel libXScrnSaver-devel libXtst-devel \
    libxkbcommon-x11-devel ncurses-compat-libs nspr-devel nss-devel \
    pam-devel pango-devel pciutils-devel pulseaudio-libs-devel \
    zlib.i686 httpd mod_ssl php php-cli python-psutil wdiff --enablerepo=epel

컴파일에 필요한 의존성을 설치하는 부분인데 EC2 인스턴스에서와는 달리 epel-release를 설치해야만 뒤에 --enablerepo=epel 부분에서 오류가 생기지 않는다.

ENV CHROMIUM_VERSION 61.0.3114.0

컴파일한 Chrome 보전을 지정하기 위해 환경 변수로 지정했다. 이는 현재 Chrome Canary의 버전이고 각 빌드 버전을 Chromium의 태그 목록에서 확인할 수 있다. 이후 소스를 컴파일할 때 버전을 지정하므로 컴파일할 버전을 여기서 지정하면 된다.

WORKDIR /chrome
RUN git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

ENV PATH="/opt/gtk/bin:${PATH}:/chrome/depot_tools"

앞에서와 같이 depot_tools를 설치하고 이를 PATH에 추가해서 이후에 관련 스크립트를 사용할 수 있게 했다.

WORKDIR /chrome/Chromium
RUN git clone https://chromium.googlesource.com/chromium/src.git

WORKDIR /chrome/Chromium/src
RUN git checkout -b build "${CHROMIUM_VERSION}"

앞에서는 fetch로 소스코드를 가져왔는데 여기서는 직접 Chromium 저장소를 클론했다. fetch로 가져오면 git 저장소를 통째로 가져오는 것이 아니라 최신 릴리스 버전의 스냅샷(?)만 가져와서 컴파일한다. fetch만 해도 한 30분 이상 걸리는데(인터넷이 빠른 경우) 통째로 가져오면 거의 1시간 정도 걸리는 것 같다. 그리고 위에 지정한 버전의 태그로 체크아웃했다. 직접 git 저장소를 다루는 것 외에는 원하는 릴리스 태그로 컴파일하는 방법을 알지 못해서 직접 이렇게 했다.

ADD .gclient /chrome/Chromium/
RUN gclient sync --with_branch_heads --jobs 16

gclient sync는 현재 소스에서 관련 의존성이나 필요한 파일을 가져오는 명령어다. fetch를 사용하면 gclient sync를 알아서 실행해 준다. 이는 Working with Release Branches를 참고했다. .gclientgclient 설정 파일인데 fetch에서는 자동으로 만들어주는 듯하지만 직접 하려니 필요해서 파일을 미리 만들어서 추가했다.

RUN sed -e '/if (use_dev_shm) {/i use_dev_shm = false;\n' -i src/base/files/file_util_posix.cc

앞에서는 그냥 src/base/files/file_util_posix.cc 파일을 열어서 수정했지만 여기서는 Dockerfile 내에서 해야 하므로 sedif (use_dev_shm) {부분 위에 use_dev_shm = false; 코드를 삽입했다. 이는 나중에 Chromium의 소스코드가 변경되면 달라질 수도 있는 부분이다.

RUN mkdir -p out/Headless && \
    echo 'import("//build/args/headless.gn")' > out/Headless/args.gn && \
    echo 'is_debug = false' >> out/Headless/args.gn && \
    echo 'symbol_level = 0' >> out/Headless/args.gn && \
    echo 'is_component_build = false' >> out/Headless/args.gn && \
    echo 'remove_webcore_debug_symbols = true' >> out/Headless/args.gn && \
    echo 'enable_nacl = false' >> out/Headless/args.gn && \
    gn gen out/Headless

# build chromium headless shell
RUN ninja -C out/Headless headless_shell

이제 컴파일 플래그를 지정하고 컴파일을 한다.

완성된 Docker 이미지

위 Docker 이미지는 EC2 c4.xlarge 인스턴스에 올려서 실행했을 때 3시간이 좀 더 걸린 것 같고 완성된 Docker 이미지는 21.7GB나 된다. 처음에는 내 맥북에서 작업을 계속했는데 메모리, 디스크 부족 현상을 계속 만나서 결국 EC2에 올려서 작업했다. Checking out and building Chromium on Linux를 보면 최소 8GB 램에 16GB를 권장하고 디스크는 100GB가 필요하다고 하는데 디스크는 50GB 정도에서도 크게 문제는 없었다.

최종 결과물은 GitHub에 올려 두었고 DockerHub에도 올려두었다.(21기가나 되어도 DockerHub에 잘 올라가서 신기했다.) 이 Docker 이미지를 받으면 안에서 headless_shell을 가져올 수 있고 다른 버전으로 컴파일하려면 CHROMIUM_VERSION 환경 변수를 바꿔서 Docker 이미지를 빌드하면 된다.(다시 말하지만 3시간 이상 걸린다.)

매번 빌드할 필요는 없으므로 위 GitHub 저장소에 릴리스 부분에 컴파일한 headless_shell을 올려두었다. 이를 가져다가 그냥 사용하면 된다. 참고로 Amazon Linux에서 컴파일한 버전이고 130MB다.

중간에 작업하면서 자주 할 일도 아닌데 괜히 headless_shell을 컴파일할 환경을 구성해 놓겠다는 이상한 목표를 잡아놔서 이 고생을 하고 있나 고민을 많이 했지만(원래 하려던 건 headless chrome을 AWS Lambda 함수를 하나 올리려던 거였는데...) 다 만들고 나니 기쁘긴 하다. GitHub를 검색하면 headless chrome을 컴파일해 놓고 S3 등에 올려놔서 받아와서 사용하는 프로젝트들이 꽤 있기는 한데 내가 찾아봤을 때는 실제로 어떻게 컴파일했는지 정보가 없기도 하고 구성된 프로젝트가 내가 원하는 것과는 약간은 달라서 직접 컴파일하게 됐다.

2017/06/05 04:18 2017/06/05 04:18