7839

雑草魂エンジニアブログ

【AWS】Route 53をDNSサービスとして使用するサブドメインの作成

Amazon Route 53にてドメインを管理している状態で、ドメインを管理しているアカウントとは別のアカウントでサブドメインを作成し、ACMにてSSL証明書を発行したい場合、あれ?どうやるんだったけ?と忘れることがあるので、備忘録として残しておく。

概要

定義

今回、ドメインアプリ開発アカウントに移行することなく、親ドメインアカウントでドメインを管理しつつも、アプリ開発アカウントでサブドメインを作成し、ACMにてSSL証明書を発行できるようにする。

サブドメイン作成

アプリ開発アカウントのRoute 53にてサブドメインを作成する。

ホストゾーンの作成」ボタンから以下の設定でホストゾーンを作成する。

  • ドメイン名:sample-api.hogehoge.com(サブドメイン名を入力)
  • 説明:(使用用途などを明記)
  • タイプ:パブリックホストゾーン(default)
  • タグ:(必要であれば追加)

ホストゾーンを作成すると、NSレコードとSOAレコードが作成される。 NSレコードを親ドメインアカウント側に追加して更新する必要があるので、NSレコードをコピーしておく。

サブドメインのネームサーバーレコードでDNS サービスを更新する

ドメインアカウントのRoute 53にて、hogehoge.comのホストゾーンにサブドメインのネームサーバーレコードを追加する。

  • レコード名:sample-api(hogehoge.com)
  • レコードタイプ:NS-ホストゾーンのネームサーバー
  • 値:サブドメインのNSレコード
  • TTL :300(default)
  • ルーティングポリシー:シンプルルーティング(default)

ACMから証明書発行

アプリ開発アカウントAWS Certificate Manager(ACM)にて証明書の発行をリクエストする。

  • 証明書タイプ:パブリック証明書をリクエス
  • ドメイン名:sample-api.hogehoge.com(サブドメイン名を入力)
  • 検証方法を選択:DNS検証 - 推奨(default)
  • タグ:(必要であれば追加)

作成した証明書の詳細を確認すると、ステータスは「保留中の検証」となっている。

「Route 53 でレコードを作成」→「レコードを作成」を実行する。これにより、Route 53のサブドメインに、CNAMEレコードが追加される。

あとは、DNS検証が完了するのを待つのみ。数分経過すると、「発行済み」のステータスになる。

以降は、通常の設定と同様である。

ロードバランサーの作成

  1. Application Load Balancer(ALB)(HTTP / HTTPS)を選択する
  2. リスナー「ロードバランサープロトコル」に、"HTTPS"を追加する
  3. セキュリティ設定で、SSL証明書を登録する
    • 証明書タイプ:ACM から証明書を選択する (推奨)
    • 証明書の名前:「ACMで発行した証明書」を選択する
  4. ターゲットの登録で、使用するEC2インスタンスを登録する
  5. 使用するEC2インスタンスのセキュリティグループに、HTTPを許可するルールを追加し、ソースには作成したELBのセキュリティグループを指定する。

Route53 ロードバランサー紐付け

サブドメインに、以下のレコードを追加する

  • レコード名:(空欄のまま)
  • レコードタイプ:A - IPv4アドレスと一部のAWSリソースにトラフィックをルーティングします。
  • エイリアスを「有効」にする
  • トラフィックのルーティング先:「Application Load Balancer と Classic Load Balancerへのエイリアス」→ ALBのリージョンを選択 → 作成したALBを選択する

まとめ

無事、ドメインアプリ開発アカウントに移行することなく、親ドメインアカウントでドメインを管理したまま、アプリ開発アカウントでサブドメインを作成し、ACMにてSSL証明書を発行できるようにすることができた。

何度かやっているのに、毎回忘れて調べているので、メモとして残しておく。

参考記事

docs.aws.amazon.com

【Rails】Faraday2.0を使う

Railsアプリケーションで外部APIを使う場合に、導入していた Faraday。v1からv2にアップデートしており、保守性を考慮して少し仕様が変わっていた。Faradayの使い方を備忘録として残しておく。

Faradayとは

