Skip to content

Commit

Permalink
feat(apprunner): add AutoScalingConfiguration for AppRunner Service (#…
Browse files Browse the repository at this point in the history
…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*
  • Loading branch information
mazyu36 committed Jun 21, 2024
1 parent f2c5f68 commit a598508
Show file tree
Hide file tree
Showing 15 changed files with 1,020 additions and 0 deletions.
21 changes: 21 additions & 0 deletions packages/@aws-cdk/aws-apprunner-alpha/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,27 @@ when required.

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.

## Auto Scaling Configuration

To associate an App Runner service with a custom Auto Scaling Configuration, define `autoScalingConfiguration` for the service.

```ts
const autoScalingConfiguration = new apprunner.AutoScalingConfiguration(this, 'AutoScalingConfiguration', {
autoScalingConfigurationName: 'MyAutoScalingConfiguration',
maxConcurrency: 150,
maxSize: 20,
minSize: 5,
});

new apprunner.Service(this, 'DemoService', {
source: apprunner.Source.fromEcrPublic({
imageConfiguration: { port: 8000 },
imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest',
}),
autoScalingConfiguration,
});
```

## VPC Connector

To associate an App Runner service with a custom VPC, define `vpcConnector` for the service.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import * as cdk from 'aws-cdk-lib/core';
import { Construct } from 'constructs';
import { CfnAutoScalingConfiguration } from 'aws-cdk-lib/aws-apprunner';

/**
* Properties of the App Runner Auto Scaling Configuration.
*/
export interface AutoScalingConfigurationProps {
/**
* The name for the Auto Scaling Configuration.
*
* @default - a name generated by CloudFormation
*/
readonly autoScalingConfigurationName?: string;

/**
* The maximum number of concurrent requests that an instance processes.
* If the number of concurrent requests exceeds this limit, App Runner scales the service up.
*
* Must be between 1 and 200.
*
* @default 100
*/
readonly maxConcurrency?: number;

/**
* The maximum number of instances that a service scales up to.
* At most maxSize instances actively serve traffic for your service.
*
* Must be between 1 and 25.
*
* @default 25
*/
readonly maxSize?: number;

/**
* The minimum number of instances that App Runner provisions for a service.
* The service always has at least minSize provisioned instances.
*
*
* Must be between 1 and 25.
*
* @default 1
*/
readonly minSize?: number;
}

/**
* Attributes for the App Runner Auto Scaling Configuration.
*/
export interface AutoScalingConfigurationAttributes {
/**
* The name of the Auto Scaling Configuration.
*/
readonly autoScalingConfigurationName: string;

/**
* The revision of the Auto Scaling Configuration.
*/
readonly autoScalingConfigurationRevision: number;
}

/**
* Represents the App Runner Auto Scaling Configuration.
*/
export interface IAutoScalingConfiguration extends cdk.IResource {
/**
* The ARN of the Auto Scaling Configuration.
* @attribute
*/
readonly autoScalingConfigurationArn: string;

/**
* The Name of the Auto Scaling Configuration.
* @attribute
*/
readonly autoScalingConfigurationName: string;

/**
* The revision of the Auto Scaling Configuration.
* @attribute
*/
readonly autoScalingConfigurationRevision: number;
}

/**
* The App Runner Auto Scaling Configuration.
*
* @resource AWS::AppRunner::AutoScalingConfiguration
*/
export class AutoScalingConfiguration extends cdk.Resource implements IAutoScalingConfiguration {
/**
* Imports an App Runner Auto Scaling Configuration from attributes
*/
public static fromAutoScalingConfigurationAttributes(scope: Construct, id: string,
attrs: AutoScalingConfigurationAttributes): IAutoScalingConfiguration {
const autoScalingConfigurationName = attrs.autoScalingConfigurationName;
const autoScalingConfigurationRevision = attrs.autoScalingConfigurationRevision;

class Import extends cdk.Resource implements IAutoScalingConfiguration {
public readonly autoScalingConfigurationName = autoScalingConfigurationName;
public readonly autoScalingConfigurationRevision = autoScalingConfigurationRevision;
public readonly autoScalingConfigurationArn = cdk.Stack.of(this).formatArn({
resource: 'autoscalingconfiguration',
service: 'apprunner',
resourceName: `${attrs.autoScalingConfigurationName}/${attrs.autoScalingConfigurationRevision}`,
});
}

return new Import(scope, id);
}

/**
* Imports an App Runner Auto Scaling Configuration from its ARN
*/
public static fromArn(scope: Construct, id: string, autoScalingConfigurationArn: string): IAutoScalingConfiguration {
const resourceParts = cdk.Fn.split('/', autoScalingConfigurationArn);

if (!resourceParts || resourceParts.length < 3) {
throw new Error(`Unexpected ARN format: ${autoScalingConfigurationArn}`);
}

const autoScalingConfigurationName = cdk.Fn.select(0, resourceParts);
const autoScalingConfigurationRevision = Number(cdk.Fn.select(1, resourceParts));

class Import extends cdk.Resource implements IAutoScalingConfiguration {
public readonly autoScalingConfigurationName = autoScalingConfigurationName;
public readonly autoScalingConfigurationRevision = autoScalingConfigurationRevision;
public readonly autoScalingConfigurationArn = autoScalingConfigurationArn;
}

return new Import(scope, id);
}

/**
* The ARN of the Auto Scaling Configuration.
* @attribute
*/
readonly autoScalingConfigurationArn: string;

/**
* The name of the Auto Scaling Configuration.
* @attribute
*/
readonly autoScalingConfigurationName: string;

/**
* The revision of the Auto Scaling Configuration.
* @attribute
*/
readonly autoScalingConfigurationRevision: number;

public constructor(scope: Construct, id: string, props: AutoScalingConfigurationProps = {}) {
super(scope, id, {
physicalName: props.autoScalingConfigurationName,
});

this.validateAutoScalingConfiguration(props);

const resource = new CfnAutoScalingConfiguration(this, 'Resource', {
autoScalingConfigurationName: props.autoScalingConfigurationName,
maxConcurrency: props.maxConcurrency,
maxSize: props.maxSize,
minSize: props.minSize,
});

this.autoScalingConfigurationArn = resource.attrAutoScalingConfigurationArn;
this.autoScalingConfigurationRevision = resource.attrAutoScalingConfigurationRevision;
this.autoScalingConfigurationName = resource.ref;
}

private validateAutoScalingConfiguration(props: AutoScalingConfigurationProps) {
if (
props.autoScalingConfigurationName !== undefined &&
!cdk.Token.isUnresolved(props.autoScalingConfigurationName) &&
!/^[A-Za-z0-9][A-Za-z0-9\-_]{3,31}$/.test(props.autoScalingConfigurationName)
) {
throw new Error(`autoScalingConfigurationName must match the ^[A-Za-z0-9][A-Za-z0-9\-_]{3,31}$ pattern, got ${props.autoScalingConfigurationName}`);
}

const isMinSizeDefined = typeof props.minSize === 'number';
const isMaxSizeDefined = typeof props.maxSize === 'number';
const isMaxConcurrencyDefined = typeof props.maxConcurrency === 'number';

if (isMinSizeDefined && (props.minSize < 1 || props.minSize > 25)) {
throw new Error(`minSize must be between 1 and 25, got ${props.minSize}`);
}

if (isMaxSizeDefined && (props.maxSize < 1 || props.maxSize > 25)) {
throw new Error(`maxSize must be between 1 and 25, got ${props.maxSize}`);
}

if (isMinSizeDefined && isMaxSizeDefined && !(props.minSize < props.maxSize)) {
throw new Error('maxSize must be greater than minSize');
}

if (isMaxConcurrencyDefined && (props.maxConcurrency < 1 || props.maxConcurrency > 200)) {
throw new Error(`maxConcurrency must be between 1 and 200, got ${props.maxConcurrency}`);
}
}

}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-apprunner-alpha/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// AWS::AppRunner CloudFormation Resources:
export * from './auto-scaling-configuration';
export * from './service';
export * from './vpc-connector';
14 changes: 14 additions & 0 deletions packages/@aws-cdk/aws-apprunner-alpha/lib/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Lazy } from 'aws-cdk-lib/core';
import { Construct } from 'constructs';
import { CfnService } from 'aws-cdk-lib/aws-apprunner';
import { IVpcConnector } from './vpc-connector';
import { IAutoScalingConfiguration } from './auto-scaling-configuration';

