• Simple AWS
  • Posts
  • Automating Deployment of Configuration Updates

Automating Deployment of Configuration Updates

Setting up AppConfig to store configurations, and CodePipeline to trigger on changes to an S3 object

You have an app deployed in some EC2 instances. There's a config file that you need to update simultaneously across all instances. First you did this manually in each instance, but it was prone to errors. So you moved the file to an S3 bucket, and you're SSHing into every instance and pulling the new file from S3. You'd like this to happen simultaneously and automatically, with no downtime.

Create a configuration profile in AppConfig and set up CodePipeline to update all instances when the file in S3 is updated

Here's the initial setup, and you can deploy it here:

Step by step

Step 0: Test the app

  • Open the CloudFormation console

  • Select the initial state stack

  • Click the Outputs tab

  • Copy the value for EC2InstancePublicIp

  • Paste it in the browser, append :3000 and hit Enter/Return

  • Verify that the message says "to do: write thank you message"

Step 1: Create the AWS AppConfig application

  • Open the AWS Systems Manager AppConfig console

  • Click Get Started. If you don't see a Get Started button, go to the Applications tab and click Create application

  • Enter a name and description for the application, such as SimpleAWS38

  • Click Create application

Step 2: Create an environment

  • Go back to the AppConfig console

  • On the Applications tab, click on the application you just created

  • Go to the Environments tab and click Create environment

  • Enter a name and description for the environment, such as "Dev"

  • Click Create environment

Step 3: Create an AppConfig freeform configuration profile

  • Go back to the AppConfig console

  • On the Applications tab click on your application

  • On the Configuration profiles and feature flags tab, click Create

  • Select Freeform Configuration and click Select

  • Enter a name and description

  • Under Configuration source, select AWS CodePipeline

  • Click Next

  • Under Add validators, click the Remove button for Validator 1

  • Click Create configuration profile

Step 4: Create a CodePipeline pipeline

  • Go to the CodePipeline console

  • Click Create pipeline

  • Enter a name for the pipeline

  • Click Next

  • For Source provider, select Amazon S3

  • In Bucket, select the name of your S3 configs bucket (which you set when launching the initial template)

  • In S3 object key, enter "review.txt.zip"

  • Click Next

  • Click Skip build stage and accept the warning by clicking Skip again

  • Click Next

  • For Deploy provider, select AWS AppConfig

  • For Application, select the application you created in Step 1

  • For Environment, select the environment you created in Step 2

  • For Configuration profile, select the configuration profile you created in Step 3

  • For Deployment strategy, select AppConfig.AllAtOnce

  • In Input artifact configuration path, enter "review.txt"

  • Click Next

  • Review the information and click Create pipeline

Step 5: Test the app again

  • Go back to the browser tab where you pasted the public IP address of the instance and refresh the page

  • Verify that the message still says "to do: write thank you message"

Step 6: Make a change to the config file and upload it to S3

  • Go to the S3 console

  • Click on the bucket you created with the initial setup

  • Click on the file called "review.txt.zip"

  • Click Download

  • Unzip the file

  • Open the file with a text editor

  • Write a short review (one or two sentences) of the Simple AWS newsletter that I can publish on the website

  • Save the review.txt file with the changes

  • Zip the file into a file called "review.txt.zip"

  • Go back to the S3 console

  • Click Upload

  • Click Add files

  • Find and select the file review.txt

  • Click Upload

Step 7: Update environment variables

  • Go back to the AppConfig console

  • Click on your application

  • Copy the Application ID

  • Go to the Environments tab and click on your environment

  • Copy the Environment ID

  • Go back to your application, go to the Configuration Profiles and Feature Flags tab and click on your configuration

  • Copy the Configuration Profile ID

  • Go to the EC2 console

  • Select the instance that was created with the initial template

  • Click Connect

  • Go to the Session Manager tab and click Connect

  • Type the following commands, replacing REPLACE_APPCONFIG_APPLICATION_ID for the ID of the AppConfig application, REPLACE_APPCONFIG_ENVIRONMENT_ID for the ID of the environment, and REPLACE_APPCONFIG_CONFIGURATION_ID for the ID of the configuration.
    Note: You'll need to copy and paste them one by one

sudo su

echo "APPCONFIG_APPLICATION=REPLACE_APPCONFIG_APPLICATION_ID" >> /home/ec2-user/app/.env

echo "APPCONFIG_ENVIRONMENT=REPLACE_APPCONFIG_ENVIRONMENT_ID" >> /home/ec2-user/app/.env

echo "APPCONFIG_CONFIGURATION=REPLACE_APPCONFIG_CONFIGURATION_ID" >> /home/ec2-user/app/.env

