Coverage for gco / stacks / regional_api_gateway_stack.py: 100.00%

98 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-10-28 16:19 +0000

1""" 

2Regional API Gateway bridge for authenticated access to private regional ALBs. 

3 

4Every deployment creates this regional bridge so the centralized aggregator has 

5a reachable, IAM-authenticated path into each regional VPC. In the commercial 

6``aws`` partition, ``api_gateway.regional_api_enabled`` optionally admits other 

7same-account principals. In every other AWS partition, Global Accelerator is 

8omitted and this regional IAM path is enabled for same-account callers 

9regardless of that setting. 

10 

11Architecture: 

12 Aggregator → Regional API Gateway → buffered VPC Lambda → Internal ALB → EKS pods 

13 User (optional in ``aws``; required elsewhere) ────────┤ 

14 └→ streaming VPC Lambda → inference proxy 

15 

16Security: 

17 - API Gateway uses AWS-managed TLS and IAM authentication (SigV4) 

18 - The resource policy always admits only the aggregator role by default 

19 - Optional direct mode additionally admits IAM-authorized account principals 

20 - Lambda runs inside the VPC with access to the internal ALB 

21 - Lambda verifies the deployment-local ALB certificate with explicit SNI 

22 - Lambda adds a short-lived per-request HMAC envelope to the ALB request 

23 - No public exposure of the ALB or EKS API 

24 

25Configuration: 

26 In the commercial ``aws`` partition, set 

27 ``api_gateway.regional_api_enabled`` to ``true`` when callers need direct 

28 region-pinned access. Outside that partition, the regional API is the 

29 supported workload ingress and same-account access is forced on. Global 

30 aggregation always uses its dedicated role in every partition. 

31""" 

32 

33from typing import Any 

34 

35from aws_cdk import ( 

36 CfnOutput, 

37 Duration, 

38 RemovalPolicy, 

39 Stack, 

40) 

41from aws_cdk import aws_apigateway as apigateway 

42from aws_cdk import aws_ec2 as ec2 

43from aws_cdk import aws_iam as iam 

44from aws_cdk import aws_lambda as lambda_ 

45from aws_cdk import aws_logs as logs 

46from constructs import Construct 

47 

48from gco.config.config_loader import ConfigLoader 

49from gco.stacks.constants import ( 

50 AGGREGATOR_REGIONAL_API_ROUTES, 

51 DEFAULT_MAX_REQUEST_BODY_BYTES, 

52 LAMBDA_NODEJS_RUNTIME, 

53 LAMBDA_PYTHON_RUNTIME, 

54 api_gateway_auth_secret_name, 

55 backend_tls_root_ca_parameter_name, 

56 backend_tls_server_name, 

57 validated_request_body_limit, 

58) 

59from gco.stacks.lambda_assets import lambda_source_code 

60 

61# <pyflowchart-code-diagram> BEGIN - auto-inserted, do not edit 

62# Generated at (UTC): 2026-10-16T16:57:50Z 

63# Generated from Git commit: 3458f76ef1541be718c00458b8ecc8f7c770c6ad 

64# Flowchart(s) generated from this file: 

65# * ``GCORegionalApiGatewayStack.__init__`` -> ``diagrams/code_diagrams/gco/stacks/regional_api_gateway_stack.GCORegionalApiGatewayStack___init__.html`` 

66# (PNG: ``diagrams/code_diagrams/gco/stacks/regional_api_gateway_stack.GCORegionalApiGatewayStack___init__.png``) 

67# Regenerate with ``SOURCE_DATE_EPOCH=<unix-seconds> GCO_DIAGRAM_SOURCE_COMMIT=<40-char-sha> python diagrams/generate.py --code-only``. 

68# <pyflowchart-code-diagram> END 

69 

70 

71class GCORegionalApiGatewayStack(Stack): 

