リモート開発メインのソフトウェア開発企業のエンジニアブログです

S3にあるファイルを編集するWebアプリを作った

仕事でS3にあるファイルを編集するWebアプリを作ったため、まとめておきたいと思います。

これはインターネットからS3にあるindex.htmlにアクセスして、S3にあるJSONファイルを編集する、というものです。構成は以下のようになります。

今回は認証を行わず、WAF(Web Application Firewall)にてIPアドレスだけを制限しています。

動作の流れ

  1. ブラウザからCloudFrontにアクセス
  2. WAFでIPアドレスを検査し、許可したIPアドレスであれば通過させる
  3. S3に設置したindex.htmlをブラウザに返す→ブラウザにJSONエディタが表示される
  4. JSONエディタよりAPI GatewayのAPIを呼び出す
  5. API Gatewayを呼び出す際のIPアドレスをWAFで検査し、許可したIPアドレスであれば通過させる。
  6. APIに統合されたLambda関数が呼び出される
  7. Lambda関数によりS3のJSONの読み込み・書き込みを行う。

ロールを用意する

Lambda関数で使用するロールを用意する必要があります。ロールには以下の権限が必要となります。

  • Lambdaのログを出力させる権限(AWSLambdaBasicExecutionRole)
  • S3オブジェクトをダウンロードする(読み込む)・アップロードする権限

S3への権限は以下のようなカスタムポリシーを使用するほうが安全です。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::your-bucket-name/path/to/your.json"
    }
  ]
}

Resourceのパスは実際にデータの読み込み・書き込みを行うJSONファイルのものに書き換えてください。なお、ロールはIAMで作成します。

Lambda関数を作成する

Lambda関数はS3にあるJSONファイルの読み込み・書き込みを担当します。今回はわかりやすくするためにJSONファイルを読み込む機能と書き込む機能を別関数として作成します。

関数を作成において、ランタイムはPython3.13を、デフォルトの実行ロールは上記で用意したものを指定しています。

ここではセキュリティを考慮して関数の実装は行いません。

API Gatewayを設定する

API Gatewayはアプリケーションが外部と通信するための単一のエントリーポイントとなるものです。S3に置いたindex.htmlのJavaScriptから実行され、APIに関連付けられたLambda関数を実行します。

APIを作成する

まずREST APIを作成します。左のメニューからAPIを選択し、右上の「APIの作成」→REST APIを「構築」→「新しいAPI」を選択します。APIの機能としてはHTTP APIで十分なのですが、APIのエンドポイントへのアクセスをWAF(Web Application Firewall)にて制限するため、REST APIで作成しています。

  • API名:名前を設定します。
  • APIエンドポイントタイプ:今回はインターネットに公開するため、リージョンまたはエッジ最適化を選びます。
  • IPアドレスのタイプ:IPv4でしか接続を許可しないのであればIPv4、そうでなければデュアルスタックを選択します。

リソースについて

リソースにはAPIの構造化、URIパスの定義、HTTPメソッドの適用などの役割があります。今回はリソースを作らず、既にあるリソース「/」にメソッドを追加していきます。

メソッドの作成と設定

APIの作成が終わるとリソースの画面になります。右側のメソッドの欄にある「メソッドの作成」をクリックし、メソッドを作成します。

OPTIONSメソッドの作成

  • メソッドタイプ:OPTIONS
  • 統合タイプ:Mock

GETメソッドとPOSTメソッドはCloudFrontディストリビューションドメイン名が決定したあとに作成します。

ここで一旦設定を終え、デプロイします。新しいステージの作成を求められます。ステージ名は開発環境であればDevなどで良いでしょう。この時点でこのAPIのエンドポイントが有効になっているので、外部からのアクセスを制限するため、次はWAFを設定します。

API Gateway用のWAFを設定する

WAFはWeb Application Firewallの略で、その名の通りWebアプリケーションのファイアウォールです。今回はIPアドレスによる制限を行っています。特定のIPアドレスのみを許可し、それ以外をブロックする設定です。ここではAPI Gateway用のWAFを設定しますが、のちにCloudFront用のWAFも設定します。

IP setを作成する

IP setはアクセスを許可または拒否したいIPアドレスやIPアドレス範囲をまとめたリストです。IP setを作成するには左のメニューから「IP sets」をクリックします。ここではAPI GatewayとCloudFront用の2つのIP setを作成します。

最初にAPI Gateway用のIP setを作成するためリージョンをAPI Gatewayと同じ領域にして、「Create IP set」をクリックします。

  • IP set name:名前を設定します。
  • Region:API Gatewaと同じリージョンを選択する必要があります。
  • IP version:接続元がIPv6の場合はIPv6を選択し、そうでない場合はIPv4を選択します。
  • IP address:IPアドレスとプレフィックス長を記入します。IP versionで選択したバージョンのIPを設定します。

