agentsclimarketplace

Workload translation

Skill geoffsdesk/portage/skills/workload-translation

Agent-driven EKS to GKE migrations. PSO-grade outcomes, no PSO required.

Install
npx -y skills add geoffsdesk/portage --skill workload-translation

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Translate Kubernetes manifests, Helm charts, and Kustomize overlays from EKS conventions to GKE conventions. Rewrites AWS-specific annotations, replaces ALB Ingress with Gateway API, swaps ebs.csi.aws.com StorageClasses to pd.csi.storage.gke.io, fixes node selectors and tolerations, removes IRSA annotations in favor of Workload Identity, and produces a clean diff per workload. Use when "translate this Helm chart for GKE", "convert manifests to GKE", or after identity-translation in a Portage migration.

SKILL.md

15.8 KB, as published. Nobody here has run it

Workload Translation

You translate Kubernetes manifests, Helm charts, and Kustomize overlays so they run unchanged on GKE. Every change is a tracked diff with a rationale.

Purpose

Convert EKS-conventioned workload definitions to GKE-conventioned equivalents. Produce auditable per-workload diffs with rationale. Do not modify business logic.

When to use this skill

  • Phase 3 of a Portage migration.
  • A user asks to "translate manifests", "convert this Helm chart", "make this work on GKE".