72 """Regional aggregation bridge with optional direct caller access. 

73 

74 The VPC Lambda gives the global aggregator a reachable path to one internal 

75 regional ALB. Direct region-pinned access for other IAM-authorized account 

76 principals is optional in the commercial ``aws`` partition and mandatory 

77 in partitions where Global Accelerator is unavailable. 

78 

79 Attributes: 

80 api: Regional REST API with IAM authentication. 

81 proxy_lambda: Buffered VPC Lambda for ``/api/v1/*`` requests. 

82 inference_proxy_lambda: Response-streaming VPC Lambda for ``/inference/*``. 

83 """ 

84 

85 def __init__( 

86 self, 

87 scope: Construct, 

88 construct_id: str, 

89 config: ConfigLoader, 

90 region: str, 

91 vpc: ec2.IVpc, 

92 auth_secret_arn: str, 

93 aggregator_role_arn: str, 

94 alb_dns_name: str | None = None, 

95 **kwargs: Any, 

96 ) -> None: 

97 super().__init__(scope, construct_id, **kwargs) 

98 

99 self.config = config 

100 self.deployment_region = region 

101 self.vpc = vpc 

102 self.alb_dns_name = alb_dns_name 

103 supports_global_accelerator = getattr(config, "supports_global_accelerator", None) 

104 self.global_accelerator_enabled = ( 

105 bool(supports_global_accelerator()) if callable(supports_global_accelerator) else True 

106 ) 

107 self.auth_secret_arn = auth_secret_arn 

108 self.aggregator_role_arn = aggregator_role_arn 

109 

110 # Keep control-plane calls on the established buffered Python proxy and 

111 # give inference a separate Node.js response-streaming runtime. 

112 self.proxy_lambda = self._create_vpc_proxy_lambda() 

113 self.inference_proxy_lambda = self._create_inference_proxy_lambda() 

114 

115 # Create regional API Gateway 

116 self.api = self._create_api_gateway() 

117 

118 # Export outputs 

119 self._create_outputs() 

120 

121 # Apply cdk-nag suppressions 

122 self._apply_nag_suppressions() 

123 

124 def _apply_nag_suppressions(self) -> None: 

125 """Apply cdk-nag suppressions for this stack.""" 

126 from gco.stacks.nag_suppressions import apply_all_suppressions 

127 

128 apply_all_suppressions( 

129 self, 

130 stack_type="regional_api_gateway", 

131 global_region=self.config.get_global_region(), 

132 project_name=self.config.get_project_name(), 

133 ) 

134 

135 def _auth_secret_read_resource(self) -> str: 

136 """IAM resource for reading the backend HMAC auth secret. 

137 

138 Built from the secret's name, the API Gateway Region, and this 

139 account, never from ``self.auth_secret_arn``. That argument is a 

140 cross-stack reference: an ARN built from the secret's name, without the 

141 random suffix, when this stack is in another Region from the API 

142 Gateway stack, but a native export 

143 (``<project>-api-gateway:ExportsOutputRef...``) when it shares that 

144 Region. A grant built on the export failed ``cdk synth`` there (PR #461; 

145 issue #125 was the same failure on the regional stack's roles). 

146 

147 The Region must be the API Gateway's, not the global one: the API 

148 Gateway stack creates the secret, and ``deployment_regions`` lets the 

149 two differ, in which case a grant in the global Region denies every 

150 read. ``-??????`` matches the six random characters Secrets Manager 

151 appends to the name in the ARN, and no other secret. IAM evaluates the 

152 request against that full ARN even though, outside the API Gateway 

153 Region, the Lambdas name the secret by the suffix-less ARN they get in 

154 ``SECRET_ARN``. 

155 """ 

156 return ( 

157 f"arn:{self.partition}:secretsmanager:{self.config.get_api_gateway_region()}:" 

158 f"{self.account}:secret:" 

159 f"{api_gateway_auth_secret_name(self.config.get_project_name())}-??????" 

160 ) 

161 

162 def _create_vpc_proxy_lambda(self) -> lambda_.Function: 

