Sinatra使っているとfaviconを置くのを忘れがちというところがあるのは私だけ? というかAPIサーバなのでfavicon要らんねんけど…。というときに使える小ネタを知ったのでメモとして記しておく。
# app.rb before '/favicon.ico' do halt 204 end
これでルーティングエラーログや404のログも出なくなるのでよい👍
前回、Lambda Web Adapterを使ってSinatraアプリをAWS Lambdaで動かす話をしました。
今回は、Lambda Web Adapterを使っているコンテナでサーバーレスでタスクを実行する方法についてです。
はい。私も最初はそう思っておりました…。そのため、新たにLambdaを作って、同じDocker Imageを使うようにして、DockerのCMDを上書きしてrake taskを実行するようにしてみました。そしてLambda関数の詳細画面のテストタブから、テストを実施。
すると、期待通りのrake taskは実行されているんだけれど、テストが必ず失敗しました。原因は、『Lambda Web AdapterがHTTPのリクエストを期待して待っている間にrake taskが終了してexit 0が行われるため、Lambda Web Adapterからしたら勝手に終了させられた!と認識してしまうため』でした(長い)。
ということで、Lambda Web Adapterを含んでいるDockerコンテナでは、rake taskは使えません!!
まぁ、ちゃんと本家のREADMEを読めって話ですよ…。
Non-HTTP Event Triggersは、他のAWSのサービスからLambdaを呼び出すときに使うトリガーということですが、その受付口として、デフォルトで POST /events
のパスにアクセスすればいいとあります。
ということで、Sinatraで早速、POST /events
で処理を受け付けるようにして、rake taskで行っていた処理を移植しました。
前回の実装でAPI Gatewayから全てのリクエストはLambdaに転送するようにしてあるので、特に修正は不要。rake task用に作っていたLambda関数は削除しました。Docker Imageを更新してLambdaにデプロイし直した後、テストタブからLambda関数に対して POST /events
に向けてリクエストを実施。普通に処理されました🎉
決まった時間帯に処理を行いたかったため、EventBridge Schedulerを定義してみました。cronのルールで対象のLambda関数を呼び出すようにして、JSONペイロードに POST /events
を表すものを記載してみました。パラメータはbody
に定義するので、適宜変えてください。
{ "version": "2.0", "routeKey": "POST /events", "rawPath": "/events", "rawQueryString": "", "headers": { "Content-Type": "application/x-www-form-urlencoded" }, "requestContext": { "http": { "method": "POST", "path": "/events" } }, "body": "name=taro&age=35", "isBase64Encoded": false }
また、決まった時間に処理を行うことよりも確実に処理を行える可能性を高めるほうを重視して、フレックスタイムウィンドウを15分に設定しました。
翌日、動作したか確認したところ、問題なく動作してたのでOK!!いやー、どんどん『サーバーレスチョットデキル』ようになってきています!✌️
まだ移行はしてないんですが、Railsを動かしているVPSの利用料が月額4,000円近くなのに対し、API Gateway + Lambda + DynamoDBに移行したら恐らく月額500~1,000円程度になるのでは?と思います。小さなコストカットではありますが、似たようなRailsアプリがちょこちょこあるので、どんどんこの構成に移植すれば、塵も積もればで年間で十数万のコスト削減になるかなと考えてます。あと金銭的なメリットもさることながら、サーバーレスであることが嬉しい。
Railsで運用されていたアプリをSinatraで実装し直した上でサーバーレスで動かしたかったのでやってみました。 こちらのRailsアプリも長いことメンテされておらず、RubyのバージョンもRailsのバージョンも古すぎた…。
シンプルなWebAPIを作った(Rails + PostgreSQL → Sinatra + DynamoDB)
サーバーレスで動かすのを大前提としていたので、API Gateway + Lambda + DynamoDBで考えてました。 なんかうっすらLambdaでsinatra動かしてみた、みたいな記事を見たことがあった記憶があったので、まぁできるんだろうと安易に考えておりました(結構ハマった)
ぶっちゃけLambdaもAPI GatewayもDynamoDBもほぼ初めてだったので(なんかのチュートリアルでよくわからず使ったことはある)、めちゃめちゃ手探りでした。
DynamoDBのクライアントはdynamoidを使いました。dynamoidはActiveRecord風にDynamoDBが操作できるライブラリです。
特にないだろうか…。AWSもSAA持ってるくせして実際に動かすのはよくわからないのでSAMでデプロイとかもよくわからず、とりあえずマネジメントコンソールから作業しまくってました。動けばええんやで(後で自動化すればええんや)
「Lambda Sinatra」でググると、Qiitaの記事がヒットしました。
Lambda Web Adapterというものがあることを知ったのでこれを使ってみることにしました。 やり方は上記の記事に書いてある通りなのですが、Lambdaで動かすDockerイメージを作成しました。 そして、イメージにタグを定義してECRにDockerイメージをpush。
AWSマネジメントコンソールからLambdaの関数を作成し、ECRからイメージを指定しました。
ここまでは、上記の記事のままでできました。ここからが分からない…。API Gatewayの設定を適当にしてみたものの、全く上手く動かなかったので、一旦放置…。
そもそも定義したLambda関数がちゃんと動くのかどうかを検証しないと、Lambdaが原因なのか、API Gatewayの設定が原因なのかがわからない。 Lambda関数の詳細画面のテストタブから、テストできることに気付いたので、そこから各APIに対してリクエストを試して、動作することを確認。問題なし。逆にAPI Gatewayの設定で何が悪いのかがわからん…。再度API Gatewayと向き合うことに。
API GatewayでHTTP APIを作成。ステージをProdという名前で作成。統合で、定義したLambda関数を指定。そしてデプロイ。
それで https://[API ID].execute-api.[リージョン].amazonaws.com/[ステージ名(Prod)]
というURLが発行されます。
発行されたURLに対してアクセスするも、not foundと言われる…。ただ、このnot foundというメッセージは私がSinatraで定義していた、パスが見つからない場合に呼ばれているものだったので、明らかにSinatraは動いている。
なんとなく、 /Prod
というステージ名をSinatraがパスとして理解してnot foundになっているんじゃないか?という推測したんだけれど、これをどう修正すればいいのかがわからず、半日くらい潰してしまいました。
統合の設定のところで、パラメータマッピングという項目があり、こちらでパラメータを追加したり上書きしたりできるとあったので確認したところ、pathを上書きすることができると分かったので、一旦staticで /
で上書きしたところ、Sinatraの正常なリクエストが返ってくることを確認。絶対に推測通りだなと思ったのですが、どうやったら /[ステージ名]
を除外できるのかがわからず。候補で出てくるものの内容を以下のサイトで確認。
リクエストパス | $request.path または ${request.path} | ステージ名を含まないリクエストパス。 |
これだ!と思い、 path
を $request.path
で上書きするように設定したところ、動きました!🎉
Sinatraでルーティングは決まっているので、それに合わせてroutesをいくつも定義しないといけないかなと思ってたのですが、$default
という特殊なルーティングができるようです。
どのルーティングにも当てはまらないときに $default
にルーティングされるため、最低1つはルーティングを定義しておかないといけないみたいだったので、 /
を定義し、/
と $default
のどちらにも統合で同じLambda関数を指定すればOK。これで、全てのリクエストはSinatraに渡されて評価されるので、見つからないときはnot foundが返るし、Sinatraで定義されているルートならば、その処理が実行されます。
LambdaでSinatraを動かすこと自体は簡単なのだけれど、API Gatewayとの接続方法で結構ハマりましたが、やり方さえ分かればどうってことないですね!これで私もようやくサーバーレスチョットデキルと言える!!
部署向けアプリケーションを動かしていたサーバーのOSサポート期間がギリギリになってきたので、他のサーバーへの移行を行う際に、AWS Lightsailを使ってみました。
Lightsailは、AWSで使えるVPSです。EC2のように柔軟に使えないものの、月額支払いのため、非常に安く利用することができます。 また、VPC等の設定も不要なため、直ぐに利用開始できます。
Lightsailのメリットは直ぐに始められるところです。
デメリットはメリットの裏返しなところが多いです。
サーバの引っ越しついでにマネージドの恩恵を受けながら安く済ませたいということで、今回の構成は、こうしました。 因みに動かしているのはRailsアプリケーションです。
SSL証明書をLet's Encryptを使ってサーバに持たせたらもっとコスト削減できるのですが、今回は面倒だったので任せてしまいました🤫
Railsアプリケーションを起動していたら、時間経過と共に、応答がなくなったり、SSH接続ができなくなったりすることがありました。あまりにも不安定…😢
原因を探ったところ、普通にメモリを使い果たして停止してしまっているようでした。ググってみると、同じようなことはよくあるようで、最低金額の$3.5のプランでWordPressを動かしていたらよく止まる、という記事がチラホラ。
回避方法は、スワップ領域を確保すること。確かに、デフォルトだとスワップ領域が0でした。以下の記事を参考にスワップ領域を2GB分ほど作成したところ、動作が安定しました👍
Lightsail、めちゃくちゃ簡単でした! 今回調べるまで知らなかったのが、マネージドDBがあることです。AWS=RDSを使わないといけない=最低でもそこそこ高い、と思っていたのですが、$15/月からPostgreSQLのマネージドDBが使えるというのはビックリです。社内向けのウェブアプリケーションだと、どうせ無停止で動かすことが多いので、EC2+RDSで24時間動かさないといけないんだったら、Lightsailインスタンス+マネージドDBのほうが費用対効果高いんじゃないかな?と思いました。無論、柔軟な設定ができないので、要件次第ではありますが。
先週の日曜日の早朝に、長男に1階全部を水浸しにされました。
長男は知的障害児で現在小学校2年生で支援学校に通っています。 水遊びが好きで、家でも目を離すとすぐに水道を出しっぱなしにして大変です。それのせいと、大きくなってきたのと、ある意味で知恵がついてきたので鍵をかけてもどこかから台になるものを持ってきて軽々と開けてしまうため、親としては気が抜けません。 困っていたので、リフォームを敢行しました。リフォームの話はまた別の機会にしようと思うのですが、今回の件と関係しているので、とりあえず対策としてキッチンに鍵をつけて入りにくいようにしたということだけ伝えておきます。
基本的に、寝る前には、お風呂の鍵とキッチンの鍵をかけるようにしていたのですが、この日に限ってはなぜかキッチンの鍵をかけ忘れていました。 普段は私が長男と一緒に寝るようにしているのですが、時々夜中に起きて癇癪を起こしたりすることがあるのですが、この日も起きてしまいました。私もうっすらと目が覚めたのですが、妻のいる寝室に行ったみたいだったので、まぁそれならいいかと見過ごしてそのまま寝てしまいました。
その後、遠くから「ピピピピピピ…」という音が聞こえてきて「これは冷蔵庫が開けっ放しになっている音だ!」と気付き、ガバッと起きてすぐに1階に降りたのですが(寝室は2階)、そこはもう全部が水浸しとなり、長男はマットの上で泣いていました。案の定、冷蔵庫は空いており、キッチンの蛇口が引っ張れるタイプなのですが、それがシンクを超えた状態で水が出続けていました…。まさか1階全てが水浸しになっているなんて想像もしていなかったので、軽くパニックになりましたが、ひとまず水道を止め、冷蔵庫を閉めたのに、まだ音が鳴っている…。確認すると、食洗機が水漏れしているというエラーを出していました…。水がシンク下の収納のところに一杯に入っており、そこから食洗機側に流れ込んだようでした。絶望です。
まずは長男をどうにかしないといけないので、2階の寝室に連れていったら、泣き疲れたのか、すぐに寝てしまいました。本人は気楽なものです…。
この時点で朝6時半くらいで、妻を起こして二人で水をなんとかするために作業を開始。ぐしょ濡れになった衣類は洗濯かごに入り切らないし水をたっぷり吸っているので、とりあえず風呂場へ移動させ、洗面所あたりはの水は風呂に流し、リビングの水は庭に捨てるようにしました。ひたすらバスタオルに吸わせてバケツに絞り、を繰り返し、二人ともクタクタになりながら全部を片付け終わったのはお昼すぎでした。8割くらいは昼前に終わってたけれど、そこからの残りをやる気力・体力がなかなか出ませんでした。
当然ですが、床においてあった全てのものが濡れてしまいました。本やオモチャなど、使い物にならなくなったものが結構ありました。長男の学校用カバンもぐしょ濡れ。次男の幼稚園用カバンもぐしょ濡れ。たまたまいい天気の日だったので、ウッドデッキに広げて干せました。
数週間前にお風呂のシャワーを洗面所に向かって出されて洗面所が水浸しになったことがあったり、以前にも水を出しっぱなしにされたりしたこともあったせいもあり、私と妻の感想は「不幸中の幸いで、日曜日だったからよかった」でした。もし、平日だったら、仕事を休んで対応しないと床が浸水しすぎて剥がれてくるかもしれなかったですし、子どもたちも学校に行けないし(カバンが使えないから)、大混乱していたことでしょう。いやー、おさるのジョージに出てくる黄色い帽子のおじさんばりによく訓練されている…。
リフォームして油断していたというのもあるかと思うのですが、もう本当に知的障害児の育児は気が抜けません。話せないから言葉が通じないし、怒っても理解はできないでただ癇癪を起こして暴れるだけ。まぁ一応、怒られたら止めるのだけれど、私達がいなくなった隙をみてすぐにやってやろうとするので、それがもう本当に腹が立ちます。ダメ、を理解しているわけではないので、どうやったら本当にダメなことをわかってもらえるのか…。
今までの子育ての中で一番の大事件だったんじゃないかなぁと思います。マジで疲れました。もう二度とやりたくないです。こういうの、なんか保険とか使えないかなぁ…。片付けちゃったから証拠がもうないんだけれど。あ、食洗機は壊れたままです。使えないと、本当に不便です。
Hanami v2.0を試そうとしているのだけれど、まだHanami v2系にはHanami Modelがリリースされておらず、rom-rbを直接使うことが推奨されている状態なので、rom-rbを使ってDBのマイグレーションを行っていたのですが、これロールバックはどうするんだろうか?と思って調べていました。しかし、ロールバックについての情報は見つからない…。
rom-rbでDBを使うためのgem rom-sqlでは、sequelを使っているので、sequelのほうの情報を見てみました。
sequel/migration.rdoc at master · jeremyevans/sequel · GitHub
running migrationのところを確認すると、-M
でロールバックになると書いてあったので、ロールバックの仕組みはある…。
今一度、rom-sqlのほうのコードを確認してみようと思い立ち、rake taskのコードを確認したところ、rake db:migrate[version]
でいけることが分かりました。
rom-sql/migration_tasks.rake at 596fea491ace84ae1aea12599879f84cb0ce5b64 · rom-rb/rom-sql · GitHub
こんな感じ。指定したバージョンまで戻るので、明示的にrollbackと書くことがないので、ググってもなかなかヒットしないわけですね。
bundle exec rake db:migrate[20230405151448] # zshの場合 bundle exec rake db:migrate\[20230405151448\]
というかdescに書いてあるやん…。
$ bundle exec rake -T rake db:clean # Perform migration down (removes all tables) rake db:create_migration[name,version] # Create a migration (parameters: NAME, VERSION) rake db:migrate[version] # Migrate the database (options [version_number])] rake db:reset # Perform migration reset (full erase and migration up) rake environment # Load the app environment rake spec # Run RSpec code examples
Hanami v2.1で登場するHanami Modelの場合はこの辺りをサポートしてもらえると嬉しい。
何年も前に社内向けにシンプルなアプリをRailsで作っていたのだけれど、長いことメンテナンスできずにいた。 Rubyのバージョンも古いし、Railsのバージョンも古かったので、バージョンアップをしないとなぁと思っていたのだけれど、やったところでまた数年後に同じ思いをすることになりそうだなぁ…と思ったので、もっとバージョンアップのしやすいSinatraに書き換えることにした。
実はSinatraを試したことくらいはあったけれど、ちゃんとデプロイまでしたことはなかったので、よい経験になった。
シンプルなWebAPIを作った。JSONを返すだけ。
CORSをするときに、Bearer認証を使っていたため、Authorizationヘッダーを追加しているのでpreflight requestが発生するようになり、エラーが発生していた。
# app.rb require 'sinatra' require 'sinatra/activerecord' require 'sinatra/contrib' require "sinatra/cors" require 'rack/bearer_auth' use Rack::BearerAuth::Middleware do match path: "/api/v1/foos", via: [:all] do |token| AccessToken.exists?(token: token) end end set :database_file, 'config/database.yml' set :allow_origin, '*' set :allow_methods, 'GET' set :allow_headers, 'content-type,if-modified-since' set :allow_credentials, true # 略
調査をした結果、以下の修正が必要だった。
# app.rb require 'sinatra' require 'sinatra/activerecord' require 'sinatra/contrib' require "sinatra/cors" require 'rack/bearer_auth' use Rack::BearerAuth::Middleware do match path: "/api/v1/foos", via: [:get] do |token| # <= via: [:get]に変更してoptionsの場合を対象外に変更 AccessToken.exists?(token: token) end end set :database_file, 'config/database.yml' set :allow_origin, '*' set :allow_methods, 'GET,OPTIONS' # <= OPTIONSを追加 set :allow_headers, 'content-type,if-modified-since,authorization' # <= authorizationを追加 set :allow_credentials, true # 略
これでCORSできるようになった!
最初のCORSの設定を行ったとき、当初はBearer認証を実装してない状態だったのでシンプルなCORSでpreflight requestが発生していなかったので気づかなかった。 Bearer認証を実装したときには、CORSを試さずに単にREST Clientで試して問題なかったので、気づかなかった。 普通にデプロイ後に一応手動テストを行ったらCORSに失敗して非常に焦った…😅
pumaコマンドで起動する場合、config.ruファイルが必要。
# config.ru require './app' run Sinatra::Application
単にpumaで起動した場合、ファイルを修正しても反映されないので、一旦pumaを止めてまた起動しなければならない。
bundle exec puma
rerunを使うことで、ファイルを修正したらホットリロードされるようになる。
bundle exec rerun puma
修正が即反映されてとっても捗る。
これでRailsアプリを1つメンテナンスしなくて済むようになったのでよかった。Sinatraならメンテ不要というわけでもないけれど、まぁ基本的にbundle updateだけで済みそうでしょう。
久々にブログを書いたけれど、やっぱりメモ的な内容でもいいからアウトプットしていかないとなぁ〜と思った。管理職になってからコードを書く量がめちゃ減っているので頑張っていきたい。