本記事では、API Gatewayが最大29秒でタイムアウトする問題を対策内容をご紹介します。
Lambdaのタイムアウト15分をクリアするように設計したのに、APIGatewayと連携すると、29秒になってしまう・・・
このままでは外部公開できない、なんとかする方法はないのか!?
と、困っている方におすすめです。
この記事では、以下のことを紹介してます。
- APIGatewayのタイムアウト対策で利用するAWSサービスの紹介
- API Gatewayタイムアウト対策方法
- タイムアウト対策にあたりAWSサービス側の設定内容
- タイムアウト対策にあたりフロントエンド側の実装方法
- 注意点
ReactやVueなどで作成されたSPA方式のアプリケーションでは、バックエンドのWebAPIを呼び出して機能を利用者に提供します。バックエンドのWebAPIをAWSで作成する場合、API GatewayとLambdaで構築するのがよくある構成です。
Lambda自体のタイムアウトは15分まで設定できますが、API Gatewayは29秒でタイムアウトする仕様となってます。Lambdaのタイムアウト時間よりAPI Gatewayのタイムアウトが短いため、Lambdaの処理タイムアウト前に利用者にタイムアウトのレスポンスが発生してしまいます。
このAPIGatewayのタイムアウトの時間は変更することができますが、最大値の29秒以上に設定することができません。Lambdaのタイムアウト15分まで許容した設計だと、処理時間が29秒を超えることはよくあります。
今回は、このような場合の対策方法の一つを紹介した記事となります。
当記事の対策内容はあくまで対策方法の一つです。
採用するかは、対象プロダクトの特性を踏まえて判断してください。
利用するAWSサービスについて
APIGatewayのタイムアウトを対策するにあたり利用するサービスを紹介します。
API Gateway
API GatewayとはAWSのサービスの一つです。
REST APIやWebsocket API を管理、公開するためのサービスです。Lambdaなどで作成したAPIをインターネットに公開できます。また、APIのバージョンを管理することもでき、簡単に過去のバージョンに戻すことができるサービスとなります。
詳細な説明はAWSの以下のドキュメントを参照ください。
AWS StapFunctions
ワークフローをGUIで設定できるAWSのサービスとなります。
利用時にステートマシンと呼ばれるものを作成し実行します。
AWS StepFunctionsには無料枠が存在しますので、無料枠内で検証していきます。
Lambda
サーバーレスでプログラムを実行することができるAWSのサービスです。
マイクロサービスのプログラムを構築する際に利用されます。
Lambdaについては、以下記事を参照ください。JavaでLambdaを作成する方法を紹介してます。
API Gatewayのタイムアウトの設定方法
タイムアウトの設定は、AWS Console上から設定できます。
タイムアウトの設定は、リソースのメソッド単位で設定することができます。
API Gatewayのメニューに移動し、対象のAPIを選択します。タイムアウトを設定するメソッドを選択します。
画面中段に「統合リクエスト」のタブがあるのでクリックすると以下の画面が表示されます。
右上の「編集」ボタンをクリックすると画面が切り替わり、画面最下部にタイムアウトの設定があります。説明にもある通り最大29,000(ミリ秒)までしか設定できません。
AWSのドキュメントにも最大29秒までしか設定することができないことが記載されています。また、申請による上限値の引き上げもすることができません。
対応方法
結論
フロントエンドからバックエンドの呼び出しを非同期処理にするしか方法はありません。
API Gatewayのタイムアウトが29秒から変更することができない以上、同期処理では対応することができないためです。厳密にいうとバックエンド側のLambdaの性能を上げて、29秒以内に処理を終わらせるようにする対策はありますが、コストの面や対象データが増加した場合などに再発することを考えると現実的な対策方法ではありません。
同期処理を非同期に変更する方法
API GatewayとLambdaの間にStep Functionsというサービスを挟むことで非同期処理を実現できます。
StepFunctionsを利用することで、APIGatewayからLambdaの呼び出しを同期処理から非同期処理に変更できます。変更するために完了検知用のLambdaなど新しいLambdaを作成する必要がなく、対応することができます。
フロントエンド側は、バックエンド通信する箇所をは同期処理から非同期処理に変更します。
StepFunctionsを利用せずにLambda呼び出しを非同期する方法もあります。その場合は、以下記事を参照ください。
https://z-a-k-i.com/2023/12/18/【aws】api-gatewayでlambdaを非同期呼び出しする方法/
StepFunctionsの利用イメージ
まずは利用するStepFunctionsのイメージから説明します。
StepFunctionsのステートマシンはいくつかのAPIを提供しています。今回は、処理の実行を指示する「StartExecution API」と処理の状況を確認する「DescribeExecution API」を利用します。
Lambdaを呼び出していたAPI Gatewayのリソースを、StepFunctionsのステートマシンの「StartExecution API」、「DescribeExecution API」を呼び出す二つのリソースを呼び出す構成に変更します。
変更前にLambdaから返却されていた値は、変更後は「DescribeExecution API」から返却されます。
全体作業
以下手順でStepFunctionsを利用する構成に変更します。
- IAMロールの作成
- StepFunctionsの作成
- API Gatewayの作成
- Postmanを使ったテスト
1. IAMロールの作成
API GatewayからStepFunctionsの呼出しを許可するロールを作成します。
下記赤枠のポリシーを保持したIAMロールを作成してください。
以下、AWS公式サイトのドキュメントのステップ1の手順で作成します。
2. StepFunctionsの作成
AWSマネジメントコンソールのStepFunctionsのメニューに移動し、フローを作成します。(このフローを「ステートマシン」と呼びます。)
作成するステートマシンはLambda Invokeのみを実行するよう作ります。
以下の手順で作成します。
- 1からワークフローを作成
- アクションのLambda Invokeを配置
- Lambda Invokeに呼び出すLambdaを設定
Lambda呼び出しするだけの単純なフローであるため、以下のようなステートマシンとなります。
3. API Gatewayの作成
API Gatewayで上で作成したStepFunctionsのステートマシンを呼び出すリソースを作成していきます。処理実行用APIと処理状況確認用APIの二つのリソースを作成します。
処理実行用APIの作成
Lambda処理を起動するためのAPIです。
処理実行用APIは「execution」というリソースで作成します。別オリジンから呼び出される場合は「API Gateway CORSを有効にする」にチェックをします。
作成したexecutionのリソースを選択し、メソッドを作成します。今回はPOSTメソッドを作成してます。
以下の設定で作成します。
「アクション」に「StartExecution」を設定することで、ステートマシンの「StartExecution API」が呼び出され、Lambdaを実行させることができます。
下にスクロールし、マッピングテンプレートを以下のように設定します。この設定がないと、リクエストパラメータがステートマシンに連携されません。
以下例では、json形式のパラメータを想定し、Content-Typeをapplication/jsonを設定していますが、ファイルアップロードのパラメータの場合は、multipart/form-dataを指定してください。
StateMachinArnは、「2」で作成したステートマシンのArnを設定します。
【上記のマッピングテンプレートの内容】
{
"input": "$util.escapeJavaScript($input.json('$'))",
"stateMachineArn": "arn:aws:states:ap-northeast-1:xxxxxxx:stateMachine:MyStateMachine-xxxxxx"
}
※application/jsonではなく、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の有効化」をクリックしてください。下記画像の赤枠内を「’*’」に変更して作成します。
処理状況確認用APIの作成
処理状況を確認するAPIを作成します。
今回はexecution/statusというリソースで、以下の設定で作成します。
POSTメソッドは以下の設定で作成します。
アクションに「DescribeExecution」を指定した以外、上で作成した処理実行用APIと同様の設定です。
処理実行用APIで設定したマッピングテンプレートの設定は、処理状況確認用APIは不要です。
こちらは、処理実行用APIと同様にCORSの設定もします。
以上で設定は完了です。
APIをデプロイし、外部からアクセスしてみます。
デプロイするステージを「test」という名前で作成したので、この後説明するバックエンドURLに「test」の文言が入ります。
4. Postmanを使ったテスト
処理実行用APIに対してリクエストを実行します。
リクエストを実行する際には、HeadersのContent-Typeの設定をします。valueには、application/jsonを設定します
リクエスト本文の設定をします。
「raw」を選択し、Json形式でリクエストデータを設定します。今回は、テスト用に以下画像のようなデータを設定しました。
実行後、以下画像のような戻り値が返却されれば成功です。この戻り値を状況確認APIにリクエストすることで処理状況が確認できます。
今回、テスト用に作成したLambdaはパラメータを使って何かしてるわけではないので、ステートマシンのログを見て想定通りにパラメータが連携されているか確認しておきます。
処理実行用APIがうまく行った後、次は処理状況確認APIにリクエストを投げます。
POSTするURLを「execution/status」に変更してBodyの値を以下のように設定します。ここで設定する内容は処理実行用APIを実行した際に返却された戻り値です。
リクエストを実行後、以下画像のような内容が返却されれば成功です。
今回は、Lambdaからの返却データがないので確認はできませんでしたが、返却データがある場合output属性のbodyに格納されます。
おまけ
Postmanを使って処理を確認しましたが、参考までにfetchAPIを使った場合のReactのコードを紹介します。
以下のコードに加えて、処理結果を確認するために、処理状況確認APIの呼び出しをポーリングする必要がありますが、フロントエンド側のコードの雰囲気がわかると思います。
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;
注意点
上記の対応で一見成功したように見えますが、この構成には落とし穴があります。
それはStepFunctionsのペイロードにバイト制限があることです。
StepFunctionsのステートマシンを利用する場合、パラメータを256KBのサイズまでしか渡せません。
データ取得するための検索条件などの値であれば問題ありませんが、multipart/form-dataを使ったファイルアップロード処理などではこの制限に引っかかってしまいますので、ご注意ください。
まとめ
API GatewayからLambda呼び出し時の29秒のタイムアウトを対策するのまとめ
本記事では、フロントエンドアプリケーションからAPI Gatewayで公開されているバックエンドを呼び出す時のタイムアウトの制約を回避する方法を紹介しました。
API Gatewayは、現在のトレンドのSPA方式のアプリケーションを構築する際には必須のサービスです。利用頻度が高いサービスですので、タイムアウトの制限と対策方法を知っておくことで適切な場面で利用できます。
皆様の参考になりましたら幸いです。
今回の記事を作成するにあたり、以下サイトを参考にさせていただきました。
ありがとうございます。
コメント