AWS Cloud Operations Blog

Cross-account configuration with AWS AppConfig

Customers will often start using various AWS services through a single AWS account. As customers continue their AWS journey, they increase the number and diversity of workloads operating on AWS. Furthermore, as the number of users grows, managing this account becomes difficult and time consuming. Then, customers create more accounts for multiple users. This helps isolate and manage business applications and data, thereby helping optimizations in line with AWS Well-Architected Framework pillars, including operational excellence, security, reliability, and cost optimization.

This document provides general guidance and examples. However, every organization must define its specific multi-account structure. Following best practices, it is advisable to separate CI/CD management capabilities from workloads by using a set of production deployment accounts in a Deployments OU to provide CI/CD management capabilities. This helps by isolating critical CI/CD roles and managing production and non-production environments from a single point with common tooling. Furthermore, these reasons apply to application configuration management. For deploying application configurations, customers use AWS AppConfig, a feature of AWS Systems Manager that manages and quickly deploys application configurations at application runtime.

Configuration management has many similarities with CI/CD, and we will use an account under Deployments OU to host the apps configuration. This is because:

  • we can handle configuration for all of the environments from one single place
  • we can isolate running workloads from their configuration management
  • we can manage the configuration tooling from a single place

To illustrate how AWS AppConfig could be used to manage configuration across different AWS accounts, the following sections will describe how to set up an example environment.  The sample code references Ireland region eu-west-1, but feel free to change it according to your preferences.

In this diagram we can see the different accounts used in the following example. Account 1 is the configuration account, where we use AWS AppConfig to define an application, its Configuration Profile, and the different versions that can be applied to the corresponding environments. The other accounts are where our application will run, based on the configuration provided by Account 1.

To simplify, in this environment we will work with two accounts:

  • central application configuration account (config account)
  • application development account (app account)

But this could be easily extended to work with more accounts, as shown in the diagram above.

Account setup

First, you must create and configure the access to both AWS accounts.

  1. Using AWS Organizations, create two new linked accounts for config and app from the AWS Organizations console. It is also possible to configure this solution using independent accounts, but for simplicity we will assume you work with accounts inside an organization.
    Important: write down the account numbers, as you will need them afterward.
  2. You can access the new accounts using AWS Sigle Sign-On, or using the preconfigured roles named OrganizationAccountAccessRole.
  3. Set up AWS Command Line Interface (CLI) access with AWS SSO, configuring one specific profile for each account, as described here.
    In our case, we used “aws configure sso” command to define a profile called “config” for the configuration account, and “dev” for the app development account, choosing a role with administration privileges in both cases.

Build the Application

To keep it simple, we will use a simple containerized python flask app running in App Runner.

This app lists and returns the AWS services in the region that it is running. Using AWS AppConfig, we can change the behavior of the application in different environments, from a central management account, and with no application redeployments.

