Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as cdk from 'aws-cdk-lib';
import { IntegTest } from '@aws-cdk/integ-tests-alpha';
import * as apigw from 'aws-cdk-lib/aws-apigateway';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';

/**
* Integration test for API Gateway custom domain routing rules.
* Creates a domain with routingMode ROUTING_RULE_ONLY and multiple routing rules
* (path-based, header-based, and catch-all).
*/
class RoutingRulesStack extends cdk.Stack {
constructor(scope: cdk.App, id: string) {
super(scope, id);

const certArn = 'arn:aws:acm:us-east-1:111111111111:certificate/00000000-0000-0000-0000-000000000000';
const certificate = acm.Certificate.fromCertificateArn(this, 'Cert', certArn);

const usersApi = new apigw.RestApi(this, 'UsersApi', { deploy: true });
usersApi.root.addMethod('GET');

const ordersApi = new apigw.RestApi(this, 'OrdersApi', { deploy: true });
ordersApi.root.addMethod('GET');

const defaultApi = new apigw.RestApi(this, 'DefaultApi', { deploy: true });
defaultApi.root.addMethod('GET');

const domainName = `api-${this.stackName.toLowerCase().replace(/[^a-z0-9-]/g, '-')}.example.com`;
const domain = new apigw.DomainName(this, 'Domain', {
domainName,
certificate,
endpointType: apigw.EndpointType.REGIONAL,
routingMode: apigw.RoutingMode.ROUTING_RULE_ONLY,
});

domain.addRoutingRule('UsersRule', {
priority: 100,
conditions: { basePaths: ['users'] },
action: {
restApi: usersApi,
stripBasePath: true,
},
});

domain.addRoutingRule('HeaderV2Rule', {
priority: 50,
conditions: {
headers: [{ header: 'x-api-version', valueGlob: 'v2' }],
},
action: { restApi: ordersApi },
});

domain.addRoutingRule('CatchAllRule', {
priority: 999999,
action: { restApi: defaultApi },
});
}
}

const app = new cdk.App();
const stack = new RoutingRulesStack(app, 'integ-routing-rules');

