【AWS】Route 53をDNSサービスとして使用するサブドメインの作成
Amazon Route 53にてドメインを管理している状態で、ドメインを管理しているアカウントとは別のアカウントでサブドメインを作成し、ACMにてSSL証明書を発行したい場合、あれ?どうやるんだったけ?と忘れることがあるので、備忘録として残しておく。
概要
定義
- 親ドメインアカウント:Route 53にてドメインを管理しているアカウント
- アプリ開発アカウント:サブドメインを作成し、ACMにてSSL証明書を発行したいアカウント
- hogehoge.com:Route 53にて管理しているドメイン
- sample-api.hogehoge.com:作成したいサブドメイン
今回、ドメインをアプリ開発アカウントに移行することなく、親ドメインアカウントでドメインを管理しつつも、アプリ開発アカウントでサブドメインを作成し、ACMにてSSL証明書を発行できるようにする。
サブドメイン作成
アプリ開発アカウントのRoute 53にてサブドメインを作成する。
「ホストゾーンの作成」ボタンから以下の設定でホストゾーンを作成する。
ホストゾーンを作成すると、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検証が完了するのを待つのみ。数分経過すると、「発行済み」のステータスになる。
以降は、通常の設定と同様である。
ロードバランサーの作成
- Application Load Balancer(ALB)(HTTP / HTTPS)を選択する
- リスナー「ロードバランサーのプロトコル」に、"HTTPS"を追加する
- セキュリティ設定で、SSL証明書を登録する
- ターゲットの登録で、使用するEC2インスタンスを登録する
- 使用するEC2インスタンスのセキュリティグループに、HTTPを許可するルールを追加し、ソースには作成したELBのセキュリティグループを指定する。
Route53 ロードバランサー紐付け
サブドメインに、以下のレコードを追加する
- レコード名:(空欄のまま)
- レコードタイプ:A - IPv4アドレスと一部のAWSリソースにトラフィックをルーティングします。
- エイリアスを「有効」にする
- トラフィックのルーティング先:「Application Load Balancer と Classic Load Balancerへのエイリアス」→ ALBのリージョンを選択 → 作成したALBを選択する
まとめ
無事、ドメインをアプリ開発アカウントに移行することなく、親ドメインアカウントでドメインを管理したまま、アプリ開発アカウントでサブドメインを作成し、ACMにてSSL証明書を発行できるようにすることができた。
何度かやっているのに、毎回忘れて調べているので、メモとして残しておく。
参考記事
【Rails】Faraday2.0を使う
Railsアプリケーションで外部APIを使う場合に、導入していた Faraday。v1
からv2
にアップデートしており、保守性を考慮して少し仕様が変わっていた。Faradayの使い方を備忘録として残しておく。
- Ruby:v3.0.1
- Ruby on Rails : v6.1.3.2
- Faraday : v2.3.0
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
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
conn = Faraday.new(...) do |conn| conn.request :authorization, :basic, 'username', 'password' end
Basicのユーザー名とパスワードを自動的にBase64エンコードしてくれる。
Instrumentation Middleware
ログに関して、Logger Middlewareで標準出力にログを出力することができる。しかしながら、ログの整形が好みでなかったので、Instrumentation Middleware
を使うようにした。
ActiveSupport::Notifications.instrument
(ActiveSupportが提供する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の先頭にはスラッシュを入れない
- 先頭にスラッシュを入れると、絶対パスと認識され、Faraday Connectionで設定したベースURLがドメイン部分のみ適応される。
- https://base-url.dom/api/v1としていても、ドメイン以下の
api/v1
は勝手に破棄されてしまう。 - 先頭にスラッシュがない場合、相対パスとして認識される。
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 ~ end
とraise
で処理を明示的に書くようにした。
Windows10のWSL2上のUbuntu20.04でCH340 USBシリアルモジュールを操作する
Windows10のWSL2上のUbuntu20.04で、USBシリアルモジュールを操作しようとした際に、少し手間取ったので備忘録として残しておく。
CH340とは
CH340は中国の南京沁恒微电子股份有限公司(WCH)で開発された、USBシリアル変換チップである。 低価格であるが、通信性能が良いらしい。
ドライバーはWindows10では認識して、自動的にドライバーを入れてくれるのでドライバーのインストールは不要。もし認識されなかった場合は、以下からダウンロード可能である。
WSL2 + Ubuntu-20.04インストール
usbipd-win インストールおよび設定
動作試験
- Ubuntu-20.04を起動する
【管理者Windows PowerShell】起動確認
PS C:\Windows\system32> wsl -l -v NAME STATE VERSION * Ubuntu-20.04 Running 2 Ubuntu Stopped 2
Ubuntu-20.04の起動を確認できた
【管理者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'.
【Ubuntu】Ubuntuで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
が認識されていることを確認できた【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)モジュールに対して信号を送り、モジュールが信号通りに動作することを確認できた。
【管理者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タスクを実行する方法を検討したので備忘録として残しておく。
- Ruby:v3.0.1
- Ruby on Rails : v6.1.3.2
Rakeタスクとは
Rake とはRuby製のビルドプログラム(UNIXのビルドプログラム"Make"のようなビルドプログラム)で、プログラム実行を”タスク”という単位でまとめて扱うことができる。
Rake is a Make-like program implemented in Ruby. Tasks and dependencies are specified in standard Ruby syntax.
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にはrails
とrake
という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タスクを実行する」パターンは緊急対応用で、利用頻度が多くないと想定しているのでこのまま進めることにした。
参考記事
【Rails】camelCase⇆snake_caseの変換
今回は久しぶりのRailsアプリケーション開発。
社内の一部業務の自動化のために簡単なアプリケーションを前任者から引継ぎ、一部改良を行った。構成はバックエンドがRuby on Railsで、フロントエンドがNuxt.jsの構成だ。
インターフェースとしてAPIを実装する中で、「jsonデータのkeyに関して、Rails側ではsnake_case、フロント側ではcamelCaseで扱いたい」と思った。フロント側で$axiosのmiddlewareでsnake_case⇆camelCaseの変換を行うことも考えたが、今回Rails側で対応を行った。その方法を備忘録として残しておく。
- Ruby:v3.0.1
- Ruby on Rails : v6.1.3.2
- active_model_serializers:v0.10.12
命名規則に関して
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 では、ブロック変数を用いるブロック渡しより、&:メソッド名渡しの方が高速だと説明されている。)
&
はProc coercion演算子とも呼ばれ、SymbolオブジェクトをProcオブジェクトに変換する。そのために、Symbol#to_procが呼び出される。- 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の使い方
- gem追加
gem 'active_model_serializers'
- モデル生成
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
- 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は、RailsでAPIを実装する際にとても便利なGemであるように思えた。この便利なGemのお陰で、爆速でAPIが開発できるように感じた。
もっと他にいい方法があれば、是非教えてください。よろしくお願いします。