Consolidates the five cogarchhubgitops-* source skills into one skill with scope routing: ArgoCD Applications, Copier templates, Helm values, environment answer files, and the end-to-end resource-addition walkthrough. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8.4 KiB
Environment Configuration
Per-environment copier-answers.yaml files: feature flags, image tags, global application
settings, drift between environments, and re-rendering with copier update.
Read this when enabling or disabling a resource for one environment, onboarding a new environment, auditing what is switched on, or investigating why two environments differ.
1. How environments work
Each deployed environment is a rendered copy of the template repository. The rendering is
driven by an answer file supplying environment-specific values for the variables declared in
copier.yaml:
Template repo
+ <env>/copier-answers.yaml
─────────────────────────────
→ rendered cluster state (what ArgoCD reads)
The rendered state normally lives on a separate branch or repository per environment. The
answer files themselves live at <env>/copier-answers.yaml in the template repo.
2. Answer file structure
Copier maintains two metadata fields automatically; everything else is user-defined:
_src_path: ../ # relative path to the template source
_commit: <git-commit-sha> # template commit used for the last render
gitops:
repo_url: https://github.com/org/repo
branch: main
operators:
is_enabled: true
eso_is_enabled: true
cnpg_is_enabled: false
redis_is_enabled: false
configuration:
is_enabled: true
cluster_secret_store_is_enabled: true
applications:
is_enabled: true
global:
is_internal: "false"
ca_env: production
host: myapp.example.com
Keys must match copier.yaml exactly. Copier silently ignores unknown keys — a typo does
not error, it just leaves the variable at its default, which is the most common cause of a
"why is this environment misconfigured" investigation.
3. Variable categories
Feature flags
Flags follow <domain>.is_enabled and <domain>.<sub>_is_enabled:
| Variable | Effect when true |
|---|---|
operators.is_enabled |
Renders the 200-operators band |
operators.eso_is_enabled |
Renders ESO operator group, subscription, configuration |
operators.cnpg_is_enabled |
Renders CNPG operator group and subscription |
operators.redis_is_enabled |
Renders Redis operator group and subscription |
configuration.is_enabled |
Renders the 300-configuration band |
configuration.cluster_secret_store_is_enabled |
Renders ClusterSecretStore |
databases.is_enabled |
Renders the 400-databases band |
advantage_mcp.is_enabled |
Renders the 700-advantage-mcp band |
applications.is_enabled |
Renders the 800-applications band |
Parent flags gate whole bands. With operators.is_enabled: false, every sub-flag under
operators is irrelevant — the entire 200-operators directory is omitted regardless of
what the sub-flags say. When a resource unexpectedly fails to render, check the parent flag
before the specific one.
Image variables
<domain>:
<service>:
image:
address: <registry>/<image-name>
tag: <semver-or-sha>
Every enabled workload needs non-empty values for both.
Global application settings
applications.global carries settings shared by all services in the release:
applications:
global:
is_internal: "true" # string bool
enable_instana: "false" # string bool
ca_secret_name: ca-secret
ca_env: <env-name> # dev | staging | production
maintenance: "false" # string bool
host: <public-hostname>
ca_host: <internal-hostname>
suffix_url: <path-suffix>
node_selector_app: <node-label>
mongo:
host: <mongodb-host>
port: "27017"
user: <mongodb-user>
redis:
host: <redis-host>
port: "6379"
max_retries: "3"
neo4j:
url: <neo4j-bolt-url>
user: <neo4j-user>
4. Booleans: two different kinds
This trips people up constantly:
| Kind | Written as | Why |
|---|---|---|
Copier feature flag (*_is_enabled) |
YAML boolean true / false |
Evaluated as a Jinja2 conditional deciding whether a directory renders |
Helm string-bool (is_internal, maintenance, enable_instana) |
Quoted string "true" / "false" |
The values templates emit them as strings and the chart reads them as strings |
A feature flag written as "false" is a non-empty string — truthy in Jinja2 — so the
resource renders when you meant to disable it. This fails silently and deploys something.
5. Rules
Answer files
_src_pathand_commitare Copier-managed; never edit them by hand.- Keep answer files minimal — only override what differs from the
copier.yamldefault. - Never commit real secrets. Use placeholders and inject at runtime via ExternalSecret.
Multi-environment
- One answer file per environment; never share a file across environments.
- When adding a variable to
copier.yaml, update every answer file needing a non-default value — the rest correctly fall back to the default.
Enabling a resource
- Set the flag to
true. - Supply all required image and configuration variables for that resource.
- Re-render the environment.
- Review the rendered diff before committing.
Skipping step 2 renders a values file with empty fields; Helm then fails at sync with an error that names the chart, not the answer file, which makes it slow to trace back.
Disabling a resource
- Set the flag to
false. - Re-render.
- ArgoCD prunes the Application and its cluster resources on the next sync, because
prune: trueis set. This is destructive and immediate — confirm the resource is really meant to leave the cluster, not just be paused.
6. Workflows
New environment
mkdir <new-env>
# Option A — interactive prompts
copier copy . /tmp/gitops-<new-env> --answers-file <new-env>/copier-answers.yaml
# Option B — start from the nearest existing environment
cp <base-env>/copier-answers.yaml <new-env>/copier-answers.yaml
# then edit for environment-specific values
Option B is faster but carries over every override from the source environment; walk the whole file rather than only the fields you came to change.
Re-render after a template change
copier update /path/to/rendered-<env> \
--answers-file <env>/copier-answers.yaml \
--overwrite
git -C /path/to/rendered-<env> diff
Compare two environments
copier copy . /tmp/env-a --overwrite --answers-file env-a/copier-answers.yaml
copier copy . /tmp/env-b --overwrite --answers-file env-b/copier-answers.yaml
diff -rq /tmp/env-a/cluster /tmp/env-b/cluster
Audit which resources are enabled
python3 -c "
import yaml, sys
a = yaml.safe_load(open(sys.argv[1]))
def walk(d, prefix=''):
for k, v in (d.items() if isinstance(d, dict) else []):
if k.endswith('_is_enabled') or k == 'is_enabled':
print(f'{prefix}{k}: {v}')
elif isinstance(v, dict):
walk(v, prefix + k + '.')
walk(a)
" <env>/copier-answers.yaml
7. Validation
# Render the environment
copier copy . /tmp/gitops-validate --overwrite --answers-file <env>/copier-answers.yaml
# No .jinja may leak through
find /tmp/gitops-validate -name "*.jinja" | wc -l # must be 0
# Every rendered YAML parses
find /tmp/gitops-validate -name "*.yaml" -exec \
python3 -c "import yaml, sys; yaml.safe_load(open(sys.argv[1])); print('OK:', sys.argv[1])" {} \;
# Every Application has its resources directory
for app in $(find /tmp/gitops-validate/cluster/argocd -name "application.yaml" -exec \
python3 -c "import yaml,sys; print(yaml.safe_load(open(sys.argv[1]))['spec']['source']['path'])" {} \;); do
[ -d "/tmp/gitops-validate/$app" ] || echo "MISSING resources dir: $app"
done
8. Mistakes to avoid
- Editing
_src_pathor_commit— breakscopier update. - Enabling a flag without supplying the resource's image and configuration variables.
- Writing a feature flag as
"true"instead oftrue— truthy either way, so it renders when disabled was intended. - Editing an answer file without re-rendering and reviewing the diff.
- Setting
applications.globaladdresses (host, mongo, redis, neo4j) without confirming the environment's real infrastructure — wrong values misconfigure every service silently. - Putting environment secrets in the answer file rather than an external secret manager.
- Assuming a sub-flag controls a resource when its parent band flag is
false.