Amazon Route 53のパブリックホストゾーンの作成・削除と連動させてNotionデータベースを更新してみる

2022.09.20

Amazon Route 53 のパブリックホストゾーン一覧を Notion データベースで一元的に管理したいと思い、自動化を試してみました。

以前にも Amazon EventBridge と Notion API を利用して、パブリックホストゾーンの作成に合わせて Notion データベースへのページ追加を自動的に行うことは試していたのですが、そのときは削除の連携までは行っていませんでした。今回は、AWS Lambda も利用して追加・削除の両方を行ってみました。

実現イメージ

実現方法として、Amazon Route 53 のホストゾーンの作成と削除に対して、Amazon EventBridge 経由で AWS Lambda を実行して Notion データベースを操作します。

Amazon Route 53 はグローバルサービスであり、イベントはバージニア北部リージョンで発生するため、設定はバージニア北部リージョンで行います。

以降は実際に試してみます。

始めに Notion API の設定とデータベースの作成を行い、その後に AWS Lambda と Amazon EventBridge の設定を行います。付随するサービスとして Systems Manager パラメータストアと IAM ロールの作成も行います。

Notion API の設定

Notion API のインテグレーション設定は次のページで行います。

Notion API Integration

今回は新しいインテグレーションを作成して下図の権限としています。コメント機能とユーザー機能は必要ないため利用しない設定としています。

作成後にトークン情報が表示されるため大切に管理します。

なお、Notion API のリファレンスは次のページで公開されています。

Introduction

今回のブログで利用する API は、ページ作成時にはCreate a pageを利用し、ページの削除時にはQuery a databaseUpdate pageを利用します。

Create a page

Query a database

Update page

Notion データベースの作成と共有設定

Amazon Route 53 のパブリックホストゾーンを管理するためのデータベースを作成します。

項目毎の種類(プロパティ)は下表の通りです。

項目名 種類(プロパティ)
Hosted Zone ID タイトル
Public Hosted Zone Name テキスト
Account ID テキスト
作成日時 作成日時
作成者 作成者
最終更新日時 最終更新日時
最終更新者 最終更新者

次に、作成したインテグレーションAWSからデータベースを操作できる設定を行います。以前は「共有」設定から行っていましたが、執筆時点ではコネクト設定から行うようになっています。作成したインテグレーションAWSを選択します。

設定後には権限の確認を行うこともできます。

後ほど利用するため、データベース ID を控えておきます。データベース ID は Notion データベースの URL におけるnotion.so/?v=xxxxxの間の 32 桁の文字列が該当します。

以上で Notion 側の設定は完了です。以降は AWS 側の設定となります。

AWS Systems Manager パラメータストアの作成

始めに、Notion API と接続するために利用するデータベース ID とトークン情報を AWS Systems Manager パラメータストアに格納します。

バージニア北部リージョンで次の設定で 2 つのパラメータを追加します。

名前 タイプ KMS キーソース KMS キー ID
notion-database-id 文字列 - - データベース ID(32 桁)
notion-token 安全な文字列 現在のアカウント alias/aws/ssm secret_xxxxxxxxxxxxxxxxxxxxxxx

設定後の画面です。

IAM ロールの作成

AWS Lambda にアタッチするための IAM ロールを作成します。

まず、先ほど作成したパラメータストアへのアクセス権限を設定した IAM ポリシーnotion-get-parameter-policyを作成します。

notion-get-parameter-policy

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "ssm:GetParameter",
      "Resource": [
        "arn:aws:ssm:us-east-1:111122223333:parameter/notion-database-id",
        "arn:aws:ssm:us-east-1:111122223333:parameter/notion-token"
      ]
    }
  ]
}

IAM ロールnotion-lambda-roleを作成して次の 2 つの IAM ポリシーをアタッチします。

  • AWSLambdaBasicExecutionRole
  • notion-get-parameter-policy

AWSLambdaBasicExecutionRoleは AWS 管理ポリシーであり、Amazon CloudWatch Logs に対する書き込み権限が設定されています。

IAM ロールの信頼ポリシーは次の通りです。AWS Lambda を信頼します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "lambda.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

AWS Lambda の設定

AWS Lambda の設定では、始めに利用するライブラリを設定したレイヤーを作成した後に Notion データベースのページを追加する関数と削除する関数を作成します。

AWS Lambda 関数は Python で作成しており、ランタイムはPython 3.8を利用しています。

設定はバージニア北部リージョンで行います。

Lambda Layers の作成

後ほど作成する 2 つの関数においてrequestsモジュールを使用することから、事前の準備としてレイヤーの作成を行います。

始めに、レイヤーにアップロードするための zip ファイルを作成します。

$ zip -r Layer.zip python/
$ pip3 install requests -t ./python
$ zip -r Layer.zip python/

レイヤーの設定を行います。上記で作成したLayer.zipをアップロードして作成します。