163 """Create VPC Lambda that proxies requests to internal ALB.""" 

164 project_name = self.config.get_project_name() 

165 backend_tls_config = self.config.get_backend_tls_config() 

166 root_ca_parameter_name = backend_tls_root_ca_parameter_name(project_name) 

167 

168 # Create security group for Lambda 

169 lambda_sg = ec2.SecurityGroup( 

170 self, 

171 "ProxyLambdaSg", 

172 vpc=self.vpc, 

173 description="Security group for regional API proxy Lambdas", 

174 allow_all_outbound=True, 

175 ) 

176 self._proxy_lambda_security_group = lambda_sg 

177 

178 # Create IAM role for Lambda 

179 # role_name intentionally omitted - let CDK generate unique name 

180 lambda_role = iam.Role( 

181 self, 

182 "ProxyLambdaRole", 

183 assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"), 

184 managed_policies=[ 

185 iam.ManagedPolicy.from_aws_managed_policy_name( 

186 "service-role/AWSLambdaVPCAccessExecutionRole" 

187 ) 

188 ], 

189 ) 

190 

191 # Grant read access to the auth secret, scoped to its deterministic ARN 

192 # in the API Gateway Region (see ``_auth_secret_read_resource``). 

193 lambda_role.add_to_policy( 

194 iam.PolicyStatement( 

195 effect=iam.Effect.ALLOW, 

196 actions=[ 

197 "secretsmanager:GetSecretValue", 

198 "secretsmanager:DescribeSecret", 

199 ], 

200 resources=[self._auth_secret_read_resource()], 

201 ) 

202 ) 

203 

204 # The Ingress-created ALB does not exist during CDK synthesis. Resolve 

205 # its current hostname from the project-scoped SSM registry at request 

206 # time, then verify that the hostname belongs to this account, region, 

207 # EKS cluster, and platform Ingress before forwarding any request. 

208 registry_region = self.config.get_global_region() 

209 registry_parameter_arn = ( 

210 f"arn:{self.partition}:ssm:{registry_region}:{self.account}:" 

211 f"parameter/{project_name}/alb-hostname-{self.deployment_region}" 

212 ) 

213 root_ca_parameter_arn = ( 

214 f"arn:{self.partition}:ssm:{registry_region}:{self.account}:" 

215 f"parameter/{root_ca_parameter_name.lstrip('/')}" 

216 ) 

217 lambda_role.add_to_policy( 

218 iam.PolicyStatement( 

219 effect=iam.Effect.ALLOW, 

220 actions=["ssm:GetParameter"], 

221 resources=[registry_parameter_arn, root_ca_parameter_arn], 

222 ) 

223 ) 

224 lambda_role.add_to_policy( 

225 iam.PolicyStatement( 

226 effect=iam.Effect.ALLOW, 

227 actions=[ 

228 "elasticloadbalancing:DescribeLoadBalancers", 

229 "elasticloadbalancing:DescribeTags", 

230 ], 

231 resources=["*"], 

232 ) 

233 ) 

234 

235 from gco.stacks.nag_suppressions import acknowledge_nag_findings 

236 

237 acknowledge_nag_findings( 

238 lambda_role, 

239 [ 

240 { 

241 "id": "AwsSolutions-IAM5", 

242 "reason": ( 

243 "ELB DescribeLoadBalancers and DescribeTags do not support " 

244 "resource-level scoping. They are read-only and are used only " 

245 "to verify that the SSM-registered hostname belongs to this " 

246 "account's exact regional GCO cluster and platform Ingress." 

247 ), 

248 "appliesTo": ["Resource::*"], 

249 } 

250 ], 

251 ) 

252 

253 # Create log group 

254 # log_group_name intentionally omitted - let CDK generate unique name 

255 log_group = logs.LogGroup( 

256 self, 

257 "ProxyLambdaLogGroup", 

258 retention=logs.RetentionDays.ONE_WEEK, 

259 removal_policy=RemovalPolicy.DESTROY, 

260 ) 

