PR

【AWS】API Gatewayのタイムアウトを回避!処理時間が29秒以上でもOKな対策方法を紹介!

記事内に広告が含まれています。
SNSフォローボタン
z_a_k_iをフォローする

本記事では、API Gatewayの29秒でタイムアウトする事象を対策した事例を紹介します

こんな悩みをお持ちの方にオススメ!
  • LambdaのプログラムをAPI Gatewayで外部公開しようとしている
  • Lambdaのタイムアウト15分をクリアするように設計したがAPI Gatewayが30秒でタイムアウトしてしまう。
  • Lambdaの処理を30秒以下にすることはムリ。
  • 処理を大幅に変えずに回避する方法はないのか!?

ReactやVueなどで作成されたフロントエンドアプリケーションでは、バックエンドのWebAPIを呼び出して機能を利用者に提供します。

WebAPIをAWSで作成する場合、API GatewayとLambdaで構築するのがよくある構成です。

しかし、この構成では問題があります。

それは、Lambdaのタイムアウトは15分まで設定できるのに対し、API Gatewayは29秒までしか設定できないことです。

そのため、Lambdaのタイムアウト前にAPI Gatewayから利用者にタイムアウトのレスポンスが発生してしまいます。

Lambdaですこし重たい処理を行うと、処理時間が29秒を超えることはよくあり、負荷が高くなるとタイムアウトエラーが頻出し困ります。

そこでこの記事では、API GatewayとLambdaの構成で、API Gatewayのタイムアウトする事象を対策した事例を紹介します。

当記事の対策内容はあくまで回避方法の一つです。

採用するかは、対象プロダクトの特性を踏まえて判断してください。

本対策で利用するAWSサービス

本事象を回避するにあたって、利用するサービスの紹介です。

API Gateway

REST APIやWebsocket API を管理、公開するためのサービスです。

Lambdaで作成したWebAPIを外部公開できます。

APIのバージョン管理ができ、簡単に過去のバージョンに戻すことも可能。

詳細は、AWSの公式ドキュメントを参照ください。

AWS StapFunctions

ワークフローをGUIで設定できるサービスです。

ステートマシンと呼ばれるものを作成し実行します。

AWS StepFunctionsには無料枠が存在しますので、無料枠内で検証できます。

Step Functionsの無料枠

Lambda

サーバーレスでプログラムを実行することができるAWSのサービスです。

サーバーレスのため、プログラムを実行するサーバーやミドルウェアを準備せずにプログラムを作成できます。

マイクロサービスのプログラムを構築する際に利用されます。

Lambdaには無料枠が存在しますので、無料枠内で検証できます。

API Gatewayのタイムアウト設定方法

API Gatewayでは、リソースのメソッド単位でタイムアウト時間を設定できます。

ここでは、マネジメントコンソールでタイムアウト設定項目までの移動手順を紹介します。

タイムアウト設定項目までの移動手順
  • 手順1
    AWSマネジメントコンソール上でAPI Gatewayのメニューに移動
  • 手順2
    対象のAPIを選択
  • 手順3
    タイムアウトを設定するメソッドを選択
  • 手順4
    画面中段に「統合リクエスト」のタブがあるのでクリック

    API Gateway、統合リクエストの設定画面

  • 手順5
    右上の「編集」ボタンをクリック
  • 手順6
    画面最下部にタイムアウトの設定が表示
    API Gatewayのタイムアウトの設定画面

画面上の説明にもある通り、最大29,000(ミリ秒)までしか設定できません。

API Gatewayのタイムアウトの設定画面

AWSのドキュメントにも最大29秒までしか設定することができないことの記載あり。

申請による上限値の引き上げも不可。

API Gatewayの29秒タイムアウト対策方法

結論:非同期処理に変更する

API Gatewayの 29秒タイムアウトを回避するには、API GatewayからLambdaの呼び出しを非同期にするしか方法はありません。

