Skip to content

feat(core): ship default CloudFormation validation rules in Rego - #38448

Open
iankhou wants to merge 7 commits into
mainfrom
iankhou/gamelift-rego-default-rules
Open

feat(core): ship default CloudFormation validation rules in Rego#38448
iankhou wants to merge 7 commits into
mainfrom
iankhou/gamelift-rego-default-rules

Conversation

@iankhou

@iankhou iankhou commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Closes #38456

Description

CloudFormationValidatePlugin accepts custom Rego rules via its regoRules prop, but the auto-registered default instance doesn't pass any rules. This change loads CDK-authored .rego rules from core/lib/validation/rules/ into the default plugin instance so they run on each synth.

We use ruleset gamelift-fleet.rego for alpha module aws-gamelift-alpha to proof this change. The ruleset validates BuildFleet class L2 validations (name/description length, location and ingress-rule counts, non-negative location capacity) to template-level checks on AWS::GameLift::Fleet.

In the ruleset at packages/aws-cdk-lib/core/lib/validation/rules/gamelift-fleet.rego, rules 001-003 validate to ensure against configurations that will fail deployment. Rules 004-005 validate against configurations that are not effective on the service and would deploy correctly, but have no-op behavior. Rules 006-007 are cross-resource rules: they join a fleet to the AWS::GameLift::Build it references through the template's Ref graph and check that server-process launch paths match the build's operating system — an invariant no single construct can validate in isolation (see below).

Placement policy: why do these rules live in the CDK and not in cloudformation-validate?