261 

262 # A literal endpoint remains available for isolated stack synthesis and 

263 # compatibility callers. Production app wiring omits it so replacements 

264 # are discovered from SSM without requiring an ALB at deploy time. 

265 environment = { 

266 "SECRET_ARN": self.auth_secret_arn, 

267 "REGISTRY_REGION": registry_region, 

268 "TARGET_REGION": self.deployment_region, 

269 "PROJECT_NAME": project_name, 

270 "AWS_ACCOUNT_ID": self.account, 

271 "AWS_URL_SUFFIX": self.url_suffix, 

272 "BACKEND_TLS_SERVER_NAME": backend_tls_server_name(project_name), 

273 "BACKEND_TLS_ROOT_CA_PARAMETER": root_ca_parameter_name, 

274 "BACKEND_TLS_ROOT_CA_REGION": registry_region, 

275 "BACKEND_TLS_CA_CACHE_TTL_SECONDS": str(backend_tls_config["trust_cache_ttl_seconds"]), 

276 "BACKEND_TLS_CA_MAX_STALE_SECONDS": str( 

277 backend_tls_config["trust_cache_max_stale_seconds"] 

278 ), 

279 } 

280 if self.alb_dns_name: 

281 environment["ALB_ENDPOINT"] = self.alb_dns_name 

282 

283 # Create Lambda function in VPC 

284 proxy_lambda = lambda_.Function( 

285 self, 

286 "RegionalProxyFunction", 

287 function_name=f"{project_name}-regional-proxy-{self.deployment_region}", 

288 runtime=getattr(lambda_.Runtime, LAMBDA_PYTHON_RUNTIME), 

289 handler="handler.lambda_handler", 

290 code=lambda_source_code("lambda/regional-api-proxy"), 

291 timeout=Duration.seconds(29), 

292 memory_size=256, 

293 role=lambda_role, 

294 vpc=self.vpc, 

295 vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS), 

296 security_groups=[lambda_sg], 

297 environment=environment, 

298 log_group=log_group, 

299 description=f"Regional API proxy for {self.deployment_region} (VPC Lambda)", 

300 tracing=lambda_.Tracing.ACTIVE, 

301 ) 

302 

303 return proxy_lambda 

304 

305 def _create_inference_proxy_lambda(self) -> lambda_.Function: 

306 """Create the VPC Lambda that streams inference responses from the ALB.""" 

307 project_name = self.config.get_project_name() 

308 backend_tls_config = self.config.get_backend_tls_config() 

309 max_request_body_bytes = validated_request_body_limit( 

310 self.config.get_manifest_processor_config().get( 

311 "max_request_body_bytes", DEFAULT_MAX_REQUEST_BODY_BYTES 

312 ) 

313 ) 

314 registry_region = self.config.get_global_region() 

315 root_ca_parameter_name = backend_tls_root_ca_parameter_name(project_name) 

316 registry_parameter_arn = ( 

317 f"arn:{self.partition}:ssm:{registry_region}:{self.account}:" 

318 f"parameter/{project_name}/alb-hostname-{self.deployment_region}" 

319 ) 

320 root_ca_parameter_arn = ( 

321 f"arn:{self.partition}:ssm:{registry_region}:{self.account}:" 

322 f"parameter/{root_ca_parameter_name.lstrip('/')}" 

323 ) 

324 

325 role = iam.Role( 

326 self, 

327 "InferenceStreamingProxyRole", 

328 assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"), 

329 managed_policies=[ 

330 iam.ManagedPolicy.from_aws_managed_policy_name( 

331 "service-role/AWSLambdaVPCAccessExecutionRole" 

332 ) 

333 ], 

334 ) 

335 # The HMAC secret lives in the API Gateway Region, not the global one 

336 # ``registry_region`` names (see ``_auth_secret_read_resource``). 

