生成AIを活用した環境構築をやってみた　第5回：WEBコンテンツを公開する仕組み

# 1. はじめに

「生成AIを活用して過去の写真の情報をもとに旅行プランを生成する」仕組みの実現に関する記事の第5回です。記事は参加したメンバと分担して作成しており、今回は私が担当します。

前回の記事までで旅行プランの生成・htmlファイルとしての出力までが実現できました。今回の記事ではWebコンテンツを公開する仕組み作りにチャレンジします。

構成のベースも生成AIに考えてもらい、生成AIの回答は正しいとは限らないという前提のもと生成AIに提示された構成を参考にして調査して環境を作り上げていきます。生成AIに大枠を考えてもらい、AWS認定資格の学習で身に着けた知識で良し悪しを判断する形で進めます。



関連記事
[生成AIを使った仕組みを考えてみた 第1回：アイデア出し編](https://qiita.com/harasawa3A/items/c191d5262f31af8c566f)
[生成AIを使った仕組みを考えてみた 第2回：写真の内容を解析しデータを貯める仕組み](https://qiita.com/mSatoCubesystem/items/0fe5a220e44bfda4516d)
[生成AIを使った仕組みを考えてみた 第3回：蓄積されたデータを基に旅行プランを生成する（蓄積データの活用編）](https://qiita.com/dMoriCubeSystem/items/b680048f6959052b8c99)
[生成AIを使った仕組みを考えてみた 第4回：蓄積されたデータを基に旅行プランを生成する（音声ファイル編）](https://qiita.com/dMoriCubeSystem/items/eeb7d4b1412d22974862)



## 要件・構成の検討
今回作りたい仕組みは、記事の第3回、4回でS3バケットに出力したhtmlをコンテンツとしたWebページをインターネットに公開する仕組みです。この部分にフォーカスして検討を行います。

構成を検討するにあたり極力AWSサービスのサービス名を指定しない形で生成AIに問い合わせてみました。ポイントとして生成AIによる「もっともらしい嘘」に惑わされないよう「細かい解説は不要」の一文を入れています。

``` 
AWSでWEBコンテンツを公開する仕組みを作りたいです。
極力少ないAWSサービスで実現する方法を教えてください。細かい解説は不要です。

要件
１．HTTPSでWEBブラウザからアクセスできます。
２．このコンテンツはAWS環境にアップロードすることで公開されます。
３．このコンテンツにはユーザID及びパスワードでのアクセス制限を行います。
４．このコンテンツは、特定の拠点からのアクセスの場合認証なしで閲覧できます。
５．このシステムは、独自のドメインを使用しAWSで管理します。
```

## 生成AIの回答
``` 
以下の構成をお勧めします：

Amazon S3: コンテンツのホスティング。
Amazon CloudFront: HTTPSアクセスの提供とコンテンツ配信。
AWS Certificate Manager (ACM): SSL/TLS証明書の管理。
Amazon Route 53: 独自ドメインのDNS管理。
Lambda@Edge: ユーザー認証の実装と特定の拠点からのアクセス制御。
この構成により、最小限のAWSサービスで要件を満たすことができます。
```

今回使用するサービスは、上記の5つであるという回答でした。
なるほど、見た感じ全体構成として考えたものとほぼ同じですね。


## 構成イメージ
文章だけだと想像しにくいので、構成のイメージを作成しました。
今回作る範囲は以下の黄色い枠の範囲です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/eae43ac2-92fd-4d59-bca1-532d1c261cb8.png)

# 2. 仕組みの構築
作成したイメージをもとにそれぞれのサービスの設定を行います。

## 2.1 ドメインの準備
この仕組みはインターネットから使用するシステムとしたいため、Route 53のパブリックホストゾーンを使用します。今回は既存のRoute 53で管理しているドメインを使用しています。新規取得ではないため、こちらの記事ではRoute 53への移管については割愛します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/b67c154b-7f76-4f61-a85b-7d17842586d0.png)




## 2.2 SSL/TLS証明書の取得と設定
HTTPSアクセスする際に使用される証明書を、AWSが提供する証明書管理サービスであるAWS Certificate Manager (ACM) で発行します。ACMではパブリック証明書を無料で発行できます。

### AWS Certificate Manager (ACM) で証明書を取得

※今回取得する証明書は、CloudFront用となります。CloudFrontで使用する証明書は米国東部 (バージニア北部) リージョン (us-east-1) の証明書である必要があるので注意しましょう。

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/cnames-and-https-requirements.html#https-requirements-aws-region


ACMコンソールへ遷移しバージニア北部リージョンであることを確認できたら、「リクエスト」を押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/6059c6c2-d9ed-4e3c-8f02-61b154fadff4.png)


「証明書をリクエスト」の画面で、「パブリック証明書をリクエスト」を選択し次へを押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/18f3e6c6-be1c-48bc-b471-e550a046d550.png)