All of these rules are plain Rego over the engine's template model, so any of them could eventually be contributed upstream to the engine's built-in ruleset. CDK-authored default rules are the fast-iteration tier: they ship and fix on every aws-cdk-lib release (the engine is pinned and versioned independently — cf. the existing IGNORE_RULES block, which exists because upstream rules can only be worked around on CDK's schedule), and they sit next to the L2 constructs whose validation behavior they mirror. Rules that prove stable and generically useful are candidates for upstreaming to cloudformation-validate, at which point the CDK drops its copy.

How did we come up with these rules?

We probed the GameLift API directly with various misconfigurations, and deployed a CDK app containing misconfigurations.

Direct calls

CDK-GameLift-001
$ aws gamelift create-fleet \
    --name probe-inverted-ports \
    --build-id build-8aedb7d8-3e3c-49bb-be0a-ee0d111d4477 \
    --ec2-instance-type c5.large \
    --ec2-inbound-permissions 'FromPort=9000,ToPort=80,IpRange=10.0.0.0/24,Protocol=TCP' \
    --runtime-configuration 'ServerProcesses=[{LaunchPath=/local/game/server,ConcurrentExecutions=1}]'

An error occurred (InvalidRequestException) when calling the CreateFleet operation:
Invalid port range (9000-80).  FromPort should be less than or equal to ToPort.
CDK-GameLift-002
$ aws gamelift update-fleet-capacity \
    --fleet-id fleet-84f422a2-414e-4bc7-b76f-0d0201ded654 \
    --min-size 5 --max-size 2 --desired-instances 3 --location us-east-1

An error occurred (InvalidRequestException) when calling the UpdateFleetCapacity operation:
Desired instances (3) must not be less than minimum instances (5).
CDK-GameLift-003
$ aws gamelift update-fleet-capacity \
    --fleet-id fleet-84f422a2-414e-4bc7-b76f-0d0201ded654 \
    --min-size 0 --max-size 1 --desired-instances 5 --location us-east-1

An error occurred (InvalidRequestException) when calling the UpdateFleetCapacity operation:
Maximum instances (1) must not be less than desired instances (5).
CDK-GameLift-004

For this case, we set Message, even though we configure SIMPLE routing. According to the docs, only TERMINAL routing uses Message.

Note that there are no API failures for this.

Pay particular attention to Message=goodbye

$ aws gamelift create-alias \
    --name probe-simple-with-message \
    --routing-strategy 'Type=SIMPLE,FleetId=fleet-1111
1111-2222-3333-4444-555555555555,Message=goodbye'
{
    "Alias": {
        "AliasId":
"alias-b5c853b2-fcbc-4f9e-adab-64acd0a87cf8",
        "Name": "probe-simple-with-message",
        "AliasArn": "arn:aws:gamelift:us-east-1::alias
/alias-b5c853b2-fcbc-4f9e-adab-64acd0a87cf8",
        "RoutingStrategy": {
            "Type": "SIMPLE",
            "FleetId":
"fleet-11111111-2222-3333-4444-555555555555"
        },
        "CreationTime":
"2026-07-30T16:31:21.308000-04:00",
        "LastUpdatedTime":
"2026-07-30T16:31:21.308000-04:00"
    }
}
# get the properties of the alias
$ aws gamelift describe-alias --alias-id
alias-b5c853b2-fcbc-4f9e-adab-64acd0a87cf8
{
    "Alias": {
        "AliasId":
"alias-b5c853b2-fcbc-4f9e-adab-64acd0a87cf8",
        "Name": "probe-simple-with-message",
        "AliasArn": "arn:aws:gamelift:us-east-1::alias
/alias-b5c853b2-fcbc-4f9e-adab-64acd0a87cf8",
        "RoutingStrategy": {
            "Type": "SIMPLE",
            "FleetId":
"fleet-11111111-2222-3333-4444-555555555555"
        },
        "CreationTime":
"2026-07-30T16:31:21.308000-04:00",
        "LastUpdatedTime":
"2026-07-30T16:31:21.308000-04:00"
    }
}

Message is gone, since the API just dropped it (instead of erroring on it), but there was no failure.

# get the Fleet ID from the alias


$ aws gamelift resolve-alias --alias-id
alias-b5c853b2-fcbc-4f9e-adab-64acd0a87cf8
{
    "FleetId":
"fleet-11111111-2222-3333-4444-555555555555",
    "FleetArn": "arn:aws:gamelift:us-east-1:3696024654
11:fleet/fleet-11111111-2222-3333-4444-555555555555"
}

The Message is absent from both the creation response
and the stored state as it was silently discarded by the API. The alias routes normally; the terminal message the author configured can never be shown.

CDK-GameLift-005

In this example, we provide a FleetID with routing strategy TERMINAL. However, only SIMPLE routing uses the FleetID. What happens if we pass it?

$ aws gamelift create-alias \
      --name probe-terminal-with-fleetid \
      --routing-strategy 'Type=TERMINAL,FleetId=fleet-11
  111111-2222-3333-4444-555555555555,Message=goodbye'

{
    "Alias": {
        "AliasId":
"alias-53c7844a-412c-4410-8ab6-b21570bd0dad",
        "Name": "probe-terminal-with-fleetid",
        "AliasArn": "arn:aws:gamelift:us-east-1::alias
/alias-53c7844a-412c-4410-8ab6-b21570bd0dad",
        "RoutingStrategy": {
            "Type": "TERMINAL",
            "Message": "goodbye"
        },
        "CreationTime":
"2026-07-30T16:32:00.099000-04:00",
        "LastUpdatedTime":
"2026-07-30T16:32:00.099000-04:00"
    }
}

The API doesn't throw. But it ignores FleetID and doesn't include it in the response.

# get the properties of the alias
# there will be no FleetID returned, since it's not used by the TERMINAL routing strategy

$ aws gamelift describe-alias --alias-id
alias-53c7844a-412c-4410-8ab6-b21570bd0dad
{
    "Alias": {
        "AliasId":
"alias-53c7844a-412c-4410-8ab6-b21570bd0dad",
        "Name": "probe-terminal-with-fleetid",
        "AliasArn": "arn:aws:gamelift:us-east-1::alias
/alias-53c7844a-412c-4410-8ab6-b21570bd0dad",
        "RoutingStrategy": {
            "Type": "TERMINAL",
            "Message": "goodbye"
        },
        "CreationTime":
"2026-07-30T16:32:00.099000-04:00",
        "LastUpdatedTime":
"2026-07-30T16:32:00.099000-04:00"
    }
}

Since the operator configured the alias with a FleetId, they might expect to be able to resolve the alias and see it. But that will result in an error.

$ aws gamelift resolve-alias --alias-id alias-53c7844a-412c-4410-8ab6-b21570bd0dad

  An error occurred (TerminalRoutingStrategyException) when calling the ResolveAlias operation: goodbye

The FleetId is absent from both the creation response and the stored state, as it is silently discarded by the API. Clients resolving the alias get the terminal exception instead of a game server, with no indication a FleetId was ever supplied.

GameLift CDK app deployment

We deployed an app with the following misconfiguration:

    const fleet = new gamelift.CfnFleet(this, 'GameFleet', {
      ...
      locations: [
        {
          location: 'us-east-1',
          // MISCONFIGURATION: desired capacity outside [minSize, maxSize]
          locationCapacity: { desiredEc2Instances: 10, minSize: 1, maxSize: 4 },
        },
      ],
      runtimeConfiguration: {
        serverProcesses: [
          { launchPath: '/local/game/server', concurrentExecutions: 1 },
        ],
      },
    });

The misconfigured fields are schema-valid, so template validation passes and the deploy proceeds. GameLift builds the fleet for ~5 minutes, then fails to apply the capacity, and CloudFormation rolls the stack back.

From aws describe-stack-events post-deployment:

2026-07-30T18:26:50.209      probe-capacity-minmax   CREATE_IN_PROGRESS   User Initiated
2026-07-30T18:26:53.131      ProbeFleet              CREATE_IN_PROGRESS   -
2026-07-30T18:26:54.456      ProbeFleet              CREATE_IN_PROGRESS   Resource creation Initiated
                             ... ~5 minutes of fleet provisioning ...
2026-07-30T18:32:03.683      ProbeFleet              CREATE_FAILED        Resource handler returned message: "Resource of type
                                                                          'AWS::GameLift::Fleet' with identifier
                                                                          'fleet-2af5407b-08be-4610-a979-8ca473cbffd7' did not
                                                                          stabilize." (HandlerErrorCode: NotStabilized)
2026-07-30T18:32:04.037      probe-capacity-minmax   ROLLBACK_IN_PROGRESS The following resource(s) failed to create: [ProbeFleet].
                                                                          Rollback requested by user.
2026-07-30T18:32:05.424      ProbeFleet              DELETE_IN_PROGRESS   -
2026-07-30T18:33:35.772      ProbeFleet              DELETE_COMPLETE      -
2026-07-30T18:33:36.240      probe-capacity-minmax   ROLLBACK_COMPLETE    -

This whole deploy-time failure took about 7 minutes. With rule 002, we detect these misconfigurations at synth time.

Findings surface as warnings and do not fail synth unless the app opts in using flag @aws-cdk/core:validateAgainstDefaultRules.

Full example app code:

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

    const buildBucket = new s3.Bucket(this, 'BuildArtifacts', {
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      encryption: s3.BucketEncryption.S3_MANAGED,
      enforceSSL: true,
    });

    const buildRole = new iam.Role(this, 'BuildAccessRole', {
      assumedBy: new iam.ServicePrincipal('gamelift.amazonaws.com'),
    });
    buildBucket.grantRead(buildRole);

    const build = new gamelift.CfnBuild(this, 'ServerBuild', {
      name: 'game-server-build',
      operatingSystem: 'AMAZON_LINUX_2023',
      serverSdkVersion: '5.1.0',
      storageLocation: {
        bucket: buildBucket.bucketName,
        key: 'builds/server-1.0.0.zip',
        roleArn: buildRole.roleArn,
      },
    });

    const fleet = new gamelift.CfnFleet(this, 'GameFleet', {
      name: 'game-fleet',
      buildId: build.ref,
      ec2InstanceType: 'c5.large',
      fleetType: 'ON_DEMAND',
      ec2InboundPermissions: [
        // Mistake 1: inverted port range — FromPort > ToPort
        { fromPort: 7877, toPort: 7777, ipRange: '0.0.0.0/0', protocol: 'UDP' },
        { fromPort: 443, toPort: 443, ipRange: '0.0.0.0/0', protocol: 'TCP' },
      ],
      locations: [
        {
          location: 'us-east-1',
          // Mistake 2: desired capacity outside [minSize, maxSize]
          locationCapacity: { desiredEc2Instances: 10, minSize: 1, maxSize: 4 },
        },
      ],
      runtimeConfiguration: {
        serverProcesses: [
          { launchPath: '/local/game/server', concurrentExecutions: 1 },
        ],
      },
    });

    new gamelift.CfnAlias(this, 'LiveAlias', {
      name: 'live',
      // Mistake 3: SIMPLE routing to a fleet, but also a terminal message
      routingStrategy: { type: 'SIMPLE', fleetId: fleet.ref, message: 'server offline' },
    });
  }
}

