20
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

コネヒトAdvent Calendar 2019

Day 2

サーバーレスでURL短縮サービスを40分で作ってリリースした話

Last updated at Posted at 2019-12-01

この記事は コネヒト Advent Calendar 2019 2日目 の記事です。

はじめに

社内のSlackでURL短縮サービスが自社にあればよいなー、という呟きがあったので、昼休みに一人ハッカソンをしてみた時の話です。(※実運用はしていないです)

概要

https://s.hyiromori.com/ にアクセスするとURL短縮用の画面が表示されて https://s.hyiromori.com/[5文字の乱数] のようなURLを発行できるようなWebアプリを作りました。

30247f56-ec47-f361-a930-826730cdc916.png

ドメインが短くないと、あまり短縮された感じがないですが、まだ実験段階なのでご勘弁ください。
ソースは GitHub にあります。

技術スタック

使い慣れている serverless フレームワークを使って、作ってみました。
使用しているAWSのサービスは以下のようになります。

タイムライン

コミットをサボっていたので、覚えている限りで書きます。

1. プロジェクトの作成

何個か作ったことがあったので、以下のファイルを別プロジェクトからコピペしました。

.eslint.json とかもコピペしましたが、本質ではないので割愛します。

2. serverless.yml の定義

DynamoDBの定義

DynamoDBはAWSマネージドなNoSQLデータベースです。 serverless と相性が良いです。

serverlessresource セクションは CloudFormation で書くので、公式ドキュメント を参考に記述します。

serverless.yml
resources:
  Resources:
    ShortenTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: serverless-shorten
        AttributeDefinitions:
          - AttributeName: key
            AttributeType: S
        KeySchema:
          - AttributeName: key
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 3
          WriteCapacityUnits: 3

ここでは、短縮したURLのキーを key という名前のハッシュキーで格納する、純粋なKey-Valueストアとして定義しています。
一応ですが、IAMポリシーも定義しておかないとアクセスできないので注意してください(詳しくはGitHubリポジトリを参照してください)

エンドポイントの定義

functions セクションに handler (Lambdaのエントリポイント)を記述して events にHTTPの設定らしきものを書くと、serverless が自動的に API GatewayLambda を設定してくれます。便利!

serverless.yml
functions:
  index:
    handler: handler/index.handler
    events:
      - http:
          method: GET
          path: ''
          cors: true
  post:
    handler: handler/post.handler
    events:
      - http:
          method: POST
          path: ''
          cors: true
  get:
    handler: handler/get.handler
    events:
      - http:
          method: GET
          path: '{key}'
          cors: true

ここでは、以下の3つのエンドポイントを定義しています。

  • GET / : URL変換を実行するためのHTMLを返すエンドポイント
  • POST / : URL変換を実行して、短縮したURLを返すAPI用エンドポイント
  • GET /{key} : 短縮URLでアクセスして、リダイレクトするエンドポイント

3. エンドポイントの実装1: GET /

表示用の HTML を返却するだけのエンドポイントです。

index.js
const html = `
<!doctype html>
<html lang="ja">
  <!-- 省略 -->
</html>
`;

module.exports.handler = async () => ({
  statusCode: 200,
  headers: {
    'Access-Control-Allow-Origin': '*',
    'Content-Type': 'text/html',
  },
  body: html,
});

面倒くさかった 時間なかったので、JSにそのままHTMLを記述してそれを返しています。
当然、別ファイルにした方が良いですので、あまり真似しないでください。
いつか暇な時に対応しようと思います。

4. エンドポイントの実装2: POST /

短縮URLを生成するエンドポイントです。
キーとなる5文字の乱数を生成して、ポストされたURLと一緒に DynamoDB に保存して、短縮したURLを返却しています。
ちなみに、もし乱数が衝突しても無言で上書きします😨
今回はとりあえず作ることを目的にしているので省略しますが、ちゃんと運用する時はチェックを忘れないようにしましょう!

post.js
const crypto = require('crypto');
const { DynamoDB } = require('aws-sdk');

