Amplifyで作成したLambdaからアクセスキーを用いてSESでメール送信する

Amplifyで作成したアプリケーションでメール送信をしようと思い、Lambda経由でSESを呼んでみた。

メール送信用のIAMを作成してアクセスキーを使うパターンと、LambdaにSESでのメール送信権限を付与するパターン、両方試したが、まずはアクセスキーを用いるパターンをメモ。

環境

Amplify CLI v8.0.2。

パッケージマネージャーにはYarn、AWS SDK for JavaScriptはv3を使用。

フロントエンドの実装にはTypeScriptを使用。

実装

メール送信用のIAMの作成や、SESのドメインの検証は省略。アクセスキーIDとシークレットアクセスキーを確認しておく。

方法はこちらを参照。

Lambdaの作成

amplify add function で、メール送信用のLambdaを作成する。関数名は sendMail 、言語はNode.jsを指定した。

amplify add function

> Lambda function (serverless function)

? Provide an AWS Lambda function name: sendMail

? Choose the runtime that you want to use: (Use arrow keys)
> NodeJS

? Choose the function template that you want to use: (Use arrow keys)
> Hello World

? Do you want to configure advanced settings? Yes
? Do you want to access other resources in this project from your Lambda function? Yes

? Select the categories you want this function to have access to. (Press <space> to select, <a> to toggle all, <i> to in
>(*) api

? Select the operations you want to permit on amplifyexample (Press <space> to select, <a> to toggle all, <i> to invert
selection)
>(*) Mutation

? Do you want to invoke this function on a recurring schedule? No
? Do you want to enable Lambda layers for this function? No
? Do you want to configure environment variables for this function? No
? Do you want to configure secret values this function can access? Yes
? Enter a secret name (this is the key used to look up the secret value): AWS_SES_ACCESS_KEY_ID
? Enter the value for AWS_SES_ACCESS_KEY_ID: [input is hidden]
? What do you want to do? Add a secret
? Enter a secret name (this is the key used to look up the secret value): AWS_SES_SECRET_ACCESS_KEY
? Enter the value for AWS_SES_SECRET_ACCESS_KEY: [input is hidden]
? What do you want to do? I'm done

Select the categories you want this function to have access toAPIを選択しているが、これは不要だったかも。

Do you want to configure secret values this function can access にて、シークレットを指定できるため、そこでアクセスキーIDとシークレットアクセスキーを指定した。

シークレットはAWS Systems Managerのパラメータストアに、 /amplify/<AmplifyアプリID>/<環境名>/AMPLIFY_<関数名>_<シークレット名> という名前の、SecureStringで保存される。

これで amplify/backend/function/<関数名> というディレクトリが生成される。

Lambdaの実装

Systems ManagerおよびSESを使うので、作成されたディレクトリ配下のsrcディレクトリに、AWS SDKをインストールする。

cd amplify/backend/function/sendMail/src
yarn add @aws-sdk/client-ssm @aws-sdk/client-ses

Lambdaの内容は、 amplify/backend/function/<関数名>/src/index.js に記述する。

GraphQL の引数は、 handler関数の引数.arguments に設定されるので、メール送信に必要な情報を取得するようにした。

// ファイル先頭の Amplify Params コメントは省略

const { SSMClient, GetParametersCommand } = require('@aws-sdk/client-ssm')
const { SESClient, SendEmailCommand } = require('@aws-sdk/client-ses')

/**
 * @type {import('@types/aws-lambda').APIGatewayProxyHandler}
 */
exports.handler = async event => {
  // シークレットの取得
  const ssmClient = new SSMClient()
  const ssmGetParametersCommand = new GetParametersCommand({
    Names: ['AWS_SES_ACCESS_KEY_ID', 'AWS_SES_SECRET_ACCESS_KEY'].map(
      secretName => process.env[secretName],
    ),
    WithDecryption: true,
  })

  const { Parameters } = await ssmClient.send(ssmGetParametersCommand)

  // SESクライアントの作成
  const sesClient = new SESClient({
    region: 'ap-northeast-1',
    credentials: {
      accessKeyId: Parameters[0].Value,
      secretAccessKey: Parameters[1].Value,
    },
  })

  const params = event.arguments
  const sendEmailCommandInput = {
    Source: params.from,
    Destination: {
      ToAddresses: params.to,
      CcAddresses: params.cc,
      BccAddresses: params.bcc,
    },
    Message: {
      Subject: { Data: params.subject },
      Body: {
        Text: {
          Data: params.body,
        },
      },
    },
  }

  const sendEmailCommand = new SendEmailCommand(sendEmailCommandInput)
  const sendEmailCommandOutput = await sesClient.send(sendEmailCommand)

  // メッセージIDを返す
  return {
    statusCode: 200,
    body: sendEmailCommandOutput.MessageId,
  }
}

実装が終わったら、 amplify mock function <関数名> で動作確認できる。

以下のような event.json でメール送信できることを確認。

{
  "arguments": {
    "subject": "mail subject",
    "body": "mail body",
    "from": "from@example.com",
    "to": ["to@example.com"],
    "cc": [],
    "bcc": []
  }
}

amplify function push で、AWS側に反映。

GraphQL定義

schema.graphqltype Mutation に、Lambdaの呼び出しを記述する。

Lambda関数は、<関数名>-<環境名> のように、環境ごとに作成されるため、 @functionname関数名-${env} のように指定する。

type Mutation {
  sendMail(
    subject: String!
    body: String!
    from: String!
    to: [String!]!
    cc: [String!]
    bcc: [String!]
  ): String
    @function(name: "sendMail-${env}")
    @auth(rules: [{ allow: private }])
}

amplify api push で、AWS側に反映。また、push時にファイル定義の更新を行うと、 graphql/mutations.tssendMailAPI.tsSendMailMutationVariables が追加される

GraphQL定義の注意点

GraphQLではデフォルト値を指定できるため、当初、from: String = "from@example.com" のように記述していた。

だが、これだとLambdaから event.arguments.from のように値を参照した際、 from 未指定だと null になっていたため、デフォルト値は無視される模様。

コードから呼び出し

APIのpushで追加されたインターフェースを使用して、呼び出しを実装する。

Cognitoでログインしているユーザーにメールを送りたい場合、以下のようになる。

import { Amplify, API, Auth, graphqlOperation } from 'aws-amplify'
import { sendMail } from 'graphql/mutations'
import { SendMailMutationVariables } from 'API'

const sendMail = async () => {
  const currentUser = await Auth.currentAuthenticatedUser()
  const sendMailParams: SendMailMutationVariables = {
    subject: 'subject',
    body: 'body',
    to: [currentUser.attributes.email],
  }

  return await API.graphql(
    graphqlOperation(sendMail, sendMailParams),
  )
}

振り返り

ひとまず実装できた。Amplifyに含まれない仕組みは、Lambdaを使って何とかするのが想定されているのだろうか?

シークレットを指定した場合の動きを確認したかったのでアクセスキーを使用したが、実際にはLambdaにSESでのメール送信権限を付与するパターンで実装したので、そちらもまとめる予定。