🦔

Amplify Custom AWS resources (CDK)でSNSを使ってメール送信

2022/12/08に公開

概要

  • Amplify標準ではSNSはないのでamplify customで拡張します
  • Amplifyで作れば環境ごとにリソースを作ってくれます
  • 呼び出しはAmplify Functions経由になりがちです

この記事を書いたときのライブラリのバージョン
Amplify CLI 10.5.1
aws-amplify ^5.0.4

リソースの受け渡し方法

Custom Resourceの情報をどうやって受け渡すかが肝です。
以前はcdk-stack.tsでCfnOutputしたものがbackend-config.jsonを設定することで読み込めたようですが、現在はこの方法は取れません。
また公式でparameters.jsonの書き換えについての言及もあったようですが、削除されています。
2,3か月前までは
https://github.com/aws-amplify/amplify-cli/issues/9087#issuecomment-1262155500
が有効でした。
この方法だけではうまくいかず、一度 amplify env checkoutで環境をチェックアウトする必要があります。
また@10.5.1ではAmplify functionsではparameters.jsonは作られないので自分で作る必要があります。
ちょっと前にプロダクトでできるようになってこれはいったん整理しておかないとわからなくなるなと思っていましたが、またAmplifyが進化したようです。

Amplify add xxxでAWSのリソースを爆速で使えるAmplifyですが、SNSやSQSといったサービスには対応していません。
そのためCustom AWS resourcesで追加する必要があります。

❌ amplify add sns
⭕ amplify add custom

環境を作る(Vue.js X amplify init)

最初にVuetifyでVue.jsプロジェクトを作ります。

yarn create vuetify
yarn create v1.22.19
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...

success Installed "create-vuetify@1.0.3" with binaries:
      - create-vuetify
[#######################################] 39/39
Vuetify.js - Material Component Framework for Vue

✔ Project name: … amplify-sns
✔ Use TypeScript? … No / Yes
✔ Would you like to install dependencies with yarn, npm, or pnpm? › yarn

◌ Generating scaffold...
◌ Installing dependencies with yarn...

yarn install v1.22.19
info No lockfile found.
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
Done in 30.48s.

amplify-sns has been generated at /app/amplify-sns

Discord community: https://community.vuetifyjs.com
Github: https://github.com/vuetifyjs/vuetify
Support Vuetify: https://github.com/sponsors/johnleider
Done in 47.59s.

続いてamplify initをします。

cd amplify-sns
amplify init  
Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project amplifysns

設定はデフォルトのままでOKです。
App Typeがjavascriptになっていますが、問題ありません。
2022/12/02のAmplifyはtypescriptの選択肢がありません。

AWS Cloud Development Kit (AWS CDK)

Custom AWS resourcesはCDKまたはCloudFormationで作ります。
今回はCDKを使います。

amplify add custom

続いてCDKかCloudFormationの選択になります。

? How do you want to define this custom resource? …  (Use arrow keys or type to filter)
❯ AWS CDK
  AWS CloudFormation

あとはナビゲーションに従っていくとソースコード画面になります。

Provide a name for your custom resource ‣ customResourcef7bcd2ee

ちなみに↑のリソース名はなるべく小さくわかりやすい名称にしましょう。
デフォルトの名称のようにcustomResourcef7bcd2eeだとあとから大変です。
私はSnsTopicとしました。
以下の手順でリソースを追加します。

  1. package.jsonに必要なライブラリを入れます。今回はインストール済みです。(例:@aws-cdk/lambda)
  2. cdk-stack.tsを編集します。import 文を忘れずに@aws-cdk/aws-sns,@aws-cdk/aws-sns-subscriptions
  3. amplify build を実行してNPM依存ファイルを入れたスタックを作ります。
  4. amplify publish で環境にリソースを作ります。
  5. リソースにアクセスする手段構築。今回はAmplify functions

サンプルコード

amplify/backend/custom/<resource-name>/cdk-stack.ts
import * as cdk from '@aws-cdk/core';
import * as AmplifyHelpers from '@aws-amplify/cli-extensibility-helper';
import { AmplifyDependentResourcesAttributes } from '../../types/amplify-dependent-resources-ref';
//import * as iam from '@aws-cdk/aws-iam';
import * as sns from '@aws-cdk/aws-sns';
import * as subs from '@aws-cdk/aws-sns-subscriptions';
//import * as sqs from '@aws-cdk/aws-sqs';

export class cdkStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps, amplifyResourceProps?: AmplifyHelpers.AmplifyResourceProps) {
    super(scope, id, props);
    /* Do not remove - Amplify CLI automatically injects the current deployment environment in this input parameter */
    new cdk.CfnParameter(this, 'env', {
      type: 'String',
      description: 'Current Amplify CLI env name',
    });
    /**
    *↑ここまではテンプレです。import 文以外は触ることがないです。
    *↓以下を解説します。
    */
    // projectNameを取得します。
    const { projectName } = AmplifyHelpers.getProjectInfo();
    // topicNameにproject名と環境名をつけて
    const topic = new sns.Topic(this, 'sns-topic', {
      topicName: `sns-topic-${projectName}-${cdk.Fn.ref('env')}`
    });
    // メールアドレスを設定します。
    topic.addSubscription(new subs.EmailSubscription("<your-email-address>"));
    // toplicArnを書き出す
    new cdk.CfnOutput(this, 'TopicArn', {
      value: topic.topicArn,
      description: 'Arn of Amazon SNS Topic',
    });
  }
}