// DynamoDB のクライアントの設定
const client = new DynamoDB.DocumentClient({ apiVersion: '2012-10-08', region: 'ap-northeast-1' });

module.exports.handler = async (event) => {
  const { Origin } = event.headers;
  const { url } = JSON.parse(event.body);

  // URLをチェックして、不正なURLの場合は400を返却する
  if (!url.match(/^(http|https):\/\/([\w-]+\.)+[\w-]+(\/[\w-./?%&=]*)?$/g)) {
    return {
      statusCode: 400,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ error: 'ERROR: Invalid URL.' }),
    };
  }

  // ランダムなキーを生成する
  const key = crypto.randomBytes(10).toString('base64').replace(/[+/]/g, '').substr(0, 5);

  // DynamoDB にキーとURL情報を保存する
  const params = {
    TableName: process.env.DYNAMODB_TABLE,
    Item: { key, url },
  };
  await client.put(params).promise();

  // 短縮したURLを返却する
  const payload = {
    url: `${Origin}/${key}`,
  };
  return {
    statusCode: 200,
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload),
  };
};

5. エンドポイントの実装3: GET /{key}

短縮URLでアクセスされ、元のURLにリダイレクトするためのエンドポイントです。
アクセスされたパスをキーに、元のURLを DynamoDB から取得して 302 でリダイレクトさせています。

const { DynamoDB } = require('aws-sdk');

// DynamoDB のクライアントの設定
const client = new DynamoDB.DocumentClient({ apiVersion: '2012-10-08', region: 'ap-northeast-1' });

module.exports.handler = async (event) => {
  const { key } = event.pathParameters;

  // キーに該当するURLを取得する
  const params = {
    TableName: process.env.DYNAMODB_TABLE,
    Key: { key },
  };
  const res = await client.get(params).promise();

  // 該当するURLが見つからない場合は404を返却する
  if (!res || !res.Item || !res.Item.url) {
    return {
      statusCode: 404,
      headers: { 'Content-Type': 'text/plain' },
      body: '404 Not Found',
    };
  }

  // 該当のURLにリダイレクトさせる
  const { url } = res.Item;
  return {
    statusCode: 302,
    headers: {
      'Content-Type': 'text/plain',
      Location: url,
    },
    body: `Redirect to: ${url}`,
  };
};

※ 「DynamoDB のクライアントの設定」あたりは PUT / と同じ処理ですが、理解しやすくするためにワザと重複を許容しています。本来はこういう共通処理は切り出してまとめたほうが良いです。

6. デプロイ

AWS CLI の設定ができていれば npm run deploy とコマンドを打てば一発でデプロイできます!
ちなみに私は aws-vault をAWSの認証情報を管理しているので、こんな感じで実行しています。

aws-vault exec [profile] -- npm run deploy

とてもセキュアになるので、非常におすすめです。
これもいつか記事にしよう。

7. 完成

https://s.hyiromori.com/ にアクセスして、動作することを確認します。

30247f56-ec47-f361-a930-826730cdc916.png

生成された s.hyiromori.com/IDuhK にアクセスすると無事 https://example.com/ にリダイレクトされました!

おわりに

いかがでしたか?
serverless を使うと、こんなに手軽に実装&デプロイができますよー。
そして、インフラの管理がいらない&費用も(アクセスがあまりないので)ほとんどかからない、と良いことたくさんです!
皆さんも、とりあえずなんかWebサービス作ってみたいと思ったら、serverless 使ってみてくださいね!

今後の課題

  • ドメイン短くしたい!
  • アクセス数とか記録しておくと良さそう。
  • ついでに分析とかまでできると完璧!

PR

コネヒトでは、一緒に働いてくれるエンジニア募集中です!興味があればこちらもみてください!
パパも大活躍!!ママリのwebフロントエンドを支えるエンジニア募集!

※追記(2019/12/02 13:30)

継続的に運用するつもりはないので、アドベントカレンダー終了後にクローズ予定です。
クローズしました。(2019/12/27 23:00)

20
21
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?