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 HealthCheckConfiguration property in Service #27029

Merged
merged 15 commits into from
Sep 27, 2023
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 @@ -215,3 +215,24 @@ const service = new apprunner.Service(stack, 'Service', {

service.addSecret('LATER_SECRET', apprunner.Secret.fromSecretsManager(secret, 'field'));
```

## HealthCheck Configuration

To configure the health check for the service, use the `healthCheckConfiguration` attribute.

```ts
new apprunner.Service(this, 'Service', {
source: apprunner.Source.fromEcrPublic({
imageConfiguration: { port: 8000 },
imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest',
}),
healthCheckConfiguration: {
healthyThreshold: 5,
interval: Duration.seconds(10),
path: '/',
protocol: apprunner.HealthCheckProtocolType.HTTP,
timeout: Duration.seconds(10),
unhealthyThreshold: 10,
},
});
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd like us to use an enum like class for this, because HTTP and TCP health checks have different props:

Suggested change
new apprunner.Service(this, 'Service', {
source: apprunner.Source.fromEcrPublic({
imageConfiguration: { port: 8000 },
imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest',
}),
healthCheckConfiguration: {
healthyThreshold: 5,
interval: Duration.seconds(10),
path: '/',
protocol: apprunner.HealthCheckProtocolType.HTTP,
timeout: Duration.seconds(10),
unhealthyThreshold: 10,
},
});
new apprunner.Service(this, 'Service', {
source: apprunner.Source.fromEcrPublic({
imageConfiguration: { port: 8000 },
imageIdentifier: 'public.ecr.aws/aws-containers/hello-app-runner:latest',
}),
healthCheck: apprunner.HealthCheck.http({
healthyThreshold: 5,
interval: Duration.seconds(10),
path: '/',
timeout: Duration.seconds(10),
unhealthyThreshold: 10,
}),
});

Have a look at appmesh.HealthCheck

Copy link
Contributor Author

@go-to-k go-to-k Sep 23, 2023

Choose a reason for hiding this comment

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

@mrgrain

I changed to use the HealthCheck class like appmesh.HealthCheck.

d0cd255

70c125e

```
116 changes: 116 additions & 0 deletions packages/@aws-cdk/aws-apprunner-alpha/lib/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,13 @@ export interface ServiceProps {
* @default - no VPC connector, uses the DEFAULT egress type instead
*/
readonly vpcConnector?: IVpcConnector;

/**
* Settings for the health check that AWS App Runner performs to monitor the health of a service.
*
* @default - no health check configuration
*/
readonly healthCheckConfiguration?: HealthCheckConfiguration;
}

/**
Expand Down Expand Up @@ -848,6 +855,72 @@ export class GitHubConnection {
}
}

/**
* The health check protocol type
*/
export enum HealthCheckProtocolType {
/**
* HTTP protocol
*/
HTTP = 'HTTP',

/**
* TCP protocol
*/
TCP = 'TCP',
}

/**
* Describes the settings for the health check that AWS App Runner performs to monitor the health of a service.
*/
export interface HealthCheckConfiguration {
/**
* The number of consecutive checks that must succeed before App Runner decides that the service is healthy.
*
* @default 1
*/
readonly healthyThreshold?: number;

/**
* The time interval, in seconds, between health checks.
*
* @default Duration.seconds(5)
*/
readonly interval?: cdk.Duration;

/**
* The URL that health check requests are sent to.
*
* `path` is only applicable when you set `protocol` to `HTTP`.
*
* @default /
*/
readonly path?: string;

/**
* The IP protocol that App Runner uses to perform health checks for your service.
*
* If you set `protocol` to `HTTP`, App Runner sends health check requests to the HTTP path specified by `path`.
*
* @default HealthCheckProtocolType.TCP
*/
readonly protocol?: HealthCheckProtocolType;

/**
* The time, in seconds, to wait for a health check response before deciding it failed.
*
* @default Duration.seconds(2)
*/
readonly timeout?: cdk.Duration;

/**
* The number of consecutive checks that must fail before App Runner decides that the service is unhealthy.
*
* @default 5
*/
readonly unhealthyThreshold?: number;
}

/**
* Attributes for the App Runner Service
*/
Expand Down Expand Up @@ -1074,6 +1147,35 @@ export class Service extends cdk.Resource implements iam.IGrantable {
throw new Error('configurationValues cannot be provided if the ConfigurationSource is Repository');
}

if (this.props.healthCheckConfiguration?.path !== undefined) {
Copy link
Contributor

@lpizzinidev lpizzinidev Sep 7, 2023

Choose a reason for hiding this comment

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

The implementation is correct.
We should create a separate function to handle the logic to keep the constructor cleaner, something like:

private validateHealthCheckConfiguration(healthCheckConfiguration?: HealthCheckConfiguration) {
    // Add guard to avoid using ? on every successive control
    if (!healthCheckConfiguration) return; 
    // Validate parameters...
}

if (this.props.healthCheckConfiguration?.protocol !== HealthCheckProtocolType.HTTP) {
throw new Error('path is only applicable when you set Protocol to HTTP');
}
if (this.props.healthCheckConfiguration?.path.length === 0) {
throw new Error('path length must be greater than 0');
}
}
if (this.props.healthCheckConfiguration?.healthyThreshold !== undefined) {
if (this.props.healthCheckConfiguration?.healthyThreshold < 1 || this.props.healthCheckConfiguration?.healthyThreshold > 20) {
throw new Error('healthyThreshold must be greater than or equal to 1 and less than or equal to 20');
Copy link
Contributor

Choose a reason for hiding this comment

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

We should add the received value in the error message as a feedback to the user and make the message more concise:

throw new Error(`healthyThreshold must be between 1 and 20, got ${healthCheckConfiguration.healthyThreshold}`);

}
}
if (this.props.healthCheckConfiguration?.unhealthyThreshold !== undefined) {
if (this.props.healthCheckConfiguration?.unhealthyThreshold < 1 || this.props.healthCheckConfiguration?.unhealthyThreshold > 20) {
throw new Error('unhealthyThreshold must be greater than or equal to 1 and less than or equal to 20');
}
}
if (this.props.healthCheckConfiguration?.interval !== undefined) {
if (this.props.healthCheckConfiguration?.interval.toSeconds() < 1 || this.props.healthCheckConfiguration?.interval.toSeconds() > 20) {
throw new Error('interval must be greater than or equal to 1 and less than or equal to 20');
}
}
if (this.props.healthCheckConfiguration?.timeout !== undefined) {
if (this.props.healthCheckConfiguration?.timeout.toSeconds() < 1 || this.props.healthCheckConfiguration?.timeout.toSeconds() > 20) {
throw new Error('timeout must be greater than or equal to 1 and less than or equal to 20');
}
}

const resource = new CfnService(this, 'Resource', {
serviceName: this.props.serviceName,
instanceConfiguration: {
Expand All @@ -1097,6 +1199,9 @@ export class Service extends cdk.Resource implements iam.IGrantable {
vpcConnectorArn: this.props.vpcConnector?.vpcConnectorArn,
},
},
healthCheckConfiguration: this.props.healthCheckConfiguration ?
this.renderHealthCheckConfiguration(this.props.healthCheckConfiguration) :
undefined,
});

// grant required privileges for the role
Expand Down Expand Up @@ -1259,4 +1364,15 @@ export class Service extends cdk.Resource implements iam.IGrantable {
},
});
}

private renderHealthCheckConfiguration(props: HealthCheckConfiguration) {
return {
healthyThreshold: props.healthyThreshold,
interval: props.interval?.toSeconds(),
path: props.path,
protocol: props.protocol,
timeout: props.timeout?.toSeconds(),
unhealthyThreshold: props.unhealthyThreshold,
};
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Fixture with packages imported, but nothing else
import { Stack, SecretValue } from 'aws-cdk-lib';
import { Duration, Stack, SecretValue } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as apprunner from '@aws-cdk/aws-apprunner-alpha';
import * as path from 'path';
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
{
"Resources": {
"ServiceInstanceRoleDFA90CEC": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "tasks.apprunner.amazonaws.com"
}
}
],
"Version": "2012-10-17"
}
}
},
"ServiceDBC79909": {
"Type": "AWS::AppRunner::Service",
"Properties": {
"HealthCheckConfiguration": {
"HealthyThreshold": 5,
"Interval": 10,
"Path": "/",
"Protocol": "HTTP",
"Timeout": 10,
"UnhealthyThreshold": 10
},
"InstanceConfiguration": {
"InstanceRoleArn": {
"Fn::GetAtt": [
"ServiceInstanceRoleDFA90CEC",
"Arn"
]
}
},
"NetworkConfiguration": {
"EgressConfiguration": {
"EgressType": "DEFAULT"
}
},
"SourceConfiguration": {
"AuthenticationConfiguration": {},
"ImageRepository": {
"ImageConfiguration": {
"Port": "8000"
},
"ImageIdentifier": "public.ecr.aws/aws-containers/hello-app-runner:latest",
"ImageRepositoryType": "ECR_PUBLIC"
}
}
}
}
},
"Parameters": {
"BootstrapVersion": {
"Type": "AWS::SSM::Parameter::Value<String>",
"Default": "/cdk-bootstrap/hnb659fds/version",
"Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"
}
},
"Rules": {
"CheckBootstrapVersion": {
"Assertions": [
{
"Assert": {
"Fn::Not": [
{
"Fn::Contains": [
[
"1",
"2",
"3",
"4",
"5"
],
{
"Ref": "BootstrapVersion"
}
]
}
]
},
"AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI."
}
]
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading