エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Apex + AWS Lambda + Ruby 覚書

Ruby で書いた AWS Lambda の関数を Apex を使ってデプロイできたので、その時の覚書です。 誤りや認識間違いが混ざっているかもしれません。ご指摘いただけたら幸いです。

仕事ではずっと Serverless + nodejs を使っていたのでこちらの方が慣れているのですが、別件で Apex を使うことになったのでその学習も兼ねて。

インストールとか

詳細は公式サイトを参照してください。

$ curl https://raw.githubusercontent.com/apex/apex/master/install.sh | sh

プロジェクトを作成する

適当なディレクトリを作成して移動し、apex init コマンドを実行します。 プロジェクト名とその説明の入力を求められるので、適当な内容を入力します。

$ mkdir apex-ruby
$ cd apex-ruby/
$ apex init


             _    ____  _______  __
            / \  |  _ \| ____\ \/ /
           / _ \ | |_) |  _|  \  /
          / ___ \|  __/| |___ /  \
         /_/   \_\_|   |_____/_/\_\



  Enter the name of your project. It should be machine-friendly, as this
  is used to prefix your functions in Lambda.

    Project name: apex-ruby

  Enter an optional description of your project.

    Project description: Apex + AWS Lambda + Ruby

  [+] creating IAM apex-ruby_lambda_function role
  [+] creating IAM apex-ruby_lambda_logs policy
  [+] attaching policy to lambda_function role.
  [+] creating ./project.json
  [+] creating ./functions

  Setup complete, deploy those functions!

    $ apex deploy

作成されるディレクトリとファイルはこんな感じ。

$ tree
.
├── functions
│   └── hello
│       └── index.js
└── project.json

project.json の内容はこんな感じ。

{
  "name": "apex-ruby",
  "description": "Apex + AWS Lambda + Ruby",
  "memory": 128,
  "timeout": 5,
  "role": "arn:aws:iam::548673361492:role/apex-ruby_lambda_function",
  "environment": {}
}

function/hello/index.js は不要なので削除します。

$ rm functions/hello/index.js

関数を作成する

function/hello/index.rb を作成してエントリポイントを記述します。

エントリポイントは eventcontext をキーワード引数で受け取るトップレベルのメソッドかクラスメソッドとして定義します。

def handler(event:, context:)
  'Hello, Apex + AWS Lambda + Ruby!'
end

念のため動作を確認。

$ ruby -r './functions/hello/index.rb' -e 'puts handler(event: nil, context: nil)'
Hello, Apex + AWS Lambda + Ruby!

設定を記述する

functions/hello/ の下に function.json というファイルを作成し設定を記述します。

{
  "runtime": "ruby2.5",
  "handler": "index.handler"
}

runtimeAWS Lambda で利用するランタイムです。今回は Ruby を使うので ruby2.5 を指定します。 handler は実行時に呼び出されるメソッドを指定します。トップレベルのメソッドの場合は ファイル名.メソッド名 という形式で、クラスメソッドの場合は ファイル名.クラス名.メソッド名 という形式で指定します。

デプロイする

apex deploy コマンドでデプロイします。

$ apex deploy
   • creating function         env= function=hello
   • created alias current     env= function=hello version=1
   • function created          env= function=hello name=apex-ruby_hello version=1

デプロイ状況を確認します。ここでは AWS CLI で関数名の一覧を取得して確認しています。

$ aws lambda list-functions --query Functions[].FunctionName
[
    "apex-ruby_hello"
]

関数名は プロジェクト名_(functionsの下の)ディレクトリ名 という形式になっています。

実行する

apex invoke コマンドで実行します。指定する関数名は AWS Lambda の関数名ではなく、Apex 内の名前になります。

$ apex invoke hello
"Hello, Apex + AWS Lambda + Ruby!"

削除する

apex delete コマンドで削除します。

$ apex delete
Are you sure? (yes/no) yes
   • deleting                  env= function=hello
   • function deleted          env= function=hello

AWS SDK for Ruby を使う

AWS SDK for Ruby はランタイムに含まれているので require するだけで利用することができます。

require 'aws-sdk-lambda'

def handler(event:, context:)
  Aws::Lambda::GEM_VERSION
end

再デプロイして実行します。

$ apex deploy
$ apex invoke hello
"1.15.0"

なお関数をデプロイしただけでは色々と許可されていないので、このままでは AWS のリソースにアクセスしようとするとエラーになってしまいます。

require 'aws-sdk-lambda'

def handler(event:, context:)
  # AWS Lambda の関数を一覧する
  client = Aws::Lambda::Client.new
  resp = client.list_functions
  resp.functions.map(&:function_name).join(',')
end
$ apex deploy
$ apex invoke hello
   ⨯ Error: function response: User: arn:aws:sts::xxxxxxxxxxxx:assumed-role/apex-ruby_lambda_function/apex-ruby_hello is not authorized to perform: lambda:ListFunctions on resource: *

Terraform で色々設定する

Apex 自身にはこれらを設定する機能はありませんが、Terraform を利用して解決します。

プロジェクト内にディレクトinfrastructure を作成し、そこに Terraform の設定を記述します。

ポリシーの設定を infrastructure/main.tf に記述します。ここで roleproject.json にある role の内容を記述します。

