diff --git a/docs/providers/aws/events/alb.md b/docs/providers/aws/events/alb.md index f3382bcfe..ccb69d62b 100644 --- a/docs/providers/aws/events/alb.md +++ b/docs/providers/aws/events/alb.md @@ -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. diff --git a/docs/providers/aws/guide/serverless.yml.md b/docs/providers/aws/guide/serverless.yml.md index 8f31f738f..df802faf8 100644 --- a/docs/providers/aws/guide/serverless.yml.md +++ b/docs/providers/aws/guide/serverless.yml.md @@ -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) diff --git a/lib/plugins/aws/package/compile/events/alb/lib/healthCheck.test.js b/lib/plugins/aws/package/compile/events/alb/lib/healthCheck.test.js new file mode 100644 index 000000000..dfe9551ae --- /dev/null +++ b/lib/plugins/aws/package/compile/events/alb/lib/healthCheck.test.js @@ -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); + }); +}); diff --git a/lib/plugins/aws/package/compile/events/alb/lib/targetGroups.js b/lib/plugins/aws/package/compile/events/alb/lib/targetGroups.js index e354c27e8..4380ce7ba 100644 --- a/lib/plugins/aws/package/compile/events/alb/lib/targetGroups.js +++ b/lib/plugins/aws/package/compile/events/alb/lib/targetGroups.js @@ -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, }); diff --git a/lib/plugins/aws/package/compile/events/alb/lib/targetGroups.test.js b/lib/plugins/aws/package/compile/events/alb/lib/targetGroups.test.js index c8727458f..d16119083 100644 --- a/lib/plugins/aws/package/compile/events/alb/lib/targetGroups.test.js +++ b/lib/plugins/aws/package/compile/events/alb/lib/targetGroups.test.js @@ -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'], }); diff --git a/lib/plugins/aws/package/compile/events/alb/lib/validate.js b/lib/plugins/aws/package/compile/events/alb/lib/validate.js index 6c3adf5d9..5fea6d058 100644 --- a/lib/plugins/aws/package/compile/events/alb/lib/validate.js +++ b/lib/plugins/aws/package/compile/events/alb/lib/validate.js @@ -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 }; + }, };