aws-cdk-conf-n-tags
v0.1.2
Published
Per-environment configuration loading (schema-validated) and stack tagging for AWS CDK apps.
Maintainers
Readme
aws-cdk-conf-n-tags
Per-environment configuration loading and stack tagging for AWS CDK apps.
- One YAML file per environment, selected with
--context env=<name>. - Schema-validated with zod — typos and bad values fail loudly at synth time, and the validated config is fully typed.
- Account/region guard — refuses to synth an environment against the wrong AWS account or region.
- Immutable config — the loaded values are frozen; this is a config object, not a place to stash resource references.
- Centralized tagging — apply arbitrary tags plus auto
CreatedBy/WorkDirtags to every resource in a stack.
Install
This package is used inside an AWS CDK app. If you don't have one yet, scaffold it first with the CDK CLI in an empty directory:
mkdir my-infra && cd my-infra
cdk init app --language typescriptThen, from inside that CDK app directory (where package.json and cdk.json live),
install this package:
npm install aws-cdk-conf-n-tags zodThis both adds the entries to your
package.jsondependenciesand installs them, in one step (modern npm writes topackage.jsonby default — no--saveneeded). If you prefer, you can instead add the dependencies topackage.jsonby hand and then runnpm installwith no arguments.
aws-cdk-lib (^2), constructs (^10), and zod (^4) are peer dependencies.
Your CDK app already has the first two; install zod yourself — you import it to declare
your schema, and a single shared zod instance is required for validation to work
(a second copy would break the internal schema checks).
Quick start
1. Declare your schema
Extend BaseConfigSchema (which already covers awsAccount, awsRegion, tags, and
the tag-name overrides) with the parameters your stack needs:
// config-schema.ts
import { BaseConfigSchema } from 'aws-cdk-conf-n-tags';
import { z } from 'zod';
export const ConfigSchema = BaseConfigSchema.extend({
vpcId: z.string(),
asgMinCapacity: z.number().int().default(1),
asgMaxCapacity: z.number().int().default(1),
ec2InstanceType: z.string().default('t4g.micro'),
auroraEngineVersion: z.enum(['5.7', '8.0']).default('5.7'),
serverlessMinCapacity: z.number().min(0.5).default(1),
certificateArns: z.array(z.string()).optional(),
});2. Load and use it in your stack
import { Stack, type StackProps } from 'aws-cdk-lib';
import { Vpc } from 'aws-cdk-lib/aws-ec2';
import type { Construct } from 'constructs';
import { Config } from 'aws-cdk-conf-n-tags';
import { ConfigSchema } from './config-schema.js';
import { MyAsg } from './my-asg.js'; // your own construct
export class MyStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// Resolves env from `--context env=...`, reads config/<env>.yaml,
// validates it, and checks the account/region. `conf.values` is typed + frozen.
const conf = Config.load(this, ConfigSchema);
// Tag every resource in the stack (config tags + auto CreatedBy/WorkDir).
conf.tagStack(this);
// Pass config values to constructs explicitly.
const vpc = Vpc.fromLookup(this, 'vpc', { vpcId: conf.values.vpcId });
new MyAsg(this, 'asg', { vpc, min: conf.values.asgMinCapacity });
}
}3. Add a config file per environment
# config/production.yaml
awsAccount: "012345678901" # quote it — preserves leading zeros
awsRegion: us-west-2
vpcId: vpc-0abc123def4567890
asgMinCapacity: 2
auroraEngineVersion: "8.0"
tags:
Product: my-product
Cluster: web-001
Env: Production
RetentionDays: 28 # non-string values are coerced to strings when taggingDeploy with:
cdk deploy --context env=productionUsing from JavaScript
The package ships both ESM and CommonJS builds, so it works in JavaScript CDK apps
(cdk init app --language javascript) just as well as TypeScript ones. The API is
identical — you only give up the compile-time layer (z.infer types and tsc
checking). Runtime validation still catches typos, bad types, and out-of-range values
and applies defaults, because that is zod behavior, not TypeScript.
ESM JavaScript (a project with "type": "module" in its package.json):
import { Stack } from 'aws-cdk-lib';
import { Config, BaseConfigSchema } from 'aws-cdk-conf-n-tags';
import { z } from 'zod';
const ConfigSchema = BaseConfigSchema.extend({
vpcId: z.string(),
asgMinCapacity: z.number().int().default(1),
});
export class MyStack extends Stack {
constructor(scope, id, props) {
super(scope, id, props);
const conf = Config.load(this, ConfigSchema);
conf.tagStack(this);
// conf.values.vpcId, conf.values.asgMinCapacity, ...
}
}CommonJS JavaScript (the default cdk init --language javascript template) is the same,
with require:
const { Config, BaseConfigSchema } = require('aws-cdk-conf-n-tags');
const { z } = require('zod');Tip: even in
.jsfiles, editors like VS Code read the package's bundled type declarations and offer autocomplete and hover docs onconf.values.
How it behaves
- Environment selection —
Config.loadreads the env name from CDK context (--context env=<name>; the key is configurable). It then loadsconfig/<env>.yamlrelative to the current working directory. - Validation — unknown keys are rejected by default (
unknownKeys: 'strict'), so a mistyped key likeasgMinCapcityerrors instead of silently using a default. Types, enums, numeric ranges, andrequired/defaultare all enforced by your schema. Switch to'strip'or'passthrough'if you need leniency. - Account/region guard —
awsAccount/awsRegionin the file are checked againstCDK_DEFAULT_ACCOUNT/CDK_DEFAULT_REGION(override via options, or disable withverifyAccountRegion: false). - Immutability —
conf.valuesisObject.freeze'd, so it can't double as a mutable bag for created resources. Share those via explicit args or theStackResourceshelper instead.
Sharing resources between constructs
Because conf.values is frozen, it can't accumulate created resources the way a single
mutable config object used to. To hand resources (VPC, database, …) from one construct to
the next, you have two options.
Explicit props (most type-safe). Give each construct exactly what it needs:
const vpc = Vpc.fromLookup(this, 'vpc', { vpcId: conf.values.vpcId });
const db = new MyDb(this, 'db', { vpc, conf });
new MyBackup(this, 'backup', { dbCluster: db.cluster });StackResources (one bag, fail-loud). If you prefer threading a single object
through every construct, use the exported StackResources container. Unlike a plain
object, reading a resource that hasn't been created yet throws a clear error instead of
yielding undefined — valuable in JavaScript stacks where there's no compiler to catch it:
import { Config, StackResources } from 'aws-cdk-conf-n-tags';
const res = new StackResources();
res.set('vpc', Vpc.fromLookup(this, 'vpc', { vpcId: conf.values.vpcId }));
const db = res.set('db', new MyDb(this, 'db', { conf, res })); // set() returns the value
new MyBackup(this, 'backup', { conf, res }); // internally reads res.get('db').cluster
// res.get('cache') before the cache is created throws:
// Resource "cache" was requested before it was created. Check the order ...set(key, value) stores and returns the value; get(key) returns it or throws;
has(key) checks; keys() lists what's been set. In TypeScript, parameterize it for
typed keys and values: new StackResources<{ vpc: IVpc; db: MyDb }>().
API
Config.load(scope, schema, options?)
Returns a Config with:
| Member | Description |
|---|---|
| conf.values | The validated, frozen config (typed as z.infer<typeof schema>). |
| conf.environment | The resolved environment name. |
| conf.path | Absolute path of the file that was loaded. |
| conf.tagStack(scope, options?) | Tag a stack using this config's tags + auto-tags. |
options (all optional):
| Option | Default | Description |
|---|---|---|
| contextKey | 'env' | CDK context key holding the environment name. |
| environment | — | Provide the env name explicitly (skips context). |
| configDir | 'config' | Directory holding <env>.yaml, relative to cwd. |
| configPath | — | Explicit file path (overrides configDir). |
| cwd | process.cwd() | Base directory for relative paths. |
| unknownKeys | 'strict' | 'strict' | 'strip' | 'passthrough'. |
| verifyAccountRegion | true | Cross-check account/region. |
| account / region | CDK_DEFAULT_* | Expected account/region. |
tagStack(scope, options?)
Standalone tagging (no Config needed). Applies the working-directory tag, the
created-by tag, then the explicit tags map, to every taggable resource in scope.
import { tagStack } from 'aws-cdk-conf-n-tags';
tagStack(stack, {
tags: { Product: 'my-product', Env: 'Production' },
auto: { workDir: false }, // disable individual auto-tags
tagNames: { createdBy: 'Blame' }, // rename auto-tag keys
providers: { createdBy: () => 'ci-bot' }, // override how a value is computed
});By default CreatedBy is derived from git config user.name / user.email (falling
back to $USER), and WorkDir is process.cwd().
StackResources<T>
A fail-loud container for resources created during synthesis — see Sharing resources between constructs.
| Method | Description |
|---|---|
| set(key, value) | Store a resource and return it. |
| get(key) | Return it, or throw if it was never set. |
| has(key) | Whether a resource has been set. |
| keys() | The keys set so far. |
loadConfig(schema, options)
The validation core without any CDK scope — useful for tests or non-CDK tooling.
Returns { values, path }. See LoadOptions for the option shape.
Migrating from the original Config class
If you used the original JavaScript Config (which flattened every YAML key onto the
instance and doubled as a mutable dependency bag):
- Read config via
conf.values.<key>instead ofconf.<key>. - Declare a zod schema instead of relying on scattered
|| defaultfallbacks. - Stop writing resources back onto the config object (
conf.vpc = vpc); pass them to constructs explicitly. setTags(stack)→conf.tagStack(stack).
Development
npm install
npm run build # tsup → dist (ESM + CJS + d.ts/d.cts)
npm test # vitest
npm run typecheck # tsc --noEmit
npm run check:exports # publint + attwRun a single test: npx vitest run test/loader.test.ts (or -t "<name>").
License
BSD-3-Clause © Eduard Moskvin
