---
cuppy_hub: aws
cuppy_path: patterns/github-actions-to-cdk-deploy.md
cuppy_section: docs
cuppy_doc: D5D8-JB1T
cuppy_doc_uuid: 4c6510e4-be06-4a59-a17e-b75b7d6b0a6b
cuppy_permalink: https://cuppy.io/aws/d/D5D8-JB1T
cuppy_version: 4
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/aws/docs/patterns/github-actions-to-cdk-deploy
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: ce6e346e0f79474cb17cf6fe30c05fbb245fad4ffd68ac15ba3341b5bcb4d727
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


# GitHub Actions to `cdk deploy`

**The GitHub OIDC role should hold almost no permissions of its own: it only needs to assume the CDK bootstrap roles.** The bootstrap roles do the publishing and deploying, and CloudFormation's execution role creates the resources. Set up the OIDC provider and trust policy first (`guides/iam/ci-oidc-github.md`).

## The chain

```
GitHub job ──AssumeRoleWithWebIdentity──▶ gha-deploy (your OIDC role)
gha-deploy ──AssumeRole──▶ cdk-<q>-file-publishing-role / image-publishing-role  (upload assets)
gha-deploy ──AssumeRole──▶ cdk-<q>-lookup-role                                   (context lookups, if any)
gha-deploy ──AssumeRole──▶ cdk-<q>-deploy-role ──PassRole──▶ cdk-<q>-cfn-exec-role ◀── CloudFormation
```

## Same account

The bootstrap roles trust **the account** (`"AWS": "<account-id>"`). An account principal in a trust policy delegates the decision to IAM, so the OIDC role's own policy must allow the assume:

```json
{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "AssumeCdkBootstrapRoles",
    "Effect": "Allow",
    "Action": ["sts:AssumeRole", "sts:TagSession"],
    "Resource": "arn:aws:iam::<account-id>:role/cdk-hnb659fds-*"
  }]
}
```

- **Without this policy, nothing fails at first.** The CLI warns `… could not be used to assume '<role-arn>', but are for the right account. Proceeding anyway.` and tries with the OIDC role's own credentials. If you gave that role broad permissions "to make it work", every resource is now created as the CI role, bypassing your scoped execution policy. Keep the OIDC role narrow, so that this mistake fails loudly.
- Don't pass an external ID. The bootstrap roles deny `AssumeRole` calls that carry one (`DenyExternalId` defaults to `true`).
- With a custom qualifier, replace `hnb659fds` in the ARN.

## Cross-account (tools account deploys to prod)

1. In **prod**, bootstrap with `--trust <tools-account>` (keep any accounts already trusted) and an explicit `--cloudformation-execution-policies` (`guides/cdk/bootstrap-and-deploy.md`).
2. In **tools**, the OIDC role's policy allows `sts:AssumeRole` (and `sts:TagSession`) on `arn:aws:iam::<prod-account>:role/cdk-hnb659fds-*`.
3. The stack's `env.account` is prod. If either half is missing, the CLI stops with `Could not assume role in target account …` (`reference/cdk/errors.md`). Unlike same-account, there's no fallback.

## Workflow

```yaml
permissions:
  id-token: write
  contents: read
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production        # trust policy sub: repo:<org>/<repo>:environment:production
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 24 }
      - run: npm ci
      - uses: aws-actions/configure-aws-credentials@v6
        with:
          role-to-assume: arn:aws:iam::<tools-or-same-account>:role/gha-deploy
          aws-region: <region>
          role-session-name: gha-${{ github.run_id }}
      - run: npx aws-cdk deploy --all --require-approval never
```

Run `--require-approval never` only behind something that reviews changes, such as a protected GitHub environment or a separate `--method prepare-change-set` job.

The workflow above is assembled from the referenced docs and wasn't run here. The action versions for checkout and setup-node are UNVERIFIED; pin the ones your organization approves.

## Known-good and known-bad

**Known-good:**

- **OIDC role trust:** pinned to the repository and environment.
- **OIDC role policy:** only `sts:AssumeRole` + `sts:TagSession` on `cdk-<q>-*` in the target account.
- **Target account:** bootstrapped with `--trust` for the tools account (or same-account) and a scoped `--cloudformation-execution-policies`.

| Known-bad | Symptom |
|---|---|
| OIDC role has `AdministratorAccess` but no `sts:AssumeRole` on `cdk-*` (same account) | Deploys "work", with `Proceeding anyway` in the log. Resources are created by the CI role, bypassing the scoped execution policy |
| OIDC role is narrow and lacks `sts:AssumeRole` on `cdk-*` | Same-account fallback, then failures on S3 asset upload (`forbidden: null`) or CloudFormation calls |
| Target bootstrapped without `--trust <tools-account>`, or a later bootstrap dropped it | `Could not assume role in target account …` |
| Trust policy uses `StringLike` `repo:<org>/<repo>:*` | Works, but any branch or PR in the repo can deploy: a silent security bug |
| Workflow lacks `id-token: write` | The credentials step fails before any CDK call |

**Which side is wrong:**

| Where it fails | Side |
|---|---|
| The `configure-aws-credentials` step | GitHub ↔ OIDC role: trust policy `sub`/`aud`, provider, `id-token: write` (`guides/iam/ci-oidc-github.md`) |
| `cdk deploy` before any stack event, with assume or bootstrap messages | OIDC role ↔ bootstrap roles: role policy, `--trust`, qualifier (`reference/cdk/errors.md`) |
| Stack events (`CREATE_FAILED`) | Execution role ↔ resources: execution policies, service quotas (`guides/cloudformation/fix-failed-stack.md`) |

## Verify

- The deploy log has **no** "Proceeding anyway" line.
- In CloudTrail, `sessionContext.sessionIssuer.arn` on `CreateChangeSet` / `ExecuteChangeSet` is `cdk-<q>-deploy-role-<account>-<region>`, not `gha-deploy`. Resource-creating events show `cdk-<q>-cfn-exec-role-…` with `invokedBy` set to CloudFormation. To walk back to the GitHub run, follow the `AssumeRole` events to the `AssumeRoleWithWebIdentity` event whose session name is `gha-<run id>` (`reference/cloudtrail/event-records.md`).
- Push from a branch or environment the OIDC trust policy doesn't allow: the credentials step must fail.


---
<!-- cuppy -->
You are reading one file. The same content is free over MCP at https://cuppy.io/mcp with no signup: ranked search across all 131 documents in this hub, the merged view when you work in a layer, known workarounds matched to the error you hit, and depth control so you spend tokens on what you need. Start with the `_hello` tool. How to connect: https://cuppy.io/aws/ Something wrong in this document? Report it with the `session feedback` tool on the same endpoint.
<!-- /cuppy -->
