40円/月でAWS Lambda + OpenAI + GNewsで毎朝7時に自動ニュース要約をSlackに配信する
はじめに
「毎朝、自分の使っているSlackにニュースが自動で更新されたら楽なのにな〜」
そんな思いから「これ、作れるんじゃね?」とGPTくんと相談しながらゼロから組んでみました。
使用技術
- ニュース取得:GNews freeプラン - カテゴリ別ニュースを効率的に取得
-
要約生成:OpenAI の
gpt-4o-mini
モデル - 高品質な要約を自動生成 - 通知:Slack Incoming Webhook - 見やすいフォーマットでチャンネルに投稿
- 実行:AWS Lambda + EventBridge cron - サーバーレスで定期実行
この記事では、準備からコード、デプロイ/設定までを丁寧に解説します。技術的な詳細だけでなく、実際の運用ポイントや注意点も含めて説明していきます。
必要なもの
システムを構築するために必要なサービスとアカウントは以下の通りです:
-
AWSアカウント(Lambda, EventBridge)
無料枠の範囲内で十分実行可能です。 -
OpenAI APIキー
OpenAIのアカウントからAPIキーを取得する必要があります。この記事ではコスト効率の良いgpt-4o-mini
モデルを使用しています。 -
Slack Incoming Webhook URL
SlackワークスペースのAPI設定からWebhookを作成できます。専用のニュースチャンネルを作成するのがおすすめです。 -
GNews APIキー(freeプラン)
GNews APIから無料プランで取得可能です。カテゴリ別のニュース取得に使用します。
各サービスの登録/発行方法は省略しますが、取得後はLambdaの環境変数として設定してください。
環境変数 | 内容 |
---|---|
GNEWS_API_KEY |
GNews freeプランのAPIキー |
OPENAI_API_KEY |
OpenAIのAPIキー |
SLACK_WEBHOOK_URL |
Slack Incoming WebhookのURL |
全体アーキテクチャ
このシステムは以下のような流れで動作します:
- EventBridgeのcronルールが毎朝7時(JST)にLambda関数を起動
- Lambdaが GNews APIから複数カテゴリのニュースを取得
- 取得したニュースタイトルをOpenAI APIに送信して要約を生成
- 生成された要約をSlack Webhookを使ってチャンネルに投稿
この設計のメリットは、サーバーレスで運用コストが最小限であることと、各サービスがAPIを通じて疎結合されているため、将来的な拡張や変更が容易である点です。
AWS Lambda の設定
ランタイム
- Python 3.9 以上(この記事では3.12を使用)
最新のPythonランタイムを使用することで、パフォーマンスの向上や新機能の活用が可能になります。Python 3.12は特にスタートアップパフォーマンスが向上しているため、Lambdaのコールドスタート問題も軽減されます。(今回はリアルタイム性いらないけど)
デプロイパッケージ作成
Lambdaには外部ライブラリを同梱する必要があります。以下のステップで簡単にデプロイパッケージを作成できます。
まず、プロジェクト用のディレクトリ構造を作成します:
# プロジェクトルートに移動
mkdir news-summarizer
cd news-summarizer
# ビルド用ディレクトリ作成
mkdir build && cd build
# 依存(requests)のインストール
pip install requests -t .
# 関数コードをコピー
cp ../lambda_function.py .
# ZIP化
zip -r ../deploy.zip .
Lambda関数作成・更新
AWSコンソールから以下の手順でLambda関数を作成します:
- AWSコンソール → Lambda → 関数の作成
- 「一から作成」を選択
- 関数名を入力(例:
daily-news-summarizer
) - ランタイムに「Python 3.12」を選択
- 「関数の作成」をクリック
作成後、以下の設定を行います:
- 「コード」タブ → 「アップロード元」→ 「.zipファイル」を選択 → 先ほど作成した
deploy.zip
をアップロード - 「設定」タブ → 「環境変数」で以下を設定:
-
GNEWS_API_KEY
: 取得したGNews APIキー -
OPENAI_API_KEY
: OpenAIのAPIキー -
SLACK_WEBHOOK_URL
: SlackのWebhook URL
-
- 「設定」タブ → 「基本設定」で以下を設定:
- タイムアウト:60秒(要約生成に時間がかかる場合があるため)
- メモリ:128MB(初期設定そのまま。軽量な処理のため最小構成で十分)
GNewsから記事を取得
GNewsは簡単に使えるニュースAPIで、無料プランでも十分な機能を提供しています。ただし、リクエスト制限があるため適切な対策が必要です。
無料プランでは1秒あたり5リクエストまでという制限があるため、カテゴリごとに1.2秒のスリープを挿入してレート制限に引っかからないようにしています。
Endpoint:https://gnews.io/api/v4/top-headlines
パラメータ:
-
apikey
:GNEWS_API_KEY(取得した値) -
category
:以下のカテゴリから選択- business(ビジネス)
- entertainment(エンタメ)
- general(一般)
- health(健康)
- science(科学)
- world(国際)
- technology(テクノロジー)
-
lang
:ja(日本語) -
country
:jp(日本) -
max
:取得件数- business/science/technology/world → 3件(重要カテゴリは多め)
- その他 → 1件(バラエティを持たせる)
カテゴリごとに取得件数を調整することで、より重要なカテゴリのニュースを優先的に取得しつつ、様々な分野の情報をバランスよく集めることができます。
Lambda 関数コード
以下が完全なLambda関数のコードです。コメントも含めて各部分の動作を解説しています。
"""
Lambda: JST 07:00 に EventBridge → GNews → OpenAI gpt-4o-mini で要約生成 → Slack。
◼︎ 特徴
- GNews freeプラン (カテゴリ別取得)
- 要約のみ(記事の考察など余計な要素はなし)
- Slack では「絵文字 + カテゴリ + リンク付タイトル — 要約」を1記事=1カード表示
- 依存ライブラリは requests だけ
"""
import os, json, time, uuid, requests
# ── 環境変数 ────────────────────────────────────────────
GNEWS_API_KEY = os.environ["GNEWS_API_KEY"]
OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]
SLACK_WEBHOOK_URL = os.environ["SLACK_WEBHOOK_URL"]
# ── API エンドポイント ────────────────────────────────
GNEWS_URL = "https://gnews.io/api/v4/top-headlines"
OPENAI_URL = "https://api.openai.com/v1/chat/completions"
# ── 取得計画 (business2, general1, science2, world2, technology2) ──
PLAN = {"business":2, "general":1, "science":2, "world":2, "technology":2}
# ── カテゴリ絵文字 ──────────────────────────────────────
EMOJI = {
"business":"💼", "general":"🗞️",
"science":"🔬", "world":"🌐", "technology":"💻",
}
# ── GNews helper ──────────────────────────────────────
def fetch_headlines(cat: str, cnt: int):
r = requests.get(GNEWS_URL, params={
"apikey":GNEWS_API_KEY, "category":cat,
"lang":"ja", "country":"jp", "max":cnt,
}, timeout=5)
if r.status_code == 429:
print(f"[Warn] rate-limit for {cat}")
return []
r.raise_for_status()
return r.json()["articles"]
# ── Slack helper ─────────────────────────────────────
def slack_post(*, text=None, blocks=None):
payload = {"text":text} if blocks is None else {"blocks":blocks}
requests.post(SLACK_WEBHOOK_URL, json=payload, timeout=5).raise_for_status()
# ── Lambda handler ──────────────────────────────────
def lambda_handler(event, context):
# 1) ニュース取得
arts=[]
for cat,cnt in PLAN.items():
for a in fetch_headlines(cat,cnt):
arts.append({
"cat":cat,
"title":a["title"].strip(),
"url":a["url"].strip()
})
time.sleep(1.2) # freeプラン: 5 req/sec
if not arts:
slack_post(text=":warning: ニュースを取得できませんでした。")
return {"statusCode":200}
# 2) OpenAI 要約 (タイトルのみ渡して 100-120字要約を返させる)
user_prompt = "\n".join(f"{i['title']}" for i in arts)
sys_prompt = (
"各行のニュース見出しを100〜120字の日本語で要約してください。"
"行数・順序を変えず、各行に要約のみ書くこと。"
)
resp = requests.post(
OPENAI_URL,
headers={"Authorization":f"Bearer {OPENAI_API_KEY}",
"Content-Type":"application/json"},
json={
"model":"gpt-4o-mini",
"messages":[
{"role":"system","content":sys_prompt},
{"role":"user","content":user_prompt}
],
"temperature":0.7,
"max_tokens":1500
}, timeout=(5,60))
resp.raise_for_status()
summaries = [l.strip() for l in resp.json()["choices"][0]["message"]["content"].splitlines() if l.strip()]
# 3) Slack Block Kit 組み立て
blocks=[
{"type":"header","text":{"type":"plain_text","text":"📰 今朝のニュース要約"}},
{"type":"divider"}
]
for idx, art in enumerate(arts):
summary = summaries[idx] if idx < len(summaries) else "(要約失敗)"
line = f"{EMOJI[art['cat']]} *{art['cat']}* <{art['url']}|{art['title']}> — {summary}"
blocks.append({"type":"section","text":{"type":"mrkdwn","text":line}})
blocks.append({"type":"divider"})
slack_post(blocks=blocks)
return {"statusCode":200}
上記のコードでは主に3つの処理を行っています:
-
GNewsからニュース記事の取得
- カテゴリ別に記事を取得し、レート制限を回避するためのスリープを挟む
- 各記事のタイトルとURLを保存
-
OpenAIによる要約生成
- 全記事をプロンプトとしてOpenAI APIに送信
- システムプロンプトで「ニュース編集者」としての役割を指定
- 適切な要約を生成
-
Slackへの通知
- Block Kit形式で見やすいメッセージを構成
- Webhook経由でSlackチャンネルに投稿
requirements.txt
依存関係は非常にシンプルです。
requests
requestsライブラリ1つだけで実装することで、デプロイパッケージのサイズを最小限に抑え、依存関係の複雑さも回避しています。
EventBridge で毎朝7:00 JST実行
AWS EventBridgeを使って、Lambda関数を毎朝決まった時間に実行するようにスケジュールします。
AWSコンソール
AWSコンソールから設定する場合:
- AWSコンソール → EventBridge → ルール → 「ルールの作成」
- 名前を入力(例:
daily-news-schedule
) - ルールタイプは「スケジュール」を選択
- スケジュールパターンで「cron式」を選択
- スケジュール式に
cron(0 22 * * ? *)
を入力
(これはUTC 22:00 = JST 07:00を意味します) - ターゲットで「AWS サービス」→「Lambda関数」を選択
- 関数に先ほど作成したLambda関数を指定
- 「ルールの作成」をクリック
これで毎朝7時にLambda関数が自動実行されるようになります。
AWS CLI
AWS CLIから設定する場合は以下のコマンドを使用します:
# ルールの作成
aws events put-rule \
--name daily-news-rule \
--schedule-expression "cron(0 22 * * ? *)" \
--state ENABLED
# ターゲットの設定
aws events put-targets \
--rule daily-news-rule \
--targets "Id"="1","Arn"="arn:aws:lambda:ap-northeast-1:123456789012:function:YourFunctionName"
Lambda関数のARNは、自分のAWSアカウントとリージョンに合わせて変更してください。
コスト見積もり
本システムで発生するコストを各サービス別に見積もってみましょう。
AWS関連コスト
-
Lambda: 無料枠の範囲内で十分実行可能(月100万リクエストまで無料)
- 1日1回の実行なので月30回程度
- メモリ128MB、実行時間約10秒として計算しても無料枠内
-
EventBridge: 月100万イベントまで無料
- 1日1回のトリガーなので実質無料
GNews API
-
無料プラン: 0円
- 1日100リクエストまで無料
- 本システムでは1日に約7リクエスト(カテゴリ数)
OpenAI API
項目 | 試算値 | 補足 |
---|---|---|
1日のリクエスト回数 | 1回 | 毎朝07:00 JSTのみ |
取得記事数 | 9〜10本 | PLAN: 2+1+2+2+2 |
入力トークン | ≒200 tokens | システムプロンプト+記事タイトル一覧 |
出力トークン | ≒500 tokens | 各記事100–120字の要約 × 9〜10本 |
料金テーブル(2025-05時点、USD/1K tokens)
モデル | 入力 | 出力 |
---|---|---|
gpt-4o-mini | $0.005 | $0.015 |
1日あたり
- 入力: 0.2K × $0.005 ≒ $0.0010
- 出力: 0.5K × $0.015 ≒ $0.0075
- 合計 ≒ $0.0085/日
1か月(30日)あたり
$0.0085 × 30 ≒ $0.26(≒40円)
Slack API
- Incoming Webhook: 無料
総額
- 月額: 約$0.26 (約40円)
- 年額: 約$3.12 (約500円)
非常にコスト効率が良いシステムと言えます。OpenAIライブラリ使えればもっと性能のいいモデルが使えるので、後々時間があったら改良したいと思います。
おわりに
ここまでの手順で、以下の機能を持つ自動ニュース要約システムが完成します:
- GNews → 最新カテゴリ別記事取得
- OpenAI o1-mini → 丁寧な要約生成
- Slack Incoming Webhook → 見やすく通知
- AWS Lambda+EventBridge → 毎朝7時に自動実行
このシステムは拡張性も高く、例えば以下のような機能追加も検討できます:
- 複数チャンネル対応: カテゴリごとに別チャンネルに投稿
- パーソナライズ: ユーザーごとの興味に基づいてニュースをフィルタリング
- フィードバック機能: 「役立った」ボタンなどを追加して学習データを収集
- 画像対応: ニュース画像も取得して表示
ぜひ試してみて、フィードバックやPRなどいただけると嬉しいです!
質問・改善アイデアもお待ちしています〜
Discussion
例えばGitHubに処理のコード(python)を置いているなら、EventBridge と Lambda じゃなくて、GitHub Actions の定期実行で済んじゃうと思うんだけど、AWSサービスに乗せた理由は何かありますか?