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
を作成してエントリポイントを記述します。
エントリポイントは event
と context
をキーワード引数で受け取るトップレベルのメソッドかクラスメソッドとして定義します。
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"
}
runtime
は AWS 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 はランタイムに含まれているので 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:)
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
に記述します。ここで role
は project.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 init
、 apex 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
Gemfile
に faker
を追加します。
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.json
の timeout
の値を 15
程度に変更してから再度デプロイし実行します。
$ apex invoke hello
"Dwayne Murphy DVM"
無事起動しました。
gem のインストールを自動化する
Apex のフック機能を利用して gem のインストールを自動化します。
function.json
に hooks
を追加します。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
これを development
と production
に分離します。
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、タッチダウン
想像していたよりもガチな内容で楽しめました。曰く「相模原でカプセルのフタを開けるまでが遠足です。まだまだ長いです」。今からでも副読本にどうぞ。