💎

Ruby + AWS LambdaでDiscordの/コマンドを受け取る関数を作る話1

2024/04/19に公開

Ruby + AWS LambdaでDiscordの/コマンドを受け取る関数を作る話1

Ruby+AWS LambdaでDiscordの/コマンド(スラッシュコマンド)を受け取って、よしなな返事をするbotを作りたいのです。

ところが、RubyでAWS Lambdaを動かしたり、エンドポイント経由でDiscordのスラッシュコマンドを受け取ったりする話がほとんどありません。初心者である私には荷が重かったので、まとめておきます。

AWSでLambdaの初期設定をする

AWSで実行ロールを作り、新しくLambda関数を設定する方法については割愛します(気が向いたら書きます

実行ロールを作る

「IAM」から、「アクセス管理」の「ロール」を選択、
「ロールを作成」を選ぶと画面が遷移します。

「AWSのサービス(デフォルト)」を選択し、ユースケースは「Lambda」にします。
適切に許可ポリシーも設定します
AWSのロールの作成画面。許可ポリシーの設定します

ロール名は「Lambda_Basic_Executiion」でもなんでも構いません。
「ロールを作成」をクリックして終わりです。
ここでつけられなかった許可ポリシーも後から、後から別のロールを付け加えられるので、安心してください。

Lambda関数を作る

Lambdaを選び、「関数の作成」を選択します。
関数名はお好きに。
ランタイムに「Ruby」を選びます。執筆時点ではRuby 3.3がありましたので、それを選びます。

ここで「デフォルトの実行ロールの変更」を行います。
「既存のロールを使用する」を選び、先ほど作成した「Lambda_Basic_Executiion」などの実行ロールを選びます。

詳細設定の

  • 「関数URLを有効化」のチェックを入れて、「認証タイプ」は「NONE」にします。あとで認証メカニズムは実装します。「呼び出しモード」は「BUFFERED(デフォルト)」、「オリジン間リソース共有 (CORS) を設定」は設定しません、
  • 「VPCを有効化」はチェックを入れません。

「関数の作成」をクリックすると素敵な関数ができあがります。
関数の画面にあるhttps:から始まるURLを、DiscordのDeevloper PortalのApplicationからアプリケーションを設定する際の「INTERACTIONS ENDPOINT URL」に設定すると、Discordからメッセージが届きます。正しく認証ができるアプリケーションであることを確認するものです。

これをこれから作っていきます。

Lambda関数を実装する

色々調べましたが、Rubyで実装するLambda関数でDiscordのインタラクションを受け取る実装はほとんどなく、ライブラリ(gems)もありませんでした。唯一ED25519のgemを使ったものがありましたので、ED25519のgemを使っていきます。

ところが、この中身を見ていただければ分かりますが、C言語のコードを含んでいます。お手元の環境がLambdaの実行環境と同じとは限りませんので、以下のAWSのページに従い、セットアップしていきます。

Ruby Lambda関数の.zipファイルアーカイブの操作 - ネイティブライブラリで.zip展開パッケージを作成する

ディレクトリを作る

まずはディレクトリを作ります。ゆくゆくは別にもう一つLambda関数を作って非同期処理をさせたいので、プロジェクト名のディレクトリをまず作り、その下に今回作りたい機能の名前でもう一つディレクトリを作り、terminalでこのディレクトリの中に入ります。

最初のスクリプトを設置する

terminalで

$ touch lambda_function.rb

をして、お好きなテキストエディタで次のように編集します(Lambda関数のデフォルトの状態です)。

equire 'json'

def lambda_handler(event:, context:)
    # TODO implement
    { statusCode: 200, body: JSON.generate('Hello from Lambda!') }
end
Gemfileを作る

本来のやり方ではないようですが、
terminalで

$ touch Gemfile

して、任意のテキストエディタで開いて次のように編集しました。

source "https://rubygems.org"

gem 'ed25519'

dockerとbundleでRubyのスクリプトと参照ライブラリを同梱・圧縮し、アップロードする

bundleのインストール先ディレクトリを設定する

また、「.bundle」と言うディレクトリも作って、その中に「config」と言うファイルも次のように設置しました。(これも本来のやり方ではありません。)

---
BUNDLE_PATH: "vendor/bundle"

Dockerfileを設定する

もとのディレクトリで

$ touch Dockerfile

して、適当なテキストエディタで開き、

FROM public.ecr.aws/sam/build-ruby3.3:latest-x86_64
RUN gem update bundler 
CMD "/bin/bash"

このように設定します。

Dockerコンテナを作成する

Dockerのアプリが起動した状態で、

$ docker build -t awsruby33 .

を実行します。

Dockerコンテナを起動てライブラリ(の依存関係)を解決する

続いて、

$ docker run --rm -it -v $PWD:/var/task -w /var/task awsruby33

を実行すると、シェルの画面が新しく出てきます。

#

次を実行します。

# bundle config set --local path 'vendor/bundle' && bundle install

関数ファイルと、依存関係のあるライブラリファイル(パッケージ)を圧縮します。

# zip -r my_deployment_package.zip lambda_function.rb vendor

コンテナを終了します。

# exit

圧縮したパッケージを適用する

ディレクトリに「my_deployment_package.zip」ができていますので、これをアップロードします。

S3に一度あげたり、CUIからアップロードもできるようですが、ここではLambdaのブラウザ画面から行います。
AWSで先ほど作ったLambda関数の画面で、「コード」を選ぶと、「アップロード元」というボタンがあります。クリックするとzipファイルをアップロードできますので、アップロードしましょう。

続いて、テストボタンを押すと、特にエラーもなく実行されることがわかると思います。

Lambda関数のソースコードを編集する

AWSで先ほど作ったLambda関数の画面で、「コード」を選ぶと、「lambda_function」というタブがあります。ここに以下のコードを貼り付けます。
ED25519のgemを使った実装がwebにはありましたが、適切に動かなかったので、次のように直してあります。このコードは

  • Discordから送られてきた認証確認の要求に対して、
  • 正しい認証情報の場合も、そうでない場合にも、適切に応答する。後者の場合はエラーを出す
    ものです。
    環境変数はDiscordのDeveloper PortalのApplicationsで作るアプリケーションの画面から得られるものを、Lambda関数の画面の「環境変数」から設定します。

以下は、編集後、必ず、「File」>「Save」して、「Deploy」して、更新を待ったから、テストしたり、DicordのDeveloper Portalで「INTERACTIONS ENDPOINT URL」を実行して認証してください。

require "json"
require "ed25519"

puts "定数を読み込みます"
DC_APPLICATION_ID = ENV['DC_APPLICATION_ID']
DC_PUBLIC_KEY = ENV['DC_PUBLIC_KEY']
DC_TOKEN = ENV['DC_TOKEN']

puts "定数をフリーズします"
DC_APPLICATION_ID.freeze
DC_PUBLIC_KEY.freeze
DC_TOKEN.freeze


class Responder
  ALLOWED_CLOCK_SKEW = 10

  # コンストラクタ。DC_PUBLIC_KEYを16進文字列に変換して、VerifyKeyを得る
  def initialize
    puts 'コンストラクタが呼び出されました'
    @verification_key = Ed25519::VerifyKey.new([DC_PUBLIC_KEY].pack("H*"))
  end

  def respond(event)
    
    puts 'リクエストのbodyをJSONでパースします'
    interaction = JSON.parse(event['body'])
     
    begin
      unless verify_request(event)
        raise
      end
    rescue
        # 不適なリクエストに対して401を返す
        puts 'error: 認証エラー'
        puts '不適なリクエストなので401をセットします'
        return [401, {"Content-Type": "text/plain"}, ["invalid request signature"]]
    end
    
    begin
      if interaction["type"] == 1
        # type: 1を返す
        puts '1を返します'
        return {type: 1}
      else
        raise
      end
    rescue
      # 400を返す
      puts 'error: 認証エラー'
      puts '400を返します'
      return [400, {"Content-Type": "text/plain"}, ["Unrecognized interaction type"]]
    end
    
    
    
  end

  private

  # タイムスタンプと署名を用いてリクエストを認証する
  def verify_request(event)
    puts 'ヘッダに書かれているx-signature-timestampからタイムスタンプを取得'
    timestamp = event['headers']['x-signature-timestamp']
#    current_time = Process.clock_gettime(Process::CLOCK_REALTIME)
#    clock_skew = (current_time - timestamp.to_i).abs
#    return false if clock_skew > ALLOWED_CLOCK_SKEW

    puts 'リクエストのbodyとタイムスタンプに対して署名を得る'
    signature = event['headers']['x-signature-ed25519'] #.pack("H*")
    puts 'signature: ' + signature.to_s.force_encoding("UTF-8")
    puts 'message: ' + timestamp + event['body']
    begin
      puts '署名を確認'
      @verification_key.verify([signature].pack("H*"), timestamp + event['body'])
      
      puts '署名確認に成功したのでtrueを返す'
      true
    rescue Ed25519::VerifyError
      puts '署名確認に失敗したのでerrorを返す'
      false
    end
  end
end

def lambda_handler(event:, context:)
  puts "こんにちは!discordさん!イベントハンドラが呼ばれたよ"
  puts event.to_s
  puts event["headers"]['body']
  puts context.to_s
  Responder.new.respond(event)
end

必ず、「File」>「Save」して、「Deploy」して、更新を待ったから、テストしたり、DicordのDeveloper Portalで「INTERACTIONS ENDPOINT URL」を実行して認証してください。

これで第一段階は完了です。

ローカルのエディタのみを使って編集、アップロードする

上例ではブラウザのエディタにコードを貼り付けましたが、ローカルのみで行うことも可能です。
依存関係のあるライブラリが大きくなるなどで、ブラウザのエディタが使えなくなることがあります。
この場合、上掲のterminalでのdockerの操作の際で、

$ docker build -t awsruby33 .
$ docker run --rm -it -v $PWD:/var/task -w /var/task awsruby33

と実行したあと

# bundle config set --local path 'vendor/bundle' && bundle install
# zip -r my_deployment_package.zip lambda_function.rb vendor

としていますが、このzipコマンドのタイミングで、ローカルマシンのlambda_function.rbを参照し、圧縮しています。

ゆえに、dockerを立ち上げた状態でもlambda_function.rb の更新はできますし、dockerに(のshellに)入った状態でも更新はできます。
zipコマンドを行うことで圧縮ファイルの中身が更新されるからです。

出てきたmy_deployment_packageをつどAWS Lambdaにアップロードすれば関数の確認ができます。

コンテナを終了には次の通りにしてください。

# exit

Discussion