今回のサイトで使用するドメイン名を入力して、その他項目はデフォルト値のままリクエストを押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/466614b3-1f08-41e1-9b65-35e6cae4b1e8.png)

証明書の発行リクエストにより証明書が作成されますが、この段階ではステータスが「保留中の検証」となります。今回はDNSを用いてドメインの所有者であることの検証を行います。

「Route 53でレコードを作成」を押下します。



![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/f7deaa87-39e0-4ea9-80e4-fb4e60e4243f.png)


画面が遷移しますが、この画面だと検証が成功したかが分かりません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/76b61cd4-7b5f-4b80-bffb-cd39b5cfe0c1.png)


再度、作成した証明書の画面に遷移しましょう。ステータスが発行済となっているはずです。無事、DNSによる検証が終わり証明書の発行ができました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/fb9ff345-a5f2-4274-b03b-84ec39da83d6.png)



# 2.3 Amazon CloudFront の設定
この仕組みでは、前回の記事で作成したS3バケットを参照するようにCloudFrontの設定を行います。S3単体でもWebホスティング機能はあるのですが、S3に格納したコンテンツをHTTPSで公開するにはCloudFrontを使用する必要があります。この設定により、インターネットから作成したコンテンツを参照できるようにできます。

### CloudFrontディストリビューションの作成
CloudFrontコンソールより、ディストリビューションを作成を押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/0feac391-a35c-4557-b73a-c4c5473f7236.png)


オリジンに、前回の記事で作成したS3バケットを指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/b0c1d0df-9a7e-4229-ba13-94b2edc0f828.png)


オリジンアクセスには「Origin access control settings」を指定します。この設定により一般公開していないS3バケットのコンテンツを今回使用したCloudFrontから参照できるようにします。
「Create new OAC」を押下し、任意の名前で作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/52ee0322-6b00-4155-89f6-ff69739502c2.png)





![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/931b9180-db07-4bd0-a0ec-839fbc66b1ad.png)


「設定」の「代替ドメイン名（CNAME）」へ証明書取得時に設定したドメイン名を指定し、「Custom SSL certificate」で取得した証明書を指定しましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/c49e3ca9-63a2-4a56-8a17-0d99dbf6cd94.png)

S3に格納されているファイルは現時点では見られたら困る内容でないため、動作確認しやすいようアクセス制限の設定は後から行うこととします。S3バケットの指定、オリジンアクセス、ドメインの設定、証明書の指定以外はデフォルト設定のまま「ディストリビューションの作成」を押下するとCloudFrontディストリビューションが作成されます。

画面上部にメッセージが出ていますが、S3のバケットポリシーを更新する必要があります。S3バケットに対して、CloudFrontからのアクセスを許可するポリシーの設定を行いましょう。「ポリシーをコピー」を押下すると、設定すべきポリシーがクリップボードにコピーされます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/f8037d2f-6c69-4eb8-8b24-7c3e9faaed68.png)

コピーされるポリシーは以下のもので、今回作成したCloudFrontからのみ当該S3バケットのオブジェクトを取得することを許可するようなものとなっているようですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/2c302a3b-59ff-4359-9317-bf777d0de0fd.png)


S3コンソールより今回使用するS3バケット指定し、コピーしたポリシーの設定を行います。
json形式でのポリシー作成は難しそうですが、今回はコピーしたものをペーストするだけです。恐れずに「編集」を押下しましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/297ccacb-7572-4f1e-abc8-67f55a73b939.png)

ポリシーエディタにペーストして、「変更の保存」を押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/dda87ba0-f134-4e57-b269-d02dd767ee0d.png)

これだけでS3側の設定は完了となります。

### 動作確認

この時点でアクセスできるか確認してみましょう。「ディストリビューションドメイン名」に記載されているドメイン名をWebブラウザに入力して、前回出力したhtmlを指定すると以下のようにインターネット経由でhtmlファイルを参照することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/9f25eb86-be19-4973-a244-8c7dc8d606e1.png)

続いてCloudFrontで指定した代替ドメイン名を指定してアクセスをしてみましょう。
ページに到達できませんね。現時点で実施したCloudFrontの代替ドメイン名の設定だけでは、ドメインとCloudFrontの紐づけが出来ていない状態となっているようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/6d8a0c2f-737a-44da-84b5-8ef85e4e5e3c.png)

# 2.4 Amazon CloudFront へのドメイン名の紐づけ

指定したドメインを使用して作成したCloudFrontディストリビューションにアクセスするための設定を行います。

### CNAMEレコードの作成
CloduFrontへアクセスする際に利用するカスタムドメインの設定を行います。先ほど画面確認したCloudFrontディストリビューションの「ディストリビューションドメイン名」を控えておきましょう。



