diff --git a/README.md b/README.md index 224c209..2117739 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,22 @@ See detailed architecture and workflows in: - `templates/service-catalog/template.yaml` +## Implemented platform product guardrails + +The CDK app now applies two productized guardrails from the platform review recommendations: + +- **Typed environment configuration** via `lib/platform-config.ts` with explicit `dev|stage|prod` validation and fail-fast errors for missing or invalid values. +- **Mandatory governance tags** standardized at stack level and validated in tests for key resources: `environment`, `project`, `owner`, `cost-center`, and `data-classification`. + +Set the environment explicitly with either CDK context or environment variable: + +```bash +npm run synth # defaults to dev when PLATFORM_ENV is unset +PLATFORM_ENV=stage npm run synth +# or, direct CDK command +npm run cdk -- synth -c platformEnv=prod +``` + ## Code review resolution Review feedback and implemented fixes are tracked in: diff --git a/bin/app.ts b/bin/app.ts index 06b7f4f..f544181 100644 --- a/bin/app.ts +++ b/bin/app.ts @@ -2,15 +2,16 @@ import 'source-map-support/register'; import * as cdk from 'aws-cdk-lib'; import { CdkAppStack } from '../lib/cdk-app-stack'; - -// initialize the CDK app +import { loadPlatformConfig } from '../lib/platform-config'; const app = new cdk.App(); +const platformEnv = app.node.tryGetContext('platformEnv') ?? process.env.PLATFORM_ENV; +const platformConfig = loadPlatformConfig(platformEnv); -// deploy the clouformation stack to the default account and region new CdkAppStack(app, 'CdkAppStack', { - env: { - account: process.env.CDK_DEFAULT_ACCOUNT, - region: process.env.CDK_DEFAULT_REGION + env: { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION, }, + platformConfig, }); diff --git a/lib/cdk-app-stack.ts b/lib/cdk-app-stack.ts index 4bb9580..cda89b0 100644 --- a/lib/cdk-app-stack.ts +++ b/lib/cdk-app-stack.ts @@ -13,9 +13,14 @@ import * as logs from 'aws-cdk-lib/aws-logs'; import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; import * as cwActions from 'aws-cdk-lib/aws-cloudwatch-actions'; import * as sns from 'aws-cdk-lib/aws-sns'; +import { PlatformConfig } from './platform-config'; + +export interface CdkAppStackProps extends cdk.StackProps { + readonly platformConfig: PlatformConfig; +} export class CdkAppStack extends cdk.Stack { - constructor(scope: Construct, id: string, props?: cdk.StackProps) { + constructor(scope: Construct, id: string, props: CdkAppStackProps) { super(scope, id, props); const encryptionKey = new kms.Key(this, 'PlatformDataKey', { @@ -274,7 +279,10 @@ export class CdkAppStack extends cdk.Stack { }); // Optional: Add Tags to Resources - cdk.Tags.of(this).add('Environment', 'Development'); - cdk.Tags.of(this).add('Project', 'DemoAPI'); + cdk.Tags.of(this).add('environment', props.platformConfig.environment); + cdk.Tags.of(this).add('project', props.platformConfig.project); + cdk.Tags.of(this).add('owner', props.platformConfig.owner); + cdk.Tags.of(this).add('cost-center', props.platformConfig.costCenter); + cdk.Tags.of(this).add('data-classification', props.platformConfig.dataClassification); } } diff --git a/lib/platform-config.ts b/lib/platform-config.ts new file mode 100644 index 0000000..5f75282 --- /dev/null +++ b/lib/platform-config.ts @@ -0,0 +1,57 @@ +export type PlatformEnvironment = 'dev' | 'stage' | 'prod'; + +export interface PlatformConfig { + readonly environment: PlatformEnvironment; + readonly owner: string; + readonly costCenter: string; + readonly dataClassification: 'public' | 'internal' | 'confidential' | 'restricted'; + readonly project: string; +} + +const CONFIG_BY_ENV: Record> = { + dev: { + owner: 'platform-engineering', + costCenter: 'ENG-PLATFORM', + dataClassification: 'internal', + project: 'DemoAPI', + }, + stage: { + owner: 'platform-engineering', + costCenter: 'ENG-PLATFORM', + dataClassification: 'confidential', + project: 'DemoAPI', + }, + prod: { + owner: 'platform-engineering', + costCenter: 'ENG-PLATFORM', + dataClassification: 'confidential', + project: 'DemoAPI', + }, +}; + +export const resolvePlatformEnvironment = (value?: string): PlatformEnvironment => { + const normalized = value?.trim().toLowerCase(); + + if (!normalized) { + throw new Error( + 'Platform environment must be explicitly specified via platformEnv context or PLATFORM_ENV. Allowed values: dev, stage, prod.', + ); + } + + if (normalized === 'dev' || normalized === 'stage' || normalized === 'prod') { + return normalized; + } + + throw new Error( + `Invalid platform environment \"${value}\". Allowed values: dev, stage, prod.`, + ); +}; + +export const loadPlatformConfig = (environmentValue?: string): PlatformConfig => { + const environment = resolvePlatformEnvironment(environmentValue); + + return { + environment, + ...CONFIG_BY_ENV[environment], + }; +}; diff --git a/package.json b/package.json index 22be7c2..3d606c7 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "cdk": "cdk", "deploy": "cdk deploy", "destroy": "cdk destroy", - "synth": "cdk synth" + "synth": "bash -lc \"cdk synth -c platformEnv=${PLATFORM_ENV:-dev}\"" }, "devDependencies": { "@types/aws-lambda": "^8.10.122", diff --git a/test/__snapshots__/cdk-app-stack.test.ts.snap b/test/__snapshots__/cdk-app-stack.test.ts.snap index 3ae1e1f..c9b06ba 100644 --- a/test/__snapshots__/cdk-app-stack.test.ts.snap +++ b/test/__snapshots__/cdk-app-stack.test.ts.snap @@ -134,11 +134,23 @@ exports[`CdkAppStack matches synthesized snapshot 1`] = ` "RetentionInDays": 731, "Tags": [ { - "Key": "Environment", - "Value": "Development", + "Key": "cost-center", + "Value": "ENG-PLATFORM", }, { - "Key": "Project", + "Key": "data-classification", + "Value": "internal", + }, + { + "Key": "environment", + "Value": "dev", + }, + { + "Key": "owner", + "Value": "platform-engineering", + }, + { + "Key": "project", "Value": "DemoAPI", }, ], @@ -154,15 +166,27 @@ exports[`CdkAppStack matches synthesized snapshot 1`] = ` "InstanceTenancy": "default", "Tags": [ { - "Key": "Environment", - "Value": "Development", + "Key": "cost-center", + "Value": "ENG-PLATFORM", + }, + { + "Key": "data-classification", + "Value": "internal", + }, + { + "Key": "environment", + "Value": "dev", }, { "Key": "Name", "Value": "SnapshotStack/AppVpc", }, { - "Key": "Project", + "Key": "owner", + "Value": "platform-engineering", + }, + { + "Key": "project", "Value": "DemoAPI", }, ], @@ -173,15 +197,27 @@ exports[`CdkAppStack matches synthesized snapshot 1`] = ` "Properties": { "Tags": [ { - "Key": "Environment", - "Value": "Development", + "Key": "cost-center", + "Value": "ENG-PLATFORM", + }, + { + "Key": "data-classification", + "Value": "internal", + }, + { + "Key": "environment", + "Value": "dev", }, { "Key": "Name", "Value": "SnapshotStack/AppVpc", }, { - "Key": "Project", + "Key": "owner", + "Value": "platform-engineering", + }, + { + "Key": "project", "Value": "DemoAPI", }, ], @@ -215,15 +251,27 @@ exports[`CdkAppStack matches synthesized snapshot 1`] = ` "Properties": { "Tags": [ { - "Key": "Environment", - "Value": "Development", + "Key": "cost-center", + "Value": "ENG-PLATFORM", + }, + { + "Key": "data-classification", + "Value": "internal", + }, + { + "Key": "environment", + "Value": "dev", }, { "Key": "Name", "Value": "SnapshotStack/AppVpc/privateSubnet1", }, { - "Key": "Project", + "Key": "owner", + "Value": "platform-engineering", + }, + { + "Key": "project", "Value": "DemoAPI", }, ], @@ -259,15 +307,27 @@ exports[`CdkAppStack matches synthesized snapshot 1`] = ` "Value": "Private", }, { - "Key": "Environment", - "Value": "Development", + "Key": "cost-center", + "Value": "ENG-PLATFORM", + }, + { + "Key": "data-classification", + "Value": "internal", + }, + { + "Key": "environment", + "Value": "dev", }, { "Key": "Name", "Value": "SnapshotStack/AppVpc/privateSubnet1", }, { - "Key": "Project", + "Key": "owner", + "Value": "platform-engineering", + }, + { + "Key": "project", "Value": "DemoAPI", }, ], @@ -304,15 +364,27 @@ exports[`CdkAppStack matches synthesized snapshot 1`] = ` "Properties": { "Tags": [ { - "Key": "Environment", - "Value": "Development", + "Key": "cost-center", + "Value": "ENG-PLATFORM", + }, + { + "Key": "data-classification", + "Value": "internal", + }, + { + "Key": "environment", + "Value": "dev", }, { "Key": "Name", "Value": "SnapshotStack/AppVpc/privateSubnet2", }, { - "Key": "Project", + "Key": "owner", + "Value": "platform-engineering", + }, + { + "Key": "project", "Value": "DemoAPI", }, ], @@ -337,15 +409,27 @@ exports[`CdkAppStack matches synthesized snapshot 1`] = ` "Value": "Private", }, { - "Key": "Environment", - "Value": "Development", + "Key": "cost-center", + "Value": "ENG-PLATFORM", + }, + { + "Key": "data-classification", + "Value": "internal", + }, + { + "Key": "environment", + "Value": "dev", }, { "Key": "Name", "Value": "SnapshotStack/AppVpc/privateSubnet2", }, { - "Key": "Project", + "Key": "owner", + "Value": "platform-engineering", + }, + { + "Key": "project", "Value": "DemoAPI", }, ], @@ -375,15 +459,27 @@ exports[`CdkAppStack matches synthesized snapshot 1`] = ` "Domain": "vpc", "Tags": [ { - "Key": "Environment", - "Value": "Development", + "Key": "cost-center", + "Value": "ENG-PLATFORM", + }, + { + "Key": "data-classification", + "Value": "internal", + }, + { + "Key": "environment", + "Value": "dev", }, { "Key": "Name", "Value": "SnapshotStack/AppVpc/publicSubnet1", }, { - "Key": "Project", + "Key": "owner", + "Value": "platform-engineering", + }, + { + "Key": "project", "Value": "DemoAPI", }, ], @@ -407,15 +503,27 @@ exports[`CdkAppStack matches synthesized snapshot 1`] = ` }, "Tags": [ { - "Key": "Environment", - "Value": "Development", + "Key": "cost-center", + "Value": "ENG-PLATFORM", + }, + { + "Key": "data-classification", + "Value": "internal", + }, + { + "Key": "environment", + "Value": "dev", }, { "Key": "Name", "Value": "SnapshotStack/AppVpc/publicSubnet1", }, { - "Key": "Project", + "Key": "owner", + "Value": "platform-engineering", + }, + { + "Key": "project", "Value": "DemoAPI", }, ], @@ -437,15 +545,27 @@ exports[`CdkAppStack matches synthesized snapshot 1`] = ` "Properties": { "Tags": [ { - "Key": "Environment", - "Value": "Development", + "Key": "cost-center", + "Value": "ENG-PLATFORM", + }, + { + "Key": "data-classification", + "Value": "internal", + }, + { + "Key": "environment", + "Value": "dev", }, { "Key": "Name", "Value": "SnapshotStack/AppVpc/publicSubnet1", }, { - "Key": "Project", + "Key": "owner", + "Value": "platform-engineering", + }, + { + "Key": "project", "Value": "DemoAPI", }, ], @@ -470,15 +590,27 @@ exports[`CdkAppStack matches synthesized snapshot 1`] = ` "Value": "Public", }, { - "Key": "Environment", - "Value": "Development", + "Key": "cost-center", + "Value": "ENG-PLATFORM", + }, + { + "Key": "data-classification", + "Value": "internal", + }, + { + "Key": "environment", + "Value": "dev", }, { "Key": "Name", "Value": "SnapshotStack/AppVpc/publicSubnet1", }, { - "Key": "Project", + "Key": "owner", + "Value": "platform-engineering", + }, + { + "Key": "project", "Value": "DemoAPI", }, ], @@ -507,15 +639,27 @@ exports[`CdkAppStack matches synthesized snapshot 1`] = ` "Properties": { "Tags": [ { - "Key": "Environment", - "Value": "Development", + "Key": "cost-center", + "Value": "ENG-PLATFORM", + }, + { + "Key": "data-classification", + "Value": "internal", + }, + { + "Key": "environment", + "Value": "dev", }, { "Key": "Name", "Value": "SnapshotStack/AppVpc/publicSubnet2", }, { - "Key": "Project", + "Key": "owner", + "Value": "platform-engineering", + }, + { + "Key": "project", "Value": "DemoAPI", }, ], @@ -551,15 +695,27 @@ exports[`CdkAppStack matches synthesized snapshot 1`] = ` "Value": "Public", }, { - "Key": "Environment", - "Value": "Development", + "Key": "cost-center", + "Value": "ENG-PLATFORM", + }, + { + "Key": "data-classification", + "Value": "internal", + }, + { + "Key": "environment", + "Value": "dev", }, { "Key": "Name", "Value": "SnapshotStack/AppVpc/publicSubnet2", }, { - "Key": "Project", + "Key": "owner", + "Value": "platform-engineering", + }, + { + "Key": "project", "Value": "DemoAPI", }, ], @@ -592,11 +748,23 @@ exports[`CdkAppStack matches synthesized snapshot 1`] = ` "RetentionInDays": 30, "Tags": [ { - "Key": "Environment", - "Value": "Development", + "Key": "cost-center", + "Value": "ENG-PLATFORM", + }, + { + "Key": "data-classification", + "Value": "internal", + }, + { + "Key": "environment", + "Value": "dev", + }, + { + "Key": "owner", + "Value": "platform-engineering", }, { - "Key": "Project", + "Key": "project", "Value": "DemoAPI", }, ], @@ -616,11 +784,23 @@ exports[`CdkAppStack matches synthesized snapshot 1`] = ` "MessageRetentionPeriod": 1209600, "Tags": [ { - "Key": "Environment", - "Value": "Development", + "Key": "cost-center", + "Value": "ENG-PLATFORM", + }, + { + "Key": "data-classification", + "Value": "internal", + }, + { + "Key": "environment", + "Value": "dev", + }, + { + "Key": "owner", + "Value": "platform-engineering", }, { - "Key": "Project", + "Key": "project", "Value": "DemoAPI", }, ], @@ -695,11 +875,23 @@ exports[`CdkAppStack matches synthesized snapshot 1`] = ` }, "Tags": [ { - "Key": "Environment", - "Value": "Development", + "Key": "cost-center", + "Value": "ENG-PLATFORM", }, { - "Key": "Project", + "Key": "data-classification", + "Value": "internal", + }, + { + "Key": "environment", + "Value": "dev", + }, + { + "Key": "owner", + "Value": "platform-engineering", + }, + { + "Key": "project", "Value": "DemoAPI", }, ], @@ -737,11 +929,23 @@ exports[`CdkAppStack matches synthesized snapshot 1`] = ` }, "Tags": [ { - "Key": "Environment", - "Value": "Development", + "Key": "cost-center", + "Value": "ENG-PLATFORM", + }, + { + "Key": "data-classification", + "Value": "internal", + }, + { + "Key": "environment", + "Value": "dev", + }, + { + "Key": "owner", + "Value": "platform-engineering", }, { - "Key": "Project", + "Key": "project", "Value": "DemoAPI", }, ], @@ -801,11 +1005,23 @@ exports[`CdkAppStack matches synthesized snapshot 1`] = ` "Name": "Demo API", "Tags": [ { - "Key": "Environment", - "Value": "Development", + "Key": "cost-center", + "Value": "ENG-PLATFORM", + }, + { + "Key": "data-classification", + "Value": "internal", + }, + { + "Key": "environment", + "Value": "dev", + }, + { + "Key": "owner", + "Value": "platform-engineering", }, { - "Key": "Project", + "Key": "project", "Value": "DemoAPI", }, ], @@ -859,11 +1075,23 @@ exports[`CdkAppStack matches synthesized snapshot 1`] = ` ], "Tags": [ { - "Key": "Environment", - "Value": "Development", + "Key": "cost-center", + "Value": "ENG-PLATFORM", }, { - "Key": "Project", + "Key": "data-classification", + "Value": "internal", + }, + { + "Key": "environment", + "Value": "dev", + }, + { + "Key": "owner", + "Value": "platform-engineering", + }, + { + "Key": "project", "Value": "DemoAPI", }, ], @@ -871,7 +1099,7 @@ exports[`CdkAppStack matches synthesized snapshot 1`] = ` "Type": "AWS::IAM::Role", "UpdateReplacePolicy": "Retain", }, - "RestAPIDeploymentD35A5380dc5fde4fc47fdd1e8c0d8c911c4c286a": { + "RestAPIDeploymentD35A538022916227e2890c9cef2e63fbe89caa4d": { "DependsOn": [ "RestAPIGET232BCD01", "RestAPIitemsGET9E4F800F", @@ -906,7 +1134,7 @@ exports[`CdkAppStack matches synthesized snapshot 1`] = ` "CacheClusterEnabled": true, "CacheClusterSize": "0.5", "DeploymentId": { - "Ref": "RestAPIDeploymentD35A5380dc5fde4fc47fdd1e8c0d8c911c4c286a", + "Ref": "RestAPIDeploymentD35A538022916227e2890c9cef2e63fbe89caa4d", }, "MethodSettings": [ { @@ -923,11 +1151,23 @@ exports[`CdkAppStack matches synthesized snapshot 1`] = ` "StageName": "prod", "Tags": [ { - "Key": "Environment", - "Value": "Development", + "Key": "cost-center", + "Value": "ENG-PLATFORM", + }, + { + "Key": "data-classification", + "Value": "internal", + }, + { + "Key": "environment", + "Value": "dev", + }, + { + "Key": "owner", + "Value": "platform-engineering", }, { - "Key": "Project", + "Key": "project", "Value": "DemoAPI", }, ], @@ -1467,11 +1707,23 @@ exports[`CdkAppStack matches synthesized snapshot 1`] = ` "TableName": "DemoTable", "Tags": [ { - "Key": "Environment", - "Value": "Development", + "Key": "cost-center", + "Value": "ENG-PLATFORM", + }, + { + "Key": "data-classification", + "Value": "internal", + }, + { + "Key": "environment", + "Value": "dev", + }, + { + "Key": "owner", + "Value": "platform-engineering", }, { - "Key": "Project", + "Key": "project", "Value": "DemoAPI", }, ], @@ -1530,11 +1782,23 @@ exports[`CdkAppStack matches synthesized snapshot 1`] = ` "Runtime": "nodejs18.x", "Tags": [ { - "Key": "Environment", - "Value": "Development", + "Key": "cost-center", + "Value": "ENG-PLATFORM", }, { - "Key": "Project", + "Key": "data-classification", + "Value": "internal", + }, + { + "Key": "environment", + "Value": "dev", + }, + { + "Key": "owner", + "Value": "platform-engineering", + }, + { + "Key": "project", "Value": "DemoAPI", }, ], @@ -1581,11 +1845,23 @@ exports[`CdkAppStack matches synthesized snapshot 1`] = ` ], "Tags": [ { - "Key": "Environment", - "Value": "Development", + "Key": "cost-center", + "Value": "ENG-PLATFORM", + }, + { + "Key": "data-classification", + "Value": "internal", + }, + { + "Key": "environment", + "Value": "dev", + }, + { + "Key": "owner", + "Value": "platform-engineering", }, { - "Key": "Project", + "Key": "project", "Value": "DemoAPI", }, ], @@ -1727,11 +2003,23 @@ exports[`CdkAppStack matches synthesized snapshot 1`] = ` ], "Tags": [ { - "Key": "Environment", - "Value": "Development", + "Key": "cost-center", + "Value": "ENG-PLATFORM", + }, + { + "Key": "data-classification", + "Value": "internal", + }, + { + "Key": "environment", + "Value": "dev", + }, + { + "Key": "owner", + "Value": "platform-engineering", }, { - "Key": "Project", + "Key": "project", "Value": "DemoAPI", }, ], diff --git a/test/cdk-app-stack.test.ts b/test/cdk-app-stack.test.ts index 2d28aec..b452054 100644 --- a/test/cdk-app-stack.test.ts +++ b/test/cdk-app-stack.test.ts @@ -1,17 +1,26 @@ import * as cdk from 'aws-cdk-lib'; import { Template } from 'aws-cdk-lib/assertions'; import { CdkAppStack } from '../lib/cdk-app-stack'; +import { loadPlatformConfig } from '../lib/platform-config'; const buildTemplate = (stackName: string): Template => { const app = new cdk.App(); const stack = new CdkAppStack(app, stackName, { env: { account: '111111111111', region: 'us-east-1' }, + platformConfig: loadPlatformConfig('dev'), }); return Template.fromStack(stack); }; describe('CdkAppStack', () => { + + test('fails fast when environment is not specified', () => { + expect(() => loadPlatformConfig()).toThrow( + 'Platform environment must be explicitly specified via platformEnv context or PLATFORM_ENV. Allowed values: dev, stage, prod.', + ); + }); + test('creates core resources', () => { const template = buildTemplate('TestStack'); @@ -21,6 +30,21 @@ describe('CdkAppStack', () => { template.resourceCountIs('AWS::KMS::Key', 1); }); + + test('applies required governance tags', () => { + const template = buildTemplate('TaggedStack'); + + template.hasResourceProperties('AWS::DynamoDB::Table', { + Tags: [ + { Key: 'cost-center', Value: 'ENG-PLATFORM' }, + { Key: 'data-classification', Value: 'internal' }, + { Key: 'environment', Value: 'dev' }, + { Key: 'owner', Value: 'platform-engineering' }, + { Key: 'project', Value: 'DemoAPI' }, + ], + }); + }); + test('matches synthesized snapshot', () => { const template = buildTemplate('SnapshotStack'); const templateJson = template.toJSON() as {