Automatically set ListenerRule Priority in CloudFormation template
Asked Answered
G

1

15

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?

Gert answered 24/4, 2018 at 13:48 Comment(2)
Are you confortable with creating CloudFormation custom resources? If so, I could share you the code I wrote for working around this.Picky
I cannot say yes as I don't think I have ever used them, but I think that code would be very helpful anyways asI have never seen a workaround the issue I described.Gert
P
22

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.

Picky answered 24/4, 2018 at 15:23 Comment(6)
Thanks a lot for sharing this! I am going to give it a try as soon as I can.Gert
I am using cdk python and defins as your answer, it worksSucre
This is probably the first time in ~7y using Stack Overflow that I copied a bit of code that was >3 lines and it worked out of the box. Thanks for saving me all this time.Telephone
I have the same requirements, I'm wondering if this is still the only solution.Annunciate
Nice workaround for something that is obviously a missing feature in AWS listener rules :/Heterography
updated to work with python 3.8 since 3.6 is deprecated, had to drop requests library useAudly

© 2022 - 2024 — McMap. All rights reserved.