AmplifyHelpers.getProjectInfo()でプロジェクトの情報を取得できます。
tsを見る限り、取れるのは今のところprojectNameとenvNameの二つのようです。
envNameをリソースの変数に使う場合はcdk.Fn.ref('env')を使い、ロジック上の条件分岐ではこのenvNameを使うことが推奨されています。

export declare type AmplifyProjectInfo = {
    envName: string;
    projectName: string;
};

関連付けのためにtopicArnを書きだします。この際第2引数の名前をわかりやすい名前にしましょう。

    new cdk.CfnOutput(this, 'TopicArn', {
      value: topic.topicArn,
      description: 'Arn of Amazon SNS Topic',
    });

続いてamplify buildを行います。 CDKに何かしらの不備があればこちらでエラーがでますので、修正を行ってください。

amplify build

amplify push でリソースを環境に構築します。CDKをコピペしてメールアドレスを入れてないとちゃんとエラーで返してくれます。

amplify push

ビルドが終わるとしていたメールアドレスにAWSから確認メールがきますので、開いて中のリンクをクリックするとメール送信できる状態になります。

SNSを呼び出すAmplify function(Lambda)

amplify env checkout dev

↑なぜ必要なのかわかりませんが、一度チェックアウトしましょう。
環境変数の受け渡しがうまくいきません。

amplify add function

以下は基本デフォルト通りでOKです。
Lambdaファンクション名をsendMailとしました。

? Select which capability you want to add: Lambda function (serverless function)
? Provide an AWS Lambda function name: sendMail
? Choose the runtime that you want to use: NodeJS
? Choose the function template that you want to use: Hello World

Available advanced settings:
- Resource access permissions
- Scheduled recurring invocation
- Lambda layers configuration
- Environment variables configuration
- Secret values configuration

? Do you want to configure advanced settings? No
? Do you want to edit the local lambda function now? Yes
Edit the file in your editor: /app/amplify-sns/amplify/backend/function/sendMail/src/index.js
? Press enter to continue 

Lambdaに権限を追加する

LambdaにSnsをPublishする権限を追加します。
./amplify/backend/function/<function-name>/custom-policies.jsonを編集します。
${env}を使うことにより、環境別に対応できます。

[
 {
    "Action": ["sns:Publish"],
    "Resource": ["arn:aws:sns:*:*:sns-topic-amplify-sns-${env}"]
  }
]

カスタムリソースで出力した内容をFunctionの環境変数に設定する

  1. バックエンドの関連付けを行います。
    ./amplify/backend/backend-config.jsonを編集します。
    これまで同じ名前を付けている場合は
    <function-name> -> sendMail
    <custom-stack-name> -> SnsTopic
    <output-name> -> TopicArn
    です。
    これをつなげて使いますので打ち間違いがないように気をつけましょう。
backend-confg.json
{
    ...
    "function": {
        "<function-name>": {
            ...
            "dependsOn": [
                ...
                {
                    "category": "custom",
                    "resourceName": "<custom-stack-name>",
                    "attributes": [
                        "<output-name>"
                    ]
                }
            ]
        }
        ...
    }
    ...
}
  1. amplify に parameterを読み込みます。
    ./amplify/backend/function/<function-name>/parameters.jsonを作成します。
    parameters.jsonは自動では作られないので自分で作ってください。
    customSnsTopicTopicArnとなってしまいました。ちょっと名前の付け方間違えた気がします。
