We use Elastic Beanstalk a lot. We also use CloudFormation a lot.
We use CloudFormation to create Elastic Beanstalk applications and environments, a lot.
Something which came to light recently is that it’s not possible to apply tags to the instances instantiated as part of an Elastic Beanstalk autoscaling group.
This is a problem for us because we use tagging as a way to manage cost and control access to groups of resources, IAM policy conditions and such.
There are methods already documented for addressing this missing feature, but the problem I had with those methods is that it they rely on static tags being defined within ebextension statements.
This means our dev teams need to include a custom .ebextensions\<blahblah>.config inside of each build they do.
So I thought I’d put my Friday to good use and come up with a method for getting my instances tagged with tags assigned depending on the environment that is being deployed.
Here it is. (This is by no means the only way, but it works well for us).
All of our applications are .Net. Also, this technique assumes you're instances have the AWS Powershell tools installed (which the default AMI's do have) and that the IAM role you associate with your instances has the ec2:CreateTags right on the instances instantiated by Beanstalk.
- The first challenge is applying the tags. As I said before, I don’t want to hardcode my tags into a configuration file for each build, I’d rather keep things simple for our development teams and give them a single .ebextensions bundle. Beanstalk “Option Values” to the rescue. Whilst defining the environment, either through CloudFormation or using the Management Console, you can specific optional parameters, which are passed into the application stack as variables, PARAM1, PARAM2 etc. These option parameters provide me with my conduit for getting my tags into the stack.
- In the case of a .NET application, the values of these parameters are passed to the <AppSettings> key within the applications web.config as a number of key-value pairs.
- Next I need to capture what the value for each of these tags should be, for example, “Environment”, “Application Version” etc. Nothing which can’t be easily done thanks for a few parameters in my CloudFormation template.
"Description": "Environment setting (prod, stg, dev)",
"AllowedValues": [ "stg", "prod", "dev" ],
- Now that I have my tags and a way of getting those tags into the environment (PARAM1, PARAM2 etc) it's time for a little powershell-foo. The following section talks about that the script does.
- As you can probably tell, this script reads the contents of the web.config file, specifically the contents of the Configuration\AppSettings section, under which Beanstalk very kindly places the keys PARAM1, PARAM2 etc and their corresponding values (which in this case are our tags).
- Once it's read the contents of the AppSettings key we strip out the keys / values we're interested in, which are PARAM1, PARAM2, PARAM3 and PARAM4. Thanks to a little "Select-Object" and filtering we're able to take the values of each of our PARAM keys and pop them into a variable.
- The next thing the script does is perform a "web-request" against the local metadata to retrieve the instanceId.
- Armed with our PARAM values and instanceID the script now proceeds to run the "New-EC2Tag" command to add the tags to the local instance.
- All that remains to be done is tie this all together by getting the script into the Instance and having it run. Both of these steps are achieved using ebextensions.
- I won't dive into what ebextenions are except to say that they are away to apply more advanced customisations to your instances and applications beyond the configuration options available through the management console.
- Getting the script into the instance can be achieved either by passing the contents into file using the ebextensions "files" section or by creating the storing the powershell script in an remote location (an S3 bucket for example) and downloading it to the instance, also using the "files" section of the configuration file. I personally prefer storing the powershell scripts remotely and pulling them into the instance, this is for two reasons. Firstly, you don't need to worry about formatting the powershell correctly to comply with YAML or JSON standards (as it needs to be when included in the configuration file) and secondly, it allows me to centrally store and maintain the scripts used to build my environment.
- Above you can see an example of the ebexenstion configuration file. I will try and walk you through what each of the sections does.
- Using the "files" section we instruct the bootstap process to download our powerhsell script from a pre-determined S3 bucket location.
- Remember to set the authentication as "s3", this is important when defining the authentication resource.
- Next we define a CloudFormation authentication resource for the S3 bucket containing our powershell script. Here we define which role should be used to inherit the rights from. (We're assuming you've granted your IAM role access to the bucket in question).
- Finally, we call the powershell script in the "container_commands" section. The reason we use the "container_commands" section is that these commands run after the package has been deployed. As appose to the "commands" which run before the package has been deployed.
- Give this configuration file to you developers and ask them to place it in a folder called ".ebextensions" in the root of their visual studio project, as I've done in the screenshot below.
And that's about it. So let me walk you though what happens now:
- You set / capture the values of your tags using CloudFormation Parameters.
- The values are passed in to the instances and stored as key-value pairs in the <AppSettings> section of your .Net applications web.config file.
- During bootstrap of the instances the applytags.ps1 PowerShell script is pulled down to the instance and is executed.
- The script extracts the values from the web.config and then uses the AWS powershell toolkit command New-EC2Tag to create the tags and set them on the instance.
As I said, not the only solution I'm sure, but one which works for us quite nicely.