resource "aws_iam_role_policy" "lambda_policy" {
  name = "lambda_policy"
  role = "apex-ruby_lambda_function"
  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "lambda:*",
      "Resource": "*"
    }
  ]
}
EOF
}

適用は infrastructure ディレクトリに移動して terraform コマンドを実行する代わりに、apex infra initapex infra apply を実行することで実行できます。

$ apex infra init
$ apex infra apply

これでリソースにアクセスできるようになります。

$ apex invoke hello
"apex-ruby_hello"

これらは関数のデプロイとは独立しているので、適用や更新や削除はそれぞれ実行する必要があります。

gem を利用する

標準添付ライブラリと AWS SDK for Ruby 以外の gem を利用する場合はインストールする必要があります。

Faker を使う例で試してみます。

require 'faker'

def handler(event:, context:)
  Faker::Name.name
end

このままでデプロイ、実行すると、予想通り gem を読み込めずにエラーになります。

$ apex deploy
$ apex invoke hello
   ⨯ Error: function response: cannot load such file -- faker

Ruby のバージョンを AWS Lambda に合わせる

gem をインストールする前に、Ruby のバージョンを AWS Lambda のランタイムに合わせておきます。 rbenv などを利用してバージョンを切り替えます。

$ rbenv local 2.5.3

Gemfile を作成する

関数のディレクトリに移動し bundle init を実行するなどして Gemfile を作成します。

$ cd functions/hello/
$ bundle init

Gemfilefaker を追加します。

# frozen_string_literal: true

source 'https://rubygems.org'

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem 'faker'

gem をインストールする

Apex は関数を定義しているディレクトリの内容を AWS Lambda にアップロードします。 --path オプションで vendor/bundle/ 以下にインストールするか、Gemfile.lock 生成後に --deployment オプションを指定してインストールを実行します。

この辺りの詳細については Bundler のドキュメントを確認してください。

$ bundle --path vendor/bundle
Fetching gem metadata from https://rubygems.org/..
Using bundler 2.0.1
Fetching concurrent-ruby 1.1.4
Installing concurrent-ruby 1.1.4
Fetching i18n 1.5.3
Installing i18n 1.5.3
Fetching faker 1.9.3
Installing faker 1.9.3
Bundle complete! 1 Gemfile dependency, 4 gems now installed.
Bundled gems are installed into `./vendor/bundle`

インストールできたらプロジェクトのディレクトリに戻ります。

あらためてデプロイする

gem をインストールした状態でデプロイすると、gem も合わせてアップロードされます。

$ apex deply

実行すると gem がロードできないというエラーは出なくなりますが、タイムアウトが発生すると思います。

$ apex invoke hello
   ⨯ Error: function response: 2019-02-23T02:31:00.996Z fc0f76c0-2238-434b-8a21-722917e1042f Task timed out after 5.00 seconds

Apex はタイムアウトの初期値として 5 秒を設定していますが、初回の起動時に 10 秒あまりかかるためタイムアウトしてしまいます。 project.jsontimeout の値を 15 程度に変更してから再度デプロイし実行します。

$ apex invoke hello
"Dwayne Murphy DVM"

無事起動しました。

gem のインストールを自動化する

Apex のフック機能を利用して gem のインストールを自動化します。

function.jsonhooks を追加します。build--deployment オプションを付けてのインストールを指定します。デプロイ後に「凍結」を解除したいので frozen の削除を clean で指定しています。

{
  "runtime": "ruby2.5",
  "handler": "index.handler",
  "hooks": {
    "build": "bundle install --deployment",
    "clean": "bundle config --delete frozen"
  }
}

native extension を利用する gem を利用する場合

native extension を利用する gem の場合は実行環境と同等の環境でインストールする必要がありますが、Docker で同等の環境を入手することができるのでこれを利用すれば可能になります。

この中でタグ build-ruby2.5 を指定すると Ruby もインストールされた環境を入手できます。

環境を切り替える

開発版やリリース版などの環境を切り替える場合、設定ファイルに環境名を追加します。また Terraform は環境ごとのディレクトリを作成します。

ここまでのファイルの内容:

apex-ruby/
├── functions/
│   └── hello/
│       ├── Gemfile
│       ├── Gemfile.lock
│       ├── function.json
│       └── index.rb
├── infrastructure/
│   └── main.tf
└── project.json

これを developmentproduction に分離します。

apex-ruby/
├── functions/
│   └── hello/
│       ├── Gemfile
│       ├── Gemfile.lock
│       ├── function.development.json
│       ├── function.production.json
│       └── index.rb
├── infrastructure/
│   ├── development/
│   │   └── main.tf
│   └── production/
│       └── main.tf
├── project.development.json
└── project.production.json

project.環境名.json で指定する name の値など、環境によって内容が異なるものはそれぞれの環境用の値を設定します。

適用する環境は、コマンドの実行時に --env オプションで指定します。

$ apply --env development deploy

apply infra コマンドで --env を指定すると、infrastructure ディレクトリの下の環境名のディレクトリの内容が適用されます。

いつか読むはずっと読まない:2019-02-22、タッチダウン

想像していたよりもガチな内容で楽しめました。曰く「相模原でカプセルのフタを開けるまでが遠足です。まだまだ長いです」。今からでも副読本にどうぞ。

現代萌衛星図鑑

現代萌衛星図鑑

現代萌衛星図鑑 第2集

現代萌衛星図鑑 第2集