URL短縮サービスを API Gateway + Lambda で


概要

URL短縮サービスを AWS API Gateway + Lambda で実装した例になります。
データストアには DynamoDB を使用しています。
掲載しているコードはこちらでも公開しています。

セットアップ

デプロイにはServerlessフレームワークを使うのでなければインストールします。
serverless-hooks-pluginはデプロイコマンドをフックするためのプラグインです。

mkdir simple-url-shortener && cd simple-url-shortener
npm install -g serverless
npm install serverless-hooks-plugin

以下で使用するライブラリのGemfileを作っておきます。

Gemfile
gem 'aws-record'
gem 'hashids'

ハンドラーの作成

今回作成するルートは2です。
1つはURLを登録して短縮URLにして返す/url/shortener
もう一つは短縮URLへアクセスした時に登録したURLへリダイレクトさせる/です。

短縮URLモデル

URLの保存先にはDynamoDBを使います。
aws-recordというDynamoDBのAPIラッパーを使って短縮URLを表すモデルを作成します。

models/url_shortener.rb
require 'aws-record'

class UrlShortener
  include Aws::Record

  set_table_name ENV['DYNAMODB_TABLE']

  string_attr :id, hash_key: true
  string_attr :short_url
  string_attr :long_url
  string_attr :date

  # Check url valid
  def valid?
    url = begin
            URI.parse(long_url)
          rescue StandardError
            false
          end
    url.is_a?(URI::HTTP) || url.is_a?(URI::HTTPS)
  end
end

URLを登録して短縮URLを生成

ランダムなIDの生成にはHashidsというライブラリを使っています。

routes/url.rb
require_relative '../models/url_shortener'
require 'aws-record'
require 'hashids'

def handler(event:, context:)
  data = JSON.parse(event['body'])

  # Check whether longUrl is valid
  url = UrlShortener.new
  url.long_url = data['longUrl']

  return { statusCode: 401, body: 'Invalid long url' } unless url.valid?

  # Create url code
  now = Time.now

  hashids = Hashids.new(ENV['HASHIDS_SALT'])
  hash = hashids.encode(now.to_i, now.usec)

  base_url = ENV['BASE_URL'] || "https://#{event['headers']['Host']}/#{event['requestContext']['stage']}"
  url.id = hash
  url.short_url = "#{base_url}/#{url.id}"
  url.date = now.to_s

  begin
    url.save
  rescue Aws::Record::Errors::RecordError => e
    puts e
    { statusCode: 500, body: e }
  end

  { statusCode: 200, body: url.short_url }
end

短縮URLへアクセスしたら登録URLへリダイレクト

routes/index.rb
require_relative '../models/url_shortener'

def handler(event:, context:)
  code = event['pathParameters']['code']
  begin
    url = UrlShortener.find(id: code)
  rescue Aws::Record::Errors::NotFound => e
    puts e
    { statusCode: 404, body: 'URL not found' }
  rescue Aws::Record::Errors::RecordError => e
    puts e
    { statusCode: 500, body: 'Internal server error' }
  end

  { statusCode: 301, headers: { Location: url.long_url } }
end

デプロイ

serverless.yml を作成してデプロイコマンドを実行します。

serverless.yml
service: simple-url-shortener

provider:
  name: aws
  region: ${opt:region, 'us-east-1'}
  runtime: ruby2.5
  environment:
    DYNAMODB_TABLE: url-shortener-${opt:stage, 'dev'}
    HASHIDS_SALT: 'This is my salt'
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource: 'arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}'

plugins:
  - serverless-hooks-plugin

custom:
  hooks:
    package:initialize:
      - bundle install --deployment

functions:
  redirectOriginal:
    handler: routes/index.handler
    events:
      - http:
          path: /{code}
          method: get
  shortener:
    handler: routes/url.handler
    events:
      - http:
          path: url/shortener
          method: post
          cors: true

resources:
  Resources:
    UrlShortenerTable:
      Type: 'AWS::DynamoDB::Table'
      Properties:
        TableName: ${self:provider.environment.DYNAMODB_TABLE}
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1

これで準備が整ったのでいよいよデプロイです。

bundle
sls deploy

テスト

デプロイしたAPIをテストしてみます。
返って来たURLをブラウザで開いてみてhttps://example.comに飛んでいれば成功です。

curl -d '{"longUrl":"https://example.com"}' https://{api}.execute-api.{region}.amazonaws.com/url/shortener

さいごに

今回作成したAWSのリソースは以下のコマンドで削除できます。

sls remove