はじめに
AWS LambdaへRubyのコードをデプロイする時にマルチステージビルドのコンテナイメージを使ったので、そのサンプルを紹介します。
やりたかったこと
- RubyのコードをAWS Lambdaにデプロイしたい
- Dockerコンテナイメージをアップロードしてデプロイしたい
- Gemネイティブエクステンションを使いたい
エラーが出た
AWS Lambda向けに、AWSのRubyベースイメージを使ってbundleインストールしたらエラーが出た。
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.
Gemネイティブエクステンションのビルドに失敗
解決方法
Dockerのマルチステージビルドを使います。
Gemネイティブエクステンションを使わないのであればマルチステージビルドは不要
Gemネイティブエクステンションとは?
Gemのコードの中に、Rubyで書かれた部分と、CやC++などのネイティブコードで書かれた部分の両方を含んでいるGemのこと。
特に処理速度が必要とされるようなGemで用いられます。
”nokogiri”などがGemネイティブエクステンションにあたります。
GemネイティブエクステンションのビルドにはCやC++をビルドするためのツールが必要になります。
3つのサンプル
- 超基本のサンプル(Lambda向けのただのDockerイメージ)
- Gemネイティブエクステンションを使おうとしてエラーになるサンプル
- エラーを解消したサンプル
Macのローカルでテスト実行するまでのサンプルです。
実際のLambdaへのデプロイ方法は割愛しています。
1. 超基本のサンプル
Dockerイメージをビルドしてローカルでテスト実行するまでのサンプルです。
AWS Rubyベースイメージを使います。
このサンプルではGemネイティブエクステンションは使っていません。
module LambdaFunction
class Handler
def self.process(event:,context:)
"Hello from Lambda!"
end
end
end
FROM public.ecr.aws/lambda/ruby:3.3
COPY Gemfile Gemfile.lock ./
RUN gem install bundler:2.4.20 && \
bundle config set --local path 'vendor/bundle' && \
bundle install
COPY lambda_function.rb ./
CMD [ "lambda_function.LambdaFunction::Handler.process" ]
source 'https://rubygems.org'
gem 'aws-sdk-lambda'
空っぽのファイルで良い
イメージビルド
docker build --platform linux/amd64 -t docker-image:test .
コンテナ実行
docker run --platform linux/amd64 -p 9000:8080 docker-image:test
lambda関数呼び出し
コンテナ実行後、別のターミナルで実行してください。
curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}'
実行結果
"Hello from Lambda!"
2. Gemネイティブエクステンションを使おうとしてエラーになるサンプル
Gemネイティブエクステンションである"nokogiri"をrequireしています。
require 'nokogiri'
module LambdaFunction
class Handler
def self.process(event:,context:)
html = "<html><head><title>Hello Nokogiri!</title></head>/html>"
document = Nokogiri::HTML.parse(html)
document.title
end
end
end
Gemネイティブエクステンションである"nokogiri"を指定しています。
source 'https://rubygems.org'
gem 'aws-sdk-lambda'
gem 'nokogiri'
Gemfile.lock、Dockerのファイルの内容は1つ目のサンプルのまま
イメージのビルド
docker build --platform linux/amd64 -t docker-image:test .
ビルドするとこんなエラーが出る
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.
3. エラーを解消したサンプル
マルチステージビルドを使ったDockerファイル
#
# Gemネイティブエクステンションのビルド用イメージ
#
FROM ruby:3.3.2-slim-bookworm AS builder
# 開発ツールとパッケージをインストール
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
libssl-dev \
zlib1g-dev \
libreadline-dev \
libxml2-dev \
libxslt1-dev \
libffi-dev && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
RUN gem install bundler
WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local path '/app/vendor/bundle' && \
bundle install
#
# AWSのLambda用イメージ
#
FROM public.ecr.aws/lambda/ruby:3.3
# ビルドした環境からGemファイルをこちらのイメージにコピー
COPY --from=builder /app/vendor/bundle /var/task/vendor/bundle
# ベースディレクトリがrubyイメージと異なる
WORKDIR /var/task
COPY lambda_function.rb ./
CMD [ "lambda_function.LambdaFunction::Handler.process" ]
Gemfile、Gemfile.lock、lambda_function.rbは2つ目のサンプルから変更ありません。
イメージのビルド
docker build --platform linux/amd64 -t docker-image:test .
エラーにならずビルドできるはずです。
コンテナ実行
docker run --platform linux/amd64 -p 9000:8080 docker-image:test
Lambda関数呼び出し
コンテナ実行後、別のターミナルで実行してください。
curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}'
実行結果
"Hello Nokogiri!"
nokogiriが使えるようになりました。
Dockerファイル説明
Dockerのマルチステージビルドを利用して2つのイメージを作ります。
1つ目のイメージ
FROM ruby:3.3.2-slim-bookworm AS builder
AWS Lambda用のイメージではない一般的なRubyのDockerイメージを用いてGemネイティブエクステンションのビルドを行います。ビルドに必要なライブラリをapt-getでインストールし、GemとGemネイティブエクステンションをビルドします。
2つ目のイメージ
FROM public.ecr.aws/lambda/ruby:3.3
AWS Lambda用のRubyベースイメージを使います。1つ目のイメージで作ったGemを2つ目のイメージにコピーしてきます。その後 lambda_functionを実行しています。
COPY --from=builder /app/vendor/bundle /var/task/vendor/bundle
補足1
AWS Lambda用のRubyベースイメージにGemネイティブエクステンションのビルドに必要なライブラリをインストールすれば良いのでは?という疑問を持つかもしれません。
Rubyベースイメージにあらかじめインストールされているのは、apt-getではなくdnfというパッケージマネージャーです。このdnfを使ってビルドに必要なライブラリをインストールすることも可能です。
しかし、Gemのビルドのためだけに必要なツールが残ったままのイメージをLambdaにデプロイするのはリソースの無駄使いになってしまいます。これを避けるためにマルチステージビルドを利用しています。
補足2
Lambdaに使うことができるDockerコンテナイメージには次のものがあります。
- AWSベースイメージ(Node.js、Python、Java、Go、.NET、Rubyなど)
- AWS OS専用ベースイメージ(Amazon Linux 2023、Amazon Linux 2など)
- 非AWSベースイメージ
AWSベースイメージ以外は、エンドポイントに応答するための設定が必要です。
AWSベースイメージには、Lambdaで利用するための必要最低限のものが組み込まれていて効率的です。