APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

【初心者向け】APIGateway&CloudWatch Syntheticsを使ってE2Eテストやってみた。前編

目次

はじめに

こんにちは、クラウド事業部のサカタニです。

最近、CloudWatch Syntheticsというサービスを初めて知りました。
いい機会なので自分で手を動かしてどんなことができるサービスなのか勉強した内容のまとめです。
まずはCloudWatch Syntheticsの機能検証の前編、準備編としてAPIGatewayを使用したWebアプリケーションの作成方法をご説明します。
CloudWatch Syntheticsの機能検証のためのAPIGatewayを使用した簡単なWebアプリケーション(じゃんけん)を作成します。
準備として用意したのですが、APIGatewayを使用したWebアプリケーションを初めて1から作成したため、勉強になりました。
手順として残しておきたいと思い、記事化しました。

どんなひとに読んで欲しい

  • APIGatewayを使ったことがない人
  • CloudWatch Syntheticsを使ったことがない人(後編で扱います。)
  • E2Eテストに興味がある人(後編で扱います。)

    APIGatewayとは

docs.aws.amazon.com APIGatewayはWebAPI(HTTP、REST、WebSocket)の作成や管理運用機能を備えたフルマネージドサービスです。
フルマネージドサービスですのでインフラ管理を行う必要なく簡単にAWSのLambdaやS3などの各種サービスをバックエンドで呼び出すことができます。
詳細は上記AWS公式ドキュメントをご参照ください。
今回はRESTAPIを作成し、Lambdaを呼び出すために使用します。

ゴール

以下のようなアーキテクチャでじゃんけんができるWebアプリケーションを作成します。

以下、表示画面です。

初期画面

グーボタンクリック(勝ちました。)

再戦ボタンクリック

自分の手を選べば、勝敗が表示され、再戦ボタンをクリックすれば初期画面に戻ります。

手順

Lambda作成

任意の関数名(画面例ではjanken)、ランタイムをPython3.12でそれ以外はデフォルト設定でLambdaを作成ください。

作成したLambdaのコードを以下でアップデートしてください。

import json
import random

def lambda_handler(event, context):
    playerhand = event.get("playerhand")
    
    try:
        cpuhand = random.randrange(3)
        result = ( 3 - cpuhand + playerhand ) % 3
        
        statuscode = 200
        response = {
            "cpuhand": cpuhand,
            "result": result
        }
        
    except Exception as e:
        print("Server Error", e)
        statuscode = 500
        response = {
            "message": "Server Error"
        }
        
    finally:
        return {
            'statusCode': statuscode,
            'body': json.dumps(response)
        }

これでLambdaの準備は完了です。

APIGateway作成

左側のメニューからAPIを選択し、APIを作成をクリックし、任意の名前(画面例ではjanken)でREST API、API エンドポイントタイプはリージョンでAPIを作成してください。

APIが作成できましたら、前項目のLambdaでは送られてきたデータの検証機能を入れていないので、APIGateway側でクライアントから送られてくるデータの検証用のモデルを作成します。
作成したAPI名をクリックすると、左側のメニューに項目が追加されますので、モデルをクリックします。

モデルを作成をクリックし、任意の名前(画面例ではJankenmodel)、コンテンツタイプにapplication/jsonを入力します。

フロントエンドからは、key:playerhandとvalue:0,1,2(それぞれグー、チョキ、パーに対応)が送られてくるため、必須のkeyとしてplayerhand、valueは0,1,2の3つしか許容しないように設定します。
モデルのスキーマには以下を設定し作成します。

{
  "type" : "object",
  "required" : [ "playerhand" ],
  "properties" : {
    "playerhand" : {
      "type" : "number",
      "enum" : [ 0, 1, 2 ]
    }
  }
}

モデルが作成できたら、リソースを作成していきます。
左側のメニューのリソースを選択し、リソースを作成をクリックし、任意のリソース名(画面例はjanken)、CORS(クロスオリジンリソース共有)を有効にしてリソースを作成します。

メソッドを作成をクリックし、メソッドタイプPOST、統合タイプLambda関数、Lambda プロキシ統合をoff、Lambda 関数に前項で作成したLambda関数を指定、メソッドリクエストの設定はリクエストバリデーターを「本文を検証」、「API キーは必須です」をon、リクエスト本文にコンテンツタイプ:application/json、モデルを先ほど作成したものを設定してメソッドを作成します。