cdk synth output for this code:

lib/game-backend-stack.ts:43:19
ERROR EC2InboundPermissions.0.FromPort: Ingress rule port range is inverted: FromPort 7877 is greater than ToPort 7777 (CloudFormation Validate)
   BadGameBackendStack/GameFleet (GameFleet) aws-cdk-lib.aws_gamelift.CfnFleet
   Acknowledge with 'CloudFormation-Validate::CDK-GameLift-001'

lib/game-backend-stack.ts:43:19
ERROR Locations.0.LocationCapacity.DesiredEC2Instances: Location capacity DesiredEC2Instances 10 is outside the range [MinSize 1, MaxSize 4] (CloudFormation Validate)
   BadGameBackendStack/GameFleet (GameFleet) aws-cdk-lib.aws_gamelift.CfnFleet
   Acknowledge with 'CloudFormation-Validate::CDK-GameLift-003'

lib/game-backend-stack.ts:67:5
ERROR RoutingStrategy.Message: Alias with SIMPLE routing must not set a terminal Message; either route to a fleet or set a terminal message, not both (CloudFormation Validate)
   BadGameBackendStack/LiveAlias (LiveAlias) aws-cdk-lib.aws_gamelift.CfnAlias
   Acknowledge with 'CloudFormation-Validate::CDK-GameLift-004'