Faraday is an HTTP client library that provides a common interface over many adapters (such as Net::HTTP) and embraces the concept of Rack middleware when processing the request/response cycle.

  • HTTP クライアントライブラリである
  • Net::HTTP やその他の多くのアダプターに対して共通のインターフェースを提供する
    • Faraday 2.0より、net_httpのみを標準サポート
    • それ以外のアダプタはGemfileの追加が必要
  • Rack のようにミドルウェアの設定が可能

利用シーンとしては、Railsアプリケーションで外部のAPIなどにアクセスする場合などに用いられる。

v1からv2への更新内容は以下に記載されている。

インストール

gem 'faraday'
$ bundle install

faraday_middlewareもv1では利用していたが、v2では非推奨になった。
net_httpを使う場合、faradayのみで十分になっているように思えた。

Faraday Connection

外部のAPIを使う場合、 Faraday::Connection オブジェクトを生成することが推奨されている。 理由としては以下が挙げられる。

  • 共通のリクエストヘッダーの設定が可能
    • 'Content-Type' = 'application/json'の設定
    • authorizationの設定
  • APIのベースURLの設定が可能
  • アダプタやミドルウェアの設定が可能
    • loggerの設定

APIに関して基本的にJSONファイルでやり取りし、Bearer認証の場合、以下のミドルウェアの設定になる。

conn = Faraday.new('https://base-url.com/api/v1') do |conn|
  conn.request :json
  conn.response :json, parser_options: { symbolize_names: true }
  conn.authorization :Bearer, 'authentication-token'
  conn.request :instrumentation
end

JSON Request Middleware

conn = Faraday.new(...) do |conn|
  conn.request :json
end
  • Faraday::Request#bodyハッシュ(Key/Valueペア)をJSONリクエストボディに変換する
  • Content-Typeヘッダを自動的にapplication/jsonに設定する

JSON Response Middleware

conn = Faraday.new(...) do |conn|
  conn.response :json, **options
end
  • JSONのレスポンスボディをKey/Valueペアのハッシュにパースする
    • JSON.parseのoptionsを設定可能
    • ハッシュのキーを文字列ではなくシンボルにしたい場合
      conn.response :json, parser_options: { symbolize_names: true }
    

Authentication Middleware

Bearer認証

conn = Faraday.new(...) do |conn|
  conn.request :authorization, 'Bearer', 'authentication-token'
end

Basic認証

conn = Faraday.new(...) do |conn|
  conn.request :authorization, :basic, 'username', 'password'
end

Basicのユーザー名とパスワードを自動的にBase64エンコードしてくれる。

Instrumentation Middleware

ログに関して、Logger Middlewareで標準出力にログを出力することができる。しかしながら、ログの整形が好みでなかったので、Instrumentation Middlewareを使うようにした。

ActiveSupport::Notifications.instrumentActiveSupportが提供するInstrumentation API)を利用してイベントを発行している。

conn = Faraday.new(...) do |conn|
  conn.request :instrumentation, name: 'custom_name', instrumenter: MyInstrumenter
  ...
end

そのイベントをActiveSupport::Notifications.subscribeを使って、サブスクライブすることでデータを取得することができ、ログを好きなように整形して吐き出すことができる。

ActiveSupport::Notifications.subscribe('request.faraday') do |name, starts, ends, _, env|
  url = env[:url]
  http_method = env[:method].to_s.upcase
  request_body = env[:response].env.request_body
  duration = ends - starts
  response_body = env[:response].body
  status_logger = ActiveSupport::Logger.new('log/api-connection.log', 'daily')
  status_logger.formatter = proc do |severity, time, progname, msg|
    "[%s#%d] %5s -- %s: %s\n" % [format_datetime(time), $$, severity, progname, msg.force_encoding("UTF-8")]
  end
  status_logger.level = Rails.logger.level
  status_logger.info "#{env[:status]} #{http_method} '#{url}' #{request_body} #{duration}seconds data: #{response_body}"
end

日本語文字化け対応で、force_encoding("UTF-8")追加している。

GET,POST,PUT,PATCH,DELETE

  • get(url, params = nil, headers = nil)
  • post(url, body = nil, headers = nil)
  • put(url, body = nil, headers = nil)
  • patch(url, body = nil, headers = nil)
  • delete(url, params = nil, headers = nil)