337 role.add_to_policy( 

338 iam.PolicyStatement( 

339 effect=iam.Effect.ALLOW, 

340 actions=[ 

341 "secretsmanager:GetSecretValue", 

342 "secretsmanager:DescribeSecret", 

343 ], 

344 resources=[self._auth_secret_read_resource()], 

345 ) 

346 ) 

347 role.add_to_policy( 

348 iam.PolicyStatement( 

349 effect=iam.Effect.ALLOW, 

350 actions=["ssm:GetParameter"], 

351 resources=[registry_parameter_arn, root_ca_parameter_arn], 

352 ) 

353 ) 

354 role.add_to_policy( 

355 iam.PolicyStatement( 

356 effect=iam.Effect.ALLOW, 

357 actions=[ 

358 "elasticloadbalancing:DescribeLoadBalancers", 

359 "elasticloadbalancing:DescribeTags", 

360 ], 

361 resources=["*"], 

362 ) 

363 ) 

364 

365 from gco.stacks.nag_suppressions import acknowledge_nag_findings 

366 

367 acknowledge_nag_findings( 

368 role, 

369 [ 

370 { 

371 "id": "AwsSolutions-IAM5", 

372 "reason": ( 

373 "ELB ownership verification and the Lambda VPC/X-Ray APIs do not " 

374 "support resource-level scoping. Secret and SSM reads remain " 

375 "scoped to this deployment's exact resources." 

376 ), 

377 "appliesTo": ["Resource::*"], 

378 } 

379 ], 

380 ) 

381 

382 log_group = logs.LogGroup( 

383 self, 

384 "InferenceStreamingProxyLogGroup", 

385 retention=logs.RetentionDays.ONE_WEEK, 

386 removal_policy=RemovalPolicy.DESTROY, 

387 ) 

388 return lambda_.Function( 

389 self, 

390 "InferenceStreamingProxyFunction", 

391 function_name=(f"{project_name}-regional-inference-proxy-{self.deployment_region}"), 

392 runtime=getattr(lambda_.Runtime, LAMBDA_NODEJS_RUNTIME), 

393 handler="index.handler", 

394 code=lambda_.Code.from_asset("lambda/inference-streaming-proxy-build"), 

395 timeout=Duration.minutes(15), 

396 memory_size=256, 

397 role=role, 

398 vpc=self.vpc, 

399 vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS), 

400 security_groups=[self._proxy_lambda_security_group], 

401 environment={ 

402 "ROUTING_MODE": "regional", 

403 "MAX_REQUEST_BODY_BYTES": str(max_request_body_bytes), 

404 "SECRET_ARN": self.auth_secret_arn, 

405 "REGISTRY_REGION": registry_region, 

406 "TARGET_REGION": self.deployment_region, 

407 "PROJECT_NAME": project_name, 

408 "AWS_ACCOUNT_ID": self.account, 

409 "AWS_URL_SUFFIX": self.url_suffix, 

410 "BACKEND_TLS_SERVER_NAME": backend_tls_server_name(project_name), 

411 "BACKEND_TLS_ROOT_CA_PARAMETER": root_ca_parameter_name, 

412 "BACKEND_TLS_ROOT_CA_REGION": registry_region, 

413 "BACKEND_TLS_CA_CACHE_TTL_SECONDS": str( 

414 backend_tls_config["trust_cache_ttl_seconds"] 

415 ), 

416 "BACKEND_TLS_CA_MAX_STALE_SECONDS": str( 

417 backend_tls_config["trust_cache_max_stale_seconds"] 

418 ), 

419 }, 

420 log_group=log_group, 

421 description=( 

422 f"Regional inference response-streaming proxy for {self.deployment_region}" 

423 ), 

424 tracing=lambda_.Tracing.ACTIVE, 

425 ) 

426 

427 def _create_api_gateway(self) -> apigateway.RestApi: 

428 """Create regional API Gateway with IAM authentication.""" 

429 project_name = self.config.get_project_name() 

430 

