仕事

【AWS無料枠2年目】0円のまま短縮URLを作った話|Lambda関数URL+DynamoDB

りょうまさんの「サーバ代ほぼ0円でWebアプリを動かす方法」という動画を見て、これは自分でもやってみたいと思った。(参照:https://www.youtube.com/watch?v=4j-SZ4LtHjQ)

ところが俺のAWSアカウントは何年も前に作ったもので、最初の12か月の無料枠はとっくに終わっている。動画と同じ構成で、本当に0円で済むのか?

ということで今回は、無料枠が終わった古いAWSアカウントで、請求0円のまま短縮URLを作ってみた。

どうも、スニーキーです。

まずは結果を見てほしい

請求$0.00、DynamoDBの無料枠の使用は約3%

/sneaky にアクセスすると、このブログのトップに飛ぶ短縮URLができた。

  • 構成:Lambda関数URL+DynamoDB(東京リージョン)
  • 請求:$0.00
  • AWSの「無料利用枠」画面では、DynamoDBの使用が約3%、「無料枠を超えたサービス:0/6」
無料利用枠の画面

古いアカウントで無料なのは「ずっと無料」の分だけ

AWSの無料枠には、作ってから12か月だけ無料のものと、ずっと無料のものがある。古いアカウントで使えるのは後者だけだ。(参照:https://aws.amazon.com/free/legacy/)

古いAWSアカウントで0円で使えるもの・使えないもの
  • ずっと無料:Lambda(月100万回+40万GB秒)、DynamoDB(条件あり)、SQS、SNSのメール通知など
  • 12か月で終わる:EC2、S3、RDS、API Gateway、EFS

ちなみに、2025年7月15日以降に作ったアカウントには、最大200ドル分のクレジットがもらえる新しい無料プランがある。でも、これは昔からアカウントを持っている人は対象外。俺も対象外だった。

最初にやったこと:予算アラート

何か作る前に、0.01ドルでも料金が出たらメールが来るようにした。請求とコスト管理 → 予算 → 予算を作成 →「ゼロ支出予算」のテンプレートを選ぶだけ。

ゼロ支出予算のテンプレート

通知だけの予算は無料。お金がかかるのは、自動で処理を止める「アクション」付きの予算が3つ目以降になったときと、予算レポートをメールで定期的に送るときだけらしい。(参照:https://aws.amazon.com/aws-cost-management/aws-budgets/pricing/)

予算ができた

動画の構成から変えたところ

動画の構成は、API Gateway → Lambda → EFS+SQLite。どれも使った分だけの課金なので、ほぼ0円で動く。

ただ、古いアカウントだとAPI GatewayとEFSは無料枠がもう使えない。東京リージョンだと、API Gateway(HTTP API)は100万回で1.29ドル、EFSは1GBで月0.36ドル。テスト程度なら1円にもならないけど、0円ではない。

動画の構成と、俺が作った構成

俺が変えた理由は3つ。

  • 完全に0円にしたかった:関数URLはLambdaの料金だけで追加料金がない。DynamoDBもずっと無料の枠がある
  • 短縮URLは「キーで1件引く」だけ:sneaky で探して転送先を1件返すだけなので、SQLで表をつなぐような処理はいらない。こういう使い方はDynamoDBが得意
  • 作るものが少ない:EFSを使うには、LambdaをVPCの中に置いてEFSにつなぐ設定が必要になる。DynamoDBならその準備がいらない

逆に、DynamoDBにすると困ることもある。あとから「クリック数の多い順に並べたい」「月ごとに集計したい」と思っても、SQLのように自由には検索できない。どう検索するかを先に決めておく必要がある。表をつないで集計するようなアプリなら、動画のSQLiteやRDSのほうが向いていると思う。

ステップ1:Lambda+関数URLで「Hello」

Lambda → 関数を作成 → 一から作成。関数名は shorturl-hello、ランタイムは Node.js 24.x にした。

「その他の設定」の中にある「関数URL」をオンにすると、関数にURLが付く。認証タイプは2つある。

  • AWS_IAM:AWSの認証を通った人だけが呼べる
  • NONE:誰でも呼べる

今回はブラウザで確かめたいのでNONEにした。NONEを選ぶと、誰でも呼べるようにする設定(ポリシー)をLambdaが自動で付けてくれる。

関数URLの認証タイプ

呼び出しモードはBUFFERED(初期値)のまま。もう一つのRESPONSE_STREAMは追加料金がかかる。

作った関数URLをブラウザで開くと「Hello from Lambda!」と出た↓

ブラウザで関数URLを開いた

API Gatewayと関数URL、どっちを使う?

AWSの公式ドキュメントでは、関数URLは「シンプルなアプリや試作向け」、API Gatewayは「本番の大きなアプリや、高度な機能が必要なとき向け」と整理されている。(参照:https://docs.aws.amazon.com/lambda/latest/dg/furls-http-invoke-decision.html)

関数URL API Gateway
料金 Lambdaの料金だけ 呼び出し回数などに応じて追加料金
認証 誰でも(NONE)か、IAMだけ IAMに加えてCognito、OAuth、自作の認証(Lambdaオーソライザー)など
回数制限 同時実行数の上限だけ(超えると429エラー) API・ステージ・メソッドごとに細かく制限できる
独自ドメイン なし(ランダムなURL) 使える
その他 CORS設定くらい 入力チェック、キャッシュ、WAF、APIキー、バージョン管理など

動画でも、次の4つがいらないなら関数URLでいい、と話していた。

  • 「1分間に10回まで」のような回数制限
  • Cognitoでのユーザー認証
  • WAFでの攻撃対策
  • 独自ドメイン

今回の短縮URLは、ランダムなURLのままで試すだけなので、関数URLで十分だと判断した。ちなみに関数URLでも、同時実行数の上限(予約済み同時実行)を決めておけば、上限を超えた分は429エラーで止められる。(参照:https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html)

逆に、ちゃんと人に使ってもらう本番のサービスにするなら、API Gatewayを前に置くと思う。

ちなみに、Lambdaのログ(CloudWatch Logs)は初期設定だと「失効しない」で、ずっと残り続ける。たまり続けるといつかお金がかかるので、保存期間を1週間にしておいた。

ログの保存期間を1週間に

ステップ2:DynamoDBにつないで短縮URLにする

落とし穴:初期設定のままだと無料枠の対象外

DynamoDBの無料枠は、読み書き各25ユニットと25GB。ただし「プロビジョンド」方式で「標準」テーブルクラスのときだけ。(参照:https://aws.amazon.com/dynamodb/pricing/provisioned/)

ところが、テーブル作成画面の「デフォルト設定」はオンデマンド方式になっている。なので「設定をカスタマイズ」を選んで、こう変えた。

  • テーブルクラス:DynamoDB標準
  • キャパシティーモード:プロビジョンド
  • Auto Scaling:読み・書きともオフ(オンだとCloudWatchのアラームが自動で作られる)
  • キャパシティ:読み書き各1ユニット
プロビジョンドと標準クラスを選ぶ

画面には「推定コスト $0.67/月」と出て焦った。でもこれは無料枠を引く前の定価。実際、同じ設定の古いテーブルが前から残っていたけど、請求は毎月0ドルだった。

推定コストは無料枠を引く前の値

無料枠に収まっているかは、請求とコスト管理 →「無料利用枠」で確認できる。DynamoDBの欄に「always free」と書いてあって安心した。

Lambdaに「読むだけ」の権限を付ける

LambdaからDynamoDBを読むには、Lambdaの実行ロールに権限を足す必要がある。今回は「shorturl-links テーブルを GetItem で読むだけ」にした。必要な分だけ許可するのが基本らしい。

IAMポリシーを作った

データを1件入れて、コードを書き換える

DynamoDBの画面から、code が sneaky、url が https://sneakyblog.com/ の項目を1件入れた。

DynamoDBに項目を追加

Lambdaのコードはこれ↓

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";

const db = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const TABLE_NAME = "shorturl-links";

export const handler = async (event) => {
  const code = (event.rawPath || "/").slice(1);

  if (!/^[A-Za-z0-9_-]{1,32}$/.test(code)) {
    return { statusCode: 404, body: "Not found" };
  }

  const { Item } = await db.send(
    new GetCommand({ TableName: TABLE_NAME, Key: { code } })
  );

  if (!Item?.url) {
    return { statusCode: 404, body: "Not found" };
  }

  return { statusCode: 302, headers: { Location: Item.url } };
};

/sneaky のようなURLから sneaky を取り出して、DynamoDBで探す。あれば302でそのURLに飛ばして、なければ404を返す。英数字・ハイフン・アンダースコア以外が入っていたら、DynamoDBに問い合わせずに404にしている。

curlで確かめた結果↓

/sneaky   → 302 https://sneakyblog.com/
/nothing  → 404
/         → 404

ちなみに、URLの登録はあえてDynamoDBの画面からしかできないようにした。誰でも登録できると、怪しいサイトへ飛ばす踏み台に使われるからだ。

ハマったところ

  • IAMのJSONエディタにポリシーを書いても「アクションが指定されていません」のままだった。ビジュアルエディタで、DynamoDB → GetItem → 対象テーブルを選んだら作れた
  • Lambdaの画面のコードエディタにうまく貼り付けられなかった。コードをzipにして「.zipファイルから更新」でアップロードした

一番焦ったのは、DynamoDBの作成画面で「$0.67/月」と出たところ。0円のつもりで進めていたので一瞬止まったけど、無料枠を引く前の値だと分かって安心した。料金の画面は、無料枠を引いた後の金額を出してくれるとは限らない。最後は「無料利用枠」の画面で確かめるのが一番確実だと思う。

0円を守るためのチェックリスト

古いAWSアカウントで0円を守るチェックリスト
  • 最初に予算アラート(0.01ドル超えで通知)を作る
  • DynamoDBは「プロビジョンド」+「標準クラス」
  • DynamoDBのAuto Scalingはオフ
  • 暗号化キーは「AWSが所有するキー」(カスタマーマネージドキーはKMSの料金がかかる)
  • ログの保存期間を決める
  • EC2・NATゲートウェイ・API Gateway・EFSは作らない
  • 作ったら「無料利用枠」の画面で使用率を見る

次にやること

次は、クリック数を記録できるようにしたい。転送を遅くしないように、SQS(処理を後回しにする行列)と2つ目のLambdaで記録する予定。SQSも月100万回までは無料だ。

古いアカウントでも、「ずっと無料」の範囲を選べば、ここまでは0円でできた。1つずつ「なぜこれを使うのか」を考えながら作ると、動画で見ただけのときより、構成の意味がかなり分かるようになった。

誰かの助けになれば嬉しい。

-仕事