AWS API Gateway Pattern for iDrv5
This document outlines how to implement an API Gateway pattern using AWS API Gateway to decouple modules and enforce well-defined API contracts between components.
This document outlines how to implement an API Gateway pattern using AWS API Gateway to decouple modules and enforce well-defined API contracts between components.
Table of Contents
Section titled “Table of Contents”- Current Architecture Challenges
- API Gateway Pattern Overview
- AWS API Gateway Setup
- Module API Contracts
- Implementation Strategy
- Security Configuration
- Migration Approach
- Monitoring and Observability
Current Architecture Challenges
Section titled “Current Architecture Challenges”The current iDrv5 architecture has modules that directly interact with each other’s blueprints and templates:
flowchart TB subgraph Flask["Flask Application"] OP[Operations Portal] FM[Freight Management] CRM[CRM Module] AM[Asset Management]
OP <--> FM FM <--> CRM CRM <--> AM OP <--> CRM FM <--> AM OP <--> AM end
DI[/"Direct Imports & Coupling"/]
OP --- DI FM --- DI CRM --- DI AM --- DI
style Flask fill:#ffebee,stroke:#c62828 style DI fill:#ffcdd2,stroke:#c62828Problems:
- Tight coupling between modules
- Circular import risks
- Difficult to scale individual components
- No clear API contracts
- Hard to test modules in isolation
- Template dependencies across modules
API Gateway Pattern Overview
Section titled “API Gateway Pattern Overview”The API Gateway pattern provides a single entry point for all client requests, routing them to appropriate backend services:
flowchart TB subgraph Gateway["AWS API Gateway"] subgraph Routes["/api/v1/*"] RC["/rate-cards"] CU["/customers"] ZO["/zones"] AS["/assets"] end end
RC --> RS[Rates Service] CU --> CS[CRM Service] ZO --> ZS[Zones Service] AS --> ASS[Asset Service]
style Gateway fill:#e3f2fd,stroke:#1565c0 style Routes fill:#bbdefb,stroke:#1565c0 style RS fill:#c8e6c9,stroke:#2e7d32 style CS fill:#c8e6c9,stroke:#2e7d32 style ZS fill:#c8e6c9,stroke:#2e7d32 style ASS fill:#c8e6c9,stroke:#2e7d32Benefits:
- Clear API contracts between services
- Independent scaling of components
- Centralized authentication and authorization
- Rate limiting and throttling
- Request/response transformation
- Caching at the gateway level
- API versioning support
AWS API Gateway Setup
Section titled “AWS API Gateway Setup”Step 1: Create the API Gateway
Section titled “Step 1: Create the API Gateway”Using AWS CLI:
# Create REST APIaws apigateway create-rest-api \ --name "iDrv5-API-Gateway" \ --description "API Gateway for iDrv5 Logistics Platform" \ --endpoint-configuration types=REGIONAL \ --region ap-southeast-2
# Store the API IDAPI_ID=$(aws apigateway get-rest-apis --query "items[?name=='iDrv5-API-Gateway'].id" --output text)Step 2: Define Resource Structure
Section titled “Step 2: Define Resource Structure”# Get root resource IDROOT_ID=$(aws apigateway get-resources --rest-api-id $API_ID --query "items[?path=='/'].id" --output text)
# Create /api resourceaws apigateway create-resource \ --rest-api-id $API_ID \ --parent-id $ROOT_ID \ --path-part "api"
# Create /api/v1 resourceAPI_RESOURCE_ID=$(aws apigateway get-resources --rest-api-id $API_ID --query "items[?path=='/api'].id" --output text)
aws apigateway create-resource \ --rest-api-id $API_ID \ --parent-id $API_RESOURCE_ID \ --path-part "v1"Step 3: Terraform Configuration (Recommended)
Section titled “Step 3: Terraform Configuration (Recommended)”Create infrastructure/api_gateway/main.tf:
terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } }}
provider "aws" { region = "ap-southeast-2"}
# API Gateway REST APIresource "aws_api_gateway_rest_api" "idrv5" { name = "idrv5-api-gateway" description = "API Gateway for iDrv5 Logistics Platform"
endpoint_configuration { types = ["REGIONAL"] }
tags = { Environment = var.environment Application = "iDrv5" }}
# /api resourceresource "aws_api_gateway_resource" "api" { rest_api_id = aws_api_gateway_rest_api.idrv5.id parent_id = aws_api_gateway_rest_api.idrv5.root_resource_id path_part = "api"}
# /api/v1 resourceresource "aws_api_gateway_resource" "v1" { rest_api_id = aws_api_gateway_rest_api.idrv5.id parent_id = aws_api_gateway_resource.api.id path_part = "v1"}
# Rate Cards Resourceresource "aws_api_gateway_resource" "rate_cards" { rest_api_id = aws_api_gateway_rest_api.idrv5.id parent_id = aws_api_gateway_resource.v1.id path_part = "rate-cards"}
# Zones Resourceresource "aws_api_gateway_resource" "zones" { rest_api_id = aws_api_gateway_rest_api.idrv5.id parent_id = aws_api_gateway_resource.v1.id path_part = "zones"}
# Customers Resourceresource "aws_api_gateway_resource" "customers" { rest_api_id = aws_api_gateway_rest_api.idrv5.id parent_id = aws_api_gateway_resource.v1.id path_part = "customers"}
# Users Resourceresource "aws_api_gateway_resource" "users" { rest_api_id = aws_api_gateway_rest_api.idrv5.id parent_id = aws_api_gateway_resource.v1.id path_part = "users"}
# Bookings Resourceresource "aws_api_gateway_resource" "bookings" { rest_api_id = aws_api_gateway_rest_api.idrv5.id parent_id = aws_api_gateway_resource.v1.id path_part = "bookings"}
# Assets Resourceresource "aws_api_gateway_resource" "assets" { rest_api_id = aws_api_gateway_rest_api.idrv5.id parent_id = aws_api_gateway_resource.v1.id path_part = "assets"}Step 4: Integration with Backend Services
Section titled “Step 4: Integration with Backend Services”Create infrastructure/api_gateway/integrations.tf:
# VPC Link for private integration (if using internal ALB)resource "aws_api_gateway_vpc_link" "idrv5" { name = "idrv5-vpc-link" target_arns = [aws_lb.internal.arn]}
# Rate Cards - GET methodresource "aws_api_gateway_method" "rate_cards_get" { rest_api_id = aws_api_gateway_rest_api.idrv5.id resource_id = aws_api_gateway_resource.rate_cards.id http_method = "GET" authorization = "CUSTOM" authorizer_id = aws_api_gateway_authorizer.jwt.id}
resource "aws_api_gateway_integration" "rate_cards_get" { rest_api_id = aws_api_gateway_rest_api.idrv5.id resource_id = aws_api_gateway_resource.rate_cards.id http_method = aws_api_gateway_method.rate_cards_get.http_method integration_http_method = "GET" type = "HTTP_PROXY" uri = "${var.backend_url}/api/rate-cards/" connection_type = "VPC_LINK" connection_id = aws_api_gateway_vpc_link.idrv5.id
request_parameters = { "integration.request.header.X-Forwarded-For" = "context.identity.sourceIp" "integration.request.header.X-Request-Id" = "context.requestId" }}
# Rate Cards - POST methodresource "aws_api_gateway_method" "rate_cards_post" { rest_api_id = aws_api_gateway_rest_api.idrv5.id resource_id = aws_api_gateway_resource.rate_cards.id http_method = "POST" authorization = "CUSTOM" authorizer_id = aws_api_gateway_authorizer.jwt.id}
resource "aws_api_gateway_integration" "rate_cards_post" { rest_api_id = aws_api_gateway_rest_api.idrv5.id resource_id = aws_api_gateway_resource.rate_cards.id http_method = aws_api_gateway_method.rate_cards_post.http_method integration_http_method = "POST" type = "HTTP_PROXY" uri = "${var.backend_url}/api/rate-cards/" connection_type = "VPC_LINK" connection_id = aws_api_gateway_vpc_link.idrv5.id}Module API Contracts
Section titled “Module API Contracts”OpenAPI Specification
Section titled “OpenAPI Specification”Create api/openapi/idrv5-api.yaml:
openapi: 3.0.3info: title: iDrv5 Logistics Platform API description: API Gateway contracts for iDrv5 modules version: 1.0.0 contact: name: iDrv5 Development Team
servers: - url: https://api.idrv5.example.com/api/v1 description: Production API Gateway - url: https://api-staging.idrv5.example.com/api/v1 description: Staging API Gateway - url: http://localhost:5002/api description: Local development
paths: /rate-cards: get: summary: List all rate cards operationId: listRateCards tags: - Rate Management parameters: - name: page in: query schema: type: integer default: 1 - name: per_page in: query schema: type: integer default: 20 - name: is_active in: query schema: type: boolean responses: '200': description: Successful response content: application/json: schema: $ref: '#/components/schemas/RateCardListResponse' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalError'
post: summary: Create a new rate card operationId: createRateCard tags: - Rate Management requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RateCardCreate' responses: '201': description: Rate card created content: application/json: schema: $ref: '#/components/schemas/RateCardResponse' '400': $ref: '#/components/responses/BadRequest' '409': $ref: '#/components/responses/Conflict'
/rate-cards/{id}: get: summary: Get rate card by ID operationId: getRateCard tags: - Rate Management parameters: - name: id in: path required: true schema: type: integer responses: '200': description: Successful response content: application/json: schema: $ref: '#/components/schemas/RateCardResponse' '404': $ref: '#/components/responses/NotFound'
/zones: get: summary: List all zones operationId: listZones tags: - Zone Management parameters: - name: state in: query schema: type: string - name: zone_type in: query schema: type: string enum: [Metro, Regional, Remote] - name: is_active in: query schema: type: boolean responses: '200': description: Successful response content: application/json: schema: $ref: '#/components/schemas/ZoneListResponse'
/customers: get: summary: List customers operationId: listCustomers tags: - Customer Management responses: '200': description: Successful response content: application/json: schema: $ref: '#/components/schemas/CustomerListResponse'
/bookings: get: summary: List bookings operationId: listBookings tags: - Booking Management parameters: - name: status in: query schema: type: string enum: [pending, confirmed, in_transit, delivered, cancelled] - name: customer_id in: query schema: type: integer - name: from_date in: query schema: type: string format: date - name: to_date in: query schema: type: string format: date responses: '200': description: Successful response content: application/json: schema: $ref: '#/components/schemas/BookingListResponse'
post: summary: Create a booking operationId: createBooking tags: - Booking Management requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BookingCreate' responses: '201': description: Booking created content: application/json: schema: $ref: '#/components/schemas/BookingResponse'
/quotes: post: summary: Generate a quote operationId: createQuote tags: - Quoting requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/QuoteRequest' responses: '200': description: Quote generated content: application/json: schema: $ref: '#/components/schemas/QuoteResponse'
components: schemas: RateCardListResponse: type: object properties: success: type: boolean data: type: array items: $ref: '#/components/schemas/RateCard' pagination: $ref: '#/components/schemas/Pagination'
RateCard: type: object properties: id: type: integer name: type: string rate_type: type: string effective_date: type: string format: date expiry_date: type: string format: date is_active: type: boolean rate_calculation_method_id: type: integer description: type: string
RateCardCreate: type: object required: - name - rate_type - rate_calculation_method_id properties: name: type: string maxLength: 255 rate_type: type: string maxLength: 50 rate_calculation_method_id: type: integer effective_date: type: string format: date expiry_date: type: string format: date description: type: string is_active: type: boolean default: true
RateCardResponse: type: object properties: success: type: boolean data: $ref: '#/components/schemas/RateCard'
Zone: type: object properties: id: type: integer name: type: string short_name: type: string zone_type: type: string state: type: string is_metro: type: boolean is_active: type: boolean
ZoneListResponse: type: object properties: success: type: boolean data: type: array items: $ref: '#/components/schemas/Zone'
Customer: type: object properties: id: type: integer name: type: string email: type: string phone: type: string company: type: string is_active: type: boolean
CustomerListResponse: type: object properties: success: type: boolean data: type: array items: $ref: '#/components/schemas/Customer'
Booking: type: object properties: id: type: integer reference: type: string customer_id: type: integer status: type: string pickup_date: type: string format: date delivery_date: type: string format: date origin_zone_id: type: integer destination_zone_id: type: integer
BookingCreate: type: object required: - customer_id - origin_zone_id - destination_zone_id - pickup_date properties: customer_id: type: integer origin_zone_id: type: integer destination_zone_id: type: integer pickup_date: type: string format: date service_level_id: type: integer items: type: array items: $ref: '#/components/schemas/FreightItem'
BookingResponse: type: object properties: success: type: boolean data: $ref: '#/components/schemas/Booking'
BookingListResponse: type: object properties: success: type: boolean data: type: array items: $ref: '#/components/schemas/Booking'
FreightItem: type: object properties: description: type: string quantity: type: integer weight: type: number length: type: number width: type: number height: type: number
QuoteRequest: type: object required: - origin_zone_id - destination_zone_id - items properties: origin_zone_id: type: integer destination_zone_id: type: integer service_level_id: type: integer items: type: array items: $ref: '#/components/schemas/FreightItem' pickup_date: type: string format: date
QuoteResponse: type: object properties: success: type: boolean data: type: object properties: quote_id: type: string total_price: type: number breakdown: type: object transit_time: type: integer valid_until: type: string format: date-time
Pagination: type: object properties: page: type: integer per_page: type: integer total: type: integer pages: type: integer
Error: type: object properties: success: type: boolean example: false error: type: string code: type: string
responses: BadRequest: description: Bad request content: application/json: schema: $ref: '#/components/schemas/Error'
Unauthorized: description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error'
NotFound: description: Resource not found content: application/json: schema: $ref: '#/components/schemas/Error'
Conflict: description: Resource conflict content: application/json: schema: $ref: '#/components/schemas/Error'
InternalError: description: Internal server error content: application/json: schema: $ref: '#/components/schemas/Error'
securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT
apiKey: type: apiKey in: header name: X-API-Key
security: - bearerAuth: []Implementation Strategy
Section titled “Implementation Strategy”Phase 1: API Standardization (Internal)
Section titled “Phase 1: API Standardization (Internal)”Before introducing AWS API Gateway, standardize all internal APIs:
1. Create a base API response format:
Create utils/api_response.py:
from flask import jsonifyfrom functools import wrapsfrom typing import Any, Dict, Optional, List
class APIResponse: """Standardized API response format for all endpoints."""
@staticmethod def success( data: Any = None, message: str = None, status_code: int = 200, pagination: Dict = None ): response = { 'success': True, 'data': data } if message: response['message'] = message if pagination: response['pagination'] = pagination return jsonify(response), status_code
@staticmethod def error( error: str, code: str = None, status_code: int = 400, details: Dict = None ): response = { 'success': False, 'error': error } if code: response['code'] = code if details: response['details'] = details return jsonify(response), status_code
@staticmethod def created(data: Any = None, message: str = 'Resource created'): return APIResponse.success(data, message, 201)
@staticmethod def not_found(resource: str = 'Resource'): return APIResponse.error(f'{resource} not found', 'NOT_FOUND', 404)
@staticmethod def conflict(message: str = 'Resource already exists'): return APIResponse.error(message, 'CONFLICT', 409)
@staticmethod def validation_error(errors: List[str]): return APIResponse.error( 'Validation failed', 'VALIDATION_ERROR', 400, {'validation_errors': errors} )
def api_endpoint(f): """Decorator for consistent API error handling.""" @wraps(f) def decorated(*args, **kwargs): try: return f(*args, **kwargs) except ValueError as e: return APIResponse.error(str(e), 'VALIDATION_ERROR', 400) except PermissionError as e: return APIResponse.error(str(e), 'FORBIDDEN', 403) except LookupError as e: return APIResponse.error(str(e), 'NOT_FOUND', 404) except Exception as e: # Log the error import traceback traceback.print_exc() return APIResponse.error('Internal server error', 'INTERNAL_ERROR', 500) return decorated2. Create API service layer:
Create services/api_client.py:
import requestsfrom typing import Dict, Any, Optionalfrom flask import current_appimport os
class InternalAPIClient: """Client for making internal API calls through defined contracts."""
def __init__(self, base_url: str = None): self.base_url = base_url or os.getenv('API_GATEWAY_URL', 'http://localhost:5002/api') self.timeout = 30
def _request( self, method: str, endpoint: str, data: Dict = None, params: Dict = None, headers: Dict = None ) -> Dict[str, Any]: url = f"{self.base_url}/{endpoint.lstrip('/')}"
default_headers = { 'Content-Type': 'application/json', 'Accept': 'application/json' } if headers: default_headers.update(headers)
response = requests.request( method=method, url=url, json=data, params=params, headers=default_headers, timeout=self.timeout )
return response.json()
def get(self, endpoint: str, params: Dict = None) -> Dict: return self._request('GET', endpoint, params=params)
def post(self, endpoint: str, data: Dict = None) -> Dict: return self._request('POST', endpoint, data=data)
def put(self, endpoint: str, data: Dict = None) -> Dict: return self._request('PUT', endpoint, data=data)
def delete(self, endpoint: str) -> Dict: return self._request('DELETE', endpoint)
class RateCardsAPI(InternalAPIClient): """API client for Rate Cards service."""
def list_rate_cards(self, page: int = 1, per_page: int = 20, is_active: bool = None): params = {'page': page, 'per_page': per_page} if is_active is not None: params['is_active'] = is_active return self.get('/rate-cards/', params)
def get_rate_card(self, rate_card_id: int): return self.get(f'/rate-cards/{rate_card_id}')
def create_rate_card(self, data: Dict): return self.post('/rate-cards/', data)
def update_rate_card(self, rate_card_id: int, data: Dict): return self.put(f'/rate-cards/{rate_card_id}', data)
def delete_rate_card(self, rate_card_id: int): return self.delete(f'/rate-cards/{rate_card_id}')
def get_rate_entries(self, rate_card_id: int): return self.get(f'/rate-cards/{rate_card_id}/entries')
class ZonesAPI(InternalAPIClient): """API client for Zones service."""
def list_zones(self, state: str = None, zone_type: str = None): params = {} if state: params['state'] = state if zone_type: params['zone_type'] = zone_type return self.get('/zones/', params)
def get_zone(self, zone_id: int): return self.get(f'/zones/{zone_id}')
def get_zone_suburbs(self, zone_id: int): return self.get(f'/zones/{zone_id}/suburbs')
class CustomersAPI(InternalAPIClient): """API client for Customers service."""
def list_customers(self, search: str = None, is_active: bool = None): params = {} if search: params['search'] = search if is_active is not None: params['is_active'] = is_active return self.get('/customers/', params)
def get_customer(self, customer_id: int): return self.get(f'/customers/{customer_id}')
def get_customer_rate_cards(self, customer_id: int): return self.get(f'/customers/{customer_id}/rate-cards')
class QuotingAPI(InternalAPIClient): """API client for Quoting service."""
def generate_quote(self, data: Dict): return self.post('/quotes/', data)
def get_quote(self, quote_id: str): return self.get(f'/quotes/{quote_id}')Phase 2: Service Decoupling
Section titled “Phase 2: Service Decoupling”Refactor modules to use API clients instead of direct imports:
Before (tightly coupled):
# In portals/operations/routes.pyfrom modules.freight_management.routes import get_rate_cardsfrom models.rate_cards import RateCard
@operations_bp.route('/booking/new')def new_booking(): # Direct database access rate_cards = RateCard.query.filter_by(is_active=True).all() # Direct function import zones = get_zones_for_booking() return render_template('booking.html', rate_cards=rate_cards, zones=zones)After (decoupled via API):
# In portals/operations/routes.pyfrom services.api_client import RateCardsAPI, ZonesAPI
@operations_bp.route('/booking/new')def new_booking(): rate_cards_api = RateCardsAPI() zones_api = ZonesAPI()
# API calls instead of direct database access rate_cards_response = rate_cards_api.list_rate_cards(is_active=True) zones_response = zones_api.list_zones()
return render_template( 'booking.html', rate_cards=rate_cards_response.get('data', []), zones=zones_response.get('data', []) )Phase 3: AWS API Gateway Integration
Section titled “Phase 3: AWS API Gateway Integration”1. Configure API Gateway with OpenAPI import:
aws apigateway import-rest-api \ --body 'file://api/openapi/idrv5-api.yaml' \ --region ap-southeast-22. Set up Lambda authorizer for JWT validation:
Create infrastructure/lambda/authorizer.py:
import jwtimport osimport json
def handler(event, context): """JWT Token Authorizer for API Gateway.""" token = event.get('authorizationToken', '').replace('Bearer ', '') method_arn = event['methodArn']
try: # Decode and verify JWT payload = jwt.decode( token, os.environ['JWT_SECRET'], algorithms=['HS256'] )
# Generate allow policy return generate_policy( payload['sub'], 'Allow', method_arn, context={ 'user_id': payload.get('user_id'), 'role': payload.get('role'), 'tenant_id': payload.get('tenant_id') } )
except jwt.ExpiredSignatureError: raise Exception('Unauthorized: Token expired') except jwt.InvalidTokenError: raise Exception('Unauthorized: Invalid token')
def generate_policy(principal_id, effect, resource, context=None): policy = { 'principalId': principal_id, 'policyDocument': { 'Version': '2012-10-17', 'Statement': [{ 'Action': 'execute-api:Invoke', 'Effect': effect, 'Resource': resource }] } }
if context: policy['context'] = context
return policy3. Update environment configuration:
Add to .env.production:
# AWS API GatewayAPI_GATEWAY_URL=https://api.idrv5.example.com/api/v1API_GATEWAY_REGION=ap-southeast-2API_GATEWAY_STAGE=prod
# JWT ConfigurationJWT_SECRET=your-secret-keyJWT_ALGORITHM=HS256JWT_EXPIRY_HOURS=24Security Configuration
Section titled “Security Configuration”Authentication Flow
Section titled “Authentication Flow”sequenceDiagram participant C as Client participant AG as API Gateway participant A as Authorizer participant B as Backend
C->>AG: 1. Request + JWT Token AG->>A: 2. Validate Token A-->>AG: 3. Auth Context AG->>B: 4. Forward Request + Auth Context B-->>AG: 5. Response AG-->>C: 6. ResponseAPI Key Management
Section titled “API Key Management”Create infrastructure/api_gateway/api_keys.tf:
# API Usage Planresource "aws_api_gateway_usage_plan" "standard" { name = "idrv5-standard" description = "Standard usage plan for iDrv5 API"
api_stages { api_id = aws_api_gateway_rest_api.idrv5.id stage = aws_api_gateway_stage.prod.stage_name }
quota_settings { limit = 10000 period = "DAY" }
throttle_settings { burst_limit = 100 rate_limit = 50 }}
resource "aws_api_gateway_usage_plan" "premium" { name = "idrv5-premium" description = "Premium usage plan for iDrv5 API"
api_stages { api_id = aws_api_gateway_rest_api.idrv5.id stage = aws_api_gateway_stage.prod.stage_name }
quota_settings { limit = 100000 period = "DAY" }
throttle_settings { burst_limit = 500 rate_limit = 200 }}
# API Keys for external integrationsresource "aws_api_gateway_api_key" "partner_api" { name = "partner-integration-key" description = "API key for partner integrations" enabled = true}
resource "aws_api_gateway_usage_plan_key" "partner" { key_id = aws_api_gateway_api_key.partner_api.id key_type = "API_KEY" usage_plan_id = aws_api_gateway_usage_plan.standard.id}CORS Configuration
Section titled “CORS Configuration”Add to infrastructure/api_gateway/cors.tf:
# CORS configuration for all resourcesresource "aws_api_gateway_method" "options" { for_each = toset([ aws_api_gateway_resource.rate_cards.id, aws_api_gateway_resource.zones.id, aws_api_gateway_resource.customers.id, aws_api_gateway_resource.bookings.id ])
rest_api_id = aws_api_gateway_rest_api.idrv5.id resource_id = each.value http_method = "OPTIONS" authorization = "NONE"}
resource "aws_api_gateway_integration" "options" { for_each = aws_api_gateway_method.options
rest_api_id = aws_api_gateway_rest_api.idrv5.id resource_id = each.value http_method = aws_api_gateway_method.options[each.key].http_method type = "MOCK"
request_templates = { "application/json" = jsonencode({ statusCode = 200 }) }}
resource "aws_api_gateway_method_response" "options_200" { for_each = aws_api_gateway_method.options
rest_api_id = aws_api_gateway_rest_api.idrv5.id resource_id = each.value http_method = aws_api_gateway_method.options[each.key].http_method status_code = "200"
response_parameters = { "method.response.header.Access-Control-Allow-Headers" = true "method.response.header.Access-Control-Allow-Methods" = true "method.response.header.Access-Control-Allow-Origin" = true }}
resource "aws_api_gateway_integration_response" "options" { for_each = aws_api_gateway_method.options
rest_api_id = aws_api_gateway_rest_api.idrv5.id resource_id = each.value http_method = aws_api_gateway_method.options[each.key].http_method status_code = aws_api_gateway_method_response.options_200[each.key].status_code
response_parameters = { "method.response.header.Access-Control-Allow-Headers" = "'Content-Type,Authorization,X-Api-Key'" "method.response.header.Access-Control-Allow-Methods" = "'GET,POST,PUT,DELETE,OPTIONS'" "method.response.header.Access-Control-Allow-Origin" = "'*'" }}Migration Approach
Section titled “Migration Approach”Step-by-Step Migration Plan
Section titled “Step-by-Step Migration Plan”flowchart TB subgraph P1["Phase 1: Preparation"] direction LR P1A[Standardize API response formats] --> P1B[Create OpenAPI specifications] P1B --> P1C[Implement API service layer] P1C --> P1D[Add API versioning support] end
subgraph P2["Phase 2: Internal Decoupling"] direction LR P2A[Refactor module imports] --> P2B[Remove template dependencies] P2B --> P2C[Event-driven communication] P2C --> P2D[Add integration tests] end
subgraph P3["Phase 3: AWS Infrastructure"] direction LR P3A[Set up API Gateway] --> P3B[Configure Lambda authorizer] P3B --> P3C[Set up VPC Link] P3C --> P3D[Configure monitoring] end
subgraph P4["Phase 4: Traffic Migration"] direction LR P4A[Deploy with feature flags] --> P4B[Route 10% traffic] P4B --> P4C[Monitor performance] P4C --> P4D[Increase to 100%] end
subgraph P5["Phase 5: Optimization"] direction LR P5A[Enable API caching] --> P5B[Tune rate limiting] P5B --> P5C[Add request validation] P5C --> P5D[Response transformation] end
P1 ==> P2 ==> P3 ==> P4 ==> P5
style P1 fill:#e8f5e9,stroke:#2e7d32 style P2 fill:#e3f2fd,stroke:#1565c0 style P3 fill:#fff3e0,stroke:#ef6c00 style P4 fill:#fce4ec,stroke:#c2185b style P5 fill:#f3e5f5,stroke:#7b1fa2Feature Flags for Gradual Migration
Section titled “Feature Flags for Gradual Migration”Create utils/feature_flags.py:
import osfrom functools import wraps
class FeatureFlags: """Feature flags for gradual API Gateway migration."""
@staticmethod def use_api_gateway(): return os.getenv('USE_API_GATEWAY', 'false').lower() == 'true'
@staticmethod def api_gateway_percentage(): return int(os.getenv('API_GATEWAY_PERCENTAGE', '0'))
def with_api_fallback(api_call, direct_call): """ Execute API call with fallback to direct call. Used during migration period. """ import random
if not FeatureFlags.use_api_gateway(): return direct_call()
if random.randint(1, 100) > FeatureFlags.api_gateway_percentage(): return direct_call()
try: return api_call() except Exception as e: # Log the error and fallback print(f"API Gateway call failed: {e}, falling back to direct call") return direct_call()Monitoring and Observability
Section titled “Monitoring and Observability”CloudWatch Metrics and Alarms
Section titled “CloudWatch Metrics and Alarms”Create infrastructure/api_gateway/monitoring.tf:
# CloudWatch Dashboardresource "aws_cloudwatch_dashboard" "api_gateway" { dashboard_name = "iDrv5-API-Gateway"
dashboard_body = jsonencode({ widgets = [ { type = "metric" x = 0 y = 0 width = 12 height = 6 properties = { metrics = [ ["AWS/ApiGateway", "Count", "ApiName", aws_api_gateway_rest_api.idrv5.name], [".", "4XXError", ".", "."], [".", "5XXError", ".", "."] ] period = 300 region = var.aws_region title = "API Requests and Errors" } }, { type = "metric" x = 12 y = 0 width = 12 height = 6 properties = { metrics = [ ["AWS/ApiGateway", "Latency", "ApiName", aws_api_gateway_rest_api.idrv5.name, { stat = "Average" }], ["...", { stat = "p99" }] ] period = 300 region = var.aws_region title = "API Latency" } } ] })}
# Alarm for high error rateresource "aws_cloudwatch_metric_alarm" "api_5xx_errors" { alarm_name = "idrv5-api-5xx-errors" comparison_operator = "GreaterThanThreshold" evaluation_periods = 2 metric_name = "5XXError" namespace = "AWS/ApiGateway" period = 300 statistic = "Sum" threshold = 10 alarm_description = "High 5XX error rate on API Gateway"
dimensions = { ApiName = aws_api_gateway_rest_api.idrv5.name }
alarm_actions = [aws_sns_topic.alerts.arn]}
# Alarm for high latencyresource "aws_cloudwatch_metric_alarm" "api_high_latency" { alarm_name = "idrv5-api-high-latency" comparison_operator = "GreaterThanThreshold" evaluation_periods = 3 metric_name = "Latency" namespace = "AWS/ApiGateway" period = 300 extended_statistic = "p99" threshold = 5000 # 5 seconds alarm_description = "High API latency detected"
dimensions = { ApiName = aws_api_gateway_rest_api.idrv5.name }
alarm_actions = [aws_sns_topic.alerts.arn]}Request Logging
Section titled “Request Logging”# Enable access loggingresource "aws_api_gateway_stage" "prod" { deployment_id = aws_api_gateway_deployment.prod.id rest_api_id = aws_api_gateway_rest_api.idrv5.id stage_name = "prod"
access_log_settings { destination_arn = aws_cloudwatch_log_group.api_gateway.arn format = jsonencode({ requestId = "$context.requestId" ip = "$context.identity.sourceIp" caller = "$context.identity.caller" user = "$context.identity.user" requestTime = "$context.requestTime" httpMethod = "$context.httpMethod" resourcePath = "$context.resourcePath" status = "$context.status" protocol = "$context.protocol" responseLength = "$context.responseLength" latency = "$context.responseLatency" }) }
xray_tracing_enabled = true}
resource "aws_cloudwatch_log_group" "api_gateway" { name = "/aws/api-gateway/idrv5" retention_in_days = 30}Quick Reference
Section titled “Quick Reference”API Endpoints Summary
Section titled “API Endpoints Summary”| Service | Endpoint | Methods |
|---|---|---|
| Rate Cards | /api/v1/rate-cards | GET, POST |
| Rate Cards | /api/v1/rate-cards/{id} | GET, PUT, DELETE |
| Zones | /api/v1/zones | GET, POST |
| Zones | /api/v1/zones/{id} | GET, PUT, DELETE |
| Customers | /api/v1/customers | GET, POST |
| Customers | /api/v1/customers/{id} | GET, PUT, DELETE |
| Bookings | /api/v1/bookings | GET, POST |
| Bookings | /api/v1/bookings/{id} | GET, PUT, DELETE |
| Quotes | /api/v1/quotes | POST |
| Users | /api/v1/users | GET, POST |
| Assets | /api/v1/assets | GET, POST |
Common Commands
Section titled “Common Commands”# Deploy API Gateway changesterraform apply -target=module.api_gateway
# View API Gateway logsaws logs tail /aws/api-gateway/idrv5 --follow
# Test API endpointcurl -H "Authorization: Bearer $TOKEN" \ https://api.idrv5.example.com/api/v1/rate-cards
# Generate OpenAPI clientopenapi-generator generate -i api/openapi/idrv5-api.yaml -g python -o sdk/python
# Invalidate API Gateway cacheaws apigateway flush-stage-cache \ --rest-api-id $API_ID \ --stage-name prodEnvironment Variables
Section titled “Environment Variables”| Variable | Description | Example |
|---|---|---|
API_GATEWAY_URL | Gateway base URL | https://api.idrv5.example.com/api/v1 |
USE_API_GATEWAY | Enable gateway routing | true |
API_GATEWAY_PERCENTAGE | Traffic percentage | 100 |
JWT_SECRET | JWT signing secret | your-secret-key |
API_GATEWAY_REGION | AWS region | ap-southeast-2 |