parameters.json
{
    "custom<custom-stack-name><output-name>": {
        "Fn::GetAtt": [
            "<custom-stack-name>",
            "<output-name>"
        ]
    }
}
  1. 関数の中で読み込むための処理を行います。
    amplify/backend/function/<function-name>/<function-name>-cloudformation-template.jsonを編集します。
    長いので適切な個所を間違えないようにしましょう。
    私はenvironment-variable-nameをTopicArnとしました。
<function-name>-cloudformation-template.json
{
    ...
    "Parameters": {
        ...
        "custom<custom-stack-name><output-name>": {
            "Type": "String"
        }
    },
    ...
    "Resources": {
        "LambdaFunction": {
            ...
            "Properties": {
                ...
                "Environment": {
                    "Variables": {
                        ...
                        "<environment-variable-name>": {
                            "Ref": "custom<custom-stack-name><output-name>"
                        }
                    }
                }
            }
        }
        ...
    }
    ...
}	

Lambdaのサンプルコード

./amplify/backend/function/<function-name>/index.jsを編集します。
process.env.<environment-variable-name>で環境変数として先ほど設定した値を受けとります。

index.js
/* Amplify Params - DO NOT EDIT
    ENV
    REGION
Amplify Params - DO NOT EDIT */
const AWS = require('aws-sdk');
const sns = new AWS.SNS();

/**
 * @type {import('@types/aws-lambda').APIGatewayProxyHandler}
 */
exports.handler = async (event) => {
    console.log(`EVENT: ${JSON.stringify(event)}`);
    const { Message } = event.arguments;
    const { TopicArn } = process.env;
    await sns.publish({
        TopicArn,
        Message
    }).promise().catch(e => console.log(e))
    return {
        statusCode: 200,
        body: JSON.stringify(event.arguments),
    };
};

編集し終えたら環境に構築します。

amplify push

フロントエンドから呼び出せるようにLambda Resolverを作る

amplify add api
独り言

CDKを知らない頃、Amplify functionはAPI(GraphQL)から呼ぶことが多いだろうからもう少しスマートにならないかと考えていました。
CDKでQueueを作り、そのQueueからLambda呼び出しでAmplifyのリソースを使う仕組みを作ったときにこの謎が解けました。

今回はテスト用なのすべてデフォルトでOKです。

? Select from one of the below mentioned services: GraphQL
? Here is the GraphQL API that we will create. Select a setting to edit or continue Continue
? Choose a schema template: Single object with fields (e.g., “Todo” with ID, name, description)

graphqlを編集します。

./amplify/backend/api/<project-name>/schema.graphqlを編集します。
SnsだとMutaitonなのかQueryなのかちょっと困りました。

schema.graphql
type Mutation {
  sendMail(Message: String): String @function(name: "sendMail-${env}")
}

編集し終えたら環境に構築します。

amplify push

初めての構築なのでいろいろ聞かれます。
私はtypescriptに変更しました。
その他はデフォルトのままでOKです。

? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target typescript
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.ts
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested] 2
? Enter the file name for the generated code src/API.ts

フロントエンドを作る

yarn add aws-amplify@^5.0.4

フロントエンドの基本的な作り方を参考に必要なセットアップをお願いします。
./src/components/HelloWorld.vueを編集します。

HelloWorld.vue
<template>
  <v-container class="fill-height">
    <v-responsive class="d-flex align-center text-center fill-height">
      <v-text-field v-model.trim="Message"></v-text-field>
      <v-btn color="primary" @click="handleClickSendMail">メール送信</v-btn>
    </v-responsive>
  </v-container>
</template>

<script setup lang="ts">
import { sendMail } from '@/graphql/mutations';
import { API, graphqlOperation } from 'aws-amplify';
import { ref } from 'vue';

const Message = ref('');
const handleClickSendMail = async () => {
  if (!Message.value) return;
  const result = await API.graphql(graphqlOperation(
    sendMail,
    { Message: Message.value }));
  console.log(result);
  Message.value = '';
}
</script>

動作確認をしましょう。

yarn dev

動作確認して満足出来たらamplify delete で環境を削除しましょう。

あとがき

時間がなくて雑な記事になってしまいました。すみません。
マネジメントコンソールを使わずにamplify cliのみで環境構築できれば、設定ミスを減らすことができると思います。
parameters.jsonやfunciton-parameter.jsonの使い方を知りたいです。
amplify functionsの設定でリソースの権限のところで制御できるような雰囲気があるので早くできるようになるといいですね。
それでは皆様よきAmplifyライフを❕

Discussion