タイムアウト時間を29秒以上に変更できないため、API GatewayからLambda呼び出しを同期処理にしていると回避できません。

Z-A-K-I
Z-A-K-I

Lambdaの性能を上げて、29秒以内に処理を終わらせるようにする方法もありますが、処理データが増えた時に、再発する可能性があります。性能UPは限界もあり、コストも高くなるため、現実的な方法ではありません。

同期処理と非同期処理の構成の違い

Lambda呼び出しを非同期にするには、二つの方法があります。

Lambda呼び出しを非同期にする二つの方法
  1. Step Functionsを利用する
  2. API Gateway からLambda呼び出しを非同期にする

本記事では「1」のStep Functionsを利用して、非同期に変更する方法をご紹介します。

Step Functionsを利用して非同期にする理由

「Step Functions」を利用した方が簡単に非同期処理に変更できます。

そのためこちらの方法を採用。

「2」の方法は【AWS】APIGatewayからLambdaを非同期呼び出しする設定で紹介しています。

Step Functionsを利用せずに対応したい場合は、こちらをご覧ください。

変更前の構成(同期処理時)

変更前はこのような構成です。

API GatewayからLambdaを同期処理で呼び出しています。

このため、API GatewayはLambdaの完了を待ち、29秒を超えるとタイムアウトエラーが発生します。

また、VueやReactなどのアプリケーションからのWebAPI呼び出しも同期処理です。

変更後の構成(非同期処理時)

変更後の構成はこちらです。

API GatewayからStep Functionsを経由して、Lambdaを呼びだす構成に変更されています。

Step FunctionsはいくつかのAPIを提供しています。

今回は「StartExecution API」「DescribeExecution API」を利用して、非同期処理を実現しています。

「StartExecution API」・・・処理の実行を指示するAPI

「DescribeExecution API」・・・処理の状況を確認するAPI

変更前(同期処理時)にLambdaから返却されていた値は、変更後は「DescribeExecution API」から返却されます。

そのため、Lambdaからの返却データもこれまで通り利用することが可能です。

API Gatewayは、Step Functionsの「StartExecution API」「DescribeExecution API」を呼び出すため、二つのリソースを作成する必要があります。

通常、非同期処理を構築する場合、処理完了を検知するための新たなLambdaが必要となります。

Step Functionsを利用すると処理完了用のLambdaを作成せずに対応できます。

非同期処理への変更手順

StepFunctionsを利用する構成に必要な手順は以下です。

StepFunctionsを利用する構成への変更手順
  • Step1
    IAMロールの作成
  • Step2
    StepFunctionsの作成
  • Step3
    API Gatewayの作成
  • Step4
    動作確認(Postmanを使ったテスト)

順番に解説していきます。

1. IAMロールの作成

API GatewayからStepFunctionsの呼出しを許可するロールを作成します。

下記赤枠のポリシーを保持したIAMロールを作成してください。

API GatewayからStepFunctionsの呼出しを許可するIAMロールのß作成

ロールの作成方法は、AWS公式サイトのドキュメントのステップ1の手順を参照ください。

2. StepFunctionsの作成

AWSマネジメントコンソールのStepFunctionsのメニューに移動し、フローを作成します。

Step Functionsでは、フローを「ステートマシン」と呼びます。

ステートマシンの作成手順は以下です。

ステートマシンの作成手順
  • Step1
    1からワークフローを作成
  • Step2
    アクションのLambda Invokeを配置
  • Step3
    Lambda Invokeに呼び出すLambdaを設定

    Lambda呼び出しするだけの単純なフローであるため、以下のように単純なステートマシンとなります。

    ステートマシンの作成画面

3. API Gatewayの作成

作成したStepFunctionsのステートマシンを呼び出すリソースを作成していきます。

処理実行用APIと処理状況確認用APIの二つのリソースをAPI Gatewayに作成します。

処理実行用APIの作成

Lambda処理を起動するためのAPIを作成していきます。