echo "APPCONFIG_CLIENTID=simpleaws" >> /home/ec2-user/app/.env

source /home/ec2-user/app/.env

kill -9 $(ps aux | grep '[n]ode src/index.js' | awk '{print $2}') && node src/index.js > /home/ec2-user/app/app.log 2>&1 &

Step 8: Test the app again

  • Wait for the pipeline to finish if it hasn't. You can view the progress in the CodePipeline console

  • Go back to the browser tab where you pasted the public IP address of the instance and refresh the page

  • Verify that the message has changed and now says what you wrote in Step 6

Step 9: Send me that review you just wrote (optional)

You just wrote some nice words about Simple AWS! If you want, send them to me (reply to this email or hit me up on LinkedIn) and I'll publish them on the Simple AWS website. Please specify the name you want me to publish it under, if not I'll just use Anonymous to preserve your personal information.

If you don't want me to publish it, that's totally cool.

Explanation

Step 1: Create the AWS AppConfig application

An application in AppConfig is a logical unit that handles a set of capabilities and features for a specific application. This is where you're going to group everything else.

Step 2: Create an environment

An environment is a logical deployment group of AppConfig targets, such as applications in a beta or production environment. Having different environments for a single app lets you keep the actual values separate, while still keeping all the configuration resources grouped together.

Here we could also configure CloudWatch Alarms, so that deployments made to this environment are automatically rolled back if a CloudWatch Alarm is triggered. This is especially useful to roll back failing configurations.

Step 3: Create an AppConfig freeform configuration profile

A configuration profile is a collection of settings stored in AppConfig, which can be accessed by applications. In this case we're creating a Freeform Configuration, but another option is feature flags, which we'll talk about in a future issue.

In these instructions I told you to remove the validator, because our config file doesn't have a specific format. If you're dealing with properly formatted configuration files, such as a JSON, then a validator will prevent a deployment from even starting, in the case where the config file doesn't match the schema defined for the validator.

Step 4: Create a CodePipeline pipeline

CodePipeline is AWS's service for managing CI/CD pipelines. In this case, however, we're not dealing with a regular CI/CD pipeline where the source is a git repository and the goal is to deploy that code. Our source is an S3 bucket, and CodePipeline is effectively serving us to execute an action when a file is uploaded to that bucket. There's no Build step, and our Deploy step is sending that configuration file to AppConfig. We're not just moving the file around though, we're effectively Deploying the file, in a new Deployment in AppConfig, for that specific Application, for that specific Environment.

In this case we're choosing the deployment strategy AllAtOnce, but we could opt for a gradual rollout. This plays well with the automated rollbacks when a CloudWatch Alarm is triggered.

Step 5: Test the app again

Just check that everything is still working.

Step 6: Make a change to the config file and upload it to S3

This step is to simulate a configuration change. We're still not ready to use the configuration from AppConfig, but since the pipeline can take a few minutes, it's better to do this now.

Step 7: Update environment variables

The configuration isn't actually pushed to the instance (notice that so far we haven't ever referenced the instance), but instead to a configuration repository inside AppConfig. It's the application's responsibility to fetch it from that repository. Here's what the code actually looks like:

const application = process.env.APPCONFIG_APPLICATION; const environment = process.env.APPCONFIG_ENVIRONMENT; const configuration = process.env.APPCONFIG_CONFIGURATION; const clientId = process.env.APPCONFIG_CLIENTID; const fileNameFallback = process.env.FILE_NAME; const localFilePath = './' + fileNameFallback; async function fetchAppConfig() { const params = { Application: application, Environment: environment, Configuration: configuration, ClientId: clientId, }; try { const data = await appConfigClient.send(new GetConfigurationCommand(params)); const charCodes = new Uint8Array(data.Content); const message = String.fromCharCode(...charCodes); return message; } catch (error) { console.error(`Failed to fetch AppConfig: ${error}`); return null; } } app.get('/', async (req, res) => { let message = await fetchAppConfig(); if (!message) { try { message = fs.readFileSync(localFilePath, 'utf-8'); } catch (e) { console.log('Error: ', e); res.status(500).json({ error: 'An error occurred while reading the file' }); return; } } res.set('Cache-Control', 'no-store, no-cache, must-revalidate, private'); res.json({ message: message }); });

We need to set those environment variables with the ID of the Application, Environment and Configuration Profile that we just created, so the code knows where to go fetch that config.

Step 8: Test the app again

Now we should see the message!

Step 9: Send me that review you just wrote (optional)

