概要

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 を作っておきます。

gem 'aws-record'
gem 'hashids'

ハンドラーの作成

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

短縮 URL モデル

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

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というライブラリを使っています。

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 へリダイレクト

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 を作成してデプロイコマンドを実行します。

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