431 # Create CloudWatch log group 

432 # log_group_name intentionally omitted - let CDK generate unique name 

433 api_log_group = logs.LogGroup( 

434 self, 

435 "ApiGatewayLogs", 

436 retention=logs.RetentionDays.ONE_MONTH, 

437 removal_policy=RemovalPolicy.DESTROY, 

438 ) 

439 

440 api_config = self.config.get_api_gateway_config() 

441 configured_log_level = str(api_config["log_level"]).upper() 

442 logging_levels = { 

443 "OFF": apigateway.MethodLoggingLevel.OFF, 

444 "ERROR": apigateway.MethodLoggingLevel.ERROR, 

445 "INFO": apigateway.MethodLoggingLevel.INFO, 

446 } 

447 if configured_log_level not in logging_levels: 

448 raise ValueError( 

449 "api_gateway.log_level must be one of OFF, ERROR, or INFO; " 

450 f"got {configured_log_level!r}" 

451 ) 

452 

453 # Create regional REST API 

454 api = apigateway.RestApi( 

455 self, 

456 "RegionalApi", 

457 rest_api_name=f"{project_name}-regional-api-{self.deployment_region}", 

458 description=f"Direct regional API for {project_name} in {self.deployment_region}", 

459 endpoint_types=[apigateway.EndpointType.REGIONAL], 

460 deploy=True, 

461 deploy_options=apigateway.StageOptions( 

462 stage_name="prod", 

463 throttling_rate_limit=api_config["throttle_rate_limit"], 

464 throttling_burst_limit=api_config["throttle_burst_limit"], 

465 logging_level=logging_levels[configured_log_level], 

466 # Never put inference prompts/responses (or other API bodies) 

467 # into execution logs. Standard access logs and metrics remain. 

468 data_trace_enabled=False, 

469 metrics_enabled=api_config["metrics_enabled"], 

470 tracing_enabled=api_config["tracing_enabled"], 

471 access_log_destination=apigateway.LogGroupLogDestination(api_log_group), 

472 access_log_format=apigateway.AccessLogFormat.json_with_standard_fields( 

473 caller=True, 

474 http_method=True, 

475 ip=True, 

476 protocol=True, 

477 request_time=True, 

478 resource_path=True, 

479 response_length=True, 

480 status=True, 

481 user=True, 

482 ), 

483 ), 

484 cloud_watch_role=True, 

485 # CDK otherwise retains the generated API Gateway account role. 

486 cloud_watch_role_removal_policy=RemovalPolicy.DESTROY, 

487 ) 

488 

489 # The bridge is private at the authorization layer by default: only 

490 # the aggregator execution role is named in the API resource policy. 

491 api.add_to_resource_policy( 

492 iam.PolicyStatement( 

493 effect=iam.Effect.ALLOW, 

494 principals=[iam.ArnPrincipal(self.aggregator_role_arn)], 

495 actions=["execute-api:Invoke"], 

496 resources=[ 

497 f"execute-api:/*/{method}/{path}" 

498 for method, path in AGGREGATOR_REGIONAL_API_ROUTES 

499 ], 

500 ) 

501 ) 

502 

503 # Direct regional mode is an explicit opt-in in the commercial 

504 # partition. It becomes the required supported ingress in partitions 

505 # where Global Accelerator does not exist. Methods still require SigV4 

506 # and callers still need identity-policy permission to invoke. 

507 if ( 

508 self.config.get_api_gateway_config()["regional_api_enabled"] 

509 or not self.global_accelerator_enabled 

510 ): 

511 api.add_to_resource_policy( 

512 iam.PolicyStatement( 

513 effect=iam.Effect.ALLOW, 

514 principals=[iam.AnyPrincipal()], 

515 actions=["execute-api:Invoke"], 

516 resources=["execute-api:/*"], 

517 conditions={ 

518 "StringEquals": {"aws:PrincipalAccount": self.account}, 

519 "ArnNotEquals": {"aws:PrincipalArn": self.aggregator_role_arn}, 

520 }, 

521 ) 

522 ) 

