Canvas 1 Layer 1

AWS 람다 커스텀 런타임 만들기(feat. 루비 2.6.0)

들어가며

AWS 람다AWS Lambda에서 공식 지원하지 않는 언어나 버전을 사용하고 싶은 경우 커스텀 런타임Custom Runtime을 사용할 수 있습니다. 커스텀 런타임은 사용하고자 하는 환경을 직접 빌드해 람다에서 실행하는 기능입니다.

이번 리인벤트 2018re:Invent2018에서는 프로그래밍 언어 루비Ruby 공식 지원을 발표했지만, 현재 지원하고 있는 버전은 2.5입니다. 2018년 크리스마스에 새로 릴리스된 루비 2.6 버전은 아직 공식 지원하고 있지 않습니다. 이 글에서는 루비 2.6.0 버전의 람다 커스텀 런타임을 직접 만들어보면서 동작 방식을 알아보겠습니다.

커스텀 런타임의 구조

커스텀 런타임은 람다가 호출되면 /opt/var/task 경로 중 하나에서 실행 가능한 bootstrap 파일을 찾아서 실행합니다. bootstrap은 커스텀 런타임이 해야 할 일을 수행하고 프로그램을 종료합니다.

커스텀 런타임이 해야 할 일은 다음과 같습니다.

AWS 공식 튜토리얼에 구현되어 있는 bootstrap 예제 파일은 다음과 같습니다. 이 bootstrap 은 셸스크립트를 실행하는 커스텀 런타임이며, 람다를 실행하기 위해 커스턴 런타임이 해야할 일들을 수행하고 있습니다.

#!/bin/sh

set -euo pipefail

# 초기화 - 함수 핸들러 불러오기
source $LAMBDA_TASK_ROOT/"$(echo $_HANDLER | cut -d. -f1).sh"

# 람다 실행
while true
do
  HEADERS="$(mktemp)"
  # 람다 이벤트 정보 가져오기
  EVENT_DATA=$(curl -sS -LD "$HEADERS" -X GET "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next")
  REQUEST_ID=$(grep -Fi Lambda-Runtime-Aws-Request-Id "$HEADERS" | tr -d '[:space:]' | cut -d: -f2)

  # 함수를 실행하고 결과 저장
  RESPONSE=$($(echo "$_HANDLER" | cut -d. -f2) "$EVENT_DATA")

  # 실행 결과를 전달
  curl -X POST "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/$REQUEST_ID/response"  -d "$RESPONSE"
done

람다 실행 중 설정되는 환경변수에 대한 자세한 정보는 다음 문서에서 확인 할 수 있습니다.

위 예제의 경우 단순히 셸스크립트를 실행하는 런타임이라서 간단하게 구현되어 있지만, 다른 런타임의 경우 각 언어별로 이러한 기능을 직접 구현해야 합니다. 언어별로 직접 구현하지 않고 위의 셸스크립트 예제를 이용해 나만의 런타임을 실행하는 것도 가능합니다. 하지만 에러 추적과 같이 언어 별로 특화된 기능을 사용하려면 해당 언어로 직접 구현하는 것이 좋습니다.

루비 2.6.0으로 컴스텀 런타임 구현

커스텀 런타임의 동작 방식을 간단하게 살펴봤으니 이제 나만의 런타임을 만들어 보겠습니다. 여기서는 루비 2.6.0을 실행해보겠습니다. 루비 2.6.0은 불과 며칠 전에 발표 되었기 때문에 아직 람다에서 공식 지원하지 않고 언제 지원할지 알 수 없는 상황입니다.

공식 bootstrap 코드 추출하기

커스텀 런타임을 만들때 bootstrap을 만드는 일은 귀찮은 작업입니다. 이벤트와 컨텍스트 데이터를 가져와서 함수를 실행하고 결과나 에러를 처리하는 코드를 만들어야 하는데 막상 만들려고 하면 귀찮습니다.

AWS 리인벤트 2018에서 커스텀 런타임을 발표하면서 루비 공식 지원이 커스텀 런타임을 이용한 사례라고 했던 이야기가 생각나서 그러면 루비 런타임에 bootstrap 관련 코드가 들어있겠다는 생각이 들었습니다. 여러 삽질의 결과 공식 루비 람다 런타임의 bootstrap 코드의 경로를 알 수 있게 되었고 이 파일을 다운로드 했습니다.

람다에서 루비 2.5 런타임을 선택하고 아래 코드를 실행하면 파일을 다운로드 받을수 있는 링크가 출력됩니다.

