English | 🇯🇵 日本語
A monorepo that manages four deployment surfaces from a single label-driven CI pipeline:
- AWS infrastructure — Terragrunt + OpenTofu (aws/)
- GitHub configuration — Terragrunt for repo / branch / ruleset settings (github/)
- GCP infrastructure — Terragrunt + OpenTofu (gcp/)
- Kubernetes platform — Helmfile-rendered manifests applied by Flux CD (kubernetes/)
A PR labeled by panicboat/deploy-actions is resolved against workflow-config.yaml to expand into concrete deploy targets, and the matching reusable workflow runs.
.
├── .github/
│ ├── scripts/ # Scripts run by workflows (hydration, terragrunt release)
│ └── workflows/ # Reusable executors, deploy trigger, hydrator/builder, etc.
├── aws/ # Terragrunt stacks per service ({environment})
├── github/ # Terragrunt stacks for GitHub repo / branch settings
├── gcp/ # Terragrunt stacks for the GCP project panicboat-platform-github
├── kubernetes/
│ ├── clusters/ # Flux bootstrap (per cluster)
│ ├── components/ # Helmfile sources (per environment)
│ └── manifests/ # Rendered output that Flux applies
├── docs/ # Runbooks and design notes
├── scripts/ # EKS lifecycle helpers
├── Makefile # EKS teardown entry points
└── workflow-config.yaml # Environments and stack conventions (source of truth)
| Event | Workflow | Terragrunt |
|---|---|---|
deploy:{service} label added to a PR |
label-base-deploy--resolver.yaml | plan every target |
Push to main |
label-base-deploy--resolver.yaml | apply targets with apply_trigger: merge |
| Release PR opened / updated | release--plan.yaml | plan the service's apply_trigger: release environments |
| Release published | release--deploy.yaml | apply the service's apply_trigger: release environments |
- label-base-deploy--dispatcher.yaml adds
deploy:{service}to a PR when it is opened or updated; release-please branches get no label.- Any change under
aws/{service}/, including modules androot.hcl, adds the label.
- Any change under
label-resolverexpands the label into every environment of that service using workflow-config.yaml.- A
{service}/{environment}pair matched byexcludeof theterragruntstack is never planned or applied, including by releases.- A rule with only
environmentcovers every service.
- A rule with only
- Each
aws/{service}with anapply_trigger: releaseenvironment has its own release-please config (release-please-config.json/release-please-manifest.json).- check-release-configs.sh fails CI when a config is missing.
- Commits that touch only
apply_trigger: mergeenvironment directories do not create a release (exclude-paths). - While every
apply_trigger: releaseenvironment is excluded, the release PR is still created, but nothing is planned or applied. - To re-plan, use "Re-run all jobs" on the
Release - Planrun. - To re-apply, run
gh workflow run release--deploy.yaml --ref {service}-vX.Y.Z -f tag={service}-vX.Y.Z.
| Stack | Path convention | Tooling |
|---|---|---|
| AWS infrastructure | aws/{service}/{environment} |
Terragrunt + OpenTofu (reusable--terragrunt-runner.yaml) |
| GitHub configuration | github/{service}/{environment} |
Terragrunt + OpenTofu (reusable--terragrunt-runner.yaml) |
| GCP infrastructure | gcp/{service}/{environment} |
Terragrunt + OpenTofu (reusable--terragrunt-runner.yaml) |
| Kubernetes platform | kubernetes/components/{service}/{environment} |
Helmfile → Kustomize hydration (reusable--kubernetes-hydrator.yaml / reusable--kubernetes-builder.yaml) → Flux CD |
- Environments, AWS regions, and IAM roles are defined in workflow-config.yaml. Adding or removing an environment means editing that file.
apply_triggerin workflow-config.yaml decides when an environment is applied:mergeapplies on push tomain,releaseapplies when the service release is published.- Authentication uses GitHub OIDC. aws/github-oidc-auth/ issues per-environment plan / apply roles that the executors assume.
- GCP stacks reuse the master plan / apply roles: gcp/workload-identity/ lets them exchange their AWS credentials for GCP credentials through Workload Identity Federation, configured by gcp/credential-configuration.json in CI only. Local runs use the gcloud application default credentials.
- Name a
gcp/{service}differently from everyaws/{service}: thedeploy:{service}label and the tfcmt target do not include the provider, so equal names plan both stacks and overwrite each other's plan comment. - The GitHub environment
productionof bothplatformandmonorepoaccepts deployments only from refs that match the tag pattern*-v*. The settings are managed in github/repository/. - Terragrunt remote state lives in S3 + DynamoDB; bucket and lock-table names are defined in each
aws/{service}/root.hcl(and the equivalent under github/ and gcp/).
flowchart LR
subgraph Triggers
PRevent[PR open/sync]
Mainpush[push main]
Release[release published]
end
subgraph Labeling
Dispatcher[label-base-deploy--dispatcher<br/>panicboat/deploy-actions/label-dispatcher]
Trigger[label-base-deploy--resolver<br/>on: pull_request labeled]
Resolver[label-resolver]
end
subgraph KubernetesCI [Kubernetes CI]
Group[helmfile-targets-group<br/>group by env]
Hydrator[reusable--kubernetes-hydrator<br/>matrix: env<br/>concurrency: hydrate-PR-env]
Commit[auto-commit<br/>kubernetes/manifests/]
Builder[reusable--kubernetes-builder<br/>matrix: service x env<br/>diff only]
IndexComment[(PR comment<br/>kubernetes-index-env)]
CompComment[(PR comment<br/>kubernetes-service-env)]
end
subgraph Terragrunt
ReleaseTrigger[release--deploy]
TG[reusable--terragrunt-runner]
Plan[terragrunt plan]
Apply[terragrunt apply]
PRComment[(PR comment)]
end
subgraph Runtime
AWS[(AWS)]
FluxCD[Flux CD<br/>polls platform + monorepo]
Cluster[(cluster)]
end
PRevent --> Dispatcher
Dispatcher --> Trigger
Trigger --> Resolver
Resolver -->|stack: terragrunt| TG
Release --> ReleaseTrigger
ReleaseTrigger --> TG
TG -->|on pull_request| Plan
TG -->|on push main or release published| Apply
Plan --> PRComment
Apply --> AWS
Resolver -->|stack: helmfile| Group
Group --> Hydrator
Hydrator -->|hydrate-component.sh<br/>+ hydrate-index.sh| Commit
Hydrator -->|index diff| IndexComment
Commit --> Builder
Builder --> CompComment
Mainpush -.-> FluxCD
FluxCD --> Cluster
Commit -.->|on diff: re-fires synchronize| Dispatcher
- Flux bootstrap lives in
kubernetes/clusters/{cluster}/flux-system/. The cluster's root Kustomization aggregates the rendered manifests underkubernetes/manifests/{cluster}/and any externalGitRepository(currently the application monorepo). - Platform and application repositories are pulled independently and stay loosely coupled. Sync intervals and source URLs are configured in
kubernetes/clusters/{cluster}/— treat those manifests as the source of truth. - When kubernetes/components/ changes, CI runs scripts/kubernetes-hydrate/, commits the rendered output to kubernetes/manifests/, and posts the diff as a PR comment. Flux applies the change once merged.