new IntegTest(app, 'routing-rules', {
testCases: [stack],
});
61 changes: 61 additions & 0 deletions packages/aws-cdk-lib/aws-apigateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ running on AWS Lambda, or any web application.
- [Controlled triggering of deployments](#controlled-triggering-of-deployments)
- [Deep dive: Invalidation of deployments](#deep-dive-invalidation-of-deployments)
- [Custom Domains](#custom-domains)
- [Custom domain routing rules](#custom-domain-routing-rules)
- [Custom Domains with multi-level api mapping](#custom-domains-with-multi-level-api-mapping)
- [Access Logging](#access-logging)
- [Cross Origin Resource Sharing (CORS)](#cross-origin-resource-sharing-cors)
Expand Down Expand Up @@ -1276,6 +1277,66 @@ new route53.ARecord(this, 'CustomDomainAliasRecord', {
});
```

### Custom domain routing rules

You can route traffic by path or header using **routing rules**. Set `routingMode` to
`RoutingMode.ROUTING_RULE_ONLY` (or `ROUTING_RULE_THEN_API_MAPPING` to combine rules with API mappings).
Routing rules are only supported for **REGIONAL** endpoints.

```ts
declare const certificate: acm.ICertificate;

const domain = new apigateway.DomainName(this, 'Domain', {
domainName: 'api.example.com',
certificate,
endpointType: apigateway.EndpointType.REGIONAL,
routingMode: apigateway.RoutingMode.ROUTING_RULE_ONLY,
});

// Path-based: /users/* -> Users API
domain.addRoutingRule('UsersRule', {
priority: 100,
conditions: { basePaths: ['users'] },
action: {
restApi: usersApi,
stripBasePath: true,
},
});

// Header-based: x-api-version: v2 -> Orders v2 API
domain.addRoutingRule('HeaderV2Rule', {
priority: 50,
conditions: {
headers: [{ header: 'x-api-version', valueGlob: 'v2' }],
},
action: { restApi: ordersApiV2 },
});

// Catch-all (lowest priority)
domain.addRoutingRule('CatchAllRule', {
priority: 999999,
action: { restApi: defaultApi },
});
```

You can also create a `RoutingRule` standalone (for example, when the domain is in another stack):

```ts
declare const importedDomain: apigateway.IDomainName;
declare const restApi: apigateway.RestApi;

new apigateway.RoutingRule(this, 'Rule', {
domainName: importedDomain,
priority: 100,
conditions: { basePaths: ['users'] },
action: {
restApi,
stage: restApi.deploymentStage,
stripBasePath: true,
},
});
```

### Custom Domains with multi-level api mapping

Additional requirements for creating multi-level path mappings for RestApis:
Expand Down
89 changes: 89 additions & 0 deletions packages/aws-cdk-lib/aws-apigateway/lib/domain-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type { BasePathMappingOptions } from './base-path-mapping';
import { BasePathMapping } from './base-path-mapping';
import type { IRestApi } from './restapi';
import { EndpointType } from './restapi';
import type { AddRoutingRuleOptions } from './routing-rule';
import { RoutingRule } from './routing-rule';
import * as apigwv2 from '../../aws-apigatewayv2';
import type { IBucket } from '../../aws-s3';
import type { IResource } from '../../core';
Expand Down Expand Up @@ -43,6 +45,31 @@ export enum SecurityPolicy {
TLS_1_2 = 'TLS_1_2',
}

/**
* Routing mode for a custom domain name.
* Determines how API Gateway routes traffic from the domain to your APIs.
*
* @see https://docs.aws.amazon.com/apigateway/latest/developerguide/rest-api-routing-rules.html
*/
export enum RoutingMode {
/**
* Only base path mappings and API mappings are used. Routing rules are ignored.
* This is the default.
*/
BASE_PATH_MAPPING_ONLY = 'BASE_PATH_MAPPING_ONLY',

/**
* Only routing rules are evaluated. Base path mappings and API mappings are ignored.
* Use addRoutingRule() to define rules. Requires REGIONAL endpoint.
*/
ROUTING_RULE_ONLY = 'ROUTING_RULE_ONLY',

/**
* Routing rules are evaluated first; if no rule matches, API mappings are used.
*/
ROUTING_RULE_THEN_API_MAPPING = 'ROUTING_RULE_THEN_API_MAPPING',
}

export interface DomainNameOptions {
/**
* The custom domain name for your API. Uppercase letters are not supported.
Expand Down Expand Up @@ -83,6 +110,15 @@ export interface DomainNameOptions {
* @default - map requests from the domain root (e.g. `example.com`).
*/
readonly basePath?: string;

/**
* Routing mode for this domain. When set to ROUTING_RULE_ONLY or
* ROUTING_RULE_THEN_API_MAPPING, you can use addRoutingRule() to route by path/header.
* Routing rules are only supported for REGIONAL endpoints.
*
* @default RoutingMode.BASE_PATH_MAPPING_ONLY
*/
readonly routingMode?: RoutingMode;
}

export interface DomainNameProps extends DomainNameOptions {
Expand Down Expand Up @@ -156,16 +192,22 @@ export class DomainName extends Resource implements IDomainName {
private readonly basePaths = new Set<string | undefined>();
private readonly securityPolicy?: SecurityPolicy;
private readonly endpointType: EndpointType;
private readonly routingMode: RoutingMode;

constructor(scope: Construct, id: string, props: DomainNameProps) {
super(scope, id);
// Enhanced CDK Analytics Telemetry
addConstructMetadata(this, props);

this.endpointType = props.endpointType || EndpointType.REGIONAL;
this.routingMode = props.routingMode ?? RoutingMode.BASE_PATH_MAPPING_ONLY;
const edge = this.endpointType === EndpointType.EDGE;
this.securityPolicy = props.securityPolicy;

if (this.routingMode !== RoutingMode.BASE_PATH_MAPPING_ONLY && this.endpointType !== EndpointType.REGIONAL) {
throw new ValidationError('Routing rules are only supported for REGIONAL endpoint type', scope);
}

if (!Token.isUnresolved(props.domainName) && /[A-Z]/.test(props.domainName)) {
throw new ValidationError(`Domain name does not support uppercase letters. Got: ${props.domainName}`, scope);
}
Expand All @@ -178,6 +220,7 @@ export class DomainName extends Resource implements IDomainName {
endpointConfiguration: { types: [this.endpointType] },
mutualTlsAuthentication: mtlsConfig,
securityPolicy: props.securityPolicy,
routingMode: this.routingMode,
});

this.domainName = resource.ref;
Expand All @@ -191,6 +234,13 @@ export class DomainName extends Resource implements IDomainName {
? resource.attrDistributionHostedZoneId
: resource.attrRegionalHostedZoneId;

if (this.routingMode === RoutingMode.ROUTING_RULE_ONLY && props.mapping) {
throw new ValidationError(
'Cannot set "mapping" when routingMode is ROUTING_RULE_ONLY. Use addRoutingRule() instead.',
scope,
);
}

const multiLevel = this.validateBasePath(props.basePath);
if (props.mapping && !multiLevel) {
this.addBasePathMapping(props.mapping, {
Expand Down Expand Up @@ -232,6 +282,12 @@ export class DomainName extends Resource implements IDomainName {
*/
@MethodMetadata()
public addBasePathMapping(targetApi: IRestApiRef, options: BasePathMappingOptions = {}): BasePathMapping {
if (this.routingMode === RoutingMode.ROUTING_RULE_ONLY) {
throw new ValidationError(
'Cannot call addBasePathMapping when routingMode is ROUTING_RULE_ONLY. Set routingMode to BASE_PATH_MAPPING_ONLY or ROUTING_RULE_THEN_API_MAPPING.',
this,
);
}
if (this.basePaths.has(options.basePath)) {
throw new ValidationError(`DomainName ${this.node.id} already has a mapping for path ${options.basePath}`, this);
}
Expand Down Expand Up @@ -262,6 +318,12 @@ export class DomainName extends Resource implements IDomainName {
*/
@MethodMetadata()
public addApiMapping(targetStage: IStageRef, options: ApiMappingOptions = {}): void {
if (this.routingMode === RoutingMode.ROUTING_RULE_ONLY) {
throw new ValidationError(
'Cannot call addApiMapping when routingMode is ROUTING_RULE_ONLY. Set routingMode to BASE_PATH_MAPPING_ONLY or ROUTING_RULE_THEN_API_MAPPING.',
this,
);
}
if (this.basePaths.has(options.basePath)) {
throw new ValidationError(`DomainName ${this.node.id} already has a mapping for path ${options.basePath}`, this);
}
Expand All @@ -276,6 +338,33 @@ export class DomainName extends Resource implements IDomainName {
});
}

/**
* Adds a routing rule to this domain. Only allowed when routingMode is
* ROUTING_RULE_ONLY or ROUTING_RULE_THEN_API_MAPPING. Domain must be REGIONAL.
*
* @param id Unique id for the rule construct
* @param options Priority, conditions (base path and/or headers), and target API action
* @returns The RoutingRule construct
*/
@MethodMetadata()
public addRoutingRule(id: string, options: AddRoutingRuleOptions): RoutingRule {
if (this.routingMode === RoutingMode.BASE_PATH_MAPPING_ONLY) {
throw new ValidationError(
'Cannot call addRoutingRule when routingMode is BASE_PATH_MAPPING_ONLY. Set routingMode to ROUTING_RULE_ONLY or ROUTING_RULE_THEN_API_MAPPING.',
this,
);
}
if (this.endpointType !== EndpointType.REGIONAL) {
throw new ValidationError('Routing rules are only supported for REGIONAL endpoint type', this);
}
return new RoutingRule(this, id, {
domainName: this,
priority: options.priority,
conditions: options.conditions,
action: options.action,
});
}

private configureMTLS(mtlsConfig?: MTLSConfig): CfnDomainName.MutualTlsAuthenticationProperty | undefined {
if (!mtlsConfig) return undefined;
return {
Expand Down
1 change: 1 addition & 0 deletions packages/aws-cdk-lib/aws-apigateway/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export * from './authorizer';
export * from './json-schema';
export * from './domain-name';
export * from './base-path-mapping';
export * from './routing-rule';
export * from './cors';
export * from './authorizers';
export * from './access-log';
Expand Down
Loading
Loading