Every landing zone template is checked before it reaches CloudFormation. A schema mistake, a deprecated property, or an insecure default fails the command you just ran instead of surfacing halfway through a rollback, which matters most on a StackSet that is already rolling out to every account in your organization.
Two layers do the work:
- The offline rule set. CDK evaluates its bundled CloudFormation rules against every synthesized template. This runs locally and needs no AWS credentials.
- The CloudFormation pre-deployment checks. CDK sends each template to CloudFormation for the six pre-deployment checks: three errors (resource property syntax, resource name conflicts, S3 bucket emptiness on delete) and three warnings (service quota limits, AWS Config recorder conflicts, ECR repositories that still hold images). These run against a real account, so they need credentials for the phase's target account.
Commands
| Command | Account needed | What it runs |
|---|---|---|
pnpm run validate | None | The offline rule set across both phases in one pass |
pnpm run organization:validate | Management account | Offline rules plus the pre-deployment checks for that phase |
pnpm run landingzone:validate | Landing zone account | Offline rules plus the pre-deployment checks for that phase |
After setting landingZoneAccountId, run the offline check on your own machine or in a pre-commit hook:
pnpm run validate
Before the first organization deployment, when landingZoneAccountId is still unset, run pnpm run organization:validate --no-online to check that phase only. Local synthesis still needs an authenticated GitHub CLI, and installing the private packages requires CodeArtifact access.
To include the CloudFormation checks, assume a role in the target account first and run that phase's task:
pnpm run organization:validate # management account
pnpm run landingzone:validate # landing zone account
cdk validate is still behind the CDK CLI's --unstable flag. The generated tasks pass it for you, so the commands above are stable to script against even while the underlying command is not.
The feature flags
Two context flags in cdk.json turn validation on. Both are set by .projenrc.ts, so change them there and re-run pnpm exec projen rather than editing cdk.json:
{
"context": {
"@aws-cdk/core:validateAgainstDefaultRules": true,
"@aws-cdk/core:annotationsInValidationReport": true
}
}
validateAgainstDefaultRules promotes rule findings to errors, so an ERROR-level finding fails synthesis. annotationsInValidationReport folds construct-level warnings into the same report, so you read one list instead of two.
Acknowledging a finding
When a rule fires on something you deploy deliberately, acknowledge that specific finding in your CDK code rather than turning the rule set off. Acknowledgements are scoped to a construct and carry a reason, so the next person to read the code knows why the exception exists:
Validations.of(myConstruct).acknowledge({
id: 'CloudFormation-Validate::W2531',
reason: 'Runtime upgrade tracked in JIRA-1234',
});
The id is the finding ID from the validation report. Acknowledge the narrowest scope that covers it: a single construct rather than the whole stack, so an unrelated future resource does not inherit the exception.
Where validation runs in CI
The starter generates three CDK workflows:
- The offline validation workflow (
cdk-validate.yml) checks both phases withpnpm run validateon pull requests and manual dispatch. Its validation is offline; the job uses management-account credentials to install the private packages first. - The deployment workflow validates each phase immediately before deploying it. A validation failure fails the job, so an invalid template never reaches CloudFormation.
- The pull request workflow validates each phase before diffing it, so a broken template shows up as a failed check rather than a confusing diff.
See GitHub Actions Deployment for the workflow setup and triggers.
Validation is not a diff
Validation tells you whether a template is valid; it does not tell you what will change. Keep running pnpm run organization:diff and pnpm run landingzone:diff:all before a deploy that touches organization structure, SCPs, or StackSet targeting. The two answer different questions, and the pull request workflow runs both for exactly that reason.