I'm serious with this step. When people want to subscribe, they go to www.simpleaws.dev, view the information there, scroll down, view those reviews, and decide whether to subscribe or not. You leaving a review would be really helpful for me, since it would help me convince others that this newsletter is really valuable. So, if you'd be so kind, could you please send me a review?

Discussion

I believe the first thing to discuss here is, if AppConfig isn't pushing the config file into the instance, what's the difference between storing the configuration file in S3 and storing the configuration "file" in AppConfig?

Here's the thing: Using AppConfig isn't as easy as I made it look like (as if that was easy!!). What you need to do for a proper AppConfig configuration is start a configuration session and then fetch the latest configuration. This way, when a new configuration deployment happens, the configuration is pushed to all open sessions. I started doing it that way, but the step by step would have taken like 15 steps.

One of the benefits is the deployment strategy, which we can't appreciate with just 1 instance. When we're using AllAtOnce, all sessions get the new config at the same time. However, we can also do a rolling update, where sessions are gradually updated, without doing a rolling update at the Auto Scaling Group level (i.e. re-creating the EC2 instances). And the third option is a canary deployment: First a small percentage of sessions get the new config, then the rest.

Another benefit is the automated rollbacks. We can configure CloudWatch Alarms that are associated with an Environment, and when a new configuration deployment happens, those alarms are monitored and, if they trigger, the deployment is automatically rolled back. We could do that manually with S3, rolling back to a previous version of the object on a bucket with versioning enabled. But why do it manually?

Finally, we have feature flags. It's way too long to really discuss here, but the short of the long is this:

  • Feature flags are boolean variables (hence the name flags) that enable or disable features or parts of the code. The code is wrapped in an if statement, and if the flag is true, the code runs.

  • The idea is that feature flags are a configuration, so we can deploy the code that supports the new feature, with that configuration turned off, and the app acts like before the deployment. Then we can enable the feature with just a configuration change.

  • This is why feature flags are a part of AppConfig.

  • Feature flags are often used in Trunk-Based Development (TBD), where everyone commits their code directly to main (previously called master, previously called trunk). Since this often leads to incomplete code being committed, that code is hidden behind feature flags, so it doesn't impact the app.

  • Feature flags is an easy problem to solve at the technical level, using AppConfig or some other apps.

  • It's actually hard to solve at a process level, since you need to determine how to create new feature flags, when to delete feature flags, how all of that impacts other developers in the same team, how to mix development feature flags (for still in development features) with production feature flags (features that are deemed ready, and are released through feature flags), and how to avoid feature flag hell: there's a million feature flags and nobody knows what 60% of them do.

Something worth pointing out is that, after these changes, the instances no longer need access to S3. They'll be getting their configuration from Systems Manager AppConfig now. So, if you had already set up a VPC Endpoint for S3 like we discussed in last week's issue, you should delete it and instead create a VPC Endpoint for Systems Manager.

Best Practices

Operational Excellence

  • Use CI/CD for Configuration Changes: This is why we're using CodePipeline.

  • Use CloudWatch Alarms: As I mentioned, deployments can be automatically rolled back if a CloudWatch Metric triggers an Alarm. The obvious example metric would be percentage of responses that are 5XX.

Security

  • Use Secrets Manager: Secrets Manager can't handle configs as well as AppManager, but it's the most secure way to manage secrets like database passwords.

Reliability

  • Automated Rollbacks in CodePipeline: Set up automated rollbacks in your CodePipeline to automatically revert changes if a pipeline execution fails. This is separate from reverting the deployment from AppConfig, this is about some error in CodePipeline.

  • AppConfig Deployment Strategy: Use canary or linear deployments to gradually deploy configuration changes. This will minimize the blast radius if there's an issue during deployment, such as a bad configuration.

  • Use AppConfig Validators: Use Validators to ensure the config file conforms to a predetermined schema, thus completely avoiding deploying a badly formatted file.

Performance Efficiency

  • Optimize Config File Size: Minimize the size of configuration files for faster deployments.

Cost Optimization

  • Don't push micro updates unless you have to: AppConfig is priced at $0.0000002 per configuration request and $0.0008 per new configuration received. You can't avoid configuration requests, but you can minimize new configurations received if you only group config changes and push them together. For reference, for 2000 servers (that's a lot!) you're paying $8.64/month for configuration requests and, with 3 configuration pushes a day, $144/month for new configurations received, for a grand total of $152.64 (that's a lot! but if you have 2000 servers I bet that's less than 1% of your AWS bill)

Resources

I hope you know Martin Fowler! Software expert, most known for creating the Java framework Spring, and for his blog/wiki that he called Bliki. He has some really interesting thoughts, for example here's one about Feature Flags.

Did you like this issue?

Login or Subscribe to participate in polls.

Join the conversation

or to participate.