跳到正文
DEV Community· Sascha Mirow-Stanitzok·· 3 小時前AI 評分58

AWS AppConfig 如何在 Lambda 安全分階段推出功能旗標

AppConfig feature flags in Lambda: safe rollouts

AI 導讀

作者示範如何以 AWS CDK 和 TypeScript,在 Lambda 使用 AWS AppConfig 功能旗標分階段部署,並在 CloudWatch 告警觸發時自動回滾。

正文

AppConfig feature flags let you switch on new features in Lambda without redeploying, and they do it step by step instead of for everyone at once. That last part is why I stop using Parameter Store once a switch changes real behavior. A value in Parameter Store applies everywhere immediately. A flag in AppConfig first reaches a share of your execution environments, gets watched, and rolls back by itself if errors show up.

In this article you build all of that with AWS CDK and TypeScript: a small price API for a fictional cinema with a Sunday discount as a feature flag, a kill switch, a gradual rollout and a CloudWatch alarm that pulls the brake when things go wrong. You will also learn what it costs and where I would have tripped up myself.

In short

AWS AppConfig delivers feature flags to your Lambda functions in a controlled way: the AppConfig Agent extension keeps the flags in a local cache, and a deployment strategy rolls changes out in stages, including automatic rollback through a CloudWatch alarm. The biggest pitfall is expecting a flag to apply everywhere instantly – by design, it does not.

What you will learn in this article

  • How AppConfig, the Lambda extension and deployment strategies work together
  • Create feature flags with CDK and read them safely in TypeScript
  • Build a gradual rollout with automatic rollback through an alarm
  • Set up a kill switch that takes effect in seconds instead of minutes
  • What AppConfig costs and which mistakes to avoid

What are feature flags, and when is Parameter Store not enough?

A feature flag (or feature toggle) is a switch in your code: when it is on, the new path runs; when it is off, the old one does. Sounds trivial. The point is that you flip the switch at runtime without shipping new code. That separates two things that usually stick together: the deployment (code is in production) and the release (users see the feature).

For simple values I showed you in Parameter Store in Lambda: load configuration the right way how to load and cache switches and prices cleanly. That works well, but it has one property that hurts with risky changes: every change hits all functions at the same time, as soon as their cache expires. If the new path has a bug, every user is affected right away.

AWS AppConfig is part of AWS Systems Manager and is built for exactly this case. It checks new configurations against a schema before delivery, distributes them in stages according to a strategy, and can roll back when an alarm fires. My rule of thumb is therefore: Parameter Store is for configuration that rarely changes and is harmless. AppConfig is for switches that change behavior and can go wrong.

Parameter Store AppConfig feature flags
Change reaches all functions once the cache expires functions gradually, following a strategy
Validation none (except data type) JSON schema, limits per attribute
Automatic rollback no yes, through CloudWatch alarms
Access from Lambda SDK call or extension local HTTP call to the extension
Typical use hourly prices, log level, endpoints new features, experiments, kill switch

How do AppConfig feature flags work in Lambda?

AppConfig organizes everything in three levels. The application is the container, for example “cinema-tickets”. Below it sit environments such as “dev” and “prod”. The configuration profile holds the actual data, in our case of type AWS.AppConfig.FeatureFlags. Every change to the profile creates a new, immutable version, and only a deployment brings that version into an environment.

Diagram: Lambda handler reads AppConfig feature flags via the local agent; a CloudWatch alarm triggers rollback

The handler only asks the local agent; the agent asks AppConfig.

The AppConfig Agent extension

In Lambda you do not read the flags through the AppConfig API directly. Instead, you attach the AppConfig Agent extension to your function as a layer. A Lambda extension is a companion process that runs in the same execution environment as your handler. The agent starts during init, fetches the configuration, puts it into a local cache and serves it at http://localhost:2772. So your code only makes a local HTTP call that never leaves the environment.

