patorashのブログ

方向性はまだない

Sinatraでfaviconへのアクセスを無視する

Sinatra使っているとfaviconを置くのを忘れがちというところがあるのは私だけ? というかAPIサーバなのでfavicon要らんねんけど…。というときに使える小ネタを知ったのでメモとして記しておく。

# app.rb
before '/favicon.ico' do
  halt 204
end

これでルーティングエラーログや404のログも出なくなるのでよい👍

Lambda Web Adapterのコンテナでタスクを実行する

前回、Lambda Web Adapterを使ってSinatraアプリをAWS Lambdaで動かす話をしました。

patorash.hatenablog.com

今回は、Lambda Web Adapterを使っているコンテナでサーバーレスでタスクを実行する方法についてです。

タスクはrake taskを実行すればいいんでしょ?

はい。私も最初はそう思っておりました…。そのため、新たに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は使えません!!

Non-HTTP Event Triggers の存在を知る

まぁ、ちゃんと本家のREADMEを読めって話ですよ…。

github.com

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から呼び出す

決まった時間帯に処理を行いたかったため、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アプリがちょこちょこあるので、どんどんこの構成に移植すれば、塵も積もればで年間で十数万のコスト削減になるかなと考えてます。あと金銭的なメリットもさることながら、サーバーレスであることが嬉しい。

Lambda Web Adapterを使ってSinatraを動かす

Railsで運用されていたアプリをSinatraで実装し直した上でサーバーレスで動かしたかったのでやってみました。 こちらのRailsアプリも長いことメンテされておらず、RubyのバージョンもRailsのバージョンも古すぎた…。

作ったもの

シンプルなWebAPIを作った(Rails + PostgreSQLSinatra + DynamoDB)

構成

サーバーレスで動かすのを大前提としていたので、API Gateway + Lambda + DynamoDBで考えてました。 なんかうっすらLambdaでsinatra動かしてみた、みたいな記事を見たことがあった記憶があったので、まぁできるんだろうと安易に考えておりました(結構ハマった)

ぶっちゃけLambdaもAPI GatewayもDynamoDBもほぼ初めてだったので(なんかのチュートリアルでよくわからず使ったことはある)、めちゃめちゃ手探りでした。

DynamoDBクライアント

DynamoDBのクライアントはdynamoidを使いました。dynamoidはActiveRecord風にDynamoDBが操作できるライブラリです。

github.com

その他?

特にないだろうか…。AWSもSAA持ってるくせして実際に動かすのはよくわからないのでSAMでデプロイとかもよくわからず、とりあえずマネジメントコンソールから作業しまくってました。動けばええんやで(後で自動化すればええんや)

LambdaでSinatraを動かそうとしたが…

「Lambda Sinatra」でググると、Qiitaの記事がヒットしました。

qiita.com

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の設定するもうまく動かない

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の正常なリクエストが返ってくることを確認。絶対に推測通りだなと思ったのですが、どうやったら /[ステージ名] を除外できるのかがわからず。候補で出てくるものの内容を以下のサイトで確認。

docs.aws.amazon.com

リクエストパス $request.path または ${request.path} ステージ名を含まないリクエストパス。

これだ!と思い、 path$request.path で上書きするように設定したところ、動きました!🎉

pathを$request.pathで上書き!

API Gatewayに定義するroutesは2つでよい

Sinatraでルーティングは決まっているので、それに合わせてroutesをいくつも定義しないといけないかなと思ってたのですが、$defaultという特殊なルーティングができるようです。

どのルーティングにも当てはまらない時にルーティングされるやつ

どのルーティングにも当てはまらないときに $default にルーティングされるため、最低1つはルーティングを定義しておかないといけないみたいだったので、 / を定義し、/$default のどちらにも統合で同じLambda関数を指定すればOK。これで、全てのリクエストはSinatraに渡されて評価されるので、見つからないときはnot foundが返るし、Sinatraで定義されているルートならば、その処理が実行されます。

/と$defaultを定義

まとめと感想

LambdaでSinatraを動かすこと自体は簡単なのだけれど、API Gatewayとの接続方法で結構ハマりましたが、やり方さえ分かればどうってことないですね!これで私もようやくサーバーレスチョットデキルと言える!!

AWS Lightsailで社内向けツールのサーバ立ててみた

部署向けアプリケーションを動かしていたサーバーのOSサポート期間がギリギリになってきたので、他のサーバーへの移行を行う際に、AWS Lightsailを使ってみました。

