ちばのてっく

積極的にアウトプット

AWS最新情報をLINE通知させてみた

プル型ではなくプッシュ型でAWSの最新情報をキャッチアップしたいと思い、身近なツールであるLINEで通知するアプリケーションをつくってみた。

仕組みとしては、RSSで配信されているAWSの最新情報を、EventBridge + Lambdaで定期的に確認し新しいものがあればLINE通知する、といったもの。

キーワードは以下の通り。

  • RSS
  • Messaging API(LINE)
  • Terraform
  • Lambda + EventBridge

準備運動

RSS

Pythonにはfeedparserというライブラリが存在するため、これを使う。 feedparser.parse(任意のURL)とすれば、FeedParserDictオブジェクトを取得できる。

参考:https://feedparser.readthedocs.io/en/latest/index.html

試しに「RSSフィードで購読する」で確認できたURLで、FeedParserDictオブジェクトを取得してみた。(可視性あげるためにJSON形式で整形した&量多いので抜粋した)

{
    "bozo": false,
    "entries": [
        {
            "links": [
                {
                    "rel": "alternate",
                    "type": "text/html",
                    "href": "https://aws.amazon.com/jp/about-aws/whats-new/2024/03/fujitsu-and-aws-deepen-global-partnership-to-accelerate-legacy-applications-modernization-on-the-cloud/"
                }
            ],
            "link": "https://aws.amazon.com/jp/about-aws/whats-new/2024/03/fujitsu-and-aws-deepen-global-partnership-to-accelerate-legacy-applications-modernization-on-the-cloud/",
            "id": "84d117fbd5b1f61fa1ebf495d2643756e4ee4afd",
            "guidislink": false,
            "title": "富士通と AWS、クラウドでのレガシーシステムのモダナイゼーション加速に向けてグローバルパートナーシップを拡大",
            "title_detail": {
                "type": "text/plain",
                "language": null,
                "base": "https://aws.amazon.com/jp/about-aws/whats-new/recent/feed/",
                "value": "富士通と AWS、クラウドでのレガシーシステムのモダナイゼーション加速に向けてグローバルパートナーシップを拡大"
            },
            "summary": "<p>「Modernization Acceleration Joint Initiative」を通して、お客様の DX を支援</p>",
            "summary_detail": {
                "type": "text/html",
                "language": null,
                "base": "https://aws.amazon.com/jp/about-aws/whats-new/recent/feed/",
                "value": "<p>「Modernization Acceleration Joint Initiative」を通して、お客様の DX を支援</p>"
            },
            "published": "Mon, 18 Mar 2024 01:58:27 +0000",
            "published_parsed": [
                2024,
                3,
                18,
                1,
                58,
                27,
                0,
                78,
                0
            ],
            "tags": [],
            "authors": [
                {
                    "email": "aws@amazon.com"
                }
            ],
            "author": "aws@amazon.com",
            "author_detail": {
                "email": "aws@amazon.com"
            }
        },
        {
            "links": [
                {
                    "rel": "alternate",
                    "type": "text/html",
                    "href": "https://aws.amazon.com/jp/about-aws/whats-new/2024/03/amazon-cognito-europe-zurich-region/"
                }
            ],
            "link": "https://aws.amazon.com/jp/about-aws/whats-new/2024/03/amazon-cognito-europe-zurich-region/",
            "id": "f8f89768f2f327c71d8f139d045d572abb31bd57",
            "guidislink": false,
            "title": "Amazon Cognito が欧州 (チューリッヒ) リージョンで利用可能に",
            "title_detail": {
                "type": "text/plain",
                "language": null,
                "base": "https://aws.amazon.com/jp/about-aws/whats-new/recent/feed/",
                "value": "Amazon Cognito が欧州 (チューリッヒ) リージョンで利用可能に"
            },
            "summary": "<p>Amazon Cognito が欧州 (チューリッヒ) リージョンで利用可能になりました。Amazon Cognito では、ウェブアプリケーションやモバイルアプリケーションに認証、認可、ユーザー管理の機能を簡単に追加できます。Amazon Cognito は、数百万のユーザーに対応してスケールし、SAML 2.0 や OpenID Connect などの規格を介して、Apple、Facebook、Google、Amazon などのソーシャル ID プロバイダーやエンタープライズ ID プロバイダーを使用したサインインをサポートします。</p>",
            "summary_detail": {
                "type": "text/html",
                "language": null,
                "base": "https://aws.amazon.com/jp/about-aws/whats-new/recent/feed/",
                "value": "<p>Amazon Cognito が欧州 (チューリッヒ) リージョンで利用可能になりました。Amazon Cognito では、ウェブアプリケーションやモバイルアプリケーションに認証、認可、ユーザー管理の機能を簡単に追加できます。Amazon Cognito は、数百万のユーザーに対応してスケールし、SAML 2.0 や OpenID Connect などの規格を介して、Apple、Facebook、Google、Amazon などのソーシャル ID プロバイダーやエンタープライズ ID プロバイダーを使用したサインインをサポートします。</p>"
            },
            "published": "Fri, 22 Mar 2024 19:32:00 +0000",
            "published_parsed": [
                2024,
                3,
                22,
                19,
                32,
                0,
                4,
                82,
                0
            ],
            "tags": [
                {
                    "term": "general:products/amazon-cognito,marketing:marchitecture/security-identity-and-compliance",
                    "scheme": null,
                    "label": null
                }
            ],
            "authors": [
                {
                    "email": "aws@amazon.com"
                }
            ],
            "author": "aws@amazon.com",
            "author_detail": {
                "email": "aws@amazon.com"
            }
        },
######################## 行数多いので間を割愛 ########################
    ],
    "feed": {
        "links": [
            {
                "rel": "alternate",
                "type": "text/html",
                "href": "https://aws.amazon.com/jp/about-aws/whats-new/recent/"
            }
        ],
        "link": "https://aws.amazon.com/jp/about-aws/whats-new/recent/",
        "title": "最近の発表",
        "title_detail": {
            "type": "text/plain",
            "language": null,
            "base": "https://aws.amazon.com/jp/about-aws/whats-new/recent/feed/",
            "value": "最近の発表"
        },
        "subtitle": "AWS クラウドプラットフォームは日々拡大しています。お知らせ、新製品の発表、ニュース、イノベーションなどの詳細をアマゾン ウェブ サービスでご確認ください。",
        "subtitle_detail": {
            "type": "text/html",
            "language": null,
            "base": "https://aws.amazon.com/jp/about-aws/whats-new/recent/feed/",
            "value": "AWS クラウドプラットフォームは日々拡大しています。お知らせ、新製品の発表、ニュース、イノベーションなどの詳細をアマゾン ウェブ サービスでご確認ください。"
        },
        "authors": [
            {
                "name": "Amazon Web Services",
                "email": "aws@amazon.com"
            }
        ],
        "author": "aws@amazon.com (Amazon Web Services)",
        "author_detail": {
            "name": "Amazon Web Services",
            "email": "aws@amazon.com"
        },
        "updated": "Fri, 22 Mar 2024 19:33:37 +0000",
        "updated_parsed": [
            2024,
            3,
            22,
            19,
            33,
            37,
            4,
            82,
            0
        ],
        "published": "Fri, 22 Mar 2024 19:33:37 +0000",
        "published_parsed": [
            2024,
            3,
            22,
            19,
            33,
            37,
            4,
            82,
            0
        ],
        "docs": "http://blogs.law.harvard.edu/tech/rss",
        "image": {
            "href": "https://a0.awsstatic.com/main/images/logos/aws_logo_smile_179x109.png",
            "links": [
                {
                    "rel": "alternate",
                    "type": "text/html",
                    "href": "https://aws.amazon.com/jp/about-aws/whats-new/recent/"
                }
            ],
            "link": "https://aws.amazon.com/jp/about-aws/whats-new/recent/",
            "title": "最近の発表",
            "title_detail": {
                "type": "text/plain",
                "language": null,
                "base": "https://aws.amazon.com/jp/about-aws/whats-new/recent/feed/",
                "value": "最近の発表"
            },
            "subtitle": "AWS クラウドプラットフォームは日々拡大しています。お知らせ、新製品の発表、ニュース、イノベーションなどの詳細をアマゾン ウェブ サービスでご確認ください。",
            "subtitle_detail": {
                "type": "text/html",
                "language": null,
                "base": "https://aws.amazon.com/jp/about-aws/whats-new/recent/feed/",
                "value": "AWS クラウドプラットフォームは日々拡大しています。お知らせ、新製品の発表、ニュース、イノベーションなどの詳細をアマゾン ウェブ サービスでご確認ください。"
            }
        }
    },
    "headers": {
        "content-type": "application/rss+xml;charset=UTF-8",
        "transfer-encoding": "chunked",
        "connection": "close",
        "server": "Server",
        "date": "Sat, 23 Mar 2024 03:35:31 GMT",
        "x-amz-rid": "6PBTC6Z0XPV7YBSP6A0J",
        "set-cookie": "aws-priv=eyJ2IjoxLCJldSI6MCwic3QiOjB9; Version=1; Comment=\"Anonymous cookie for privacy regulations\"; Domain=.aws.amazon.com; Max-Age=31536000; Expires=Sun, 23 Mar 2025 03:35:31 GMT; Path=/; Secure",
        "x-frame-options": "SAMEORIGIN",
        "x-xss-protection": "1; mode=block",
        "strict-transport-security": "max-age=63072000",
        "x-amz-id-1": "6PBTC6Z0XPV7YBSP6A0J",
        "last-modified": "Sat, 23 Mar 2024 02:57:54 GMT",
        "content-security-policy-report-only": "default-src *; connect-src *; font-src * data:; frame-src *; img-src * data:; media-src *; object-src *; script-src 'nonce-xsp7AIieRU2oQ8SMHCbQKA==' *; style-src 'unsafe-inline' *; report-uri https://prod-us-west-2.csp-report.marketing.aws.dev/submit",
        "x-content-type-options": "nosniff",
        "vary": "Content-Type,Accept-Encoding,User-Agent",
        "x-cache": "Miss from cloudfront",
        "via": "1.1 7637a60a07b64cdf45697b2f5cacacee.cloudfront.net (CloudFront)",
        "x-amz-cf-pop": "NRT57-P1",
        "x-amz-cf-id": "DFFSpWXI_mkCtGqBy_xI_7p1D7QlqZM01Dw5yyp2gIpec2X4B0hObw=="
    },
    "updated": "Sat, 23 Mar 2024 02:57:54 GMT",
    "updated_parsed": [
        2024,
        3,
        23,
        2,
        57,
        54,
        5,
        83,
        0
    ],
    "href": "https://aws.amazon.com/jp/about-aws/whats-new/recent/feed/",
    "status": 200,
    "encoding": "UTF-8",
    "version": "rss20",
    "namespaces": {}
}

