Tech Hotoke Blog

IT観音とは私のことです。

AWS SESの通知ステータスを環境別に振り分ける方法 #AWS #SES #Lambda #Ruby #技術メモ

これは何?

STG/PRD環境で、AWSアカウントを共有している時、AWS SESの通知ステータスを環境別に振り分ける場合の対処法の一つをメモ

前提

Tips

  • 要件

    • STG/PRD環境で、AWSアカウントを共有している
    • SESで配信されたメールの通知ステータスをSlackに通知する
    • Slackの通知先はSTG, PRDで別チャンネルにする
    • SESのID(メールアドレス)はアカウントと1:1で紐づく
    • SESの通知に設定できるSNSも1:1で紐づく
    • Slack通知のエンドポイントはシステムごとにドメインが異なるため、以下の画像のように環境別にSNSを作成する必要がある

  • 課題

    • メールが発信された環境に応じて、SNSの通知先を振り分けられない
  • どうしたか?

    1. メールヘッダに環境を識別するカスタムヘッダX-Envを設定
    2. 新規SNSを作成し、SESの通知先に設定する
    3. 新規Lambda関数を作成し、先程作成したSNSをトリガーに設定する
    4. Lambda関数の中で環境を識別するヘッダを抽出して、publishするSNSを環境別に振り分けた

  • Lambda関数サンプルコード
# frozen_string_literal: true

require 'json'
require 'aws-sdk-sns'

def handle_target_sns(event:, context:)
// 中略
 env = custom_headers_value['X-Env']

  sns_topic_arn = sns_topic_arn(env)

  sns = Aws::SNS::Client.new(region: 'ap-northeast-1')
  
  sns.publish(topic_arn: sns_topic_arn, message: event.to_json)

  { statusCode: 200, body: JSON.generate('Message published to SNS') }

end

def sns_topic_arn(env)
  case env
  when 'staging'
    ENV.fetch('STG_SNS_TOPIC_ARN', nil)
  when 'production'
    ENV.fetch('PRD_SNS_TOPIC_ARN', nil)
  else
    raise "Unknown environment: #{env}"
  end
end

#Lambdaレイヤー導入メモ #Ruby #AWS

これは何?

Lambdaのレイヤーを使用したので、操作手順などの備忘録

前提

Tips

  • Lambdaレイヤーはどんなときに使う?
  • デプロイパッケージのサイズを小さくするため。関数の依存関係を関数から切り離せるので。

  • コア関数ロジックを依存関係から分離するため。

  • 複数の関数間で依存関係を共有するため。今回の目的はこれに該当する。

  • Lambda コンソールのコードエディターを使用するため。

依存関係を分離するのは、今のところ楽にはなってもしんどくなることはほとんどなかったので積極的に切り分けていきたい。

  • 依存関係管理のためのレイヤーの使用を示す空のサンプルアプリケーションを用意してくれている。優しい。