Lightsailは、AWSで使えるVPSです。EC2のように柔軟に使えないものの、月額支払いのため、非常に安く利用することができます。 また、VPC等の設定も不要なため、直ぐに利用開始できます。

aws.amazon.com

Lightsailのメリット

Lightsailのメリットは直ぐに始められるところです。

  • 費用は月額固定($3.5~)
  • AWSの他のサービスの設定等がほぼ不要
  • シンプルなネットワーク設定(FW)
  • Lightsail用のマネージドサービスが複数ある
    • RDSではない、マネージドDB($15~)
    • ELBではない、シンプルなロードバランサ
    • S3ではない、ストレージ
  • 自動スナップショット機能(バックアップも万全)

Lightsailのデメリット

デメリットはメリットの裏返しなところが多いです。

  • EC2のように使いたいときだけ使うことができない
  • AWSの他のサービスとの連携がほぼできない(インターネット経由でならできる)
  • 安いプランだとCPUが貧弱(バーストできるが、時間制限がある)
  • 構成がシンプルなため、複雑なことは難しい

今回の構成

サーバの引っ越しついでにマネージドの恩恵を受けながら安く済ませたいということで、今回の構成は、こうしました。 因みに動かしているのはRailsアプリケーションです。

  • Lightsailインスタンス($7のプラン) ** OSはUbuntu 22.04
  • SSL証明書の更新をマネージドにしたかったのでロードバランサ導入($18)
  • データベースはLightsailインスタンスに同梱(費用を抑えるため)
  • 自動スナップショットを有効化

SSL証明書をLet's Encryptを使ってサーバに持たせたらもっとコスト削減できるのですが、今回は面倒だったので任せてしまいました🤫

起きたトラブルと回避策

Railsアプリケーションを起動していたら、時間経過と共に、応答がなくなったり、SSH接続ができなくなったりすることがありました。あまりにも不安定…😢

原因を探ったところ、普通にメモリを使い果たして停止してしまっているようでした。ググってみると、同じようなことはよくあるようで、最低金額の$3.5のプランでWordPressを動かしていたらよく止まる、という記事がチラホラ。

回避方法は、スワップ領域を確保すること。確かに、デフォルトだとスワップ領域が0でした。以下の記事を参考にスワップ領域を2GB分ほど作成したところ、動作が安定しました👍

manualmaton.com

まとめ

Lightsail、めちゃくちゃ簡単でした! 今回調べるまで知らなかったのが、マネージドDBがあることです。AWS=RDSを使わないといけない=最低でもそこそこ高い、と思っていたのですが、$15/月からPostgreSQLのマネージドDBが使えるというのはビックリです。社内向けのウェブアプリケーションだと、どうせ無停止で動かすことが多いので、EC2+RDSで24時間動かさないといけないんだったら、Lightsailインスタンス+マネージドDBのほうが費用対効果高いんじゃないかな?と思いました。無論、柔軟な設定ができないので、要件次第ではありますが。

長男に家の1階全部を水浸しにされた話

先週の日曜日の早朝に、長男に1階全部を水浸しにされました。

長男は知的障害児で現在小学校2年生で支援学校に通っています。 水遊びが好きで、家でも目を離すとすぐに水道を出しっぱなしにして大変です。それのせいと、大きくなってきたのと、ある意味で知恵がついてきたので鍵をかけてもどこかから台になるものを持ってきて軽々と開けてしまうため、親としては気が抜けません。 困っていたので、リフォームを敢行しました。リフォームの話はまた別の機会にしようと思うのですが、今回の件と関係しているので、とりあえず対策としてキッチンに鍵をつけて入りにくいようにしたということだけ伝えておきます。

基本的に、寝る前には、お風呂の鍵とキッチンの鍵をかけるようにしていたのですが、この日に限ってはなぜかキッチンの鍵をかけ忘れていました。 普段は私が長男と一緒に寝るようにしているのですが、時々夜中に起きて癇癪を起こしたりすることがあるのですが、この日も起きてしまいました。私もうっすらと目が覚めたのですが、妻のいる寝室に行ったみたいだったので、まぁそれならいいかと見過ごしてそのまま寝てしまいました。

