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  }