以上でレイヤーの設定は完了です。

関数の作成(Notion のページ追加)

Notion データベースにページを追加する関数notion-create-pageを作成します。今回は私のローカル環境と合わせたかったこともあり、ランタイムはPython 3.8を利用しています。IAM ロールは作成したnotion-lambda-roleを指定します。

コードは次の通りです。コードの更新後に Deploy が必要です。

import os
import logging
import json
import boto3
import requests

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

def get_secret():
    paramater_name = os.environ['NOTION_TOKEN']
    try:
        ssm = boto3.client('ssm')
        ssm_response = ssm.get_parameter(Name=paramater_name, WithDecryption=True)
        return ssm_response['Parameter']['Value']
    except Exception as e:
        logger.error(e)
        raise

def get_database_id():
    paramater_name =  os.environ['NOTION_DATABASE_ID']
    try:
        ssm = boto3.client('ssm')
        ssm_response = ssm.get_parameter(Name=paramater_name, WithDecryption=False)
        return ssm_response['Parameter']['Value']
    except Exception as e:
        logger.error(e)
        raise

def lambda_handler(event, context):
    secret_token    = get_secret()
    database_id     = get_database_id()
    hostedzone_id   = event['detail']['responseElements']['hostedZone']['id'].split('/')[2]
    hostedzone_name = event['detail']['responseElements']['hostedZone']['name']
    account_id      = event['account']

    url = 'https://api.notion.com/v1/pages'
    headers = {
        'Authorization': 'Bearer ' + secret_token,
        'Content-Type': 'application/json; charset=UTF-8',
        'Notion-Version': '2022-06-28'
    }
    payload = {
        'parent': {
            'type': 'database_id',
            'database_id': database_id
        },
        'properties': {
            'Hosted Zone ID': {
                'title': [
                    {
                        'text': {
                            'content': hostedzone_id
                        }
                    }
                ]
            },
            'Public Hosted Zone Name': {
                'rich_text': [
                    {
                        'text': {
                            'content': hostedzone_name
                        }
                    }
                ]
            },
            'Account ID': {
                'rich_text': [
                    {
                        'text': {
                            'content': account_id
                        }
                    }
                ]
            }
        }
    }

    try:
        response = requests.post(url, headers=headers, data=json.dumps(payload))
        response.raise_for_status()
    except requests.exceptions.RequestException as e:
        logger.error(e)
        raise

    return None

ホストゾーン ID は/hostedzone/Z01001291JZMVUEXAMPLEの形式になっているため、split を利用してZ01001291JZMVUEXAMPLEのみを抽出して変数hostedzone_idに格納しています。ホストゾーン削除のイベントでは ID としてZ01001291JZMVUEXAMPLE部分が格納されているためです。

Notion API の通信内容は次のリファレンスを基に組み立てています。

Create a page

また、設定の環境変数から次の値を設定します。値はパラメータストアの名前です。

キー
NOTION_DATABASE_ID notion-database-id
NOTION_TOKEN notion-token

requestsを利用するためにレイヤーpython-layerも設定(追加)します。

以上で設定は終了です。トリガーは後ほど、Amazon EventBridge において設定することにします。

関数の設定(Notion のページ削除)

Notion へのページ追加と同じ要領でページ削除を行う関数notion-archive-pageも作成します。Notion へのページ追加の関数と同様に、ランタイムはPython 3.8、IAM ロールはnotion-lambda-roleを指定しています。

コードは次の通りです。コードの更新後に Deploy が必要です。

import os
import logging
import json
import boto3
import requests

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

def get_secret():
    paramater_name = os.environ['NOTION_TOKEN']
    try:
        ssm = boto3.client('ssm')
        ssm_response = ssm.get_parameter(Name=paramater_name, WithDecryption=True)
        return ssm_response['Parameter']['Value']
    except Exception as e:
        logger.error(e)
        raise

def get_database_id():
    paramater_name =  os.environ['NOTION_DATABASE_ID']
    try:
        ssm = boto3.client('ssm')
        ssm_response = ssm.get_parameter(Name=paramater_name, WithDecryption=False)
        return ssm_response['Parameter']['Value']
    except Exception as e:
        logger.error(e)
        raise