その後、遠くから「ピピピピピピ…」という音が聞こえてきて「これは冷蔵庫が開けっ放しになっている音だ!」と気付き、ガバッと起きてすぐに1階に降りたのですが(寝室は2階)、そこはもう全部が水浸しとなり、長男はマットの上で泣いていました。案の定、冷蔵庫は空いており、キッチンの蛇口が引っ張れるタイプなのですが、それがシンクを超えた状態で水が出続けていました…。まさか1階全てが水浸しになっているなんて想像もしていなかったので、軽くパニックになりましたが、ひとまず水道を止め、冷蔵庫を閉めたのに、まだ音が鳴っている…。確認すると、食洗機が水漏れしているというエラーを出していました…。水がシンク下の収納のところに一杯に入っており、そこから食洗機側に流れ込んだようでした。絶望です。

まずは長男をどうにかしないといけないので、2階の寝室に連れていったら、泣き疲れたのか、すぐに寝てしまいました。本人は気楽なものです…。

この時点で朝6時半くらいで、妻を起こして二人で水をなんとかするために作業を開始。ぐしょ濡れになった衣類は洗濯かごに入り切らないし水をたっぷり吸っているので、とりあえず風呂場へ移動させ、洗面所あたりはの水は風呂に流し、リビングの水は庭に捨てるようにしました。ひたすらバスタオルに吸わせてバケツに絞り、を繰り返し、二人ともクタクタになりながら全部を片付け終わったのはお昼すぎでした。8割くらいは昼前に終わってたけれど、そこからの残りをやる気力・体力がなかなか出ませんでした。

当然ですが、床においてあった全てのものが濡れてしまいました。本やオモチャなど、使い物にならなくなったものが結構ありました。長男の学校用カバンもぐしょ濡れ。次男の幼稚園用カバンもぐしょ濡れ。たまたまいい天気の日だったので、ウッドデッキに広げて干せました。

数週間前にお風呂のシャワーを洗面所に向かって出されて洗面所が水浸しになったことがあったり、以前にも水を出しっぱなしにされたりしたこともあったせいもあり、私と妻の感想は「不幸中の幸いで、日曜日だったからよかった」でした。もし、平日だったら、仕事を休んで対応しないと床が浸水しすぎて剥がれてくるかもしれなかったですし、子どもたちも学校に行けないし(カバンが使えないから)、大混乱していたことでしょう。いやー、おさるのジョージに出てくる黄色い帽子のおじさんばりによく訓練されている…。

リフォームして油断していたというのもあるかと思うのですが、もう本当に知的障害児の育児は気が抜けません。話せないから言葉が通じないし、怒っても理解はできないでただ癇癪を起こして暴れるだけ。まぁ一応、怒られたら止めるのだけれど、私達がいなくなった隙をみてすぐにやってやろうとするので、それがもう本当に腹が立ちます。ダメ、を理解しているわけではないので、どうやったら本当にダメなことをわかってもらえるのか…。

今までの子育ての中で一番の大事件だったんじゃないかなぁと思います。マジで疲れました。もう二度とやりたくないです。こういうの、なんか保険とか使えないかなぁ…。片付けちゃったから証拠がもうないんだけれど。あ、食洗機は壊れたままです。使えないと、本当に不便です。

rom-rb(rom-sql)でマイグレーションをロールバックする

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で作ってたアプリをSinatraで作り直した

何年も前に社内向けにシンプルなアプリをRailsで作っていたのだけれど、長いことメンテナンスできずにいた。 Rubyのバージョンも古いし、Railsのバージョンも古かったので、バージョンアップをしないとなぁと思っていたのだけれど、やったところでまた数年後に同じ思いをすることになりそうだなぁ…と思ったので、もっとバージョンアップのしやすいSinatraに書き換えることにした。

実はSinatraを試したことくらいはあったけれど、ちゃんとデプロイまでしたことはなかったので、よい経験になった。

作ったもの

シンプルなWebAPIを作った。JSONを返すだけ。

使ったgem

ハマったところ

CORSをするための設定

CORSをするときに、Bearer認証を使っていたため、Authorizationヘッダーを追加しているのでpreflight requestが発生するようになり、エラーが発生していた。

Before
# 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

# 略
After

調査をした結果、以下の修正が必要だった。

# 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コマンドでsinatraの起動

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だけで済みそうでしょう。

久々にブログを書いたけれど、やっぱりメモ的な内容でもいいからアウトプットしていかないとなぁ〜と思った。管理職になってからコードを書く量がめちゃ減っているので頑張っていきたい。