0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

インフラ初心者がAWS Lambda + API Gateway + Cognito で SOAP/REST API を 構築してみる

0
Last updated at Posted at 2026-03-28

はじめに

最近SOAP API、REST APIに触れることがあったので勘所をつかむため実際に作ってみる。
SOAP APIとREST APIを作成し、SOAPの場合はREST変換する部分も併せてLambdaで作成した。
認証はCognito使った。APIをホテルにしてるのは適当。インフラは初心者。

  • REST API(/hotels
  • SOAP API(/soap
  • SOAP↔REST 変換プロキシ(/soap-converter
  • WSDL 公開エンドポイント(/wsdl
  • Cognito による認証

構成概要

API Gateway
       │─ Lambda Authorizer(TOKEN型、TTL=0)
       │
       ├─ GET/POST /hotels
       ├─ GET/POST /soap
       ├─ GET/POST /soap-converter
       │
       └─ GET /wsdl

インフラ構成:

リソース 内容
VPC Public/Private サブネット、NAT Gateway
Lambda × 4 Python 3.12、VPC 内配置
API Gateway 認可は↓のオーソライザーを設定
Cognito User Pool + Resource Server + App Client(client_credentials)

オーソライザー:

アーキテクチャ図:
AWS-ARCHITECTURE.png


Lambda Authorizer

Cognito client_credentials フローで発行されるトークンには aud クレームが含まれない仕様らしい(ChatGPT談)ので、JWT のペイロードを自前でデコードして検証する Lambda Authorizer を採用。
↓ChatGPT談

なぜ aud が無いのか

OAuth設計的に:

client_credentialsは「誰が使うか(client)」が主体
リソースサーバーの識別は
スコープ
トークンの発行元(iss)
で行う前提

つまりCognitoは
👉 「audでAPIを区別するモデルではない」
# 認証ハンドラ
def authorizer_handler(event, context):
    token_string = event.get('authorizationToken', '')
    method_arn = event['methodArn']

    if token_string.startswith('Bearer '):
        token = token_string[7:]
    else:
        return generate_policy('Deny', method_arn)

    parts = token.split('.')
    payload = parts[1]
    # Base64 パディング補正
    padding = 4 - len(payload) % 4
    if padding < 4:
        payload += '=' * padding
    claims = json.loads(base64.urlsafe_b64decode(payload))

    # 有効期限チェック
    if claims.get('exp', 0) < time.time():
        return generate_policy('Deny', method_arn)

    # issuer / client_id / token_use / scope 検証
    if claims.get('iss') != EXPECTED_ISSUER:
        return generate_policy('Deny', method_arn)
    if claims.get('client_id') != EXPECTED_CLIENT_ID:
        return generate_policy('Deny', method_arn)
    if claims.get('token_use') != 'access':
        return generate_policy('Deny', method_arn)

    token_scopes = claims.get('scope', '').split()
    if READ_SCOPE not in token_scopes and WRITE_SCOPE not in token_scopes:
        return generate_policy('Deny', method_arn)

    return generate_policy('Allow', method_arn, claims.get('client_id'))

キャッシュで古いポリシーが残ることを防ぐため TTL は0設定

CognitoAuthorizer:
  Type: AWS::ApiGateway::Authorizer
  Properties:
    Type: TOKEN
    AuthorizerResultTtlInSeconds: 0
    IdentitySource: method.request.header.Authorization

エンドポイント設計

GET・POST /hotels — REST API

Lambdaに配置。普通のREST APIなので割愛。

GET・POST /soap — SOAP API

def soap_handler(event, context):
    http_method = event.get('httpMethod', 'POST')

    if http_method == 'GET':
        # 返すだけ
        soap_response = '''<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
               xmlns:tns="http://hotel.api.example.com/">
    <soap:Body>
        <tns:GetHotelsResponse>
            <hotels>[{"id": "101", "name": "Deluxe Room"}, {"id": "102", "name": "Standard Room"}]</hotels>
        </tns:GetHotelsResponse>
    </soap:Body>
</soap:Envelope>'''
        return {
            'statusCode': 200,
            'body': soap_response,
            'headers': {'Content-Type': 'application/soap+xml'}
        }
        
    elif http_method == 'POST':
        # SOAP リクエストを処理(ETはxml.etree)
        try:
            root = ET.fromstring(body)
            # SOAP Envelope から Body を抽出
            namespaces = {
                'soap': 'http://schemas.xmlsoap.org/soap/envelope/',
                'tns': 'http://hotel.api.example.com/'
            }
            
            body_elem = root.find('.//soap:Body', namespaces)
            if body_elem is None:
                body_elem = root.find('.//Body')
            
            if body_elem is not None:
                # オペレーション判定
                get_hotels = body_elem.find('.//{http://hotel.api.example.com/}GetHotels')
                book_hotel = body_elem.find('.//{http://hotel.api.example.com/}BookHotel')
                
                if get_hotels is not None:
                    soap_response = '''<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:tns="http://hotel.api.example.com/">
    <soap:Body>
        <tns:GetHotelsResponse>
            <hotels>[{"id": "101", "name": "Deluxe Room"}, {"id": "102", "name": "Standard Room"}]</hotels>
        </tns:GetHotelsResponse>
    </soap:Body>
</soap:Envelope>'''
                    return {
                        'statusCode': 200,
                        'body': soap_response,
                        'headers': {'Content-Type': 'application/soap+xml'}
                    }

POST では <tns:GetHotels> / <tns:BookHotel> を XML パースして振り分ける。

GET・POST /soap-converter — SOAP↔REST 変換プロキシ

/soap-converter はREST クライアントが SOAP を意識しなくて済むように HotelSoapFunction を Lambda 間実行 して SOAP(XML) レスポンスを REST(JSON) に変換。例外処理等はここではいろいろ省略している。
今回はシンプルなレスポンスだけど実際はマッピング大変そう。

def converter_handler(event, context):
    http_method = event.get('httpMethod', 'GET')

    if http_method == 'GET':
        # SOAP Lambda を直接実行
        soap_response = lambda_client.invoke(
            FunctionName='HotelSoapFunction',
            InvocationType='RequestResponse',
            Payload=json.dumps({'httpMethod': 'GET', 'body': ''})
        )
    elif http_method == 'POST':
        # REST JSON → SOAP XML に変換してから実行
        try:
            data = json.loads(body) if body else {}
            room_id = data.get('roomId', 'N/A')
            guest_name = data.get('guestName', 'N/A')
        except:
            room_id = 'N/A'
            guest_name = 'N/A'
        soap_request = '<?xml version="1.0" encoding="UTF-8"?>\n<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:tns="http://hotel.api.example.com/">\n    <soap:Body>\n        <tns:BookHotel>\n            <tns:roomId>' + room_id + '</tns:roomId>\n            <tns:guestName>' + guest_name + '</tns:guestName>\n        </tns:BookHotel>\n    </soap:Body>\n</soap:Envelope>'
        soap_response = lambda_client.invoke(
            FunctionName='HotelSoapFunction',
            InvocationType='RequestResponse',
            Payload=json.dumps({'httpMethod': 'POST', 'body': soap_request})
        )

    # SOAP XML → REST JSON 変換(ETはxml.etree)
    response_payload = json.loads(soap_response['Payload'].read().decode('utf-8'))
    soap_body = response_payload.get('body', '')
        
    # SOAP レスポンスを REST JSON に変換
    if not soap_body:
        return {
            'statusCode': 500,
            'body': json.dumps({'error': 'Empty SOAP response'})
        }
        
    try:
        root = ET.fromstring(soap_body)
            
        # Namespace を無視して全要素を探す
        # GetHotelsResponse を探す
        for elem in root.iter():
            if elem.tag.endswith('hotels'):
                if elem.text:
                    hotels_data = json.loads(elem.text)
                    return {
                        'statusCode': 200,
                        'body': json.dumps({
                            'success': True,
                            'hotels': hotels_data
                        })
                    }
            elif elem.tag.endswith('result'):
                if elem.text:
                    return {
                        'statusCode': 200,
                        'body': json.dumps({
                            'success': True,
                            'message': elem.text
                        })
                    }

今回はLambda 間実行しているので API Gateway は経由していないが、実際は ECS とかのAPIを使うだろうからその辺の設定は別途必要そう。

GET /wsdl — WSDL を専用エンドポイントで公開

SOAP データとスキーマ定義を混在させないために専用の /wsdl エンドポイントに分離。
実際はAPIカタログとかで公開するのがよい?SOAPよくわからん。


ハマったポイント

1. CloudFormation デプロイ後も古いレスポンスが返る

原因: CloudFormation は ApiDeployment リソースが変わらないと判断するとステージを更新しない。

やったこと: boto3description を変更して新しいデプロイを作成する。

import boto3
c = boto3.client('apigateway', region_name='region_name')
c.create_deployment(restApiId='XXXXXX', stageName='dev', description='force-deploy')

2. client_credentials トークンで COGNITO_USER_POOLS 認証が通らない

原因: client_credentials フローのトークンに aud クレームがない → API Gateway が拒否。

やったこと: Lambda Authorizerに切り替えて client_id クレームで検証。

3. Lambda Authorizer のキャッシュで Deny が残る

原因: TTL のデフォルトは 300 秒。ポリシー変更後も古い Deny が返り続ける。

やったこと: AuthorizerResultTtlInSeconds: 0 を設定。


テスト結果


------------------------------------------------------------
=== 認証OK (expect 200) ===
------------------------------------------------------------

  > GET https://my.endpoint.amazonaws.com/dev/hotels
  < 200 OK
  < Content-Type: application/json
  < Body: {"success": true, "message": "GET request received", "data": {}}

  > POST https://my.endpoint.amazonaws.com/dev/hotels
  > Content-Type: application/json
  > Body: {"roomId":"101","guestName":"Yamada Taro"}
  < 200 OK
  < Content-Type: application/json
  < Body: {"success": true, "message": "POST request received", "data": {"roomId": "101", "guestName": "Yamada Taro"}}

  > GET https://my.endpoint.amazonaws.com/dev/soap
  < 200 OK
  < Content-Type: application/soap+xml
  < Body: <?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:tns="http://hotel.api.example.com/">
    <soap:Body>
        <tns:GetHotelsResponse>
            <hotels>[{"id": "101", "name": "Deluxe Room"}, {"id": "102", "name": "Standard Room"}]</hotels>
        </tns:GetHotelsResponse>
    </soap:Body>
</soap:Envelope>

  > POST https://my.endpoint.amazonaws.com/dev/soap
  > Content-Type: text/xml
  > Body: <?xml version="1.0"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:tns="http://hotel.api.example.com/"><soap:Body><tns:GetHotels/></soap:Body></soap:Envelope>
  < 200 OK
  < Content-Type: application/soap+xml
  < Body: <?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:tns="http://hotel.api.example.com/">
    <soap:Body>
        <tns:GetHotelsResponse>
            <hotels>[{"id": "101", "name": "Deluxe Room"}, {"id": "102", "name": "Standard Room"}]</hotels>
        </tns:GetHotelsResponse>
    </soap:Body>
</soap:Envelope>

  > GET https://my.endpoint.amazonaws.com/dev/soap-converter
  < 200 OK
  < Content-Type: application/json
  < Body: {"success": true, "hotels": [{"id": "101", "name": "Deluxe Room"}, {"id": "102", "name": "Standard Room"}]}

  > POST https://my.endpoint.amazonaws.com/dev/soap-converter
  > Content-Type: application/json
  > Body: {"roomId":"102","guestName":"Suzuki Hanako"}
  < 200 OK
  < Content-Type: application/json
  < Body: {"success": true, "message": "Booking confirmed for room 102 under guest Suzuki Hanako"}

  > GET https://my.endpoint.amazonaws.com/dev/wsdl
  < 200 OK
  < Content-Type: application/xml
  < Body: <?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
             xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
             xmlns:tns="http://hotel.api.example.com/"
             xmlns:xsd="http://www.w3.org/2001/XMLSchema"
             targetNamespace="http://hotel.api.example.com/">
    <types>
        <xsd:schema targetNamespace="http://hotel.api.example.com/">
            <xsd:element name="GetHotels"><xsd:complexType/></xsd:element>
            <xsd:element name="GetHotelsResponse">
                <xsd:complexType><xsd:sequence>
                    <xsd:element name="hotels" type="xsd:string"/>
                </xsd:sequence></xsd:complexType>
            </xsd:element>
            <xsd:element name="BookHotel">
                <xsd:complexType><xsd:sequence>
                    <xsd:element name="roomId" type="xsd:string"/>
                    <xsd:element name="guestName" type="xsd:string"/>
                </xsd:sequence></xsd:complexType>
            </xsd:element>
            <xsd:element name="BookHotelResponse">
                <xsd:complexType><xsd:sequence>
                    <xsd:element name="result" type="xsd:string"/>
                </xsd:sequence></xsd:complexType>
            </xsd:element>
        </xsd:schema>
    </types>
    <message name="GetHotelsRequest"><part name="parameters" element="tns:GetHotels"/></message>
    <message name="GetHotelsResponse"><part name="parameters" element="tns:GetHotelsResponse"/></message>
    <message name="BookHotelRequest"><part name="parameters" element="tns:BookHotel"/></message>
    <message name="BookHotelResponse"><part name="parameters" element="tns:BookHotelResponse"/></message>
    <portType name="HotelServicePortType">
        <operation name="GetHotels">
            <input message="tns:GetHotelsRequest"/><output message="tns:GetHotelsResponse"/>
        </operation>
        <operation name="BookHotel">
            <input message="tns:BookHotelRequest"/><output message="tns:BookHotelResponse"/>
        </operation>
    </portType>
    <binding name="HotelServiceBinding" type="tns:HotelServicePortType">
        <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
        <operation name="GetHotels">
            <soap:operation soapAction="GetHotels"/>
            <input><soap:body use="literal"/></input>
            <output><soap:body use="literal"/></output>
        </operation>
        <operation name="BookHotel">
            <soap:operation soapAction="BookHotel"/>
            <input><soap:body use="literal"/></input>
            <output><soap:body use="literal"/></output>
        </operation>
    </binding>
    <service name="HotelService">
        <documentation>Hotel SOAP Service</documentation>
        <port name="HotelServicePort" binding="tns:HotelServiceBinding">
            <soap:address location="https://my.endpoint.amazonaws.com/dev/soap"/>
        </port>
    </service>
</definitions>

------------------------------------------------------------
=== 認証NG (expect 401) ===
------------------------------------------------------------

  > GET https://my.endpoint.amazonaws.com/dev/hotels
  < 401 OK
  < Content-Type: application/json
  < Body: {"message": "Unauthorized"}

  > POST https://my.endpoint.amazonaws.com/dev/hotels
  > Content-Type: application/json
  > Body: {"roomId":"101","guestName":"Yamada Taro"}
  < 401 OK
  < Content-Type: application/json
  < Body: {"message": "Unauthorized"}

  > GET https://my.endpoint.amazonaws.com/dev/soap
  < 401 OK
  < Content-Type: application/json
  < Body: {"message": "Unauthorized"}

  > POST https://my.endpoint.amazonaws.com/dev/soap
  > Content-Type: text/xml
  > Body: <?xml version="1.0"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:tns="http://hotel.api.example.com/"><soap:Body><tns:GetHotels/></soap:Body></soap:Envelope>
  < 401 OK
  < Content-Type: application/json
  < Body: {"message": "Unauthorized"}

  > GET https://my.endpoint.amazonaws.com/dev/soap-converter
  < 401 OK
  < Content-Type: application/json
  < Body: {"message": "Unauthorized"}

  > POST https://my.endpoint.amazonaws.com/dev/soap-converter
  > Content-Type: application/json
  > Body: {"roomId":"102","guestName":"Suzuki Hanako"}
  < 401 OK
  < Content-Type: application/json
  < Body: {"message": "Unauthorized"}

  > GET https://my.endpoint.amazonaws.com/dev/wsdl
  < 401 OK
  < Content-Type: application/json
  < Body: {"message": "Unauthorized"}

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?