Follow the next steps to build the application:

  1. Using a text or code editor, create a folder called AppConfigMultiAccount, with the following files in your local drive:
    1. Dockerfile
      # Set base image (host OS)
      FROM python:3.8-alpine
      # By default, listen on port 5000
      EXPOSE 5000/tcp
      # Set the working directory in the container
      WORKDIR /app
      # Copy the dependencies file to the working directory
      COPY requirements.txt .
      # Install any dependencies
      RUN pip install -r requirements.txt
      # Copy the content of the local src directory to the working directory
      COPY app.py .
      # Specify the command to run on container start
      CMD [ "python", "./app.py" ]
      
    2. requirements.txt
      boto3
      flask===1.1.2
    3. app.py
      from datetime import datetime, timedelta
      import json
      import os
      import boto3
      from flask import Flask
      
      app = Flask(__name__)
      config_token = None
      token_expiration_time = None
      cached_config_data = {}
      
      def get_config():
          global cached_config_data
          global token_expiration_time
          global config_token
          sts = boto3.client('sts', region_name='eu-west-1')
          response = sts.assume_role(RoleArn=os.environ['APPCONFIGREADROLE'],
                                     RoleSessionName='readconfigsession')
          session = boto3.Session(aws_access_key_id=response['Credentials']['AccessKeyId'],
                                  aws_secret_access_key=response['Credentials']['SecretAccessKey'],
                                  aws_session_token=response['Credentials']['SessionToken'])
          appconfigdata = session.client('appconfigdata', region_name='eu-west-1')
      
          # If we don't have a token yet, call start_configuration_session to get one
          if not config_token or datetime.now() >= token_expiration_time:
              start_session_response = appconfigdata.start_configuration_session(
                  ApplicationIdentifier=os.environ['APPCONFIGAPPNAME'],
                  EnvironmentIdentifier=os.environ['APPCONFIGENV'],
                  ConfigurationProfileIdentifier=os.environ['APPCONFIGCONF']
              )
              config_token = start_session_response['InitialConfigurationToken']
      
          get_config_response = appconfigdata.get_latest_configuration(
              ConfigurationToken=config_token
          )
          # Response always includes a fresh token to use in next call
          config_token = get_config_response['NextPollConfigurationToken']
          # Token will expire if not refreshed within 24 hours, so keep track of
          # the expected expiration time minus a bit of padding
          token_expiration_time = datetime.now() + timedelta(hours=23, minutes=59)
          # 'Configuration' in the response will only be populated the first time we
          # call GetLatestConfiguration or if the config contents have changed since
          # the last time we called. So if it's empty we know we already have the latest
          # config, otherwise we need to update our cache.
          content = get_config_response["Configuration"].read()  # type: bytes
          if content:
              if get_config_response["ContentType"] == "application/json":
                  try:
                      cached_config_data = json.loads(content.decode("utf-8"))
                      print("cached_config_data:", cached_config_data)
                  except json.JSONDecodeError as error:
                      raise ValueError(error.msg) from error
              else:
                  raise TypeError("Only application/json responses allowed")
      
          return cached_config_data
      
      @app.route('/')
      def hello_world():
          session = boto3.Session(region_name='eu-west-1')
          services = session.get_available_services()
          config = get_config()
          if config['boolEnableLimitResults']:
              returnlimit = config['intResultLimit']
          else:
              returnlimit = -1
          return json.dumps(services[0:returnlimit], indent=4, sort_keys=True)
      
      if __name__ == "__main__":
          app.run(host='0.0.0.0', port=5000)
  2. Open an AWS console in config account, and navigate to Elastic Container Registry service.
  3. Select Create a repository Get Started button
  4. In the next form, you can leave all of the fields with their default values, and for the repository name, type “listservices”.
  5. Then, select create repository.
    Once created, write down the URI of your new private ECR repository, as we will use it later.
  6. Using the DockerFile, requirements.txt, and app.py files created in Step 1, build and push image to ECR from your local AppConfigMultiAccount folder.
    Replace <Repository URI> with the value that you copied before.

    aws ecr get-login-password --profile config --region eu-west-1 | docker login \
    --username AWS --password-stdin <Repository URI>
    docker build -t flask-container .
    docker tag flask-container:latest <repository URI>:latest
    docker push <Repository URI>:latest
  7. Finally, share the container image with the app account (replace account IDs with your own). Create a repository policy file named repopolicy.json, with the following content:
    {
        "Version": "2008-10-17",
        "Statement": [
            {
                "Sid": "AllowPull",
                "Effect": "Allow",
                "Principal": {
                    "AWS": "arn:aws:iam::<development_account_id>:root"
                },
                "Action": [
                    "ecr:GetDownloadUrlForLayer",
                    "ecr:BatchGetImage",
                    "ecr:BatchCheckLayerAvailability"
                ]
            }
        ]
    }
    

    Then, use it to set the repository policy:

    aws ecr set-repository-policy --profile <config_account_id> \
    --repository-name listservices --policy-text file:./repopolicy.json
    

AWS AppConfig Setup