Synthesis finished with errors

Prior to this change, there would have been no errors.

Cross-resource rules (CDK-GameLift-006/007)

Game builds are installed on fleet instances at an OS-specific root — C:\game on Windows, /local/game on Linux (docs) — so every server-process LaunchPath must live under the root dictated by the referenced build's OperatingSystem. A mismatch is schema-valid and deploys, but the fleet then activates into ERROR state because no server process can start.

Describe any new or updated permissions being added

None.

Description of how you validated changes

Unit and integration tests. Manual testing as detailed above.

Integration test validates that the default shipped rules validate the code and report correctly.

Checklist


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

The CloudFormationValidatePlugin already accepts custom Rego rules via its
`regoRules` prop, but the auto-registered default instance passed none. This
loads CDK-authored `.rego` rules from `core/lib/validation/rules/` into the
default plugin instance so they run on every synth.

The first rule set (`gamelift-fleet.rego`) ports the aws-gamelift-alpha
BuildFleet L2 validations (name/description length, location and ingress-rule
counts, non-negative location capacity) to template-level checks on
`AWS::GameLift::Fleet`. Unlike the L2 constructor checks, these also cover
templates produced via L1 constructs, escape hatches, and CfnInclude, and they
evaluate post-synth so token-valued properties are already resolved.

