はじめに
Rails+Lambdaの記事が少なかったので投稿します。
この記事はRails+Lambda(+APIGateway)で GET /me
が実行できるところを目指します。
Railsはある程度分かるけど、Docker使ったことない、サーバレスやりたい人向けです。
今回SAMは使わずある泥臭いやり方で行います。
mysqlは今回コード上では使いませんが、デプロイの関係で構築は行っています。もしRDSやDyanamo使いたい場合は適時調べて行ってください。
環境
Docker for Windows
Ruby 2.7
Rails 6
MySQL 5.x
ディレクトリ構成
rails_api
├── Dockerfile
├── docker-compose.yml
├── Gemfile
├── Gemfile.lock
各種ファイル
Dockerfile
native extension を含む gem を lambci/lambdaの docker イメージでビルドしなおしをするため、イメージはLambdaのものを使用します。
FROM lambci/lambda:build-ruby2.7
# install required libraries
RUN yum -y install mysql-devel
# install bundler
RUN gem install bundler
WORKDIR /tmp
ADD Gemfile Gemfile
ADD Gemfile.lock Gemfile.lock
RUN bundle install
WORKDIR /app
COPY . /app
docker-compose.yml
version: '3'
services:
mysql:
image: mysql:5.7
command: mysqld --character-set-server=utf8 --collation-server=utf8_general_ci
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
environment:
MYSQL_DATABASE: app_development
MYSQL_USER: root
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: password
app:
tty: true
stdin_open: true
build: .
command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
volumes:
- .:/app
ports:
- "3000:3000"
depends_on:
- mysql
volumes:
mysql-data:
driver: local
Gemfile
railsは最新版をとりあえず。
すでにローカル環境でRailsを動かせている人は不要です。
source 'https://rubygems.org'
gem 'rails', '~> 6.0.2', '>= 6.0.2.1'
Gemfile.lock
空ファイルで作成
すでにローカル環境でRailsを動かせている人は不要です。
Rails作成(Rails new + Rails Server)
ネット環境の良い場所で行いましょう。そこそこ時間がかかります。
すでにローカル環境でRailsを動かせている人は不要です。
$ docker-compose run app bundle exec rails new . --force --database=mysql
Starting rails_api_mysql_1 ... done exist
force README.md
identical Rakefile
create .ruby-version
create config.ru
create .gitignore
force Gemfile
以下省略
ここでGemfileが新しく生成されるので不要なgemを削除します。(必要な場合は入れてください。)
## 以下削除
gem 'sass-rails', '>= 6'
# Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker
gem 'webpacker', '~> 4.0'
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
gem 'turbolinks', '~> 5'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.7'
docker build & railsサーバ起動
Railsサーバ起動
$ docker-compose build
$ docker-compose up
※問題があればここでエラーします。
app_1 | => Booting Puma
app_1 | => Rails 6.0.2.1 application starting in development
app_1 | => Run `rails server --help` for more startup options
app_1 | /var/runtime/gems/actionpack-6.0.2.1/lib/action_dispatch/middleware/stack.rb:37: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
app_1 | /var/runtime/gems/actionpack-6.0.2.1/lib/action_dispatch/middleware/static.rb:110: warning: The called method `initialize' is defined here
app_1 | Puma starting in single mode...
app_1 | * Version 4.3.3 (ruby 2.7.0-p0), codename: Mysterious Traveller
app_1 | * Min threads: 5, max threads: 5
app_1 | * Environment: development
app_1 | * Listening on tcp://0.0.0.0:3000
app_1 | Use Ctrl-C to stop
とりあえず、localhost:3000 で確認
Mysql2::Error::ConnectionErrorが発生しているので、すこし修正します。
database.ymlの修正
将来のことも考え、productionも記載します。
productionには環境変数で設定しているため、Lambdaの環境変数を使ってユーザ名、パスワードなどを設定します。
default: &default
adapter: mysql2
encoding: utf8
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
username: root
password: password
host: mysql
development:
<<: *default
database: app_development
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
database: app_test
production:
<<: *default
username: <%= ENV['MYSQL_USER'] %>
password: <%= ENV['MYSQL_ROOT_PASSWORD'] %>
database: <%= ENV['MYSQL_DATABASE'] %>
host: <%= ENV['MYSQL_HOST'] %>
socket: <%= ENV['MYSQL_SOCKET'] %>
再度Railsサーバ起動+localhost:3000 確認
$ docker-compose up
ここでエラーがでなければ、一応初期セットアップ完了、APIサーバとして動かすため、事前に/meを作成しておきます。
APIの作成
コントローラーとRouteのみ記載しlocalhostで確認します。
- app/controller/me_controller.rb
class MeController < ApplicationController
def index
render json: { 'me' => 'me' }
end
end
- config/routes.rb
Rails.application.routes.draw do
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
resources :me, only: [:index]
end
GET /me
の確認
念のためdockerコンテナを再起動して、APIの確認を行います。
$ docker-compose up
$ curl localhost:3000/me
{"me":"me"}
Lambda(+ API Gateway)化
Lambda 用の Rack ブリッジサンプルをダウンロード
下記のサンプルソースからLambda用のブリッジサンプルを入手し、
https://github.com/aws-samples/serverless-sinatra-sample/blob/master/lambda.rb
中身のパスを1箇所だけ修正します。
- $app ||= Rack::Builder.parse_file("#{__dir__}/app/config.ru").first
+ $app ||= Rack::Builder.parse_file("#{__dir__}/config.ru").first
Native Extensions のローカルへのリビルド
dockerの中に入って作業します。この作業を行わないとNative Extensions系のライブラリが動作しないです。Postgresqlを使用する場合は別途コピー元のディレクトリを変更します。
dockerの中に入るコマンド
$ docker-compose ps
※ここでdockerの名前を確認
$ docker exec -it docker名 bash
bash-4.2#
リビルド
bash-4.2# bundle install --path /bundle
bash-4.2# mv /bundle /app/vendor/
bash-4.2# sed -e 's|/bundle|vendor/bundle|' -i .bundle/config
bash-4.2# cat .bundle/config
---
BUNDLE_PATH: "vendor/bundle"
# 下記作業はmysqlを使う場合
bash-4.2# /sbin/ldconfig -p | grep mysql | cut -d\> -f2
bash-4.2# cp /usr/lib64/mysql/libmysqlclient.so.18 lib/
動くか確認します。
$ docker-compose stop
$ docker-compose up
$ curl http://localhost:3000/me
ファイルをzipで固めてlambdaへデプロイします。今回はdockerの中でやっています。
$ zip -r lambda_function.zip .bundle app config Gemfile* config.ru lambda_function.rb vendor lib public
Lambdaの作成
※細かい部分は省略。
デプロイするときzip容量が大きいのでS3からアップロード。
Lambda作成時の環境変数は以下のとおり。
Name | Value |
---|---|
BOOTSNAP_CACHE_DIR | /tmp/cache |
BUNDLE_APP_CONFIG | /var/task/.bundle |
PASSWORD | password |
RAILS_ENV | production |
RAILS_LOG_TO_STDOUT | true |
RAILS_SERVE_STATIC_FILES | true |
USER | root |
ポイントはbootsnap_cache_dir
で、bootsnap は ${WORKING_DIR}/tmp/cache にキャッシュを生成することになっており、このままLambdaを使うと、/var/task/tmp
自体は読み込み専用ディレクトリなっているためエラーが発生します。
テストイベントの作成
新しいテストイベントの作成 > イベントテンプレート > Amazon API Gateway AWS Proxy を選択
path
httpMethod
pathParameters
を下記のように/me
用に変更する
"path": "/me"
"httpMethod": "GET"
"pathParameters": {
"proxy": "/me"
}
テストを実行して下記レスポンスが返ってくるか確認する
{
"statusCode": 200,
"headers": {
"X-Frame-Options": "SAMEORIGIN",
"X-XSS-Protection": "1; mode=block",
"X-Content-Type-Options": "nosniff",
"X-Download-Options": "noopen",
"X-Permitted-Cross-Domain-Policies": "none",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Content-Type": "application/json; charset=utf-8",
"ETag": "W/\"hogehoge\"",
"Cache-Control": "max-age=0, private, must-revalidate",
"X-Request-Id": "hoge-hoge-hoge-hoge-hoge",
"X-Runtime": "0.004623"
},
"body": "{\"me\":\"me\"}"
}
API Gatewayの作成+Lambda連携
ステージからAPIGatewayのデプロイを行い、APIの実行を行う
$ curl https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/hoge/me
{"me":"me"}
おわり
あとはRoute53やAPI Gatewayのカスタムドメインの設定を行えば本番運用可能です。またAPIGatewayの代わりにALBを使用することも可能です。その場合Lambdaのコードは修正不要です。