S3を永続化層に使って、サーバレスで安価なURL短縮サービスを作ってみた(AWS lambda + API Gateway + CloudFront + S3)
はじめに
URL短縮サービスを作って遊んだので、terraformのコードを含めて共有します。
参考記事
以下の記事の構成を参考にしましたが、CloudFormationは使わずに一から作ってみました。
CloudFormationを使わなかった理由は、常用しているterraformでのコード管理との相性が悪いと感じたためです。
また、必要ない機能は極力省き、最小構成での実現を心がけました。
構成
CloudFrontにGETリクエストが来た際はS3の静的サイトホスティングの内容をそのまま返す。
S3の静的サイトホスティングではメタデータを使ったリダイレクトを設定してあるため、リダイレクトされる。
POSTリクエストが来た際は、API Gatewayからlambdaを起動し、S3のファイルを作成する。
リポジトリ
この記事では概要のみ説明するため、詳細な実装は以下のリポジトリを参考にしてください。
実際の手順
S3の作成
静的サイトホスティングに最低限必要なものがあれば十分です。
今回の例だとindex_documentにはアクセスされることを想定しないので、適当に設定しましょう。
また、S3にライフサイクルポリシーを設定することで、短縮されたURLを一定期間後に失効させることも可能です。(今回は設定していません)
lambdaの作成
s3:ListBucketの権限を付与しないと、ファイルの存在確認ができないことに注意する必要があります。
rubyのスクリプトは以下の通りです。URLの文字数はユースケースに応じて適宜変更してください。
# frozen_string_literal: true require 'aws-sdk-s3' require 'json' require 'securerandom' require 'uri' BUCKET_NAME = 'your-url-shortener' BUCKET_WEBSITE_URL = 'https://your-domain' def lambda_handler(event:, context:) s3 = Aws::S3::Resource.new url_str = event['url'] return { statusCode: 400, body: 'Invalid url parameter.' } unless valid_url?(url_str) loop do file_name = SecureRandom.alphanumeric(6) obj = s3.bucket(BUCKET_NAME).object(file_name) unless file_exists?(obj) obj.put(body: '', website_redirect_location: url_str, acl: 'public-read') return { statusCode: 200, body: "#{BUCKET_WEBSITE_URL}/#{file_name}" } end end end private def valid_url?(url_str) uri = URI.parse(url_str) uri.is_a?(URI::HTTP) && !uri.host.nil? rescue URI::InvalidURIError false end def file_exists?(obj) obj.get true rescue Aws::S3::Errors::NoSuchKey false end
API Gateway
lambdaをエンドポイントから叩けるようにするだけで、特に工夫する点はありません。
REST APIで作成しました。
CloudFront
ここまでで動かすだけならば必要はありません。
作成する際はAPI Gatewayのエンドポイントを叩き、その後はS3のリンクを直接使えば良いです。
ただ、ドメインが一つにならないことと、S3のリンクが長くて短縮の効率が下がってしまうことを避けるために、CloudFrontを使いました。
本番運用の場合はキャッシュを適切に設定することによって、非常にパフォーマンスの良いシステムになります。
感想
URL短縮サービスの構成として、永続化層にS3を使うというのは、非常に面白い発想でした。
集計周りにやや難がある以外は、安価でシンプルでスケーラブルという素晴らしい構成だと思います。
ただ、この構成はガチガチのベンダーロックインではあるので、そちらについては注意する必要がありそうです。