3

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.
Ben Swinburne
  • 25,669
  • 10
  • 69
  • 108
  • Hi Ben. Wondered if you ever got an error like the one below when you were doing this? I'm trying to get a Beanstalk env, built in cfn, to use a shared load balancer but not having much luck. The error "Service:AmazonCloudFormation, Message:Template format error: Unresolved resource dependencies [AWSEBV2LoadBalancer] in the Resources block of the template" – James Jan 26 '23 at 10:28
  • It's because I had a since-forgotten ebextension that referenced AWSEBV2LoadBalancer. – James Jan 27 '23 at 09:16

1 Answers1

1

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.

Marcin
  • 215,873
  • 14
  • 235
  • 294
  • thanks very much that was a great starting point. There were some differences which I've addressed in my question now. I was going to put it into your answer as opposed to the question but thought I'd better ask first. Thanks again – Ben Swinburne May 01 '21 at 14:36
  • from looking at my code, I don't suppose you might know why my custom resources don't delete properly? From what i can gather the lambda isn't responding appropriately to cloudformation and then eventually it times out (many hours) and then the rest of the stack deletes and leaves the custom resource. – Ben Swinburne May 11 '21 at 12:09