def lambda_handler(event, context):
    secret_token  = get_secret()
    database_id   = get_database_id()
    hostedzone_id = event['detail']['requestParameters']['id']

    url = 'https://api.notion.com/v1/databases/' + database_id + '/query'
    headers = {
        'Authorization': 'Bearer ' + secret_token,
        'Content-Type': 'application/json; charset=UTF-8',
        'Notion-Version': '2022-06-28'
    }
    payload = {
        'filter': {
            'and': [
                {
                    'property': 'Hosted Zone ID',
                    'title': {
                        'equals': hostedzone_id
                    }
                }
            ]
        }
    }

    try:
        response = requests.post(url, headers=headers, data=json.dumps(payload))
        response.raise_for_status()
    except requests.exceptions.RequestException as e:
        logger.error(e)
        raise
    else:
        response_data = json.loads(response.text)

    for result in response_data['results']:
        page_id = result['id']
        url     = 'https://api.notion.com/v1/pages/' + page_id
        payload = {
            'parent': {
                'type': 'database_id',
                'database_id': database_id
            },
            'archived': True
        }

        try:
            response = requests.patch(url, headers=headers, data=json.dumps(payload))
            response.raise_for_status()
        except requests.exceptions.RequestException as e:
            logger.error(e)
            raise

    return None

Notion API の通信内容は次のリファレンスを基に組み立てています。

Query a database

Update page

始めに削除対象ページのページ ID を取得するため、データベースに対してホストゾーン ID が一致するページを返すクエリを実行しています。その後、取得したページに対して削除を行っています。この際に、返答された複数のページを削除できるように for 文を利用しています。ホストゾーン ID でデータベースを検索しているため 1 つのページしか返答されない想定ですが、「API としては複数のページが返答される仕様であること」「手動で重複したホストゾーン ID のページの追加が可能であること」から複数ページを削除できるようにしてみました。

設定の環境変数から次の値を設定します。値はパラメータストアの名前です。

キー
NOTION_DATABASE_ID notion-database-id
NOTION_TOKEN notion-token

requestsを利用するためにレイヤーpython-layerも設定(追加)します。Notion のページ追加を行う関数と同様の設定です。

以上で設定は終了です。トリガーは後ほど、Amazon EventBridge において設定することにします。

Amazon EventBridge の設定

EventBridge の設定では、パブリックホストゾーンの作成イベントに関するルールとホストゾーンの削除イベントに関するルールを設定します。

パブリックホストゾーンの作成ルール

パブリックホストゾーンの作成に合わせて Lambda 関数notion-create-pageを実行するルールcreate-hosted-zoneを作成します。

イベントバスはdefaultとして、イベントパターンは次の設定を行います。privateZoneを利用してパブリックホストゾーンの作成のみに一致するようにしています。

{
  "source": ["aws.route53"],
  "detail-type": ["AWS API Call via CloudTrail"],
  "detail": {
    "eventSource": ["route53.amazonaws.com"],
    "eventName": ["CreateHostedZone"],
    "responseElements": {
      "hostedZone": {
        "config": {
          "privateZone": [false]
        }
      }
    }
  }
}

ターゲットには Notion のページ追加を行う関数notion-create-pageを指定します。

設定後の Lambda 関数の画面です。EventBridge がトリガーとして設定されていることを確認できます。

ホストゾーンの削除ルール

ホストゾーンの削除に合わせて Lambda 関数notion-archive-pageを実行するルールdelete-hosted-zoneを作成します。

イベントバスはdefaultとして、イベントパターンは次の設定を行います。ホストゾーンの作成時と異なり、パブリックホストゾーンであることを識別することができなさそうだったので、パブリックやプライベートに関係なくホストゾーンの削除と一致します。

{
  "source": ["aws.route53"],
  "detail-type": ["AWS API Call via CloudTrail"],
  "detail": {
    "eventSource": ["route53.amazonaws.com"],
    "eventName": ["DeleteHostedZone"]
  }
}

ターゲットには Notion のページ削除を行う関数notion-archive-pageを指定します。

設定後の Lambda 関数の画面です。EventBridge がトリガーとして設定されていることを確認できます。

イベントパターンの設定がホストゾーンの削除全てと一致していることから、プライベートホストゾーンの削除時にも Lambda が実行されてしまいます。Notion のページ削除はホストゾーン ID が一致したページを削除するため問題はないのですが、無駄に Lambda が実行されることになるため、今後の改善点となります。いまのところ良い案が思い浮かんでいません。

動作確認

最後に動作確認を行います。

Amazon Route 53 においてパブリックホストゾーンtest.example.net.を作成してみます。

作成後に Notion データベースにページが追加されています。

続いて、上記で作成したパブリックホストゾーンtest.example.net.を削除してみます。

Notion データベースからも削除されています。

削除されたページの履歴からも確認できます。

以上で動作確認は終了です。

さいごに

以前に書いたブログで Amazon EventBridge だけを利用したときは実現できていなかったパブリックホストゾーンの作成とホストゾーンの削除の両方を Notion データベースに連携する方法を試してみました。

コードの内容やプライベートホストゾーンの削除時にも Notion のページ削除を行う Lambda 関数が実行される点など、まだ改善の余地はありますが、追加・削除の両方を連携できることを確認できたことで色々できることが広がりそうです。他には購入・移管したドメインの管理や EC2 インスタンスの一覧管理などもできそうです。

以上、このブログがどなたかのご参考になれば幸いです。