In the background the agent checks for news at a fixed interval, every 45 seconds by default. If AppConfig is unreachable for a moment, it keeps serving the last known version. On top of that it handles the session management with StartConfigurationSession and GetLatestConfiguration, which you would otherwise have to build yourself.

Deployment strategy and bake time

The deployment strategy defines how fast a new version spreads. With a linear strategy of 20 percent over ten minutes, another fifth of the polling clients gets the new version every two minutes. In Lambda these clients are the individual execution environments, because each one has its own agent and its own cache. After 100 percent comes the bake time: an observation phase in which AppConfig keeps watching for alarms before it considers the deployment complete.

If a linked CloudWatch alarm fires during this time, AppConfig restores the previous version. The animation shows the flow in fast motion.

🎬 Gradual rollout with automatic rollback – this animated diagram only runs on my site: watch it on sascha-ms.de

Set up AppConfig feature flags with CDK: step by step

Now it gets practical. We build an HTTP API with a Lambda function that calculates cinema ticket prices. The flag sunday-discount grants a discount on Sundays, and the size of the discount lives as an attribute in the flag. The flag sales-stop is the kill switch: when it is on, the API answers with 503.

Prerequisites

  • Node.js 22 or newer and npm
  • An AWS account with configured credentials (aws configure or SSO)
  • An account and region bootstrapped once with npx cdk bootstrap
  • Basic knowledge of TypeScript and Lambda

Step 1: Create the project

Create an empty project and install CDK, TypeScript and the type definitions. tsx runs TypeScript directly without compiling first.

mkdir cinema-flags && cd cinema-flags
npm init -y
npm install aws-cdk-lib constructs
npm install -D aws-cdk typescript tsx esbuild @types/node @types/aws-lambda
mkdir -p bin lib src test flags

Add a tsconfig.json and a cdk.json that tells CDK how to start the app:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "lib": ["ES2022", "DOM"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "dist",
    "types": ["node"]
  },
  "include": ["bin", "lib", "src", "test"]
}
{
  "app": "npx tsx bin/app.ts"
}

Step 2: Describe the flags as JSON

The feature flag format has three parts: version (always "1"), flags with the definitions and values with the current values. The constraints are especially useful: AppConfig rejects a new version if the discount is set to 80 percent, for example. A typo in the percentage never reaches production.

{
  "version": "1",
  "flags": {
    "sunday-discount": {
      "name": "Sunday discount",
      "description": "New pricing rule: discount on all Sunday screenings",
      "attributes": {
        "discount-percent": {
          "constraints": { "type": "number", "minimum": 0, "maximum": 50, "required": true }
        }
      }
    },
    "sales-stop": {
      "name": "Sales stop",
      "description": "Kill switch: temporarily rejects all price requests"
    }
  },
  "values": {
    "sunday-discount": { "enabled": false, "discount-percent": 20 },
    "sales-stop": { "enabled": false }
  }
}

Flag and attribute names must start with a lowercase letter and may contain letters, digits, underscores and hyphens. Both flags start switched off. That way the new code lands “dark” in production first.

Step 3: Read flags safely in the handler

The most important part of the code looks unremarkable: what happens if the agent does not answer? My answer is always the same. The function falls back to safe defaults, meaning yesterday’s behavior. In addition, I set a tight timeout so that a hanging call does not block the whole request.

// src/flags.ts
// Reads feature flags through the local AppConfig Agent (Lambda extension).

export interface TicketFlags {
  sundayDiscount: { enabled: boolean; discountPercent: number };
  salesStop: { enabled: boolean };
}

// Safe defaults: if the agent does not answer, behavior stays as it was.
export const SAFE_DEFAULTS: TicketFlags = {
  sundayDiscount: { enabled: false, discountPercent: 0 },
  salesStop: { enabled: false },
};

interface RawFlag {
  enabled?: boolean;
  [attribute: string]: unknown;
}