リソースの作成

リクエストを受け付けるためのリソースを作成します。

リソースの作成内容
API GatewayでCORSを有効にする画面
  • 処理実行用APIは「execution」というリソース名で作成
  • 別オリジンから呼び出される場合は「API Gateway CORSを有効にする」にチェック
  • 「リソースの作成」ボタンをクリック
メソッドの作成

上で作成したリソース「execution」に、メソッドを作成します。

今回はPOSTメソッドで受付けます。

メソッドの作成内容
API GatewayでPOSTメソッドの作成

「アクション」に「StartExecution」を設定することで、ステートマシンの「StartExecution API」が呼び出され、Lambdaを実行させることができます。

マッピングテンプレートの作成

メソッド作成画面で下にスクロールすると、マッピングテンプレートの項目があります。

マッピングテンプレートの設定がないと、リクエストパラメータがステートマシンに連携されないので、作成していきます。

マッピングテンプレートの作成内容
マッピングテンプレートの設定
  • json形式のパラメータを想定し、Content-Typeを「application/json」を設定
  • StateMachinArnは、「2」で作成したステートマシンのArnを設定

ファイルアップロードなどのパラメータ形式の場合は、Content-Typeに「multipart/form-data」を指定してください。

マッピングテンプレートの内容(貼り付け用)

【application/jsonの場合】

{
    "input": "$util.escapeJavaScript($input.json('$'))",
    "stateMachineArn": "arn:aws:states:ap-northeast-1:xxxxxxx:stateMachine:MyStateMachine-xxxxxx"
}

【multipart/form-dataの場合】

#set( $body = $util.escapeJavaScript($input.json('$')) )
{
  "input": "{\"body\":$body,\"requestContext\":{\"requestId\":\"$context.requestId\"},\"headers\":{\"content-type\":\"$input.params().header.get('content-type')\"}}",
  "stateMachineArn": "arn:aws:states:ap-northeast-1:xxxxxx:stateMachine:Get-Amazon-Infomation_StateMachine"
}
リソースのCORSを再設定

リソース作成時にCORSを有効化してますが、「Access-Control-Allow-Headers」の設定をするため、再度設定します。

リソースを選択し、アクションから「CORSの有効化」をクリックして設定画面に移動します。

CORS再設定内容
CORSの有効化の設定
  • 赤枠内「Access-Control-Allow-headers」に「’*’」に設定
  • 「CORSを有効にして既存のCORSヘッダーを置換」をクリック

処理状況確認用APIの作成

処理状況を確認するAPIを作成していきます。

リソースの作成

「execution/status」という名前でリソースを作成します。

リソースの作成内容
処理状況確認用APIの作成
  • 別オリジンから呼び出される場合は「API Gateway CORSを有効にする」にチェック
  • 「リソースの作成」ボタンをクリック
メソッドの作成

上で作成したリソース「execution/status」を選択し、メソッドを作成していきます。

メソッドの作成内容
処理状況確認用APIのPOSTの設定
  • アクションに「DescribeExecution」を指定します。
  • アクション以外は「処理実行用API」と同様の設定です。
マッピングテンプレートの作成

マッピングテンプレートの設定は、処理状況確認用APIは不要です。

マッピングテンプレートの作成内容(設定なし)
処理状況確認用APIのマッピング転封レートの設定
リソースのCORSを際設定

処理実行用APIと同様にCORSを再設定します。

CORS再設定内容
CORSの有効化の設定
  • 赤枠内「Access-Control-Allow-headers」に「’*’」に設定
  • 「CORSを有効にして既存のCORSヘッダーを置換」をクリック

APIのデプロイ

これで設定は完了です。

任意の名前でAPIをデプロイしてください。この後動作確認をしていきます。

私はデプロイするステージを「test」という名前で作成しました。

この後の動作確認用のURLに「test」の文言が入ります。ここでデプロイした任意の名前に置き換えてください。