作成したPOSTのCORSを有効にします。
CORSを有効にするをクリックします。
POSTにチェックをつけて保存をクリックします。

作成したAPIをステージにデプロイします。
APIをデプロイをクリックします。

新しいステージを選び、任意の名前(画面例はprod)でデプロイします。

必須にしたAPIキーを設定してAPIを呼び出せるクライアントを限定してみましょう。 まずは、APIキーを関連付ける使用量プランを作成するために左側のメニューから使用量プランを選び使用量プランを作成をクリックします。
任意の名前(画面例はjankenplan)を入力し、せっかくですので、スロットリングとクォータを両方onにしてみましょう。
スロットリングはレートとバーストを1に、リクエストは1日あたり20とすることで、1秒に1回、1日20回しかできないじゃんけんゲームの完成です。
(スロットリングとクォータは制限しなくても以降の作業に差異はないので、不要だと思われたらoffにしてください。)

次に、使用量プランに関連付けるAPIキーを作成するために左側のメニューからAPIキーを選びAPIキーを作成をクリックします。

APIキーの作成をクリックし、任意の名前(画面例はjankenkey)で保存します。

作成したAPIキーを選択して、アクションから使用量プランに追加を選びます。

先ほど作成した使用量プランを選択して、保存をクリックします。

使用量プランをデプロイしたステージに関連付けます。
先ほど作成した使用量プランの名前をクリックします。
ステージを追加をクリックします。
先ほど作成したAPIのステージを選択して使用量プランに追加をクリックします。

後ほど使用しますので、作成したAPIキーの値とステージのURLはひかえておいてください。

S3バケット作成

HTMLファイルを配布するS3バケットを作成します。
バケットを作成をクリックします。
任意の名前(画面例はcloudwatchsynthhetics-test-アカウントID)をつけ、パブリックアクセスをすべて ブロックをoff、バケットとバケット内のオブジェクトが公開される可能性の警告のチェックボックスをonにし、バケットを作成をクリックします。