作成が終わったら次にCloudFront用のIP setを作成します。リージョンをGlobal(CloudFront)に変更し、上記で作ったものと同じIP setを作成します。

API Gateway用のWeb ACLを作成する

Web ACLはWebサイトやアプリケーションへのアクセスを制御するためのルールをまとめたものです。ここではAPI Gatewayのエンドポイントへのアクセスを制限します。Web ACLを作成するには左のメニューから「Web ACLs」を選択します。リージョンがAPI Gatewayと同じであるかを確認し、そうでなかったら同じにします。そして、「Create web ACL」をクリックします。

  • Web ACL details
    • Name:名前を設定します。
    • CloudWatch metric name:CloudWatchにより監視したい内容に応じて独自に定義する名前です。
  • Associated AWS resources:ここでAPI GatewayをWAFに関連付けます。Add AWS resourcesをクリックして、先ほど作成したAPI Gatewayを選択します。
  • Rules:ここで「特定のIPからのアクセスを許可し、その他はブロックする」というルールをを作成します。Add rules→Add my own rules and rule groupsを選択します。
    • Rule type:IP setを選択
    • Rule Name:ルールの名前を設定します。
    • IP set:このルールで使用するIP setを設定します。これは先程作ったIP setになります。
    • Action:ここではIP setに含まれるIPからの接続を許可するため「Allow」を選択します。
  • Default web ACL action for requests that don’t match any rules:ルールと一致しないリクエストはブロックしたいので「Block」を選択します。
  • Set rule priority:ルールを複数作った場合、ここで優先順位が設定できます。

これでAPI Gatewayのエンドポイントにアクセス制限がかかりました。

index.htmlを置くS3の場所を決める

今回はindex.htmlをS3に置いて外部からアクセスしてもらう、という方式を取っています。なんらかの手違いやバグ、将来の変更によって、そのバケットにある他のデータが漏洩するリスクを考慮すると、バケットを新規に用意することをおすすめします。なお、バケット名はアカウントやリージョン関係なく全世界で一意にする必要があります。

CloudFrontを設定する

CloudFrontはWebコンテンツを配信するサービスです。今回はS3にあるindex.htmlをWebコンテンツとして配信する役割を担っています。この配信はWAFによってアクセス制限をかけることになります。CloudFrontを設定するには左のメニューから「ディストリビューション」を選択し、右上に表示された「ディストリビューションの作成」をクリックします。

  • Origin domain:index.htmlを設定する予定のS3バケットを指定します。
  • 名前:名前を設定します。
  • オリジンアクセス:Origin access control settings (recommended)を選択
  • Origin access control:「create new OAC」でOrigin access control(OAC)を作成します。「署名動作」は署名リクエスト(推奨)のままでOKです。OACを作成すると以下のようにS3バケットポリシーに関する情報が表示されます。この内容の通り、ディストリビューション作成後にS3バケットポリシーに設定すべきポリシーステートメントが提供されす。
  • ビューワープロコトルポリシー:HTTPS only
  • 許可されたHTTPメソッド:GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE
  • キャッシュとオリジンリクエスト
    • Cache policy and origin request policy (recommended)
      • キャッシュポリシー:CachingDisabled(デバッグが終わるまではキャッシュ無効の方が良い)
  • ウェブアプリケーションファイアーウォール(WAF):セキュリティ保護を有効にする

ディストリビューションを設定すると以下のようにS3バケットポリシーに関する情報が表示されます。ポリシーをコピーしておきましょう。このポリシーはS3の設定で使用します。

Moba Pro

S3にバケットポリシーを設定する

バケットポリシーはそのバケットに保存されたオブジェクトへのアクセスを提供します。JSONによって記述します。index.htmlを設定する予定のS3バケットを開き、CloudFrontでコピーしたポリシーを貼り付けます。

S3バケット→アクセス許可→バケットポリシー→編集

CloudFront用のWeb ACLを作成する