523 

524 # Keep control-plane integration semantics unchanged. Inference uses 

525 # InvokeWithResponseStream and may remain open for API Gateway's full 

526 # 15-minute streaming integration window; request bodies are buffered. 

527 control_plane_integration = apigateway.LambdaIntegration( 

528 self.proxy_lambda, proxy=True, timeout=Duration.seconds(29) 

529 ) 

530 inference_integration = apigateway.LambdaIntegration( 

531 self.inference_proxy_lambda, 

532 proxy=True, 

533 timeout=Duration.minutes(15), 

534 response_transfer_mode=apigateway.ResponseTransferMode.STREAM, 

535 ) 

536 

537 # API Gateway greedy resources do not cross a root segment, so 

538 # /api/v1/{proxy+} cannot match /inference/{endpoint}/.... 

539 api_resource = api.root.add_resource("api") 

540 v1_resource = api_resource.add_resource("v1") 

541 api_proxy_resource = v1_resource.add_resource("{proxy+}") 

542 inference_resource = api.root.add_resource("inference") 

543 inference_proxy_resource = inference_resource.add_resource("{proxy+}") 

544 

545 for method in ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]: 

546 api_proxy_resource.add_method( 

547 method, 

548 control_plane_integration, 

549 authorization_type=apigateway.AuthorizationType.IAM, 

550 method_responses=[ 

551 apigateway.MethodResponse(status_code="200"), 

552 apigateway.MethodResponse(status_code="400"), 

553 apigateway.MethodResponse(status_code="403"), 

554 apigateway.MethodResponse(status_code="500"), 

555 ], 

556 ) 

557 

558 for method in ["GET", "HEAD", "POST"]: 

559 inference_proxy_resource.add_method( 

560 method, 

561 inference_integration, 

562 authorization_type=apigateway.AuthorizationType.IAM, 

563 method_responses=[ 

564 apigateway.MethodResponse(status_code="200"), 

565 apigateway.MethodResponse(status_code="400"), 

566 apigateway.MethodResponse(status_code="404"), 

567 apigateway.MethodResponse(status_code="500"), 

568 apigateway.MethodResponse(status_code="502"), 

569 ], 

570 ) 

571 

572 from gco.stacks.nag_suppressions import acknowledge_nag_findings 

573 

574 acknowledge_nag_findings( 

575 api.deployment_stage, 

576 [ 

577 { 

578 "id": "AwsSolutions-APIG3", 

579 "reason": ( 

580 "This regional bridge is not a general public API: every method " 

581 "requires SigV4 and its resource policy admits only the exact " 

582 "aggregator role unless account-local direct access is explicitly " 

583 "enabled. A separate WAF would duplicate those identity controls." 

584 ), 

585 }, 

586 { 

587 "id": "NIST.800.53.R5-APIGWAssociatedWithWAF", 

588 "reason": ( 

589 "The IAM-authenticated regional bridge has an aggregator-only resource " 

590 "policy by default; unauthorized traffic is rejected before integration." 

591 ), 

592 }, 

593 { 

594 "id": "PCI.DSS.321-APIGWAssociatedWithWAF", 

595 "reason": ( 

596 "The IAM-authenticated regional bridge has an aggregator-only resource " 

597 "policy by default and carries no payment-card-specific public surface." 

598 ), 

599 }, 

600 ], 

601 ) 

602 

603 return api 

604 

605 def _create_outputs(self) -> None: 

606 """Export regional API Gateway endpoint.""" 

607 project_name = self.config.get_project_name() 

608 

609 CfnOutput( 

610 self, 

611 "RegionalApiEndpoint", 

612 value=self.api.url, 

613 description=f"Regional API Gateway endpoint for {self.deployment_region}", 

614 export_name=f"{project_name}-regional-api-endpoint-{self.deployment_region}", 

615 )