CloudFrontだけで短縮URLサービス作ってみた。

## はじめに
短縮URLは、オンラインの情報共有において欠かせない存在になっています。
しかし、その便利さの裏でセキュリティ上の問題も指摘されていることがあります。
例えば、QRコードを介した不正サイトへの誘導事例などが報告されています。

[原因は「短縮URL」か？　QRコードから不正サイトへ誘導される事例が相次ぐ　オートバックスセブン、学習院大学も](https://www.itmedia.co.jp/news/articles/2311/15/news194.html)

こういったこともあり、エンジニアの皆様は自作されることも多いのではないでしょうか？

自作短縮URLサービスに関して様々なアーキテクチャがある中、CloudFront大好きな私は、エッジロケーションで完結するのでは？と考えました。

**そう、CloudFront　KeyValueStore ＋ CloudFront Functionsならね。**

URLの実態をCloudFront　KeyValueStoreに保存し、CloudFront FunctionsでそのURLと共に301を返せばいいではありませんか！

これが実現できれば低レイテンシーかつ迅速なURL変換が可能になります！

## 各種バージョン
```bash
$ node --version
# v18.8.0
$ npm --version
# 8.18.0
$ yq --version
# yq (https://github.com/mikefarah/yq/) version 4.25.2
$ jq --version
# jq-1.6
$ task --version
# Task version: v3.33.1 (h1:JJSRANHH7RQrr5Z2CTvSnTH7iWlfBlKV2W2O0JiZoLk=)
$ aws --version
# task aws-cli/2.15.8 Python/3.11.7 Darwin/23.2.0 source/x86_64 prompt/off
$ terraform -v
# Terraform v1.5.1
$ terragrunt -v
# terragrunt version v0.46.2
```
:::note warn
AWS CLIに関して
Key Value Store系のAPIは比較的新しいためバージョンによっては実行できない場合があります。
エラーが出たら場合はアップデートしてください。
:::

## 構成

リポジトリはこちら↓

https://github.com/iisyos/aws_short_url

下記のような構成を採用しています。

![名称未設定ファイル-ページ1.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/759541/109448ea-8487-6cf3-155f-4c5d9dab0c84.png)

大まかな流れは以下の通りです。
1. Clientより`Path: /{slug}`へのリクエスト
2. Viewer Requestに対してCloudFront  Functionsを起動
4. slugをKeyにKeyValueStoreから短縮URLを取得
5. それを元にリダイレクト

また便利のため`/`にアクセスした場合は、フロント画面を表示します。
入力値を元にPutKey APIを実行し、実際のURLを保存後、短縮URLを返します。
↓こんな感じです。
![スクリーンショット 2024-01-06 23.46.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/759541/bbd8b85c-d676-7c99-7443-6c4dc4b23488.png)

本来は短縮URLを運用する際、CloudFrontに手持ちのドメインを紐づけて更なる短縮化を図りますが、今回はめんど…ゴホンゴホン。割愛します。

いくつか参考になる記事を添えておくので許してください。

https://qiita.com/kanadeee/items/752e20af5b071ef0e011

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/CNAMEs.html

## デプロイ
IaCはTerragruntを採用しました。
stateを保存するバケット名を変数化したかったのと、ドメインの部分をmodule分割して管理しようと考えていましたが、めんど…ゴホンゴホン。

Cloudfront KeyValue StoreはTerraform未対応のため一部AWS CLIで作成します。
マージされたらTerraform化する予定です。

https://github.com/hashicorp/terraform-provider-aws/pull/34879

また、TaskでTerragruntやAWS CLIを実行します。

https://taskfile.dev/ja-JP/

### 1. 前処理
```bash
$ task prepare:main
```
Cloudfront KeyValueStoreを作成と環境変数の設定を行います
環境変数はTerragruntのBackendや、Cloudfront KeyValueStoreのARN等です。
すでにS3バケットがある場合は、`env.yaml`の`bucket`を変更してください。

### 2. デプロイ
```bash
$ task deploy:main
```
やっていることは単純で
1. `npm install`
2. `terragrunt appply`
3. Cloudfront FunctionsにCloudfront KeyValueStore紐付け
4. Cloudfront Functionsをデプロイ

です。

:::note info
メモ
AWS SDK for JavaScript v3にて、CloudFrontKeyValueStoreClientを使用する際、下記エラーが発生したため備忘録ですー。
> ERROR	Error occurred: InvalidSignatureException: Credential should be scoped to a valid region.

解決法：https://github.com/aws/aws-sdk-js-v3/issues/5576
:::


### 3. 確認
terraformのoutputでCloudFrontのドメインを出力しているため、それをブラウザに貼り付けてフロント画面を確認します。

もしくは下記コマンドで…。
```bash
task open
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/759541/83597ac6-2f1c-649c-b1de-af9d13689681.png" width=400><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/759541/21c82794-fb7f-5822-46c9-43a338d309db.png" width=400>

```bash
$ curl -IL https://dyhbm4vb9b4ve.cloudfront.net/YfUl3w
HTTP/2 301 
server: CloudFront
date: Sat, 06 Jan 2024 15:01:19 GMT
content-length: 0
location: https://example.com
x-cache: FunctionGeneratedResponse from cloudfront
via: 1.1 eb9ac5638287dc15c1aa46bb047a0b9e.cloudfront.net (CloudFront)
x-amz-cf-pop: KIX50-P3
x-amz-cf-id: dQhkOP6PYqMb-TEZlKNa2S9dpqP99hJKVsqZkKF2yWqyGOcMVz8XpQ==

HTTP/2 200 
content-encoding: gzip
accept-ranges: bytes
age: 180257
cache-control: max-age=604800
content-type: text/html; charset=UTF-8
date: Sat, 06 Jan 2024 15:01:19 GMT
etag: "3147526947"
expires: Sat, 13 Jan 2024 15:01:19 GMT
last-modified: Thu, 17 Oct 2019 07:18:26 GMT
server: ECS (sac/2520)
x-cache: HIT
content-length: 648
```
OKそうですね！

## 困ったこと

#### 反映のタイミング
CloudFront KeyValueStoreの更新後、関連する関数が新しい環境変数を利用できるようになるまで、全てのCloudFrontエッジロケーションに変更が伝わるのに数秒かかります。
このため、Put Key操作後すぐに短縮URLを利用しようとすると、一時的に403エラーが返される場合があります。
これは運用に応じて調整が必要ですが、即時アクセスが求められる場面では考慮が必要です。

#### 容量制限
KeyValueStoreの容量は最大5MBまでとなっており、大規模な運用には制限があるかもしれません。
ただ、1つのURLが1KBと想定すると、約5000件のURLを保存できるため、個人使用には十分かなと思います。

#### **キャッシュ** 
CloudFront FunctionsはLambda@Edgeと異なり、Origin Request/Responseのタイミングでの実行は行われません。
そのため、CloudFrontのキャッシュ機能を活用することはできず、毎回CloudFront Functionsが実行されます。
費用は1,000,000回の呼び出しにつき0.10 USDであり、最初の1年間は毎月2,000,000回までの呼び出しが無料です。
そのため、個人利用の範囲では費用面での問題はほとんどないと思います。

## まとめ

制限はありましたが、なんとか実現できました👏

今後、CloudFront FunctionsでRDB操作できる未来を期待しています！✨

ご指摘ご意見ございましたらお気軽にコメントください！
特にTaskは初めて書いたので、お作法に則っているか不安です（笑）

最後に、後処理を忘れずに！٩( ᐛ )و
```bash
task destroy:main
```

## 後日談

Cloudflare… D1…？