/**
* The image repository types
Expand Down Expand Up @@ -656,6 +657,18 @@ export interface ServiceProps {
*/
readonly autoDeploymentsEnabled?: boolean;

/**
* Specifies an App Runner Auto Scaling Configuration.
*
* A default configuration is either the AWS recommended configuration,
* or the configuration you set as the default.
*
* @see https://docs.aws.amazon.com/apprunner/latest/dg/manage-autoscaling.html
*
* @default - the latest revision of a default auto scaling configuration is used.
*/
readonly autoScalingConfiguration?: IAutoScalingConfiguration;

/**
* The number of CPU units reserved for each instance of your App Runner service.
*
Expand Down Expand Up @@ -1272,6 +1285,7 @@ export class Service extends cdk.Resource implements iam.IGrantable {
encryptionConfiguration: this.props.kmsKey ? {
kmsKey: this.props.kmsKey.keyArn,
} : undefined,
autoScalingConfigurationArn: this.props.autoScalingConfiguration?.autoScalingConfigurationArn,
networkConfiguration: {
egressConfiguration: {
egressType: this.props.vpcConnector ? 'VPC' : 'DEFAULT',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { Match, Template } from 'aws-cdk-lib/assertions';
import * as cdk from 'aws-cdk-lib';
import { AutoScalingConfiguration } from '../lib';

let stack: cdk.Stack;
beforeEach(() => {
stack = new cdk.Stack();
});

test.each([
['MyAutoScalingConfiguration'],
['my-autoscaling-configuration_1'],
])('create an Auto scaling Configuration with all properties (name: %s)', (autoScalingConfigurationName: string) => {
// WHEN
new AutoScalingConfiguration(stack, 'AutoScalingConfiguration', {
autoScalingConfigurationName,
maxConcurrency: 150,
maxSize: 20,
minSize: 5,
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::AppRunner::AutoScalingConfiguration', {
AutoScalingConfigurationName: autoScalingConfigurationName,
MaxConcurrency: 150,
MaxSize: 20,
MinSize: 5,
});
});

test('create an Auto scaling Configuration without all properties', () => {
// WHEN
new AutoScalingConfiguration(stack, 'AutoScalingConfiguration');

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::AppRunner::AutoScalingConfiguration', {
AutoScalingConfigurationName: Match.absent(),
MaxConcurrency: Match.absent(),
MaxSize: Match.absent(),
MinSize: Match.absent(),
});
});

test.each([-1, 0, 26])('invalid minSize', (minSize: number) => {
expect(() => {
new AutoScalingConfiguration(stack, 'AutoScalingConfiguration', {
minSize,
});
}).toThrow(`minSize must be between 1 and 25, got ${minSize}`);
});

test.each([0, 26])('invalid maxSize', (maxSize: number) => {
expect(() => {
new AutoScalingConfiguration(stack, 'AutoScalingConfiguration', {
maxSize,
});
}).toThrow(`maxSize must be between 1 and 25, got ${maxSize}`);
});

test('minSize greater than maxSize', () => {
expect(() => {
new AutoScalingConfiguration(stack, 'AutoScalingConfiguration', {
minSize: 5,
maxSize: 3,
});
}).toThrow('maxSize must be greater than minSize');
});

test.each([0, 201])('invalid maxConcurrency', (maxConcurrency: number) => {
expect(() => {
new AutoScalingConfiguration(stack, 'AutoScalingConfiguration', {
maxConcurrency,
});
}).toThrow(`maxConcurrency must be between 1 and 200, got ${maxConcurrency}`);
});

test.each([
['tes'],
['test-autoscaling-configuration-name-over-limitation'],
['-test'],
['test-?'],
])('invalid autoScalingConfigurationName (name: %s)', (autoScalingConfigurationName: string) => {
expect(() => {
new AutoScalingConfiguration(stack, 'AutoScalingConfiguration', {
autoScalingConfigurationName,
});
}).toThrow(`autoScalingConfigurationName must match the ^[A-Za-z0-9][A-Za-z0-9\-_]{3,31}$ pattern, got ${autoScalingConfigurationName}`);
});

test('create an Auto scaling Configuration with tags', () => {
// WHEN
const autoScalingConfiguration = new AutoScalingConfiguration(stack, 'AutoScalingConfiguration', {
autoScalingConfigurationName: 'my-autoscaling-config',
maxConcurrency: 150,
maxSize: 20,
minSize: 5,
});

cdk.Tags.of(autoScalingConfiguration).add('Environment', 'production');

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::AppRunner::AutoScalingConfiguration', {
Tags: [
{
Key: 'Environment',
Value: 'production',
},
],
});
});
Loading

0 comments on commit a598508

Please sign in to comment.