CloudFrontによってS3のindex.htmlが公開されますが、そこにアクセス制限をかけます。リージョンをGlobal(CloudFront)に変更します。そして、Create web ACLをクリックします。

  • Web ACL details
    • Name:名前を設定します。
    • CloudWatch metric name:CloudWatchにより監視したい内容に応じて独自に定義する名前です。
  • Associated AWS resources:ここでCloudFrontをWAFに関連付けます。Add AWS resourcesをクリックして、CloudFront Distributionsから先ほど作成したCloudFrontを選択します。
  • Rules:ここで「特定のIPからのアクセスを許可し、その他はブロックする」というルールをを作成します。Add rules→Add my own rules and rule groupsを選択します。
    • Rule type:IP setを選択
    • Rule Name:ルールの名前を設定します。
    • IP set:このルールで使用するIP setを設定します。これは事前に作ったIP setになります。
    • Action:ここではIP setに含まれるIPからの接続を許可するため「Allow」を選択します。
  • Default web ACL action for requests that don’t match any rules:ルールと一致しないリクエストはブロックしたいので「Block」を選択します。
  • Set rule priority:ルールを複数作った場合、ここで優先順位が設定できます。

CloudFrontで公開するhtmlファイルを指定する

ディストリビューションを選択→一般→設定の「デフォルトルートオブジェクト」にて、ディストリビューションドメインにアクセスした際に最初に表示されるページを指定できます。

S3バケット直下にindex.htmlを置くのであれば「index.html」を記入すればOKです。プレフィックスを指定する場合は「test/index.html」のように記述します。指定が終わったら、詳細の「ディストリビューションドメイン名」をコピーしておきます。

API Gatewayのメソッドを追加する

API Gatewayのリソースを選択し、メソッドを追加していきます。GETメソッドはJSONファイルの読み込みを、POSTメソッドはJSONファイルの書き込みを目的としています。

GETメソッドの作成

  • メソッドタイプ:GET
  • 統合タイプ:Lambda関数
  • Lambdaプロキシ統合:オフ
  • Lambda関数:JSON読み込み用関数を指定

GETメソッドの設定

  • メソッドレスポンスを編集→ヘッダー名:Access-Control-Allow-Origin
  • 統合レスポンスを編集→ヘッダーのマッピング→Access-Control-Allow-Originのマッピングの値:’(CloudFrontのディストリビューションドメイン名)’

POSTメソッドの作成

  • メソッドタイプ:POST
  • 統合タイプ:Lambda関数
  • Lambdaプロキシ統合:オフ
  • Lambda関数:JSON書き込み用関数を指定

POSTメソッドの設定

  • メソッドレスポンスを編集→ヘッダー名:Access-Control-Allow-Origin
  • 統合レスポンスを編集→ヘッダーのマッピング→Access-Control-Allow-Originのマッピングの値:’(CloudFrontのディストリビューションドメイン名)’
  • 統合リクエストを編集→マッピングテンプレート
    • コンテンツタイプ:application/json(薄い文字で最初から書いてありますが、これは仮のものなので自分で入力する必要があります)
    • テンプレート本文:下記を記入
{
 "body": $input.json('$')
}

Access-Control-Allow-Originのマッピング値はシングルクォートで囲みます。また、ディストリビューションドメイン名の末尾にスラッシュをつけないようにします。

API GatewayのCORSを有効にする

CORSはCross-Origin Resource共有のことです。CORSにおける「オリジン (Origin)」とは、ウェブコンテンツ(HTMLドキュメント、JavaScript、CSSなど)がどこから来たかを特定するための概念です。CORSは、ウェブブラウザのセキュリティ機能の一つである「同一オリジンポリシー」を緩和するために存在します。同一オリジンポリシーは、あるオリジンで動作しているウェブページから、異なるオリジンのリソースに対して XMLHttpRequestや Fetch APIを用いた HTTPリクエストを行うことをデフォルトで禁止しています。CORSの設定では、サーバー側が Access-Control-Allow-Origin というレスポンスヘッダーを使って、どのオリジンからのリクエストを許可するかを指定します。

リソースの「/」をクリックし、リソースの詳細にある「CORSを有効にする」をクリックします。

  • ゲートウェイのレスポンス:すべてチェックを入れる
  • Access-Control-Allow-Methods:すべてチェックを入れる
  • Access-Control-Allow-Origin:ディストリビューションドメイン名を入力する。末尾にスラッシュがつかないようにする。

設定が終わったら右上のボタンよりAPIをデプロイします。

Lambda関数を実装・設定する

コードは以下のようになります。なお、これらのコードはAPI GatewayにてREST APIを使う前提で書いてあります。

読み込み関数

import boto3
import json
import os
 
s3 = boto3.client('s3')
 
def lambda_handler(event, context):
 
    bucket_name = os.getenv('S3_BUCKET')
    file_key = os.getenv('S3_SAVE_PATH')
 
    try:
        # S3からJSONファイルを取得
        response = s3.get_object(Bucket=bucket_name, Key=file_key)
        file_content = response['Body'].read().decode('utf-8')
        data = json.loads(file_content)
         
        return {
            'statusCode': 200,
            'body': json.dumps(data),
            'headers': {
                'Content-Type': 'application/json'
            }
        }
 
    except Exception as e:
        return {
            'statusCode': 500,
            'body': json.dumps({'error': str(e)}),
            'headers': {
                'Content-Type': 'application/json'
            }
        }

