Skip to main content
The production posture: the config lives in git, every change is planned on a pull request, and the apply runs from CI on merge. No credentials are stored in the pipeline.
edit _infra/synthetiq.yaml ──▶ open PR ──▶ plan job posts the diff ──▶ review & merge ──▶ apply job provisions

Authentication

Each job authenticates with short-lived OIDC tokens issued by GitHub Actions for that run. The pull-request and merge contexts map to different identities:
ContextGitHub OIDC subjectAWS roleSynthetiq identity
Pull requestrepo:<github-org>/<repo>:pull_requestPlan role (change-set verbs only)None — generate doesn’t need one
Merge to mainrepo:<github-org>/<repo>:ref:refs/heads/mainApply role (full provisioning policy)Service account holding infra:provision
The apply job exchanges its GitHub token for a short-lived Synthetiq token through the service account’s trust; deleting the trust revokes access immediately.
The identity split is the security boundary — not workflow YAML or CLI flags. A PR branch controls its own workflow file, so the pull_request subject must map only to the plan role: no apply permissions, no Synthetiq identity. The apply identities are reachable only from merged code.

One-time setup

1

Create the AWS roles

Two IAM roles in the target account, trusting GitHub’s OIDC provider (token.actions.githubusercontent.com), scoped to your repository:
  • synthetiq-infra-plan — trusted for repo:<github-org>/<repo>:pull_request; policy from synthetiq infra permissions --stage generate
  • synthetiq-infra-apply — trusted for repo:<github-org>/<repo>:ref:refs/heads/main; policy from synthetiq infra permissions --stage provision
2

Create the Synthetiq service account and trust

Find the role id for CI Provision Apply:
synthetiq role list
Create the service account with that role:
synthetiq service-account create "CI Provisioner" --role-id <role-id>
Create the OIDC trust so CI can authenticate as it:
synthetiq trust create \
  --service-account-id <service-account-id> \
  --issuer https://token.actions.githubusercontent.com \
  --subject "repo:<github-org>/<repo>:ref:refs/heads/main"
See Service Account.
3

Add the workflow

The published Synthetiq workflow wires up both jobs with the correct triggers, OIDC permissions, and a concurrency guard so two changes can’t apply at once:
name: synthetiq-infra
on:
  # The pinned @synthetiq/cli version is a generation input — bumping it
  # changes the rendered stack — so package.json triggers a plan too.
  pull_request:
    paths: ["_infra/**", "package.json", "package-lock.json"]
  push:
    branches: [main]
    paths: ["_infra/**", "package.json", "package-lock.json"]
permissions:
  id-token: write      # OIDC tokens for AWS and Synthetiq
  contents: write      # plan commits the reviewed changeset to the PR branch
  pull-requests: write # the plan comment
jobs:
  infra:
    uses: SynthetiqAI/infra-workflow/.github/workflows/plan-apply.yml@v1
    with:
      plan-role-arn: arn:aws:iam::<your-aws-account-id>:role/synthetiq-infra-plan
      apply-role-arn: arn:aws:iam::<your-aws-account-id>:role/synthetiq-infra-apply
      organization-id: <your-org-id>
    secrets: inherit   # forwards the SYNTHETIQ_NPM_KEY repo secret

The flow on every change

  1. Edit _infra/synthetiq.yaml in a branch; open a PR.
  2. The plan job runs synthetiq infra generate: change sets are parked in your AWS account, the rendered CloudFormation template and changeset are committed to the branch, and a single PR comment (updated on later pushes) summarizes the impact — change counts by area and any replacements. The full template diff is in the PR’s Files changed.
  3. Review and merge through your normal branch-protection process.
  4. The apply job runs synthetiq infra provision, executing exactly the parked change set. If the stack moved since the plan, CloudFormation refuses it and the job fails asking for a re-plan. On the first provision, the job log ends with the three app DNS records to create.
  5. Re-running with no config change is a green no-op — schedule the workflow for a free drift check.
For a second gate after merge, put the apply job behind a GitHub environment with required reviewers.

Staying current

Synthetiq publishes infrastructure updates as new CLI versions (all packages share one version number); an update can mean a diff to your stacks. An upgrade is just another pull request:
  1. Pin @synthetiq/cli in the infra repo (package.json + lockfile, installed by both jobs). Never @latest — a floating version injects surprise diffs into unrelated PRs and lets the plan and apply jobs run different versions.
  2. Upgrades arrive as version-bump PRs — point Renovate or Dependabot at the pin. Because package.json/package-lock.json are in the workflow’s trigger paths, the bump alone runs the plan job on the new version, and the PR comment shows the exact diff that release causes in your account.
  3. Review and merge like any other change; security patches are just fast merges.
  4. Same config + same version is a green no-op, so scheduled runs only go red for genuine drift.
Two guards backstop this: CloudFormation refuses a parked change set if the stack moved since planning, and provision refuses a changeset generated by a different CLI version (--allow-version-skew overrides).

Security model

  • No stored credentials — AWS and Synthetiq access are per-run OIDC exchanges; tokens expire in minutes.
  • Immediate revocation — delete the trust or demote the service account’s role and the next exchange fails.
  • The AWS side is yours — your OIDC provider, roles, and trust policies; Synthetiq only specifies the policy contents.
  • Fork PRs are inert — GitHub withholds OIDC tokens from fork pull requests.
  • Audit trail — tokens minted through a trust carry the trust, issuer, and subject in their claims, so every CI action is attributable to the repository and ref that performed it.