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

95 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-09-14 22:07 +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 backend_tls_root_ca_parameter_name, 

55 backend_tls_server_name, 

56 validated_request_body_limit, 

57) 

58 

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

60# Generated at (UTC): 2026-09-01T14:42:56Z 

61# Generated from Git commit: 89b000378ed5a912a38c06f4feab2b029936ebcc 

62# Flowchart(s) generated from this file: 

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

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

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

66# <pyflowchart-code-diagram> END 

67 

68 

69class GCORegionalApiGatewayStack(Stack): 

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

71 

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

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

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

75 in partitions where Global Accelerator is unavailable. 

76 

77 Attributes: 

78 api: Regional REST API with IAM authentication. 

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

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

81 """ 

82 

83 def __init__( 

84 self, 

85 scope: Construct, 

86 construct_id: str, 

87 config: ConfigLoader, 

88 region: str, 

89 vpc: ec2.IVpc, 

90 auth_secret_arn: str, 

91 aggregator_role_arn: str, 

92 alb_dns_name: str | None = None, 

93 **kwargs: Any, 

94 ) -> None: 

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

96 

97 self.config = config 

98 self.deployment_region = region 

99 self.vpc = vpc 

100 self.alb_dns_name = alb_dns_name 

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

102 self.global_accelerator_enabled = ( 

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

104 ) 

105 self.auth_secret_arn = auth_secret_arn 

106 self.aggregator_role_arn = aggregator_role_arn 

107 

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

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

110 self.proxy_lambda = self._create_vpc_proxy_lambda() 

111 self.inference_proxy_lambda = self._create_inference_proxy_lambda() 

112 

113 # Create regional API Gateway 

114 self.api = self._create_api_gateway() 

115 

116 # Export outputs 

117 self._create_outputs() 

118 

119 # Apply cdk-nag suppressions 

120 self._apply_nag_suppressions() 

121 

122 def _apply_nag_suppressions(self) -> None: 

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

124 from gco.stacks.nag_suppressions import apply_all_suppressions 

125 

126 apply_all_suppressions( 

127 self, 

128 stack_type="regional_api_gateway", 

129 global_region=self.config.get_global_region(), 

130 project_name=self.config.get_project_name(), 

131 ) 

132 

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

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

135 project_name = self.config.get_project_name() 

136 backend_tls_config = self.config.get_backend_tls_config() 

137 root_ca_parameter_name = backend_tls_root_ca_parameter_name(project_name) 

138 

139 # Create security group for Lambda 

140 lambda_sg = ec2.SecurityGroup( 

141 self, 

142 "ProxyLambdaSg", 

143 vpc=self.vpc, 

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

145 allow_all_outbound=True, 

146 ) 

147 self._proxy_lambda_security_group = lambda_sg 

148 

149 # Create IAM role for Lambda 

150 # role_name intentionally omitted - let CDK generate unique name 

151 lambda_role = iam.Role( 

152 self, 

153 "ProxyLambdaRole", 

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

155 managed_policies=[ 

156 iam.ManagedPolicy.from_aws_managed_policy_name( 

157 "service-role/AWSLambdaVPCAccessExecutionRole" 

158 ) 

159 ], 

160 ) 

161 

162 # Grant read access to auth secret. 

163 lambda_role.add_to_policy( 

164 iam.PolicyStatement( 

165 effect=iam.Effect.ALLOW, 

166 actions=[ 

167 "secretsmanager:GetSecretValue", 

168 "secretsmanager:DescribeSecret", 

169 ], 

170 resources=[f"{self.auth_secret_arn}*"], 

171 ) 

172 ) 

173 

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

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

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

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

178 registry_region = self.config.get_global_region() 

179 registry_parameter_arn = ( 

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

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

182 ) 

183 root_ca_parameter_arn = ( 

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

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

186 ) 

187 lambda_role.add_to_policy( 

188 iam.PolicyStatement( 

189 effect=iam.Effect.ALLOW, 

190 actions=["ssm:GetParameter"], 

191 resources=[registry_parameter_arn, root_ca_parameter_arn], 

192 ) 

193 ) 

194 lambda_role.add_to_policy( 

195 iam.PolicyStatement( 

196 effect=iam.Effect.ALLOW, 

197 actions=[ 

198 "elasticloadbalancing:DescribeLoadBalancers", 

199 "elasticloadbalancing:DescribeTags", 

200 ], 

201 resources=["*"], 

202 ) 

203 ) 

204 

205 from gco.stacks.nag_suppressions import acknowledge_nag_findings 

206 

207 acknowledge_nag_findings( 

208 lambda_role, 

209 [ 

210 { 

211 "id": "AwsSolutions-IAM5", 

212 "reason": ( 

213 "ELB DescribeLoadBalancers and DescribeTags do not support " 

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

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

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

217 ), 

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

219 } 

220 ], 

221 ) 

222 

223 # Create log group 

224 # log_group_name intentionally omitted - let CDK generate unique name 

225 log_group = logs.LogGroup( 

226 self, 

227 "ProxyLambdaLogGroup", 

228 retention=logs.RetentionDays.ONE_WEEK, 

229 removal_policy=RemovalPolicy.DESTROY, 

230 ) 

231 

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

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

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

235 environment = { 

236 "SECRET_ARN": self.auth_secret_arn, 

237 "REGISTRY_REGION": registry_region, 

238 "TARGET_REGION": self.deployment_region, 

239 "PROJECT_NAME": project_name, 

240 "AWS_ACCOUNT_ID": self.account, 

241 "AWS_URL_SUFFIX": self.url_suffix, 

242 "BACKEND_TLS_SERVER_NAME": backend_tls_server_name(project_name), 

243 "BACKEND_TLS_ROOT_CA_PARAMETER": root_ca_parameter_name, 

244 "BACKEND_TLS_ROOT_CA_REGION": registry_region, 

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

246 "BACKEND_TLS_CA_MAX_STALE_SECONDS": str( 

247 backend_tls_config["trust_cache_max_stale_seconds"] 

248 ), 

249 } 

250 if self.alb_dns_name: 

251 environment["ALB_ENDPOINT"] = self.alb_dns_name 

252 

253 # Create Lambda function in VPC 

254 proxy_lambda = lambda_.Function( 

255 self, 

256 "RegionalProxyFunction", 

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

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

259 handler="handler.lambda_handler", 

260 code=lambda_.Code.from_asset("lambda/regional-api-proxy"), 

261 timeout=Duration.seconds(29), 

262 memory_size=256, 

263 role=lambda_role, 

264 vpc=self.vpc, 

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

266 security_groups=[lambda_sg], 

267 environment=environment, 

268 log_group=log_group, 

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

270 tracing=lambda_.Tracing.ACTIVE, 

271 ) 

272 

273 return proxy_lambda 

274 

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

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

277 project_name = self.config.get_project_name() 

278 backend_tls_config = self.config.get_backend_tls_config() 

279 max_request_body_bytes = validated_request_body_limit( 

280 self.config.get_manifest_processor_config().get( 

281 "max_request_body_bytes", DEFAULT_MAX_REQUEST_BODY_BYTES 

282 ) 

283 ) 

284 registry_region = self.config.get_global_region() 

285 root_ca_parameter_name = backend_tls_root_ca_parameter_name(project_name) 

286 registry_parameter_arn = ( 

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

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

289 ) 

290 root_ca_parameter_arn = ( 

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

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

293 ) 

294 

295 role = iam.Role( 

296 self, 

297 "InferenceStreamingProxyRole", 

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

299 managed_policies=[ 

300 iam.ManagedPolicy.from_aws_managed_policy_name( 

301 "service-role/AWSLambdaVPCAccessExecutionRole" 

302 ) 

303 ], 

304 ) 

305 role.add_to_policy( 

306 iam.PolicyStatement( 

307 effect=iam.Effect.ALLOW, 

308 actions=[ 

309 "secretsmanager:GetSecretValue", 

310 "secretsmanager:DescribeSecret", 

311 ], 

312 resources=[f"{self.auth_secret_arn}*"], 

313 ) 

314 ) 

315 role.add_to_policy( 

316 iam.PolicyStatement( 

317 effect=iam.Effect.ALLOW, 

318 actions=["ssm:GetParameter"], 

319 resources=[registry_parameter_arn, root_ca_parameter_arn], 

320 ) 

321 ) 

322 role.add_to_policy( 

323 iam.PolicyStatement( 

324 effect=iam.Effect.ALLOW, 

325 actions=[ 

326 "elasticloadbalancing:DescribeLoadBalancers", 

327 "elasticloadbalancing:DescribeTags", 

328 ], 

329 resources=["*"], 

330 ) 

331 ) 

332 

333 from gco.stacks.nag_suppressions import acknowledge_nag_findings 

334 

335 acknowledge_nag_findings( 

336 role, 

337 [ 

338 { 

339 "id": "AwsSolutions-IAM5", 

340 "reason": ( 

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

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

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

344 ), 

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

346 } 

347 ], 

348 ) 

349 

350 log_group = logs.LogGroup( 

351 self, 

352 "InferenceStreamingProxyLogGroup", 

353 retention=logs.RetentionDays.ONE_WEEK, 

354 removal_policy=RemovalPolicy.DESTROY, 

355 ) 

356 return lambda_.Function( 

357 self, 

358 "InferenceStreamingProxyFunction", 

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

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

361 handler="index.handler", 

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

363 timeout=Duration.minutes(15), 

364 memory_size=256, 

365 role=role, 

366 vpc=self.vpc, 

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

368 security_groups=[self._proxy_lambda_security_group], 

369 environment={ 

370 "ROUTING_MODE": "regional", 

371 "MAX_REQUEST_BODY_BYTES": str(max_request_body_bytes), 

372 "SECRET_ARN": self.auth_secret_arn, 

373 "REGISTRY_REGION": registry_region, 

374 "TARGET_REGION": self.deployment_region, 

375 "PROJECT_NAME": project_name, 

376 "AWS_ACCOUNT_ID": self.account, 

377 "AWS_URL_SUFFIX": self.url_suffix, 

378 "BACKEND_TLS_SERVER_NAME": backend_tls_server_name(project_name), 

379 "BACKEND_TLS_ROOT_CA_PARAMETER": root_ca_parameter_name, 

380 "BACKEND_TLS_ROOT_CA_REGION": registry_region, 

381 "BACKEND_TLS_CA_CACHE_TTL_SECONDS": str( 

382 backend_tls_config["trust_cache_ttl_seconds"] 

383 ), 

384 "BACKEND_TLS_CA_MAX_STALE_SECONDS": str( 

385 backend_tls_config["trust_cache_max_stale_seconds"] 

386 ), 

387 }, 

388 log_group=log_group, 

389 description=( 

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

391 ), 

392 tracing=lambda_.Tracing.ACTIVE, 

393 ) 

394 

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

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

397 project_name = self.config.get_project_name() 

398 

399 # Create CloudWatch log group 

400 # log_group_name intentionally omitted - let CDK generate unique name 

401 api_log_group = logs.LogGroup( 

402 self, 

403 "ApiGatewayLogs", 

404 retention=logs.RetentionDays.ONE_MONTH, 

405 removal_policy=RemovalPolicy.DESTROY, 

406 ) 

407 

408 api_config = self.config.get_api_gateway_config() 

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

410 logging_levels = { 

411 "OFF": apigateway.MethodLoggingLevel.OFF, 

412 "ERROR": apigateway.MethodLoggingLevel.ERROR, 

413 "INFO": apigateway.MethodLoggingLevel.INFO, 

414 } 

415 if configured_log_level not in logging_levels: 

416 raise ValueError( 

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

418 f"got {configured_log_level!r}" 

419 ) 

420 

421 # Create regional REST API 

422 api = apigateway.RestApi( 

423 self, 

424 "RegionalApi", 

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

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

427 endpoint_types=[apigateway.EndpointType.REGIONAL], 

428 deploy=True, 

429 deploy_options=apigateway.StageOptions( 

430 stage_name="prod", 

431 throttling_rate_limit=api_config["throttle_rate_limit"], 

432 throttling_burst_limit=api_config["throttle_burst_limit"], 

433 logging_level=logging_levels[configured_log_level], 

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

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

436 data_trace_enabled=False, 

437 metrics_enabled=api_config["metrics_enabled"], 

438 tracing_enabled=api_config["tracing_enabled"], 

439 access_log_destination=apigateway.LogGroupLogDestination(api_log_group), 

440 access_log_format=apigateway.AccessLogFormat.json_with_standard_fields( 

441 caller=True, 

442 http_method=True, 

443 ip=True, 

444 protocol=True, 

445 request_time=True, 

446 resource_path=True, 

447 response_length=True, 

448 status=True, 

449 user=True, 

450 ), 

451 ), 

452 cloud_watch_role=True, 

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

454 cloud_watch_role_removal_policy=RemovalPolicy.DESTROY, 

455 ) 

456 

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

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

459 api.add_to_resource_policy( 

460 iam.PolicyStatement( 

461 effect=iam.Effect.ALLOW, 

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

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

464 resources=[ 

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

466 for method, path in AGGREGATOR_REGIONAL_API_ROUTES 

467 ], 

468 ) 

469 ) 

470 

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

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

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

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

475 if ( 

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

477 or not self.global_accelerator_enabled 

478 ): 

479 api.add_to_resource_policy( 

480 iam.PolicyStatement( 

481 effect=iam.Effect.ALLOW, 

482 principals=[iam.AnyPrincipal()], 

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

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

485 conditions={ 

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

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

488 }, 

489 ) 

490 ) 

491 

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

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

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

495 control_plane_integration = apigateway.LambdaIntegration( 

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

497 ) 

498 inference_integration = apigateway.LambdaIntegration( 

499 self.inference_proxy_lambda, 

500 proxy=True, 

501 timeout=Duration.minutes(15), 

502 response_transfer_mode=apigateway.ResponseTransferMode.STREAM, 

503 ) 

504 

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

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

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

508 v1_resource = api_resource.add_resource("v1") 

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

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

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

512 

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

514 api_proxy_resource.add_method( 

515 method, 

516 control_plane_integration, 

517 authorization_type=apigateway.AuthorizationType.IAM, 

518 method_responses=[ 

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

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

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

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

523 ], 

524 ) 

525 

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

527 inference_proxy_resource.add_method( 

528 method, 

529 inference_integration, 

530 authorization_type=apigateway.AuthorizationType.IAM, 

531 method_responses=[ 

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

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

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

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

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

537 ], 

538 ) 

539 

540 from gco.stacks.nag_suppressions import acknowledge_nag_findings 

541 

542 acknowledge_nag_findings( 

543 api.deployment_stage, 

544 [ 

545 { 

546 "id": "AwsSolutions-APIG3", 

547 "reason": ( 

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

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

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

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

552 ), 

553 }, 

554 { 

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

556 "reason": ( 

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

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

559 ), 

560 }, 

561 { 

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

563 "reason": ( 

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

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

566 ), 

567 }, 

568 ], 

569 ) 

570 

571 return api 

572 

573 def _create_outputs(self) -> None: 

574 """Export regional API Gateway endpoint.""" 

575 project_name = self.config.get_project_name() 

576 

577 CfnOutput( 

578 self, 

579 "RegionalApiEndpoint", 

580 value=self.api.url, 

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

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

583 )