【AWS×サーバーレス】S3 + API Gateway + Lambda で簡単なアプリ作ってみた

## 初めに
今回はAWS を使って簡単な Web アプリを作成してみました。
本アプリは、S3 上に配置した HTML フロントエンドから数字を入力し、その入力値に Lambda で消費税を計算して返すという、とてもシンプルなものです。
バックエンドの処理は API Gateway 経由で Lambda を呼び出す形にしているため、フロント側からは REST API として利用できる構成になっています。

## 構成
今回のアプリの全体構成です。S3 に配置した静的ウェブホスティングの HTML ファイルを通して、API Gateway → Lambda と進み、消費税計算の結果を返す流れです。
![構成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3817084/00c93763-2731-fc2c-04a2-1cfae1197170.png)

## コード
ここでは、実際に使用したフロントエンド (HTML) とバックエンド (Lambda) のサンプルコードを紹介します。
### HTML (S3 ホスティング用)
API gateway作成後にconst apiUrlにAPI URLを記入してS3バケットにホスティングします。
~~~
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>消費税計算アプリ</title>
  <style>
    /* リセット・基本設定 */
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    
    body {
      font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
      background: linear-gradient(135deg, #e0f7e9, #ffffff);
      color: #333;
      min-height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
      padding: 20px;
    }
    
    /* コンテナを中央に縦一列に配置 */
    .container {
      background-color: #f4fff4;
      border: 1px solid #a2d5a2;
      border-radius: 10px;
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
      padding: 30px;
      max-width: 500px;
      width: 100%;
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 20px; /* 各要素間の隙間 */
    }
    
    h1 {
      color: #2a8c2a;
      font-size: 2em;
    }
    
    p {
      font-size: 1.1em;
      text-align: center;
    }
    
    /* 入力フィールド */
    input[type="number"] {
      width: 100%;
      padding: 10px;
      font-size: 1em;
      border: 2px solid #2a8c2a;
      border-radius: 5px;
      outline: none;
      transition: border-color 0.3s;
    }
    
    input[type="number"]:focus {
      border-color: #1e6f1e;
    }
    
    /* ボタン */
    button {
      width: 100%;
      padding: 12px;
      font-size: 1.1em;
      color: #fff;
      background-color: #2a8c2a;
      border: none;
      border-radius: 5px;
      cursor: pointer;
      transition: background-color 0.3s;
    }
    
    button:hover {
      background-color: #277227;
    }
    
    /* 結果表示 */
    #result {
      font-weight: bold;
      font-size: 1.2em;
      color: #2a8c2a;
      text-align: center;
    }
  </style>
  <script>
    function calculateTax() {
      const price = document.getElementById('price').value;
      const apiUrl = 'あなたのAPI URLを記入してください';
      const data = { price: price };

      fetch(apiUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(data)
      })
      .then(response => response.json())
      .then(result => {
        if(result.error) {
          document.getElementById('result').textContent = "エラー: " + result.error;
        } else {
          document.getElementById('result').textContent = "消費税 (10%): " + result.tax;
        }
      })
      .catch(error => {
        document.getElementById('result').textContent = "エラー: " + error;
      });
    }
  </script>
</head>
<body>
  <div class="container">
    <h1>消費税計算アプリ</h1>
    <p>金額を入力してください（消費税10%を計算します）</p>
    <input type="number" id="price" placeholder="金額">
    <button onclick="calculateTax()">計算する</button>
    <p id="result"></p>
  </div>
</body>
</html>
~~~

### Lambda (Python)
~~~
import json

def lambda_handler(event, context):
    try:
        # event.body が文字列の場合はJSONに変換
        body = event.get('body')
        if body:
            data = json.loads(body) if isinstance(body, str) else body
        else:
            data = event

        if 'price' not in data:
            return {
                'statusCode': 400,
                'headers': {
                    'Access-Control-Allow-Origin': '*'
                },
                'body': json.dumps({'error': 'price パラメータがありません'})
            }
        
        try:
            price = float(data['price'])
        except ValueError:
            return {
                'statusCode': 400,
                'headers': {
                    'Access-Control-Allow-Origin': '*'
                },
                'body': json.dumps({'error': 'price パラメータが数字ではありません'})
            }
        
        # 消費税10%の計算し、整数に丸める
        tax = int(round(price * 0.1))

        return {
            'statusCode': 200,
            'headers': {
                'Access-Control-Allow-Origin': '*',  # CORS を許可
                'Content-Type': 'application/json'
            },
            'body': json.dumps({
                'price': price,
                'tax': tax
            })
        }
    except Exception as e:
        return {
            'statusCode': 500,
            'headers': {
                'Access-Control-Allow-Origin': '*'
            },
            'body': json.dumps({'error': str(e)})
        }
~~~

## 手順
### Lambda関数の作成
1). Lambdaを開き「関数を作成」を押します。
![1.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3817084/e49b56ed-3845-f7ba-45c0-4a49a68a62f8.jpeg)

2). 「設計図の仕様」を選択し、以下項目を入力し「関数の作成」を押します。
- 設計図名：ランタイムがpythonのものならおそらく何でもいいです
- 関数名：任意の関数名
- 実行ロール：基本的なLambdaアクセス権限で新しいロールを作成
![2.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3817084/94009943-f5f0-7a87-1f21-69901f384732.jpeg)

![3.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3817084/78b4931e-917c-9f88-ba58-3ef61b479ef1.jpeg)

3). 赤枠のエディタ部分に以下関数を記入し、「Deploy」を押します。

