Skip to content

GitOps Integration

Attune performs in-place pod resizes via the /resize subresource, which modifies the running pod without changing the Deployment, StatefulSet, or DaemonSet spec. This has specific implications for GitOps workflows.

How it works with GitOps

In-place resize changes the pod resources, not the workload template. Your Git-stored Deployment spec remains unchanged. This means:

  • No drift: ArgoCD/Flux won't detect a diff because the Deployment spec hasn't changed.
  • Rollouts reset resizes: When a Deployment is updated (new image, env change), the new pods start with the original resources from the template. The operator will re-evaluate and resize again after collecting metrics.
  • No feedback loop: The operator doesn't write back to Git. The recommended values live only in the AttunePolicy status.

Default in-place resize does not update Git or workload templates. After a deploy, new pods start with Git resources until Attune re-collects metrics and resizes again. Use this flow so recommended sizes survive the next deploy.

1. Start in Recommend mode with export

Deploy AttunePolicy resources in your GitOps repo with type: Recommend and ConfigMap export enabled.

spec:
  updateStrategy:
    type: Recommend
    export:
      configMap: true

2. Review recommendations

kubectl attune recommendations -n production
kubectl attune explain -n production api-services
kubectl attune export list -n production

3. Emit a workload template patch and open a PR

# Strategic-merge-style YAML for Deployment/StatefulSet/CronJob templates
kubectl attune diff -n production -o yaml > /tmp/attune-resource-patches.yaml

Review the patch, apply it to your Git repo (or feed it to kustomize/helm values), and let Argo CD / Flux sync. New pods then start near recommended sizes without waiting for a full re-observation cycle.

Alternatively, read ConfigMaps named <policy>-<workload>-recommendations (schema key schema-version: v1).

4. Optional continuous in-place between deploys

Once you trust recommendations, switch to Auto (or Canary first). In-place resizes keep running pods healthy between deploys; keep committing template updates periodically so rollouts stay near target.

Do not enable templatePersistence under unmanaged GitOps sync unless you intentionally want the cluster template to diverge from Git (see configuration reference).

ArgoCD-specific notes

  • Resource tracking: ArgoCD tracks Deployments and StatefulSets but not individual pod specs. In-place resizes are invisible to ArgoCD.
  • Health checks: The AttunePolicy CRD has a Ready condition. ArgoCD can use this for health status if you add a custom health check.
  • Sync waves: Deploy the operator (Helm chart) in an early sync wave, and AttunePolicy resources in a later wave.

Flux-specific notes

  • Kustomization ordering: Use dependsOn to ensure the operator is healthy before AttunePolicy resources are applied.
  • Health checks: Flux can check the Ready condition on AttunePolicy resources via healthChecks in the Kustomization.

When to update Git vs let the operator handle it

Scenario Action
Initial right-sizing of a new service Let operator recommend, then commit to Git
Ongoing optimization of stable services Use Auto mode; commit periodically based on savings reports
Pre-deployment sizing Use Recommend mode, review, commit before promoting
Cost reporting Use kubectl attune savings or the Grafana dashboard
Pure GitOps (no direct resizes) Recommend + export.configMap; CI pipeline consumes the ConfigMaps and proposes Git patches (see below). Do not enable templatePersistence under unmanaged sync; the operator would patch live templates and Git would thrash them.
Cluster is source of truth Optional updateStrategy.templatePersistence (Deployment/StatefulSet) so new pods inherit recommended sizes after resize or on recommendation; see configuration reference

Export mode for GitOps pipelines

For environments that want the operator to compute recommendations but require all resource changes to flow through Git (ArgoCD, Flux, etc.):

  1. Set updateStrategy.type: Recommend (or Auto with export also enabled) plus export.configMap: true.
  2. The operator creates one ConfigMap per workload (named <policy>-<workload>-recommendations) containing per-container CPU/memory recommendations, confidence, and a RFC3339 last-updated timestamp. The ConfigMap carries the attune.io/policy label. If status.recommendations[].stale is true, the operator does not rewrite that ConfigMap or bump last-updated. Do not treat a frozen last-updated as fresh. GitOps apply PRs are not opened from that recommendation.
  3. Your CI/CD pipeline (or a lightweight sidecar) reads the ConfigMaps and proposes patches to the Deployment/StatefulSet specs stored in Git.
  4. GitOps applies the patches through the normal sync/approval flow.