Next, create all of the the AWS AppConfig elements.

  1. Log in to the config account console and navigate to AWS AppConfig.
    If it’s the first time that you have opened this console, then select Get Started.
  2. Next, create an application called ListServices:
    On Create Application step, provide a name and select Create Application.
  3. Once in the new application detail page, create three new environments:
    – development
    – testing
    – production
    On the Create Environment step, provide a name and select Create environment.
  4. Navigate back to the new Application detail page, and select the Configuration Profiles tab.
    On the Configuration profiles list, select Create configuration profile.
  5. Create a new configuration profile, and name it ListServicesAppConfigConfigProfile.
    Leave the marked configuration source default value “AWS AppConfig hosted configuration“. For content, choose JSON, and then provide the next initial configuration:

    {
        "boolEnableLimitResults": false,
        "intResultLimit": 10
    }
  6. Select next, and you will now see an option to add configuration validators.
    Adding this validator will prevent us from pushing some of the most common errors to our application configuration. Choose JSON Schema validator, and add the following code:

    {
        "$schema": "http://json-schema.org/draft-04/schema#",
          "description": "AppConfig Validator example",
            "type": "object",
              "properties": {
          "boolEnableLimitResults": {
            "type": "boolean"
          },
          "intResultLimit": {
            "type": "number"
          }
        },
        "minProperties": 2,
          "required": [
            "intResultLimit",
            "boolEnableLimitResults"
          ]
    }
  7. Finally, select “Create configuration profile”

We will now deploy this initial configuration to our first environment: development.

  1. From the ListServices application detail page, choose development environment.
  2. Select “Start deployment”.
  3. In the next form, change the deployment strategy to “AppConfig.AllAtOnce(Quick)“ to make things faster.
  4. Then, select “Start deployment“.

Roles, permissions setup

Now, it’s time to define roles for the application and the required permissions. First, we will grant cross-account access permissions.

  1. Get your AWS AppConfig application ID by running the following:
    aws appconfig list-applications --profile config
  2. Open the IAM console from the config account, and create the next IAM policy:
    Policy Name: AppConfigListServicesAccessPolicy
    Policy definition: Use the next definition by putting your own region, account ID, and the application ID from the previous step.

    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": [
                    "appconfig:ListTagsForResource",
                    "appconfig:GetHostedConfigurationVersion",
                    "appconfig:GetDeployment"
                ],
                "Resource": [
                    "arn:aws:appconfig:<region>:<config_account_id>:application/<application_id>/configurationprofile/*/hostedconfigurationversion/*",
                    "arn:aws:appconfig:<region>:<config_account_id>:application/<application_id>/environment/*/deployment/*"
                ]
            },
            {
                "Effect": "Allow",
                "Action": [
                    "appconfig:ListEnvironments",
                    "appconfig:ListTagsForResource",
                    "appconfig:GetEnvironment",
                    "appconfig:ListConfigurationProfiles",
                    "appconfig:GetHostedConfigurationVersion",
                    "appconfig:ListDeployments",
                    "appconfig:GetDeployment",
                    "appconfig:GetLatestConfiguration",
                    "appconfig:StartConfigurationSession",                
                    "appconfig:GetApplication",
                    "appconfig:GetConfigurationProfile",
                    "appconfig:ListHostedConfigurationVersions"
                ],
                "Resource": [
                    "arn:aws:appconfig:<region>:<config_account_id>:application/<application_id>",
                    "arn:aws:appconfig:<region>:<config_account_id>:application/<application_id>/configurationprofile/*",
                    "arn:aws:appconfig:<region>:<config_account_id>:application/<application_id>/environment/*"
                ]
            },
            {
                "Effect": "Allow",
                "Action": [
                    "appconfig:ListApplications",
                    "appconfig:ListDeploymentStrategies"
                ],
                "Resource": "*"
            }
        ]
    }
  3. Once you have created the IAM policy, create a new IAM role in the config account from the same console.
  4. As trusted entity, choose Another AWS Account, and provide the account ID for the app account.
  5. Attach the policy that you just created to it.
  6. When asked for the role name, call it AppConfigListServicesAccessRole.

