AWS S3に対してデータをアップロードするためのWebページをAWS EC2上に作成する

こんにちは。
株式会社クラスアクト インフラストラクチャ事業部の大塚です。
今回はAWS EC2とかS3、IAMで少し遊んでみます。

# 環境
今回用意した環境は以下となります。
EC2を使ってAmazon Linux 2023ベースのVMを起動。そこにDockerをインストールしてhttpdコンテナをデプロイ。さらにAWS側でS3バケットとEC2にアタッチする用のIAMロールを作成します。
環境を作ったあとはIAMロールの効力でEC2からaws s3コマンドでS3にデータ送信はできるが、Webブラウジング経由では（≒HTML/CSS/JavaScript経由では）アップロードが出来ないことを確認。
その後HTML経由でもアップロードが出来るようにするためにHTMLファイル内にクレデンシャルをハードコピーしてみて挙動の変化を見ていきます。
※ハードコピーはセキュリティの観点から基本的にダメです。AWS Systems Manager Parameter Storeでこれを回避するかCognitoあたりを使えばハードコピーの問題は回避できそうですが、、、どうなんでしょう。。。近いうちに触ってみたいところです。

![AWS ECS-ページ3.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/5090b4ee-14f1-6584-2e28-001badce744a.png)

https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/systems-manager-parameter-store.html

https://docs.aws.amazon.com/ja_jp/sdk-for-javascript/v2/developer-guide/s3-example-photo-album.html

# 環境構築
## IAMロールの作成
EC2にアタッチする用のIAMロールを作成します。このロールでEC2からS3にアップロード等を可能にします。

マネジメントコンソールでIAMロールの画面を開きます。その後ロールを作成を押下します。
![image (51).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/761e4515-ac78-ea69-48f3-1fbbd18136bf.png)
AWSのサービスを選択し、ユースケースにEC2を入力しラジオボタンを押下。次に進みます。
![screencapture-us-east-1-console-aws-amazon-iamv2-home-2023-10-05-23_07_28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/ca389b48-e932-2059-722e-ebf8fb100147.png)
本来であれば細かく決めたほうがいいのでしょうが、今回は雑にFullAccessを選択し次に進みます。この後作成されているかを確認して下さい。
![image (53).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/27b1c799-9b30-c068-de1a-7d99c44c90af.png)

## S3の作成
S3を作成します。今回は以下の設定で作成を行いました。
![screencapture-s3-console-aws-amazon-s3-bucket-create-2023-10-05-23_05_19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/12473922-5c41-6b12-5e80-eb79393d6108.png)
作成後CORSの設定を行います。S3バケット選択後、アクセス許可の下にあるCORSの設定に対して以下を追記します。
```
[
    {
        "AllowedHeaders": ["*"],
        "AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
        "AllowedOrigins": ["*"],
        "ExposeHeaders": []
    }
]
```
![image (54).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/2d203ac3-d86c-4583-b31b-284b0bc05265.png)