4. 動作確認(Postmanを使ったテスト)

処理実行用APIの呼び出し

処理実行用APIに対してリクエストを実行します。

実行時のリクエスト内容

【ヘッダーの設定】

Postmanを利用したリクエストの画面
  • 「Headers」タブを選択
  • 「Content-Type」に「application/json」を設定

【リクエスト本文の設定】

Postmanのリクエストデータの設定
  • 「Body」タブを選択
  • 「raw」を選択
  • Json形式でリクエストデータを設定
    ※私は画像のようなリクエストデータを設定

実行後、以下のように戻り値が返却されれば成功です。

Postmanでレスポンスを確認する画面

この戻り値を使って状況確認APIにリクエストすると処理状況が確認できます。

ログの確認

正しくパラメータが連携されているか確認します。

確認はステートマシンのログを見ます。

処理実行用APIの呼び出し

処理実行用APIが成功したことを確認後、処理状況確認APIにリクエストを実行します。

実行時のリクエスト内容

【URLの変更】

  • POSTするURLを「execution/status」に変更

【ヘッダーの設定】

  • 処理実行用APIと同様

【リクエスト本文の設定】

処理状況確認APIにリクエストの画面
  • 「Body」タブを選択
  • 「raw」を選択
  • Json形式でリクエストデータを設定
    ここで設定する内容は処理実行用APIを実行した際に返却された戻り値です。

リクエストを実行後、以下のような内容が返却されれば成功です。

返却データがある場合output属性のbodyに格納されます。(今回はなし)

処理状況確認APIのレスポンスの画面

おまけ

今回の動作確認は、Postmanを使って処理を確認しましたが、fetchAPIを使った場合のReactのコードを紹介します。

フロントエンド側のコードの雰囲気がわかると思います。

import logo from './logo.svg';
import './App.css';
import {Button} from "@mui/material"

function App() {
  const aaa = async () =>{
    console.log("aaa");

    const apiUrl = 'https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/develop/execution'; // 実際のAPIのエンドポイントに置き換える

    const requestData = {
      body: { aaa: '1111' },
      stateMachineArn: 'arn:aws:states:ap-northeast-1:xxxxxxx:stateMachine:MyStateMachine-b4ej5xjt2'
    };
    
    let work_res_data;

    // 実行要求
    await fetch(apiUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        // 他に必要なヘッダーがあれば追加
      },
      body: JSON.stringify(requestData),
    })
      .then(response => {
        if (!response.ok) {
          throw new Error(`HTTP error! Status: ${response.status}`);
        }
        return response.json();
      })
      .then(data => {
        work_res_data = data;
        console.log('APIからの応答:', data);
        // 応答を使って何か処理を行う
      })
      .catch(error => {
        console.error('エラー発生:', error);
        // エラーが発生した場合の処理
      });

      // 結果確認
    await fetch(apiUrl + "/status", {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        // 他に必要なヘッダーがあれば追加
      },
      body: JSON.stringify(work_res_data),
    })
      .then(response => {
        if (!response.ok) {
          throw new Error(`HTTP error! Status: ${response.status}`);
        }
        return response.json();
      })
      .then(data => {
        console.log('APIからの応答:', data);
        // 応答を使って何か処理を行う
      })
      .catch(error => {
        console.error('エラー発生:', error);
        // エラーが発生した場合の処理
      });

  }

  return (
    <div className="App">
      <Button onClick={aaa}>StepFunctionsnAPIのテスト用</Button>
    </div>
  );
}

export default App;

multipart/form-dataの場合は以下のコードとなります。

import logo from './logo.svg';
import './App.css';
import {Button} from "@mui/material"
import {MuiFileInput} from "mui-file-input"
import {useState} from "react"