github.com

  • 関数にレイヤーを追加すると、Lambda はレイヤーのコンテンツをその実行環境の /optディレクトリに読み込みます。例えば、アップロードするフォルダのディレクトリ構造がruby/3.2/* だったら、環境変数GEM_PATH /opt/ruby/3.2.0 を設定する必要があります。

  • レイヤーには、1 つまたは複数のレイヤーバージョンを含めることができる。

  • 既存のレイヤーバージョンの権限はいつでも変更することができる。

  • ただし、コードを更新したり、その他の構成を変更したりするには、新しいバージョンのレイヤーを作成する必要があります。

  • レイヤーのアップロードにはS3からもアップロードできる。

レイヤーを作成し設定する

  • ローカル環境でGemfileを作成し、bundle install を実行する(bundle install --path vendor/bundle でvendorディレクトリの配下に作成した)
  • bundle install の成果物をzipする(zip -r ../ruby_layer.zip .)
  • zip ファイルをアップロードする(手動)
  • Lambda関数にレイヤーの設定を行う(レイヤーはバージョン管理されるので、バージョンが更新されている場合はバージョンの指定を忘れずに。ここの更新忘れでハマりそう。。。)
  • 環境変数GEM_PATHを設定する(zipファイルのディレクトリ名もちゃんと確認すること)

#RSpec Mock: 引数に応じて返り値を変える方法 #Ruby

これは何?

RSpecを書くときの小技メモ。

前提

Tips

  • RSpecで特定のメソッドをMockする
  • Mockするメソッドはテストの中で流用されている
  • 引数などの条件によって返り値を変えたい
  • どうする?

  • A:

    • receiveメソッドにはブロックが渡せるので、ブロックの中で条件分岐させる
  before do
                allow(hoge).to receive(:hogehoge) do |h|
                  if h.dig(:criteria, :ids)
                    # idsが含まれる場合、空の配列を返す        
                    [{ id: 'PUsR1qvef3', start_date: Date.today }]
                  elsif h.dig(:criteria, :codes) 
                    # codesが含まれる場合、空の配列を返す
                    []
                  end
                end
              end

備考

  • 引数に差はないけど返り値を制御したい場合などはどうしよう

スッキリ!Rubyコード改善術: おしゃれにHashを追加してみた #Ruby #Ruby on Rails #Rails

これは何?

条件によってHashにプロパティを追加したい場合に、一行一行追加する記述が冗長だったので、もう少しRubyっぽく書けないか施工錯誤したメモ。 結論はtapを使うといい感じだった。(サンプルの例がダサいコードの方がスッキリ見えてしまい申し訳ないですが...)

前提

Tips

  • tapはself を引数としてブロックを評価し、self を返します。メソッドチェインの途中で直ちに操作結果を表示するためにメソッドチェインに "入り込む" ことが、このメソッドの主目的です。 とあります。この説明だけではピンときていませんでした。
  • しかし、今回のようなケースで使うと、ブロックを渡すことができるため、条件分岐などのロジックを含めてオブジェクトを操作することができて柔軟にコードが書けるという気付きを得ました。
  • Hashオブジェクト自身にチェーンして記述するのでコードが一つの塊として読めるのも個人的にはわかりやすくていいなと感じました。Hashのプロパティ追加操作がHash本体と離れているとコードを読む時にストレスに感じるので。

Object#tap (Ruby 3.3 リファレンスマニュアル)

  • ダサいコード
  def hoge(flag)
    hash = {  fuga: false  }

  // その他の処理
  // その他の処理
  // その他の処理
  // その他の処理

   hash[:piyo] = "Piyo" if flag
   hash[:fuga] = "Fuga" if flag
   hash[:hogehoge] = "hogehoge" if flag
  end
  • おしゃれなコード
  def hoge(flag)
    {
      fuga: false
    }.tap do |hash|
      if flag
        hash[:piyo] = "Piyo"
        hash[:fuga] = "Fuga"
        hash[:hogehoge] = "hogehoge"
      end
    end
  end

備考

  • 実際のところ、どちらが良いのだろう? 好み?

デバッグ方法:AWS ECSタスク更新時のトラブルシューティング

これは何?

AWS ECSのタスクがデプロイ時に正常に更新されなかった場合のデバッグ方法のメモ

前提

AWS Pipeline, CodeBuild, ECSを使用していること

Tips

  • Pipeline・CodeBuildともにステータスに異常はない
  • ECSのタスクを確認すると、タスクが更新されていない
  • どうする?

  • A:

    • イベントタグを確認する
    • イベントタグのログにdeployment failed: tasks failed to start. 的な文言が出ている。
    • 直下にfailしたタスクのIDリンクが存在するので、そちらに遷移
    • 遷移先のページに画像のような原因が書かれている(今回はパラメータストアに設定したパスとデプロイされたコードの参照しているパスが異なっていたことが原因だった)

備考

  • ECSタスクが更新されなかったら、Slackなどに通知するみたいなことができるか調べてみる

useEffect × ReactQueryを使った、処理実行順序の制御

これは何?

カスタムフックの処理が実行される前に、useEffectの処理を先に行いたいケースが出てきた。その際にすぐに解決方法がわからなかったので備忘録として書く。

前提

  • React18
  • TypeScript: 4.7.4
  • ReactQuery
  • ReactHookForm

Tips

  • ケース

    • 画面遷移時にuseFuga()を使用してPOSTリクエストが走る。
    • useFuga()はReactQueryで実装されたカスタムフック。
    • POSTリクエストの前に、POSTするdataの値を更新したい。
    • しかし、useEffect()はカスタムフックの後に実行される。
    • どうする???
  • A:

    • useStateを作成
    • useEffectの中でstateの更新
    • stateの値をReactQueryのconfig:{enabled: }に設定する
    • これで、POSTするdataの値を更新した後にカスタムフックの呼び出しを行うことができる。
サンプルコード
  const [isUpdated,  setIsUpdated] = useState(false)

  useEffect(() => {
    const fields = getValues('hoge')
    const updatedFields = fields.map((field, index) => ({
      ...field,
      position: index + 1
    }))

    setValue('hogeAttributes', updatedFields)
    setIsUpdated(true)
  }, [])

  const { data, isLoading, isError, isSuccess } =
    useFuga({
      params: {
        data: getValues()
      },
      config: {
        enabled: isSetPinned
      }
    })

AWS Lambda 入門

これは何?

AWS Lamdaを触ることになった。とりあえず、なんとなくの雰囲気で実装してみたところ、かなり初歩の段階でエラーが発生した。 この機会なので、学習を兼ねて、まずはちゃんとドキュメントを読んでからエラーの解消にあたって行こうと思い書いてます。

前提

  • Rubyで実装しているので、Rubyベースで。
  • コードは実務で使っているものになってしまうので、内容を改変・抜粋して掲載しています。(コピペでは動かないと思ってください)
  • 公式リファレンスを網羅しているわけではありません。
  • AWS SESでLambdaを使用することを前提にしています

AWS Lambdaって何?

  • AWS Lambda はサーバーのプロビジョニングや管理をする必要がなく、コードを実行できるコンピューティングサービスです。
    • いわゆるサーバーレスアーキテクチャというやつですかね。Lambdaの他に、グーグルの「Google Cloud Functions」、IBMの「OpenWhisk」、マイクロソフトの「Azure Functions」などが挙げられますが、定義がそれぞれ異なるため一概にサーバーレスという言葉でくくることができないのが注意。
    • AWSにおけるサーバーレスは「インスタンスベースの仮想サーバー(EC2など)を使わずにアプリケーションを開発するアーキテクチャ」と個人的には解釈しています(間違っていたらご指摘ください)
    • 必要な場合にのみ関数を実行する
    • 自動的にスケーリング
    • 従量課金
    • フルマネージドサービス(コードを実行するメモリのバランス、CPU、ネットワーク、その他のリソースを提供するコンピューティングフリート)
    • コンピューティングインスタンスにログインしたり、提供されたランタイムのオペレーティングシステムをカスタマイズしたりすることはできない → コンピュートリソースを管理する必要がある場合は向かない

Lambdaの概念

  • 関数:Lambda でコードを実行するために呼び出すことができるリソース -トリガー:Lambda 関数を呼び出すリソースまたは設定
  • イベント:処理する Lambda 関数のデータを含む JSON 形式のドキュメント。ランタイムによりオブジェクトに変換された上で、関数のコードに渡される。
  • ランタイム環境:関数の実行に必要なプロセスとリソースが、実行環境により管理される。
  • 命令セットアーキテクチャ:Lambda が関数の実行に使用するコンピュータプロセッサタイプ。2024/02/27時点では、arm64かx86_64が選択できる。
  • レイヤー:追加のコードまたはデータを含むことができる .zip ファイルアーカイブ。レイヤーには、ライブラリ、 カスタムランタイム 、データ、または設定ファイルを含めることができる。

    • 各関数につき 最大 5 つ のレイヤーを含めることができる
    • 関数にレイヤーを含むと、実行環境においてコンテンツが /opt ディレクトリに抽出される。
    • デフォルトでは、作成したレイヤーは AWS アカウントに対してプライベートになる。
    • コンテナイメージとしてデプロイされた関数はレイヤーを使用しない。代わりに、コンテナイメージをビルドする際、必要なランタイム、ライブラリ、およびその他の依存関係を、そのイメージ内にパッケージ化する。
  • 拡張機能

    • 関数を拡張できる。
      • 例) 任意のモニタリングツール、オブザーバビリティツール、セキュリティツール、およびガバナンスツールに関数を統合できます。
    • AWSか提供するツールセットが存在する。カスタムで作成も可能。
    • 拡張機能は内部モードと外部モード、2つのタイプに分かれるらしい。
    • 内部拡張機能は、ランタイムプロセスで実行され、ランタイムと同じライフサイクルを共有する。
    • 外部拡張機能は、実行環境で別のプロセスとして実行される。外部拡張機能は、関数が呼び出される前に初期化される。また、関数のランタイムと並行して実行され、関数の呼び出しが完了した後も引き続き実行される。
  • 同時実行

    • ある時点で関数が処理しているリクエストの数。
    • リクエストの処理中に関数が再度呼び出されると、別のインスタンスがプロビジョンされるため、関数の同時実行数が増加する。
  • エイリアス

    • 関数を呼び出したり表示したりするときに、バージョンまたはエイリアスを指定するための修飾子を含めることができる。
    • バージョンは、数値修飾子を持つ関数のコードと設定の変更不可能なスナップショット。たとえば、my-function:1 と指定できる。
  • 送信先

    • Lambda が非同期呼び出しからイベントを送信できる AWS リソース。
    • 処理に失敗したイベントの送信先を設定できます。一部のサービスでは、正常に処理されたイベントの宛先もサポートしている。

今回発生したエラーと対応

{
    "errorMessage": "cannot load such file -- lambda_function",
    "errorType": "Init<LoadError>",
    "stackTrace": [
        "<internal:/var/lang/lib/ruby/site_ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:86:in `require'",
        "<internal:/var/lang/lib/ruby/site_ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:86:in `require'"
    ]
}

lambda_functionが存在しないとのことでしたが、Handler設定でデフォルトがlambda_function として設定されていました。 私のコードではルートディレクトリにhandler.rbファイルを作成し、notifyメソッドを定義していたため、添付画像のような形式になるのが正でした。

AWS SESからどんなデータが渡ってくるのか?

{
  "Records": [
    {
      "eventVersion": "1.0",
      "ses": {
        "mail": {
          "commonHeaders": {
            "from": [
              "Jane Doe <janedoe@example.com>"
            ],
            "to": [
              "johndoe@example.com"
            ],
            "returnPath": "janedoe@example.com",
            "messageId": "<0123456789example.com>",
            "date": "Wed, 7 Oct 2015 12:34:56 -0700",
            "subject": "Test Subject"
          },
          "source": "janedoe@example.com",
          "timestamp": "1970-01-01T00:00:00.000Z",
          "destination": [
            "johndoe@example.com"
          ],
          "headers": [
            {
              "name": "Return-Path",
              "value": "<janedoe@example.com>"
            },
            {
              "name": "Received",
              "value": "from mailer.example.com (mailer.example.com [203.0.113.1]) by inbound-smtp.us-west-2.amazonaws.com with SMTP id o3vrnil0e2ic for johndoe@example.com; Wed, 07 Oct 2015 12:34:56 +0000 (UTC)"
            },
            {
              "name": "DKIM-Signature",
              "value": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=example; h=mime-version:from:date:message-id:subject:to:content-type; bh=jX3F0bCAI7sIbkHyy3mLYO28ieDQz2R0P8HwQkklFj4=; b=sQwJ+LMe9RjkesGu+vqU56asvMhrLRRYrWCbV"
            },
            {
              "name": "MIME-Version",
              "value": "1.0"
            },
            {
              "name": "From",
              "value": "Jane Doe <janedoe@example.com>"
            },
            {
              "name": "Date",
              "value": "Wed, 7 Oct 2015 12:34:56 -0700"
            },
            {
              "name": "Message-ID",
              "value": "<0123456789example.com>"
            },
            {
              "name": "Subject",
              "value": "Test Subject"
            },
            {
              "name": "To",
              "value": "johndoe@example.com"
            },
            {
              "name": "Content-Type",
              "value": "text/plain; charset=UTF-8"
            }
          ],
          "headersTruncated": false,
          "messageId": "o3vrnil0e2ic28tr"
        },
        "receipt": {
          "recipients": [
            "johndoe@example.com"
          ],
          "timestamp": "1970-01-01T00:00:00.000Z",
          "spamVerdict": {
            "status": "PASS"
          },
          "dkimVerdict": {
            "status": "PASS"
          },
          "processingTimeMillis": 574,
          "action": {
            "type": "Lambda",
            "invocationType": "Event",
            "functionArn": "arn:aws:lambda:us-west-2:111122223333:function:Example"
          },
          "spfVerdict": {
            "status": "PASS"
          },
          "virusVerdict": {
            "status": "PASS"
          }
        }
      },
      "eventSource": "aws:ses"
    }
  ]
}

参考: Amazon SES で AWS Lambda を使用する - AWS Lambda

AWS Lambdaにレイヤーを作成する

coming soon...