response = connection.get('users', {
  page: 1,
  perPage: 10  
})
# => GET https://base-url.com/api/v1/users?page=1&perPage=10

response = connection.post('item', {
  name: 'hoge',
  price: 100
})
# => POST https://base-url.com/api/v1/item

if response.success?
  response.body[:id]
end

注意事項

  • urlの先頭にはスラッシュを入れない

POST 'application/x-www-form-urlencoded'

上記で作成したFaraday ConnectionはJSON形式なので、application/x-www-form-urlencodedにする場合は、以下のようにconn.request :url_encodedを指定する。

connection = Faraday.new('https://base-url.com/api/v1') do |conn|
  conn.request :url_encoded
  conn.response :json, parser_options: { symbolize_names: true }
  conn.authorization :Bearer, 'authentication-token'
  conn.request :instrumentation
end
response = connection.post('item', {
  name: 'hoge',
  price: 100
})

Tokenを上書きする場合

Tokenを上書きする場合は、以下のように直接上書きすればいい。

connection.headers['Authorization'] = "Bearer #{token}"

まとめ

改めて、FaradayのDocumentを読んでみて、とても便利だと思えた。Raise Error Middlewareも確かに便利であるが、モジュールの中ではなく、begin ~ rescue ~ endraiseで処理を明示的に書くようにした。

Windows10のWSL2上のUbuntu20.04でCH340 USBシリアルモジュールを操作する

Windows10のWSL2上のUbuntu20.04で、USBシリアルモジュールを操作しようとした際に、少し手間取ったので備忘録として残しておく。

CH340とは

CH340は中国の南京沁恒微电子股份有限公司(WCH)で開発された、USBシリアル変換チップである。 低価格であるが、通信性能が良いらしい。

ドライバーはWindows10では認識して、自動的にドライバーを入れてくれるのでドライバーのインストールは不要。もし認識されなかった場合は、以下からダウンロード可能である。

WSL2 + Ubuntu-20.04インストール

usbipd-win インストールおよび設定

動作試験

  1. Ubuntu-20.04を起動する
  2. 【管理者Windows PowerShell】起動確認

     PS C:\Windows\system32> wsl -l -v
       NAME            STATE           VERSION
     * Ubuntu-20.04    Running         2
       Ubuntu          Stopped         2
    

    Ubuntu-20.04の起動を確認できた

  3. 【管理者Windows PowerShell】USB(CH340)モジュールをPCに接続し、認識されているUSBを確認し、USB(CH340)モジュールをWSLにアタッチする

     PS C:\Windows\system32> usbipd list
     Connected:
     BUSID  DEVICE                                                        STATE
     2-1    Realtek USB 2.0 Card Reader                                   Not shared
     2-4    HD Webcam                                                     Not shared
     3-4    インテル(R) ワイヤレス Bluetooth(R)                                Not shared
     5-2    USB-SERIAL CH340 (COM7)                                       Shared
    
     Persisted:
     GUID                                  DEVICE
    
     PS C:\Windows\system32> usbipd wsl attach --busid 5-2
     usbipd: info: Using default distribution 'Ubuntu-20.04'.
    
  4. UbuntuUbuntuでUSBが認識できているか確認する

     serip39@DESKTOP-P8SJAGB:~$ lsusb
     Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
     Bus 001 Device 003: ID 1a86:7523 QinHeng Electronics HL-340 USB-Serial adapter
     Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
    

    QinHeng Electronics HL-340 USB-Serial adapterが認識されていることを確認できた

  5. Ubuntu】/dev/ttyUSB0にアクセスする際に、(アクセス権限の設定が必要だったため)今回はroot権限で実行する

     serip39@DESKTOP-P8SJAGB:~$ sudo su
     root@DESKTOP-P8SJAGB:/home/serip39# stty -F /dev/ttyUSB0 115200 | cat /dev/ttyUSB0 &
     [1] 4813
     root@DESKTOP-P8SJAGB:/home/serip39# echo "0x5a\0x00\0xff\0xfe\0xa0\0x05" | xxd -r > /dev/ttyUSB0
     root@DESKTOP-P8SJAGB:/home/serip39# echo "0x5a\0x00\0xff\0xfe\0xa0\0x06" | xxd -r > /dev/ttyUSB0
    

    USB(CH340)モジュールに対して信号を送り、モジュールが信号通りに動作することを確認できた。

  6. 【管理者Windows PowerShell】WSLからデタッチする

     PS C:\Windows\system32> usbipd wsl detach --busid 5-2
     PS C:\Windows\system32> usbipd list
     Connected:
     BUSID  DEVICE                                                        STATE
     2-1    Realtek USB 2.0 Card Reader                                   Not shared
     2-4    HD Webcam                                                     Not shared
     3-4    インテル(R) ワイヤレス Bluetooth(R)                           Not shared
     5-2    USB-SERIAL CH340 (COM7)                                       Shared
    
     Persisted:
     GUID                                  DEVICE
    

