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/
│ └── workflows/ # Workflows that call the shared workflows of panicboat/.github
├── aws/ # Terragrunt stacks per service ({environment})
├── github/ # Terragrunt stacks for GitHub repo / branch settings
├── gcp/ # Terragrunt stacks, one directory per GCP project display name
├── 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 non-production targets |
| Release PR opened / updated | release--plan.yaml | plan the service's production environment |
| Release published | release--deploy.yaml | apply the service's production environment |
- 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. - A change only under an environment that
excludecovers, such asaws/{service}/production/today, adds no label.
- Any change under
label-resolverreadsservices[].stacksof workflow-config.yaml and expands the label into every environment of that service.- Each target goes to the workflow for its
typeattribute (terragruntorhelmfile). - 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 a production environment has its own release-please config (release-please-config.json/release-please-manifest.json). - Commits that touch only non-production environment directories count as changes of the service's release-please package.
- While the production environment 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 |
| GitHub configuration | github/{service}/{environment} |
Terragrunt + OpenTofu |
| GCP infrastructure | gcp/{project}/{service}/{environment} |
Terragrunt + OpenTofu |
| Kubernetes platform | kubernetes/components/{service}/{environment} |
Helmfile → Kustomize hydration → Flux CD |
- Environments are declared in
services[].stacks[].environmentsof workflow-config.yaml. AWS regions and IAM roles for Terragrunt live in itsprofiles, which an environment references withuse. Adding or removing an environment means editing that file. - The
terragruntstack applies the environment namedproductionwhen the service release is published, and every other environment on push tomain. - 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/github-actions-master/workload-identity/ creates the CI project and 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.
{project}is the display name of the GCP project;{project}/project.hclholds its ID, which can differ from the display name.- The workload-identity stack of the CI project is applied from a local machine first, because CI cannot reach GCP before it exists. The organization and billing account roles of the CI principals (project creation and billing use for apply, read-only for plan) are granted by hand, so that the apply role does not need to administer organization IAM.
- Give every
{service}a name that no other stack uses, acrossaws/,github/and every GCP project: thedeploy:{service}label and the tfcmt target include neither the provider nor the GCP project, 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/). The state key isplatform/<unit path from the repository root>/terraform.tfstate, so it never collides across providers.
flowchart LR
subgraph Triggers
PRevent[PR open/sync]
ReleasePR[release 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[group targets by env]
Hydrator[helmfile hydration<br/>per env]
Commit[auto-commit<br/>kubernetes/manifests/]
Builder[kustomize diff<br/>per service x env]
IndexComment[(PR comment<br/>kubernetes-index-env)]
CompComment[(PR comment<br/>kubernetes-service-env)]
end
subgraph Terragrunt
PlanTrigger[release--plan]
ReleaseTrigger[release--deploy]
TG[terragrunt]
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 -->|type: terragrunt| TG
ReleasePR --> PlanTrigger
PlanTrigger --> TG
Release --> ReleaseTrigger
ReleaseTrigger --> TG
TG -->|on PR or release PR| Plan
TG -->|on push main or release published| Apply
Plan --> PRComment
Apply --> AWS
Resolver -->|type: helmfile| Group
Group --> Hydrator
Hydrator --> 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 renders the components, commits the rendered output to kubernetes/manifests/, and posts the diff as a PR comment. Flux applies the change once merged.