export function buildFlagsUrl(env: NodeJS.ProcessEnv = process.env): string {
  const port = env.AWS_APPCONFIG_EXTENSION_HTTP_PORT ?? "2772";
  const app = env.APPCONFIG_APPLICATION;
  const stage = env.APPCONFIG_ENVIRONMENT;
  const profile = env.APPCONFIG_PROFILE;
  if (!app || !stage || !profile) {
    throw new Error("APPCONFIG_APPLICATION, APPCONFIG_ENVIRONMENT and APPCONFIG_PROFILE must be set");
  }
  return `http://localhost:${port}/applications/${app}/environments/${stage}/configurations/${profile}`;
}

export function parseFlags(raw: Record<string, RawFlag>): TicketFlags {
  const sunday = raw["sunday-discount"] ?? {};
  const stop = raw["sales-stop"] ?? {};
  const percent = Number(sunday["discount-percent"] ?? 0);
  return {
    sundayDiscount: {
      enabled: sunday.enabled === true,
      // Second safeguard on top of the validation in AppConfig
      discountPercent: Number.isFinite(percent) ? Math.min(Math.max(percent, 0), 50) : 0,
    },
    salesStop: { enabled: stop.enabled === true },
  };
}

export async function loadFlags(url: string, timeoutMs = 500): Promise<TicketFlags> {
  try {
    const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
    if (!response.ok) {
      console.warn(JSON.stringify({ msg: "AppConfig Agent returned an error", status: response.status }));
      return SAFE_DEFAULTS;
    }
    return parseFlags((await response.json()) as Record<string, RawFlag>);
  } catch (error) {
    console.warn(JSON.stringify({ msg: "AppConfig Agent unreachable, using defaults", error: String(error) }));
    return SAFE_DEFAULTS;
  }
}

For feature flag profiles the agent only returns the values, so an object like {"sunday-discount":{"enabled":false,"discount-percent":20}}. The function parseFlags turns that into a typed object and caps the discount once more on its own. Belt and braces, especially when money is involved.

Step 4: Pricing logic and handler

The pricing logic stays free of AWS, so you can test it without mocks. It receives the flags as a parameter:

// src/pricing.ts
// Pure pricing logic without AWS dependencies, so it is easy to test.
import type { TicketFlags } from "./flags";

export type SeatType = "standard" | "premium";

const PRICE_CENT: Record<SeatType, number> = { standard: 1100, premium: 1450 };

export function quotePrice(seat: SeatType, tickets: number, showtime: Date, flags: TicketFlags): number {
  const base = PRICE_CENT[seat] * tickets;
  const isSunday = showtime.getUTCDay() === 0;
  if (flags.sundayDiscount.enabled && isSunday) {
    return Math.round(base * (1 - flags.sundayDiscount.discountPercent / 100));
  }
  return base;
}

The handler loads the flags on every invocation. That is fine here, because the call only goes to the agent’s local cache. I build the URL once outside the handler, so missing environment variables show up during the cold start.

// src/price-handler.ts
import type { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from "aws-lambda";
import { buildFlagsUrl, loadFlags } from "./flags";
import { quotePrice, type SeatType } from "./pricing";

const FLAGS_URL = buildFlagsUrl();

export const handler = async (event: APIGatewayProxyEventV2): Promise<APIGatewayProxyResultV2> => {
  const flags = await loadFlags(FLAGS_URL);

  if (flags.salesStop.enabled) {
    return { statusCode: 503, body: JSON.stringify({ message: "Ticket sales are paused right now. Please try again later." }) };
  }

  const seat = (event.queryStringParameters?.seat ?? "standard") as SeatType;
  const tickets = Number(event.queryStringParameters?.tickets ?? "1");
  const showtime = new Date(event.queryStringParameters?.showtime ?? Date.now());
  if (!["standard", "premium"].includes(seat) || !Number.isInteger(tickets) || tickets < 1 || tickets > 10 || Number.isNaN(showtime.getTime())) {
    return { statusCode: 400, body: JSON.stringify({ message: "seat: standard or premium, tickets: 1 to 10, showtime: ISO date" }) };
  }

  const priceCent = quotePrice(seat, tickets, showtime, flags);
  return {
    statusCode: 200,
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ seat, tickets, priceCent, sundayDiscount: flags.sundayDiscount.enabled }),
  };
};

