6
1

AWS Lambdaへのデプロイにマルチステージビルドのコンテナイメージを使う【Ruby】

Posted at

はじめに

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つのサンプル

  1. 超基本のサンプル(Lambda向けのただのDockerイメージ)
  2. Gemネイティブエクステンションを使おうとしてエラーになるサンプル
  3. エラーを解消したサンプル

Macのローカルでテスト実行するまでのサンプルです。
実際のLambdaへのデプロイ方法は割愛しています。

1. 超基本のサンプル

Dockerイメージをビルドしてローカルでテスト実行するまでのサンプルです。

AWS Rubyベースイメージを使います。
このサンプルではGemネイティブエクステンションは使っていません。

lambda_function.rb
module LambdaFunction
  class Handler
    def self.process(event:,context:)
      "Hello from Lambda!"
    end
  end
end
Dockerfile
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" ]
Gemfile
source 'https://rubygems.org'
gem 'aws-sdk-lambda'
Gemfile.lock
空っぽのファイルで良い

イメージビルド

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しています。

lambda_function.rb
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"を指定しています。

Gemfile
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ファイル

Dockerfile
#
# 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で利用するための必要最低限のものが組み込まれていて効率的です。

参考

AWS公式のコンテナイメージのデプロイの記事
AWS公式のコンテナイメージの最適化の記事

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1