feat(AWS ALB): Support health check configuration target groups (#7947)

This commit is contained in:
David Septimus 2020-07-23 03:07:30 -04:00 committed by GitHub
parent 6a7fd44efc
commit a2f977c8ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 334 additions and 1 deletions

View File

@ -195,3 +195,50 @@ functions:
conditions:
path: /hello
```
## Configuring Health Checks
Health checks for target groups with a _lambda_ target type are disabled by default.
To enable the health check on a target group associated with an alb event, set the alb event's `healthCheck` property to `true`.
```yml
functions:
albEventConsumer:
handler: handler.hello
events:
- alb:
listenerArn: arn:aws:elasticloadbalancing:us-east-1:12345:listener/app/my-load-balancer/50dc6c495c0c9188/
priority: 1
conditions:
path: /hello
healthCheck: true
```
If you need to configure advanced health check settings, you can provide additional health check configuration.
```yml
functions:
albEventConsumer:
handler: handler.hello
events:
- alb:
listenerArn: arn:aws:elasticloadbalancing:us-east-1:12345:listener/app/my-load-balancer/50dc6c495c0c9188/
priority: 1
conditions:
path: /hello
healthCheck:
path: /health
intervalSeconds: 35
timeoutSeconds: 30
healthyThresholdCount: 2
unhealthyThresholdCount: 2
matcher:
httpCode: 200,201
```
All advanced health check settings are optional. If any advanced health check settings are present, the target group's health check will be enabled.
The target group's health check will use default values for any undefined settings.
Read the [AWS target group health checks documentation](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/target-group-health-checks.html)
for setting descriptions, constraints, and default values.

View File

@ -408,6 +408,14 @@ functions:
conditions:
host: example.com
path: /hello
healthCheck: # optional, can also be set using a boolean value
path: / # optional
intervalSeconds: 35 # optional
timeoutSeconds: 30 # optional
healthyThresholdCount: 5 # optional
unhealthyThresholdCount: 5 # optional
matcher: # optional
httpCode: '200'
- eventBridge:
# using the default AWS event bus
schedule: rate(10 minutes)

View File

@ -0,0 +1,228 @@
'use strict';
const chai = require('chai');
const runServerless = require('../../../../../../../../tests/utils/run-serverless');
const fixtures = require('../../../../../../../../tests/fixtures');
const { expect } = chai;
const healthCheckDefaults = {
HealthCheckEnabled: false,
HealthCheckPath: '/',
HealthCheckIntervalSeconds: 35,
HealthCheckTimeoutSeconds: 30,
HealthyThresholdCount: 5,
UnhealthyThresholdCount: 5,
Matcher: { HttpCode: '200' },
};
const serverlessConfigurationExtension = {
functions: {
default: {
handler: 'index.handler',
events: [
{
alb: {
listenerArn:
'arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/my-load-balancer/50dc6c495c0c9188/f2f7dc8efc522ab2',
conditions: {
path: '/',
},
priority: 1,
},
},
],
},
enabledTrue: {
handler: 'index.handler',
events: [
{
alb: {
listenerArn:
'arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/my-load-balancer/50dc6c495c0c9188/f2f7dc8efc522ab2',
conditions: {
path: '/',
},
priority: 2,
healthCheck: true,
},
},
],
},
enabledFalse: {
handler: 'index.handler',
events: [
{
alb: {
listenerArn:
'arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/my-load-balancer/50dc6c495c0c9188/f2f7dc8efc522ab2',
conditions: {
path: '/',
},
priority: 3,
healthCheck: false,
},
},
],
},
enabledAdvanced: {
handler: 'index.handler',
events: [
{
alb: {
listenerArn:
'arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/my-load-balancer/50dc6c495c0c9188/f2f7dc8efc522ab2',
conditions: {
path: '/',
},
priority: 4,
healthCheck: {
path: '/health',
intervalSeconds: 70,
timeoutSeconds: 50,
healthyThresholdCount: 7,
unhealthyThresholdCount: 7,
matcher: {
httpCode: '200-299',
},
},
},
},
],
},
enabledAdvancedPartial: {
handler: 'index.handler',
events: [
{
alb: {
listenerArn:
'arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/my-load-balancer/50dc6c495c0c9188/f2f7dc8efc522ab2',
conditions: {
path: '/',
},
priority: 5,
healthCheck: {
path: '/health',
intervalSeconds: 70,
},
},
},
],
},
},
};
describe('ALB TargetGroup Health Checks', () => {
let cfResources;
let naming;
after(fixtures.cleanup);
before(() =>
fixtures
.extend('function', serverlessConfigurationExtension)
.then(fixturePath =>
runServerless({
cwd: fixturePath,
cliArgs: ['package'],
})
)
.then(({ cfTemplate, awsNaming }) => {
({ Resources: cfResources } = cfTemplate);
naming = awsNaming;
})
);
it('should be forcibly reverted to its default state (disabled) if healthCheck is not set', () => {
const albTargetGroupName = naming.getAlbTargetGroupLogicalId('default', '50dc6c495c0c9188');
const targetGroup = cfResources[albTargetGroupName];
expect(targetGroup.Type).to.equal('AWS::ElasticLoadBalancingV2::TargetGroup');
const properties = targetGroup.Properties;
expect(properties.HealthCheckEnabled).to.equal(healthCheckDefaults.HealthCheckEnabled);
expect(properties.HealthCheckPath).to.be.undefined;
expect(properties.HealthCheckIntervalSeconds).to.be.undefined;
expect(properties.HealthCheckTimeoutSeconds).to.be.undefined;
expect(properties.HealthyThresholdCount).to.be.undefined;
expect(properties.UnhealthyThresholdCount).to.be.undefined;
expect(properties.Matcher).to.be.undefined;
});
it('should be disabled when healthCheck is explicitly false', () => {
const albTargetGroupName = naming.getAlbTargetGroupLogicalId(
'enabledFalse',
'50dc6c495c0c9188'
);
const targetGroup = cfResources[albTargetGroupName];
expect(targetGroup.Type).to.equal('AWS::ElasticLoadBalancingV2::TargetGroup');
const properties = targetGroup.Properties;
expect(properties.HealthCheckEnabled).to.be.false;
});
it('should be enabled with default settings when healthCheck is explicitly true', () => {
const albTargetGroupName = naming.getAlbTargetGroupLogicalId('enabledTrue', '50dc6c495c0c9188');
const targetGroup = cfResources[albTargetGroupName];
expect(targetGroup.Type).to.equal('AWS::ElasticLoadBalancingV2::TargetGroup');
const properties = targetGroup.Properties;
expect(properties.HealthCheckEnabled).to.be.true;
expect(properties.HealthCheckPath).to.equal(healthCheckDefaults.HealthCheckPath);
expect(properties.HealthCheckIntervalSeconds).to.equal(
healthCheckDefaults.HealthCheckIntervalSeconds
);
expect(properties.HealthCheckTimeoutSeconds).to.equal(
healthCheckDefaults.HealthCheckTimeoutSeconds
);
expect(properties.HealthyThresholdCount).to.equal(healthCheckDefaults.HealthyThresholdCount);
expect(properties.UnhealthyThresholdCount).to.equal(
healthCheckDefaults.UnhealthyThresholdCount
);
expect(properties.Matcher).to.deep.equal(healthCheckDefaults.Matcher);
});
it('should be enabled with custom settings when healthCheck value is an object', () => {
const albTargetGroupName = naming.getAlbTargetGroupLogicalId(
'enabledAdvanced',
'50dc6c495c0c9188'
);
const targetGroup = cfResources[albTargetGroupName];
expect(targetGroup.Type).to.equal('AWS::ElasticLoadBalancingV2::TargetGroup');
const properties = targetGroup.Properties;
expect(properties.HealthCheckEnabled).to.be.true;
expect(properties.HealthCheckPath).to.equal('/health');
expect(properties.HealthCheckIntervalSeconds).to.equal(70);
expect(properties.HealthCheckTimeoutSeconds).to.equal(50);
expect(properties.HealthyThresholdCount).to.equal(7);
expect(properties.UnhealthyThresholdCount).to.equal(7);
expect(properties.Matcher.HttpCode).to.equal('200-299');
});
it('should use defaults for any undefined advanced settings', () => {
const albTargetGroupName = naming.getAlbTargetGroupLogicalId(
'enabledAdvancedPartial',
'50dc6c495c0c9188'
);
const targetGroup = cfResources[albTargetGroupName];
expect(targetGroup.Type).to.equal('AWS::ElasticLoadBalancingV2::TargetGroup');
const properties = targetGroup.Properties;
expect(properties.HealthCheckEnabled).to.be.true;
expect(properties.HealthCheckPath).to.equal('/health');
expect(properties.HealthCheckIntervalSeconds).to.equal(70);
expect(properties.HealthCheckTimeoutSeconds).to.equal(
healthCheckDefaults.HealthCheckTimeoutSeconds
);
expect(properties.HealthyThresholdCount).to.equal(healthCheckDefaults.HealthyThresholdCount);
expect(properties.UnhealthyThresholdCount).to.equal(
healthCheckDefaults.UnhealthyThresholdCount
);
expect(properties.Matcher).to.deep.equal(healthCheckDefaults.Matcher);
});
});

View File

@ -2,10 +2,20 @@
const resolveLambdaTarget = require('../../../../../utils/resolveLambdaTarget');
const healthCheckDefaults = {
HealthCheckEnabled: false,
HealthCheckPath: '/',
HealthCheckIntervalSeconds: 35,
HealthCheckTimeoutSeconds: 30,
HealthyThresholdCount: 5,
UnhealthyThresholdCount: 5,
Matcher: { HttpCode: '200' },
};
module.exports = {
compileTargetGroups() {
this.validated.events.forEach(event => {
const { functionName, albId, multiValueHeaders = false } = event;
const { functionName, albId, multiValueHeaders = false, healthCheck } = event;
const targetGroupLogicalId = this.provider.naming.getAlbTargetGroupLogicalId(
functionName,
@ -16,6 +26,32 @@ module.exports = {
functionName
);
const healthCheckProperties = { HealthCheckEnabled: healthCheckDefaults.HealthCheckEnabled };
if (healthCheck) {
Object.assign(healthCheckProperties, healthCheckDefaults);
if (healthCheck.enabled != null) {
healthCheckProperties.HealthCheckEnabled = healthCheck.enabled;
}
if (healthCheck.intervalSeconds != null) {
healthCheckProperties.HealthCheckIntervalSeconds = healthCheck.intervalSeconds;
}
if (healthCheck.path != null) {
healthCheckProperties.HealthCheckPath = healthCheck.path;
}
if (healthCheck.timeoutSeconds != null) {
healthCheckProperties.HealthCheckTimeoutSeconds = healthCheck.timeoutSeconds;
}
if (healthCheck.healthyThresholdCount != null) {
healthCheckProperties.HealthyThresholdCount = healthCheck.healthyThresholdCount;
}
if (healthCheck.unhealthyThresholdCount) {
healthCheckProperties.UnhealthyThresholdCount = healthCheck.unhealthyThresholdCount;
}
if (healthCheck.matcher && healthCheck.matcher.httpCode) {
healthCheckProperties.Matcher = { HttpCode: healthCheck.matcher.httpCode };
}
}
const functionObj = this.serverless.service.getFunction(functionName);
const TargetGroup = {
Type: 'AWS::ElasticLoadBalancingV2::TargetGroup',
@ -42,6 +78,7 @@ module.exports = {
},
DependsOn: [registerTargetPermissionLogicalId],
};
Object.assign(TargetGroup.Properties, healthCheckProperties);
Object.assign(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
[targetGroupLogicalId]: TargetGroup,
});

View File

@ -93,6 +93,7 @@ describe('#compileTargetGroups()', () => {
Value: 'some-service-first-50dc6c495c0c9188-dev',
},
],
HealthCheckEnabled: false,
},
DependsOn: ['FirstLambdaPermissionRegisterTarget'],
});
@ -120,6 +121,7 @@ describe('#compileTargetGroups()', () => {
Value: 'some-service-second-50dc6c495c0c9188-dev',
},
],
HealthCheckEnabled: false,
},
DependsOn: ['SecondLambdaPermissionRegisterTarget'],
});

View File

@ -72,6 +72,9 @@ module.exports = {
if (event.alb.authorizer) {
albObj.authorizers = this.validateEventAuthorizers(event, authorizers, functionName);
}
if (event.alb.healthCheck) {
albObj.healthCheck = this.validateAlbHealthCheck(event);
}
events.push(albObj);
}
}
@ -221,4 +224,12 @@ module.exports = {
return auth;
},
validateAlbHealthCheck(event) {
const eventHealthCheck = event.alb.healthCheck;
if (_.isObject(eventHealthCheck)) {
return Object.assign(eventHealthCheck, { enabled: true });
}
return { enabled: true };
},
};