github.com/mweagle/Sparta@v1.15.0/decorator/application_load_balancer.go (about) 1 package decorator 2 3 import ( 4 "fmt" 5 "strings" 6 7 "github.com/aws/aws-sdk-go/aws/session" 8 sparta "github.com/mweagle/Sparta" 9 gocf "github.com/mweagle/go-cloudformation" 10 "github.com/pkg/errors" 11 "github.com/sirupsen/logrus" 12 ) 13 14 type targetGroupEntry struct { 15 conditions *gocf.ElasticLoadBalancingV2ListenerRuleRuleConditionList 16 lambdaFn *sparta.LambdaAWSInfo 17 priority int64 18 } 19 20 // ApplicationLoadBalancerDecorator is an instance of a service decorator that 21 // handles registering Lambda functions with an Application Load Balancer. 22 type ApplicationLoadBalancerDecorator struct { 23 alb *gocf.ElasticLoadBalancingV2LoadBalancer 24 port int64 25 protocol string 26 defaultLambdaHandler *sparta.LambdaAWSInfo 27 targets []*targetGroupEntry 28 Resources map[string]gocf.ResourceProperties 29 } 30 31 // LogicalResourceName returns the CloudFormation resource name of the primary 32 // ALB 33 func (albd *ApplicationLoadBalancerDecorator) LogicalResourceName() string { 34 return sparta.CloudFormationResourceName("ELBv2Resource", "ELBv2Resource") 35 } 36 37 // AddConditionalEntry adds a new lambda target that is conditionally routed 38 // to depending on the condition value. 39 func (albd *ApplicationLoadBalancerDecorator) AddConditionalEntry(condition gocf.ElasticLoadBalancingV2ListenerRuleRuleCondition, 40 lambdaFn *sparta.LambdaAWSInfo) *ApplicationLoadBalancerDecorator { 41 42 return albd.AddConditionalEntryWithPriority(condition, 0, lambdaFn) 43 } 44 45 // AddConditionalEntryWithPriority adds a new lambda target that is conditionally routed 46 // to depending on the condition value using the user supplied priority value 47 func (albd *ApplicationLoadBalancerDecorator) AddConditionalEntryWithPriority(condition gocf.ElasticLoadBalancingV2ListenerRuleRuleCondition, 48 priority int64, 49 lambdaFn *sparta.LambdaAWSInfo) *ApplicationLoadBalancerDecorator { 50 51 return albd.AddMultiConditionalEntryWithPriority(&gocf.ElasticLoadBalancingV2ListenerRuleRuleConditionList{condition}, 52 priority, 53 lambdaFn) 54 } 55 56 // AddMultiConditionalEntry adds a new lambda target that is conditionally routed 57 // to depending on the multi condition value. 58 func (albd *ApplicationLoadBalancerDecorator) AddMultiConditionalEntry(conditions *gocf.ElasticLoadBalancingV2ListenerRuleRuleConditionList, 59 lambdaFn *sparta.LambdaAWSInfo) *ApplicationLoadBalancerDecorator { 60 61 return albd.AddMultiConditionalEntryWithPriority(conditions, 0, lambdaFn) 62 } 63 64 // AddMultiConditionalEntryWithPriority adds a new lambda target that is conditionally routed 65 // to depending on the multi condition value with the given priority index 66 func (albd *ApplicationLoadBalancerDecorator) AddMultiConditionalEntryWithPriority(conditions *gocf.ElasticLoadBalancingV2ListenerRuleRuleConditionList, 67 priority int64, 68 lambdaFn *sparta.LambdaAWSInfo) *ApplicationLoadBalancerDecorator { 69 70 // Add a version resource to the lambda so that we target that resource... 71 albd.targets = append(albd.targets, &targetGroupEntry{ 72 conditions: conditions, 73 priority: priority, 74 lambdaFn: lambdaFn, 75 }) 76 return albd 77 } 78 79 // DecorateService satisfies the ServiceDecoratorHookHandler interface 80 func (albd *ApplicationLoadBalancerDecorator) DecorateService(context map[string]interface{}, 81 serviceName string, 82 template *gocf.Template, 83 S3Bucket string, 84 S3Key string, 85 buildID string, 86 awsSession *session.Session, 87 noop bool, 88 logger *logrus.Logger) error { 89 90 portScopedResourceName := func(prefix string, parts ...string) string { 91 return sparta.CloudFormationResourceName(fmt.Sprintf("%s%d", prefix, albd.port), 92 parts...) 93 } 94 95 //////////////////////////////////////////////////////////////////////////// 96 // Closure to manage the permissions, version, and alias resources needed 97 // for each lambda target group 98 // 99 visitedLambdaFuncs := make(map[string]bool) 100 ensureLambdaPreconditions := func(lambdaFn *sparta.LambdaAWSInfo, dependentResource *gocf.Resource) error { 101 _, exists := visitedLambdaFuncs[lambdaFn.LogicalResourceName()] 102 if exists { 103 return nil 104 } 105 // Add the lambda permission 106 albPermissionResourceName := portScopedResourceName("ALBPermission", lambdaFn.LogicalResourceName()) 107 lambdaInvokePermission := &gocf.LambdaPermission{ 108 Action: gocf.String("lambda:InvokeFunction"), 109 FunctionName: gocf.GetAtt(lambdaFn.LogicalResourceName(), "Arn"), 110 Principal: gocf.String(sparta.ElasticLoadBalancingPrincipal), 111 } 112 template.AddResource(albPermissionResourceName, lambdaInvokePermission) 113 // The stable alias resource and unstable, retained version resource 114 aliasResourceName := portScopedResourceName("ALBAlias", lambdaFn.LogicalResourceName()) 115 versionResourceName := portScopedResourceName("ALBVersion", lambdaFn.LogicalResourceName(), buildID) 116 117 versionResource := &gocf.LambdaVersion{ 118 FunctionName: gocf.GetAtt(lambdaFn.LogicalResourceName(), "Arn").String(), 119 } 120 lambdaVersionRes := template.AddResource(versionResourceName, versionResource) 121 lambdaVersionRes.DeletionPolicy = "Retain" 122 123 // Add the alias that binds the lambda to the version... 124 aliasResource := &gocf.LambdaAlias{ 125 FunctionVersion: gocf.GetAtt(versionResourceName, "Version").String(), 126 FunctionName: gocf.Ref(lambdaFn.LogicalResourceName()).String(), 127 Name: gocf.String("live"), 128 } 129 template.AddResource(aliasResourceName, aliasResource) 130 // One time only 131 dependentResource.DependsOn = append(dependentResource.DependsOn, 132 albPermissionResourceName, 133 versionResourceName, 134 aliasResourceName) 135 visitedLambdaFuncs[lambdaFn.LogicalResourceName()] = true 136 return nil 137 } 138 139 //////////////////////////////////////////////////////////////////////////// 140 // START 141 // 142 // Add the alb. We'll link each target group inside the loop... 143 albRes := template.AddResource(albd.LogicalResourceName(), albd.alb) 144 defaultListenerResName := portScopedResourceName("ALBListener", "DefaultListener") 145 defaultTargetGroupResName := portScopedResourceName("ALBDefaultTarget", albd.defaultLambdaHandler.LogicalResourceName()) 146 147 // Create the default lambda target group... 148 defaultTargetGroupRes := &gocf.ElasticLoadBalancingV2TargetGroup{ 149 TargetType: gocf.String("lambda"), 150 Targets: &gocf.ElasticLoadBalancingV2TargetGroupTargetDescriptionList{ 151 gocf.ElasticLoadBalancingV2TargetGroupTargetDescription{ 152 ID: gocf.GetAtt(albd.defaultLambdaHandler.LogicalResourceName(), "Arn").String(), 153 }, 154 }, 155 } 156 // Add it... 157 targetGroupRes := template.AddResource(defaultTargetGroupResName, defaultTargetGroupRes) 158 159 // Then create the ELB listener with the default entry. We'll add the conditional 160 // lambda targets after this... 161 listenerRes := &gocf.ElasticLoadBalancingV2Listener{ 162 LoadBalancerArn: gocf.Ref(albd.LogicalResourceName()).String(), 163 Port: gocf.Integer(albd.port), 164 Protocol: gocf.String(albd.protocol), 165 DefaultActions: &gocf.ElasticLoadBalancingV2ListenerActionList{ 166 gocf.ElasticLoadBalancingV2ListenerAction{ 167 TargetGroupArn: gocf.Ref(defaultTargetGroupResName).String(), 168 Type: gocf.String("forward"), 169 }, 170 }, 171 } 172 defaultListenerRes := template.AddResource(defaultListenerResName, listenerRes) 173 defaultListenerRes.DependsOn = append(defaultListenerRes.DependsOn, defaultTargetGroupResName) 174 175 // Make sure this is all hooked up 176 ensureErr := ensureLambdaPreconditions(albd.defaultLambdaHandler, targetGroupRes) 177 if ensureErr != nil { 178 return errors.Wrapf(ensureErr, "Failed to create precondition resources for Lambda TargetGroup") 179 } 180 // Finally, ensure that each lambdaTarget has a single InvokePermission permission 181 // set so that the ALB can actually call them... 182 for eachIndex, eachTarget := range albd.targets { 183 // Create a new TargetGroup for this lambda function 184 conditionalLambdaTargetGroupResName := portScopedResourceName("ALBTargetCond", 185 eachTarget.lambdaFn.LogicalResourceName()) 186 conditionalLambdaTargetGroup := &gocf.ElasticLoadBalancingV2TargetGroup{ 187 TargetType: gocf.String("lambda"), 188 Targets: &gocf.ElasticLoadBalancingV2TargetGroupTargetDescriptionList{ 189 gocf.ElasticLoadBalancingV2TargetGroupTargetDescription{ 190 ID: gocf.GetAtt(eachTarget.lambdaFn.LogicalResourceName(), "Arn").String(), 191 }, 192 }, 193 } 194 // Add it... 195 targetGroupRes := template.AddResource(conditionalLambdaTargetGroupResName, conditionalLambdaTargetGroup) 196 // Create the stable alias resource resource.... 197 preconditionErr := ensureLambdaPreconditions(eachTarget.lambdaFn, targetGroupRes) 198 if preconditionErr != nil { 199 return errors.Wrapf(preconditionErr, "Failed to create precondition resources for Lambda TargetGroup") 200 } 201 202 // Priority is either user defined or the current slice index 203 rulePriority := eachTarget.priority 204 if rulePriority <= 0 { 205 rulePriority = int64(1 + eachIndex) 206 } 207 208 // Now create the rule that conditionally routes to this Lambda, in priority order... 209 listenerRule := &gocf.ElasticLoadBalancingV2ListenerRule{ 210 Actions: &gocf.ElasticLoadBalancingV2ListenerRuleActionList{ 211 gocf.ElasticLoadBalancingV2ListenerRuleAction{ 212 TargetGroupArn: gocf.Ref(conditionalLambdaTargetGroupResName).String(), 213 Type: gocf.String("forward"), 214 }, 215 }, 216 Conditions: eachTarget.conditions, 217 ListenerArn: gocf.Ref(defaultListenerResName).String(), 218 Priority: gocf.Integer(rulePriority), 219 } 220 // Add the rule... 221 listenerRuleResName := portScopedResourceName("ALBRule", 222 eachTarget.lambdaFn.LogicalResourceName(), 223 fmt.Sprintf("%d", eachIndex)) 224 225 // Add the resource 226 listenerRes := template.AddResource(listenerRuleResName, listenerRule) 227 listenerRes.DependsOn = append(listenerRes.DependsOn, conditionalLambdaTargetGroupResName) 228 } 229 // Add any other CloudFormation resources, in any order 230 for eachKey, eachResource := range albd.Resources { 231 template.AddResource(eachKey, eachResource) 232 // All the secondary resources are dependencies for the ALB 233 albRes.DependsOn = append(albRes.DependsOn, eachKey) 234 } 235 portOutputName := func(prefix string) string { 236 return fmt.Sprintf("%s%d", prefix, albd.port) 237 } 238 albOutput := func(label string, value interface{}) *gocf.Output { 239 return &gocf.Output{ 240 Description: fmt.Sprintf("%s (port: %d, protocol: %s)", label, albd.port, albd.protocol), 241 Value: value, 242 } 243 } 244 // Add the output to the template 245 template.Outputs[portOutputName("ApplicationLoadBalancerDNS")] = albOutput( 246 "ALB DNSName", 247 gocf.GetAtt(albd.LogicalResourceName(), "DNSName")) 248 249 template.Outputs[portOutputName("ApplicationLoadBalancerName")] = albOutput( 250 "ALB Name", 251 gocf.GetAtt(albd.LogicalResourceName(), "LoadBalancerName")) 252 253 template.Outputs[portOutputName("ApplicationLoadBalancerURL")] = albOutput( 254 "ALB URL", 255 gocf.Join("", 256 gocf.String(strings.ToLower(albd.protocol)), 257 gocf.String("://"), 258 gocf.GetAtt(albd.LogicalResourceName(), "DNSName"), 259 gocf.String(fmt.Sprintf(":%d", albd.port)))) 260 261 return nil 262 } 263 264 // NewApplicationLoadBalancerDecorator returns an application load balancer 265 // decorator that allows one or more lambda functions to be marked 266 // as ALB targets 267 func NewApplicationLoadBalancerDecorator(alb *gocf.ElasticLoadBalancingV2LoadBalancer, 268 port int64, 269 protocol string, 270 defaultLambdaHandler *sparta.LambdaAWSInfo) (*ApplicationLoadBalancerDecorator, error) { 271 return &ApplicationLoadBalancerDecorator{ 272 alb: alb, 273 port: port, 274 protocol: protocol, 275 defaultLambdaHandler: defaultLambdaHandler, 276 targets: make([]*targetGroupEntry, 0), 277 Resources: make(map[string]gocf.ResourceProperties), 278 }, nil 279 }