Skip to content

Commit bc3db02

Browse files
feat(cloudfront-origins): allow custom originPath for apigateway.RestApi constructs (#24023)
### Current behavior - the `apigateway.RestApi` splits the `restApi.url` and uses the stage name as `originPath` / "Origin path - optional" option in the CloudFront Origin by default ### Issue / Limitation - when using multiple Behaviors in my CloudFront Distribution: - S3 as the default behavior - Regional API Gateway as additional behavior and CloudFront Origin My API Gateway receives an extra `/<api-gateway-stage>/` in its event path and doesn't trigger the correct lambda integration. The below CDK example reproduces the behavior mentioned above: ```typescript import * as cdk from "aws-cdk-lib"; const meLambda = new cdk.aws_lambda_nodejs.NodejsFunction( this, "meLambda", { entry: "handlers/me.ts", } ); const s3_bucket = new cdk.aws_s3.Bucket(this, "somebucket"); const api = new cdk.aws_apigateway.RestApi(this, "somerestapi", { endpointConfiguration: { types: [apigw.EndpointType.REGIONAL], }, defaultCorsPreflightOptions: { allowOrigins: ["*"], }, }); api.root .addResource("me") .addMethod("GET", new apigw.LambdaIntegration(meLambda)); const apiOrigin = new cdk.aws_cloudfront_origins.RestApiOrigin(api); const cdn = new cdk.aws_cloudfront.Distribution(this, "websitecdn", { defaultBehavior: { origin: new cdk.aws_cloudfront_origins.S3Origin(s3_bucket), allowedMethods: cdk.aws_cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS, viewerProtocolPolicy: cdk.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, }, additionalBehaviors: { "prod/*": { // <<<---- The "prod/" is added in the CloudFront Origin Path, NOT DESIRED IS THIS CASE origin: apiOrigin, allowedMethods: cdk.aws_cloudfront.AllowedMethods.ALLOW_ALL, cachePolicy: cdk.aws_cloudfront.CachePolicy.CACHING_DISABLED, viewerProtocolPolicy: cdk.aws_cloudfront.ViewerProtocolPolicy.HTTPS_ONLY, }, }, }); ``` The above will have the following URLs: - `https://<api-gateway-url>/<api-gateway-stage>` - `https://<cloudfront-distribution-url>/some-image.png` => Default Behavior, S3 Bucket - `https://<cloudfront-distribution-url>/<api-gateway-stage>` => API Gateway forward request Because the CloudFront Origin created by `RestApiOrigin` appends the `<api-gateway-stage>` in its CloudFront Origin "Origin Path - optional" option, when using `https://<cloudfront-distribution-url>/<api-gateway-stage>/me` the API Gateway receives `/<api-gateway-stage>/<api-gateway-stage>/me` instead of `/<api-gateway-stage>/me` Removing the "Origin Path - optional" value, fixes the problem. ![00](https://user-images.githubusercontent.com/829902/216874002-06338400-9d88-4c98-b393-da56cc63be02.png) ![01](https://user-images.githubusercontent.com/829902/216874042-643ef152-b2ad-465e-bdda-8a98c68c6dcd.png) ### Workaround - To fix the problem described above, I need to use a custom `cdk.aws_cloudfront_origins.HttpOrigin` construct Replace the `apiOrigin` construct in the example above by: ```typescript const apiOrigin = new origins.HttpOrigin( `${api.restApiId}.execute-api.${cdk.Aws.REGION}.${cdk.Aws.URL_SUFFIX}`, { originSslProtocols: [cloudfront.OriginSslPolicy.TLS_V1_2], protocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY, } ); ``` ### Solution - This PR allows the customization of `apigateway.RestApi` by extending its props from `cloudfront.OriginProps` - Allowing the consumer to pass a `props.originPath` to the `RestApiOrigin` class
1 parent f1fa09c commit bc3db02

20 files changed

+1828
-4
lines changed

Diff for: packages/@aws-cdk/aws-cloudfront-origins/README.md

+9-2
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ new cloudfront.Distribution(this, 'myDist', {
122122
## From an API Gateway REST API
123123

124124
Origins can be created from an API Gateway REST API. It is recommended to use a
125-
[regional API](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-endpoint-types.html) in this case.
125+
[regional API](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-endpoint-types.html) in this case. The origin path will automatically be set as the stage name.
126126

127127
```ts
128128
declare const api: apigateway.RestApi;
@@ -131,4 +131,11 @@ new cloudfront.Distribution(this, 'Distribution', {
131131
});
132132
```
133133

134-
The origin path will automatically be set as the stage name.
134+
If you want to use a different origin path, you can specify it in the `originPath` property.
135+
136+
```ts
137+
declare const api: apigateway.RestApi;
138+
new cloudfront.Distribution(this, 'Distribution', {
139+
defaultBehavior: { origin: new origins.RestApiOrigin(api, { originPath: '/custom-origin-path' }) },
140+
});
141+
```

Diff for: packages/@aws-cdk/aws-cloudfront-origins/lib/rest-api-origin.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { validateSecondsInRangeOrUndefined } from './private/utils';
66
/**
77
* Properties for an Origin for an API Gateway REST API.
88
*/
9-
export interface RestApiOriginProps extends cloudfront.OriginOptions {
9+
export interface RestApiOriginProps extends cloudfront.OriginProps {
1010
/**
1111
* Specifies how long, in seconds, CloudFront waits for a response from the origin, also known as the origin response timeout.
1212
* The valid range is from 1 to 180 seconds, inclusive.
@@ -40,7 +40,7 @@ export class RestApiOrigin extends cloudfront.OriginBase {
4040
// Splitting on '/' gives: ['https', '', '<rest-api-id>.execute-api.<region>.amazonaws.com', '<stage>']
4141
// The element at index 2 is the domain name, the element at index 3 is the stage name
4242
super(cdk.Fn.select(2, cdk.Fn.split('/', restApi.url)), {
43-
originPath: `/${cdk.Fn.select(3, cdk.Fn.split('/', restApi.url))}`,
43+
originPath: props.originPath ?? `/${cdk.Fn.select(3, cdk.Fn.split('/', restApi.url))}`,
4444
...props,
4545
});
4646

Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"version":"30.1.0"}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"version": "30.1.0",
3+
"files": {
4+
"caae80484d7f18b3e0e3e74d50e278f1f6a6f6ad5d264fa03a7fe46851fc2d8c": {
5+
"source": {
6+
"path": "integ-cloudfront-rest-api-origin-custom-origin-path.template.json",
7+
"packaging": "file"
8+
},
9+
"destinations": {
10+
"current_account-current_region": {
11+
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
12+
"objectKey": "caae80484d7f18b3e0e3e74d50e278f1f6a6f6ad5d264fa03a7fe46851fc2d8c.json",
13+
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
14+
}
15+
}
16+
}
17+
},
18+
"dockerImages": {}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
{
2+
"Resources": {
3+
"RestApi0C43BF4B": {
4+
"Type": "AWS::ApiGateway::RestApi",
5+
"Properties": {
6+
"EndpointConfiguration": {
7+
"Types": [
8+
"REGIONAL"
9+
]
10+
},
11+
"Name": "RestApi"
12+
}
13+
},
14+
"RestApiCloudWatchRoleE3ED6605": {
15+
"Type": "AWS::IAM::Role",
16+
"Properties": {
17+
"AssumeRolePolicyDocument": {
18+
"Statement": [
19+
{
20+
"Action": "sts:AssumeRole",
21+
"Effect": "Allow",
22+
"Principal": {
23+
"Service": "apigateway.amazonaws.com"
24+
}
25+
}
26+
],
27+
"Version": "2012-10-17"
28+
},
29+
"ManagedPolicyArns": [
30+
{
31+
"Fn::Join": [
32+
"",
33+
[
34+
"arn:",
35+
{
36+
"Ref": "AWS::Partition"
37+
},
38+
":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"
39+
]
40+
]
41+
}
42+
]
43+
},
44+
"UpdateReplacePolicy": "Retain",
45+
"DeletionPolicy": "Retain"
46+
},
47+
"RestApiAccount7C83CF5A": {
48+
"Type": "AWS::ApiGateway::Account",
49+
"Properties": {
50+
"CloudWatchRoleArn": {
51+
"Fn::GetAtt": [
52+
"RestApiCloudWatchRoleE3ED6605",
53+
"Arn"
54+
]
55+
}
56+
},
57+
"DependsOn": [
58+
"RestApi0C43BF4B"
59+
],
60+
"UpdateReplacePolicy": "Retain",
61+
"DeletionPolicy": "Retain"
62+
},
63+
"RestApiDeployment180EC50368af6d4b358eff290c08cb2de07c4042": {
64+
"Type": "AWS::ApiGateway::Deployment",
65+
"Properties": {
66+
"RestApiId": {
67+
"Ref": "RestApi0C43BF4B"
68+
},
69+
"Description": "Automatically created by the RestApi construct"
70+
},
71+
"DependsOn": [
72+
"RestApiGET0F59260B"
73+
]
74+
},
75+
"RestApiDeploymentStageprod3855DE66": {
76+
"Type": "AWS::ApiGateway::Stage",
77+
"Properties": {
78+
"RestApiId": {
79+
"Ref": "RestApi0C43BF4B"
80+
},
81+
"DeploymentId": {
82+
"Ref": "RestApiDeployment180EC50368af6d4b358eff290c08cb2de07c4042"
83+
},
84+
"StageName": "prod"
85+
},
86+
"DependsOn": [
87+
"RestApiAccount7C83CF5A"
88+
]
89+
},
90+
"RestApiGET0F59260B": {
91+
"Type": "AWS::ApiGateway::Method",
92+
"Properties": {
93+
"HttpMethod": "GET",
94+
"ResourceId": {
95+
"Fn::GetAtt": [
96+
"RestApi0C43BF4B",
97+
"RootResourceId"
98+
]
99+
},
100+
"RestApiId": {
101+
"Ref": "RestApi0C43BF4B"
102+
},
103+
"AuthorizationType": "NONE",
104+
"Integration": {
105+
"Type": "MOCK"
106+
}
107+
}
108+
},
109+
"Distribution830FAC52": {
110+
"Type": "AWS::CloudFront::Distribution",
111+
"Properties": {
112+
"DistributionConfig": {
113+
"DefaultCacheBehavior": {
114+
"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
115+
"Compress": true,
116+
"TargetOriginId": "integcloudfrontrestapiorigincustomoriginpathDistributionOrigin1635825EA",
117+
"ViewerProtocolPolicy": "allow-all"
118+
},
119+
"Enabled": true,
120+
"HttpVersion": "http2",
121+
"IPV6Enabled": true,
122+
"Origins": [
123+
{
124+
"CustomOriginConfig": {
125+
"OriginProtocolPolicy": "https-only",
126+
"OriginSSLProtocols": [
127+
"TLSv1.2"
128+
]
129+
},
130+
"DomainName": {
131+
"Fn::Select": [
132+
2,
133+
{
134+
"Fn::Split": [
135+
"/",
136+
{
137+
"Fn::Join": [
138+
"",
139+
[
140+
"https://",
141+
{
142+
"Ref": "RestApi0C43BF4B"
143+
},
144+
".execute-api.",
145+
{
146+
"Ref": "AWS::Region"
147+
},
148+
".",
149+
{
150+
"Ref": "AWS::URLSuffix"
151+
},
152+
"/",
153+
{
154+
"Ref": "RestApiDeploymentStageprod3855DE66"
155+
},
156+
"/"
157+
]
158+
]
159+
}
160+
]
161+
}
162+
]
163+
},
164+
"Id": "integcloudfrontrestapiorigincustomoriginpathDistributionOrigin1635825EA",
165+
"OriginPath": ""
166+
}
167+
]
168+
}
169+
}
170+
}
171+
},
172+
"Outputs": {
173+
"RestApiEndpoint0551178A": {
174+
"Value": {
175+
"Fn::Join": [
176+
"",
177+
[
178+
"https://",
179+
{
180+
"Ref": "RestApi0C43BF4B"
181+
},
182+
".execute-api.",
183+
{
184+
"Ref": "AWS::Region"
185+
},
186+
".",
187+
{
188+
"Ref": "AWS::URLSuffix"
189+
},
190+
"/",
191+
{
192+
"Ref": "RestApiDeploymentStageprod3855DE66"
193+
},
194+
"/"
195+
]
196+
]
197+
}
198+
}
199+
},
200+
"Parameters": {
201+
"BootstrapVersion": {
202+
"Type": "AWS::SSM::Parameter::Value<String>",
203+
"Default": "/cdk-bootstrap/hnb659fds/version",
204+
"Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"
205+
}
206+
},
207+
"Rules": {
208+
"CheckBootstrapVersion": {
209+
"Assertions": [
210+
{
211+
"Assert": {
212+
"Fn::Not": [
213+
{
214+
"Fn::Contains": [
215+
[
216+
"1",
217+
"2",
218+
"3",
219+
"4",
220+
"5"
221+
],
222+
{
223+
"Ref": "BootstrapVersion"
224+
}
225+
]
226+
}
227+
]
228+
},
229+
"AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI."
230+
}
231+
]
232+
}
233+
}
234+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"version": "30.1.0",
3+
"testCases": {
4+
"rest-api-origin-custom-origin-path/DefaultTest": {
5+
"stacks": [
6+
"integ-cloudfront-rest-api-origin-custom-origin-path"
7+
],
8+
"assertionStack": "rest-api-origin-custom-origin-path/DefaultTest/DeployAssert",
9+
"assertionStackName": "restapiorigincustomoriginpathDefaultTestDeployAssertCD227A2A"
10+
}
11+
}
12+
}

0 commit comments

Comments
 (0)