Skip to content

Commit a598508

Browse files
authored
feat(apprunner): add AutoScalingConfiguration for AppRunner Service (#30358)
### Issue # (if applicable) Closes #30353 . ### Reason for this change At the moment, L2 Construct does not support a custom auto scaling configuration for the AppRunner Service. ### Description of changes * Add `AutoScalingConfiguration` Class * Add `autoScalingConfiguration` property to the `Service` Class ### Description of how you validated changes Add unit tests and integ tests. ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent f2c5f68 commit a598508

15 files changed

+1020
-0
lines changed

packages/@aws-cdk/aws-apprunner-alpha/README.md

+21
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,27 @@ when required.
154154

155155
See [App Runner IAM Roles](https://docs.aws.amazon.com/apprunner/latest/dg/security_iam_service-with-iam.html#security_iam_service-with-iam-roles) for more details.
156156

157+
## Auto Scaling Configuration
158+
159+
To associate an App Runner service with a custom Auto Scaling Configuration, define `autoScalingConfiguration` for the service.
160+
161+
```ts
162+
const autoScalingConfiguration = new apprunner.AutoScalingConfiguration(this, 'AutoScalingConfiguration', {
163+
autoScalingConfigurationName: 'MyAutoScalingConfiguration',
164+
maxConcurrency: 150,
165+
maxSize: 20,
166+
minSize: 5,
167+
});
168+
169+
new apprunner.Service(this, 'DemoService', {
170+
source: apprunner.Source.fromEcrPublic({
171+
imageConfiguration: { port: 8000 },
172+
imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest',
173+
}),
174+
autoScalingConfiguration,
175+
});
176+
```
177+
157178
## VPC Connector
158179

159180
To associate an App Runner service with a custom VPC, define `vpcConnector` for the service.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import * as cdk from 'aws-cdk-lib/core';
2+
import { Construct } from 'constructs';
3+
import { CfnAutoScalingConfiguration } from 'aws-cdk-lib/aws-apprunner';
4+
5+
/**
6+
* Properties of the App Runner Auto Scaling Configuration.
7+
*/
8+
export interface AutoScalingConfigurationProps {
9+
/**
10+
* The name for the Auto Scaling Configuration.
11+
*
12+
* @default - a name generated by CloudFormation
13+
*/
14+
readonly autoScalingConfigurationName?: string;
15+
16+
/**
17+
* The maximum number of concurrent requests that an instance processes.
18+
* If the number of concurrent requests exceeds this limit, App Runner scales the service up.
19+
*
20+
* Must be between 1 and 200.
21+
*
22+
* @default 100
23+
*/
24+
readonly maxConcurrency?: number;
25+
26+
/**
27+
* The maximum number of instances that a service scales up to.
28+
* At most maxSize instances actively serve traffic for your service.
29+
*
30+
* Must be between 1 and 25.
31+
*
32+
* @default 25
33+
*/
34+
readonly maxSize?: number;
35+
36+
/**
37+
* The minimum number of instances that App Runner provisions for a service.
38+
* The service always has at least minSize provisioned instances.
39+
*
40+
*
41+
* Must be between 1 and 25.
42+
*
43+
* @default 1
44+
*/
45+
readonly minSize?: number;
46+
}
47+
48+
/**
49+
* Attributes for the App Runner Auto Scaling Configuration.
50+
*/
51+
export interface AutoScalingConfigurationAttributes {
52+
/**
53+
* The name of the Auto Scaling Configuration.
54+
*/
55+
readonly autoScalingConfigurationName: string;
56+
57+
/**
58+
* The revision of the Auto Scaling Configuration.
59+
*/
60+
readonly autoScalingConfigurationRevision: number;
61+
}
62+
63+
/**
64+
* Represents the App Runner Auto Scaling Configuration.
65+
*/
66+
export interface IAutoScalingConfiguration extends cdk.IResource {
67+
/**
68+
* The ARN of the Auto Scaling Configuration.
69+
* @attribute
70+
*/
71+
readonly autoScalingConfigurationArn: string;
72+
73+
/**
74+
* The Name of the Auto Scaling Configuration.
75+
* @attribute
76+
*/
77+
readonly autoScalingConfigurationName: string;
78+
79+
/**
80+
* The revision of the Auto Scaling Configuration.
81+
* @attribute
82+
*/
83+
readonly autoScalingConfigurationRevision: number;
84+
}
85+
86+
/**
87+
* The App Runner Auto Scaling Configuration.
88+
*
89+
* @resource AWS::AppRunner::AutoScalingConfiguration
90+
*/
91+
export class AutoScalingConfiguration extends cdk.Resource implements IAutoScalingConfiguration {
92+
/**
93+
* Imports an App Runner Auto Scaling Configuration from attributes
94+
*/
95+
public static fromAutoScalingConfigurationAttributes(scope: Construct, id: string,
96+
attrs: AutoScalingConfigurationAttributes): IAutoScalingConfiguration {
97+
const autoScalingConfigurationName = attrs.autoScalingConfigurationName;
98+
const autoScalingConfigurationRevision = attrs.autoScalingConfigurationRevision;
99+
100+
class Import extends cdk.Resource implements IAutoScalingConfiguration {
101+
public readonly autoScalingConfigurationName = autoScalingConfigurationName;
102+
public readonly autoScalingConfigurationRevision = autoScalingConfigurationRevision;
103+
public readonly autoScalingConfigurationArn = cdk.Stack.of(this).formatArn({
104+
resource: 'autoscalingconfiguration',
105+
service: 'apprunner',
106+
resourceName: `${attrs.autoScalingConfigurationName}/${attrs.autoScalingConfigurationRevision}`,
107+
});
108+
}
109+
110+
return new Import(scope, id);
111+
}
112+
113+
/**
114+
* Imports an App Runner Auto Scaling Configuration from its ARN
115+
*/
116+
public static fromArn(scope: Construct, id: string, autoScalingConfigurationArn: string): IAutoScalingConfiguration {
117+
const resourceParts = cdk.Fn.split('/', autoScalingConfigurationArn);
118+
119+
if (!resourceParts || resourceParts.length < 3) {
120+
throw new Error(`Unexpected ARN format: ${autoScalingConfigurationArn}`);
121+
}
122+
123+
const autoScalingConfigurationName = cdk.Fn.select(0, resourceParts);
124+
const autoScalingConfigurationRevision = Number(cdk.Fn.select(1, resourceParts));
125+
126+
class Import extends cdk.Resource implements IAutoScalingConfiguration {
127+
public readonly autoScalingConfigurationName = autoScalingConfigurationName;
128+
public readonly autoScalingConfigurationRevision = autoScalingConfigurationRevision;
129+
public readonly autoScalingConfigurationArn = autoScalingConfigurationArn;
130+
}
131+
132+
return new Import(scope, id);
133+
}
134+
135+
/**
136+
* The ARN of the Auto Scaling Configuration.
137+
* @attribute
138+
*/
139+
readonly autoScalingConfigurationArn: string;
140+
141+
/**
142+
* The name of the Auto Scaling Configuration.
143+
* @attribute
144+
*/
145+
readonly autoScalingConfigurationName: string;
146+
147+
/**
148+
* The revision of the Auto Scaling Configuration.
149+
* @attribute
150+
*/
151+
readonly autoScalingConfigurationRevision: number;
152+
153+
public constructor(scope: Construct, id: string, props: AutoScalingConfigurationProps = {}) {
154+
super(scope, id, {
155+
physicalName: props.autoScalingConfigurationName,
156+
});
157+
158+
this.validateAutoScalingConfiguration(props);
159+
160+
const resource = new CfnAutoScalingConfiguration(this, 'Resource', {
161+
autoScalingConfigurationName: props.autoScalingConfigurationName,
162+
maxConcurrency: props.maxConcurrency,
163+
maxSize: props.maxSize,
164+
minSize: props.minSize,
165+
});
166+
167+
this.autoScalingConfigurationArn = resource.attrAutoScalingConfigurationArn;
168+
this.autoScalingConfigurationRevision = resource.attrAutoScalingConfigurationRevision;
169+
this.autoScalingConfigurationName = resource.ref;
170+
}
171+
172+
private validateAutoScalingConfiguration(props: AutoScalingConfigurationProps) {
173+
if (
174+
props.autoScalingConfigurationName !== undefined &&
175+
!cdk.Token.isUnresolved(props.autoScalingConfigurationName) &&
176+
!/^[A-Za-z0-9][A-Za-z0-9\-_]{3,31}$/.test(props.autoScalingConfigurationName)
177+
) {
178+
throw new Error(`autoScalingConfigurationName must match the ^[A-Za-z0-9][A-Za-z0-9\-_]{3,31}$ pattern, got ${props.autoScalingConfigurationName}`);
179+
}
180+
181+
const isMinSizeDefined = typeof props.minSize === 'number';
182+
const isMaxSizeDefined = typeof props.maxSize === 'number';
183+
const isMaxConcurrencyDefined = typeof props.maxConcurrency === 'number';
184+
185+
if (isMinSizeDefined && (props.minSize < 1 || props.minSize > 25)) {
186+
throw new Error(`minSize must be between 1 and 25, got ${props.minSize}`);
187+
}
188+
189+
if (isMaxSizeDefined && (props.maxSize < 1 || props.maxSize > 25)) {
190+
throw new Error(`maxSize must be between 1 and 25, got ${props.maxSize}`);
191+
}
192+
193+
if (isMinSizeDefined && isMaxSizeDefined && !(props.minSize < props.maxSize)) {
194+
throw new Error('maxSize must be greater than minSize');
195+
}
196+
197+
if (isMaxConcurrencyDefined && (props.maxConcurrency < 1 || props.maxConcurrency > 200)) {
198+
throw new Error(`maxConcurrency must be between 1 and 200, got ${props.maxConcurrency}`);
199+
}
200+
}
201+
202+
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
// AWS::AppRunner CloudFormation Resources:
2+
export * from './auto-scaling-configuration';
23
export * from './service';
34
export * from './vpc-connector';

packages/@aws-cdk/aws-apprunner-alpha/lib/service.ts

+14
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Lazy } from 'aws-cdk-lib/core';
99
import { Construct } from 'constructs';
1010
import { CfnService } from 'aws-cdk-lib/aws-apprunner';
1111
import { IVpcConnector } from './vpc-connector';
12+
import { IAutoScalingConfiguration } from './auto-scaling-configuration';
1213

1314
/**
1415
* The image repository types
@@ -656,6 +657,18 @@ export interface ServiceProps {
656657
*/
657658
readonly autoDeploymentsEnabled?: boolean;
658659

660+
/**
661+
* Specifies an App Runner Auto Scaling Configuration.
662+
*
663+
* A default configuration is either the AWS recommended configuration,
664+
* or the configuration you set as the default.
665+
*
666+
* @see https://docs.aws.amazon.com/apprunner/latest/dg/manage-autoscaling.html
667+
*
668+
* @default - the latest revision of a default auto scaling configuration is used.
669+
*/
670+
readonly autoScalingConfiguration?: IAutoScalingConfiguration;
671+
659672
/**
660673
* The number of CPU units reserved for each instance of your App Runner service.
661674
*
@@ -1272,6 +1285,7 @@ export class Service extends cdk.Resource implements iam.IGrantable {
12721285
encryptionConfiguration: this.props.kmsKey ? {
12731286
kmsKey: this.props.kmsKey.keyArn,
12741287
} : undefined,
1288+
autoScalingConfigurationArn: this.props.autoScalingConfiguration?.autoScalingConfigurationArn,
12751289
networkConfiguration: {
12761290
egressConfiguration: {
12771291
egressType: this.props.vpcConnector ? 'VPC' : 'DEFAULT',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { Match, Template } from 'aws-cdk-lib/assertions';
2+
import * as cdk from 'aws-cdk-lib';
3+
import { AutoScalingConfiguration } from '../lib';
4+
5+
let stack: cdk.Stack;
6+
beforeEach(() => {
7+
stack = new cdk.Stack();
8+
});
9+
10+
test.each([
11+
['MyAutoScalingConfiguration'],
12+
['my-autoscaling-configuration_1'],
13+
])('create an Auto scaling Configuration with all properties (name: %s)', (autoScalingConfigurationName: string) => {
14+
// WHEN
15+
new AutoScalingConfiguration(stack, 'AutoScalingConfiguration', {
16+
autoScalingConfigurationName,
17+
maxConcurrency: 150,
18+
maxSize: 20,
19+
minSize: 5,
20+
});
21+
22+
// THEN
23+
Template.fromStack(stack).hasResourceProperties('AWS::AppRunner::AutoScalingConfiguration', {
24+
AutoScalingConfigurationName: autoScalingConfigurationName,
25+
MaxConcurrency: 150,
26+
MaxSize: 20,
27+
MinSize: 5,
28+
});
29+
});
30+
31+
test('create an Auto scaling Configuration without all properties', () => {
32+
// WHEN
33+
new AutoScalingConfiguration(stack, 'AutoScalingConfiguration');
34+
35+
// THEN
36+
Template.fromStack(stack).hasResourceProperties('AWS::AppRunner::AutoScalingConfiguration', {
37+
AutoScalingConfigurationName: Match.absent(),
38+
MaxConcurrency: Match.absent(),
39+
MaxSize: Match.absent(),
40+
MinSize: Match.absent(),
41+
});
42+
});
43+
44+
test.each([-1, 0, 26])('invalid minSize', (minSize: number) => {
45+
expect(() => {
46+
new AutoScalingConfiguration(stack, 'AutoScalingConfiguration', {
47+
minSize,
48+
});
49+
}).toThrow(`minSize must be between 1 and 25, got ${minSize}`);
50+
});
51+
52+
test.each([0, 26])('invalid maxSize', (maxSize: number) => {
53+
expect(() => {
54+
new AutoScalingConfiguration(stack, 'AutoScalingConfiguration', {
55+
maxSize,
56+
});
57+
}).toThrow(`maxSize must be between 1 and 25, got ${maxSize}`);
58+
});
59+
60+
test('minSize greater than maxSize', () => {
61+
expect(() => {
62+
new AutoScalingConfiguration(stack, 'AutoScalingConfiguration', {
63+
minSize: 5,
64+
maxSize: 3,
65+
});
66+
}).toThrow('maxSize must be greater than minSize');
67+
});
68+
69+
test.each([0, 201])('invalid maxConcurrency', (maxConcurrency: number) => {
70+
expect(() => {
71+
new AutoScalingConfiguration(stack, 'AutoScalingConfiguration', {
72+
maxConcurrency,
73+
});
74+
}).toThrow(`maxConcurrency must be between 1 and 200, got ${maxConcurrency}`);
75+
});
76+
77+
test.each([
78+
['tes'],
79+
['test-autoscaling-configuration-name-over-limitation'],
80+
['-test'],
81+
['test-?'],
82+
])('invalid autoScalingConfigurationName (name: %s)', (autoScalingConfigurationName: string) => {
83+
expect(() => {
84+
new AutoScalingConfiguration(stack, 'AutoScalingConfiguration', {
85+
autoScalingConfigurationName,
86+
});
87+
}).toThrow(`autoScalingConfigurationName must match the ^[A-Za-z0-9][A-Za-z0-9\-_]{3,31}$ pattern, got ${autoScalingConfigurationName}`);
88+
});
89+
90+
test('create an Auto scaling Configuration with tags', () => {
91+
// WHEN
92+
const autoScalingConfiguration = new AutoScalingConfiguration(stack, 'AutoScalingConfiguration', {
93+
autoScalingConfigurationName: 'my-autoscaling-config',
94+
maxConcurrency: 150,
95+
maxSize: 20,
96+
minSize: 5,
97+
});
98+
99+
cdk.Tags.of(autoScalingConfiguration).add('Environment', 'production');
100+
101+
// THEN
102+
Template.fromStack(stack).hasResourceProperties('AWS::AppRunner::AutoScalingConfiguration', {
103+
Tags: [
104+
{
105+
Key: 'Environment',
106+
Value: 'production',
107+
},
108+
],
109+
});
110+
});

0 commit comments

Comments
 (0)