## EC2による環境デプロイ
### ①VM作成からコマンドによるアップロード失敗確認。IAMポリシー付与によりアップロードを可能にする
スクショは特にとっていませんが、Amazon Linux 2023をベースとしたVMを起動します。
検証の為、ここではIAMロールはアタッチせずにセキュリティグループで22と80を開けるようにします。
![image (55).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/334cdebd-bf5c-5f70-4b64-1616dcdf13ac.png)
EC2にsshして環境を作っていきます。
まずはVMにdockerをインストール、httpdコンテナをデプロイ、コンテナ内に入り必要なファイルを作成するところまでを行います。
```
[ec2-user@dev-docker ~]$ sudo su -
Last login: Sat Oct  7 14:42:47 UTC 2023 on pts/0
[root@dev-docker ~]# yum update
[root@dev-docker ~]# yum upgrade -y
[root@dev-docker ~]# yum install -y docker
[root@dev-docker ~]# systemctl start docker
[root@dev-docker ~]# systemctl enable docker
Created symlink /etc/systemd/system/multi-user.target.wants/docker.service → /usr/lib/systemd/system/docker.service.
[root@dev-docker ~]# docker pull httpd:latest
[root@dev-docker ~]# docker run -d --name httpd-container -p 80:80 httpd:latest
559ff6adab85efdfdf7fb6f1815868756e4007b18ed199aab85582d1fbc16fce
[root@dev-docker ~]# docker ps
CONTAINER ID   IMAGE          COMMAND              CREATED         STATUS         PORTS                               NAMES
559ff6adab85   httpd:latest   "httpd-foreground"   4 seconds ago   Up 3 seconds   0.0.0.0:80->80/tcp, :::80->80/tcp   httpd-container
[root@dev-docker ~]# docker exec -it httpd-container /bin/bash
root@559ff6adab85:/usr/local/apache2# apt update
root@559ff6adab85:/usr/local/apache2# apt upgrade -y
root@559ff6adab85:/usr/local/apache2# apt install -y vim
root@559ff6adab85:/usr/local/apache2# cd htdocs/
root@559ff6adab85:/usr/local/apache2/htdocs# cp -p index.html index.html.org
root@559ff6adab85:/usr/local/apache2/htdocs# mkdir css
root@559ff6adab85:/usr/local/apache2/htdocs# touch file_upload.html
root@559ff6adab85:/usr/local/apache2/htdocs# touch css/file_upload.css
root@559ff6adab85:/usr/local/apache2/htdocs# ls -ltR
.:
total 8
drwxr-xr-x. 2 root root  29 Oct  7 14:53 css
-rw-r--r--. 1 root root   0 Oct  7 14:53 file_upload.html
-rw-r--r--. 1  501 staff 45 Jun 11  2007 index.html
-rw-r--r--. 1  501 staff 45 Jun 11  2007 index.html.org
./css:
total 0
-rw-r--r--. 1 root root 0 Oct  7 14:53 file_upload.css
```
次にこのコンテナにvimやawsコマンドを突っ込みます。IAMをアタッチしていないのでクレデンシャルを取得できず、aws s3 lsコマンドで弾かれることが確認できます。
```
root@8b2a738cc3c2:/usr/local/apache2/htdocs# cd
root@8b2a738cc3c2:~# apt install -y vim curl unzip npm
root@8b2a738cc3c2:~# curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
root@8b2a738cc3c2:~# unzip awscliv2.zip
root@8b2a738cc3c2:~# ./aws/install
root@8b2a738cc3c2:~# rm -rf aws awscliv2.zip
root@8b2a738cc3c2:~# npm install aws-sdk
root@8b2a738cc3c2:~# aws s3 ls
Unable to locate credentials. You can configure credentials by running "aws configure".
```
この状態でIAMロールをアタッチします。
![image (56).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/00e20476-e173-244d-5c05-5a93ef3e3b55.png)
再度aws s3コマンドを叩いてみると今度はクレデンシャルを認識してコマンドの実行、ファイルのアップロードまで問題ないことが確認できました。
```
root@8b2a738cc3c2:~# aws s3 ls
2023-10-07 14:19:44 httpd-s3-bucket
root@8b2a738cc3c2:~# touch IAM_CHECK.txt
root@8b2a738cc3c2:~# aws s3 cp IAM_CHECK.txt s3://your-bucket
upload: ./IAM_CHECK.txt to s3://your-bucket/IAM_CHECK.txt
```
![image (57).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/4e847727-8676-aa36-4c5f-f0436f85928e.png)

### ②IAMポリシーがアタッチされていてもHTML（JavaScript）でアップロードできないことを確認する
確認するためにコンテナに以下のHTML/CSSファイルを用意します。
```index.html
root@559ff6adab85:/usr/local/apache2/htdocs# cat index.html
<html>
        <body>
                <h1>It works!</h1>
                <a href="file_upload.html">for file upload page</a>
        </body>
</html>
```
```file_upload.html
file_upload.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>File Upload for AWS S3</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/aws-sdk/2.1128.0/aws-sdk.min.js"></script>
    <link rel="stylesheet" href="css/file_upload.css">
</head>
<body>
    <input type="file" id="fileInput">
    <button onclick="uploadFile()">Upload to S3</button>
    <script>
        AWS.config.update({
            region: 'ap-northeast-1'　★
        });
        const s3 = new AWS.S3();
        function uploadFile() {
            const fileInput = document.getElementById('fileInput');
            const file = fileInput.files[0];
            if (!file) {
                alert('choose files');
                return;
            }
            const fileName = file.name;
            const params = {
                Bucket: 'your_bucket', ★
                Key: fileName,
                Body: file
            };
            s3.upload(params, (err, data) => {
                if (err) {
                    console.error('upload error', err);
                    alert('upload error');
                } else {
                    console.log('upload successfully', data.Location);
                    alert('upload successfully');
                }
            });
        }
    </script>
</body>
</html>
```
```css/file_upload.css
root@559ff6adab85:/usr/local/apache2/htdocs# cat css/file_upload.css
body {
    font-family: Arial, sans-serif;
    text-align: center;
}
.upload-container {
    margin: 50px auto;
    width: 300px;
}
.upload-btn-wrapper {
    position: relative;
    overflow: hidden;
    display: inline-block;
}
.btn {
    border: 2px solid gray;
    color: gray;
    background-color: white;
    padding: 8px 20px;
    border-radius: 8px;
    font-size: 20px;
    font-weight: bold;
}
.upload-btn-wrapper input[type=file] {
    font-size: 100px;
    position: absolute;
    left: 0;
    top: 0;
    opacity: 0;
}
.file-name {
    margin-left: 10px;
}
```
WebブラウザでこのHTMLにアクセスしてみます。リンクが表示されているはずなので先に行きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/0c0713f8-66b1-5e85-93ab-362268e7adbc.png)
Webブラウジング経由でアップロードを試みようとしてもエラーが返ってくることが確認できます。
なぜならこのアップロードの処理はHTML内のJavaScriptが担っており、これはローカルで処理をしているからです。ローカルにクレデンシャル情報はないため弾かれてしまう。（aws s3コマンドはサーバ上で実行されいてる。サーバにはIAMロールが付与されているため問題なかった）
![image (58).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/f2183eb7-9423-ff92-636f-f94c4d61b2e1.png)
ChatGPTの解説
>このコードは、ブラウザのクライアント側で実行されるJavaScriptコードです。具体的には、ウェブページ上のフォームからファイルを選択し、選択したファイルをAmazon S3というクラウドストレージサービスにアップロードするためのコードです。
したがって、このコードはユーザーのPC（ブラウザ）で実行され、選択したファイルをAWS S3バケットにアップロードする際に、ユーザーのPCからS3サービスにアクセスします。

