Operator Runbooks¶
Recovery procedures for the failure modes you'll actually hit operating an Astrolift install. Each entry is Symptom → Root cause → Resolution → Prevention.
These cover the cluster lifecycle, the deploy loop, teardown, secret rotation, and the provider-specific traps on AWS and GCP. For the happy path, see the Quickstart. For the full install, see Installing Astrolift.
Where to look first
Most deploy and cluster issues surface in two places: the
Deployments detail page lifecycle log (/deployments/<id>) and
the cluster row Health check (/clusters). Read those before
reaching for kubectl — the platform usually already named the
failing step.
1. Cluster registration stuck / failed¶
The cluster never reaches a healthy state after Clusters → Register, or the Health check button reports a failure.
1a. Missing IAM role or kubeconfig permissions¶
Symptom. Registration hangs on "verifying connectivity", or the
health check returns forbidden / Unauthorized / could not list
namespaces.
Root cause. The credential the control plane uses can't reach or can't authorize against the cluster API. For the IAM path, the role isn't assumable (trust policy) or lacks EKS describe/access-entry permission. For the kubeconfig path, the embedded service account lacks RBAC for the namespaces Astrolift manages.
Resolution.
- IAM (EKS): confirm the control plane's identity can assume the role
and that the role has an EKS access entry (or
aws-authmapping) granting cluster access: - kubeconfig: test the exact kubeconfig you pasted, then verify RBAC: Grant the service account a ClusterRole that covers namespaces, deployments, services, ingresses, secrets, and pods/exec.
Prevention. Register with a purpose-built service account (or IRSA role), not a personal admin context that can rotate or expire. Run the Health check immediately after registering and before the first deploy.
1b. cert-manager not installed on the target cluster¶
Symptom. Health check flags cert-manager not detected. Or
registration succeeds but the first app deploy stalls at the TLS step and
no Certificate resource ever goes Ready.
Root cause. Astrolift delegates TLS issuance to cert-manager on the tenant cluster. Without the CRDs and controller, certificate requests are never reconciled.
Resolution.
kubectl get crd certificates.cert-manager.io # missing -> not installed
helm repo add jetstack https://charts.jetstack.io && helm repo update
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager --create-namespace --set crds.enabled=true
kubectl -n cert-manager rollout status deploy/cert-manager-webhook
Re-run the cluster health check.
Prevention. Make cert-manager a registration prerequisite (it's listed
in the Quickstart prerequisites). The
astrolift-prereqs umbrella chart in astrolift-opscode installs it
alongside the other cluster operators if you'd rather manage it there.
1c. Network policy blocking control plane → cluster communication¶
Symptom. Connectivity is intermittent or one-directional: the health
check passes but heartbeats later drop, or vice versa. kubectl from your
laptop works fine, but the control plane can't reach the API server or the
in-cluster agent.
Root cause. A deny-by-default NetworkPolicy (or a security group / firewall rule) is blocking the control-plane source from the cluster API endpoint or the namespaces it manages. This is common when the control plane runs outside the tenant cluster.
Resolution.
- Confirm the cluster API endpoint is reachable from the control plane's egress IP/SG (private-endpoint EKS clusters need the control plane in an allowed CIDR).
- Audit NetworkPolicies in the managed namespaces — Astrolift's own policies are deny-by-default; an over-broad operator policy can also block the platform's agent:
- Allow ingress from the control-plane source to the platform agent and egress to the cluster API.
Prevention. Document the control plane's egress IP/SG and add it to the cluster's API allowlist at registration time. Keep platform-managed namespaces' network policies under Astrolift's control rather than a cluster-wide operator policy that doesn't know about them.
2. App deploy stuck in "deploying"¶
The deployment sits in deploying past the normal ~2–3 minutes. Open
/deployments/<id> and read the lifecycle log — it names the step
that's wedged. Match it below.
2a. Image pull error¶
Symptom. Lifecycle log stalls after "apply"; pods show
ImagePullBackOff / ErrImagePull.
Root cause. Expired or missing registry credentials, or a wrong image tag/digest (the rendered manifest references an image that was never pushed, or a tag that was overwritten).
Resolution.
kubectl -n <org>-<app> describe pod <pod> | grep -A5 -i "failed to pull"
kubectl -n <org>-<app> get serviceaccount default -o yaml # imagePullSecrets present?
- Wrong tag: redeploy from the Deployments page (Redeploy) so the platform re-renders against the image it actually pushed.
- Expired creds: the registry pull secret rotates with the cluster's
workload identity; re-run the cluster Health check to refresh it, or
for ECR confirm the IRSA role still has
ecr:GetAuthorizationToken.
Prevention. Use immutable digests, not mutable :latest-style tags,
so a tag overwrite can't desync the manifest from the registry. Let the
platform manage pull secrets via workload identity rather than hand-placed
imagePullSecrets.
2b. DNS not propagating / cert challenge failing¶
Symptom. Deploy reaches "wait healthy" / "assign URL" but the URL
returns TLS errors or never resolves. The Certificate is pending; the
ACME Challenge is failing.
Root cause. The cert-manager HTTP-01/DNS-01 challenge can't be
satisfied — usually because base_domain is wrong (the hostname doesn't
point at this install's ingress) or DNS hasn't propagated.
Resolution.
kubectl -n <org>-<app> get certificate,challenge
kubectl -n <org>-<app> describe challenge <name> # shows why ACME is failing
dig +short my-first-app.astro.example.com # resolves to the ingress?
- Confirm the wildcard/app hostname resolves to this install's ingress address.
- Confirm the control plane's
baseDomain/ASTROLIFT_BASE_DOMAINmatches the zone you delegated. - For custom domains, the validation TXT token must be in DNS — copy it
from
/apps/<slug>/domainsand click Recheck after it propagates.
Prevention. Verify the base zone delegation and a wildcard record
during install, before the first app. Set baseDomain to a zone you
actually control — it's a required value precisely so it can't silently
default to something wrong.
2c. Pod CrashLoopBackOff¶
Symptom. Lifecycle log shows the rollout applied but never goes
healthy; pods cycle CrashLoopBackOff.
Root cause. The app process exits on start — a bad/missing env var, a secret that isn't attached, or a healthcheck path/port that doesn't match what the process serves (the platform marks the pod unready and the rollout never completes).
Resolution.
kubectl -n <org>-<app> logs <pod> --previous # why the last start died
kubectl -n <org>-<app> describe pod <pod> # readiness probe failures
- Missing/empty env: add it on
/apps/<slug>/secrets, or confirm an attached managed service isactiveso its envelope (e.g.DATABASE_URL) is injected. - Healthcheck mismatch: the
healthcheckinastrolift.toml(e.g.http:/health) must hit a path/port the container actually answers200on. Fix it in the manifest and redeploy.
Prevention. Keep the manifest's port and healthcheck in sync with
the app. Use the Config page render preview to catch obvious manifest
errors before deploy. Attach secrets/services before the deploy that
depends on them.
2d. Rollout timeout¶
Symptom. Lifecycle log ends in a rollout timeout; some replicas never
become Ready even though the image is fine and the app starts.
Root cause. Insufficient cluster resources to schedule the requested
replicas (CPU/memory requests exceed allocatable, or no node fits), or a
misconfigured HPA whose minimum can't be satisfied. Pods sit Pending.
Resolution.
kubectl -n <org>-<app> get pods # any Pending?
kubectl -n <org>-<app> describe pod <pod> # "Insufficient cpu/memory" / "didn't match"
kubectl describe node | grep -A5 Allocated
kubectl -n <org>-<app> get hpa
- Resource pressure: scale the node group out (see
§7 for EKS) or lower the workload's
requests/replica count in
astrolift.toml. - HPA: confirm
min/maxare sane and the metrics server is running; an HPA min above what the cluster can schedule will time out forever.
Prevention. Right-size requests/limits and HPA bounds per workload on
/apps/<slug>/workloads/<slug>. Keep headroom on the node group, or
enable the cluster autoscaler so a larger replica count can actually land.
3. ALB FailedNetworkReconcile (AWS-specific)¶
Symptom. On EKS, the ALB target group shows targets unhealthy
after an Ingress change — classically right after toggling the auth gate
on an app. The AWS Load Balancer Controller logs a
FailedNetworkReconcile loop:
expected exactly one securityGroup tagged with kubernetes.io/cluster/....
Root cause. A dual-SG tag conflict. Both the EKS cluster security
group and the node shared security group carry
kubernetes.io/cluster/<name>: owned. The controller's
TargetGroupBinding reconciler discovers the backend SG by that tag and
expects exactly one; finding two, it can't decide which to add the
node-traffic rule to and loops, leaving target-group health stuck on
every Ingress reconcile.
Resolution.
- Confirm the controller is told not to manage the backend SG rules
itself — it should run with
manageBackendSecurityGroupRules=falseand an explicitbackendSecurityGroup. Astrolift sets both at cluster bootstrap (seebackend/providers/aws/cluster_eks.py, where the ALB Helm values pinmanageBackendSecurityGroupRulestoFalseonce abackendSecurityGroupis discovered or supplied): - With the controller no longer managing the rule, the inbound rule must exist once. If targets are still unhealthy, add inbound TCP:80 (and your target port) from the node SG to the backend SG manually:
- Confirm the dual-tag situation:
Prevention. Set manageBackendSecurityGroupRules=false and pin
backendSecurityGroup at cluster registration time — Astrolift's EKS
registration does this for you, which is why clusters registered through
the platform don't hit the loop. If you install the controller by hand
outside Astrolift, replicate those two values. Don't tag both the cluster
SG and the node shared SG owned; one owned, others shared.
4. Teardown stuck¶
TeardownAppWorkflow (or a cluster teardown) doesn't finish; resources
linger.
4a. Namespace stuck in Terminating¶
Symptom. The app's namespace sits in Terminating indefinitely; the
teardown report never closes out.
Root cause. A finalizer on a resource in the namespace can't complete — commonly a cert-manager, load-balancer, or CRD finalizer waiting on an upstream cloud resource that's already gone or unreachable.
Resolution.
kubectl get ns <org>-<app> -o json | jq '.status.conditions'
kubectl api-resources --verbs=list --namespaced -o name \
| xargs -n1 kubectl get -n <org>-<app> --ignore-not-found
Find the resource still holding a finalizer and clear it once you've confirmed the upstream object is actually gone:
Then re-run teardown from the platform so the report reconciles.
Prevention. Let the platform tear down in order — deprovision managed
services and remove domains before deleting the app, so finalizers
have their upstream dependencies removed first. The workflow does this
when you delete top-down; manual kubectl delete ns skips the ordering.
4b. PVC not releasing¶
Symptom. Teardown completes for compute but the underlying disk/volume remains; a re-create later collides or storage cost lingers.
Root cause. The StorageClass reclaimPolicy is Retain, so deleting
the PVC intentionally leaves the PV (and the cloud disk) behind.
Resolution.
If retention was intentional, archive/snapshot then delete the PV and the
cloud disk manually. If it wasn't, set the class to Delete for future
apps.
Prevention. Decide the reclaim policy per app deliberately — Retain
for stateful data you can't lose, Delete for ephemeral workloads. The
teardown report lists retained volumes explicitly so they're never a
silent surprise; read it to completion.
4c. Confirming the teardown report¶
After a clean teardown the workflow emits a report of what it removed
(namespace, registry repo, DNS records, managed services, identity
bindings) and what it deliberately retained (e.g. Retain PVCs). Find
it on the app's events / the workflow detail. A teardown isn't "done"
until that report renders without an error step — if it's missing,
teardown is still in flight or wedged on one of the cases above.
5. Secret rotation procedure¶
Rotating a credential without taking the app down.
Managed-service credential.
- On
/apps/<slug>/managed-services, the active service owns its connection envelope (e.g.DATABASE_URL). Rotate the upstream credential through the service's rotation action; the platform updates the injected envelope and triggers a rolling restart so pods pick up the new value with zero-downtime overlap. - Watch the resulting deployment in Deployments — the rollout keeps old pods serving until new pods (with the new credential) are healthy.
DeployToken (CI credential).
- On
/apps/<slug>/tokens, click Rotate on the token. A new secret is shown once — copy it into your CI secret store. - The previous secret stays valid for a 24-hour grace window (the default), so an in-flight CI run or a not-yet-updated pipeline doesn't break the moment you rotate.
- Update CI, then let the old secret lapse. Use Revoke (not Rotate) only when you need an immediate cutoff — Revoke breaks CI flows using that token right away.
Confirming rotation completed without downtime.
- Deployment shows
succeeded, not a restart loop. - For DeployTokens, a CI run using the new secret authenticates; the old one still works until the grace window elapses, then 401s.
Prevention. Always Rotate for graceful handoff; reserve Revoke for compromise. Rotate CI tokens within the 24h window rather than letting them expire unattended. Use managed-service rotation actions rather than editing connection strings by hand, so the rolling restart is coordinated.
6. GKE credential expiry¶
Symptom. A registered GKE cluster flips to unreachable and its heartbeat stops. Health check returns an auth/token error. Deploys to that cluster queue or fail.
Root cause. The credential behind the cluster's source connection
expired — a short-lived gcloud token wasn't refreshed, or the service
account key was rotated/revoked upstream.
Resolution.
gcloud auth login # or: gcloud auth activate-service-account --key-file=key.json
gcloud container clusters get-credentials <cluster> \
--region <region> --project <project>
kubectl get ns # confirm the refreshed creds work
Then update the stored credential on the cluster's SourceConnection (Settings → Source Providers, or the cluster registration) with the refreshed value, and re-run the cluster Health check. The heartbeat resumes once the new credential authenticates.
Prevention. Bind GKE access to Workload Identity (or a managed
service account) rather than a user gcloud token, so the control plane
holds a continuously-refreshable credential instead of one that expires
out from under it.
7. EKS node group stuck scaling¶
Symptom. New pods sit Pending and the EKS managed node group won't
scale out — replica count or new workloads can't be scheduled even though
the autoscaler should be adding nodes.
Root cause. The Auto Scaling Group can't launch instances. The usual culprits: Spot capacity unavailable in the AZ/instance type, the AMI referenced by the launch template is missing/deregistered, or the node IAM role is missing required permissions (so nodes launch but never join, or never launch).
Resolution.
- Confirm pods are actually pending on scheduling, not on something else:
- Check the ASG activity history for launch errors — AWS console →
EC2 → Auto Scaling Groups →
→ Activity . Or: - Map the error:
- Spot capacity (
capacity-not-available/ spot interruption): add fallback instance types to the node group, spread across more AZs, or add an on-demand node group as backstop. - AMI not found: update the launch template / node group to a current EKS-optimized AMI release.
- IAM role missing: ensure the node role has the EKS worker, CNI, and ECR-read managed policies and a valid instance profile.
Prevention. Configure node groups with multiple instance types and AZs so a single Spot pool drying up doesn't block scheduling. Keep the cluster autoscaler / Karpenter healthy and its IAM permissions current. Pin a maintained AMI release channel rather than a specific AMI ID that can be deregistered. Watch the node group health field — EKS surfaces these exact causes there before pods start piling up.