cdkでsnsのtopicとsubscriptionを作ってlambdaからpublishしたメモ

## 概要
メールをlambdaからSNSを通して送信するサンプル。

[ソースコード](https://github.com/hibohiboo/aws-cdk-v2/blob/530648f34d184caeac7e14d19e8fc7eee60a6638/cdk/lib/alarm-sns-stack.ts)

## コード

```ts:cdk/lib/alarm-sns-stack.ts
import { Aspects, CfnOutput, Stack, StackProps, Tag } from "aws-cdk-lib";
import { Runtime } from "aws-cdk-lib/aws-lambda";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
import { Topic } from "aws-cdk-lib/aws-sns";
import { EmailSubscription } from "aws-cdk-lib/aws-sns-subscriptions";
import { Construct } from "constructs";

interface AlarmSNSStackProps extends StackProps { emailAdress: string }

export class AlarmSNSStack extends Stack {
  constructor(scope: Construct, id: string, props: AlarmSNSStackProps) {
    super(scope, id, props);
    const topicName = 'topic'
    const snsTopic = new Topic(this, topicName, { displayName: topicName, topicName, fifo: false });
    snsTopic.addSubscription(new EmailSubscription(props.emailAdress));

    snsTopic.grantPublish(new NodejsFunction(this, 'publishLambda', {
      runtime: Runtime.NODEJS_18_X,
      entry: `../src/handler/invoke/events/sendMail.ts`,
      environment: { snsTopic: topicName },
    }))
    new CfnOutput(this, `sns-confirm-subscription`, {
      value: `aws sns confirm-subscription --topic-arn ${snsTopic.topicArn} --authenticate-on-unsubscribe true --region ${props.env?.region} --token xxxCopyAndPasteTokenfromMailxxx`,
    })
    new CfnOutput(this, `sns-list`, {
      value: `aws sns list-subscriptions-by-topic --topic-arn ${snsTopic.topicArn}`,
    })
    Aspects.of(this).add(new Tag('Stack', 'AlarmSNSStack'));
  }
}
```

```ts:src/handler/invoke/events/sendMail.ts
import { sendEMailMessage } from "@/domain/sns";
import { Handler } from "aws-lambda";

export const handler: Handler = async (event, context) => {
  console.log(event);
  const [__arn, __partition, __service, __region, accountId, __resourceId] = context.invokedFunctionArn.split(':');
  const envList = ['snsTopic', 'AWS_REGION'] as const;
  envList.forEach(k => { if (!process.env[k]) throw new Error(`${k} environment required`) });
  const processEnv = process.env as Record<typeof envList[number], string>;
  await sendEMailMessage({ account: accountId, region: processEnv.AWS_REGION, snsTopic: processEnv.snsTopic, subject: '件名テスト', message: '本文テスト' })
}
```

```ts:src/domain/sns/index.ts
import { PublishCommand, SNSClient, SubscribeCommand } from "@aws-sdk/client-sns";
let snsClient: SNSClient | null = null;
const getSNSClinent = (region: string): SNSClient => {
  if (snsClient) return snsClient;
  snsClient = new SNSClient({ region });
  return snsClient
}

const sendEMailMessage = async (props: { region: string; account: string; snsTopic: string, subject: string, message: string }) => {

  const params = {
    TopicArn: `arn:aws:sns:${props.region}:${props.account}:${props.snsTopic}`,
    Subject: props.subject,
    Message: props.message
  };

  const snsClient = getSNSClinent(props.region)
  try {
    const data = await snsClient.send(new PublishCommand(params));
    console.log("Success.", data);
    return data;
  } catch (err) {
    console.error("Error", err);
    throw err;
  }

}
export { sendEMailMessage };
```

## lambda実施前にsubscriptionの設定
デプロイ直後に`aws sns list-subscriptions-by-topic --topic-arn arn:aws:sns:ap-northeast-1:0000000000:topic`を実行すると下記の結果が得られる。

```
{
    "Subscriptions": [
        {
            "SubscriptionArn": "PendingConfirmation",
            "Owner": "0000000000",
            "Protocol": "email",
            "Endpoint": "hoge@gmail.com",
            "TopicArn": "arn:aws:sns:ap-northeast-1:0000000000:topic"
        }
    ]
}
```

[Amazon SNS メール通知の「unsubscribe」リンクを無効化する](https://dev.classmethod.jp/articles/amazon-sns-disable-unsubscribe-link/)を参考に、確認メールのリンクに含まれていたトークンから承認を行う。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/615b1064-7018-8906-b24d-269e79144482.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/7fae9199-d713-19f5-4da5-f6dc6feb6e1a.png)

リンクをコピーしたアドレス内に、下記のようなトークンが含まれているのでこれをつかう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/05e1a525-4c07-d895-7a09-38ce680bf814.png)



その後、`aws sns list-subscriptions-by-topic --topic-arn arn:aws:sns:ap-northeast-1:0000000000:topic`を実行すると下記の結果が得られる。
```
{
    "Subscriptions": [
        {
            "SubscriptionArn": "arn:aws:sns:ap-northeast-1:0000000000:topic:11111111-xxxx-xxxx-xxxx-777xxx777xxx",
            "Owner": "0000000000",
            "Protocol": "email",
            "Endpoint": "hoge@gmail.com",
            "TopicArn": "arn:aws:sns:ap-northeast-1:0000000000:topic"
        }
    ]
}
```

この状態で、コンソールからLambdaを実行し、メールが届くことを確認できた。
GMailだと、サブスクリプションの確認メールは通常のフォルダだが、その後の通知メールはプロモーションフォルダに届いていた。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/41e1d253-292f-111e-1837-fb632130de33.png)




## 参考
[デベロッパーガイド Amazon SNS メッセージの発行](https://docs.aws.amazon.com/ja_jp/sns/latest/dg/sns-publishing.html)
[誰かサブスクリプション解除した？を無くすために Amazon SNS によるメール通知の停止（Unsubscribe）リンクを無効化してみた](https://dev.classmethod.jp/articles/prevent-unsubscribe-all-sns-topic/)