Findings surface as warnings and do not fail synth unless the app opts into
`@aws-cdk/core:validateAgainstDefaultRules`, matching the existing plugin
rollout posture.
@github-actions github-actions Bot added the p2 label Jul 29, 2026
@aws-cdk-automation
aws-cdk-automation requested a review from a team July 29, 2026 20:56
@mergify mergify Bot added the contribution/core This is a PR that came from AWS. label Jul 29, 2026
@mergify
mergify Bot temporarily deployed to automation July 29, 2026 20:56 Inactive
@mergify
mergify Bot temporarily deployed to automation July 29, 2026 20:56 Inactive

@aws-cdk-automation aws-cdk-automation left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(This review is outdated)

…ants

The first version of the GameLift rules duplicated checks the engine's
built-in schema rules already report (F3031-F3034 cover string lengths,
item counts, per-field minimums and patterns). Replace them with five
cross-field invariants the resource schema cannot express:

- CDK-GameLift-001: ingress rule FromPort must not exceed ToPort
- CDK-GameLift-002: location capacity MinSize must not exceed MaxSize
- CDK-GameLift-003: DesiredEC2Instances must lie within [MinSize, MaxSize]
- CDK-GameLift-004: SIMPLE-routing alias must not set a terminal Message
- CDK-GameLift-005: TERMINAL-routing alias must not reference a fleet

The alias rules port the "terminal message or fleet, not both" check from
aws-gamelift-alpha's Alias construct; the fleet rules add API-enforced
invariants that today only fail at deploy time.
The stack deploys a compliant TERMINAL-routing GameLift alias and carries a
violating alias (TERMINAL routing plus FleetId, CDK-GameLift-005) behind a
never-true condition: the default Rego rules evaluate the full template
regardless of conditions, so the finding is captured in the snapshot's
validation-report.json, while CloudFormation never creates the invalid
resource the GameLift API would reject. Deploying proves the default warning
posture does not block deployment.
An explicitly registered CloudFormationValidatePlugin replaces the
auto-registered default instance, so passing custom rules would silently
drop the CDK default rules. The constructor now merges the default rules
with any user-supplied regoRules; a new includeDefaultRules prop
(default true) opts out entirely.

Also documents the default rules in the README (rule table, severity
posture, suppression by ID) and adds tests covering the merge, the
opt-out, and suppression of a default rule finding via
Validations.of(scope).acknowledge().
@aws-cdk-automation
aws-cdk-automation dismissed their stale review July 30, 2026 17:13

✅ Updated pull request passes all PRLinter validations. Dismissing previous PRLinter review.

…avior

Empirically probing the GameLift API showed the two failure modes the
default rules cover are different: the fleet rules (001-003) catch real
deploy-time rejections (CreateFleet refuses inverted port ranges;
UpdateFleetCapacity refuses out-of-range capacity values, surfacing as a
CloudFormation NotStabilized rollback), while the alias routing rules
(004-005) catch contradictory configuration that GameLift accepts and
partially ignores - the deployment succeeds with one field silently
unused. Reword the rule file, README, and integ test comment accordingly;
no behavioral changes.
@iankhou
iankhou marked this pull request as ready for review July 30, 2026 20:53
@iankhou
iankhou requested a review from a team as a code owner July 30, 2026 20:53
…ft-006/007)

Join a fleet to the AWS::GameLift::Build it references through the
template's Ref graph and check that every server-process launch path
lives under the install root dictated by the build's operating system
(C:\game for Windows, /local/game for Linux). A mismatch is
schema-valid and deploys, but the fleet then activates into ERROR state
because no server process can start.

This is the first cross-resource default rule: the invariant spans two
resources, so no single construct can validate it — BuildFleet only
sees an IBuild interface, which erases the build's operating system.
The rules stay silent for imported builds (literal BuildId) and builds
that omit OperatingSystem, since the OS is unknowable in those cases.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contribution/core This is a PR that came from AWS. p2 pr/needs-maintainer-review This PR needs a review from a Core Team Member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add at least 1 CDK-added "default" validation rule (info inside)

2 participants