Quick に航空券を探してみた - 認証ありAPI連携
👉本ブログは AWS AI Agent ブログ祭り (Zenn: #awsaiagentblogfes, X: #AWS_AI_AGENT_ブログ祭り) の第 8 日目です。

3 行まとめ
- Amazon Quick Suite で「航空券を探すエージェント」を自然言語で作成
- Amadeus API と連携して、日本語で航空券検索ができるシステムを構築
- 「HNDからSYDへクリスマスに行きたい」という問い合わせに対して、Agentic AI がリアルタイムの情報を用いて回答
はじめに
Amazon Quick Suite が 2025/10/09 に一般提供を開始しました。
公式のアナウンスには
(前略) 行動へと移すことができるよう支援する、新しい一連のエージェントチームメイトです。
なんかすごそう!
でも、何をしてくれたら嬉しいだろうか。
秋になりどんどん寒くなる中🍂、Agentic AI について考えていたとき、ふとクリスマスが頭をよぎりました🎄
そうだ、サーフィンをしているサンタさんを見に行きたい🎅!!
そこでこのブログでは Amazon Quick Suite でシドニーへ行く飛行機を探してみたので、その手順をご紹介します。

1. Amadeus で無料アカウントを作っておこう
Amadeus for Developers の YouTube チャンネルではセットアップの手順をすごく細やかに説明しています。動画を見ながら Amadeus への登録をしておきましょう。
2. AWS CloudFormation で Amadeus へアクセスするための事前準備をしよう
AWS CloudFormation を使って、AWS Lambda と Amazon API Gateway のリソースを作っておきましょう。
CloudFormation のコンソールページへアクセスし、スタックの作成 ボタンを押下してください。

このブログ下部 Appendix にテンプレートを書いていますので、それをローカルに amadeus-tool.yml などの名前で保存しておき、それをアップロードしてください。

amadeusのページから API Key と API Secret を確認し、パラメータへコピペしてください。
※ Amadeus へログイン済みである前提です

IAM リソースが作成されることに同意し、次へ を押下してください。

送信ボタンを押下してください。

3. Amazon Quick Suite のサインアップ
Amazon Quick Suite のサインアップ画面へアクセスし、サインアップを進めます。

- アカウント名
- アカウント通知用メールアドレス
をそれぞれ入力し、アカウントを作成ボタンを押下します。
※ アカウント名は一意である必要があります、

しばらく待つとアカウント作成が完了します。

リージョンが us-east-1(バージニア)であることに注意してください。
また、此処から先の手順は日本語で進めていきます。
4. Amadeus を接続しよう
Integration のページ のページでは、Quick Suite と外部のサービスとの Integration を設定できます。移動し、OpenAPI Specification を選択します。

スキーマとして json ファイルをアップロードします。
ブログ下部の Appendix から内容を確認し、ローカルに openapi.json 等のファイル名で保存しておいてください。
またスキーマのインポートボタンからファイルをアップロードします。

アップロードできたら、次へボタンを押下します。

名前と説明を付けます。
名前は flight
説明は API to retrieve flight information from Amadeus
としておきます。

先程の手順の AWS CloudFormation のコンソールページの出力タブより、ApiEndpoint と CognitoDomainURL を確認します。
そして、Amazon Quick Suite の ベース URL, トークン URL, 認証 URL にそれぞれ代入します。

先程の手順の AWS CloudFormation のコンソールページの出力タブより AmadeusUserPool から Amazon Cognito のコンソールページへ移動します。
そこでクライアント ID, クライアントシークレット が確認できますので、それぞれ Amazon Quick Suite のコンソールへ貼り付けてください。

アクションを確認し、作成して続行ボタンを押下します。

共有もできますが、ここでは何もせず、次へボタンを押下します。

インテグレーションのコンソールページから flight を選択します。

サインインボタンを押下します。

ここでは Amazon API Gateway を保護している Amazon Cognito の認証情報を問われています。
まだユーザーがいない状態ですので、Sign up リンクを押下し、サインアップを進めます。

Email が受領できるメールアドレスと任意のパスワードを入力し、Sign up ボタンを押下します。

認証コードを受領したら、Verification code を入力し Confirm account を押下します。

5. あなた専用のエージェントを作ろう
Amazon Quick Suite の新しいエージェントを作成するページから
飛行機の座席を見つけてくれるエージェントを作って。
と入力し、生成ボタンを押下します。

作成を押すと、あなた専用のエージェントが自動で作成されます🤖
アクショントグルを開くと flight が自動で選ばれているはずです。
ここまでできたら、クリスマスにシドニーへ行く便を探してみましょう。
HND から SYD へ 2025-12-24 に出発する便を探して
と入力し送信ボタンを押下します。

便が正しく表示されれば正常に動作しています。
正常に作成できたら、保存しておきましょう。チャットエージェントを起動ボタンをおうかし、チャットエージェントの詳細を表示するを押下します。

遷移先の画面で chat ボタンを押下し、保存済みのエージェントとチャットしてみます。

同様に
HND から SYD へ 2025-12-24 に出発する便を探して
と入力し試してみましょう。飛行機が見つけられましたね🎅!
6. Clean Up
この手順では Amazon Quick Suite と AWS CloudFormation のリソースを削除します。
Amazon Quick Suite の削除
Quick Suite の管理ページから Manage ボタンを押下し、削除を進めます。

AWS CloudFormation のコンソールページから amadeus のスタックを削除します。

まとめ
Amazon Quick Suiteで「飛行機探しエージェント」を作ってみました!
最も驚いたのは、「飛行機の席を見つけてくれるエージェントを作って」という自然言語の指示だけで、自動的にエージェントが生成されたことです🤖。さらに、日本語で「HNDからSYDへクリスマスに行きたい」と問い合わせると、リアルタイムで実際の便情報を取得してくれました✈️。また、従来は複雑だったAPI統合も、OpenAPI Specificationを使うことで驚くほど簡単に実現できました。
Amazon Quick Suiteはプレスリリースの通り、これまでの BI 製品とは大きく変わり、実際のビジネスプロセスを自動化できる本格的なAgentic AIプラットフォームでした。開発者だけでなくビジネスユーザーも、自然言語だけでAIエージェントを作れる時代が到来したことを実感できる体験でした。
みなさんも、Amazon Quick Suite で Agentic AI を作り、クリスマスにシドニーでサーフィンするサンタさんに会いに行く準備をしてみてください🎅
Appendix
AWS CloudFormation のテンプレート
AWSTemplateFormatVersion: '2010-09-09'
Description: Amadeus Flight Search API with Cognito Authorization
Parameters:
AmadeusApiKey:
Type: String
Description: Amadeus API Key
NoEcho: true
AmadeusApiSecret:
Type: String
Description: Amadeus API Secret
NoEcho: true
Resources:
# Cognito Resources
AmadeusUserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: AmadeusFlightSearchUserPool
UsernameAttributes:
- email
AutoVerifiedAttributes:
- email
AmadeusResourceServer:
Type: AWS::Cognito::UserPoolResourceServer
Properties:
Identifier: "amadeus-api"
Name: "amadeus-api"
UserPoolId: !Ref AmadeusUserPool
Scopes:
- ScopeName: "flights.read"
ScopeDescription: "Read access to flight search"
AmadeusUserPoolClient:
Type: AWS::Cognito::UserPoolClient
DependsOn: AmadeusResourceServer
Properties:
ClientName: AmadeusFlightSearchClient
UserPoolId: !Ref AmadeusUserPool
GenerateSecret: true
AllowedOAuthFlowsUserPoolClient: true
AllowedOAuthFlows:
- code
AllowedOAuthScopes:
- email
- openid
- profile
- amadeus-api/flights.read
CallbackURLs:
- "https://us-east-1.quicksight.aws.amazon.com/sn/oauthcallback"
SupportedIdentityProviders:
- COGNITO
UserPoolDomain:
Type: AWS::Cognito::UserPoolDomain
Properties:
Domain: !Sub ${AWS::AccountId}-${AWS::Region}-amadeus
UserPoolId: !Ref AmadeusUserPool
# API Gateway
AmadeusApi:
Type: AWS::ApiGateway::RestApi
Properties:
Name: AmadeusFlightSearchAPI
Description: API for searching flights using Amadeus
CognitoAuthorizer:
Type: AWS::ApiGateway::Authorizer
Properties:
Name: AmadeusAuthorizer
Type: COGNITO_USER_POOLS
IdentitySource: method.request.header.Authorization
ProviderARNs:
- !GetAtt AmadeusUserPool.Arn
RestApiId: !Ref AmadeusApi
# API Resources
FlightSearchResource:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId: !Ref AmadeusApi
ParentId: !GetAtt AmadeusApi.RootResourceId
PathPart: flight-search
FlightSearchMethod:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref AmadeusApi
ResourceId: !Ref FlightSearchResource
HttpMethod: GET
AuthorizationType: COGNITO_USER_POOLS
AuthorizerId: !Ref CognitoAuthorizer
AuthorizationScopes:
- "amadeus-api/flights.read"
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${FlightSearchFunction.Arn}/invocations
MethodResponses:
- StatusCode: 200
ApiDeployment:
Type: AWS::ApiGateway::Deployment
Properties:
RestApiId: !Ref AmadeusApi
StageName: Prod
DependsOn:
- FlightSearchMethod
# Lambda Function
FlightSearchFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: AmadeusFlightSearchFunction
Handler: index.handler
Role: !GetAtt LambdaExecutionRole.Arn
Runtime: python3.10
Timeout: 30
Environment:
Variables:
AMADEUS_API_KEY: !Ref AmadeusApiKey
AMADEUS_API_SECRET: !Ref AmadeusApiSecret
Code:
ZipFile: |
import json
import os
import urllib3
import logging
from urllib.parse import urlencode
# ログ設定
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# HTTPクライアントをグローバルで再利用
http = urllib3.PoolManager(
timeout=urllib3.Timeout(connect=5, read=30),
retries=urllib3.Retry(
total=3,
backoff_factor=0.3,
status_forcelist=[500, 502, 503, 504]
)
)
def get_amadeus_token():
"""Get access token from Amadeus API"""
try:
# 環境変数の存在チェック
api_key = os.environ.get('AMADEUS_API_KEY')
api_secret = os.environ.get('AMADEUS_API_SECRET')
if not api_key or not api_secret:
logger.error("API credentials not found in environment variables")
raise Exception("API credentials not configured")
# Request token
headers = {
'Content-Type': 'application/x-www-form-urlencoded'
}
data = {
'grant_type': 'client_credentials',
'client_id': api_key,
'client_secret': api_secret
}
logger.info("Requesting Amadeus access token")
response = http.request(
'POST',
'https://test.api.amadeus.com/v1/security/oauth2/token',
body=urlencode(data),
headers=headers
)
if response.status == 200:
token_data = json.loads(response.data.decode('utf-8'))
logger.info("Successfully obtained access token")
return token_data['access_token']
elif response.status == 401:
logger.error("Authentication failed - invalid credentials")
raise Exception("Invalid API credentials")
elif response.status == 400:
logger.error("Bad request when getting token")
raise Exception("Invalid token request")
else:
logger.error(f"Token request failed with status: {response.status}")
raise Exception(f"Failed to get token: HTTP {response.status}")
except urllib3.exceptions.TimeoutError:
logger.error("Timeout when requesting access token")
raise Exception("Token request timeout")
except urllib3.exceptions.RequestException as e:
logger.error(f"Network error when requesting token: {str(e)}")
raise Exception("Network error during authentication")
except json.JSONDecodeError:
logger.error("Invalid JSON response from token endpoint")
raise Exception("Invalid response from authentication service")
except Exception as e:
logger.error(f"Unexpected error in get_amadeus_token: {str(e)}")
raise
def search_flights(params, token):
"""Search flights using Amadeus API"""
try:
headers = {
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
}
# Build query string
query_params = {}
for key, value in params.items():
if value is not None:
query_params[key] = value
url = f"https://test.api.amadeus.com/v2/shopping/flight-offers?{urlencode(query_params)}"
logger.info(f"Searching flights with URL: {url}")
response = http.request('GET', url, headers=headers)
if response.status == 200:
logger.info("Flight search completed successfully")
return json.loads(response.data.decode('utf-8'))
elif response.status == 401:
logger.error("Flight search failed - invalid token")
raise Exception("Authentication failed - token expired or invalid")
elif response.status == 400:
logger.error("Flight search failed - bad request")
error_data = response.data.decode('utf-8')
raise Exception(f"Invalid search parameters: {error_data}")
elif response.status == 404:
logger.error("Flight search failed - no results found")
raise Exception("No flights found for the specified criteria")
else:
logger.error(f"Flight search failed with status: {response.status}")
raise Exception(f"Flight search failed: HTTP {response.status}")
except urllib3.exceptions.TimeoutError:
logger.error("Timeout during flight search")
raise Exception("Flight search timeout")
except urllib3.exceptions.RequestException as e:
logger.error(f"Network error during flight search: {str(e)}")
raise Exception("Network error during flight search")
except json.JSONDecodeError:
logger.error("Invalid JSON response from flight search")
raise Exception("Invalid response from flight search service")
except Exception as e:
logger.error(f"Unexpected error in search_flights: {str(e)}")
raise
def validate_required_params(query_params):
"""Validate required parameters"""
required_params = ['originLocationCode', 'destinationLocationCode', 'departureDate', 'adults']
missing_params = []
for param in required_params:
if not query_params.get(param):
missing_params.append(param)
if missing_params:
raise ValueError(f"Missing required parameters: {', '.join(missing_params)}")
# 基本的なパラメータ検証
try:
adults = int(query_params['adults'])
if adults < 1 or adults > 9:
raise ValueError("Adults must be between 1 and 9")
except ValueError as e:
if "invalid literal" in str(e):
raise ValueError("Adults must be a valid number")
raise
def handler(event, context):
try:
logger.info(f"Flight search request received: {event}")
# Extract query parameters
query_params = event.get('queryStringParameters', {}) or {}
# Validate required parameters
validate_required_params(query_params)
# Get Amadeus token
token = get_amadeus_token()
# Search flights
amadeus_params = {
'originLocationCode': query_params.get('originLocationCode'),
'destinationLocationCode': query_params.get('destinationLocationCode'),
'departureDate': query_params.get('departureDate'),
'returnDate': query_params.get('returnDate'),
'adults': query_params.get('adults'),
'children': query_params.get('children'),
'travelClass': query_params.get('travelClass'),
'currencyCode': query_params.get('currencyCode'),
'maxPrice': query_params.get('maxPrice'),
'nonStop': query_params.get('nonStop'),
'max': '10' # Limit results
}
flight_data = search_flights(amadeus_params, token)
logger.info("Flight search completed successfully")
return {
'statusCode': 200,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
'body': json.dumps(flight_data)
}
except ValueError as e:
logger.warning(f"Validation error: {str(e)}")
return {
'statusCode': 400,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
'body': json.dumps({'error': str(e)})
}
except Exception as e:
logger.error(f"Unexpected error in handler: {str(e)}")
return {
'statusCode': 500,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
'body': json.dumps({'error': 'Internal server error occurred'})
}
# Lambda permissions
ApiGatewayLambdaInvokePermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !GetAtt FlightSearchFunction.Arn
Action: lambda:InvokeFunction
Principal: apigateway.amazonaws.com
SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${AmadeusApi}/*/*
# IAM Role for Lambda
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Outputs:
ApiEndpoint:
Description: API Gateway endpoint URL
Value: !Sub https://${AmadeusApi}.execute-api.${AWS::Region}.amazonaws.com/Prod
CognitoDomainURL:
Description: Cognito Domain URL
Value: !Sub https://${AWS::AccountId}-${AWS::Region}-amadeus.auth.${AWS::Region}.amazoncognito.com
UserPoolId:
Description: Cognito User Pool ID
Value: !Ref AmadeusUserPool
UserPoolClientId:
Description: Cognito User Pool Client ID
Value: !Ref AmadeusUserPoolClient
OpenAPI スキーマ
{
"openapi": "3.0.0",
"info": {
"title": "Amadeus Flight Search",
"version": "1.0.0",
"description": "API for searching flight offers using Amadeus"
},
"servers": [
{
"url": "https://***.execute-api.us-east-1.amazonaws.com/Prod"
}
],
"paths": {
"/flight-search": {
"get": {
"summary": "Search flight offers",
"description": "Search for flight offers based on origin, destination, and travel dates",
"operationId": "searchFlightOffers",
"parameters": [
{
"name": "originLocationCode",
"in": "query",
"description": "Origin airport IATA code (e.g., NRT for Tokyo Narita)",
"required": true,
"schema": {
"type": "string",
"pattern": "[A-Z]{3}"
}
},
{
"name": "destinationLocationCode",
"in": "query",
"description": "Destination airport IATA code (e.g., LAX for Los Angeles)",
"required": true,
"schema": {
"type": "string",
"pattern": "[A-Z]{3}"
}
},
{
"name": "departureDate",
"in": "query",
"description": "Departure date in YYYY-MM-DD format",
"required": true,
"schema": {
"type": "string",
"format": "date"
}
},
{
"name": "returnDate",
"in": "query",
"description": "Return date in YYYY-MM-DD format (optional for one-way)",
"required": false,
"schema": {
"type": "string",
"format": "date"
}
},
{
"name": "adults",
"in": "query",
"description": "Number of adult passengers (1-9)",
"required": true,
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 9,
"default": 1
}
},
{
"name": "children",
"in": "query",
"description": "Number of child passengers (0-9)",
"required": false,
"schema": {
"type": "integer",
"minimum": 0,
"maximum": 9
}
},
{
"name": "travelClass",
"in": "query",
"description": "Travel class preference",
"required": false,
"schema": {
"type": "string",
"enum": ["ECONOMY", "PREMIUM_ECONOMY", "BUSINESS", "FIRST"]
}
},
{
"name": "currencyCode",
"in": "query",
"description": "Preferred currency (e.g., JPY, USD)",
"required": false,
"schema": {
"type": "string",
"pattern": "[A-Z]{3}"
}
},
{
"name": "maxPrice",
"in": "query",
"description": "Maximum price per traveler",
"required": false,
"schema": {
"type": "integer",
"minimum": 1
}
},
{
"name": "nonStop",
"in": "query",
"description": "Search for non-stop flights only",
"required": false,
"schema": {
"type": "boolean",
"default": false
}
}
],
"responses": {
"200": {
"description": "Flight offers found successfully",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Flight offer ID"
},
"price": {
"type": "object",
"properties": {
"currency": {
"type": "string",
"description": "Price currency"
},
"total": {
"type": "string",
"description": "Total price"
},
"base": {
"type": "string",
"description": "Base price without taxes"
}
}
},
"itineraries": {
"type": "array",
"items": {
"type": "object",
"properties": {
"duration": {
"type": "string",
"description": "Flight duration"
},
"segments": {
"type": "array",
"items": {
"type": "object",
"properties": {
"departure": {
"type": "object",
"properties": {
"iataCode": {
"type": "string"
},
"at": {
"type": "string"
}
}
},
"arrival": {
"type": "object",
"properties": {
"iataCode": {
"type": "string"
},
"at": {
"type": "string"
}
}
},
"carrierCode": {
"type": "string"
},
"number": {
"type": "string"
}
}
}
}
}
}
}
}
}
},
"meta": {
"type": "object",
"properties": {
"count": {
"type": "integer",
"description": "Number of flight offers returned"
}
}
}
}
}
}
}
},
"400": {
"description": "Bad request - invalid parameters",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string",
"description": "Error message"
}
}
}
}
}
},
"500": {
"description": "Internal server error",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string",
"description": "Error message"
}
}
}
}
}
}
},
"security": [
{
"oauth2": ["amadeus-api/flights.read"]
}
]
}
}
},
"components": {
"securitySchemes": {
"oauth2": {
"type": "oauth2",
"description": "OAuth2 Authorization Code flow for authentication",
"flows": {
"authorizationCode": {
"authorizationUrl": "https://***.auth.us-east-1.amazoncognito.com/oauth2/authorize",
"tokenUrl": "https://***.auth.us-east-1.amazoncognito.com/oauth2/token",
"refreshUrl": "https://***.auth.us-east-1.amazoncognito.com/oauth2/token",
"scopes": {
"amadeus-api/flights.read": "Read access to flight search API"
}
}
}
}
}
}
}
Discussion