CFnでSNSアクセスポリシーを自動追加する【Lambda-backedカスタムリソース】

# 背景
例えばEventBridgeから既存のAWS SNSトピックをパブリッシュするルールを作成するとき。
**1. AWSコンソール上から手動でEventBridgeを作成した場合**
→ パブリッシュ先SNSのアクセスポリシーにも自動的にEventBridgeの許可ルールが追加されます。
**2. 一方、CLIやCFnから同様にEventBridgeを作成した場合**
→ SNSにアクセス許可が自動追加されることはありません。そのままではEventBridgeからのパブリッシュがSNS側で拒否されてしまいます。

そのため、EventBridgeからの許可ポリシーもCFnで自動追加できないかなーと思いました。
（手動でポチポチ追加しても良いけど、現場では多数のAWSアカウントを管理しているため、簡単に追加できるよう自動化したい。要は楽したい。）

# 解決策1と懸念点
[クラメソさんの記事](https://dev.classmethod.jp/articles/event-bridge-notify-sns/#toc-6)にヒントをもらって、Lambda-BackedカスタムリソースをCFnに記述しアクセスポリシーを自動更新できました。
AWS SDK(python)で```set_topic_attributes ```により更新できます。

### 懸念点
ただ、この```set_topic_attributes ```だと**SNSアクセスポリシーの「追加」ではなく「上書き」**になります。
そのためもし既存のSNSトピックに予め別のアクセスポリシーが追加されている状態でこのカスタムリソースを適用した場合、予めあったポリシーは削除されてしまいます。
こういうトラブルの原因になるカスタムリソースは作りたくないなと思い、改善策を模索しました。


# 解決策2
答えはシンプル。SDKの`get_topic_attribets`でSNSのトピックポリシーを取得しEventBridgeのアクセス許可を追記して、```set_topic_attributes ```するだけ。
カスタムリソースで使用したLambda関数は以下の通り。
```TopicArn```、```AwsAccountId ```はカスタムリソースから渡すパラメータです。

```py:lambda-addSnsAccessPolicy.py
import json
import boto3
import logging
import cfnresponse

logger = logging.getLogger()
logger.setLevel(logging.INFO)

sns = boto3.client('sns')

def lambda_handler(event, context):
  logger.info("event == {}".format(event))
  response = None

  topicArn = event['ResourceProperties']['TopicArn']
  awsAccountId = event['ResourceProperties']['AwsAccountId']

  addPolicy = {
    "Sid": "AWSEvent_Allow",
    "Effect": "Allow",
    "Principal": {
      "Service": "events.amazonaws.com"
    },
    "Action": "sns:Publish",
    "Resource": topicArn,
    "Condition": {
      "StringEquals": {
        "AWS:SourceAccount": awsAccountId
      }
    }
  }

  if event['RequestType'] == 'Create':
    # 既存のトピックポリシーを取得
    attributes = sns.get_topic_attributes(TopicArn = topicArn)
    policy = eval(attributes["Attributes"]["Policy"])
    logger.info("policy = {}".format(policy))

    # EventBridgeからのパブリッシュ許可を追記
    policy["Statement"].append(addPolicy)
    logger.info("updatepolicy = {}".format(policy))

    # トピックポリシーを上書き
    responce=sns.set_topic_attributes(
      TopicArn=topicArn,
      AttributeName='Policy',
      AttributeValue=json.dumps(policy)
    )

  if event['RequestType'] == 'Update':
    cfnresponse.send(event, context, cfnresponse.SUCCESS, {})

  if event['RequestType'] == 'Update':
    cfnresponse.send(event, context, cfnresponse.SUCCESS, {})

  logger.info("response == {}".format(responce))
  cfnresponse.send(event, context, cfnresponse.SUCCESS, responce)
```

このLambda関数、カスタムリソース等のCloudFormationテンプレートは[GitHubリポジトリ](https://github.com/Makoto-Taguchi/CFn-AddSnsTopicPolicy/blob/main/CFn/EventBridge-addSnsAccesPolicy.yml)にあげています。
詳しくご覧になりたい方はどうぞ。

# 実際にやってみる
まず、既存SNSのアクセスポリシーを確認します。デフォルトのポリシーと、S3からのパブリッシュ許可が設定されている状態です。

```
$ aws sns get-topic-attributes --topic-arn arn:aws:sns:ap-northeast-1:{AWSアカウントID}:TestTopic
（ポリシーの部分だけ抜粋）
"Policy": 
"{\"Version\":\"2008-10-17\",
\"Id\":\"__default_policy_ID\",
\"Statement\":[
↓デフォルトのポリシー
{\"Sid\":\"__default_statement_ID\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"*\"},\"Action\":[\"SNS:GetTopicAttributes\",\"SNS:SetTopicAttributes\",\"SNS:AddPermission\",\"SNS:RemovePermission\",\"SNS:DeleteTopic\",\"SNS:Subscribe\",\"SNS:ListSubscriptionsByTopic\",\"SNS:Publish\"],\"Resource\":\"arn:aws:sns:ap-northeast-1:{AWSアカウントID}:TestTopic\",\"Condition\":{\"StringEquals\":{\"AWS:SourceOwner\":\"{AWSアカウントID}\"}}},
↓S3からのパブリッシュ許可ポリシー
{\"Sid\":\"S3_Allow\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"s3.amazonaws.com\"},\"Action\":\"sns:Publish\",\"Resource\":\"arn:aws:sns:ap-northeast-1:{AWSアカウントID}:TestTopic\",\"Condition\":{\"StringEquals\":{\"AWS:SourceAccount\":\"{AWSアカウントID}\"}}}
]}"
```

カスタムリソースを含むCFnをデプロイします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/577028/f4ff2295-1d9f-80f9-8e26-748cc6e02e9d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/577028/90c36ecb-e580-2940-8176-9d787a9d2653.png)


アクセスポリシーを再確認してみます。EventBridgeからのパブリッシュ許可ポリシーが追加されていることが確認できました！！もともと設定されていたポリシーも消されずに残ってますね。

```
$ aws sns get-topic-attributes --topic-arn arn:aws:sns:ap-northeast-1:{AWSアカウントID}:TestTopic
（ポリシーの部分だけ抜粋）
"Policy": 
"{\"Version\":\"2008-10-17\",
\"Id\":\"__default_policy_ID\",
\"Statement\":[
↓ もともとあったポリシー
{\"Sid\":\"__default_statement_ID\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"*\"},\"Action\":[\"SNS:GetTopicAttributes\",\"SNS:SetTopicAttributes\",\"SNS:AddPermission\",\"SNS:RemovePermission\",\"SNS:DeleteTopic\",\"SNS:Subscribe\",\"SNS:ListSubscriptionsByTopic\",\"SNS:Publish\"],\"Resource\":\"arn:aws:sns:ap-northeast-1:{AWSアカウントID}:TestTopic\",\"Condition\":{\"StringEquals\":{\"AWS:SourceOwner\":\"{AWSアカウントID}\"}}},
{\"Sid\":\"S3_Allow\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"s3.amazonaws.com\"},\"Action\":\"sns:Publish\",\"Resource\":\"arn:aws:sns:ap-northeast-1:{AWSアカウントID}:TestTopic\",\"Condition\":{\"StringEquals\":{\"AWS:SourceAccount\":\"{AWSアカウントID}\"}}},
↓ 今回追加されたEventBridgeからの許可ポリシー
{\"Sid\":\"AWSEvent_Allow\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"events.amazonaws.com\"},\"Action\":\"sns:Publish\",\"Resource\":\"arn:aws:sns:ap-northeast-1:{AWSアカウントID}:TestTopic\",\"Condition\":{\"StringEquals\":{\"AWS:SourceAccount\":\"{AWSアカウントID}\"}}}
]}"
```

# 最後に
調べてもこの自動追加方法の記事が意外と無く、自分で作ってみました。面倒なSNSアクセスポリシーの管理を楽にできて良かったです。
（解決策はシンプルですが、地味に右往左往してAWSサポートに聞いたりしてましたw）
カスタムリソースはあんまり使ってこなかったけど、自由度高くて便利ですね。良い勉強になりました。

# 参考
[・【クラメソ記事】EventBridgeからSNSに通知ができなくてハマった話
](https://dev.classmethod.jp/articles/event-bridge-notify-sns/#toc-6)
[・【ドキュメント】Amazon EventBridge のリソースベースのポリシーを使用する - Amazon SNS のアクセス許可](https://docs.aws.amazon.com/ja_jp/eventbridge/latest/userguide/eb-use-resource-based.html#eb-sns-permissions)
[・【GitHubリポジトリ】CFn Lambda-backedカスタムリソースを使ってSNSトピックポリシー追加を自動化する](https://github.com/Makoto-Taguchi/CFn-AddSnsTopicPolicy)