Search code examples
amazon-web-servicesamazon-elastic-beanstalkaws-cloudformationaws-application-load-balancer

Cloudformation Elasticbeanstalk specify target group for shared load balancer


I have two Cloudformation templates

  • one which creates a VPC, ALB and any other shared resources etc.
  • one which creates an elastic beanstalk environment and relevant listener rules to direct traffic to this environment using the imported shared load balancer (call this template Environment)

The problem I'm facing is the Environment template creates a AWS::ElasticBeanstalk::Environment which subsequently creates a new CFN stack which contains things such as the ASG, and Target Group (or process as it is known to elastic beanstalk). These resources are not outputs of the AWS owned CFN template used to create the environment.

When setting

- Namespace: aws:elasticbeanstalk:environment
  OptionName: LoadBalancerIsShared
  Value: true

In the optionsettings for my elastic beanstalk environment, a load balancer is not created which is fine. I then try to attach a listener rule to my load balancer listener.

  ListenerRule:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      Priority: 1
      ListenerArn:
        Fn::ImportValue: !Sub '${NetworkStackName}-HTTPS-Listener'
      Actions:
        - Type: forward
          TargetGroupArn: WHAT_GOES_HERE
      Conditions:
        - Field: host-header
          HostHeaderConfig:
            Values:
              - mywebsite.com
    DependsOn:
      - Environment

The problem here is that I don't have access as far as I can tell to the ARN of the target group created by the elastic beanstalk environment resource. If I create a target group then it's not linked to elastic beanstalk and no instances are present.

I found the this page which states

The resources that Elastic Beanstalk creates for your environment have names. You can use these names to get information about the resources with a function, or modify properties on the resources to customize their behavior.

But because they're in a different stack (of which i don't know the name in advance), not ouputs of the template, I have no idea how to get hold of them.

--

Edit:

Marcin pointed me in the direction of a custom resource in their answer. I have expanded on it slightly and got it working. The implementation is slightly different in a couple of ways

  1. it's in Node instead of Python
  2. the api call describe_environment_resources in the example provided returns a list of resources, but seemingly not all of them. In my implementation I grab the auto scaling group, and use the Physical Resource ID to look up the other resources in the stack to which it belongs using the Cloudformation API.
const AWS = require('aws-sdk');
const cfnResponse = require('cfn-response');
const eb = new AWS.ElasticBeanstalk();
const cfn = new AWS.CloudFormation();

exports.handler = (event, context) => {
    if (event['RequestType'] !== 'Create') {
        console.log(event[RequestType], 'is not Create');
        return cfnResponse.send(event, context, cfnResponse.SUCCESS, {
            Message: `${event['RequestType']} completed.`,
        });
    }

    eb.describeEnvironmentResources(
        { EnvironmentName: event['ResourceProperties']['EBEnvName'] },
        function (err, { EnvironmentResources }) {
            if (err) {
                console.log('Exception', e);
                return cfnResponse.send(event, context, cfnResponse.FAILED, {});
            }

            const PhysicalResourceId = EnvironmentResources['AutoScalingGroups'].find(
                (group) => group.Name
            )['Name'];

            const { StackResources } = cfn.describeStackResources(
                { PhysicalResourceId },
                function (err, { StackResources }) {
                    if (err) {
                        console.log('Exception', e);
                        return cfnResponse.send(event, context, cfnResponse.FAILED, {});
                    }
                    const TargetGroup = StackResources.find(
                        (resource) =>
                            resource.LogicalResourceId === 'AWSEBV2LoadBalancerTargetGroup'
                    );

                    cfnResponse.send(event, context, cfnResponse.SUCCESS, {
                        TargetGroupArn: TargetGroup.PhysicalResourceId,
                    });
                }
            );
        }
    );
};

The Cloudformation templates

  LambdaBasicExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AWSCloudFormationReadOnlyAccess
        - arn:aws:iam::aws:policy/AWSElasticBeanstalkReadOnly
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

  GetEBLBTargetGroupLambda:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Description: 'Get ARN of EB Load balancer'
      Timeout: 30
      Role: !GetAtt 'LambdaBasicExecutionRole.Arn'
      Runtime: nodejs12.x
      Code:
        ZipFile: |
          ... code ...
  ListenerRule:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      Priority: 1
      ListenerArn:
        Fn::ImportValue: !Sub '${NetworkStackName}-HTTPS-Listener'
      Actions:
        - Type: forward
          TargetGroupArn:
            Fn::GetAtt: ['GetEBLBTargetGroupResource', 'TargetGroupArn']
      Conditions:
        - Field: host-header
          HostHeaderConfig:
            Values:
              - mydomain.com

Things I learned while doing this which hopefully help others

  1. using async handlers in Node is difficult with the default cfn-response library which is not async and results in the Cloudformation creation (and deletion) process hanging for many hours before rolling back.
  2. the cfn-response library is included automatically by cloudformation if you use ZipFile. The code is available on the AWS Docs if you were so inclined to include it manually (you could also wrap it in a promise then and use async lambda handlers). There are also packages on npm to achieve the same effect.
  3. Node 14.x couldn't run, Cloudformation threw up an error. I didn't make note of what it was, unfortunately.
  4. The policy AWSElasticBeanstalkFullAccess used in the example provided no longer exists and has been replaced with AdministratorAccess-AWSElasticBeanstalk.
  5. My example above needs less permissive policies attached but I've not yet addressed that in my testing. It'd be better if it could only read the specific elastic beanstalk environment etc.

Solution

  • I don't have access as far as I can tell to the ARN of the target group created by the elastic beanstalk environment resource

    That's true. The way to overcome this is through custom resource. In fact I developed fully working, very similar resource for one of my previous answers, thus you can have a look at it and adopt to your templates. The resource returns ARN of the EB load balancer, but you could modify it to get the ARN of EB's target group instead.