Route 53コンソールより、今回使用するホストゾーンを指定した後「レコードを作成」を押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/a926e1d5-5d9d-472a-89a6-91250dec6ad7.png)

レコードタイプを「CNAME」とし、値には控えておいたCloudFrontディストリビューションの「ディストリビューションドメイン名」を設定しレコードを作成しましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/f52c1402-4861-4366-a700-8ea9e6b4ce9f.png)

### 動作確認
Webブラウザより、設定したドメインを使用してアクセスしてみましょう。
想定通りアクセスすることが出来ました。ACMで発行した証明書が適用されていることも確認できますね！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/f1acb8a9-debf-4c55-bd72-68181edf6956.png)

ここまででS3に格納したhtmlファイルをインターネット経由でアクセスできる仕組みを作ることが出来ました。インターネットへの情報公開は難しいものにも思いがちですが、結構簡単に実現できました。


# 2.5 認証設定
今のままでは、インターネットアクセスができる人であれば誰でもアクセスできてしまいます。今回の仕組みで公開する情報は自分向けの旅行プランなので、他の人に見られると自分の趣向がバレて恥ずかしいかもしれません。そこでアクセスできる人を限定する仕組みを設定します。

CloudFront利用時に認証の設定をする方法としてはCognitoの利用、Lambda@Edge、CloudFront Functionなどの方法があります。今回は、生成AIに推奨されたLambda@Edgeという機能を使用して実現していきます。

### Lambda@Edge ファンクションを作成
マネジメントコンソールの検索窓よりLambdaを検索し、Lambda管理画面へ遷移します。

今回作成するLambda関数はCloudFrontに紐づけるものであるため、バージニアで作る必要があります。マネジメントコンソールでバージニアリージョンとなっていることを確認したうえで、関数を作成を押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/97869e79-13e2-49a1-a829-6c4b89f2b911.png)

任意の関数名を設定し、ランタイムはPythonを指定します。実行ロールは新規のロールを作成する設定とし、「関数の作成」を押下します。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/49a1be4b-598c-4963-a372-fdd8b9c9d412.png)



### ユーザーIDとパスワードによる認証ロジックの実装

今回使用するロジックは生成AIが生成してくれた以下のものです。コメントで記載されている認証情報を設定する箇所を書き換えれば対応できそうです。

``` python
import base64

def lambda_handler(event, context):
    request = event['Records'][0]['cf']['request']
    headers = request['headers']
    
    # 認証情報を設定
    auth_user = 'username'
    auth_pass = 'password'
    auth_string = f'{auth_user}:{auth_pass}'
    auth_string_enc = base64.b64encode(auth_string.encode()).decode()

    if 'authorization' in headers:
        auth_header = headers['authorization'][0]['value']
        encoded = auth_header.split(' ')[1]

        if encoded == auth_string_enc:
            return request
        else:
            return _response_403('Invalid credentials')
    else:
        return _response_401()

def _response_401():
    return {
        'status': '401',
        'statusDescription': 'Unauthorized',
        'headers': {
            'www-authenticate': [{
                'key': 'WWW-Authenticate',
                'value': 'Basic'
            }]
        }
    }

def _response_403(message):
    return {
        'status': '403',
        'statusDescription': 'Forbidden',
        'body': message
    }

```


認証情報を任意のものに書き換え、Lambdaのエディタ部分に貼りつけDeployを押下します。CloudFrontと関連づけるため、トリガーを追加を押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/984a2d15-ff9a-40cc-8c53-90f16f7fadaa.png)

トリガーにCloudFrontを指定し、Lambda@Edgeへのデプロイを押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/c5063f98-4ede-426b-b715-71d7d6a344dd.png)


今回作成したCloudFrontディストリビューションを指定してデプロイしたところ、エラーとなりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/72a0108b-00ad-4511-ba8b-a69ad39925c7.png)


Lambda@Edgeを使用するためには、Lambdaに割り当てたロールの修正が必要となるようです。

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-edge-permissions.html#lambda-edge-permissions-function-execution


今回作成したLambda関数に割り当てたロールに対し、上記ページに記載のある信頼ポリシーの設定を行いポリシーを更新します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/21fb954a-5034-4f25-8d48-cdf9a0ed47e5.png)

再度Lambda@Edgeへデプロイしたところ、エラーなくデプロイできました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/15f82e36-6efa-4a93-ba41-f1c7289daed5.png)

### 認証の動作確認

割り当てたカスタムドメインを使用してサイトにアクセスをしてみたところ、想定通りに認証を求められました！コードに記載したユーザ名、パスワードを入力したところ無事htmlが表示されました。想定通り動いていますね！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/921066e0-9079-4732-8e25-345a54612886.png)


