Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(apprunner): add AutoScalingConfiguration for AppRunner Service #30358

Merged
merged 24 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0a5114b
feat: add autoscaling for apprunner
mazyu36 May 28, 2024
bdb85cb
fix: add @default
mazyu36 May 28, 2024
4ada3f6
feat: add fromArn method
mazyu36 May 28, 2024
7e2c80f
Merge branch 'main' into apprunner-auto-scaling-30353
mazyu36 May 28, 2024
7ed5abc
Merge branch 'main' into apprunner-auto-scaling-30353
mazyu36 May 30, 2024
61a1b00
Update packages/@aws-cdk/aws-apprunner-alpha/test/service.test.ts
mazyu36 Jun 2, 2024
84a3cce
Update packages/@aws-cdk/aws-apprunner-alpha/lib/auto-scaling-configu…
mazyu36 Jun 2, 2024
55501a7
Update packages/@aws-cdk/aws-apprunner-alpha/lib/auto-scaling-configu…
mazyu36 Jun 2, 2024
e999f83
Update packages/@aws-cdk/aws-apprunner-alpha/lib/service.ts
mazyu36 Jun 2, 2024
59a6f32
Update packages/@aws-cdk/aws-apprunner-alpha/test/auto-scaling-config…
mazyu36 Jun 2, 2024
215e5a9
Update packages/@aws-cdk/aws-apprunner-alpha/lib/auto-scaling-configu…
mazyu36 Jun 2, 2024
b161ed7
fix: incorporate review comments
mazyu36 Jun 2, 2024
50fc0d3
Update packages/@aws-cdk/aws-apprunner-alpha/lib/auto-scaling-configu…
mazyu36 Jun 2, 2024
0cac270
Update packages/@aws-cdk/aws-apprunner-alpha/lib/auto-scaling-configu…
mazyu36 Jun 2, 2024
f46a7c7
fix: unit tests
mazyu36 Jun 2, 2024
9bb0dcb
Update packages/@aws-cdk/aws-apprunner-alpha/lib/auto-scaling-configu…
mazyu36 Jun 3, 2024
3ca5979
Merge branch 'main' into apprunner-auto-scaling-30353
mazyu36 Jun 11, 2024
2c74142
Update packages/@aws-cdk/aws-apprunner-alpha/lib/auto-scaling-configu…
mazyu36 Jun 19, 2024
0aef49e
fix: incorporate review comments
mazyu36 Jun 19, 2024
361ab63
Update packages/@aws-cdk/aws-apprunner-alpha/test/auto-scaling-config…
mazyu36 Jun 19, 2024
b8c7d61
Update packages/@aws-cdk/aws-apprunner-alpha/test/auto-scaling-config…
mazyu36 Jun 19, 2024
2633a0d
fix: incorporate review comments
mazyu36 Jun 19, 2024
2c5e154
Merge branch 'main' into apprunner-auto-scaling-30353
mergify[bot] Jun 20, 2024
5a88d1d
Merge branch 'main' into apprunner-auto-scaling-30353
mergify[bot] Jun 21, 2024
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
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we allow to specify tags as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the other L2 Constructs, it seemed like there were few resources adding tags as properties, so I didn't add them.
Since tags can also be added through aspects, I thought they might be unnecessary like other resources. What do you think?
I would appreciate your opinion on the criteria for adding tags, as I'm not sure about it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense 👍 Thanks for clarifying!

/**
* 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);
}

Copy link
Contributor

@pahud pahud Jun 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we just know the Name and Revision, we can formate the Arn string.

How about just have Name and Revision in the interface:

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

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

And in the fromXxx method:

public static fromAutoScalingConfigurationAttributes(scope: Construct, id: string,
    attrs: AutoScalingConfigurationAttributes): IAutoScalingConfiguration {

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

If we can generate the Arn from given Name and Revision then Arn would not be required in the Attributes?

Think about it, if user already knows the Arn, why should they bother to use fromXxxAttributes instead of just fromArn?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. You are certainly right. I have removed arn from the attributes and made the correction.

/**
* 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we probably should make it string. Just like ecs task definition.

public readonly revision: string;

Also in the doc

AutoScalingConfigurationRevision
The revision of this auto scaling configuration. It's unique among all the active configurations that share the same AutoScalingConfigurationName.

It's unclear to me if it would be number. I am afraid some day it might support revision like latest just like ecs task revision and this would be a breaking change in CDK.

With that being said, I don't see the benefits using number over string. wdyt?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. Personally, I'm in favor of making the revision a string.
However, I have some concerns.

The revision in the Cfn (L1 construct) is a number, but would it be appropriate to change it to a string in the L2 construct? I'm not sure if there's a possibility of it changing, so if you know anything about it, please let me know.

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apprunner.CfnAutoScalingConfiguration.html#attrautoscalingconfigurationrevision

Additionally, since the already implemented VpcConnector treats the revision as a number, I'm also concerned about having a different approach from that.

https://docs.aws.amazon.com/cdk/api/v2/docs/@aws-cdk_aws-apprunner-alpha.VpcConnector.html#vpcconnectorrevisionspan-classapi-icon-api-icon-experimental-titlethis-api-element-is-experimental-it-may-change-without-noticespan

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the callout.

I checked the cfnspec and yes the type is Integer, which is number in TS.

"Attributes": {
        "AutoScalingConfigurationRevision": {
          "PrimitiveType": "Integer"
        },

I am fine having it as number for consistency here but I kind of feel if apprunner supports latest revision someday, which would be a breaking change to CFN then CDK would have breaking change as well.

@GavinZZ @paulhcsun what do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed with team internally. I think we don't need to over-optimize the type here. CFN schema type changes are not supposed to happen especially after it's already in CloudFormation resource schema registry.


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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also add validation for the autoScalingConfigurationName property (docs)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I added validation and unit tests.

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;
Copy link
Contributor

@pahud pahud Jun 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer just autoScaling?: IAutoScalingConfiguration; but also OK with autoScalingConfiguration.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. I will keep it as is, in the form of aligning with the class names or property names of the L1 construct.


/**
* 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}`);
});
mazyu36 marked this conversation as resolved.
Show resolved Hide resolved

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