Step 5: The CDK stack

The stack contains the real work: application, strategy, function with layer, alarm, environment with monitor, the flag profile and the permissions. CDK ships ready-made L2 constructs for AppConfig that save a lot of boilerplate.

// lib/cinema-flags-stack.ts
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { CfnOutput, Duration, Stack, type StackProps } from "aws-cdk-lib";
import * as appconfig from "aws-cdk-lib/aws-appconfig";
import * as apigwv2 from "aws-cdk-lib/aws-apigatewayv2";
import { HttpLambdaIntegration } from "aws-cdk-lib/aws-apigatewayv2-integrations";
import * as cloudwatch from "aws-cdk-lib/aws-cloudwatch";
import * as iam from "aws-cdk-lib/aws-iam";
import * as lambda from "aws-cdk-lib/aws-lambda";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
import type { Construct } from "constructs";

export class CinemaFlagsStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const appName = "cinema-tickets";
    const stageName = "prod";
    const profileName = "ticket-flags";

    // 1. AppConfig application
    const app = new appconfig.Application(this, "CinemaApp", { applicationName: appName });

    // 2. Rollout strategy: 20 % more every 2 minutes, then 5 minutes of observation
    const strategy = new appconfig.DeploymentStrategy(this, "GradualRollout", {
      deploymentStrategyName: "cinema-linear-20",
      rolloutStrategy: appconfig.RolloutStrategy.linear({
        growthFactor: 20,
        deploymentDuration: Duration.minutes(10),
        finalBakeTime: Duration.minutes(5),
      }),
    });

    // 3. Lambda function with the AppConfig Agent extension as a layer.
    // The layer list in CDK can lag behind the latest version:
    // "cdk deploy -c appConfigLayerArn=<ARN>" overrides it.
    const layerArn: string | undefined =
      this.node.tryGetContext("appConfigLayerArn") ??
      appconfig.Application.getLambdaLayerVersionArn(this.region, appconfig.Platform.ARM_64);
    if (!layerArn) {
      throw new Error(`No AppConfig layer ARN found for region ${this.region}`);
    }

    const priceFn = new NodejsFunction(this, "PriceFunction", {
      entry: join(__dirname, "../src/price-handler.ts"),
      runtime: lambda.Runtime.NODEJS_22_X,
      architecture: lambda.Architecture.ARM_64,
      memorySize: 256,
      timeout: Duration.seconds(5),
      layers: [lambda.LayerVersion.fromLayerVersionArn(this, "AppConfigAgentLayer", layerArn)],
      environment: {
        APPCONFIG_APPLICATION: appName,
        APPCONFIG_ENVIRONMENT: stageName,
        APPCONFIG_PROFILE: profileName,
        AWS_APPCONFIG_EXTENSION_POLL_INTERVAL_SECONDS: "30",
        AWS_APPCONFIG_EXTENSION_PREFETCH_LIST: `/applications/${appName}/environments/${stageName}/configurations/${profileName}`,
      },
    });

    // 4. Alarm: if the function throws errors, AppConfig rolls back the running change
    const errorAlarm = new cloudwatch.Alarm(this, "PriceErrorsAlarm", {
      metric: priceFn.metricErrors({ period: Duration.minutes(1) }),
      threshold: 3,
      evaluationPeriods: 1,
      treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
      alarmDescription: "Errors in the price function: AppConfig rolls back running flag changes",
    });

    const env = new appconfig.Environment(this, "ProdEnv", {
      application: app,
      environmentName: stageName,
      monitors: [appconfig.Monitor.fromCloudWatchAlarm(errorAlarm)],
    });

    new appconfig.HostedConfiguration(this, "TicketFlags", {
      application: app,
      name: profileName,
      type: appconfig.ConfigurationType.FEATURE_FLAGS,
      content: appconfig.ConfigurationContent.fromInlineJson(
        readFileSync(join(__dirname, "../flags/feature-flags.json"), "utf8"),
      ),
      deploymentStrategy: strategy,
      deployTo: [env],
    });

    // 5. Read-only permissions for the extension, limited to this application.
    // Deliberately not env.grantReadConfig(): environment -> alarm -> function -> role -> environment
    // would be a circular dependency in CloudFormation.
    priceFn.addToRolePolicy(
      new iam.PolicyStatement({
        actions: ["appconfig:StartConfigurationSession", "appconfig:GetLatestConfiguration"],
        resources: [
          this.formatArn({
            service: "appconfig",
            resource: "application",
            resourceName: `${app.applicationId}/environment/*/configuration/*`,
          }),
        ],
      }),
    );

    // 6. HTTP API in front
    const api = new apigwv2.HttpApi(this, "TicketApi");
    api.addRoutes({
      path: "/price",
      methods: [apigwv2.HttpMethod.GET],
      integration: new HttpLambdaIntegration("PriceIntegration", priceFn),
    });

    new CfnOutput(this, "PriceUrl", { value: `${api.apiEndpoint}/price?seat=premium&tickets=2` });
    new CfnOutput(this, "ApplicationId", { value: app.applicationId });
    new CfnOutput(this, "EnvironmentId", { value: env.environmentId });
    new CfnOutput(this, "DeploymentStrategyId", { value: strategy.deploymentStrategyId });
  }
}