配置したオブジェクトを公開するために作成したバケットのバケットポリシーに以下を設定します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::バケット名/*"
        }
    ]
}

以下のHTMLの const url = "ここにURL"; const apiKey = "ここにAPIキー"; 上記の""内をひかえておいたAPIキーの値とステージのURLに書き換えて作成したバケットにアップロードします。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>じゃんけん</title>
  <meta name="description" content="cloudwatchsynthetics test">
  <link rel="icon" href="data:,">
  <style>
    button {
        font-size: 20px;
        min-width: 100px;
        padding: 10px;
        margin: 10px;
        font-family: inherit;
        appearance: none;
        cursor: pointer;
        background-color: #f0f0f0;
        border: 1px solid #000;
        border-radius: 5px;
        transition: background-color 0.3s, border-color 0.3s;
    }
    
    button:hover {
        background-color: #e0e0e0;
        border-color: #333;
    }
    
    .playarea {
        margin: 20px;
        display: grid;
        grid-template-columns: 1fr;
        grid-auto-rows: auto;
        gap: 20px;
    }
    
    @media (min-width: 600px) {
        .playarea {
            grid-template-columns: 3fr 1fr 3fr;
        }
    }
    
    .playerarea, .cpuarea {
        border: 1px solid #000;
        font-size: 20px;
        display: grid;
        grid-template-columns: 1fr;
    }
    
    .areatitle {
        font-size: 30px;
        text-align: center;
        background: #c1e2ff;
        padding: 10px 0;
    }
    
    .cpuhandarea, .playerhandarea {
        text-align: center;
        font-size: 30px;
        padding: 20px;
    }
    
    .bufferarea {
        font-size: 30px;
        text-align: center;
        display: grid;
        place-content: center;
    }
    
    .displaynone {
        display: none;
    }
  </style>
</head>

<body>
    <section class="playarea">
        <section class="playerarea">
            <div class="areatitle">
                あなた
            </div>
            <div class="playerhandarea" id="buttonarea">
                <button type="button" value="0" id="rock" onclick="pushButton(this)" aria-label="グー">グー</button>
                <button type="button" value="1" id="scissors" onclick="pushButton(this)" aria-label="チョキ">チョキ</button>
                <button type="button" value="2" id="paper" onclick="pushButton(this)" aria-label="パー">パー</button>
            </div>
            <div class="playerhandarea displaynone" id="playerhandarea">
                <div id="playerhand"></div>
            </div>
        </section>
        <section class="bufferarea">
            <div id="result">
                VS
            </div>
            <div class="displaynone" id="retry">
                <button type="button" id="retrybutton" onclick="retryBattle()">再戦</button>
            </div>
        </section>
        <section class="cpuarea">
            <div class="areatitle">
                相手
            </div>
            <div class="cpuhandarea">
                <div id="cpuhand">ここに表示されます</div>
            </div>
        </section>
    </section>
<script>
    const url = "ここにURL";
    const apiKey = "ここにAPIキー";
    
    function pushButton(button){
        const hands = ["グー", "チョキ", "パー"];
        const results = ["引分", "敗北", "勝利"];
        const resultDiv = document.getElementById("result");
        const playerHandArea = document.getElementById("playerhandarea");
        const buttonArea = document.getElementById("buttonarea");
        const playerHandDiv = document.getElementById("playerhand");
        const cpuHandDiv = document.getElementById("cpuhand");
        const retry = document.getElementById("retry");
        let playerHand = button.value * 1;
        
        postData({ "playerhand": playerHand }).then((data) => {
            let body = JSON.parse(data.body);
            let cpuhand = body.cpuhand;
            let result = body.result;
            
            cpuHandDiv.textContent = hands[cpuhand];
            playerHandDiv.textContent = hands[playerHand];
            playerHandArea.classList.toggle("displaynone");
            buttonArea.classList.toggle("displaynone");
            retry.classList.toggle("displaynone");
            resultDiv.textContent = results[result];
        }).catch((error) => {
            console.error('Error:', error);
        });
    };
    
    function retryBattle(){
        const result = document.getElementById("result");
        const playerHandArea = document.getElementById("playerhandarea");
        const buttonArea = document.getElementById("buttonarea");
        const playerHandDiv = document.getElementById("playerhand");
        const cpuHandDiv = document.getElementById("cpuhand");
        const retry = document.getElementById("retry");
        
        cpuHandDiv.textContent = "ここに表示されます";
        playerHandDiv.textContent = "";
        playerHandArea.classList.toggle("displaynone");
        buttonArea.classList.toggle("displaynone");
        result.textContent = "VS";
        retry.classList.toggle("displaynone");
    }
    
    async function postData(data = {}) {
      
      try {
        const response = await fetch(url, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "X-API-KEY": apiKey
          },
          body: JSON.stringify(data),
        });
        
        return await response.json();
      } catch (error) {
        console.error('Fetch error:', error);
        throw error;
      }
    }
</script>
</body>
</html>

完成

S3の当該オブジェクトのURLにアクセスすると上記画面が表示され、じゃんけんで遊ぶことができるはずです。
1日20回しかできないので、数回動作確認したら、後編にすすんでください。

おわりに

APIGatewayはクライアントやレート、クォータの制限が簡単にできるなど様々な機能があり使いこなせれば強力なサービスなのではないかと思いました。
特にリクエストデータの検証を任せられるのはバックエンドのロジックを変えずに機能の追加ができるため素晴らしいな、と思います。
そもそもはCloudWatch Syntheticsの検証がしたかったのですが、そのために題材を自分で手を動かして作成してみたら狙いとは別の学びがありました。

よろしければ後編も引き続きお付き合いよろしくお願いいたします。

お知らせ

APCはAWS Advanced Tier Services(アドバンストティアサービスパートナー)認定を受けております。

その中で私達クラウド事業部はAWSなどのクラウド技術を活用したSI/SESのご支援をしております。
www.ap-com.co.jp

https://www.ap-com.co.jp/service/utilize-aws/

また、一緒に働いていただける仲間も募集中です!
今年もまだまだ組織規模拡大中なので、ご興味持っていただけましたらぜひお声がけください。

www.ap-com.co.jp