まとめ

Windows10のWSL2上のUbuntu20.04から、CH340 USBシリアルモジュールを操作することができた。

【Rails】ControllerからRakeタスクを実行する

バックエンド側で定期実行しているRakeタスクをフロントエンド側から任意のタイミングで実行したくなった。そのために、RailsアプリケーションのControllerからRakeタスクを実行する方法を検討したので備忘録として残しておく。

Rakeタスクとは

Rake とはRuby製のビルドプログラム(UNIXのビルドプログラム"Make"のようなビルドプログラム)で、プログラム実行を”タスク”という単位でまとめて扱うことができる。

Rake is a Make-like program implemented in Ruby. Tasks and dependencies are specified in standard Ruby syntax.

github.com

RailsにおけるRakeタスクの使い方としては、wheneverというGemと組み合わせて定期実行したい処理を定義したり、他にも様々な用途で利用することができる。

今回、私も定期実行でIoTデバイスの監視を実施したくてwheneverとRakeタスクの組み合わせでシステムを構築した。

Rakeタスク使い方

Rakeタスクファイルの作成

rails g コマンドで「devices」という名のrakeタスクを作成する。

$ rails g task devices

lib/tasks以下のディレクトリにdevices.rakeというタスクファイルが生成される。

Rakeタスクの定義

基本的な実装

namespace :タスクのファイル名 do
  desc 'タスクの説明'
  task :タスクの名称 do
    # タスクの処理内容
  end
end
namespace :devices do
  desc '全てのデバイスのステータス確認'
  task :all_check => :environment do
    Rails.logger.info("device all check")
    devices = Device.all
    devices.each do |device|
      device.check()
    end
  end

  desc '任意のデバイスのステータス確認(id指定)'
  task :check, ['device_id'] => :environment do |_, args|
    Rails.logger.info("device check, device_id = #{args.device_id}")
    target_device = Device.find(args.device_id)
    target_device.check()
  end
end
  • RakeタスクでActiveRecordを扱う場合、taskに:environmentオプションをつける必要がある
  • Rakeタスクで引数を複数受け取る場合、スペースは開けずにカンマ区切り
    • NG : rake sample_task:execute[arg1, arg2, arg3]
    • OK : rake sample_task:execute[arg1,arg2,arg3]
    • 少しハマったので注意!!
  • Rakeタスクから別のタスクを呼び出す場合、invoke(*args)で実行する

    Rake::Task['sample_task:execute'].invoke(*args)
    # ex. Rake::Task['devices:check'].invoke(39)
    

Rakeタスクの実行

Railsにはrailsrakeという2つのコマンドがあるが、Rails 5以降はrailsコマンドで全て実行できるように統一されている。(Rails 4以下は、コマンドの種類によって2つのコマンドを使い分ける必要があるので注意。Rails 5以降でも、rakeコマンドはもちろん実行できる。)

Rakeタスクの一覧表示

$ rails -T
# ~省略~
rails devices:all_check                # 全てのデバイスのステータス確認
rails devices:check[schedule_id]       # 任意のデバイスのステータス確認(id指定)

Rakeタスクの実行

$ rails devices:all_check
$ rails devices:check[39]

ControllerからRakeタスクを実行する

いよいよ今回の本題である。