Now, switch to the app account, and create the required policies and roles:

  1. From the IAM console, create a new policy to allow cross-account access.
    Policy Name: AssumeAppConfigListServicesAccessPolicy.
    Policy definition: Use the next definition by replacing config_account_id with your configuration account ID.

    {
        "Version": "2012-10-17",
        "Statement": {
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": "arn:aws:iam::<config_account_id>:role/AppConfigListServicesAccessRole"
        }
    }
  2. From the same IAM console, create a new role for the app runner instance.
  3. As trusted entity, choose EC2 – we will change it later.
  4. Attach the AssumeAppConfigListServicesAccessPolicy policy that we have just created to it.
  5. When asked for the role name, call it AppRunnerListServicesCrossConfigRole.
  6. Once created, edit the role trust policy:
    {
        "Version": "2012-10-17",
        "Statement": [
          {
            "Effect": "Allow",
            "Principal": {
              "Service": "tasks.apprunner.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
          }
        ]
    }  

Deploy and test the application

After all of this preparation, we are now ready to launch our application. We will use AWS App Runner. This is a fully managed service that makes it easy to deploy from source code or container images directly to a scalable and secure web application.

Follow the next steps:

  1. Open the AWS console from the app account, and navigate to the AWS App Runner service.
  2. If this is your first time using AWS App Runner, select “Create an App Runner Service”.
  3. At the next screen, choose Container Registry as Repository type, and ECR as Provider.
  4. In the Container Image URI field, use the repository URI that we generated in the first steps, adding “:latest“ at the end. Remember that we applied a policy before to the Container registry in the config account to allow the development account to pull images from it.
  5. In the deployment settings section, choose Manual as the deployment trigger, and “Create a new service role” option, accepting the default service role name, then select Next:
    In the AWS App Runner console, provide all of the details about the application source and deployment type as described in this post to deploy our sample application.
  6. In the next screen, define “ListServices” as the service name, and then define four environment variables as shown in the following. Remember to replace <config_account_id> with your own config account ID. AWS AppRunner will expose those environment variables to the container running the application. Furthermore, our application will use them to assume the specified IAM role in the config account and ask for the specific AWS AppCongfig Application, Environment, and Configuration Profile.
    Env Variable Name Value Used for
    APPCONFIGAPPNAME ListServices AWS AppConfig Application Name
    APPCONFIGCONF ListServicesAppConfigConfigProfile AWS AppConfig Configuration Profile
    APPCONFIGENV development AWS AppConfig Environment
    APPCONFIGREADROLE arn:aws:iam::<config_account_id>:role/AppConfigListServicesAccessRole AWS IAM Role to assume in config acct
  7. Change the port value from 8080 to 5000.
  8. Open the Security section, and select the only available Instance Role: AppRunnerListServicesCrossConfigRole.
  9. Select Next, and after verifying all of the settings, select “Create & Deploy”.

Once the application is deployed and in the running status, test it by selecting its Default Domain link from the Application detail page. You will see a list of all of the available AWS services in your browser.

Modify the application behavior

We have our application running on the development account. Let’s suppose we want to safely change its configuration.

Now we can change the application behavior across different environments in different accounts from our central config account. For example, let’s change the configuration in our development environment:

  1. Access AWS AppConfig console from config account, and open ListServices application detail. We will create a new configuration version that can be tested and applied to the different environments.
  2. Open “ListServicesAppConfigConfigProfile“ configuration profile, and create a new configuration version with this content:
    {
        "boolEnableLimitResults": true,
        "intResultLimit": 10
    }

    Now we have hosted configuration version 2, and we can apply and test it safely in our development environment.

  3. As in previous steps, open development environment under ListServices application, and then select “Start deployment”.
  4. Choose 2 as Hosted configuration version, and deployment strategy to “AppConfig.AllAtOnce(Quick)“, then select ”Start deployment“.

In a few seconds, check your application again, and it should only show the first 10 available services in the list. Now you could safely change the configuration to other environments from your config account.

Cleanup

Once you’ve finished testing this solution, remember to delete the following resources to avoid unnecessary charges:

  • Dev Account:
    • AWS AppRunner: ListServices Service
  • Config Account:
    • Amazon ECR Repositories: listservices
    • AWS AppConfig: ListServices Application

About the author

Luis Gomez

Luis Gomez is a Solutions Architect with AWS, working for public sector in Spain. He has several years of experience building and operating cloud environments, and applying devops practices. He works with customers to understand their goals and challenges, and offers prescriptive guidance to achieve their objective with AWS offerings.