これを回避するために（セキュリティ的には微妙だが）HTML上にクレデンシャルをハードコピーしてしまいます。

図示するとこんな感じでしょうか？
普段我々がWebブラウザで見ている様々な情報の動作は基本的に以下となっており、①HTMLファイルをWebサーバからとってくる。②HTMLの中身を解析する。③それを描写する。
![AWS ECS-ページ4.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/6dd7ee74-ccb9-2790-cf3f-640e8f4c1d6a.png)
今回の場合描写までは問題ないがアップロードの処理があり、それはPCからAWSのS3に対して実行される。PCはS3に対するクレデンシャルの情報を持っていないからエラーとなってしまう。それを回避するためにHTMLファイルにクレデンシャルの情報を埋め込んで渡してしまい、S3にアップロードするときはその埋め込んだ情報を使ってもらおうということです。※鍵のマークがクレデンシャルと思ってください。
![AWS ECS-ページ5.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/4f765d0c-94a7-f5b9-a1b2-e1fbec3116f4.png)
![AWS ECS-ページ6.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/6475e0b2-c169-fa7a-9efc-ea990b7177fe.png)



### ③クレデンシャル情報入手とハードコピー、アップロードリトライ
クレデンシャルを取得するために、AWS上にそれ専用のユーザを作成します。
IAMで以下のようなユーザを作成します。
![image (59).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/edccf9d2-4a2d-3fab-edf4-e927e109371e.png)
アクセスキーの作成を押下します。
![image (60).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/88396034-60c1-2ad0-7cc0-9a5efd755886.png)
EC2ように作成したいので、コンピューティング～のやつを選択
![screencapture-us-east-1-console-aws-amazon-iamv2-home-2023-10-05-23_38_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/d6e023a9-55f7-bf6b-9f6a-299481961990.png)
説明タグは任意で問題なしで、アクセスキーを作成を押下します。
![image (61).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/5dd14341-f854-9244-bcf8-24f78e5beaa2.png)
すると以下のようにアクセスキーとシークレットアクセスキーが入手できるので、これを控えます。
![image (62).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/4af3de37-5b9e-3dc1-830a-1cada415a809.png)
file_upload.htmlにクレデンシャル情報を追加します。
```file_upload.html
file_upload.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>File Upload for AWS S3</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/aws-sdk/2.1128.0/aws-sdk.min.js"></script>
    <link rel="stylesheet" href="css/file_upload.css">
</head>
<body>
    <input type="file" id="fileInput">
    <button onclick="uploadFile()">Upload to S3</button>
    <script>
        AWS.config.update({
            accessKeyId: 'ここに赤の部分の文字列を入れる',　★ 
            secretAccessKey: 'ここに***の文字列を入れる',　★ 
            region: 'ap-northeast-1'　★
        });
        const s3 = new AWS.S3();
        function uploadFile() {
            const fileInput = document.getElementById('fileInput');
            const file = fileInput.files[0];
            if (!file) {
                alert('choose files');
                return;
            }
            const fileName = file.name;
            const params = {
                Bucket: 'your_bucket', ★
                Key: fileName,
                Body: file
            };
            s3.upload(params, (err, data) => {
                if (err) {
                    console.error('upload error', err);
                    alert('upload error');
                } else {
                    console.log('upload successfully', data.Location);
                    alert('upload successfully');
                }
            });
        }
    </script>
</body>
</html>
```
再度WebブラウザでHTMLファイルにアクセスし、アップロードを試みると今度はクレデンシャル関係のエラーが出力されずにアップロードが出来るはずです。
![image (63).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/d881510e-85e0-8b69-1b6e-2e0fc95d0595.png)
![image (64).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/d166aced-8569-e9fb-ce1f-7572df542fec.png)