本記事では、API Gatewayの29秒でタイムアウトする事象を対策した事例を紹介します。
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で設定できるサービスです。
ステートマシンと呼ばれるものを作成し実行します。
Lambda
サーバーレスでプログラムを実行することができるAWSのサービスです。
サーバーレスのため、プログラムを実行するサーバーやミドルウェアを準備せずにプログラムを作成できます。
マイクロサービスのプログラムを構築する際に利用されます。
API Gatewayのタイムアウト設定方法
API Gatewayでは、リソースのメソッド単位でタイムアウト時間を設定できます。
ここでは、マネジメントコンソールでタイムアウト設定項目までの移動手順を紹介します。
- 手順1AWSマネジメントコンソール上でAPI Gatewayのメニューに移動
- 手順2対象のAPIを選択
- 手順3タイムアウトを設定するメソッドを選択
- 手順4画面中段に「統合リクエスト」のタブがあるのでクリック
- 手順5右上の「編集」ボタンをクリック
- 手順6画面最下部にタイムアウトの設定が表示
API Gatewayの29秒タイムアウト対策方法
結論:非同期処理に変更する
API Gatewayの 29秒タイムアウトを回避するには、API GatewayからLambdaの呼び出しを非同期にするしか方法はありません。
タイムアウト時間を29秒以上に変更できないため、API GatewayからLambda呼び出しを同期処理にしていると回避できません。
Lambdaの性能を上げて、29秒以内に処理を終わらせるようにする方法もありますが、処理データが増えた時に、再発する可能性があります。性能UPは限界もあり、コストも高くなるため、現実的な方法ではありません。
同期処理と非同期処理の構成の違い
Lambda呼び出しを非同期にするには、二つの方法があります。
本記事では「1」の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」を呼び出すため、二つのリソースを作成する必要があります。
非同期処理への変更手順
StepFunctionsを利用する構成に必要な手順は以下です。
- Step1IAMロールの作成
- Step2StepFunctionsの作成
- Step3API Gatewayの作成
- Step4動作確認(Postmanを使ったテスト)
順番に解説していきます。
1. IAMロールの作成
API GatewayからStepFunctionsの呼出しを許可するロールを作成します。
下記赤枠のポリシーを保持したIAMロールを作成してください。
ロールの作成方法は、AWS公式サイトのドキュメントのステップ1の手順を参照ください。
2. StepFunctionsの作成
AWSマネジメントコンソールのStepFunctionsのメニューに移動し、フローを作成します。
ステートマシンの作成手順は以下です。
- Step11からワークフローを作成
- Step2アクションのLambda Invokeを配置
- Step3Lambda Invokeに呼び出すLambdaを設定
Lambda呼び出しするだけの単純なフローであるため、以下のように単純なステートマシンとなります。
3. API Gatewayの作成
作成したStepFunctionsのステートマシンを呼び出すリソースを作成していきます。
処理実行用APIと処理状況確認用APIの二つのリソースをAPI Gatewayに作成します。
処理実行用APIの作成
Lambda処理を起動するためのAPIを作成していきます。
リソースの作成
リクエストを受け付けるためのリソースを作成します。
メソッドの作成
上で作成したリソース「execution」に、メソッドを作成します。
今回はPOSTメソッドで受付けます。
マッピングテンプレートの作成
メソッド作成画面で下にスクロールすると、マッピングテンプレートの項目があります。
マッピングテンプレートの設定がないと、リクエストパラメータがステートマシンに連携されないので、作成していきます。
リソースのCORSを再設定
リソース作成時にCORSを有効化してますが、「Access-Control-Allow-Headers」の設定をするため、再度設定します。
リソースを選択し、アクションから「CORSの有効化」をクリックして設定画面に移動します。
処理状況確認用APIの作成
処理状況を確認するAPIを作成していきます。
リソースの作成
「execution/status」という名前でリソースを作成します。
メソッドの作成
上で作成したリソース「execution/status」を選択し、メソッドを作成していきます。
マッピングテンプレートの作成
マッピングテンプレートの設定は、処理状況確認用APIは不要です。
リソースのCORSを際設定
処理実行用APIと同様にCORSを再設定します。
APIのデプロイ
これで設定は完了です。
任意の名前でAPIをデプロイしてください。この後動作確認をしていきます。
4. 動作確認(Postmanを使ったテスト)
処理実行用APIの呼び出し
処理実行用APIに対してリクエストを実行します。
実行後、以下のように戻り値が返却されれば成功です。
この戻り値を使って状況確認APIにリクエストすると処理状況が確認できます。
ログの確認
正しくパラメータが連携されているか確認します。
確認はステートマシンのログを見ます。
処理実行用APIの呼び出し
処理実行用APIが成功したことを確認後、処理状況確認APIにリクエストを実行します。
リクエストを実行後、以下のような内容が返却されれば成功です。
返却データがある場合output属性のbodyに格納されます。(今回はなし)
おまけ
今回の動作確認は、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;
注意点
上記の対応で一見成功したように見えますが、一点注意事項があります。
Step Functionsの送信ペイロードにバイト制限があることです。
同期処理の時には、ペイロードの制限はありませんでしたが、Step Functionsのステートマシンを利用する場合、パラメータを256KBのサイズまでしか渡すことができませんでした。
データ取得するための検索条件などの値であれば問題ありませんが、multipart/form-dataを使ったファイルアップロード処理などではこの制限が影響する可能性があるので、ご注意ください。
まとめ
本記事では、API Gatewayの29秒タイムアウト制限を回避する対策方法の一つを紹介しました。
API Gatewayは、現在のトレンドのSPA方式のアプリケーションを構築する際には必須のサービスです。
利用頻度が高いサービスですので、タイムアウトの制限と対策方法を知っておくことで適切に設計することができます。
本記事が皆様の参考になりましたら幸いです。
関連記事:APIGatewayからLambdaを非同期呼び出しする方法
参考サイト
本記事を作成するにあたり、以下サイトを参考にさせていただきました。
ありがとうございます。
コメント