タケユー・ウェブ日報

Ruby on Rails や Flutter といったWeb・モバイルアプリ技術を武器にお客様のビジネス立ち上げを支援する、タケユー・ウェブ株式会社の技術ブログです。

AWS SDK for Ruby でアップロード用の署名付きURLを生成する方法

s3_resource = Aws::S3::Resource.new
object = s3_resource.bucket(BUCKET_NAME).object(OBJECT_KEY)
url = URI.parse(object.presigned_url(:put))

ちなみにcurl コマンドでアップロードするには

$ curl -i -X PUT --upload-file README.md "<PRESIGNED_URL>"

HTTP/1.1 200 OK
x-amz-id-2: FdLlF7a48jiq8Q7mM/VdAe8ZmcNfCcSKTIry5/LEZNUhizUb1+ALaNNoWjB/ZRLpD+ZtnUDqjlA=
x-amz-request-id: 61D6A2090B5A471E
Date: Mon, 07 Dec 2020 04:37:30 GMT
x-amz-version-id: DQ_QAWEZz600VtEtZJdGvZqZdiT4NzBS
ETag: "0cfbd649b94d4dc64447b516ec65f1a9"
Content-Length: 0
Server: AmazonS3

Adobe XD + VSCode + Flutter でデザイントークンを開発者に共有する

先日VSCodeAdobe XDプラグインが公開され、Adobe XDで作成したプロトタイプをコーディングに利用することが可能になりました。 利用までの手順と、実際に何ができて何ができないのか?試してみたので記録しておきます。

coliss.com

letsxd.com

Adobe XD + VSCode でできること

Adobe XD + VSCode でできないこと

続きを読む

RubyでFirebase Authenticationトークンを検証するサンプル

認証をFirebase Authenticationに丸投げし、そちらで作ったトークンをサーバーに送信、認可を行う時に有効

FIREBASE_PROJECT_ID = "FIREBASEPROJECTID"
CIRTIFICATE_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'
EXP_LEEWAY = 30.seconds
VALID_ISS = "https://securetoken.google.com/#{FIREBASE_PROJECT_ID}"

CERTIFICATE_MAP = JSON.parse(Net::HTTP.get_response(URI.parse(CIRTIFICATE_URL)).body)

TokenVerifyFailed = Class.new(StandardError)
InvalidAuthTime = Class.new(TokenVerifyFailed)

def from_firebase(id_token)
  # https://firebase.google.com/docs/auth/admin/verify-id-tokens?hl=ja
  a, decoded_token_header = JWT.decode(id_token, nil, false)

  uri = URI.parse(CIRTIFICATE_URL)
  certificate = CERTIFICATE_MAP.fetch(decoded_token_header["kid"])

  public_key = OpenSSL::X509::Certificate.new(certificate).public_key
  decoded_token_payload, _ = JWT.decode(
    id_token,
    public_key,
    true,
    exp_leeway: EXP_LEEWAY,   # 有効期限の検証をするが、ゆるめに。 EXP_LEEWAY 秒は大目に見る。
    verify_iat: true,         # 発行時の検証をする
    aud: FIREBASE_PROJECT_ID,
    verify_aud: true,         # 対象の検証をする
    iss: VALID_ISS,
    verify_iss: true,         # 発行元の検証をする
    verify_sub: true,         # 件名の存在を検証する
    algorithm: decoded_token_header["alg"]
  )

  raise InvalidAuthTime.new('Invalid auth_time') unless Time.zone.at(decoded_token_payload['auth_time']).past?

  # decoded_token_payload = {
  #   "name"=>"Takeuchi Yuichi",
  #   "picture"=>"https://lh3.googleusercontent.com/a-/AAuE7mAZU7Rh7lIFStzfWGe3tC24qDIX4UIoEWR8426flA",
  #   "iss"=>"https://securetoken.google.com/rails-firebase-sample",
  #   "aud"=>"rails-firebase-sample",
  #   "auth_time"=>1580712233,
  #   "user_id"=>"Qgk3sd1HgoPLVbSy8uXAWnRmWmx1",
  #   "sub"=>"Qgk3sd1HgoPLVbSy8uXAWnRmWmx1",
  #   "iat"=>1580712233,
  #   "exp"=>1580715833,
  #   "email"=>"yuichi.takeuchi@takeyuweb.co.jp",
  #   "email_verified"=>true,
  #   "firebase"=>{
  #     "identities"=>{
  #       "google.com"=>["100008179958237311525"],
  #       "email"=>["yuichi.takeuchi@takeyuweb.co.jp"]
  #     },
  #     "sign_in_provider"=>"google.com"
  #   }
  # }

  decoded_token_payload
