VPC Interface Endpoint S3経由でのStatic File利用

# はじめに
ALB + ECSの構成におけるStatic Fileの管理方法には、複数の選択肢があります。
例えば、以下のような方法です。
- appとともに直接ecs内部に格納
- S3で管理し、CloudFront + S3の構成とする

前者の場合、static fileを直接更新するには、appのdeployが必要になります。
後者の場合、CloudFrontを利用するため、client視点では通信経路が増え、社内申請や管理が煩雑になるところもあります。

以下のいくつかの要件に対応できる案を考えた。
- Static FileをS3で管理
- CloudFrontを利用せず、追加Domainなし
- S3へのアクセスはPrivateLinkを経由

# 構成図(簡略版)及び注意点
#### appとともに直接ecs内部に格納の場合(比較するため)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470230/fafcf6df-f355-d488-52aa-bc833cd997f4.png)

## 考案(VPC interface endpoint経由)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470230/8127771c-f1c3-d415-8809-3d4ba2ad2fa1.png)

## 【注意点】Bucket名
:::note warn
Bucket名は、ALBのDNS名と同じにしないといけない
:::
- ALB DNS: 検証のため、今回の検証では単にAWS public DNS名 「internal-alb-s3-vpce-123456789.ap-northeast-1.elb.amazonaws.com」を利用。実際に閉域網における利用は、Route 53のPrivate Host Zoneを利用する必要
- Bucket名：「internal-alb-s3-vpce-123456789.ap-northeast-1.elb.amazonaws.com」

# 構築リソース(ECS、ECRは別途構築、ここでは省略)
- VPC Interface Endpoint for S3
- Bucket
- Target Group
- ALB

## VPC Interface Endpoint for S3
Target Groupを作成する際に利用するため、EndpointのIPアドレス 「10.1.1.196」をメモしておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470230/1b2daae9-503d-80c0-221a-80e2c6f235ac.png)


## Bucket
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470230/aa89a430-344e-11a9-3813-de95bc4c6f0b.png)

- Bucket Policy
VPC Interface Endpointからのアクセスを許可する。

```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject4",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::internal-alb-s3-vpce-123456789.ap-northeast-1.elb.amazonaws.com/*",
            "Condition": {
                "StringEquals": {
                    "aws:SourceVpce": "vpce-...."
                }
            }
        }
    ]
}
```

## TargetGroup
Target Groupを作成し、IPモードでVPC Interface EndpointをTarget Groupに登録します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470230/a990af72-1586-201d-b6d3-ba3c4b1d2a07.png)


## ALB
以下の通り、Listener rulesを作成します。
- Path Patternが「/static/*」の場合、上記にて作成したVPC Interface EndpointのTarget Groupにforward
- それ以外の場合、別途作成済みのECSのTarget Groupにforward

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470230/e7807c53-d5bf-6547-32c1-4e472080878e.png)

# DEMO
## 説明
このDEMOでは、S3に保存されたJavaScriptファイルを更新することで、クライアント側の画像のsrcが動的に変更され、表示する画像も変わることを確認します。

## APP構成(Container化し、ECSで利用)
```
│  app.js
└─public
        index.html
```

### index.html
初期表示時、"dogImage"の画像(300.jpg)がALB経由で取得すると記述しました。

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dog Image Button</title>
    <script defer src="https://internal-alb-s3-vpce-123456789.ap-northeast-1.elb.amazonaws.com/static/js/script.js"></script>
    <style>
        body {
            text-align: center;
            font-family: Arial, sans-serif;
        }
        img {
            max-width: 100%;
            height: auto;
            margin-top: 20px;
        }
    </style>
</head>
<body>
    <h1>Click the Button to See a Dog</h1>
    <button id="changeDog">Show Another Dog</button>
    <br>
    <img id="dogImage" src="https://internal-alb-s3-vpce-123456789.ap-northeast-1.elb.amazonaws.com/static/image/300.jpg" alt="Random Dog">
</body>
</html>

```

## Bucket folder構成
static folderの配下にimage及びjsを配置します。

```
> aws s3 ls s3://internal-alb-s3-vpce-123456789.ap-northeast-1.elb.amazonaws.com/static --recursive --human-readable --summarize | cut -c 32-

static/image/300.jpg
static/image/400.jpg
static/image/500.jpg
static/js/script.js
```

### script.js
Click eventが発生した際に、画像を「400.jpg」に変更します。

```javascript
document.addEventListener("DOMContentLoaded", function () {
    const button = document.getElementById("changeDog");
    const image = document.getElementById("dogImage");

    button.addEventListener("click", function () {
        // 新しいランダムな犬の画像を取得
        const newImageUrl = `https://internal-alb-s3-vpce-123456789.ap-northeast-1.elb.amazonaws.com/static/image/400.jpg`;
        image.src = newImageUrl;
    });
});

```

## 実際の表示
### 初期画面表示
画像300.jpgが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470230/347b19c8-716c-4604-a058-12dd4e321624.png)

### Dog画像変更
Button Clickすると、画像が400.jpgに変わります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470230/fb16c498-4eaf-4a5a-bb4f-52fed6f9b12d.png)

### S3側のscript.jsを置き換え
```diff
- const newImageUrl = `https://internal-alb-s3-vpce-123456789.ap-northeast-1.elb.amazonaws.com/js/400.jpg`;

+ const newImageUrl = `https://internal-alb-s3-vpce-123456789.ap-northeast-1.elb.amazonaws.com/js/500.jpg`;

```

### 初期画面表示
同様に画像300.jpgが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470230/976cd03d-e69f-4d9d-8370-77e394da06bf.png)


### Dog画像変更
Button Clickすると、画像が500.jpgに変わります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470230/fa6cd593-90dc-46db-bd0e-53a27ae3053e.png)


## Other
Dog画像の元URL：https://placedog.net/