How to configure AgentCore Gateway to target a private API Gateway in AWS

Issue #1059

Amazon Bedrock AgentCore Gateway turns existing APIs into MCP tools. Its built-in API Gateway target only works with public, REGIONAL or EDGE API Gateways, because it relies on API Gateway’s export-to-OpenAPI capability, which does not exist for PRIVATE endpoints. For teams whose backend is deliberately private, reachable only through a VPC interface endpoint, that target type is not an option.

The way around this is the OpenAPI schema target. Instead of pointing AgentCore at a REST API and letting it export the spec, you write the OpenAPI document yourself, point its servers URL at your private API’s VPC endpoint alias, and route the target through your VPC. AWS lays out the same pattern in their guide to secure access to private resources, and states it directly in their private connectivity patterns post:

Private REST API Gateway endpoints are not natively supported as direct Gateway targets. To connect a Private REST API Gateway endpoint, export it as an OpenAPI specification, create an OpenAPI target with a private endpoint, and set the routing domain to your API Gateway VPC endpoint DNS name.

Image

Here is a working CDK setup that does exactly that, verified against aws-cdk-lib 2.268.0’s stable aws-bedrockagentcore module.

API Gateway target vs Rest API target

The built-in API Gateway target (addApiGatewayTarget) takes a restApi reference and an optional stage. AgentCore reads the exported OpenAPI document to generate tools, and authenticates outbound calls with IAM, signing requests with SigV4. CDK enforces the public-only constraint at the type level: the backing REST API must use a public endpoint type.

The OpenAPI schema target (addOpenApiTarget) has no restApi or stage field. You supply an apiSchema built from a plain OpenAPI 3.0 document, and the backend host comes entirely from that document’s servers[].url. It authenticates with an API key or OAuth, never IAM.

Image

Both target types share the property that makes private routing possible: privateEndpoint. It sits at the top level of the generated CfnGatewayTarget, as a sibling to targetConfiguration, so it applies the same way regardless of the target type underneath. Setting privateEndpoint.managedVpcResource tells AgentCore to route its outbound call through a VPC, using the subnets and security groups you give it, instead of over the public internet.

Building the shared VPC and endpoint

Both an API Gateway target and an OpenAPI target can route through the same VPC and the same interface endpoint for execute-api. Nothing here is target-specific; it is standard private API Gateway plumbing.

const vpc = new ec2.Vpc(this, "Vpc", {
  maxAzs: 2,
  natGateways: 0,
  subnetConfiguration: [
    { name: "private", subnetType: ec2.SubnetType.PRIVATE_ISOLATED, cidrMask: 24 },
  ],
})

const vpcEndpointSg = new ec2.SecurityGroup(this, "VpcEndpointSg", {
  vpc,
  description: "Allows inbound HTTPS to the execute-api VPC endpoint",
  allowAllOutbound: true,
})
vpcEndpointSg.addIngressRule(ec2.Peer.ipv4(vpc.vpcCidrBlock), ec2.Port.tcp(443))

const executeApiEndpoint = vpc.addInterfaceEndpoint("ExecuteApiEndpoint", {
  service: ec2.InterfaceVpcEndpointAwsService.APIGATEWAY,
  subnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
  securityGroups: [vpcEndpointSg],
  privateDnsEnabled: true,
})

A single VPC endpoint can be associated with more than one private API Gateway, so one endpoint is enough for the whole setup.

The private API Gateway

The API Gateway fronting the private backend uses EndpointType.PRIVATE, and lists the VPC endpoint it should be reachable from:

const BooksApi = new apigateway.RestApi(this, "BooksApi", {
  endpointConfiguration: {
    types: [apigateway.EndpointType.PRIVATE],
    vpcEndpoints: [executeApiEndpoint],
  },
  deploy: true,
  deployOptions: { stageName: "sit" },
})
BooksApi.grantInvokeFromVpcEndpointsOnly([executeApiEndpoint])

const Books = BooksApi.root.addResource("Books")
Books.addMethod("GET", new apigateway.LambdaIntegration(BooksFunction))

endpointConfiguration.vpcEndpoints maps to CloudFormation’s EndpointConfiguration.VpcEndpointIds, so the VPC endpoint association happens declaratively at creation time. grantInvokeFromVpcEndpointsOnly is a helper on RestApiBase that builds a resource policy denying execute-api:Invoke from anywhere except the VPC endpoints you pass in, which saves you from hand-writing that policy.