書き込み関数

import json
import os
import boto3

s3 = boto3.client('s3')
   
def lambda_handler(event, context):
  
    bucket_name = os.environ['S3_BUCKET']
    file_key = os.environ['S3_SAVE_PATH']

    try:
        data = event['body']
    except (KeyError, TypeError) as e:
        return {
            'statusCode': 400,
            'data': json.dumps({'error': str(e)})
        }        
 
    try:
        # S3にJSONファイルを保存
        s3.put_object(Bucket=bucket_name, Key=file_key, Body=json.dumps(data))
        
        return {
            'statusCode': 200,
            'data': f"File successfully saved to S3 at {bucket_name}/{file_key}"
        }
         
    except Exception as e:
        print(f"Error saving file to S3: {e}")
        return {
            'statusCode': 500,
            'data': str(e)
        }

両方のLambda関数で以下の設定をしています。タイムアウトは10秒でなくても構いませんが、短すぎると関数が正常に実行されなくなります。

  • 一般設定:タイムアウト10秒
  • 環境変数:
    • S3_BUCKET:JSONファイルが置かれているS3バケット名
    • S3_SAVE_PATH:JSONファイルへのパス(バケット名を除く、ファイル名を含む)

以下は環境変数の設定例です。

コードを変更した際はデプロイを忘れないようにしましょう。

index.htmlを作成する

CloudFrontによってWebに公開されるindex.htmlを作成します。関数「fetchJson」がデータの読み込み、関数「updateJson」がデータの書き込みを担当しています。API_ENDPOINTにAPI Gatewayのステージメニューで確認できるURLを記述します。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>JSONファイル編集</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <style>
    body {
      font-family: sans-serif;
      padding: 1rem;
    }
    textarea {
      width: 100%;
      height: 300px;
    }
    button {
      margin-top: 1rem;
      padding: 0.5rem 1rem;
      font-size: 1rem;
    }
  </style>
</head>
<body>
  <h1>JSONエディタ</h1>
  <button onclick="fetchJson()">JSONを取得</button>
  <textarea id="json-editor" placeholder="ここにJSONが表示されます..."></textarea>
  <br>
  <button onclick="updateJson()">JSONを保存</button>

  <script>
    const API_ENDPOINT = '(ステージメニューの「URLを呼び出す」)'; 

    async function fetchJson() {
      console.log("test0");
      try {
        const response = await fetch(API_ENDPOINT, {
          method: 'GET',
          headers: { "Content-Type": "application/json" }
        });
        const data = await response.json();
        document.getElementById('json-editor').value = data.body;
      } catch (error) {
        alert('取得に失敗しました: ' + error);
      }
    }

    async function updateJson() {
      const rawText = document.getElementById('json-editor').value;
      let jsonData;
      try {
        jsonData = JSON.parse(rawText);
      } catch (e) {
        alert('正しいJSON形式ではありません。');
        return;
      }

      try {
        const response = await fetch(API_ENDPOINT, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(jsonData)
        });
        if (response.ok) {
          alert('保存が成功しました。');
        } else {
          const err = await response.text();
          alert('保存に失敗しました: ' + err);
        }
      } catch (error) {
        alert('通信エラー: ' + error);
      }
    }
  </script>
</body>
</html>

index.htmlとdata.jsonを作成したら、S3にアップロードします。アップロードするS3 URIはCloudFrontで指定したバケットになります。

動作確認

動作確認を行うにはブラウザでCloudFrontのディストリビューションドメイン名にアクセスします。そして、テキストエリアにJSONを記入し「JSONを保存」をクリックします。

次にJSONの内容を書き換えて「JSONを保存」をクリックします。POSTメソッドの正常に動いた場合は以下のようなポップアップウィンドウが表示されます。

ただし、書き込み自体がうまく行っているかはこの時点ではわかりません。きちんと確認する必要があります。それにはページをリロードして「JSONを取得」をクリックします。編集した内容が表示されれば書き込みが正常に行われたことになります。

また、書き込みを確認する方法としてS3のdata.jsonを直接確認することもできます。

終わりに

S3にあるファイルを編集するWebアプリを作る方法を説明してきました。しかし、ちゃんと動かない、動かない原因がわからない、という方もいらっしゃると思います。

次回は各設定のデバッグのコツを簡単にまとめたいと思います。

← 前の投稿

次の投稿 →

[Docker Compose] そのポート公開、本当に必要?

コメントを残す