This reusable workflow automates Kubernetes application deployment using Kustomize and ArgoCD.
It renders manifest files using Jinja2-style templating implemented with Nunjucks, commits the results into your continuous deployment (CD) repo, and uses the ArgoCD REST API to create, sync, and validate applications.
β First-time deployments are supported β ArgoCD apps are auto-created if missing.
ποΈ Optionaldelete_first
β force ArgoCD app deletion before re-create.
β οΈ Self-hosted runner required β must have cluster network access and tooling installed.
Name | Required | Type | Default | Description |
---|---|---|---|---|
github_runner |
β | string | β | Label of the self-hosted runner (must have cluster access) |
namespace |
β | string | β | Kubernetes namespace for deployment |
target_environment |
β | string | dev |
Logical environment (dev , qa , prod ). Still required even if target_cluster is set, as it controls approvals and environment scoping. |
target_cluster |
β | string | "" |
If set, resolves the cluster globally across all env_map environments by matching the cluster name. If omitted, the cluster is resolved from the given target_environment only. |
ref |
β | string | `${{ github.ref | |
delete_first |
β | boolean | false |
Delete ArgoCD app(s) before deploying |
cd_repo |
β | string | β | Continuous deployment repo where templated manifests are committed |
cd_repo_org |
β | string | β | Continuous deployment repo organization |
github_environment |
β | string | β | GitHub Environment for approvals and env-scoped secrets |
-
Mode A: Single App
application
(string)deploy_path
(string, defaultDeployments
)image_tag
(string, optional)image_base_name
(string, optional)image_base_names
(comma-separated string, optional)overlay_dir
(string, required)
-
Mode B: Multi App
application_details
(JSON array, optional)[ {"name":"app1","images":["repo/app1","repo/sidecar"],"path":"services/app1/overlays/prod"}, {"name":"app2","images":["repo/app2"],"path":"apps/app2/overlays/prod"} ]
The workflow needs an env_map
that defines your environments, clusters, and related metadata.
You can provide it in two ways:
- As a workflow input (
with: env_map: "<JSON>"
) β preferred - As an environment variable (
ENV_MAP
from the runner) β fallback for self-hosted runners
If neither is provided, the workflow fails.
-
If
target_cluster
is set:- Searches all clusters across all environments in the
env_map
. - Selects by exact cluster name (case-insensitive).
- Searches all clusters across all environments in the
-
If
target_cluster
is not set:- Looks inside the
target_environment
only. - If single cluster β use it.
- If multiple clusters β fail with error listing options.
- Looks inside the
{
"dev": {
"cluster_count": 1,
"clusters": [
{ "cluster": "aks-dev-weu", "dns_zone": "internal.dev.example.com", "container_registry": "ghcr.io/my-org", "uami_map": [] }
]
},
"prod": {
"cluster_count": 2,
"clusters": [
{ "cluster": "aks-prod-weu", "dns_zone": "internal.example.com", "container_registry": "ghcr.io/my-org", "uami_map": [] },
{ "cluster": "aks-prod-use", "dns_zone": "internal.example.com", "container_registry": "ghcr.io/my-org", "uami_map": [] }
]
}
}
When deploying to Azure AKS, some clusters may define User Assigned Managed Identities (UAMIs) in the env_map
.
These are passed as an array under uami_map
:
"uami_map": [
{
"uami_name": "prodcluster-app-uami",
"uami_resource_group": "rg-demo",
"client_id": "1111-aaaa"
},
{
"uami_name": "sidecar-uami",
"uami_resource_group": "rg-demo",
"client_id": "2222-bbbb"
}
]
For each entry:
-
Take the
uami_name
- If it starts with
<cluster_name>-
(the selected cluster name followed by a dash), that prefix is removed.- Example:
prodcluster-app-uami
βapp-uami
.
- Example:
- If it starts with
-
Normalize the name
- Replace
-
with_
.- Example:
sidecar-uami
βsidecar_uami
.
- Example:
- If the result doesnβt start with a letter or
_
, prepend_
.- Example:
123-identity
β_123_identity
.
- Example:
- Replace
-
Export as environment variable
- Variable name = normalized
uami_name
(lowercased). - Value =
client_id
of the UAMI. - Written into
$GITHUB_ENV
, so itβs available to all subsequent steps.
- Variable name = normalized
-
Deduplicate
- If two UAMIs normalize to the same name, duplicates are skipped with a warning.
Input:
"uami_map": [
{"uami_name": "prodcluster-app-uami", "uami_resource_group": "rg-demo", "client_id": "1111-aaaa"},
{"uami_name": "sidecar-uami", "uami_resource_group": "rg-demo", "client_id": "2222-bbbb"}
]
Output exported vars:
app_uami=1111-aaaa
sidecar_uami=2222-bbbb
You can reference these exported variables during Jinja2 templating with Nunjucks:
env:
- name: APP_UAMI_CLIENT_ID
value: {{ app_uami }}
- name: SIDECAR_UAMI_CLIENT_ID
value: {{ sidecar_uami }}
Name | Required | Type | Default | Description |
---|---|---|---|---|
delete_only |
β | boolean | false |
If true , the workflow will only delete ArgoCD apps for the specified cluster/namespace and clean up their corresponding directories in the CD repo. |
When delete_only: true
:
-
Runs:
- Environment resolution (
env_map
/target_environment
/target_cluster
) - Authentication (
argocd_auth_token
or username/password) - ArgoCD connection setup (
argocd_conn
) - ArgoCD app deletion (API calls)
- Cleanup of app directories in the CD repo (
continuous-deployment/<cluster>/<namespace>/<app>
) - Commit & push of deletions to the CD repo
- Environment resolution (
-
Skips:
- Manifest templating with Nunjucks
- Image patching
kustomize build
- Uploading artifacts
- Creating or syncing ArgoCD apps
This mode is useful for cleanly tearing down apps in both ArgoCD and your CD repo, leaving no trace of the appβs manifests.
jobs:
delete:
uses: gitopsmanager/k8s-deploy/.github/workflows/deploy.yaml@v2
with:
github_runner: my-self-hosted
cd_repo: continuous-deployment
cd_repo_org: my-org
github_environment: prod
target_environment: prod
target_cluster: aks-prod-weu
namespace: my-namespace
delete_only: true
secrets:
CONTINUOUS_DEPLOYMENT_GH_APP_ID: ${{ secrets.CD_GH_APP_ID }}
CONTINUOUS_DEPLOYMENT_GH_APP_PRIVATE_KEY: ${{ secrets.CD_GH_APP_KEY }}
ARGOCD_CA_CERT: ${{ secrets.ARGOCD_CA_CERT }}
---
### ArgoCD / Misc
| Name | Required | Type | Default | Description |
|---------------------|----------|---------|----------|-------------|
| `argocd_auth_token` | β | string | β | Direct ArgoCD API token |
| `argocd_username` | β | string | β | ArgoCD username (basic auth fallback) |
| `argocd_password` | β | string | β | ArgoCD password (basic auth fallback) |
| `kustomize_version` | β | string | `5.0.1` | Version of kustomize to install |
| `skip_status_check` | β | boolean | `false` | Skip waiting for ArgoCD sync |
| `insecure_argo` | β | boolean | `false` | Disable TLS verification for ArgoCD API |
| `debug` | β | boolean | `false` | Print copied directory structure |
---
## π Secrets
| Name | Required | Description |
|----------------------------------------|----------|-------------|
| `CONTINUOUS_DEPLOYMENT_GH_APP_ID` | β
| GitHub App ID used for CD repo pushes |
| `CONTINUOUS_DEPLOYMENT_GH_APP_PRIVATE_KEY` | β
| GitHub App private key |
| `ARGOCD_CA_CERT` | β | PEM CA cert for ArgoCD endpoint |
| `ARGOCD_USERNAME` / `ARGOCD_PASSWORD` | β | Fallback basic auth credentials |
---
## π Deployment Flow
1. Checkout repos
2. Parse `env_map`
3. Export UAMI client IDs
4. Template manifests with **Nunjucks (Jinja2 style)**
5. Copy to CD repo structure
6. Patch images
7. Build with kustomize
8. Upload artifacts
9. Commit & push
10. Authenticate with ArgoCD
11. Delete apps if `delete_first`
12. Create apps
13. Sync
14. Wait for sync (unless skipped)
---
## π§ Actions Used
| Action | Version | Purpose |
|--------|---------|---------|
| [`actions/checkout`](https://github.com/actions/checkout) | `v4` | Checkout source, reusable, and CD repos |
| [`actions/create-github-app-token`](https://github.com/actions/create-github-app-token) | `v2` | Generate GitHub App token for CD repo access |
| [`gitopsmanager/detect-cloud`](https://github.com/gitopsmanager/detect-cloud) | `v1` | Detect cloud provider (AWS, Azure, GCP, unknown) |
| [`actions/github-script`](https://github.com/actions/github-script) | `v7` | Used extensively for JSON parsing, cluster selection, templating, ArgoCD API calls, etc. |
| [`imranismail/setup-kustomize`](https://github.com/imranismail/setup-kustomize) | `v2` | Install kustomize for manifest builds |
| [`actions/upload-artifact`](https://github.com/actions/upload-artifact) | `v4` | Upload templated manifests and built kustomize manifests |
---
## π¦ Artifacts Produced
| Artifact name | Contents | Notes |
|---------------|----------|-------|
| `templated-source-manifests-<cluster>` | Application manifests after **Nunjucks (Jinja2-style) templating** | Shows cluster-specific substitutions and injected UAMI vars |
| `built-kustomize-manifest-<cluster>` | Final combined YAML output from `kustomize build` | Ready-to-apply manifests, concatenated with `---` delimiters |
---
## π§ͺ Example Usage
```yaml
jobs:
deploy:
uses: gitopsmanager/k8s-deploy/.github/workflows/deploy.yaml@v2
with:
github_runner: my-self-hosted
cd_repo: continuous-deployment
cd_repo_org: my-org
github_environment: prod
target_environment: prod
target_cluster: aks-prod-weu
namespace: my-namespace
application: my-app
deploy_path: manifests/overlays/prod
image_tag: abc123
overlay_dir: prod
env_map: ${{ secrets.ENV_MAP_JSON }}
secrets:
CONTINUOUS_DEPLOYMENT_GH_APP_ID: ${{ secrets.CD_GH_APP_ID }}
CONTINUOUS_DEPLOYMENT_GH_APP_PRIVATE_KEY: ${{ secrets.CD_GH_APP_KEY }}
ARGOCD_CA_CERT: ${{ secrets.ARGOCD_CA_CERT }}
ArgoCD ingress must be reachable at:
https://${cluster}-argocd-argocd-web-ui.${dns_zone}
@v2
β Stable v2 series@v1
β Legacy v1 series
MIT.
Third-party actions listed in THIRD-PARTY-ACTIONS-AND-TOOLS.md.