プル型ではなくプッシュ型でAWSの最新情報をキャッチアップしたいと思い、身近なツールであるLINEで通知するアプリケーションをつくってみた。
仕組みとしては、RSSで配信されているAWSの最新情報を、EventBridge + Lambdaで定期的に確認し新しいものがあればLINE通知する、といったもの。
キーワードは以下の通り。
- RSS
- Messaging API(LINE)
- Terraform
- Lambda + EventBridge
準備運動
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 APIをPythonで利用するにあたっては、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()
feeds = [
"https://aws.amazon.com/jp/about-aws/whats-new/recent/feed/"
]
def lambda_handler(event, context):
for feed in feeds:
f = feedparser.parse(feed)
dicts = [{'url': entry['link'], 'title': entry['title'], 'published': entry['published']} for entry in f['entries']]
for dict in dicts:
dict_pudlished = datetime.strptime(dict['published'],'%a, %d %b %Y %H:%M:%S %z')
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" {}