Three places deserve a second look. First, HostedConfiguration with deployTo creates an AppConfig deployment with the given strategy during cdk deploy. Second, Monitor.fromCloudWatchAlarm automatically creates an IAM role that allows AppConfig to read the alarm. Third, I grant the read permissions by hand. You will find the reason further down under typical mistakes.

The app itself is short:

// bin/app.ts
import { App } from "aws-cdk-lib";
import { CinemaFlagsStack } from "../lib/cinema-flags-stack";

const app = new App();
new CinemaFlagsStack(app, "CinemaFlagsStack", {
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: process.env.CDK_DEFAULT_REGION ?? "eu-central-1",
  },
});

Step 6: Test, synthesize, deploy

The tests run with the built-in Node.js test runner. A small local HTTP server plays the agent, so the fallback on timeout gets tested too:

// test/flags.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { createServer } from "node:http";
import type { AddressInfo } from "node:net";
import { loadFlags, parseFlags, SAFE_DEFAULTS } from "../src/flags";
import { quotePrice } from "../src/pricing";

const SUNDAY = new Date("2026-11-01T18:00:00Z");
const MONDAY = new Date("2026-11-02T18:00:00Z");

test("Sunday discount applies only when the flag is on", () => {
  const on = parseFlags({ "sunday-discount": { enabled: true, "discount-percent": 20 } });
  assert.equal(quotePrice("premium", 2, SUNDAY, on), 2320);
  assert.equal(quotePrice("premium", 2, MONDAY, on), 2900);
  assert.equal(quotePrice("premium", 2, SUNDAY, SAFE_DEFAULTS), 2900);
});

test("Discounts that are too high are capped at 50 percent", () => {
  const flags = parseFlags({ "sunday-discount": { enabled: true, "discount-percent": 90 } });
  assert.equal(flags.sundayDiscount.discountPercent, 50);
});

test("loadFlags reads the agent response", async () => {
  const server = createServer((_req, res) => {
    res.setHeader("content-type", "application/json");
    res.end(JSON.stringify({ "sales-stop": { enabled: true } }));
  }).listen(0);
  const { port } = server.address() as AddressInfo;
  const flags = await loadFlags(`http://127.0.0.1:${port}/applications/a/environments/b/configurations/c`);
  server.close();
  assert.equal(flags.salesStop.enabled, true);
  assert.equal(flags.sundayDiscount.enabled, false);
});

