背景
Astro(SSG)で構築したコーポレートサイトに、Instagram の最新投稿を表示していました。ビルド時に Instagram Graph API から投稿を取得して、画像 URL を HTML に埋め込む仕組みです。
しばらくすると画像が表示されなくなりました。Instagram CDN の画像 URL には有効期限があり、数日で失効します。SSG はビルド時に URL を静的に焼き込むので、時間が経つと壊れたリンクだけが残ってしまいます。
再ビルドすれば一時的に直りますが、根本的な解決にはなりません。
さらに、Instagram Graph API のアクセストークンにも有効期限があります。長期トークンでも最大60日で失効するため、定期的にリフレッシュしないと API 自体が叩けなくなります。画像 URL の永続化に加えて、トークンの自動更新も必要でした。
トークンの取得
Instagram Graph API を利用するには、対象の Instagram アカウントと Meta Developer アプリの設定が必要です。
認証方式の選択
Instagram Graph API には2つの認証方式があります。
- Instagram API with Facebook Login — Facebook ページとの連携が必要。ページ管理やインサイト等の高度な機能が使える
- Instagram API with Instagram Login — Facebook ページ不要。投稿の取得やメディア管理など基本的な機能に対応
今回は投稿画像の取得だけが目的なので、Facebook ページ連携が不要な Instagram Login 方式を使いました。
Meta Developer アプリの設定
- Meta Developer Dashboard でアプリを作成
- アプリに「Instagram」プロダクトを追加
- 「Instagram ログインによる API 設定」画面で対象の Instagram アカウントを接続
短期トークンの生成
Meta Developer Dashboard → アプリ → 左サイドバー「Instagram」を開くと、接続済みの Instagram アカウントが一覧で表示されます。「トークンを生成」ボタンをクリックすると短期トークン(約1時間有効)が発行されます。
長期トークンへの交換
短期トークンのままでは使えないので、長期トークンに交換します。
curl "https://graph.instagram.com/access_token?\
grant_type=ig_exchange_token&\
client_secret={App Secret}&\
access_token={短期トークン}"
レスポンスの access_token が60日有効の長期トークンです。アクセストークンデバッガーで有効期限を確認できます。
動作確認
curl "https://graph.instagram.com/me/media?\
fields=id,caption,media_type&\
access_token={長期トークン}"
投稿一覧が返ってくれば、トークンは正しく取得できています。この長期トークンを Worker の初期トークンとして設定し、以降は cron で自動リフレッシュされます。
全体構成
最終的な構成はこのようになりました。
┌──────────────────────────────────────────────────┐ │ Astro SSG(Cloudflare Pages) │ │ │ │ ビルド時: │ │ 1. Worker /feed → Instagram Graph API │ │ 2. 画像を R2 にアップロード(既存ならスキップ) │ │ 3. 不要画像を R2 から削除 │ │ 4. R2 の永続 URL で HTML に埋め込み │ └──────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────┐ │ Cloudflare Worker(instagram-proxy) │ │ │ │ GET /feed → KV キャッシュ or Instagram API │ │ cron → 月2回トークンリフレッシュ(KV保存) │ └──────────────────────────────────────────────────┘ ┌───────────────┐ ┌───────────────┐ │ Cloudflare │ │ Cloudflare │ │ R2 │ │ KV │ │ 画像保存 │ │ トークン/ │ │ (永続URL) │ │ フィードキャッシュ│ └───────────────┘ └───────────────┘
使っている Cloudflare サービスは4つです。
- Pages — サイトホスティング(SSG ビルド + デプロイ)
- Workers — Instagram API プロキシ + トークン管理
- R2 — 画像の永続ストレージ(S3 互換)
- KV — アクセストークンとフィードのキャッシュ
R2 への画像キャッシュ
ビルド時に Instagram API から取得した画像を R2 にコピーして、HTML には R2 の URL を埋め込みます。
// r2.ts import { HeadObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; const s3 = new S3Client({ region: "auto", endpoint: `https://${accountId}.r2.cloudflarestorage.com`, credentials: { accessKeyId, secretAccessKey }, }); export async function uploadImageToR2( imageUrl: string, key: string, ): Promise<string | null> { // 既に R2 にあればスキップ try { await s3.send(new HeadObjectCommand({ Bucket: bucket, Key: key })); return `${publicUrl}/${key}`; } catch { // 存在しない → アップロードに進む } const res = await fetch(imageUrl); if (!res.ok) return null; await s3.send( new PutObjectCommand({ Bucket: bucket, Key: key, Body: Buffer.from(await res.arrayBuffer()), ContentType: res.headers.get("content-type") || "image/jpeg", }), ); return `${publicUrl}/${key}`; }
キーは instagram/{account}/{postId}.jpg の形式です。投稿 ID ベースなので、同じ画像を二重にアップロードすることはありません。
ビルドのたびに R2 には現在の投稿に対応する画像だけが残るよう、不要になった画像も削除しています。ListObjectsV2 で prefix 配下のキーを列挙して、現在のアクティブなキーセットに含まれないものを DeleteObjects で一括削除します。
Worker によるトークン自動リフレッシュ
Instagram の長期アクセストークンは60日で失効します。手動で更新し続けるのは現実的ではないので、Cloudflare Worker の cron trigger で自動化しました。
# wrangler.toml [triggers] crons = ["0 0 1,15 * *"] # 毎月1日・15日 00:00 UTC
Worker の scheduled ハンドラがトークンリフレッシュ API を叩いて、新しいトークンを KV に保存します。
async function refreshToken(env: Env): Promise<void> { const token = await getToken(env); if (!token) return; const url = `https://graph.instagram.com/refresh_access_token?grant_type=ig_refresh_token&access_token=${token}`; const res = await fetch(url); if (!res.ok) return; const json = (await res.json()) as { access_token: string }; await env.INSTAGRAM_KV.put("instagram_token:main", json.access_token); }
初回のトークンだけは手動で wrangler secret put で設定します。以降は KV に保存されたトークンを cron が自動でリフレッシュし続けてくれます。
Worker の /feed エンドポイント
Worker は Instagram API のプロキシも兼ねています。GET /feed でフィードを取得して、結果を KV に6時間キャッシュします。Astro のビルド時はこのエンドポイントを叩くだけで済みます。
ビルドのたびに Instagram API を直接叩くとレートリミットが気になりますが、KV キャッシュを挟むことで API コール数を抑えられます。
Cloudflare の設定
コード以外に必要な Cloudflare 側の設定をまとめます。
R2 バケット
- Cloudflare Dashboard → 「R2 オブジェクトストレージ」→「バケットを作成」
- バケット名を入力して作成
- 作成したバケット →「設定」→「パブリック開発 URL」を有効化
有効化すると https://pub-{hash}.r2.dev 形式のパブリック URL が発行されます。これが R2_PUBLIC_URL になります。
R2 API トークン
- 「R2 オブジェクトストレージ」→ Overview ページ右側の「Account Details」→「API Tokens」横の「Manage」
- 「Create API Token」→ 権限を「Object Read & Write」、バケットを作成したものに限定して作成
- 表示される Access Key ID と Secret Access Key を控える(この画面を閉じると再表示できません)
KV namespace
Worker のトークン・フィードキャッシュ用に KV namespace を作成します。
cd workers/instagram-proxy npx wrangler kv namespace create INSTAGRAM_KV
出力される id を wrangler.toml に設定します。
[[kv_namespaces]] binding = "INSTAGRAM_KV" id = "{発行された ID}"
Worker のデプロイ
# 初回トークンを secret として設定 npx wrangler secret put INSTAGRAM_INITIAL_TOKEN # プロンプトが出るので長期トークンを貼り付け # デプロイ npx wrangler deploy
環境変数一覧
Astro ビルド(Cloudflare Pages)に必要な環境変数です。Cloudflare Dashboard → Pages → プロジェクト →「設定」→「環境変数」で設定します。
| 変数名 | 値の例 | 説明 |
|---|---|---|
INSTAGRAM_WORKER_URL |
https://xxx.workers.dev |
Worker のエンドポイント URL |
R2_ACCOUNT_ID |
effd8f1e... |
Cloudflare アカウント ID(Dashboard URL から取得) |
R2_ACCESS_KEY_ID |
d1c496... |
R2 API トークンの Access Key ID |
R2_SECRET_ACCESS_KEY |
4096a7... |
R2 API トークンの Secret Access Key |
R2_BUCKET_NAME |
my-assets |
R2 バケット名 |
R2_PUBLIC_URL |
https://pub-{hash}.r2.dev |
R2 パブリック URL |
おわりに
Cloudflare の無料枠だけで、Instagram 画像の永続化とトークン管理が完結しました。R2 は10GB/月まで無料、Workers は10万リクエスト/日まで無料なので、個人サイトやコーポレートサイト程度の規模なら費用はかかりません。
SSG + 外部画像 CDN の組み合わせでは、URL の有効期限に注意が必要です。Instagram に限らず、同様の問題が起きるサービスでは R2 / S3 への中間キャッシュが有効な手段になります。