🖼️

CloudflareImagesの画像をWorkersでキャッシュしてお小遣いを守る

2022/11/24に公開6

まずは

👇前回の記事です。読んでみてください。
https://zenn.dev/ddpn08/articles/cloudflare-images-pricing

そこでこんなコメントをいただきました。

画像キャッシュに関して、確証はないですがそれっぽい記事ならあります。
https://tech.smartcamp.co.jp/entry/price-optimization-using-cloudflare-workerkv

こ、これだ!!!!
これで全てが解決する!!(たぶん)

ということでぱぱっと実装します。

つかったもの

  • typescript
  • hono | つよいライブラリ

仕組み

紹介いただいた記事ではKVを使っていますが、今回は普通にedgeにキャッシュします。

仕組みはこんな感じ。(Cloudflareをあまり理解しきれてないのでまあまあおかしな図になっているかもしれませんがご了承ください。)

  1. ユーザーが画像をCloudflareWorkersにリクエスト
    👇
  2. Workersがキャッシュを確認、あればそれを返す。
    👇
  3. キャッシュがなかった場合、CloudflareImagesから画像を取得
    👇
  4. 取得した画像を返す&キャッシュ

max-ageは画像の種類によって分けます。
投稿の画像は編集できない仕組みになっているので変化することはないから1か月程、
ユーザーのプロフィール画像などは更新する可能性があるので4時間キャッシュしています。

コードを書こう

import { Hono } from 'hono'
import type { StatusCode } from 'hono/utils/http-status'

export const delivery = new Hono()

const CLOUDFLARE_IMAGES_URL = 'cloudflare-images-url'
const CACHE_NAME = 'aivy:imagedelivery'
const CACHE_CONTROL = [
    {
        prefix: 'post.image',
        maxAge: 60 * 60 * 24 * 30,
    },
    {
        prefix: 'user.',
        maxAge: 60 * 60 * 4,
    },
]

delivery.use('*', (c, next) => {
    c.set(CLOUDFLARE_IMAGES_URL, `https://imagedelivery.net/${c.env['ACCOUNT_HASH']}`)
    return next()
})

delivery.delete('/caches/:id/:variant', async (c) => {
    const imagedelivery = await caches.open(CACHE_NAME)
    const res = await imagedelivery.delete(c.req.url.replace('/caches', ''))
    if (res) return c.body(`The cache for ${c.req.url} has been deleted.`)
    else return c.body('Cache not found', 404)
})

delivery.get('/:id/:variant', async (c) => {
    const { id, variant } = c.req.param()
    const imagedelivery = await caches.open(CACHE_NAME)
    const url = new URL(`${c.get(CLOUDFLARE_IMAGES_URL)}/${id}/${variant}`)

    const cacheControl = CACHE_CONTROL.find((v) => id.startsWith(v.prefix)) || {
        maxAge: 60 * 60 * 24 * 2,
    }

    const cached = await imagedelivery.match(c.req.url)
    if (cached) {
        cached.headers.forEach((value, key) => c.header(key, value))
        c.header('aivy-cache-status', 'HIT')
        return c.body(cached.body, cached.status as StatusCode)
    }

    const image = await fetch(url)

    if (!image.ok)
        return c.body(
            image.body,
            image.status as StatusCode,
            Object.fromEntries(image.headers.entries()),
        )

    image.headers.forEach((value, key) => {
        if (key.toLowerCase() !== 'cache-control') c.header(key, value)
    })
    c.header('aivy-cache-status', 'MISS')
    c.header('cache-control', `max-age=${cacheControl.maxAge}`)
    const res = c.body(image.body)
    const cache = res.clone()

    c.executionCtx.waitUntil(imagedelivery.put(c.req.url, cache))

    return res
})

仕組みでいった内容をばばっとコードにしただけです。
DELETEリクエストを送ってキャッシュの削除もできるようにしておきました。

Honoとっても使いやすくて好きです。けどなんかc.req.param()の型がうまく反映されなくて困ってます。

👇リポジトリ
https://github.com/aivy-run/imagedelivery

結果

1日10万配信ほどだったのがなんと、1000配信程度に抑えることができました!!!
ほんとに助かりました!今月はまあまあな額行きましたが来月からは何とかなりそうです!

新たな課題

Aivyについてのお話です。

前回の記事にも追記したのですが、最近よくSupabaseのインスタンスが落ちるようになりました。
どうやらメモリ使用率が高くなりすぎてクラッシュしているよう...
現在原因究明、コードの改善を行っているのですが、今回SQLを初めて触るものでなかなか苦戦しています。
もしよかったらコードを公開しているのでアドバイスいただけるととても助かります...
https://github.com/aivy-run/aivy

とりあえずしばらくはProにアップグレードして応急処置しようかなとも考えてます;

最後に

👇ぜひ使ってみて下さい!!
https://aivy.run

Discussion

yusukebeyusukebe

Honoの作者です!

Honoとっても使いやすくて好きです。

ありがとうございます!!

けどなんかc.req.param()の型がうまく反映されなくて困ってます。

これ、効いてるっぽいのですがだめですか!!

SS

だだっこぱんだだだっこぱんだ

作者様!コメントありがとうございます!
なるほど、たしかにcodespacesで試してみたら効いてますね
となると僕の環境の問題かもしれません;
追記しておきます!

inaridiyinaridiy

インスタンスが落ちる問題なのですが、ランキングなどの大半の人が同じクエリを走らすものをキャッシュすると多少改善すると思います!
また、最終手段だと思うのですが、クエリに関係ないデータをDBから逃がせば少なくともDBは軽くなる気がします。

だだっこぱんだだだっこぱんだ

ありがとうございます!
たしかに、アクセスするたびに同じクエリを走らせているのが最大の原因かもしれません;
DBのキャッシュをとる方向で進めようと思います!

TFTF

例のコメントしたものです。すぐに実装してしまうとは凄いですね。ぜひ、Web3、NFTなどもチャレンジしてみてください。

だだっこぱんだだだっこぱんだ

コメントありがとうございました!ほんとに助かりました!
今後もいろいろ新しいことにチャレンジしていきます!!!