7839

雑草魂エンジニアブログ

【AWS】API Gatewayをリバースプロキシサーバーとして使用する

とあるAPIを別ドメインのURLで使用したいという話があり、試験的にAPI Gatewayをただ単にリバースプロキシサーバーとして使用してみたので、備忘録として残しておく。

概要

  • https://aaa.com/api:クライアントがアクセスするURL
  • https://bbb.com/api:実際に使いたいAPIのURL

実際に使いたいAPIのURLhttps://bbb.com/apibbb.comドメインではなく、aaa.comドメインAPIを使えるようにする。

Amazon API Gatewayとは

Amazon API Gateway は、あらゆる規模の REST、HTTP、および WebSocket API を作成、公開、維持、モニタリング、およびセキュア化するための AWS のサービスです。

システム構成

今回は上記のようなシステムを構成する。Route 53へのドメインの登録、ACMでのSSL証明書の発行に関しては、説明を省略し、すでに登録および発行がされていることを前提として、API Gatewayの設定のみを説明する。

APIの作成

  • APIタイプを選択
    • 「HTTP API」を選択して構築
  • STEP1:APIの作成

    • 統合:「統合を追加」を押す HTTP 統合を選択し、API Gateway は指定した URL にリクエストを送信し、その URL からのレスポンスで応答するようにする。
      • 統合種別:HTTP
      • メソッド:ANY
      • URLエンドポイント:https://bbb.com
    • API名:API-Reverse-Proxy(任意の名前を設定)

  • STEP2:ルートを設定(操作不要。確認のみ)

    • メソッド:(選択不可)
    • リソースパス:$default
    • 統合ターゲット:ANY https://bbb.com

    HTTP全てのメソッド・全てのパスに対して、https://bbb.comにリクエストを送信する。その場合、パスに$defaultを設定する。(API ごとに「$default」ルートを 1 つ指定することもできる。「$default」ルートは、API へのリクエストと一致するルートが他に存在しない場合に呼び出される。)

  • STEP3:ステージを定義(操作不要。確認のみ)

    • ステージ名:$default

    個別設定などは不要なので、ステージの追加はしない

  • STEP4:確認して作成

カスタムドメイン名の設定

カスタムドメインの作成

  • ドメイン名の詳細
    • ドメイン名:aaa.com
    • TLSの最小バージョン:TLS 1.2(推奨)
    • 相互 TLS 認証:無効
      相互 TLS 認証では、API実行時にクライアント証明書が必要になり、双方向のTLSによる認証によりセキュリティが強化される。(IoT機器などでのユースケースがある。)
  • エンドポイント設定
    • エンドタイプ:リージョン
    • ACM 証明書:ドメイン名に設定したACMを選択する
  • タグ:(必要であれば追加)

作成すると、ドメイン名が正常に作成される。

API Gateway ドメイン名」は後ほどRoute 53に登録する際に必要となるので、確認だけしておく。(Route 53では選択するのみなので、リージョンの確認だけしておけば問題なし。)

API マッピングの設定

API マッピングを設定」から「新しいマッピングを追加」する。

  • API:先ほど作成した「API-Reverse-Proxy」を選択
  • ステージ:$defaultしかないので、$defaultを選択
  • パス(オプション):空欄 パスを設定することで、特定のパスに対するAPIやステージの設定が可能となる

Route 53でAliasレコードを作成

カスタムドメインに追加したドメインのRoute 53のホストゾーンにAliasレコードを追加する。

デフォルトエンドポイントの無効化

現時点の設定では、以下の2つのURLからAPIを呼び出すことができる

  1. 設定したカスタムドメイン
  2. API Gatewayデプロイ時に自動生成されるデフォルトエンドポイント

  3. のデフォルトエンドポイントを無効化する。

APIの詳細を編集から、デフォルトのエンドポイントを「有効」から「無効」に変更する。

まとめ

Amazon API Gatewayは初めて使ってみたが、設定もわかりやすく、非常に簡単であった。いろいろな箇所で応用できそうであると思ったので、今度はLambdaと連携するなどしていきたいと思えた。

金額感的にも、HTTP APIであれば、最初の3億回/月までは1.00USDとかなり割安であるように思えた。

【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