See the Auto mode guide for the exact ConfigMap schema, example output, and owner-reference cleanup behavior.

Orphan cleanup (workload left the policy): When a workload leaves the policy selector (selector change, scale-to-zero, or deletion while the policy still exists), the operator automatically deletes the corresponding recommendation ConfigMap on the next reconcile. Only ConfigMaps bearing the matching attune.io/policy label are considered. That is different from recommendations[].stale: orphan cleanup removes exports for workloads that are no longer in scope; a stale flag keeps last-known status and leaves the existing ConfigMap as-is (no rewrite, no last-updated bump).

Consuming the ConfigMap from CI (example):

# In your pipeline, extract the recommendation for a container
CPU_REQ=$(kubectl get cm my-app-my-deployment-recommendations -n prod \
  -o jsonpath='{.data.main\.cpu-request}')
# Then propose a patch to your Deployment in Git (or use kustomize/helm values update)

CLI support: kubectl attune export list (or just kubectl attune export) shows all exported recommendation ConfigMaps with last-updated timestamps, workload/kind, and container counts. kubectl attune status includes an EXPORT column (CM vs -). kubectl attune explain surfaces the export effective value and prints a GitOps-mode note when Recommend+export (or Observe+export) is active. kubectl attune recommendations also prints an export footer note. This makes the recommended pure-GitOps workflow fully first-class in the plugin.

See the full schema and more examples in the Auto mode guide.

This is the primary integration pattern for strict GitOps shops: the operator provides the intelligence (usage-based recommendations), Git remains the source of truth, and the export + orphan cleanup mechanism keeps the hand-off clean and auditable.

Pull request automation (opt-in Phase B)

Default off. When enabled, Attune compares recommendations to workload pod templates (Deployment, StatefulSet, DaemonSet, ReplicaSet, Job, CronJob, and a Rollout that owns spec.template). A Rollout with spec.workloadRef is skipped, because that template lives on the referenced workload. If any container request drifts by at least minChangePercent (default 10), the operator opens or updates a GitHub or GitLab pull request (subject to cooldown, default 24h). Native sidecars (init containers with restartPolicy: Always) are included; regular Job-style init containers are not.

Security

  • Token is read from a Kubernetes Secret via tokenSecretRef. The creating user must be allowed to get that Secret.
  • Tokens are never written to logs, events, or status.
  • Use a fine-scoped PAT with write access to create branches and PRs:
  • GitHub (fine-grained): repository Contents: Read and write and Pull requests: Read and write on the target repo (classic: repo on private repos, or public_repo for public-only).
  • GitLab: api (or project-scoped token with write_repository + write to merge requests) on one project. Prefer short-lived or fine-grained tokens.
  • Optional apiUrl (Enterprise GitHub / self-hosted GitLab) must be https, must not include userinfo, and is rejected when it targets loopback, link-local, or cloud metadata hosts (169.254.169.254 and similar). Private RFC1918/ULA hosts are rejected unless allowPrivateEndpoints: true (self-hosted GitLab/Gitea/Bitbucket). This is stricter than Prometheus addresses so a policy cannot aim the operator token at in-cluster endpoints. Corporate HTTPS_PROXY is not treated as the SSRF target. A blocked endpoint sets GitOpsEndpointBlocked on the GitOpsPullRequest condition.

Example

spec:
  updateStrategy:
    type: Recommend
    export:
      configMap: true
      pullRequest:
        enabled: true
        provider: github          # or gitlab
        repository: org/app-manifests
        baseBranch: main
        tokenSecretRef:
          name: attune-gitops
          key: token
        minChangePercent: 10
        cooldown: 24h
        dryRun: true              # log + status only; no API call
        labels:
          - attune
          - rightsizing

Status

Condition type GitOpsPullRequest:

Reason Meaning
PullRequestDisabled Feature off
NoDrift Templates already near recommendations
PullRequestUnchanged Drift table matches the last PR; no new empty PR
PullRequestCooldown Waiting for cooldown
PullRequestDryRun Would open/update PR
PullRequestOpen PR URL in message
PullRequestFailed API or config error (message is safe)
GitOpsEndpointBlocked apiUrl resolved to a disallowed address (set allowPrivateEndpoints for RFC1918 self-hosted forges)

Metric: attune_gitops_pr_total{result=created|updated|dry_run|failed}.

Dry-run path

Set dryRun: true for first enablement or CI. The operator records PullRequestDryRun and increments dry_run without calling GitHub/GitLab.

