Kappacake
Kappacake

Reputation: 1947

Automatically set ListenerRule Priority in CloudFormation template

I have a CloudFormation template that contains an Application Load Balancer ListenerRule. One of the required properties of a ListenerRule is its Priority (a number between 1 and 50000). The priority for each ListenerRule must be unique.

I need to deploy the same template multiple times. The Priority for the ListenerRule should change every time I launch the template.

At the moment, I have turned the Priority into a parameter you can set when launching the stack and this works fine. Is there a way I can automatically set the priority of the ListenerRule to the next available priority?

Upvotes: 15

Views: 10029

Answers (1)

Laurent Jalbert Simard
Laurent Jalbert Simard

Reputation: 6349

No it's currently not possible to have it automatically allocated using only the AWS::ElasticLoadBalancingV2::ListenerRule resource. However, it can be achieved using a custom resource.

First let's create the actual custom resource Lambda code.

allocate_alb_rule_priority.py:

import json
import os
import random
import uuid

import boto3
import urllib3

SUCCESS = "SUCCESS"
FAILED = "FAILED"
# Member must have value less than or equal to 50000
ALB_RULE_PRIORITY_RANGE = 1, 50000


def lambda_handler(event, context):
    try:
        _lambda_handler(event, context)
    except Exception as e:
        # Must raise, otherwise the Lambda will be marked as successful, and the exception
        # will not be logged to CloudWatch logs.
        # Always send a response otherwise custom resource creation/update/deletion will be stuck
        send(
            event,
            context,
            response_status=FAILED if event['RequestType'] != 'Delete' else SUCCESS,
            # Do not fail on delete to avoid rollback failure
            response_data=None,
            physical_resource_id=uuid.uuid4(),
            reason=e,
        )
        raise


def _lambda_handler(event, context):
    print(json.dumps(event))

    physical_resource_id = event.get('PhysicalResourceId', str(uuid.uuid4()))
    response_data = {}

    if event['RequestType'] == 'Create':
        elbv2_client = boto3.client('elbv2')
        result = elbv2_client.describe_rules(ListenerArn=os.environ['ListenerArn'])

        in_use = list(filter(lambda s: s.isdecimal(), [r['Priority'] for r in result['Rules']]))

        priority = None
        while not priority or priority in in_use:
            priority = str(random.randint(*ALB_RULE_PRIORITY_RANGE))

        response_data = {
            'Priority': priority
        }

    send(event, context, SUCCESS, response_data, physical_resource_id)


def send(event, context, response_status, response_data, physical_resource_id, reason=None):
    response_url = event['ResponseURL']

    http = urllib3.PoolManager()

    body = {
        'Status': response_status,
        'Reason': reason or 'See the details in CloudWatch Log Stream: ' + context.log_stream_name,
        'PhysicalResourceId': physical_resource_id,
        'StackId': event['StackId'],
        'RequestId': event['RequestId'],
        'LogicalResourceId': event['LogicalResourceId'],
        'Data': response_data,
    }
    encoded_body = json.dumps(body).encode('utf-8')
    headers = {
        'content-type': '',
        'content-length': str(len(encoded_body)),
    }

    http.request('PUT', response_url, body=encoded_body, headers=headers)

According to your question, you need to create multiple stacks with the same template. For that reason I suggest the Custom Resource is placed within a template that is deployed only once. Then have the other template import its ServiceToken.

allocate_alb_rule_priority_custom_resouce.yml:

Resources:
  AllocateAlbRulePriorityCustomResourceLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Sid: ''
          Effect: Allow
          Principal:
            Service: lambda.amazonaws.com
          Action: sts:AssumeRole
      Path: /
      Policies:
      - PolicyName: DescribeRulesPolicy
        PolicyDocument:
          Version: "2012-10-17"
          Statement:
          - Effect: Allow
            Action:
            - elasticloadbalancing:DescribeRules
            Resource: "*"
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

  AllocateAlbRulePriorityCustomResourceLambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: allocate_alb_rule_priority.lambda_handler
      Role: !GetAtt AllocateAlbRulePriorityCustomResourceLambdaRole.Arn
      Code: allocate_alb_rule_priority.py
      Runtime: python3.8
      Timeout: '30'
      Environment:
        Variables:
          ListenerArn: !Ref LoadBalancerListener

Outputs:
  AllocateAlbRulePriorityCustomResourceLambdaArn:
    Value: !GetAtt AllocateAlbRulePriorityCustomResourceLambdaFunction.Arn
    Export:
      Name: AllocateAlbRulePriorityCustomResourceLambdaArn

You can notice that we're passing a ListenerArn to the Lambda function. It's because we want to avoid priority number collision on new allocation.

Lastly, we can now use our new custom resource in the template that is meant to be deployed multiple times.

template_meant_to_be_deployed_multiple_times.yml:

  AllocateAlbRulePriorityCustomResource:
    Type: Custom::AllocateAlbRulePriority
    Condition: AutoAllocateAlbPriority
    Properties:
      ServiceToken:
        Fn::ImportValue: AllocateAlbRulePriorityCustomResourceLambdaArn

  ListenerRule:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      Priority: !GetAtt AllocateAlbRulePriorityCustomResource.Priority
      [...]

These are snippets and may not work as-is, although they were taken from working code. I hope it gives you a general idea of how it can be achieved. Let me know if you need more help.

Upvotes: 22

Related Questions