各記事に相当するentries内に、publishedという日時の項目があったため、これを使って通知する記事を抽出しようと思う。

Messaging API(LINE)

Line Developersでアカウントつくって、チャネルつくって、といったところは割愛する。)

メッセージを送信する方法は5種類あるが、今回は友達登録者全員にメッセージを送るブロードキャストメッセージを採用することにする。

  • 応答メッセージ
  • プッシュメッセージ:1対1
  • マルチキャストメッセージ:1対多(ユーザーID指定)
  • ナローキャストメッセージ:1対多(絞り込み配信)
  • ブロードキャストメッセージ:1対多(すべての友だち)

引用:メッセージの送信方法
https://developers.line.biz/ja/docs/messaging-api/sending-messages/

Messaging APIPythonで利用するにあたっては、line-bot-sdk-pythonを利用する。
試しに動かしてみたところ、期待通りhogeというメッセージがLINEに届いた。

from linebot.v3 import (
    WebhookHandler
)
from linebot.v3.messaging import (
    Configuration,
    ApiClient,
    MessagingApi,
    TextMessage,
    BroadcastRequest
)

channel_access_token = "<自作したチャネルで発行したトークン>"
channel_secret = "<自作したチャネルで発行したシークレット>"

configuration = Configuration(access_token=channel_access_token)
api_client = ApiClient(configuration) 
line_bot_api = MessagingApi(api_client)
handler = WebhookHandler(channel_secret)