First enablement

Use this ordered path when turning on PR automation for the first time.

  1. Export recommendations first (optional but recommended): set export.configMap: true and confirm kubectl attune export list shows ConfigMaps. See Export mode above.
  2. Create a fine-grained token with the scopes in Security (GitHub: Contents + Pull requests write on one repo; GitLab: project write for repository and merge requests).
  3. Store the token in the policy namespace (never in the CR):
    kubectl -n production create secret generic attune-gitops \
      --from-literal=token="ghp_..."
    
  4. Enable dry-run on the policy (pullRequest.enabled: true, dryRun: true, repository, tokenSecretRef). Wait for condition GitOpsPullRequest with reason PullRequestDryRun (first pass) or PullRequestUnchanged (same table on the next reconcile). NoDrift means templates already match. Metric attune_gitops_pr_total{result="dry_run"} increases once when drift is first recorded.
  5. Inspect what a real PR would say: the condition message includes the repository and drift count; the body template is the same as live PRs (see Status).
  6. Turn off dry-run (dryRun: false or omit). On the first live run with drift, Attune bootstraps the head branch if missing, then opens the PR. A prior dry-run of the same table records the fingerprint only; it does not set a PR URL, so it does not block that first live PR:
  7. GitHub: empty bootstrap commit on attune/recommendations-<ns>-<policy> (same tree as baseBranch).
  8. GitLab: branch from baseBranch plus .attune/RECOMMENDATION_DRIFT.md (create, or update if that file already exists on base after a prior merge).
  9. Apply template patches via Git, not by hoping the empty/bootstrap commit is enough:
    kubectl attune diff -n production -o yaml > /tmp/attune-patches.yaml
    # Review, commit to the PR branch (or a new branch), merge via normal GitOps.
    
  10. Verify: condition reason PullRequestOpen and message containing the PR URL; annotation attune.io/gitops-pr-url; metric result="created" or updated. Failures set PullRequestFailed without logging the token.

Cooldown (default 24h) prevents PR thrash. After a merge, the next cycle may bootstrap again only when the drift table changed. Deleting the head branch is one way that happens. On GitHub, a leftover head branch that is not ahead of baseBranch is moved forward with another empty commit so the next pull request is not rejected for having no commits between the refs. A head branch that is already ahead is left as-is. If template vs recommendation is the same set as the last PR, the condition is PullRequestUnchanged and Attune does not open another empty PR. A prior successful PR (attune.io/gitops-pr-url) with no stored fingerprint (upgrades from 0.1.22/0.1.23) is treated the same way: Attune records the live table and skips instead of opening one more empty PR. The same fingerprint is stored on status.gitopsPR so a Flux or Argo apply that replaces metadata.annotations does not start the empty-PR loop again. Attune does not rewrite those annotations on an Unchanged skip; rewriting them would fight the next GitOps apply. Inspect with kubectl get attunepolicy <name> -o jsonpath='{.status.gitopsPR}'. Field reference: configuration. Commit the patch from kubectl attune diff -o yaml so drift clears (NoDrift). The command prints the patch. It does not edit the workload.

Branch bootstrap

The operator updates an existing open PR or MR only when head/source is the Attune branch (attune/recommendations-<ns>-<policy>) and the target is baseBranch (default main). Changing baseBranch or retargeting the existing PR/MR can leave the old one open and create a new one against the current baseBranch.

When no matching open PR/MR exists, Attune makes sure the head branch can accept a new pull request:

  • GitHub: if the branch is missing, an empty bootstrap commit is created (same tree as baseBranch). If the branch exists but is not ahead of baseBranch (typical after merge when the branch was kept), Attune adds another empty commit and moves the branch to it. If the branch is already ahead, it is left unchanged.
  • GitLab: if the branch is missing, it is created from baseBranch with .attune/RECOMMENDATION_DRIFT.md. If the branch exists, Attune updates that marker so the next merge request still has a file delta.

The PR/MR description still carries the full drift table. Template patches remain a review step (kubectl attune diff or your pipeline). Dry-run never creates branches or PRs.

On GitLab, if .attune/RECOMMENDATION_DRIFT.md already exists on baseBranch, Attune retries with an update action so the delta stays non-empty.

If bootstrap fails (token scopes, missing baseBranch, network), status shows PullRequestFailed with the static message PR API request failed. The condition does not include the API response body.