One constraint is easy to miss: a PRIVATE-type API Gateway cannot use an IPv4-only address type. Setting ipAddressType: IpAddressType.IPV4 makes CDK throw:

Private APIs can only have a dualstack IP address type

Leave ipAddressType unset; dualstack is the default for private APIs.

Building the OpenAPI schema

A PRIVATE API Gateway has no public DNS name, so servers[].url cannot point at the API’s default hostname. Associating the VPC endpoint with the API makes AWS generate a VPC-endpoint-specific Route 53 alias instead, in the form https://{restApiId}-{vpceId}.execute-api.{region}.amazonaws.com/{stage}. It resolves in public DNS, but is only reachable from inside the associated VPC.

Image
const BooksApiUrl = `https://${BooksApi.restApiId}-${executeApiEndpoint.vpcEndpointId}.execute-api.${cdk.Stack.of(this).region}.amazonaws.com/sit`

const BooksApiSchema = agentcore.ApiSchema.fromInline(
  JSON.stringify({
    openapi: "3.0.1",
    info: { title: "Books API", version: "1.0.0" },
    servers: [{ url: BooksApiUrl }],
    paths: {
      "/Books": {
        get: {
          operationId: "getBooks",
          summary: "List Norwegian Books",
          responses: {
            "200": {
              description: "A list of Books",
              content: {
                "application/json": {
                  schema: {
                    type: "object",
                    properties: {
                      Books: {
                        type: "array",
                        items: {
                          type: "object",
                          properties: {
                            title: { type: "string" },
                            director: { type: "string" },
                          },
                        },
                      },
                    },
                  },
                },
              },
            },
          },
        },
      },
    },
  }),
)

BooksApiUrl is built from CDK tokens, so it stays an unresolved placeholder in the string until CDK embeds it into the final template. ApiSchema.fromInline takes that JSON string directly and needs no IAM grants, unlike fromLocalAsset or fromS3File, which upload the schema to S3 first.

There is a second way to point the target at the same backend, and it is the one AWS’s own guidance leads with. Instead of baking the VPCE alias into servers[].url, leave that URL as the API’s plain default hostname (BooksApi.url) and tell AgentCore where to actually send traffic with routingDomain, a field that sits on managedVpcResource next to subnetIds and securityGroupIds. Set it to your VPC endpoint’s DNS name and AgentCore ignores the host in the schema entirely. This is the same behavior you get configuring a routing domain by hand in the console.

Wiring the target and routing it through the VPC

With the private API Gateway and its OpenAPI document ready, the target itself is a single call:

const BooksTarget = gateway.addOpenApiTarget("BooksTarget", {
  apiSchema: BooksApiSchema,
})
Image

addOpenApiTarget has no VPC-routing option in its arguments, so setting privateEndpoint means reaching into the L1 construct CDK generates underneath:

const cfnBooksTarget = BooksTarget.node.defaultChild as agentcore.CfnGatewayTarget
cfnBooksTarget.privateEndpoint = {
  managedVpcResource: {
    vpcIdentifier: vpc.vpcId,
    subnetIds: vpc.isolatedSubnets.map((subnet) => subnet.subnetId),
    securityGroupIds: [vpcEndpointSg.securityGroupId],
    endpointIpAddressType: "IPV4",
    // Optional: route by DNS name instead of by the servers[].url host.
    // routingDomain: "vpce-0123456789abcdef0-xxxxxxxx.execute-api.eu-west-1.vpce.amazonaws.com",
  },
}

privateEndpoint is a typed property on CfnGatewayTargetProps, not something buried behind addPropertyOverride, so this reads like ordinary CDK code. It is the same block you would use for an API Gateway target: the mechanism for routing AgentCore’s call through your VPC does not change based on the target type underneath. Whether you reach the backend through the alias baked into servers[].url or through routingDomain, the resuxlt is the same call, routed inside your VPC.

From here, getBooks shows up as a callable MCP tool, and when an agent calls it, AgentCore’s request never leaves the VPC on its way to the backend Lambda behind the private API Gateway.

Read more

Written by

I’m open source contributor, writer, speaker and product maker.

Start the conversation