line_bot_api.broadcast(
    BroadcastRequest(
        messages=[TextMessage(text="hoge")]
    )
)

なお、送信できるブロードキャストメッセージの上限は、60リクエスト/時ということを一応押さえておくと良い。(いろいろ試していたらこの上限にひっかかって処理に失敗し、Too Many Requestsが返ってきた、、)

参考:https://developers.line.biz/ja/reference/messaging-api/#rate-limits

つくってみた

イメージとしてはこんな感じ。

EventBridgeは、Schedulerを利用して、毎朝8時にLambdaを実行する。
Lambdaは、feedparserで取得した各記事のpublishedから、24時間以内に公開された記事がないか確認し、あればMessaging API(LINE)経由でLINE友達登録者にブロードキャストメッセージを送信する。

ディレクトリ構造

.
├── app.py
├── layer
│   ├── python
│   └── requirements.txt
└── main.tf

app.py

import feedparser
import os
from datetime import datetime
from linebot.v3 import (
    WebhookHandler
)
from linebot.v3.messaging import (
    Configuration,
    ApiClient,
    MessagingApi,
    TextMessage,
    BroadcastRequest
)

channel_access_token = os.getenv('CHANNEL_ACCESS_TOKEN')
channel_secret = os.getenv('CHANNEL_SECRET')