function App() {

  const [file, setFile] = useState(null);
  const handleChangeFile =(newFile) =>{
    setFile(newFile);
  }

  const aaa = async () =>{
    console.log("aaa");


    const apiUrl = 'https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/test/execution'; // 実際のAPIのエンドポイントに置き換える


    let formData = new FormData();
    formData.append('file', file);
   
    let work_res_data;

    // 実行要求
    await fetch(apiUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'multipart/form-data'
        // 他に必要なヘッダーがあれば追加
      },
      body: formData,
    })
      .then(response => {
        if (!response.ok) {
          throw new Error(`HTTP error! Status: ${response.status}`);
        }
        return response.json();
      })
      .then(data => {
        work_res_data = data;
        console.log('APIからの応答:', data);
        // 応答を使って何か処理を行う
      })
      .catch(error => {
        console.error('エラー発生:', error);
        // エラーが発生した場合の処理
      });

      // 結果確認
      var resStatusData = await confirmRequest(apiUrl, work_res_data)
      
      while (resStatusData.status === "RUNNING") {
        console.log(resStatusData.status);
        await sleep(1000);
        resStatusData = await confirmRequest(apiUrl, work_res_data)
      }
      console.log("abc");
  }

  function sleep(milliseconds) {
    return new Promise(resolve => setTimeout(resolve, milliseconds));
  }

  async function confirmRequest(apiUrl, work_res_data){

    let returnData;
    
    await fetch(apiUrl + "/status", {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(work_res_data),
    })
      .then(response => {
        if (!response.ok) {
          throw new Error(`HTTP error! Status: ${response.status}`);
        }
        return response.json();
      })
      .then(data => {
        console.log('APIからの応答:', data);
        returnData = data;
      })
      .catch(error => {
        console.error('エラー発生:', error);
        returnData = error;
      });
    return returnData;
  }

  return (
    <div className="App">
      <input type="file" id="fileInput"></input>
      <MuiFileInput value={file} onChange={handleChangeFile}></MuiFileInput>
      <Button onClick={aaa}>StepFunctionsnAPIのテスト</Button>
    </div>
  );
}

export default App;

処理結果を確認するために、処理状況確認APIの呼び出しをポーリングする必要があります。

ポーリング処理は上のコードに組み込まれてませんのでご留意ください。

注意点

上記の対応で一見成功したように見えますが、一点注意事項があります。

Step Functionsの送信ペイロードにバイト制限があることです。

同期処理の時には、ペイロードの制限はありませんでしたが、Step Functionsのステートマシンを利用する場合、パラメータを256KBのサイズまでしか渡すことができませんでした。

データ取得するための検索条件などの値であれば問題ありませんが、multipart/form-dataを使ったファイルアップロード処理などではこの制限が影響する可能性があるので、ご注意ください。

ペイロードのサイズ制限は、Step Functions特有のものではなく、非同期処理全般にありそうです。

Step Functionsを利用せずに非同期にした場合でも同様の事象となりました。

まとめ

本記事では、API Gatewayの29秒タイムアウト制限を回避する対策方法の一つを紹介しました。

API Gatewayは、現在のトレンドのSPA方式のアプリケーションを構築する際には必須のサービスです。

利用頻度が高いサービスですので、タイムアウトの制限と対策方法を知っておくことで適切に設計することができます。

本記事が皆様の参考になりましたら幸いです。

関連記事:APIGatewayからLambdaを非同期呼び出しする方法

参考書籍

参考サイト

本記事を作成するにあたり、以下サイトを参考にさせていただきました。

ありがとうございます。

APIGatewayタイムアウト不可避を回避(神回避) - Qiita
APIGateway初心者あるある「LambdaとAPIGatewayでAPI作ったで!ちょい処理重めやけどLambdaは最大15分まで動くし、15分もあったら余裕やわ」ポチっ(実行)30秒後…
【AWS】Step Functionsによる非同期APIの作り方 - Qiita
普通にLambdaで関数を作成し、API GatewayでWepアプリ化しても、以下の制限により使い物にならない場合がある。API Gatewayの最大タイムアウト30秒以内にレスポンスが無い場合…

コメント