☸️ Kubernetes on the Cloud: A Managed Cluster and kubectl

What a managed cluster is on Azure, AWS, Google Cloud and Oracle Cloud, how to create one and connect to it, and how to talk to it: kubeconfig, contexts, manifests, Secrets and clean-up, with the parts that need no cluster run for real

💡 How this page was tested. The kubectl parts were run for real. On 6 October 2026 I downloaded kubectl v1.37.1 from the Kubernetes release server, checked its SHA-256 checksum (it matched), and ran it in a throwaway folder with an empty home directory. With no cluster I could still run everything that works offline: the errors you see before you are configured, kubeconfig files and contexts (with dummy servers), manifests written by --dry-run=client, a base64 demonstration with a Secret, and kubectl kustomize. The address I pointed the fake cluster at (203.0.113.10) is in a range reserved for documentation, so nothing answers there, which is also a real example of what an unreachable cluster looks like. Creating a managed cluster was not run: no cloud account was available, so the creation commands are from the documentation, not run here. The downloaded folder was deleted afterwards. Long error lines are shortened (the date and process number at the start of kubectl's log lines are removed). Related pages: containers, networking, IAM and the Kubernetes node access tutorial.

What Kubernetes is

Kubernetes runs containers across many machines and keeps them running the way you described. You do not start containers by hand: you give Kubernetes a desired state ("two copies of this image, reachable on port 80"), and controllers keep comparing reality with that description and fixing the difference. A copy dies, a machine fails, you change the image: the loop notices and acts. It is the same "declare, compare, reconcile" idea as Terraform, applied to running programs.

The parts, from the Kubernetes documentation (read on 6 October 2026):

What "managed" hides

The documentation says it plainly: managed services (EKS, GKE, AKS) typically handle the entire control plane for you, and you manage the node side and your workloads. Running the control plane yourself (etcd backups, upgrades, certificates) is a job of its own. On a cloud, almost nobody should do it. Some newer modes go further and manage the nodes too.

The four managed services

The pattern across all four: a mode where the cloud manages the nodes (AKS Automatic, EKS Auto Mode, GKE Autopilot, OKE virtual nodes) is easiest to run, and a mode where you manage node pools gives more control and more work. For a first cluster, take the managed-nodes mode unless you need something it does not allow.

Before you create anything

A cluster is the most expensive thing in this tutorial series to leave running. A checklist, with the pages that explain each item:

  1. An account and a budget alert (the account pages for each cloud). Note that free trials and always-free tiers do not cover a normal cluster: nodes, load balancers and control plane fees are billed or use your credit quickly.
  2. A region near you, and zones if you want resilience (the regions pages).
  3. Network space. In many setups pods get their own addresses from your network, so a cluster eats address space much faster than a few servers do. Plan a large enough range (see CIDR in the networking page).
  4. Identities. The cluster needs permission to create load balancers and disks, and people and pods need access to the cluster (the IAM page). The cloud usually creates a role for the cluster for you.
  5. The CLI signed in and kubectl installed (below). The CLI pages show the sign-in methods.
  6. A plan to delete it (the last section).

Creating a cluster (from the documentation, not run here)

The exact options change often: run the command with --help and read the current documentation. The shape of it on each cloud:

# Azure: AKS
az group create --name lab-rg --location westeurope
az aks create --resource-group lab-rg --name lab-aks --node-count 2 --generate-ssh-keys

# AWS: EKS (eksctl is the EKS command-line tool)
eksctl create cluster --name lab-eks --region eu-west-1 --nodes 2

# Google Cloud: GKE Autopilot
gcloud container clusters create-auto lab-gke --region us-central1

# Oracle Cloud: OKE
#   the console's "Quick create" wizard is the easiest start: it builds the network,
#   the cluster and a node pool in one go

Connecting: kubeconfig

kubectl is the Kubernetes command-line client. A cluster does not log you in by itself: each cloud has one command that writes the cluster and your credentials into your kubeconfig file (from the documentation, not run here):

az aks get-credentials --resource-group lab-rg --name lab-aks
aws eks update-kubeconfig --name lab-eks --region eu-west-1
gcloud container clusters get-credentials lab-gke --region us-central1
oci ce cluster create-kubeconfig --cluster-id CLUSTER_OCID --file $HOME/.kube/config --region eu-frankfurt-1

After that, kubectl works. Now, how kubectl behaves. First, with nothing configured (real):

user@lab:~$ kubectl version --client
Client Version: v1.37.1
Kustomize Version: v5.8.1
user@lab:~$ kubectl get pods
E: "Couldn't get current server API group list" err="Get \"http://localhost:8080/api?timeout=32s\": dial tcp 127.0.0.1:8080: connect: connection refused"
The connection to the server localhost:8080 was refused - did you specify the right host or port?
user@lab:~$ ls ~/.kube 2>&1 | head -2
ls: cannot access '~/.kube': No such file or directory

Contexts: which cluster am I talking to?

A kubeconfig holds three lists: clusters (an address and how to trust it), users (credentials) and contexts (a cluster, a user and a default namespace, under a name). kubectl always acts on the current context. I made two contexts with dummy servers (real output):

user@lab:~$ kubectl config set-cluster aks-lab --server=https://203.0.113.10:6443 --insecure-skip-tls-verify=true
Cluster "aks-lab" set.
user@lab:~$ kubectl config set-cluster gke-lab --server=https://203.0.113.20 --insecure-skip-tls-verify=true
Cluster "gke-lab" set.
user@lab:~$ kubectl config set-credentials lab-admin --token=EXAMPLE-TOKEN-NOT-REAL
User "lab-admin" set.
user@lab:~$ kubectl config set-context aks-dev --cluster=aks-lab --user=lab-admin --namespace=dev
Context "aks-dev" created.
user@lab:~$ kubectl config set-context gke-test --cluster=gke-lab --user=lab-admin --namespace=test
Context "gke-test" created.
user@lab:~$ kubectl config get-contexts
CURRENT   NAME       CLUSTER   AUTHINFO    NAMESPACE
          aks-dev    aks-lab   lab-admin   dev
          gke-test   gke-lab   lab-admin   test
user@lab:~$ kubectl config use-context aks-dev
Switched to context "aks-dev".
user@lab:~$ kubectl config current-context
aks-dev
user@lab:~$ kubectl config view --minify
apiVersion: v1
clusters:
- cluster:
    insecure-skip-tls-verify: true
    server: https://203.0.113.10:6443
  name: aks-lab
contexts:
- context:
    cluster: aks-lab
    namespace: dev
    user: lab-admin
  name: aks-dev
current-context: aks-dev
kind: Config
users:
- name: lab-admin
  user:
    token: REDACTED
user@lab:~$ ls -l ~/.kube | sed 's/user/user/g'
total 4
-rw------- 1 user user 511 Oct  6 18:36 config
user@lab:~$ kubectl get pods
E: "Couldn't get current server API group list" err="Get \"https://203.0.113.10:6443/api?timeout=32s\": dial tcp 203.0.113.10:6443: i/o timeout"
Unable to connect to the server: dial tcp 203.0.113.10:6443: i/o timeout

Read it:

More than one kubeconfig file

kubectl config --help explains the loading rules (real output): --kubeconfig loads only that file; the KUBECONFIG environment variable is a list of files that are merged; otherwise ~/.kube/config. With two files:

user@lab:~$ ls kc
a.yaml
b.yaml
user@lab:~$ KUBECONFIG=kc/a.yaml kubectl config get-contexts
CURRENT   NAME    CLUSTER   AUTHINFO   NAMESPACE
          ctx-a   c-a       u-a        
user@lab:~$ KUBECONFIG=kc/a.yaml:kc/b.yaml kubectl config get-contexts
CURRENT   NAME    CLUSTER   AUTHINFO   NAMESPACE
          ctx-a   c-a       u-a        
          ctx-b   c-b       u-b        
user@lab:~$ kubectl get --help | head -n 8
Display one or many resources.

 Prints a table of the most important information about the specified resources. You can filter the list using a label selector and the --selector flag. If the desired resource type is namespaced you will only see results in the current namespace if you don't specify any namespace.

 By specifying the output as 'template' and providing a Go template as the value of the --template flag, you can filter the attributes of the fetched resources.

Use "kubectl api-resources" for a complete list of supported resources.

user@lab:~$ kubectl config --help | sed -n 1,12p
Modify kubeconfig files using subcommands like "kubectl config set current-context my-context".

 The loading order follows these rules:

  1.  If the --kubeconfig flag is set, then only that file is loaded. The flag may only be set once and no merging takes place.
  2.  If $KUBECONFIG environment variable is set, then it is used as a list of paths (normal path delimiting rules for your system). These paths are merged. When a value is modified, it is modified in the file that defines the stanza. When a value is created, it is created in the first file that exists. If no files in the chain exist, then it creates the last file in the list.
  3.  Otherwise, ${HOME}/.kube/config is used and no merging takes place.

Available Commands:
  current-context   Display the current-context
  delete-cluster    Delete the specified cluster from the kubeconfig
  delete-context    Delete the specified context from the kubeconfig

With KUBECONFIG=kc/a.yaml one context is visible, and with KUBECONFIG=kc/a.yaml:kc/b.yaml both appear. That is a clean way to keep one file per cluster (for example one per cloud account) and combine them only when you need to.

Writing manifests

Everything in Kubernetes is an object described in YAML. You rarely write one from a blank page: kubectl create ... --dry-run=client -o yaml prints the manifest it would create, without creating anything. These ran for real, with no cluster:

user@lab:~$ kubectl create namespace lab --dry-run=client -o yaml
apiVersion: v1
kind: Namespace
metadata:
  name: lab
spec: {}
status: {}
user@lab:~$ kubectl create deployment web --image=nginx:1.27 --replicas=2 --port=80 --dry-run=client -o yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: web
  name: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  strategy: {}
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - image: nginx:1.27
        name: nginx
        ports:
        - containerPort: 80
        resources: {}
status: {}
user@lab:~$ kubectl create service loadbalancer web --tcp=80:80 --dry-run=client -o yaml
apiVersion: v1
kind: Service
metadata:
  labels:
    app: web
  name: web
spec:
  ports:
  - name: 80-80
    port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: web
  type: LoadBalancer
status:
  loadBalancer: {}

Applying them: even a "dry run" talks to the cluster

With the saved web.yaml and the unreachable cluster still selected:

user@lab:~$ ls
svc.yaml
web.yaml
user@lab:~$ kubectl apply -f web.yaml --dry-run=client
error: error validating "web.yaml": error validating data: failed to download openapi: Get "https://203.0.113.10:6443/openapi/v2?timeout=32s": dial tcp 203.0.113.10:6443: i/o timeout; if you choose to ignore these errors, turn validation off with --validate=false
user@lab:~$ kubectl apply -f web.yaml --dry-run=client --validate=false
E: "Couldn't get current server API group list" err="Get \"https://203.0.113.10:6443/api?timeout=32s\": dial tcp 203.0.113.10:6443: i/o timeout"
error: unable to recognize "web.yaml": Get "https://203.0.113.10:6443/api?timeout=32s": dial tcp 203.0.113.10:6443: i/o timeout
user@lab:~$ kubectl apply -f web.yaml --dry-run=server
error: error validating "web.yaml": error validating data: failed to download openapi: Get "https://203.0.113.10:6443/openapi/v2?timeout=32s": dial tcp 203.0.113.10:6443: i/o timeout; if you choose to ignore these errors, turn validation off with --validate=false

kubectl apply --dry-run=client still tried to download the cluster's API description (openapi) to validate the file, and failed after the timeout. With --validate=false it then failed on discovery ("unable to recognize"). The lesson: apply needs the cluster, even for a dry run, and --dry-run=server (the more faithful check, which asks the real API server what it would do) needs it even more. To generate files with no cluster, use kubectl create ... --dry-run=client -o yaml as above. Then kubectl apply -f web.yaml creates or updates the objects (from the documentation, not run here), and kubectl get pods and kubectl get svc show them.

Kustomize: one base, many environments

kubectl has Kustomize built in (the version line above says v5.8.1), and it needs no cluster. A base holds the common manifests and an overlay per environment changes only what differs. This ran for real (kz/base/deployment.yaml is the Deployment above):

user@lab:~$ cat kz/prod/kustomization.yaml
resources:
  - ../base
namespace: prod
replicas:
  - name: web
    count: 5
images:
  - name: nginx
    newTag: "1.27.2"
user@lab:~$ kubectl kustomize kz/prod
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: web
  name: web
  namespace: prod
spec:
  replicas: 5
  selector:
    matchLabels:
      app: web
  strategy: {}
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - image: nginx:1.27.2
        name: nginx
        ports:
        - containerPort: 80
        resources: {}
status: {}

The overlay prod/kustomization.yaml says: use the base, put everything in namespace prod, set replicas to 5 and the image tag to 1.27.2. kubectl kustomize kz/prod printed the combined result: the same Deployment with namespace: prod, replicas: 5 and image: nginx:1.27.2. To apply an overlay, kubectl apply -k kz/prod (from the documentation, not run here). It is the simplest, built-in way to avoid copying YAML for dev, test and prod.

Secrets: not secret by default

A Kubernetes Secret holds a password or a key. Making one without a cluster (real):

user@lab:~$ kubectl create secret generic db --from-literal=password=S3cret-demo --dry-run=client -o yaml
apiVersion: v1
data:
  password: UzNjcmV0LWRlbW8=
kind: Secret
metadata:
  name: db
user@lab:~$ echo UzNjcmV0LWRlbW8= | base64 -d; echo
S3cret-demo

The Secret manifest shows password: UzNjcmV0LWRlbW8=, and base64 -d turns it back into S3cret-demo in one command. Base64 is an encoding, not encryption. The Kubernetes documentation (read on 6 October 2026) says it directly: Secrets are stored unencrypted in etcd by default, and base64 "provides no confidentiality". Its advice, in order:

  1. Turn on encryption at rest for Secrets in etcd (on managed services this is a setting or already on: check yours).
  2. Least-privilege RBAC. Whoever can list Secrets can read them, and whoever can run a pod that mounts a Secret can read it too.
  3. Use an external secret store and let the cluster fetch from it (the cloud's own: Key Vault, Secrets Manager, Secret Manager, Vault, as the services maps list), through the Secrets Store CSI driver.
  4. Prefer mounting Secrets as files to passing them as environment variables, and mount them only into the containers that need them.
  5. Never commit a Secret manifest to source control.

Access and identity

What it costs, and how to clean up

kubectl delete -f web.yaml
az group delete --name lab-rg --yes --no-wait
eksctl delete cluster --name lab-eks --region eu-west-1
gcloud container clusters delete lab-gke --region us-central1

(For OKE, delete the cluster and its node pool from the console or CLI, and then the VCN it created. If you used Terraform, terraform destroy removes everything it made.) Check the billing page the next day.

When it does not work

(Those commands are from the documentation, not run here. kubectl describe, logs and get need a cluster.)

Common mistakes

What this page did not cover

Ingress and gateways, persistent storage in depth, autoscaling, Helm, service meshes, upgrades and node pools, network policies, GitOps tools, and running a cluster yourself (kubeadm, k3s). The node access tutorial covers reaching the nodes, and the Kubernetes documentation covers the rest.

The cheat sheet

← Containers on the Cloud: Images, Registries and Serverless Containers · ↑ All tutorials · EC2 From Scratch: A Server You Can Trust →