Do NOT use to change application code or container images (that's registry-migration).

Prerequisites

  • 01-discovery/inventory.json and the raw workloads YAML.
  • 04-network-translation/manifests/ for Gateway/HTTPRoute targets.
  • 05-identity-translation/identity-map.json for KSA → GSA mapping.

Procedure

Step 1 — Build a per-workload manifest set

For each workload, gather:

  • The current YAML manifest (from discovery raw or from the source repo).
  • The originating Helm chart and values.yaml (if Helm-managed).
  • The originating Kustomize overlay (if Kustomize-managed).

For Helm:

helm get manifest <release> -n <ns> > <release>.rendered.yaml
helm get values <release> -n <ns> > <release>.values.yaml
helm get all <release> -n <ns> > <release>.helm-all.yaml

For Kustomize:

kubectl kustomize ./overlays/prod > overlays-prod.rendered.yaml

Step 2 — Apply the translation rules

Walk every resource and apply rules. Each rule has a category (drop, replace, add, rewrite) and a rationale.

Annotations to drop

AnnotationAction
eks.amazonaws.com/role-arn (on ServiceAccount)drop; replaced by iam.gke.io/gcp-service-account from identity-translation
eks.amazonaws.com/compute-type: fargatedrop; map workload to Autopilot if cluster is Autopilot
kubernetes.io/ingress.class: albdrop; replaced by Gateway API resources
alb.ingress.kubernetes.io/*drop; covered by Gateway/HTTPRoute/HealthCheckPolicy/GCPBackendPolicy
service.beta.kubernetes.io/aws-load-balancer-*drop; covered by Gateway API or cloud.google.com/* annotations
external-dns.alpha.kubernetes.io/aws-*drop; replace external-dns config with Cloud DNS provider (handled in network-translation)

Annotations to add

AnnotationWhen
iam.gke.io/gcp-service-account: <gsa-email> (on ServiceAccount)When KSA appears in identity-map
cloud.google.com/load-balancer-type: "Internal" (on LB Service)For internal LBs replacing internal NLBs
networking.gke.io/load-balancer-class: "regionalExternal"For pass-through L4 external
cloud.google.com/neg: '{"ingress": true}'For Services backing GKE Gateway

Resource type swaps

SourceTargetNote
Ingress (with alb class)Gateway + HTTPRouteTake from 04-network-translation/manifests/
Service type=LoadBalancer annotated for AWS NLBSame Service, swap annotationsDon't change kind; rewrite the annotation set
StorageClass with provisioner: ebs.csi.aws.comprovisioner: pd.csi.storage.gke.ioRewrite parameters per storage-translation
StorageClass with provisioner: efs.csi.aws.comprovisioner: filestore.csi.storage.gke.ioRewrite per storage-translation
EndpointSlice with addressType: IPv4 referencing AWS DNS LBReuseNo change
MutatingWebhookConfiguration for aws-pod-identity-webhookdropPod identity injection is native on GKE

nodeSelector / tolerations / affinity

Map common EKS labels to GKE labels:

EKSGKE
eks.amazonaws.com/nodegroup: <ng>cloud.google.com/gke-nodepool: <pool>
node.kubernetes.io/instance-type: m5.largenode.kubernetes.io/instance-type: e2-standard-2 (or actual GCE)
topology.kubernetes.io/zone: us-east-1atopology.kubernetes.io/zone: us-central1-a (target region)
karpenter.sh/capacity-type: spotcloud.google.com/gke-spot: "true"
karpenter.sh/nodepool: gpumatched node pool with the same labels (created in landing zone)
Bottlerocket-specific selectorsdrop; COS-only on GKE

For Karpenter NodePools: do not translate the NodePool resource. Instead, build the equivalent GKE Node Pools in the landing zone with the labels and taints captured during discovery. Then nodeSelectors in workloads continue to match.

Topology spread

topologySpreadConstraints mostly translate unchanged. Validate that topologyKey: topology.kubernetes.io/zone works against GKE's regional cluster zones.

Resource requests/limits

Translate verbatim. If targeting Autopilot, validate against Autopilot resource ranges. Workloads outside the ranges either need to move to a Standard cluster or have requests rounded to allowed values. Surface as escalation, do not silently round.

hostPath, hostNetwork, hostPID

  • hostPath referencing AWS-specific paths (/var/lib/kubelet/plugins/...) — usually applies only to CSI/CNI-level DaemonSets. Validate per workload.
  • hostNetwork: true — preserve unchanged. Confirms with discovery escalation list.
  • hostPID: true — preserve unchanged.

In Autopilot clusters: hostNetwork, hostPID, hostPath, and most privileged options are denied. Workloads using them must run on a Standard cluster or be redesigned. This is an escalation, not a silent translation.

Pod Security

EKS cluster defaults rarely enforce PSA. GKE Autopilot enforces restricted by default. Standard clusters can configure PSA.

For each namespace:

  1. Set pod-security.kubernetes.io/enforce: baseline (start) or restricted (target).
  2. Validate every workload manifest against the chosen level. Workloads that fail are surfaced as escalations with a list of violating fields.

Image references

Rewrite image refs to point at Artifact Registry equivalents (output of registry-migration):

123456789012.dkr.ecr.us-east-1.amazonaws.com/payments/api:1.4.2
→
us-central1-docker.pkg.dev/artifact-registry-prod/payments/api:1.4.2

Use the mapping file 08-registry-migration/image-map.json produced by registry-migration. If a workload references an image not yet in that map, surface as an escalation rather than guessing.

Step 3 — Helm-specific translation

For each Helm release in scope:

  1. If the chart is publicly maintained and offers GKE-compatible values, switch values rather than the chart.
  2. Produce a delta values.yaml with only the changed keys, named values.gke.yaml. Examples:
# values.gke.yaml — overrides for GKE
serviceAccount:
  annotations:
    iam.gke.io/gcp-service-account: [email protected]

ingress:
  enabled: false   # using Gateway API resources instead

gateway:
  enabled: true
  className: gke-l7-global-external-managed
  hostnames:
    - api.example.com

persistence:
  storageClass: pd-balanced

nodeSelector:
  cloud.google.com/gke-nodepool: payments
  1. If the chart is internal and embeds AWS assumptions in templates, fork the chart, mark the fork with a MIGRATION_NOTES.md documenting every template change, and store under 06-workload-translation/charts/<chart>-gke/.

Step 4 — Kustomize-specific translation

For each Kustomize root:

  1. Add a new overlay overlays/gke-prod/.
  2. Use patches: to apply transformations rather than editing the base.
  3. Where possible, build a gke-common/ component (KSA annotations, NodePool selectors) that all GKE overlays import.

Step 5 — Per-workload diff and rationale

For every workload, output a side-by-side diff and a rationale list:

# Workload: storefront/web
## Changes
| File                           | Before                                   | After                                   | Rationale |
|--------------------------------|------------------------------------------|-----------------------------------------|-----------|
| Deployment.spec.template…annotations | eks.amazonaws.com/skip-* removed | (removed)                                | EKS-only, no GKE equivalent |
| Deployment.spec.template…serviceAccount | (none)                          | web                                       | Needed for WI binding |
| ServiceAccount.metadata.annotations | eks.amazonaws.com/role-arn         | iam.gke.io/gcp-service-account: …         | identity-translation |
| Ingress (kind: Ingress)        | spec…                                    | DROPPED → see HTTPRoute web.yaml        | Gateway API |
| StorageClass: gp3              | provisioner: ebs.csi.aws.com             | provisioner: pd.csi.storage.gke.io      | storage-translation |
| Container.image                | …ecr…/web:1.2.0                          | …pkg.dev…/web:1.2.0                      | registry-migration |
| Tolerations / nodeSelector     | karpenter.sh/capacity-type: spot         | cloud.google.com/gke-spot: "true"        | node label mapping |

Step 6 — Validate

For each workload:

# Lint
kubectl --dry-run=server apply -f 06-workload-translation/manifests/<workload>/

# Server-side dry-run with PSA enforcement
kubectl --dry-run=server apply --validate=strict -f ...

# Helm template validation
helm template release-name ./chart -f values.gke.yaml | kubectl --dry-run=server apply -f -

For Autopilot targets, also run:

gcloud container clusters describe <cluster> --location <region> \
  --format='value(autopilot.enabled)'
# If true: validate workloads don't use forbidden features.

Decision points

DecisionDefaultWhen to deviate
Fork Helm chart vs values overlayValues overlayFork only when the chart blocks WI annotations or uses AWS-only resources
Convert Ingress to HTTPRoute now vs at cutoverAt translation timeAt cutover if the user wants to ship Ingress first and Gateway later
Default PSA levelbaseline enforced, restricted warnedrestricted enforced if compliance requires
Spot tolerationsTranslate karpenter.sh/capacity-type=spot to cloud.google.com/gke-spot=truePreserve mixed-instance with affinity if cost-sensitive

Outputs / Deliverables

06-workload-translation/
├── manifests/                # Per-workload translated YAML
│   └── <namespace>/<workload>/
├── charts/                   # Forked Helm charts (only when forked)
├── values/                   # values.gke.yaml per release
├── kustomize/                # gke overlays
├── diffs/                    # Diff + rationale per workload (Markdown)
├── psa-violations.md         # Workloads that fail target PSA level
└── escalations.md

Validation

  • Every Deployment, StatefulSet, DaemonSet, Job, CronJob in inventory.json has a translated artifact OR is explicitly marked intentionally-not-migrated (e.g., AWS-only DaemonSet that has no GKE equivalent).
  • kubectl --dry-run=server passes for every translated manifest against the target cluster.
  • Every image reference matches an entry in image-map.json.
  • Every KSA reference matches an entry in identity-map.json.
  • Every StorageClass reference matches a translated StorageClass.

Escalation triggers

  • Workload uses an admission webhook that doesn't have a GKE-compatible version (e.g., aws-pod-identity-webhook, custom webhooks tied to AWS auth).
  • Workload references a CRD whose operator isn't translated (still on EKS).
  • Autopilot target with workloads outside resource bounds.
  • Helm chart with templates that conditionally render AWS-only resources and don't have a flag to disable.

Common pitfalls

  • Translating image refs by string-replace in YAML. Some manifests embed images in non-obvious places (init containers, sidecars in CRD specs, JSON-encoded strings). Walk the entire YAML tree.
  • Forgetting CronJob backoff differences. GKE Autopilot warns/denies certain CronJob configurations.
  • PodTopologySpread + cluster autoscaler. Underprovisioned clusters with strict spread will leave pods Pending. Tune CA's max bounds first.
  • Operator CRDs missing. If an operator (e.g., Postgres operator) is in EKS, you have to install it on GKE before its CRs apply. Surface in the workload-translation phase, not at apply time.
  • Karpenter NodePools do not translate to GKE. Capture taints/labels and rebuild as GKE node pools (or NAP rules) in gke-landing-zone. The community Karpenter-on-GCP provider exists but is not production-ready. See LFF-16.
  • Workloads tuned to Karpenter's seconds-class scale-up will queue under NAP. Increase HPA minReplicas and tune target utilization down for these workloads. See LFF-17.
  • App Mesh retirement. App Mesh retires 2026-09-30; target for a GKE move is Cloud Service Mesh. See LFF-29.

References

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.