파일을 다운로드 받으면 bootstrap 파일과 함께 루비로 작성된 람다 실행 코드를 얻을수 있습니다. 다음은 bootstrap 파일 내용중 일부 입니다.

#!/usr/bin/env bash
... 생략
/var/runtime/lib/runtime.rb

환경변수 설정을 하고 루비 파일을 실행하는 역할을 하는 스크립트 파일임을 알 수 있습니다. bootstrap 파일을 그대로 사용할 수 없고 파일 경로 등을 상황에 맞게 변경해야 합니다. 변경에 대한 자세한 내용은 뒤에서 다루겠습니다.

람다 실행 환경에서 루비 2.6.0 빌드 하기

람다에서 루비 2.6.0을 사용하려면 먼저 루비 인터프리터를 준비해야합니다. 이 때 람다가 실행되는 환경과 같은 환경에서 루비 2.6.0을 빌드하고, 그 결과물을 람다 커스텀 런타임에 복사해야만 새로운 버전의 루비가 정상적으로 동작합니다. 람다가 실행되는 환경은 아마존 리눅스Amazon Linux이며 정확한 AMI는 amzn-ami-hvm-2017.03.1.20170812-x86_64-gp2입니다. 이 정보는 추후 변경될 수 있으므로 공식문서에서 최신 환경을 확인 합니다.

아마존 리눅스 환경을 도커Docker에서 이용할 수 있도록 공개한 이미지를 이용하면 EC2 없이도 루비 2.6.0을 빌드 할 수 있습니다. lambci/lambda-base:build 도커 이미지를 이용해 컨테이너를 실행하고 루비 2.6.0을 설치합니다.

$ docker run -it --rm -v $(pwd):/var/task lambci/lambda-base:build /bin/bash
bash# yum install -y git bzip2 openssl-devel libyaml-devel libffi-devel \
        readline-devel zlib-devel gdbm-devel ncurses-devel \
        gcc gcc-c++ autoconf automake libtool bison
bash# git clone https://github.com/rbenv/ruby-build.git
bash# PREFIX=/usr/local ./ruby-build/install.sh
bash# ruby-build 2.6.0 /var/task/ruby

위 예제는 루비를 /var/task/ruby 경로에 설치 했습니다. 이 경로는 람다로 만들었을때 루비가 위치할 경로와 동일하게 맞춰야 합니다. 설치할 때 경로와 실제 경로가 다른 경우 루비를 실행하면 아래와 같은 에러가 발생합니다.

Traceback (most recent call last):
    1: from <internal:gem_prelude>:2:in `<internal:gem_prelude>'
<internal:gem_prelude>:2:in `require': cannot load such file -- rubygems.rb (LoadError)

루비 설치 결과물은 ruby 폴더에 저장됩니다.

커스텀 런타임 만들기

bootstrap 파일 및 같이 추출된 lib 폴더, 도커로 빌드한 루비 결과물 ruby를 하나의 폴더에 추가합니다.

여기에 추가로 커스텀 런타임에서 실행할 람다 함수 파일을 lambda_function.rb로 생성하고 내용을 입력합니다. 루비 2.6.0이 맞는지 확인하기 위해 루비의 현재 실행 버전을 출력하고, 2.6.0에 들어간 신규 문법인 끝없는(endless) 범위를 실행해봅니다.

require 'json'
def lambda_handler(event:, context:)
  r = {
    version: "Current lambda ruby versions is #{RUBY_VERSION}",
    endless: [0, 1, 2][0..]
  }
  { statusCode: 200, body: JSON.generate(r) }
end
.
├─ bootstrap
├─ lib
├─ ruby
└─ lambda_function.rb

bootstrap 파일에서 /var/runtime으로 된 부분을 /var/task/ruby와 같이 적절하게 변경합니다. 수정후 bootstrap 파일은 다음과 같습니다.

#!/usr/bin/env bash

if [ -z "$GEM_HOME" ]; then
  export GEM_HOME=/var/task/vendor/bundle/ruby/2.6.0
fi

if [ -z "$GEM_PATH" ]; then
  export GEM_PATH=/var/task/vendor/bundle/ruby/2.6.0:/var/task/ruby/gems/2.6.0:/var/task/ruby/lib/ruby/gems/2.6.0
fi

if [ -z "$AWS_EXECUTION_ENV" ]; then
  export AWS_EXECUTION_ENV=AWS_Lambda_custom_ruby2.6
fi

if [ -z "$RUBYLIB" ]; then
  export RUBYLIB=/var/task:/var/task/ruby/lib