test("loadFlags falls back to safe defaults on timeout", async () => {
  const server = createServer(() => {
    /* never answers on purpose */
  }).listen(0);
  const { port } = server.address() as AddressInfo;
  const flags = await loadFlags(`http://127.0.0.1:${port}/x`, 100);
  server.closeAllConnections();
  server.close();
  assert.deepEqual(flags, SAFE_DEFAULTS);
});
npx tsc --noEmit
npx tsx --test test/*.test.ts
npx cdk synth
npx cdk deploy

Expected result: four green tests, a synthesized template without warnings and, after the deploy, the output PriceUrl. Because the first AppConfig deployment uses the strategy, the first cdk deploy takes a bit longer. A call should look like this:

curl "https://<your-api-id>.execute-api.eu-central-1.amazonaws.com/price?seat=premium&tickets=2&showtime=2026-11-08T18:00:00Z"
# {"seat":"premium","tickets":2,"priceCent":2900,"sundayDiscount":false}

Step 7: Switch a flag on in production

Now comes the actual benefit. You can flip the flag in the AWS console or through the CLI. With the CLI you create a new version and start a deployment. For feature flags you pass the complete file with definitions and values:

# change the value in the JSON file: "sunday-discount": { "enabled": true, ... }
aws appconfig create-hosted-configuration-version \
  --application-id <ApplicationId> \
  --configuration-profile-id <ProfileId> \
  --content-type "application/json" \
  --content fileb://flags/feature-flags.json \
  version.json
cat version.json   # contains the new version number

aws appconfig start-deployment \
  --application-id <ApplicationId> \
  --environment-id <EnvironmentId> \
  --deployment-strategy-id <DeploymentStrategyId> \
  --configuration-profile-id <ProfileId> \
  --configuration-version <VersionNumber>

You will find the IDs for application, environment and strategy in the stack outputs, and the profile ID in the AppConfig console. aws appconfig get-deployment shows the progress in percent. By the way, flipping a flag in the console needs no JSON at all: there is a dedicated feature flag editor that product owners can use too.

Important: if you change the JSON file in the repository and run cdk deploy, the same thing happens through the infrastructure path. Agree as a team which path is the source of truth. Otherwise the next pipeline deployment overwrites the flag someone flipped in the console.

How do I build a kill switch with AppConfig?

For the sales-stop flag a ten-minute rollout would be absurd. When the payment system is on fire, sales should stop right away. For that you simply pick a different strategy when deploying. AppConfig ships predefined strategies, including AppConfig.AllAtOnce, which delivers the version without stages.

aws appconfig start-deployment \
  --application-id <ApplicationId> \
  --environment-id <EnvironmentId> \
  --deployment-strategy-id AppConfig.AllAtOnce \
  --configuration-profile-id <ProfileId> \
  --configuration-version <VersionNumber>

After that, only the agent’s poll interval decides how fast the switch takes effect. In the stack it is set to 30 seconds. AWS itself recommends around 15 seconds for kill switches and advises against going below 5 seconds. If a deployment is running that you want to cancel, aws appconfig stop-deployment helps: clients then get the previous version again.

Typical mistakes with AppConfig feature flags

Circular dependency in the CDK stack

The obvious way to grant permissions would be env.grantReadConfig(fn). In this stack, however, that creates a loop: the environment needs the alarm, the alarm needs the function, the function needs its role, and the role points back to the environment. CloudFormation then reports a circular dependency. A policy at the application level (environment/*) solves this and still stays limited to this one application.

Outdated layer ARN

Application.getLambdaLayerVersionArn() is convenient, but it uses a list that ships with the CDK version. The actual latest version of the extension can be well ahead. Check the ARN in the official version list and pass it with -c appConfigLayerArn=... if needed. Also make sure architecture and layer match: an arm64 function needs the layer with the Arm64 suffix.

No fallback when the agent stays silent

Writing await fetch(...) without a timeout and without try/catch turns a configuration problem into an outage. The agent is very reliable, but not infallible, for example when permissions are missing and it never gets a first version. Safe defaults cost five lines.

Expecting “everywhere, instantly”

A flag you switch on only applies everywhere after the strategy has finished plus the poll interval. During the rollout some requests see the new price and others the old one, because Lambda spreads requests across several execution environments. For a discount that is fine. For something that must stay consistent across several requests, such as a new data format, you need a different solution or a per-user rollout. AppConfig now offers multi-variant flags with rules for exactly this case.

Zombie flags in the code

Every flag is a branch that needs testing. A Sunday discount that has been on for a year no longer belongs behind a flag. AppConfig can mark flags with _deprecation as “planned”. In addition, I would create a cleanup ticket together with every new flag.

What do AppConfig feature flags cost?

AppConfig charges by usage, with two items: configuration requests cost USD 0.0000002 per request, configurations received cost USD 0.0008 each. A configuration counts as received whenever a client actually gets new data, so at a cold start and after every change.

Let us run rough numbers for our cinema. Assume that on average 10 execution environments are warm during the month and poll every 30 seconds. That is 10 × 2 × 60 × 24 × 30 = 864,000 requests, around USD 0.17. Cold starts matter more: every new agent fetches the configuration once. With 5,000 cold starts a month you add USD 4. In other words, the lever is the number of cold starts rather than the poll interval. Exact prices and the free tier are listed on the Systems Manager pricing page.

My take

I like AppConfig because it takes a problem seriously that many teams underestimate: configuration changes are deployments. They can break just as much as code, yet they often run without review, without tests and without a way back. The combination of schema, stages and alarm brings them into the same safe process.

Still, I would not move every project to AppConfig right away. For two or three harmless switches, Parameter Store is simpler and usually free. Switching pays off once flags affect revenue, data flows or interfaces, or once more than one team turns the knobs. And a flag does not replace tests: it only limits the damage when the tests missed something.

What I would not underestimate is the question of ownership. Who may flip which flag in production, and where is that recorded? That is not an AWS topic but a team agreement. It belongs in the same place as the code, though.

FAQ

What is the difference between AppConfig and Parameter Store?

Both store configuration. AppConfig validates new versions against a schema, distributes them gradually and can roll back through an alarm. Parameter Store delivers every change to everyone immediately and is meant for simple, harmless values.

How fast does a flag change take effect in Lambda?

That depends on the deployment strategy and the agent’s poll interval. With AppConfig.AllAtOnce and a 15-second interval it is usually a few seconds up to about a quarter of a minute. With a linear strategy over ten minutes it takes correspondingly longer.

Do I need the Lambda extension for AppConfig?

No, you can also query the data plane with the SDK using StartConfigurationSession and GetLatestConfiguration. Then you have to manage session, token and cache yourself. The extension does that for you and costs nothing extra.

Can I switch a flag on only for certain users?

Yes. With multi-variant flags you define rules that AppConfig evaluates against a context you send along, such as a user ID or a segment. The agent then receives the context with each request.

What does AppConfig cost for a small Lambda application?

Usually a few US dollars a month. You pay for configuration requests and configurations received. In Lambda, cold starts are the main driver, because every new agent fetches the configuration once.

Your next step

Pick one switch in your current project that lives in an environment variable or Parameter Store today and has caused trouble before. Move exactly that one into an AppConfig profile, with constraints and a linear strategy in your test environment. You will quickly see whether the extra effort pays off for your team. If you already catch Lambda failures cleanly, for example with Lambda Destinations, you almost have the right metrics for the rollback alarm in place.

Now I am curious: who is allowed to flip feature flags in production on your team, and how do you record who did it and when?

All code from this article, ready to run: smirowstanitzok/sascha-ms-examples

Sources

This post was created with AI assistance.


👉 Next step: Curious what your own Lambda functions cost? Try my Lambda cost calculator – it runs right in your browser.

Originally published on sascha-ms.de. More articles on serverless, AI in everyday development and leading tech teams: sascha-ms.de/en

來源:DEV Community · dev.to