![4.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3817084/ef1b807a-e963-51ab-56b0-6ab24b531f38.jpeg)

```
import json

def lambda_handler(event, context):
    try:
        # event.body が文字列の場合はJSONに変換
        body = event.get('body')
        if body:
            data = json.loads(body) if isinstance(body, str) else body
        else:
            data = event

        if 'price' not in data:
            return {
                'statusCode': 400,
                'headers': {
                    'Access-Control-Allow-Origin': '*'
                },
                'body': json.dumps({'error': 'price パラメータがありません'})
            }
        
        try:
            price = float(data['price'])
        except ValueError:
            return {
                'statusCode': 400,
                'headers': {
                    'Access-Control-Allow-Origin': '*'
                },
                'body': json.dumps({'error': 'price パラメータが数字ではありません'})
            }
        
        # 消費税10%の計算し、整数に丸める
        tax = int(round(price * 0.1))

        return {
            'statusCode': 200,
            'headers': {
                'Access-Control-Allow-Origin': '*',  # CORS を許可
                'Content-Type': 'application/json'
            },
            'body': json.dumps({
                'price': price,
                'tax': tax
            })
        }
    except Exception as e:
        return {
            'statusCode': 500,
            'headers': {
                'Access-Control-Allow-Origin': '*'
            },
            'body': json.dumps({'error': str(e)})
        }
```

### API gatewayの作成
1).  API Gatewayを開き、「REST API」の「構築」を押します。
![2.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3817084/424fad22-56fd-6389-0729-a11a786bd367.jpeg)

2). 以下項目を入力し、「APIを作成」を押します。
- APIの詳細：新しいAPI
- API名：任意のAPI名
- APIエンドポイントタイプ：リージョン
![4.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3817084/3debc288-3198-cbf3-6d96-97637d1264b5.jpeg)


3). 「メソッドを作成」を押します。
![5.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3817084/0247c40c-4528-f854-bfa8-dfe7b46bd00c.jpeg)


4). 以下項目を入力し、「メソッドを作成」を押します。
- メソッドタイプ：POST
- 統合タイプ：Lambda関数
- Lambdaプロキシ統合：有効
- Lambda関数：先ほど作成した関数

![6.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3817084/ef48106e-7b16-a701-2529-f08c38ca3b0c.jpeg)


5). リソースがルートになっていることを確認して、「CORSを有効にする」を押します。
![7.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3817084/4f7fe422-b47f-41b6-7f93-7157e4791409.jpeg)



6). 以下項目を入力し、「保存」を押します。
- ゲートウェイのレスポンス：DEFAULT 4XX
- Access-Control-Methods：POST
![8.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3817084/143ea6f3-384a-ca80-b5fc-22a54138351f.jpeg)


7). 「APIをデプロイ」を押します。
![9.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3817084/157d5641-abb8-9d2e-9d3d-63600f2edbb9.jpeg)


8). 以下項目を入力し、「デプロイ」を押します。
- ステージ：新しいステージ
- ステージ名：任意のステージ名

![11.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3817084/0093b60c-cc18-0506-a669-bc64259a28a0.jpeg)


9). これでREST APIの作成は完了です。
APIの呼び出しで使うので赤枠部分のURLをコピーします。
![12.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3817084/375b41f0-2ebe-4072-ef45-29ea7487affa.jpeg)

### ホスティング
HTMLファイルのconst apiUrl部分にさきほどコピーしたAPI URLを記入してS3バケットにホスティングします。
ホスティングとウェブサイト公開手順は下記と同じなのでHTMLファイルは置き換えて実行してください。

[【AWS】S3＋CloudFrontのOACでセキュアに静的ウェブサイトを公開する方法](https://qiita.com/SoraoKosaka/items/8972d7f8969df28e0f70)

### 結果
上記無事にホスティングできるとこのようなウェブサイトができると思います！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3817084/d3ebafd4-2b1e-b4b0-78a1-80d43af53e8b.png)


## 削除手順
このまま運用する予定がない方は必要以上に料金が発生しないようにリソースを削除します。

### API Gatewayインスタンスの削除
API Gatewayインスタンスの削除をします。
削除手順がわからない場合、以下を参照ください。
 [API Gatewayインスタンス削除手順](https://qiita.com/SoraoKosaka/items/0674d54222a7f568690c)

 ### Lambda関数の削除
 Lambda関数の削除をします。
 削除手順がわからない場合、以下を参照ください。
 [Lambda関数削除手順](https://qiita.com/SoraoKosaka/items/0e6b34b8433baaa37fc1)

 ### ディストリビューションの削除
 ディストリビューションの削除をします。
 削除手順がわからない場合、以下を参照ください。
 [【AWS】ディストリビューションの削除手順](https://qiita.com/SoraoKosaka/items/4b6f217ea2b077faf428)

 ### S3バケットの削除
 S3バケットの削除をします。
 削除手順がわからない場合、以下を参照ください。
 [【AWS】S3バケットの削除手順](https://qiita.com/SoraoKosaka/items/b7cd1b789a9a168e825d)

 

## 終わりに
今回は S3 + API Gateway + Lambda の組み合わせで、ごく簡単な消費税計算アプリを作ってみました。S3 上の静的コンテンツとサーバーレス環境を連携させることで、フロントとバックエンドをスモールスタートで実装できるのが魅力です。慣れてきたら、ユーザー認証やデータベース連携などを加えて、より実用的なアプリケーションに拡張してみるのも面白いでしょう。

この記事が参考になりましたらぜひ「いいね」「フォロー」など励みになるのでよろしくお願いします！