else
  export RUBYLIB=/var/task:/var/task/ruby/lib:$RUBYLIB
fi

export PATH=/var/task/ruby/bin:$PATH
/var/task/lib/runtime.rb

이제 폴더를 하나의 압축 파일로 만듭니다.

$ zip -r ruby_260.zip bootstrap lib ruby lambda_function.rb

ruby_260.zip으로 압축된 파일로 람다 함수를 생성하고 실행 해보겠습니다.(역할role은 본인 계정에 맞게 변경합니다.)

$ aws lambda create-function \
    --function-name "aws-lambda-ruby-260" \
    --zip-file "fileb://ruby_260.zip" \
    --handler "lambda_function.lambda_handler" \
    --runtime provided \
    --role arn:aws:iam::xxxxxx:role/lambda_basic_execution

$ aws lambda invoke \
    --function-name "aws-lambda-ruby-260" \
    --payload '{"text":"Hello"}' \
    response.txt

$ cat response.txt
{"statusCode":200,"body":"{\"version\":\"Current lambda ruby versions is 2.6.0\",\"endless\":[0,1,2]}"}

루비 2.6.0 커스텀 런타임에서 람다 코드 실행에 성공했습니다.

커스텀 런타임을 포함한 람다 레이어 생성

여기까지 루비 2.6.0을 실행하는 커스텀 런타임을 만들어보았습니다. 하지만 간단한 루비 파일만 가지고 있음에도 압축한 파일이 43MB나 됩니다. 람다 함수는 압축 후 50MB 제한을 가지고 있습니다. 따라서 용량도 7MB 밖에 남지 않습니다. 또한 루비 2.6.0을 사용하고 싶을 때마다 같은 작업을 반복하는 것은 시간 낭비입니다. 이를 해결하기 위해 루비 2.6.0 커스텀 런타임을 람다 레이어로 만들어 보겠습니다.

람다 레이어를 사용하면 업로드한 압축파일이 /var/task 가 아닌 /opt에 풀립니다. 따라서 앞서 빌드 했던 루비 빌드 결과를 그대로 사용할 수 없으므로 /opt/ruby에 루비를 설치하는 작업을 다시 합니다. 도커 이미지를 이용해서 /opt/ruby에 루비 2.6.0을 설치하고 결과물을 복사합니다. 마찬가지로 bootstrap에서 수정했던 /var/task/ruby/opt/ruby로 변경합니다.

이제 아래와 같은 폴더 구조로 만들고 zip 파일로 압축합니다.

.
├─ bootstrap
├─ lib
└─ ruby
$ zip -r ruby_260_layer.zip bootstrap lib ruby

AWS CLI를 이용해 람다 레이어를 생성합니다.

$ aws lambda publish-layer-version \
    --layer-name ruby_260 \
    --description "Ruby 2.6.0" \
    --compatible-runtimes provided \
    --license-info MIT \
    --zip-file fileb://ruby_260_layer.zip

루비 2.6.0 커스텀 런타임을 포함하는 람다 레이어가 생성 되었습니다. 앞으로는 이 레이어를 이용해 루비 2.6.0을 실행 할 수 있습니다.

마치며

람다에 추가된 커스텀 런타임 기능을 이용해 아직 지원하지 않는 루비 최신 버전을 람다에서 실행 하고 이를 람다 레이어로 만들어 재사용할 수 있었습니다. 파이썬Python, 노드Node.js, 자바Java등 공식 지원하는 언어에서 최신 버전을 사용하고 싶다면 비슷한 방법으로 사용 가능하며 지원하지 않는 언어도 커스텀 런타임을 이용해 쉽게 사용할 수 있습니다.

루비 2.6.0 커스텀 런타임은 다음 ARN을 이용해 람다 레이어로 누구나 사용할 수 있도록 공유했습니다. <region> 부분에 본인이 사용하고자 하는 리전값을 넣으면 됩니다.

arn:aws:lambda:<region>:350831304703:layer:ruby-260:1

서울의 경우 ARN은 다음과 같습니다.

arn:aws:lambda:ap-northeast-2:350831304703:layer:ruby-260:1

루비 2.6.0 커스텀 런타임을 만들고 공유하는 코드는 깃허브GitHub seapy/aws-lambda-custom-runtime-builder-for-ruby에 공개해두었습니다. 람다 레이어 공유에 대한 자세한 내용은 람다 레이어를 다른 계정이나 조직과 공유하기 글을 참고 바랍니다.