end

Railsにおける利用例

例えばこんな感じで、

  • Bearer トークンとして受け取って、対応する User モデルを作る/返す
  • セッションに記録されたものがあればそれを使う

みたいなことができる

  def current_user
    return @current_user if defined?(@current_user)

    # fallback
    if session[:authorization].present? && request.headers['Authorization'].blank?
      request.headers['Authorization'] = "Bearer #{session[:authorization]}";
    end

    user = authenticate_with_http_token do |id_token, options|
      User.from_firebase(id_token)
    rescue JWT::DecodeError, User::TokenVerifyFailed => e
      Rails.logger.error(e)
      nil
    end
    if user&.persisted?
      @current_user = user
    else
      @current_user = nil
    end
  end

graphql-ruby + Multiple Databases with ActiveRecord

Query では読み込み専用 、Mutation では読み書きを使うようにしたかった。

github.com

上記のコメントで動くのだが、テストでトランザクションを使ったロールバック戦略を採用していると、レプリカに変更が伝搬されずに失敗する点に注意。(関連:Rails 6.0の複数DBでリードレプリカのテストするのたぶん大変

module Tracers
  class DatabaseRoleTracer
    EVENT_NAME = 'execute_multiplex'.freeze

    def trace(event, data)
      # テストではテストケースごとにトランザクションをロールバックするためコミットされずレプリカに伝搬しない
      # ので、やむなくテストモードでは無効=プライマリに繋ぐ
      if event == EVENT_NAME && !Rails.env.test?
        multiplex = data[:multiplex]

        role = multiplex.queries.all?(&:query?) ?
          ActiveRecord::Base.reading_role :
          ActiveRecord::Base.writing_role

        Rails.logger.debug("[#{self.class.name}] ActiveRecord::Base.connected_to(role: #{role.inspect})")
        ActiveRecord::Base.connected_to(role: role) do
          yield
        end
      else
        yield
      end
    end
  end
end

class MyAppSchema < GraphQL::Schema
  mutation(Types::MutationType)
  query(Types::QueryType)

  # 省略

  # Query か Mutation かで接続先データベースを変更する
  tracer Tracers::DatabaseRoleTracer.new
end

railsguides.jp

graphql-ruby.org

ActiveRecordでid以外によるアソシエーションを設定する

foreign_key:primary_key: オプションを利用する。

# == Schema Information
#
# Table name: twitter_accounts
#
#  id                                 :bigint(8)        not null, primary key
#  screen_name                        :string(255)      not null
#
# Indexes
#
#  index_twitter_accounts_on_screen_name  (screen_name) UNIQUE
#
class TwitterAccount < ActiveRecord
  validates :screen_name, presence: true, uniqueness: true
  has_many :users, foreign_key: :twitter_screen_name, primary_key: :screen_name
end

# == Schema Information
#
# Table name: users
#
#  id                                 :bigint(8)        not null, primary key
#  twitter_screen_name                :string(255)
class User < ActiveRecord
end
twitter_account = TwitterAccount.first
twitter_account.users.to_sql #=> "SELECT `users`.`id`, `users`.`twitter_screen_name` FROM `users` WHERE `users`.`twitter_screen_name` = 'takeyuweb'"

コレクションキャッシュでキャッシュのキーをカスタマイズする方法

コレクションキャッシュについてコードリーディングを行ったところ、キャッシュキーのカスタマイズ方法がわかったのでメモ。

この記事のまとめ

Railsガイドには書いてありませんが、:cached に callable なオブジェクトを渡すと、その結果をキャッシュキーとして使ってくれます。

# cached: true と同じ
<%= render partial: 'posts/post', collection: @posts, cached: -> (post) { post } %>
# posts.comments の結果に変更があった場合も違うキーになる
<%= render partial: 'posts/post', collection: @posts, cached: -> (post) { [post, post.comments] } %>
# locals はキャシュキーに影響しないため、locals によってキーを変えるときは自分で指定してやる必要がある
<%= render partial: 'posts/post', collection: @posts, locals: { flag: true }, cached: -> (post) { [{flag: true}, post] } %>

github.com

コレクションキャッシュ

一覧などを表示する際に便利な :collection オプションは :cached オプションを渡すことで、各アイテムの描画でキャッシュを使ってくれます。

たとえば

<%= render partial: 'posts/post', collection: @posts, cached: true %>

とすると、ログは次のようになります。

Rendered collection of posts/_post.html.erb [0 / 10 cache hits] (Duration: 1733.3ms | Allocations: 2034040)
Rendered collection of posts/_post.html.erb [10 / 10 cache hits] (Duration: 1.2ms | Allocations: 546)

キャッシュのキー

キャッシュのキーは、ビューファイルのパス、内容のハッシュ値、コレクションアイテム(今回の場合Postインスタンスオブジェクト)による配列となります。(6.0.3現在)

# キャッシュのキー
[:views, "posts/_post:88c97eec416c8a7c8aaa37acb5edbbbd", #<Post id: 123, ... >]

rails/collection_caching.rb at b378bda17d710eb08949771390e7b7d77ee2b39d · rails/rails · GitHub

なお、配列のキーの場合、Railsのキャッシュ機構は配列要素から文字列のキーとキャッシュバージョンを取り出して使用します。(5.2から) 今回の場合、 Post#cache_version #=> updated_at から作った文字列 がキャッシュバージョンに含まれるため、 Post を更新すると新しいキャッシュとなります。

キャッシュのキーを変更したいケース

上記のような標準のキャッシュキーではうまくキャッシュを破棄できない場合があります。

コレクションアイテムの依存先の変更で描画内容を変化させたい

次のような Post#comments によって描画内容が変化するテンプレートの場合

<!-- posts/_post.html.erb -->
<h3><%= post.title %></h3>
<p><%= post.comments.count %> comments</p>

post.comments.count が変わってもにキャッシュキーが変化しないため、

<%= render partial: 'posts/post', collection: @posts, cached: true %>

ではキャッシュが入れ替わりません。

これを避けるためには、次のどちらかがよく採用されると思います。

  1. comments が変更される度に、 postupdated_at を更新してキャッシュバージョンを変化させる( post.touch
  2. キャッシュキーに comments も含める

1の方法はコールバックなどの仕組みが必要で、モデルの管理が難しくなるので、私は2の方が使いやすいと思います。

コレクションアイテム以外の入力で描画内容が変化するとき

次の例は、人気の記事と新着記事で同一のテンプレートを使い、一方では人気順のバッジを表示する、というものです。

<h2>人気の記事<h2>
<%= render partial: 'posts/post', collection: @popular_posts, locals: { show_rank_badge: true }, cache: true %>
<h2>新着記事</h2>
<%= render partial: 'posts/post', collection: @recent_posts, locals: { show_rank_badge: false }, cache: true %>

このような場合、たとえば @popular_posts@recent_posts に同じ記事が含まれていた場合、新着記事の表示欄でもバッジ付きの表示になってしまいます。 これを避けるためには、それぞれ違うキャッシュキーになるようにしなければいけません。

キャッシュのキーを変更する方法

ActionView::CollectionCaching のコードを確認すると次のようにしてキーを変更することができました。(Rails 6.0.3)

<%= render partial: 'posts/post', collection: @posts, cached: -> (post) { [post, post.comments] } %>
<%= render partial: 'posts/post', collection: @posts, locals: { show_rank_badge: true }, cached: -> (post) { [post, { show_rank_badge: true }] } %>

github.com

cron の代わり CloudWatch Events + Lambda + RunCommand でタスク実行を冗長化するCloudFormationテンプレート

f:id:uzuki05:20200814014902p:plain

前の記事では、実行したいコマンドをCloudWatch Events のターゲットのパラメータとして、JSONで流し込む方式で作りました。

blog.takeyuweb.co.jp

しかしながら、実行するコマンドという危険な内容を外部から受け取る点に、怖さがありました。また、JSON特殊文字を含むコマンドを指定できない問題がありました。 また、この方法だと複数のタスクを同一のステートマシン/Lambda関数で処理するため、ログが混ざってしまい、デバッグしづらい問題がありました。

そこで、これらの問題を解消するため、次のように変更を加えたCloudFormationテンプレートを作成しました。

  • タスクごとに別々のスタックを作成し、タスクごとに別々のステートマシン/Lambda関数で実行する
  • コマンドをLambda関数の環境変数として与える
続きを読む