✈️

Quick に航空券を探してみた - 認証ありAPI連携

に公開

👉本ブログは AWS AI Agent ブログ祭り (Zenn: #awsaiagentblogfes, X: #AWS_AI_AGENT_ブログ祭り) の第 8 日目です。

top

3 行まとめ

  • Amazon Quick Suite で「航空券を探すエージェント」を自然言語で作成
  • Amadeus API と連携して、日本語で航空券検索ができるシステムを構築
  • 「HNDからSYDへクリスマスに行きたい」という問い合わせに対して、Agentic AI がリアルタイムの情報を用いて回答

はじめに

Amazon Quick Suite が 2025/10/09 に一般提供を開始しました。

公式のアナウンスには

(前略) 行動へと移すことができるよう支援する、新しい一連のエージェントチームメイトです。

なんかすごそう!

でも、何をしてくれたら嬉しいだろうか。

秋になりどんどん寒くなる中🍂、Agentic AI について考えていたとき、ふとクリスマスが頭をよぎりました🎄

そうだ、サーフィンをしているサンタさんを見に行きたい🎅!!

そこでこのブログでは Amazon Quick Suite でシドニーへ行く飛行機を探してみたので、その手順をご紹介します。

architecure

1. Amadeus で無料アカウントを作っておこう

Amadeus for Developers の YouTube チャンネルではセットアップの手順をすごく細やかに説明しています。動画を見ながら Amadeus への登録をしておきましょう。

https://www.youtube.com/watch?v=Lyy26SlgBZU&list=PLBehidtj-OiqQ0sIHBPvwf-8GAjMTJehF&index=2

2. AWS CloudFormation で Amadeus へアクセスするための事前準備をしよう

AWS CloudFormation を使って、AWS Lambda と Amazon API Gateway のリソースを作っておきましょう。

CloudFormation のコンソールページへアクセスし、スタックの作成 ボタンを押下してください。

createstack

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

uploadstack

amadeusのページから API Key と API Secret を確認し、パラメータへコピペしてください。

※ Amadeus へログイン済みである前提です

secret

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

ok-iam

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

createcfn

3. Amazon Quick Suite のサインアップ

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

signup

  • アカウント名
  • アカウント通知用メールアドレス

をそれぞれ入力し、アカウントを作成ボタンを押下します。

※ アカウント名は一意である必要があります、

admindata

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

moveqs

リージョンが us-east-1(バージニア)であることに注意してください。
また、此処から先の手順は日本語で進めていきます。

4. Amadeus を接続しよう

Integration のページ のページでは、Quick Suite と外部のサービスとの Integration を設定できます。移動し、OpenAPI Specification を選択します。

openapi

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

upload

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

ckjson

名前と説明を付けます。

名前は flight

説明は API to retrieve flight information from Amadeus

としておきます。

naming

先程の手順の AWS CloudFormation のコンソールページの出力タブより、ApiEndpoint と CognitoDomainURL を確認します。

そして、Amazon Quick Suite の ベース URL, トークン URL, 認証 URL にそれぞれ代入します。

tokenauth

先程の手順の AWS CloudFormation のコンソールページの出力タブより AmadeusUserPool から Amazon Cognito のコンソールページへ移動します。

そこでクライアント ID, クライアントシークレット が確認できますので、それぞれ Amazon Quick Suite のコンソールへ貼り付けてください。

client

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

ckaciton

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

ckshare

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

listaction

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

signin

ここでは Amazon API Gateway を保護している Amazon Cognito の認証情報を問われています。

まだユーザーがいない状態ですので、Sign up リンクを押下し、サインアップを進めます。

signup

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

signup

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

verification

5. あなた専用のエージェントを作ろう

Amazon Quick Suite の新しいエージェントを作成するページから

飛行機の座席を見つけてくれるエージェントを作って。

と入力し、生成ボタンを押下します。

createagent

作成を押すと、あなた専用のエージェントが自動で作成されます🤖

アクショントグルを開くと flight が自動で選ばれているはずです。
ここまでできたら、クリスマスにシドニーへ行く便を探してみましょう。

HND から SYD へ 2025-12-24 に出発する便を探して

と入力し送信ボタンを押下します。

previewagent

便が正しく表示されれば正常に動作しています。

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

createagent

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

chatagent

同様に

HND から SYD へ 2025-12-24 に出発する便を探して

と入力し試してみましょう。飛行機が見つけられましたね🎅!

6. Clean Up

この手順では Amazon Quick Suite と AWS CloudFormation のリソースを削除します。

Amazon Quick Suite の削除

Quick Suite の管理ページから Manage ボタンを押下し、削除を進めます。

deleteqs

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

deleteamadeus

まとめ

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