🗞️

40円/月でAWS Lambda + OpenAI + GNewsで毎朝7時に自動ニュース要約をSlackに配信する

に公開1

はじめに

「毎朝、自分の使っている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

全体アーキテクチャ

このシステムは以下のような流れで動作します:

  1. EventBridgeのcronルールが毎朝7時(JST)にLambda関数を起動
  2. Lambdaが GNews APIから複数カテゴリのニュースを取得
  3. 取得したニュースタイトルをOpenAI APIに送信して要約を生成
  4. 生成された要約を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関数を作成します:

  1. AWSコンソール → Lambda → 関数の作成
  2. 「一から作成」を選択
  3. 関数名を入力(例:daily-news-summarizer
  4. ランタイムに「Python 3.12」を選択
  5. 「関数の作成」をクリック

作成後、以下の設定を行います:

  • 「コード」タブ → 「アップロード元」→ 「.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秒のスリープを挿入してレート制限に引っかからないようにしています。

Endpointhttps://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_function.py
"""
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つの処理を行っています:

  1. GNewsからニュース記事の取得

    • カテゴリ別に記事を取得し、レート制限を回避するためのスリープを挟む
    • 各記事のタイトルとURLを保存
  2. OpenAIによる要約生成

    • 全記事をプロンプトとしてOpenAI APIに送信
    • システムプロンプトで「ニュース編集者」としての役割を指定
    • 適切な要約を生成
  3. Slackへの通知

    • Block Kit形式で見やすいメッセージを構成
    • Webhook経由でSlackチャンネルに投稿

requirements.txt

依存関係は非常にシンプルです。

requirements.txt
requests

requestsライブラリ1つだけで実装することで、デプロイパッケージのサイズを最小限に抑え、依存関係の複雑さも回避しています。

EventBridge で毎朝7:00 JST実行

AWS EventBridgeを使って、Lambda関数を毎朝決まった時間に実行するようにスケジュールします。

AWSコンソール

AWSコンソールから設定する場合:

  1. AWSコンソール → EventBridge → ルール → 「ルールの作成」
  2. 名前を入力(例:daily-news-schedule
  3. ルールタイプは「スケジュール」を選択
  4. スケジュールパターンで「cron式」を選択
  5. スケジュール式に cron(0 22 * * ? *) を入力
    (これはUTC 22:00 = JST 07:00を意味します)
  6. ターゲットで「AWS サービス」→「Lambda関数」を選択
  7. 関数に先ほど作成したLambda関数を指定
  8. 「ルールの作成」をクリック

これで毎朝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

rakiraki

例えばGitHubに処理のコード(python)を置いているなら、EventBridge と Lambda じゃなくて、GitHub Actions の定期実行で済んじゃうと思うんだけど、AWSサービスに乗せた理由は何かありますか?