Controllerでフロントエンドからのリクエストを受け付けて、Rakeタスクを実行する。IoTデバイスのステータスを確認するために、実際のデバイスのステータスを取得したり、整合性の確認をしたりするので最大30秒ほどかかってしまう。複数台のステータスを確認する場合、デバイスの台数分だけ処理に時間がかかってしまう。そのため、リクエストを受け付けて、Rakeタスクを発行したら、処理の完了を待たずにレスポンスを返せるようにしないと、504 (Gateway Time-out)が出てしまう。

試験的にタスクの処理にsleep 30を追加した。

namespace :devices do
  desc '任意のデバイスのステータス確認(id指定)'
  task :check, ['device_id'] => :environment do |_, args|
    Rails.logger.info("device check, device_id = #{args.device_id}")
    target_device = Device.find(args.device_id)
    sleep 30 #試験的に追加
  end
end

%記法を使ってコマンド出力

コマンドを出力する方法は以下が挙げられる。

  • system("cmd")
  • %x("cmd")
  • Kernel#exec("cmd")

今回は、%記法を用いて、バックグラウンド実行になるように末尾に&を入れてみた。

class DevicesController < ApplicationController
  def update
    %x(rake device:check[#{params[:id]}] &)
    render status: 200, json: schedule
  end
end

しかしながら、%x("cmd")はサブシェルで実行したコマンド結果が返り値となり、プロセスの終了を待ち合わせてしまった。

実際に実行してみると、案の定、504 (Gateway Time-out)が出てしまった。

spawn

一時的に別スレッドを立ち上げて実行できないかと探していた時にspawnを見つけた。spawnは子プロセスを起動するメソッドであり、引数で渡したコマンドを実行することができる。また、親プロセスは生成した子プロセスの終了を待ち合わせない。

class DevicesController < ApplicationController
  def update
    spawn("rake device:check[#{params[:id]}]")
    render status: 200, json: schedule
  end
end

実際に実行してみると、APIリクエストを送信してから即座にレスポンスが返ってきた。そして、Rakeタスクも実行された。

まとめ

無事に「ControllerからRakeタスクを実行する」ことができた。この利用パターンはかなりイレギュラーな気がしている。

また、正直同時に大量のプロセスを立ち上げると、メモリを圧迫しすぎてしまうことがあるので安易に使うことはできないと思えた。ただ、今回の「ControllerからRakeタスクを実行する」パターンは緊急対応用で、利用頻度が多くないと想定しているのでこのまま進めることにした。

参考記事

stackoverflow.com

【Rails】camelCase⇆snake_caseの変換

今回は久しぶりのRailsアプリケーション開発。
社内の一部業務の自動化のために簡単なアプリケーションを前任者から引継ぎ、一部改良を行った。構成はバックエンドがRuby on Railsで、フロントエンドがNuxt.jsの構成だ。

インターフェースとしてAPIを実装する中で、「jsonデータのkeyに関して、Rails側ではsnake_case、フロント側ではcamelCaseで扱いたい」と思った。フロント側で$axiosのmiddlewareでsnake_case⇆camelCaseの変換を行うことも考えたが、今回Rails側で対応を行った。その方法を備忘録として残しておく。

命名規則に関して

Ruby命名規則

復習も兼ねて、Rubyスタイルガイドから命名規則について列挙しておく。

  • シンボル、メソッド、変数にはsnake_caseを用いる
    • 文字と数字を分離しない(×:var_10, ○:var10)
    • 述語(boolean値が返る)メソッドは疑問符で終わる(ex.empty?)
    • 危険な可能性(破壊的な変更)のあるメソッドは感嘆符で終わる(ex.update!)
  • クラスやモジュールにはCamelCaseを用いる
  • 定数はSCREAMING_SNAKE_CASEを用いる

JavaScript命名規則

復習も兼ねて、Google JavaScript Style Guideから命名規則について列挙しておく。

  • メソッド、変数には(先頭小文字の)camelCaseを用いる
  • クラスには(先頭大文字の)CamelCaseを用いる
  • 定数はSCREAMING_SNAKE_CASEを用いる

RequestのkeysをcamelCaseからsnake_caseへ変換

Requestに関しては、before_actionでparamsを変換するように、以下の処理を追加した。

class ApplicationController < ActionController::API
  before_action :snake2camel_params

  def snake2camel_params
    params.deep_transform_keys!(&:underscore)
  end
end

deep_transform_keys!は、ハッシュに対して、ブロック内で変換されたキーを含むハッシュに変換してくれる破壊的メソッドである。ソースコードは以下のようになっている。

class Hash
  # 〜省略〜
  # Destructively converts all keys by using the block operation.
  # This includes the keys from the root hash and from all
  # nested hashes and arrays.
  def deep_transform_keys!(&block)
    _deep_transform_keys_in_object!(self, &block)
  end
  # 〜省略〜
  private
  # Support methods for deep transforming nested hashes and arrays.
    def _deep_transform_keys_in_object!(object, &block)
      case object
      when Hash
        object.keys.each do |key|
          value = object.delete(key)
          object[yield(key)] = _deep_transform_keys_in_object!(value, &block)
        end
        object
      when Array
        object.map! { |e| _deep_transform_keys_in_object!(e, &block) }
      else
        object
      end
    end
end

underscoreでkeyをcamelCaseからsnake_caseへ変換している。ソースコードは以下のようになっている。

class String
  # 〜省略〜
  def underscore
    ActiveSupport::Inflector.underscore(self)
  end
end
module ActiveSupport
  module Inflector
    # 〜省略〜
    def underscore(camel_cased_word)
      return camel_cased_word unless /[A-Z-]|::/.match?(camel_cased_word)
      word = camel_cased_word.to_s.gsub("::", "/")
      word.gsub!(inflections.acronyms_underscore_regex) { "#{$1 && '_' }#{$2.downcase}" }
      word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
      word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
      word.tr!("-", "_")
      word.downcase!
      word
    end
  end
end

実装方法に関して

&:underscoreはブロック変数ではなく、& + Symbolオブジェクトを用いている。(Use fast ruby idioms by oniofchaos · Pull Request #32337 では、ブロック変数を用いるブロック渡しより、&:メソッド名渡しの方が高速だと説明されている。)

  1. &はProc coercion演算子とも呼ばれ、SymbolオブジェクトをProcオブジェクトに変換する。そのために、Symbol#to_procが呼び出される。
  2. Symbol#to_procでは、ブロック引数に対してSymbolオブジェクトのメソッドが呼び出される。
params.deep_transform_keys!(&:underscore)
# 同じ実装である
params.deep_transform_keys!{|key| key.underscore}

(復習)RubyのSymbolとは何か?

Rubyの内部実装では、メソッド名や変数名、定数名、クラス名などの`名前'を整数で管理しています。これは名前を直接文字列として処理するよりも速度面で有利だからです。そしてその整数をRubyのコード上で表現したものがシンボルです。

Responseのkeysをsnake_caseからcamelCaseへ変換

jsonデータへの整形も含め、Active Model Serializersを用いた。その場合、initializersに以下のファイルを追加するだけでsnake_caseからcamelCaseへ変換することができた。

ActiveModelSerializers.config.key_transform = :camel_lower

公式ドキュメントは以下を参考にして欲しい。

ActiveModelSerializersの使い方

  1. gem追加
gem 'active_model_serializers'
  1. モデル生成
rails g serializer <-ModelName->
ex. rails g serializer User
class UserSerializer < ActiveModel::Serializer
  attributes :id, :name, :email
  attribute: :password, if: -> {instance_options[:password]}
  has_many :posts
  has_many :tasks
end
  1. controllerにシリアライザーを指定
class UsersController < ApplicationController
  def index
    users = User.all.includes(:posts)
    render json: users,
                each_serializer: UserSerializer, #複数の場合、each_serializer
                include: ['posts'] #アソシエーションを指定
  end

  def show
    users = User.find(params[:id])
    render json: users,
                serializer: UserSerializer, #単数の場合、serializer
                password: true #オプション指定
  end
end

アソシエーションを指定する場合、N+1問題が発生する場合があるので、includesメソッドを使うなど対応が必要となる。 また、アソシエーションを指定することでActiveModelSerializers側でクエリを発行することもできるのでパフォーマンスなどはよく確認する必要があるように思えた。

まとめ

ActiveModelSerializersは、RailsAPIを実装する際にとても便利なGemであるように思えた。この便利なGemのお陰で、爆速でAPIが開発できるように感じた。

もっと他にいい方法があれば、是非教えてください。よろしくお願いします。