念のため誤ったユーザID/パスワードを入力し、正しい認証情報出ない場合は閲覧できないことも確認します。認証機能を導入する際は、誤った情報の場合はアクセスできないことを確認するのも大事です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/00ab085c-c690-4e13-a21a-95b5b28f3ac9.png)


## 2.6 特定拠点からのアクセス許可設定

毎回認証を求められてしまうと、ちょっと面倒ですよね。今回の仕組みを社員旅行など、社内レクレーションに活用することを想定すると、特定のIPアドレスからのアクセスの場合は認証なしで閲覧できる仕組みを実装します。

生成AIに
```
lambdaのコードを修正したいです。
特定の拠点（IPアドレス）からのアクセスの場合は、認証せずにアクセスできるようにしてください。
```
とインプットしたところ、以下の回答を得られました。ほんの数行しか変わっていませんが、IPアドレスのチェックをするロジックが追加になっていますね。

``` py
import base64

def lambda_handler(event, context):
    request = event['Records'][0]['cf']['request']
    headers = request['headers']
    request_ip = request['clientIp']

    # 許可されたIPアドレスのリスト
    allowed_ips = ['123.456.789.0', '987.654.321.0']  # 変更する必要があります

    # クライアントIPアドレスが許可されているかをチェック
    if request_ip in allowed_ips:
        return request

    # 基本認証の設定
    auth_user = 'username'
    auth_pass = 'password'
    auth_string = f'{auth_user}:{auth_pass}'
    auth_string_enc = base64.b64encode(auth_string.encode()).decode()

    if 'authorization' in headers:
        auth_header = headers['authorization'][0]['value']
        encoded = auth_header.split(' ')[1]

        if encoded == auth_string_enc:
            return request
        else:
            return _response_403('Invalid credentials')
    else:
        return _response_401()

def _response_401():
    return {
        'status': '401',
        'statusDescription': 'Unauthorized',
        'headers': {
            'www-authenticate': [{
                'key': 'WWW-Authenticate',
                'value': 'Basic'
            }]
        }
    }

def _response_403(message):
    return {
        'status': '403',
        'statusDescription': 'Forbidden',
        'body': message
    }
```


Lambdaのコードを更新し、Deployします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/660c6548-ee28-408b-b932-adae3a5abc2f.png)


動作確認してみますが、どうやら認証がかかったままのようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/2850f140-168b-4605-9bfb-397f53bf6750.png)


CloudFrontへの紐づけが正しくできているか確認してみましょう。

CloudFront > ディストリビューション > ディストリビューション名　> ビヘイビア > 編集 より、ビヘイビアに関連付けられた関数を確認したところ、関数ARNの最後に「:1」がついているようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/b2bf73ab-a237-42de-a18b-7ffa475f54d8.png)


今回作成したLambda関数を確認するとLambda関数自体のARNの末尾には「:1」がついていないようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/38b5810a-ffc5-40af-af19-e0736b78a7c5.png)


バージョンタブより「1」を確認したところ、「バージョン:1」のARNが「:1」が付いたものとなっていました。CloudFrontに紐づけるLambda関数のバージョンを更新してあげる必要がありそうですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/ef0e3e51-b637-429a-a314-68c21561f5d1.png)


一つ上の階層に戻り、アクション > 新しいバージョンを発行 より、新しいバージョンを発行します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/bafd53c8-8a49-45e4-b563-fc6b6e8a2e4b.png)


以下のように「バージョン:2」が発行されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/8d5606d6-7340-494c-8e60-d202888e7187.png)


CloudFront > ディストリビューション > ディストリビューション名　> ビヘイビア > 編集 より、ビヘイビアに関連付けられた関数を更新します。関数ARNの最後を「:2」に書き換えてみましょう。※CloudFrontへの反映には時間がかかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/08ab3f13-1dcd-4ce0-b77a-4a6f3136da8f.png)

### 認証を求めない拠点からのアクセス動作確認
CloudFrontへの反映がされたころを見計らって再度アクセスしたところ、認証なしでアクセスすることが出来ました！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/97dd804f-b957-49c0-88be-909a4445115b.png)

別の回線からアクセスすると、想定通り認証を求められました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3677642/30bcdfdb-a86c-4899-ad0b-d7c8fda37d76.png)



# おわりに
生成AIにアドバイスをもらいながら設計・構築することで定めた要件を実現することが出来ました。生成AIの活用によりAWSを使用した仕組みの構成検討や、Lambdaのコード作成などにかかる時間は大幅に短縮できますね。

今回は生成AIのアドバイスをもとに進めましたが、S3上のファイル公開だけであればAmplify ホスティングを使用した方が手軽に実現できたかもしれません。生成AIのアプトプットを鵜呑みにせず、正否を判断する知識をしっかり持ったうえで、生成AIを活用していきましょう！