AppConfig is AWS's native solution for feature flagging and dynamic configuration management. It provides a comparable feature set to third party feature flagging tools such as LaunchDarkly or StatSig. Coming from an organization that used these third party tools, there were some hurdles that I encountered when implementing AppConfig for our applications with the same ease as these third party tools. In this article, I will describe these challenges and our solutions to them. Specifically, I will cover these topics:
- AWS Organizations setup with an AppConfig shared service account
- Configuring IAM roles and Systems Manager Parameters to simplify application access to the correct environments configured in AppConfig
- Setting up AppConfig Agent with Lambda functions, including Docker Image Functions
Authorization and access compared to third party tools
If you're familiar with using a feature flagging tool such as LaunchDarkly, you will be used to using API keys that are associated with specific environments. This abstracts the association between an environment (a set of flags/configs tied to a deployment environment, such as dev, staging, or prod) and the application. The application client just needs to be configured with the correct API key, and will pull flags that are specific to that environment without needing to be explicitly configured with information about that environment.
With AppConfig, there is no concept of API keys. All access to the service is through the AWS native IAM integration. So the question is how best to set up AppConfig and the IAM roles allowed to access it in a multi-account, multi-environment AWS Organization?
AWS Organization setup
The structure we settled on is:
- A separate, shared services AWS account dedicated to AppConfig
- IAM roles specific to an AppConfig "Application"
- For each Application and Environment within that application, an IAM role that grants access to get configurations specific to that environment
- SSM Systems manager parameters for each Application deployed to each workload account that specifies the role to assume and the environment name
- AWS IAM Identity Center permission set granting access to the AppConfig account
CloudFormation templates
To implement this structure, we'll need two CloudFormation templates:
- A template for the AppConfig account that creates the application, environments, and cross-account IAM roles
- A template for the workload accounts (dev/prod) that creates the SSM parameters
AppConfig Account Template
Deploy this template to your central AppConfig account. It creates an AppConfig application with dev and prod environments, plus the IAM roles that will be assumed by applications in the dev and prod OUs. The AssumeRolePolicyDocument can be modified to support your specific AWS Organization structure (for example, granting access to a specific account, etc.).
The IAM roles created in the AppConfig account need policies that grant access to the appropriate AppConfig resources. The policy below shows the permissions needed:
- Basic AppConfig read operations like
GetConfiguration
andStartConfigurationSession
scoped to the specific environment - Additional list/describe operations needed by the AppConfig Agent to discover configurations and lookup ids based on names.
# Deploy this template to the AppConfig account
Parameters:
ApplicationName:
Type: String
Description: Name of the AppConfig application
Default: myapp
prodOuId:
Type: String
Description: ID of the Production OU
sdlcOuId:
Type: String
Description: ID of the SDLC OU
sandboxOuId:
Type: String
Description: ID of the Sandbox OU
organizationPrincipalId:
Type: String
Description: ID of the Organization Principal
organizationRootOuId:
Type: String
Description: ID of the Organization Root OU
Resources:
Application:
Type: AWS::AppConfig::Application
Properties:
Name: !Ref ApplicationName
Description: 'Feature flags and configuration for applications'
ProdEnvironment:
Type: AWS::AppConfig::Environment
Properties:
ApplicationId: !Ref Application
Name: prod
Description: 'Production environment'
DevEnvironment:
Type: AWS::AppConfig::Environment
Properties:
ApplicationId: !Ref Application
Name: dev
Description: 'Development environment'
ProdAccessRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub 'appconfig-${ApplicationName}-prod'
Description: !Sub 'Cross-account access role for ${ApplicationName} Production AppConfig environment'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: sts:AssumeRole
Principal:
AWS: '*'
Condition:
'ForAnyValue:StringLike':
'aws:PrincipalOrgPaths': !Sub '${organizationPrincipalId}/${organizationRootOuId}/${prodOuId}/*'
Policies:
- PolicyName: AppConfigAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- appconfig:GetConfiguration
- appconfig:StartConfigurationSession
- appconfig:GetLatestConfiguration
Resource: !Sub 'arn:aws:appconfig:*:${AWS::AccountId}:application/${Application}/environment/${ProdEnvironment}/configuration/*'
- Effect: Allow
Action:
- appconfig:GetApplication
- appconfig:GetEnvironment
- appconfig:GetConfigurationProfile
- appconfig:ListApplications
- appconfig:ListConfigurationProfiles
- appconfig:ListEnvironments
- appconfig:ListHostedConfigurationVersions
Resource:
- !Sub 'arn:aws:appconfig:*:${AWS::AccountId}:application/${Application}'
- !Sub 'arn:aws:appconfig:*:${AWS::AccountId}:application/${Application}/environment/*'
- !Sub 'arn:aws:appconfig:*:${AWS::AccountId}:application/${Application}/configurationprofile/*'
DevAccessRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub 'appconfig-${ApplicationName}-dev'
Description: !Sub 'Cross-account access role for ${ApplicationName} Development AppConfig environment'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: sts:AssumeRole
Principal:
AWS: '*'
Condition:
'ForAnyValue:StringLike':
'aws:PrincipalOrgPaths':
- !Sub '${organizationPrincipalId}/${organizationRootOuId}/${sdlcOuId}/*'
- !Sub '${organizationPrincipalId}/${organizationRootOuId}/${sandboxOuId}/*'
Policies:
- PolicyName: AppConfigAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- appconfig:GetConfiguration
- appconfig:StartConfigurationSession
- appconfig:GetLatestConfiguration
Resource: !Sub 'arn:aws:appconfig:*:${AWS::AccountId}:application/${Application}/environment/${DevEnvironment}/configuration/*'
- Effect: Allow
Action:
- appconfig:GetApplication
- appconfig:GetEnvironment
- appconfig:GetConfigurationProfile
- appconfig:ListApplications
- appconfig:ListConfigurationProfiles
- appconfig:ListEnvironments
- appconfig:ListHostedConfigurationVersions
Resource:
- !Sub 'arn:aws:appconfig:*:${AWS::AccountId}:application/${Application}'
- !Sub 'arn:aws:appconfig:*:${AWS::AccountId}:application/${Application}/environment/*'
- !Sub 'arn:aws:appconfig:*:${AWS::AccountId}:application/${Application}/configurationprofile/*'
Outputs:
ApplicationId:
Description: AppConfig Application ID
Value: !Ref Application
ProdEnvironmentId:
Description: Production Environment ID
Value: !Ref ProdEnvironment
DevEnvironmentId:
Description: Development Environment ID
Value: !Ref DevEnvironment
Workload Account Template
Deploy this template to each of your workload accounts (development and production). It creates SSM parameters that applications will use to discover the correct IAM role to assume and environment ID to use.
# Deploy this template to each workload account (dev/prod)
Parameters:
ApplicationName:
Type: String
Description: Name of the AppConfig application
Default: myapp
AppConfigAccountId:
Type: String
Description: AWS Account ID where AppConfig is deployed
Environment:
Type: String
Description: Environment name (dev or prod)
AllowedValues: [dev, prod]
Resources:
AppConfigRoleArnParameter:
Type: AWS::SSM::Parameter
Properties:
Name: !Sub '/appconfig/${ApplicationName}/role-arn'
Type: String
Value: !Sub 'arn:aws:iam::${AppConfigAccountId}:role/appconfig-${ApplicationName}-${Environment}'
Description: !Sub 'Cross-account role ARN for ${ApplicationName} AppConfig access'
AppConfigEnvironmentNameParameter:
Type: AWS::SSM::Parameter
Properties:
Name: !Sub '/appconfig/${ApplicationName}/environment-name'
Type: String
Value: !Ref Environment
Description: !Sub 'AppConfig Environment Name for ${ApplicationName} ${Environment} environment'
When deploying these templates:
- First deploy the AppConfig account template
- Then deploy the workload account template to each account, using:
- The appropriate Environment value (dev/prod)
- The AppConfigAccountId where you deployed the first template
This setup provides a consistent way for applications to discover and access their AppConfig configuration, regardless of which account or environment they're running in. In the next section, we'll look at how applications can use these SSM parameters to access their configuration.
Setting up AppConfig Agent for Lambda functions
CDK code
const APP_NAME = 'myapp';
export function enableAppConfig(fn: LambdaFunction) {
const appConfigRoleArn = StringParameter.fromStringParameterName(
fn,
'AppConfigRoleArn',
`/appconfig/${APP_NAME}/role-arn`,
).stringValue;
const appConfigEnvironment = StringParameter.fromStringParameterName(
fn,
'AppConfigEnvironment',
`/appconfig/${APP_NAME}/environment-name`,
).stringValue;
const appConfigRole = Role.fromRoleArn(fn, 'AppConfigRole', appConfigRoleArn);
fn.addEnvironment('AWS_APPCONFIG_EXTENSION_ROLE_ARN', appConfigRoleArn);
fn.addEnvironment('APPCONFIG_ENVIRONMENT', appConfigEnvironment);
if (!fn.role) {
throw new Error('Lambda role is undefined');
}
appConfigRole.grantAssumeRole(fn.role);
}
export function enableAppConfigLayer(fn: LambdaFunction) {
let appConfigLayer: ILayerVersion;
if (fn.architecture === Architecture.ARM_64) {
appConfigLayer = LayerVersion.fromLayerVersionArn(
fn,
'AppConfigLayer',
'arn:aws:lambda:us-west-2:359756378197:layer:AWS-AppConfig-Extension-Arm64:131',
);
} else {
appConfigLayer = LayerVersion.fromLayerVersionArn(
fn,
'AppConfigLayer',
'arn:aws:lambda:us-west-2:359756378197:layer:AWS-AppConfig-Extension:229',
);
}
fn.addLayers(appConfigLayer);
}
export class DockerImageFunction extends CdkDockerImageFunction {
constructor(scope: Construct, id: string, props: DockerImageFunctionProps) {
super(scope, id, props);
enableAppConfig(this);
}
}
export class NodejsFunction extends CdkNodejsFunction {
constructor(scope: Construct, id: string, props: NodejsFunctionProps) {
super(scope, id, props);
enableAppConfig(this);
enableAppConfigLayer(this);
}
}
export class PythonFunction extends CdkPythonFunction {
constructor(scope: Construct, id: string, props: PythonFunctionProps) {
super(scope, id, props);
enableAppConfig(this);
enableAppConfigLayer(this);
}
}
Docker Image Functions
Docker image functions can't use the AppConfig Agent Lambda Layer, so we need to build our own. This Dockerfile builds a base image with the AppConfig extension, and then builds the application on top of that. The extension is downloaded from the AWS public registry, and then installed into the /opt/extensions directory. The application is then built on top of that base image. The access tokens are only used in the first stage, so they are not included in the final image.
# First stage to fetch AppConfig extension
FROM amazon/aws-cli:latest as appconfig-extension
ARG INCLUDE_APPCONFIG=0
ARG AWS_DEFAULT_REGION
ARG AWS_ACCESS_KEY_ID
ARG AWS_SECRET_ACCESS_KEY
ARG AWS_SESSION_TOKEN
ENV AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION}
ENV AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
ENV AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
ENV AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN}
# Install unzip before trying to use it
RUN yum install -y unzip
# Create directory and download extension if enabled
RUN mkdir -p /opt/extensions && \
if [ "$INCLUDE_APPCONFIG" = "1" ]; then \
aws lambda get-layer-version-by-arn \
--arn arn:aws:lambda:us-west-2:359756378197:layer:AWS-AppConfig-Extension:229 \
--query 'Content.Location' --output text | xargs curl -o layer.zip && \
unzip -j layer.zip "extensions/*" -d /opt/extensions && \
rm layer.zip; \
fi
# Main build stage
FROM python:3.13-slim as base
# Copy the AppConfig extension from the first stage
RUN mkdir -p /opt/extensions
COPY --from=appconfig-extension /opt/extensions /opt/extensions
RUN ls -la /opt/extensions
Conclusion
This architecture provides several key benefits for managing feature flags and configuration across a multi-account AWS organization:
Centralized Management: By hosting AppConfig in a dedicated account, we create a single source of truth for all feature flags and configuration. This makes it easier to manage and audit configuration changes across environments.
-
Secure Access Control: Using AWS Organizations and OU-based IAM roles ensures that:
- Production applications can only access production configurations
- Development applications can only access development configurations
- Access is automatically granted to new accounts added to the appropriate OUs
-
Simple Application Integration:
- Applications only need to know their application name to discover the appropriate role and environment
- The same application code works across all environments without environment-specific configuration
- SSM Parameters provide a consistent interface for applications to discover their AppConfig settings
-
Lambda Integration: The provided CDK constructs make it easy to add AppConfig support to Lambda functions:
- Automatic layer addition for Node.js and Python functions
- Docker image support with a base image that includes the AppConfig extension
- Environment-aware configuration without code changes
This solution achieves the same simplicity of third-party feature flag services while maintaining the benefits of AWS's native services and security model. Teams can deploy applications across accounts and environments without worrying about AppConfig access configuration, while security teams can maintain strict access controls through AWS Organizations.
Bonus, for users of org-formation-cli
Tasks file:
Parameters:
<<: !Include '../../_parameters.yml'
appName:
Type: String
Default: 'myapp'
AppConfigMyApp:
Type: update-stacks
Template: ./application.yml
StackName: !Sub '${resourcePrefix}-appconfig-${appName}'
StackDescription: !Sub 'AppConfig Application, Environments, Roles and Parameters for ${appName}'
TerminationProtection: false
DefaultOrganizationBindingRegion: 'us-west-2'
OrganizationBindings:
ApplicationBinding:
Account: !Ref AppConfigAccount
ProdParametersBinding:
IncludeMasterAccount: false
OrganizationalUnit:
- !Ref ProdOu
Region: 'us-west-2'
DevParametersBinding:
IncludeMasterAccount: false
OrganizationalUnit:
- !Ref SdlcOu
- !Ref SandboxOu
Region: 'us-west-2'
Parameters:
applicationName: !Ref appName
prodOuId: !Ref ProdOu
sdlcOuId: !Ref SdlcOu
sandboxOuId: !Ref SandboxOu
organizationPrincipalId: !Ref organizationPrincipalId
organizationRootOuId: !Ref organizationRootOuId
appConfigAccountId: !Ref AppConfigAccount
application.yml:
Parameters:
applicationName:
Type: String
Description: Name of the AppConfig application
prodOuId:
Type: String
Description: ID of the Production OU
sdlcOuId:
Type: String
Description: ID of the SDLC OU
sandboxOuId:
Type: String
Description: ID of the Sandbox OU
organizationPrincipalId:
Type: String
Description: ID of the Organization Principal
organizationRootOuId:
Type: String
Description: ID of the Organization Root OU
appConfigAccountId:
Type: String
Description: ID of the AppConfig account
Resources:
# AppConfig Resources - Created in AppConfig Account
Application:
Type: AWS::AppConfig::Application
OrganizationBinding: !Ref ApplicationBinding
Properties:
Name: !Ref applicationName
Description: 'Feature flags and configuration for applications'
ProdEnvironment:
Type: AWS::AppConfig::Environment
OrganizationBinding: !Ref ApplicationBinding
Properties:
ApplicationId: !Ref Application
Name: prod
Description: 'Production environment'
DevEnvironment:
Type: AWS::AppConfig::Environment
OrganizationBinding: !Ref ApplicationBinding
Properties:
ApplicationId: !Ref Application
Name: dev
Description: 'Development environment'
ProdAccessRole:
Type: AWS::IAM::Role
OrganizationBinding: !Ref ApplicationBinding
Properties:
RoleName: !Sub 'appconfig-${applicationName}-prod'
Description: !Sub 'Cross-account access role for ${applicationName} Production AppConfig environment'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: sts:AssumeRole
Principal:
AWS: '*'
Condition:
'ForAnyValue:StringLike':
'aws:PrincipalOrgPaths': !Sub '${organizationPrincipalId}/${organizationRootOuId}/${prodOuId}/*'
Policies:
- PolicyName: AppConfigAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- appconfig:GetConfiguration
- appconfig:StartConfigurationSession
- appconfig:GetLatestConfiguration
Resource: !Sub 'arn:aws:appconfig:*:${AWS::AccountId}:application/${Application}/environment/${ProdEnvironment}/configuration/*'
- Effect: Allow
Action:
- appconfig:GetApplication
- appconfig:GetEnvironment
- appconfig:GetConfigurationProfile
- appconfig:ListApplications
- appconfig:ListConfigurationProfiles
- appconfig:ListEnvironments
- appconfig:ListHostedConfigurationVersions
Resource:
- !Sub 'arn:aws:appconfig:*:${AWS::AccountId}:application/${Application}'
- !Sub 'arn:aws:appconfig:*:${AWS::AccountId}:application/${Application}/environment/*'
- !Sub 'arn:aws:appconfig:*:${AWS::AccountId}:application/${Application}/configurationprofile/*'
DevAccessRole:
Type: AWS::IAM::Role
OrganizationBinding: !Ref ApplicationBinding
Properties:
RoleName: !Sub 'appconfig-${applicationName}-dev'
Description: !Sub 'Cross-account access role for ${applicationName} Development AppConfig environment'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: sts:AssumeRole
Principal:
AWS: '*'
Condition:
'ForAnyValue:StringLike':
'aws:PrincipalOrgPaths':
- !Sub '${organizationPrincipalId}/${organizationRootOuId}/${sdlcOuId}/*'
- !Sub '${organizationPrincipalId}/${organizationRootOuId}/${sandboxOuId}/*'
Policies:
- PolicyName: AppConfigAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- appconfig:GetConfiguration
- appconfig:StartConfigurationSession
- appconfig:GetLatestConfiguration
Resource: !Sub 'arn:aws:appconfig:*:${AWS::AccountId}:application/${Application}/environment/${DevEnvironment}/configuration/*'
- Effect: Allow
Action:
- appconfig:GetApplication
- appconfig:GetEnvironment
- appconfig:GetConfigurationProfile
- appconfig:ListApplications
- appconfig:ListConfigurationProfiles
- appconfig:ListEnvironments
- appconfig:ListHostedConfigurationVersions
Resource:
- !Sub 'arn:aws:appconfig:*:${AWS::AccountId}:application/${Application}'
- !Sub 'arn:aws:appconfig:*:${AWS::AccountId}:application/${Application}/environment/*'
- !Sub 'arn:aws:appconfig:*:${AWS::AccountId}:application/${Application}/configurationprofile/*'
# SSM Parameters - Created in Production Accounts
ProdAppConfigRoleParameter:
Type: AWS::SSM::Parameter
OrganizationBinding: !Ref ProdParametersBinding
Properties:
Name: !Sub '/appconfig/${applicationName}/role-name'
Type: String
Value: !Sub 'appconfig-${applicationName}-prod'
Description: !Sub 'Cross-account role name for ${applicationName} AppConfig access'
ProdAppConfigRoleArnParameter:
Type: AWS::SSM::Parameter
OrganizationBinding: !Ref ProdParametersBinding
Properties:
Name: !Sub '/appconfig/${applicationName}/role-arn'
Type: String
Value: !Sub 'arn:aws:iam::${appConfigAccountId}:role/appconfig-${applicationName}-prod'
Description: !Sub 'Cross-account role ARN for ${applicationName} AppConfig access'
ProdAppConfigEnvironmentParameter:
Type: AWS::SSM::Parameter
OrganizationBinding: !Ref ProdParametersBinding
Properties:
Name: !Sub '/appconfig/${applicationName}/environment-id'
Type: String
Value: !Ref ProdEnvironment
Description: !Sub 'AppConfig Environment ID for ${applicationName} Production environment'
ProdAppConfigEnvironmentNameParameter:
Type: AWS::SSM::Parameter
OrganizationBinding: !Ref ProdParametersBinding
Properties:
Name: !Sub '/appconfig/${applicationName}/environment-name'
Type: String
Value: 'prod'
Description: !Sub 'AppConfig Environment Name for ${applicationName} Production environment'
# SSM Parameters - Created in Development Accounts
DevAppConfigRoleParameter:
Type: AWS::SSM::Parameter
OrganizationBinding: !Ref DevParametersBinding
Properties:
Name: !Sub '/appconfig/${applicationName}/role-name'
Type: String
Value: !Sub 'appconfig-${applicationName}-dev'
Description: !Sub 'Cross-account role name for ${applicationName} AppConfig access'
DevAppConfigRoleArnParameter:
Type: AWS::SSM::Parameter
OrganizationBinding: !Ref DevParametersBinding
Properties:
Name: !Sub '/appconfig/${applicationName}/role-arn'
Type: String
Value: !Sub 'arn:aws:iam::${appConfigAccountId}:role/appconfig-${applicationName}-dev'
Description: !Sub 'Cross-account role ARN for ${applicationName} AppConfig access'
DevAppConfigEnvironmentParameter:
Type: AWS::SSM::Parameter
OrganizationBinding: !Ref DevParametersBinding
Properties:
Name: !Sub '/appconfig/${applicationName}/environment-id'
Type: String
Value: !Ref DevEnvironment
Description: !Sub 'AppConfig Environment ID for ${applicationName} Development environment'
DevAppConfigEnvironmentNameParameter:
Type: AWS::SSM::Parameter
OrganizationBinding: !Ref DevParametersBinding
Properties:
Name: !Sub '/appconfig/${applicationName}/environment-name'
Type: String
Value: 'dev'
Description: !Sub 'AppConfig Environment Name for ${applicationName} Development environment'
Outputs:
ApplicationId:
Description: AppConfig Application ID
Value: !Ref Application
Export:
Name: !Sub '${AWS::StackName}-application-id'
ProdEnvironmentId:
Description: Production Environment ID
Value: !Ref ProdEnvironment
Export:
Name: !Sub '${AWS::StackName}-prod-env-id'
DevEnvironmentId:
Description: Development Environment ID
Value: !Ref DevEnvironment
Export:
Name: !Sub '${AWS::StackName}-dev-env-id'
ProdAccessRoleArn:
Description: ARN of the Production access role
Value: !GetAtt ProdAccessRole.Arn
Export:
Name: !Sub '${AWS::StackName}-prod-role-arn'
DevAccessRoleArn:
Description: ARN of the SDLC access role
Value: !GetAtt DevAccessRole.Arn
Export:
Name: !Sub '${AWS::StackName}-dev-role-arn'
Top comments (0)