configuration = Configuration(access_token=channel_access_token)
api_client = ApiClient(configuration) 
line_bot_api = MessagingApi(api_client)
handler = WebhookHandler(channel_secret)

now = datetime.now()

# 対象RSS
feeds = [
    "https://aws.amazon.com/jp/about-aws/whats-new/recent/feed/"
]

def lambda_handler(event, context):
    # FeedParserDictオブジェクトを取得
    for feed in feeds:
        f = feedparser.parse(feed)
        # 各記事のlink、title、publishedを辞書に格納する
        dicts = [{'url': entry['link'], 'title': entry['title'], 'published': entry['published']} for entry in f['entries']]
        for dict in dicts:
            # publishedをstrからdatetime型へ変換
            dict_pudlished = datetime.strptime(dict['published'],'%a, %d %b %Y %H:%M:%S %z') # https://docs.python.org/ja/3/library/datetime.html
            # publishedの日時が24時間以内であれば、該当記事のlinkとtitleをブロードキャストメッセージで送信
            if now.timestamp() - dict_pudlished.timestamp() < 86400:
                line_bot_api.broadcast(
                    BroadcastRequest(
                        messages=[TextMessage(text = str(dict['title']) + str(dict['url']))]
                    )
                )
    return "OK"

requirements.txt

line-bot-sdk==3.7.0
feedparser==6.0.11

main.tf

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.42"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

#
# eventbridge
#

# EventBridgeを定期実行するルールを定義
resource "aws_cloudwatch_event_rule" "rss" {
  name                = "rss-by-terraform"
  schedule_expression = "cron(0 23 * * ? *)" # 毎日AM8:00
}

# EventBridgeがキックするターゲットを定義
resource "aws_cloudwatch_event_target" "rss" {
  rule      = aws_cloudwatch_event_rule.rss.name
  target_id = "rss-by-terraform"
  arn       = aws_lambda_function.rss_function.arn
}

#
# IAM
#

# LambdaがCloudWatch Logsにログを出力する権限を定義
resource "aws_iam_role" "iam_for_lambda" {
  name               = "iam_for_lambda-by-terraform"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
  managed_policy_arns = [
    "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
  ]
}

# LambdaがIAMロールを利用する権限を定義
data "aws_iam_policy_document" "assume_role" {
  statement {
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }

    actions = ["sts:AssumeRole"]
  }
}

#
# Layer
#

# 事前にリストアップした外部ライブラリをインストールする
resource "null_resource" "rss_layer" {
  provisioner "local-exec" {
    command = "pip3 install -r layer/requirements.txt --target=layer/python"
  }
}

# Lambdaレイヤーに必要なものをzip化する
data "archive_file" "rss_layer" {
  type        = "zip"
  source_dir  = "layer"
  output_path = "layer_by_tf.zip"
}

# Lambdaレイヤーを定義
resource "aws_lambda_layer_version" "rss_layer" {
  filename            = data.archive_file.rss_layer.output_path
  layer_name          = "rss_layer-by-terraform"
  source_code_hash    = data.archive_file.rss_layer.output_base64sha256
  compatible_runtimes = ["python3.12"]
}

#
# Function
#

# Lambda関数に必要なものをzip化する
data "archive_file" "rss_function" {
  type        = "zip"
  source_file = "app.py"
  output_path = "app_by_tf.zip"
}

# Lambda関数を定義
resource "aws_lambda_function" "rss_function" {
  depends_on = [
    aws_lambda_layer_version.rss_layer
  ]

  filename         = data.archive_file.rss_function.output_path
  function_name    = "rss-by-terraform"
  role             = aws_iam_role.iam_for_lambda.arn
  handler          = "app.lambda_handler"
  source_code_hash = data.archive_file.rss_function.output_base64sha256
  runtime          = "python3.12"
  timeout          = 30 #seconds
  layers = [
    aws_lambda_layer_version.rss_layer.arn
  ]
  environment {
    variables = {
      CHANNEL_SECRET       = "hoge"
      CHANNEL_ACCESS_TOKEN = "hoge"
    }
  }

  lifecycle {
    ignore_changes = [
      environment
    ]
  }
}

# EventBridgeがLambdaをキックする許可を定義
resource "aws_lambda_permission" "rss_function" {
  function_name = aws_lambda_function.rss_function.function_name
  action        = "lambda:InvokeFunction"
  principal     = "events.amazonaws.com"
  source_arn    = aws_cloudwatch_event_rule.rss.arn
}

# アカウントIDなどを取得
data "aws_caller_identity" "current" {}