From b69bf94b1498ca05ac8263519effce76c75d473e Mon Sep 17 00:00:00 2001 From: Kelsey Hightower Date: Tue, 31 Oct 2023 23:16:49 -0700 Subject: [PATCH] Remove cloud provider and move to ARM64 --- .gitignore | 1 + README.md | 28 +- ca.conf | 206 ++++++++ configs/10-bridge.conf | 15 + configs/99-loopback.conf | 5 + configs/containerd-config.toml | 13 + configs/kube-apiserver-to-kubelet.yaml | 33 ++ configs/kube-proxy-config.yaml | 6 + configs/kube-scheduler.yaml | 6 + configs/kubelet-config.yaml | 21 + deployments/coredns-1.7.0.yaml | 180 ------- deployments/kube-dns.yaml | 206 -------- docs/01-prerequisites.md | 67 +-- docs/02-client-tools.md | 118 ----- docs/02-jumpbox.md | 121 +++++ docs/03-compute-resources.md | 378 ++++++++------- docs/04-certificate-authority.md | 440 +++--------------- docs/05-kubernetes-configuration-files.md | 134 +++--- docs/06-data-encryption-keys.md | 27 +- docs/07-bootstrapping-etcd.md | 114 ++--- ...08-bootstrapping-kubernetes-controllers.md | 402 ++++------------ docs/09-bootstrapping-kubernetes-workers.md | 315 ++++--------- docs/10-configuring-kubectl.md | 63 ++- docs/11-pod-network-routes.md | 88 ++-- docs/12-dns-addon.md | 81 ---- docs/{13-smoke-test.md => 12-smoke-test.md} | 158 +++---- docs/13-cleanup.md | 7 + docs/14-cleanup.md | 63 --- docs/images/tmux-screenshot.png | Bin 118848 -> 0 bytes downloads.txt | 11 + units/containerd.service | 19 + units/etcd.service | 22 + units/kube-apiserver.service | 36 ++ units/kube-controller-manager.service | 22 + units/kube-proxy.service | 12 + units/kube-scheduler.service | 13 + units/kubelet.service | 17 + 37 files changed, 1297 insertions(+), 2151 deletions(-) create mode 100644 ca.conf create mode 100644 configs/10-bridge.conf create mode 100644 configs/99-loopback.conf create mode 100644 configs/containerd-config.toml create mode 100644 configs/kube-apiserver-to-kubelet.yaml create mode 100644 configs/kube-proxy-config.yaml create mode 100644 configs/kube-scheduler.yaml create mode 100644 configs/kubelet-config.yaml delete mode 100644 deployments/coredns-1.7.0.yaml delete mode 100644 deployments/kube-dns.yaml delete mode 100644 docs/02-client-tools.md create mode 100644 docs/02-jumpbox.md delete mode 100644 docs/12-dns-addon.md rename docs/{13-smoke-test.md => 12-smoke-test.md} (53%) create mode 100644 docs/13-cleanup.md delete mode 100644 docs/14-cleanup.md delete mode 100644 docs/images/tmux-screenshot.png create mode 100644 downloads.txt create mode 100644 units/containerd.service create mode 100644 units/etcd.service create mode 100644 units/kube-apiserver.service create mode 100644 units/kube-controller-manager.service create mode 100644 units/kube-proxy.service create mode 100644 units/kube-scheduler.service create mode 100644 units/kubelet.service diff --git a/.gitignore b/.gitignore index 8033371..d23374e 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,4 @@ service-account.csr service-account.pem service-account-csr.json *.swp +.idea/ \ No newline at end of file diff --git a/README.md b/README.md index 4043b02..c1e3399 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # Kubernetes The Hard Way -This tutorial walks you through setting up Kubernetes the hard way. This guide is not for people looking for a fully automated command to bring up a Kubernetes cluster. If that's you then check out [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine), or the [Getting Started Guides](https://kubernetes.io/docs/setup). - -Kubernetes The Hard Way is optimized for learning, which means taking the long route to ensure you understand each task required to bootstrap a Kubernetes cluster. +This tutorial walks you through setting up Kubernetes the hard way. This guide is not for someone looking for a fully automated tool to bring up a Kubernetes cluster. Kubernetes The Hard Way is optimized for learning, which means taking the long route to ensure you understand each task required to bootstrap a Kubernetes cluster. > The results of this tutorial should not be viewed as production ready, and may receive limited support from the community, but don't let that stop you from learning! @@ -13,24 +11,25 @@ Kubernetes The Hard Way is optimized for learning, which means taking the long r ## Target Audience -The target audience for this tutorial is someone planning to support a production Kubernetes cluster and wants to understand how everything fits together. +The target audience for this tutorial is someone who wants to understand the fundamentals of Kubernetes and how the core components fit together. ## Cluster Details -Kubernetes The Hard Way guides you through bootstrapping a highly available Kubernetes cluster with end-to-end encryption between components and RBAC authentication. +Kubernetes The Hard Way guides you through bootstrapping a basic Kubernetes cluster with all control plane components running on a single node, and two worker nodes, which is enough to learn the core concepts. -* [kubernetes](https://github.com/kubernetes/kubernetes) v1.21.0 -* [containerd](https://github.com/containerd/containerd) v1.4.4 -* [coredns](https://github.com/coredns/coredns) v1.8.3 -* [cni](https://github.com/containernetworking/cni) v0.9.1 -* [etcd](https://github.com/etcd-io/etcd) v3.4.15 +Component versions: + +* [kubernetes](https://github.com/kubernetes/kubernetes) v1.28.x +* [containerd](https://github.com/containerd/containerd) v1.7.x +* [cni](https://github.com/containernetworking/cni) v1.3.x +* [etcd](https://github.com/etcd-io/etcd) v3.4.x ## Labs -This tutorial assumes you have access to the [Google Cloud Platform](https://cloud.google.com). While GCP is used for basic infrastructure requirements the lessons learned in this tutorial can be applied to other platforms. +This tutorial requires four (4) ARM64 based virtual or physical machines connected to the same network. While ARM64 based machines are used for the tutorial, the lessons learned can be applied to other platforms. * [Prerequisites](docs/01-prerequisites.md) -* [Installing the Client Tools](docs/02-client-tools.md) +* [Setting up the Jumpbox](docs/02-jumpbox.md) * [Provisioning Compute Resources](docs/03-compute-resources.md) * [Provisioning the CA and Generating TLS Certificates](docs/04-certificate-authority.md) * [Generating Kubernetes Configuration Files for Authentication](docs/05-kubernetes-configuration-files.md) @@ -40,6 +39,5 @@ This tutorial assumes you have access to the [Google Cloud Platform](https://clo * [Bootstrapping the Kubernetes Worker Nodes](docs/09-bootstrapping-kubernetes-workers.md) * [Configuring kubectl for Remote Access](docs/10-configuring-kubectl.md) * [Provisioning Pod Network Routes](docs/11-pod-network-routes.md) -* [Deploying the DNS Cluster Add-on](docs/12-dns-addon.md) -* [Smoke Test](docs/13-smoke-test.md) -* [Cleaning Up](docs/14-cleanup.md) +* [Smoke Test](docs/12-smoke-test.md) +* [Cleaning Up](docs/13-cleanup.md) diff --git a/ca.conf b/ca.conf new file mode 100644 index 0000000..eb17657 --- /dev/null +++ b/ca.conf @@ -0,0 +1,206 @@ +[req] +distinguished_name = req_distinguished_name +prompt = no +x509_extensions = ca_x509_extensions + +[ca_x509_extensions] +basicConstraints = CA:TRUE +keyUsage = cRLSign, keyCertSign + +[req_distinguished_name] +C = US +ST = Washington +L = Seattle +CN = CA + +[admin] +distinguished_name = admin_distinguished_name +prompt = no +req_extensions = default_req_extensions + +[admin_distinguished_name] +CN = admin +O = system:masters + +# Service Accounts +# +# The Kubernetes Controller Manager leverages a key pair to generate +# and sign service account tokens as described in the +# [managing service accounts](https://kubernetes.io/docs/admin/service-accounts-admin/) +# documentation. + +[service-accounts] +distinguished_name = service-accounts_distinguished_name +prompt = no +req_extensions = default_req_extensions + +[service-accounts_distinguished_name] +CN = service-accounts + +# Worker Nodes +# +# Kubernetes uses a [special-purpose authorization mode](https://kubernetes.io/docs/admin/authorization/node/) +# called Node Authorizer, that specifically authorizes API requests made +# by [Kubelets](https://kubernetes.io/docs/concepts/overview/components/#kubelet). +# In order to be authorized by the Node Authorizer, Kubelets must use a credential +# that identifies them as being in the `system:nodes` group, with a username +# of `system:node:`. + +[node-0] +distinguished_name = node-0_distinguished_name +prompt = no +req_extensions = node-0_req_extensions + +[node-0_req_extensions] +basicConstraints = CA:FALSE +extendedKeyUsage = clientAuth, serverAuth +keyUsage = critical, digitalSignature, keyEncipherment +nsCertType = client +nsComment = "Node-0 Certificate" +subjectAltName = DNS:node-0, IP:127.0.0.1 +subjectKeyIdentifier = hash + +[node-0_distinguished_name] +CN = system:node:node-0 +O = system:nodes +C = US +ST = Washington +L = Seattle + +[node-1] +distinguished_name = node-1_distinguished_name +prompt = no +req_extensions = node-1_req_extensions + +[node-1_req_extensions] +basicConstraints = CA:FALSE +extendedKeyUsage = clientAuth, serverAuth +keyUsage = critical, digitalSignature, keyEncipherment +nsCertType = client +nsComment = "Node-1 Certificate" +subjectAltName = DNS:node-1, IP:127.0.0.1 +subjectKeyIdentifier = hash + +[node-1_distinguished_name] +CN = system:node:node-1 +O = system:nodes +C = US +ST = Washington +L = Seattle + + +# Kube Proxy Section +[kube-proxy] +distinguished_name = kube-proxy_distinguished_name +prompt = no +req_extensions = kube-proxy_req_extensions + +[kube-proxy_req_extensions] +basicConstraints = CA:FALSE +extendedKeyUsage = clientAuth, serverAuth +keyUsage = critical, digitalSignature, keyEncipherment +nsCertType = client +nsComment = "Kube Proxy Certificate" +subjectAltName = DNS:kube-proxy, IP:127.0.0.1 +subjectKeyIdentifier = hash + +[kube-proxy_distinguished_name] +CN = system:kube-proxy +O = system:node-proxier +C = US +ST = Washington +L = Seattle + + +# Controller Manager +[kube-controller-manager] +distinguished_name = kube-controller-manager_distinguished_name +prompt = no +req_extensions = kube-controller-manager_req_extensions + +[kube-controller-manager_req_extensions] +basicConstraints = CA:FALSE +extendedKeyUsage = clientAuth, serverAuth +keyUsage = critical, digitalSignature, keyEncipherment +nsCertType = client +nsComment = "Kube Controller Manager Certificate" +subjectAltName = DNS:kube-proxy, IP:127.0.0.1 +subjectKeyIdentifier = hash + +[kube-controller-manager_distinguished_name] +CN = system:kube-controller-manager +O = system:kube-controller-manager +C = US +ST = Washington +L = Seattle + + +# Scheduler +[kube-scheduler] +distinguished_name = kube-scheduler_distinguished_name +prompt = no +req_extensions = kube-scheduler_req_extensions + +[kube-scheduler_req_extensions] +basicConstraints = CA:FALSE +extendedKeyUsage = clientAuth, serverAuth +keyUsage = critical, digitalSignature, keyEncipherment +nsCertType = client +nsComment = "Kube Scheduler Certificate" +subjectAltName = DNS:kube-scheduler, IP:127.0.0.1 +subjectKeyIdentifier = hash + +[kube-scheduler_distinguished_name] +CN = system:kube-scheduler +O = system:system:kube-scheduler +C = US +ST = Washington +L = Seattle + + +# API Server +# +# The Kubernetes API server is automatically assigned the `kubernetes` +# internal dns name, which will be linked to the first IP address (`10.32.0.1`) +# from the address range (`10.32.0.0/24`) reserved for internal cluster +# services. + +[kube-api-server] +distinguished_name = kube-api-server_distinguished_name +prompt = no +req_extensions = kube-api-server_req_extensions + +[kube-api-server_req_extensions] +basicConstraints = CA:FALSE +extendedKeyUsage = clientAuth, serverAuth +keyUsage = critical, digitalSignature, keyEncipherment +nsCertType = client +nsComment = "Kube Scheduler Certificate" +subjectAltName = @kube-api-server_alt_names +subjectKeyIdentifier = hash + +[kube-api-server_alt_names] +IP.0 = 127.0.0.1 +IP.1 = 10.32.0.1 +DNS.0 = kubernetes +DNS.1 = kubernetes.default +DNS.2 = kubernetes.default.svc +DNS.3 = kubernetes.default.svc.cluster +DNS.4 = kubernetes.svc.cluster.local +DNS.5 = server.kubernetes.local +DNS.6 = api-server.kubernetes.local + +[kube-api-server_distinguished_name] +CN = kubernetes +C = US +ST = Washington +L = Seattle + + +[default_req_extensions] +basicConstraints = CA:FALSE +extendedKeyUsage = clientAuth +keyUsage = critical, digitalSignature, keyEncipherment +nsCertType = client +nsComment = "Admin Client Certificate" +subjectKeyIdentifier = hash \ No newline at end of file diff --git a/configs/10-bridge.conf b/configs/10-bridge.conf new file mode 100644 index 0000000..e9a3bff --- /dev/null +++ b/configs/10-bridge.conf @@ -0,0 +1,15 @@ +{ + "cniVersion": "1.0.0", + "name": "bridge", + "type": "bridge", + "bridge": "cni0", + "isGateway": true, + "ipMasq": true, + "ipam": { + "type": "host-local", + "ranges": [ + [{"subnet": "SUBNET"}] + ], + "routes": [{"dst": "0.0.0.0/0"}] + } +} \ No newline at end of file diff --git a/configs/99-loopback.conf b/configs/99-loopback.conf new file mode 100644 index 0000000..98d6dc1 --- /dev/null +++ b/configs/99-loopback.conf @@ -0,0 +1,5 @@ +{ + "cniVersion": "1.1.0", + "name": "lo", + "type": "loopback" +} \ No newline at end of file diff --git a/configs/containerd-config.toml b/configs/containerd-config.toml new file mode 100644 index 0000000..954c2e3 --- /dev/null +++ b/configs/containerd-config.toml @@ -0,0 +1,13 @@ +version = 2 + +[plugins."io.containerd.grpc.v1.cri"] + [plugins."io.containerd.grpc.v1.cri".containerd] + snapshotter = "overlayfs" + default_runtime_name = "runc" + [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] + runtime_type = "io.containerd.runc.v2" + [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] + SystemdCgroup = true +[plugins."io.containerd.grpc.v1.cri".cni] + bin_dir = "/opt/cni/bin" + conf_dir = "/etc/cni/net.d" \ No newline at end of file diff --git a/configs/kube-apiserver-to-kubelet.yaml b/configs/kube-apiserver-to-kubelet.yaml new file mode 100644 index 0000000..e56770a --- /dev/null +++ b/configs/kube-apiserver-to-kubelet.yaml @@ -0,0 +1,33 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" + labels: + kubernetes.io/bootstrapping: rbac-defaults + name: system:kube-apiserver-to-kubelet +rules: + - apiGroups: + - "" + resources: + - nodes/proxy + - nodes/stats + - nodes/log + - nodes/spec + - nodes/metrics + verbs: + - "*" +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: system:kube-apiserver + namespace: "" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:kube-apiserver-to-kubelet +subjects: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: kubernetes \ No newline at end of file diff --git a/configs/kube-proxy-config.yaml b/configs/kube-proxy-config.yaml new file mode 100644 index 0000000..7b49fe3 --- /dev/null +++ b/configs/kube-proxy-config.yaml @@ -0,0 +1,6 @@ +kind: KubeProxyConfiguration +apiVersion: kubeproxy.config.k8s.io/v1alpha1 +clientConnection: + kubeconfig: "/var/lib/kube-proxy/kubeconfig" +mode: "iptables" +clusterCIDR: "10.200.0.0/16" \ No newline at end of file diff --git a/configs/kube-scheduler.yaml b/configs/kube-scheduler.yaml new file mode 100644 index 0000000..42c5553 --- /dev/null +++ b/configs/kube-scheduler.yaml @@ -0,0 +1,6 @@ +apiVersion: kubescheduler.config.k8s.io/v1 +kind: KubeSchedulerConfiguration +clientConnection: + kubeconfig: "/var/lib/kubernetes/kube-scheduler.kubeconfig" +leaderElection: + leaderElect: true \ No newline at end of file diff --git a/configs/kubelet-config.yaml b/configs/kubelet-config.yaml new file mode 100644 index 0000000..22d9608 --- /dev/null +++ b/configs/kubelet-config.yaml @@ -0,0 +1,21 @@ +kind: KubeletConfiguration +apiVersion: kubelet.config.k8s.io/v1beta1 +authentication: + anonymous: + enabled: false + webhook: + enabled: true + x509: + clientCAFile: "/var/lib/kubelet/ca.crt" +authorization: + mode: Webhook +clusterDomain: "cluster.local" +clusterDNS: + - "10.32.0.10" +cgroupDriver: systemd +containerRuntimeEndpoint: "unix:///var/run/containerd/containerd.sock" +podCIDR: "SUBNET" +resolvConf: "/etc/resolv.conf" +runtimeRequestTimeout: "15m" +tlsCertFile: "/var/lib/kubelet/kubelet.crt" +tlsPrivateKeyFile: "/var/lib/kubelet/kubelet.key" \ No newline at end of file diff --git a/deployments/coredns-1.7.0.yaml b/deployments/coredns-1.7.0.yaml deleted file mode 100644 index e471d9f..0000000 --- a/deployments/coredns-1.7.0.yaml +++ /dev/null @@ -1,180 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: coredns - namespace: kube-system ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - labels: - kubernetes.io/bootstrapping: rbac-defaults - name: system:coredns -rules: -- apiGroups: - - "" - resources: - - endpoints - - services - - pods - - namespaces - verbs: - - list - - watch -- apiGroups: - - "" - resources: - - nodes - verbs: - - get ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - annotations: - rbac.authorization.kubernetes.io/autoupdate: "true" - labels: - kubernetes.io/bootstrapping: rbac-defaults - name: system:coredns -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: system:coredns -subjects: -- kind: ServiceAccount - name: coredns - namespace: kube-system ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: coredns - namespace: kube-system -data: - Corefile: | - .:53 { - errors - health - ready - kubernetes cluster.local in-addr.arpa ip6.arpa { - pods insecure - fallthrough in-addr.arpa ip6.arpa - } - prometheus :9153 - cache 30 - loop - reload - loadbalance - } ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: coredns - namespace: kube-system - labels: - k8s-app: kube-dns - kubernetes.io/name: "CoreDNS" -spec: - replicas: 2 - strategy: - type: RollingUpdate - rollingUpdate: - maxUnavailable: 1 - selector: - matchLabels: - k8s-app: kube-dns - template: - metadata: - labels: - k8s-app: kube-dns - spec: - priorityClassName: system-cluster-critical - serviceAccountName: coredns - tolerations: - - key: "CriticalAddonsOnly" - operator: "Exists" - nodeSelector: - beta.kubernetes.io/os: linux - containers: - - name: coredns - image: coredns/coredns:1.7.0 - imagePullPolicy: IfNotPresent - resources: - limits: - memory: 170Mi - requests: - cpu: 100m - memory: 70Mi - args: [ "-conf", "/etc/coredns/Corefile" ] - volumeMounts: - - name: config-volume - mountPath: /etc/coredns - readOnly: true - ports: - - containerPort: 53 - name: dns - protocol: UDP - - containerPort: 53 - name: dns-tcp - protocol: TCP - - containerPort: 9153 - name: metrics - protocol: TCP - securityContext: - allowPrivilegeEscalation: false - capabilities: - add: - - NET_BIND_SERVICE - drop: - - all - readOnlyRootFilesystem: true - livenessProbe: - httpGet: - path: /health - port: 8080 - scheme: HTTP - initialDelaySeconds: 60 - timeoutSeconds: 5 - successThreshold: 1 - failureThreshold: 5 - readinessProbe: - httpGet: - path: /ready - port: 8181 - scheme: HTTP - dnsPolicy: Default - volumes: - - name: config-volume - configMap: - name: coredns - items: - - key: Corefile - path: Corefile ---- -apiVersion: v1 -kind: Service -metadata: - name: kube-dns - namespace: kube-system - annotations: - prometheus.io/port: "9153" - prometheus.io/scrape: "true" - labels: - k8s-app: kube-dns - kubernetes.io/cluster-service: "true" - kubernetes.io/name: "CoreDNS" -spec: - selector: - k8s-app: kube-dns - clusterIP: 10.32.0.10 - ports: - - name: dns - port: 53 - protocol: UDP - - name: dns-tcp - port: 53 - protocol: TCP - - name: metrics - port: 9153 - protocol: TCP diff --git a/deployments/kube-dns.yaml b/deployments/kube-dns.yaml deleted file mode 100644 index 5e19117..0000000 --- a/deployments/kube-dns.yaml +++ /dev/null @@ -1,206 +0,0 @@ -# Copyright 2016 The Kubernetes Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -apiVersion: v1 -kind: Service -metadata: - name: kube-dns - namespace: kube-system - labels: - k8s-app: kube-dns - kubernetes.io/cluster-service: "true" - addonmanager.kubernetes.io/mode: Reconcile - kubernetes.io/name: "KubeDNS" -spec: - selector: - k8s-app: kube-dns - clusterIP: 10.32.0.10 - ports: - - name: dns - port: 53 - protocol: UDP - - name: dns-tcp - port: 53 - protocol: TCP ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: kube-dns - namespace: kube-system - labels: - kubernetes.io/cluster-service: "true" - addonmanager.kubernetes.io/mode: Reconcile ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: kube-dns - namespace: kube-system - labels: - addonmanager.kubernetes.io/mode: EnsureExists ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: kube-dns - namespace: kube-system - labels: - k8s-app: kube-dns - kubernetes.io/cluster-service: "true" - addonmanager.kubernetes.io/mode: Reconcile -spec: - # replicas: not specified here: - # 1. In order to make Addon Manager do not reconcile this replicas parameter. - # 2. Default is 1. - # 3. Will be tuned in real time if DNS horizontal auto-scaling is turned on. - strategy: - rollingUpdate: - maxSurge: 10% - maxUnavailable: 0 - selector: - matchLabels: - k8s-app: kube-dns - template: - metadata: - labels: - k8s-app: kube-dns - annotations: - scheduler.alpha.kubernetes.io/critical-pod: '' - spec: - tolerations: - - key: "CriticalAddonsOnly" - operator: "Exists" - volumes: - - name: kube-dns-config - configMap: - name: kube-dns - optional: true - containers: - - name: kubedns - image: gcr.io/google_containers/k8s-dns-kube-dns-amd64:1.14.7 - resources: - # TODO: Set memory limits when we've profiled the container for large - # clusters, then set request = limit to keep this container in - # guaranteed class. Currently, this container falls into the - # "burstable" category so the kubelet doesn't backoff from restarting it. - limits: - memory: 170Mi - requests: - cpu: 100m - memory: 70Mi - livenessProbe: - httpGet: - path: /healthcheck/kubedns - port: 10054 - scheme: HTTP - initialDelaySeconds: 60 - timeoutSeconds: 5 - successThreshold: 1 - failureThreshold: 5 - readinessProbe: - httpGet: - path: /readiness - port: 8081 - scheme: HTTP - # we poll on pod startup for the Kubernetes master service and - # only setup the /readiness HTTP server once that's available. - initialDelaySeconds: 3 - timeoutSeconds: 5 - args: - - --domain=cluster.local. - - --dns-port=10053 - - --config-dir=/kube-dns-config - - --v=2 - env: - - name: PROMETHEUS_PORT - value: "10055" - ports: - - containerPort: 10053 - name: dns-local - protocol: UDP - - containerPort: 10053 - name: dns-tcp-local - protocol: TCP - - containerPort: 10055 - name: metrics - protocol: TCP - volumeMounts: - - name: kube-dns-config - mountPath: /kube-dns-config - - name: dnsmasq - image: gcr.io/google_containers/k8s-dns-dnsmasq-nanny-amd64:1.14.7 - livenessProbe: - httpGet: - path: /healthcheck/dnsmasq - port: 10054 - scheme: HTTP - initialDelaySeconds: 60 - timeoutSeconds: 5 - successThreshold: 1 - failureThreshold: 5 - args: - - -v=2 - - -logtostderr - - -configDir=/etc/k8s/dns/dnsmasq-nanny - - -restartDnsmasq=true - - -- - - -k - - --cache-size=1000 - - --no-negcache - - --log-facility=- - - --server=/cluster.local/127.0.0.1#10053 - - --server=/in-addr.arpa/127.0.0.1#10053 - - --server=/ip6.arpa/127.0.0.1#10053 - ports: - - containerPort: 53 - name: dns - protocol: UDP - - containerPort: 53 - name: dns-tcp - protocol: TCP - # see: https://github.com/kubernetes/kubernetes/issues/29055 for details - resources: - requests: - cpu: 150m - memory: 20Mi - volumeMounts: - - name: kube-dns-config - mountPath: /etc/k8s/dns/dnsmasq-nanny - - name: sidecar - image: gcr.io/google_containers/k8s-dns-sidecar-amd64:1.14.7 - livenessProbe: - httpGet: - path: /metrics - port: 10054 - scheme: HTTP - initialDelaySeconds: 60 - timeoutSeconds: 5 - successThreshold: 1 - failureThreshold: 5 - args: - - --v=2 - - --logtostderr - - --probe=kubedns,127.0.0.1:10053,kubernetes.default.svc.cluster.local,5,SRV - - --probe=dnsmasq,127.0.0.1:53,kubernetes.default.svc.cluster.local,5,SRV - ports: - - containerPort: 10054 - name: metrics - protocol: TCP - resources: - requests: - memory: 20Mi - cpu: 10m - dnsPolicy: Default # Don't use cluster DNS. - serviceAccountName: kube-dns diff --git a/docs/01-prerequisites.md b/docs/01-prerequisites.md index d2474ce..1a781ad 100644 --- a/docs/01-prerequisites.md +++ b/docs/01-prerequisites.md @@ -1,63 +1,30 @@ # Prerequisites -## Google Cloud Platform +In this lab you will review the machine requirements necessary to follow this tutorial. -This tutorial leverages the [Google Cloud Platform](https://cloud.google.com/) to streamline provisioning of the compute infrastructure required to bootstrap a Kubernetes cluster from the ground up. [Sign up](https://cloud.google.com/free/) for $300 in free credits. +## Virtual or Physical Machines -[Estimated cost](https://cloud.google.com/products/calculator#id=873932bc-0840-4176-b0fa-a8cfd4ca61ae) to run this tutorial: $0.23 per hour ($5.50 per day). +This tutorial requires four (4) virtual or physical ARM64 machines running Debian 12 (bookworm). The follow table list the four machines and thier CPU, memory, and storage requirements. -> The compute resources required for this tutorial exceed the Google Cloud Platform free tier. +| Name | Description | CPU | RAM | Storage | +|---------|------------------------|-----|-------|---------| +| jumpbox | Administration host | 1 | 512MB | 10GB | +| server | Kubernetes server | 1 | 2GB | 20GB | +| node-0 | Kubernetes worker node | 1 | 2GB | 20GB | +| node-1 | Kubernetes worker node | 1 | 2GB | 20GB | -## Google Cloud Platform SDK +How you provision the machines is up to you, the only requirement is that each machine meet the above system requirements including the machine specs and OS version. Once you have all four machine provisioned, verify the system requirements by running the `uname` command on each machine: -### Install the Google Cloud SDK - -Follow the Google Cloud SDK [documentation](https://cloud.google.com/sdk/) to install and configure the `gcloud` command line utility. - -Verify the Google Cloud SDK version is 338.0.0 or higher: - -``` -gcloud version +```bash +uname -mov ``` -### Set a Default Compute Region and Zone +After running the `uname` command you should see the following output: -This tutorial assumes a default compute region and zone have been configured. - -If you are using the `gcloud` command-line tool for the first time `init` is the easiest way to do this: - -``` -gcloud init +```text +#1 SMP Debian 6.1.55-1 (2023-09-29) aarch64 GNU/Linux ``` -Then be sure to authorize gcloud to access the Cloud Platform with your Google user credentials: +You maybe surprised to see `aarch64` here, but that is the official name for the Arm Architecture 64-bit instruction set. You will often see `arm64` used by Apple, and the maintainers of the Linux kernel, when referring to support for `aarch64`. This tutorial will use `arm64` consistently throughout to avoid confusion. -``` -gcloud auth login -``` - -Next set a default compute region and compute zone: - -``` -gcloud config set compute/region us-west1 -``` - -Set a default compute zone: - -``` -gcloud config set compute/zone us-west1-c -``` - -> Use the `gcloud compute zones list` command to view additional regions and zones. - -## Running Commands in Parallel with tmux - -[tmux](https://github.com/tmux/tmux/wiki) can be used to run commands on multiple compute instances at the same time. Labs in this tutorial may require running the same commands across multiple compute instances, in those cases consider using tmux and splitting a window into multiple panes with synchronize-panes enabled to speed up the provisioning process. - -> The use of tmux is optional and not required to complete this tutorial. - -![tmux screenshot](images/tmux-screenshot.png) - -> Enable synchronize-panes by pressing `ctrl+b` followed by `shift+:`. Next type `set synchronize-panes on` at the prompt. To disable synchronization: `set synchronize-panes off`. - -Next: [Installing the Client Tools](02-client-tools.md) +Next: [setting-up-the-jumpbox](02-jumpbox.md) diff --git a/docs/02-client-tools.md b/docs/02-client-tools.md deleted file mode 100644 index 94d93be..0000000 --- a/docs/02-client-tools.md +++ /dev/null @@ -1,118 +0,0 @@ -# Installing the Client Tools - -In this lab you will install the command line utilities required to complete this tutorial: [cfssl](https://github.com/cloudflare/cfssl), [cfssljson](https://github.com/cloudflare/cfssl), and [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl). - - -## Install CFSSL - -The `cfssl` and `cfssljson` command line utilities will be used to provision a [PKI Infrastructure](https://en.wikipedia.org/wiki/Public_key_infrastructure) and generate TLS certificates. - -Download and install `cfssl` and `cfssljson`: - -### OS X - -``` -curl -o cfssl https://storage.googleapis.com/kubernetes-the-hard-way/cfssl/1.4.1/darwin/cfssl -curl -o cfssljson https://storage.googleapis.com/kubernetes-the-hard-way/cfssl/1.4.1/darwin/cfssljson -``` - -``` -chmod +x cfssl cfssljson -``` - -``` -sudo mv cfssl cfssljson /usr/local/bin/ -``` - -Some OS X users may experience problems using the pre-built binaries in which case [Homebrew](https://brew.sh) might be a better option: - -``` -brew install cfssl -``` - -### Linux - -``` -wget -q --show-progress --https-only --timestamping \ - https://storage.googleapis.com/kubernetes-the-hard-way/cfssl/1.4.1/linux/cfssl \ - https://storage.googleapis.com/kubernetes-the-hard-way/cfssl/1.4.1/linux/cfssljson -``` - -``` -chmod +x cfssl cfssljson -``` - -``` -sudo mv cfssl cfssljson /usr/local/bin/ -``` - -### Verification - -Verify `cfssl` and `cfssljson` version 1.4.1 or higher is installed: - -``` -cfssl version -``` - -> output - -``` -Version: 1.4.1 -Runtime: go1.12.12 -``` - -``` -cfssljson --version -``` -``` -Version: 1.4.1 -Runtime: go1.12.12 -``` - -## Install kubectl - -The `kubectl` command line utility is used to interact with the Kubernetes API Server. Download and install `kubectl` from the official release binaries: - -### OS X - -``` -curl -o kubectl https://storage.googleapis.com/kubernetes-release/release/v1.21.0/bin/darwin/amd64/kubectl -``` - -``` -chmod +x kubectl -``` - -``` -sudo mv kubectl /usr/local/bin/ -``` - -### Linux - -``` -wget https://storage.googleapis.com/kubernetes-release/release/v1.21.0/bin/linux/amd64/kubectl -``` - -``` -chmod +x kubectl -``` - -``` -sudo mv kubectl /usr/local/bin/ -``` - -### Verification - -Verify `kubectl` version 1.21.0 or higher is installed: - -``` -kubectl version --client -``` - -> output - -``` -Client Version: version.Info{Major:"1", Minor:"21", GitVersion:"v1.21.0", GitCommit:"cb303e613a121a29364f75cc67d3d580833a7479", GitTreeState:"clean", BuildDate:"2021-04-08T16:31:21Z", GoVersion:"go1.16.1", Compiler:"gc", Platform:"linux/amd64"} -``` - -Next: [Provisioning Compute Resources](03-compute-resources.md) diff --git a/docs/02-jumpbox.md b/docs/02-jumpbox.md new file mode 100644 index 0000000..7168252 --- /dev/null +++ b/docs/02-jumpbox.md @@ -0,0 +1,121 @@ +# Set Up The Jumpbox + +In this lab you will set up one of the four machines to be a `jumpbox`. This machine will be used to run commands in this tutorial. While a dedicated machine is being used to ensure consistency, these commands can also be run from just about any machine including your personal workstation running macOS or Linux. + +Think of the `jumpbox` as the administration machine that you will use as a home base when setting up your Kubernetes cluster from the ground up. One thing we need to do before we get started is install a few command line utilities and clone the Kubernetes The Hard Way git repository, which contains some additional configuration files that will be used to configure various Kubernetes components throughout this tutorial. + +Log in to the `jumpbox`: + +```bash +ssh root@jumpbox +``` + +All commands will be run as the `root` user. This is being done for the sake of convenience, and will help reduce the number of commands required to set everything up. + +### Install Command Line Utilities + +Now that you are logged into the `jumpbox` machine as the `root` user, you will install the command line utilities that will be used to preform various tasks throughout the tutorial. + +```bash +apt-get -y install wget curl vim openssl git +``` + +### Sync GitHub Repository + +Now it's time to download a copy of this tutorial which contains the configuration files and templates that will be used build your Kubernetes cluster from the ground up. Clone the Kubernetes The Hard Way git repository using the `git` command: + +```bash +git clone --depth 1 \ + https://github.com/kelseyhightower/kubernetes-the-hard-way.git +``` + +Change into the `kubernetes-the-hard-way` directory: + +```bash +cd kubernetes-the-hard-way +``` + +This will be the working directory for the rest of the tutorial. If you ever get lost run the `pwd` command to verify you are in the right directory when running commands on the `jumpbox`: + +```bash +pwd +``` + +```text +/root/kubernetes-the-hard-way +``` + +### Download Binaries + +In this section you will download the binaries for the various Kubernetes components. The binaries will be stored in the `downloads` directory on the `jumpbox`, which will reduce the amount of internet bandwidth required to complete this tutorial as we avoid downloading the binaries multiple times for each machine in our Kubernetes cluster. + +From the `kubernetes-the-hard-way` directory create a `downloads` directory using the `mkdir` command: + +```bash +mkdir downloads +``` + +The binaries that will be downloaded are listed in the `downloads.txt` file, which you can review using the `cat` command: + +```bash +cat downloads.txt +``` + +Download the binaries listed in the `downloads.txt` file using the `wget` command: + +```bash +wget -q --show-progress \ + --https-only \ + --timestamping \ + -P downloads \ + -i downloads.txt +``` + +Depending on your internet connection speed it may take a while to download the `584` megabytes of binaries, and once the download is complete, you can list them using the `ls` command: + +```bash +ls -loh downloads +``` + +```text +total 584M +-rw-r--r-- 1 root 41M May 9 13:35 cni-plugins-linux-arm64-v1.3.0.tgz +-rw-r--r-- 1 root 34M Oct 26 15:21 containerd-1.7.8-linux-arm64.tar.gz +-rw-r--r-- 1 root 22M Aug 14 00:19 crictl-v1.28.0-linux-arm.tar.gz +-rw-r--r-- 1 root 15M Jul 11 02:30 etcd-v3.4.27-linux-arm64.tar.gz +-rw-r--r-- 1 root 111M Oct 18 07:34 kube-apiserver +-rw-r--r-- 1 root 107M Oct 18 07:34 kube-controller-manager +-rw-r--r-- 1 root 51M Oct 18 07:34 kube-proxy +-rw-r--r-- 1 root 52M Oct 18 07:34 kube-scheduler +-rw-r--r-- 1 root 46M Oct 18 07:34 kubectl +-rw-r--r-- 1 root 101M Oct 18 07:34 kubelet +-rw-r--r-- 1 root 9.6M Aug 10 18:57 runc.arm64 +``` + +### Install kubectl + +In this section you will install the `kubectl`, the official Kubernetes client command line tool, on the `jumpbox` machine. `kubectl will be used to interact with the Kubernetes control once your cluster is provisioned later in this tutorial. + +Use the `chmod` command to make the `kubectl` binary executable and move it to the `/usr/local/bin/` directory: + +```bash +{ + chmod +x downloads/kubectl + cp downloads/kubectl /usr/local/bin/ +} +``` + +At this point `kubectl` is installed and can be verified by running the `kubectl` command: + +```bash +kubectl version --client +``` + +```text +Client Version: v1.28.3 +Kustomize Version: v5.0.4-0.20230601165947-6ce0bf390ce3 +``` + +At this point the `jumpbox` has been set up with all the command line tools and utilities necessary to complete the labs in this tutorial. + +Next: [Provisioning Compute Resources](03-compute-resources.md) diff --git a/docs/03-compute-resources.md b/docs/03-compute-resources.md index a5402bb..207805a 100644 --- a/docs/03-compute-resources.md +++ b/docs/03-compute-resources.md @@ -1,227 +1,225 @@ # Provisioning Compute Resources -Kubernetes requires a set of machines to host the Kubernetes control plane and the worker nodes where containers are ultimately run. In this lab you will provision the compute resources required for running a secure and highly available Kubernetes cluster across a single [compute zone](https://cloud.google.com/compute/docs/regions-zones/regions-zones). +Kubernetes requires a set of machines to host the Kubernetes control plane and the worker nodes where containers are ultimately run. In this lab you will provision the machines required for setting up a Kubernetes cluster. -> Ensure a default compute zone and region have been set as described in the [Prerequisites](01-prerequisites.md#set-a-default-compute-region-and-zone) lab. +## Machine Database -## Networking +This tutorial will leverage a text file, which will serve as a machine database, to store the various machine attributes that will be used when setting up the Kubernetes control plane and worker nodes. The following schema represents entries in the machine database, one entry per line: -The Kubernetes [networking model](https://kubernetes.io/docs/concepts/cluster-administration/networking/#kubernetes-model) assumes a flat network in which containers and nodes can communicate with each other. In cases where this is not desired [network policies](https://kubernetes.io/docs/concepts/services-networking/network-policies/) can limit how groups of containers are allowed to communicate with each other and external network endpoints. - -> Setting up network policies is out of scope for this tutorial. - -### Virtual Private Cloud Network - -In this section a dedicated [Virtual Private Cloud](https://cloud.google.com/compute/docs/networks-and-firewalls#networks) (VPC) network will be setup to host the Kubernetes cluster. - -Create the `kubernetes-the-hard-way` custom VPC network: - -``` -gcloud compute networks create kubernetes-the-hard-way --subnet-mode custom +```text +IPV4_ADDRESS FQDN HOSTNAME POD_SUBNET ``` -A [subnet](https://cloud.google.com/compute/docs/vpc/#vpc_networks_and_subnets) must be provisioned with an IP address range large enough to assign a private IP address to each node in the Kubernetes cluster. +Each of the columns corresponds to a machine IP address `IPV4_ADDRESS`, fully qualified domain name `FQDN`, host name `HOSTNAME`, and the IP subnet `POD_SUBNET`. Kubernetes assigns one IP address per `pod` and the `POD_SUBNET` represents the unique IP address range assigned to each machine in the cluster for doing so. -Create the `kubernetes` subnet in the `kubernetes-the-hard-way` VPC network: +Here is an example machine database similar to the one used when creating this tutorial. Notice the IP addresses have been masked out. Your machines can be assigned any IP address as long as each machine is reachable from each other and the `jumpbox`. -``` -gcloud compute networks subnets create kubernetes \ - --network kubernetes-the-hard-way \ - --range 10.240.0.0/24 +```bash +cat machines.txt ``` -> The `10.240.0.0/24` IP address range can host up to 254 compute instances. - -### Firewall Rules - -Create a firewall rule that allows internal communication across all protocols: - -``` -gcloud compute firewall-rules create kubernetes-the-hard-way-allow-internal \ - --allow tcp,udp,icmp \ - --network kubernetes-the-hard-way \ - --source-ranges 10.240.0.0/24,10.200.0.0/16 +```text +XXX.XXX.XXX.XXX server.kubernetes.local server +XXX.XXX.XXX.XXX node-0.kubernetes.local node-0 10.200.0.0/24 +XXX.XXX.XXX.XXX node-1.kubernetes.local node-1 10.200.1.0/24 ``` -Create a firewall rule that allows external SSH, ICMP, and HTTPS: - -``` -gcloud compute firewall-rules create kubernetes-the-hard-way-allow-external \ - --allow tcp:22,tcp:6443,icmp \ - --network kubernetes-the-hard-way \ - --source-ranges 0.0.0.0/0 -``` - -> An [external load balancer](https://cloud.google.com/compute/docs/load-balancing/network/) will be used to expose the Kubernetes API Servers to remote clients. - -List the firewall rules in the `kubernetes-the-hard-way` VPC network: - -``` -gcloud compute firewall-rules list --filter="network:kubernetes-the-hard-way" -``` - -> output - -``` -NAME NETWORK DIRECTION PRIORITY ALLOW DENY DISABLED -kubernetes-the-hard-way-allow-external kubernetes-the-hard-way INGRESS 1000 tcp:22,tcp:6443,icmp False -kubernetes-the-hard-way-allow-internal kubernetes-the-hard-way INGRESS 1000 tcp,udp,icmp Fals -``` - -### Kubernetes Public IP Address - -Allocate a static IP address that will be attached to the external load balancer fronting the Kubernetes API Servers: - -``` -gcloud compute addresses create kubernetes-the-hard-way \ - --region $(gcloud config get-value compute/region) -``` - -Verify the `kubernetes-the-hard-way` static IP address was created in your default compute region: - -``` -gcloud compute addresses list --filter="name=('kubernetes-the-hard-way')" -``` - -> output - -``` -NAME ADDRESS/RANGE TYPE PURPOSE NETWORK REGION SUBNET STATUS -kubernetes-the-hard-way XX.XXX.XXX.XXX EXTERNAL us-west1 RESERVED -``` - -## Compute Instances - -The compute instances in this lab will be provisioned using [Ubuntu Server](https://www.ubuntu.com/server) 20.04, which has good support for the [containerd container runtime](https://github.com/containerd/containerd). Each compute instance will be provisioned with a fixed private IP address to simplify the Kubernetes bootstrapping process. - -### Kubernetes Controllers - -Create three compute instances which will host the Kubernetes control plane: - -``` -for i in 0 1 2; do - gcloud compute instances create controller-${i} \ - --async \ - --boot-disk-size 200GB \ - --can-ip-forward \ - --image-family ubuntu-2004-lts \ - --image-project ubuntu-os-cloud \ - --machine-type e2-standard-2 \ - --private-network-ip 10.240.0.1${i} \ - --scopes compute-rw,storage-ro,service-management,service-control,logging-write,monitoring \ - --subnet kubernetes \ - --tags kubernetes-the-hard-way,controller -done -``` - -### Kubernetes Workers - -Each worker instance requires a pod subnet allocation from the Kubernetes cluster CIDR range. The pod subnet allocation will be used to configure container networking in a later exercise. The `pod-cidr` instance metadata will be used to expose pod subnet allocations to compute instances at runtime. - -> The Kubernetes cluster CIDR range is defined by the Controller Manager's `--cluster-cidr` flag. In this tutorial the cluster CIDR range will be set to `10.200.0.0/16`, which supports 254 subnets. - -Create three compute instances which will host the Kubernetes worker nodes: - -``` -for i in 0 1 2; do - gcloud compute instances create worker-${i} \ - --async \ - --boot-disk-size 200GB \ - --can-ip-forward \ - --image-family ubuntu-2004-lts \ - --image-project ubuntu-os-cloud \ - --machine-type e2-standard-2 \ - --metadata pod-cidr=10.200.${i}.0/24 \ - --private-network-ip 10.240.0.2${i} \ - --scopes compute-rw,storage-ro,service-management,service-control,logging-write,monitoring \ - --subnet kubernetes \ - --tags kubernetes-the-hard-way,worker -done -``` - -### Verification - -List the compute instances in your default compute zone: - -``` -gcloud compute instances list --filter="tags.items=kubernetes-the-hard-way" -``` - -> output - -``` -NAME ZONE MACHINE_TYPE PREEMPTIBLE INTERNAL_IP EXTERNAL_IP STATUS -controller-0 us-west1-c e2-standard-2 10.240.0.10 XX.XX.XX.XXX RUNNING -controller-1 us-west1-c e2-standard-2 10.240.0.11 XX.XXX.XXX.XX RUNNING -controller-2 us-west1-c e2-standard-2 10.240.0.12 XX.XXX.XX.XXX RUNNING -worker-0 us-west1-c e2-standard-2 10.240.0.20 XX.XX.XXX.XXX RUNNING -worker-1 us-west1-c e2-standard-2 10.240.0.21 XX.XX.XX.XXX RUNNING -worker-2 us-west1-c e2-standard-2 10.240.0.22 XX.XXX.XX.XX RUNNING -``` +Now it's your turn to create a `machines.txt` file with the details for the three machines you will be using to create your Kubernetes cluster. Use the example machine database from above and add the details for your machines. ## Configuring SSH Access -SSH will be used to configure the controller and worker instances. When connecting to compute instances for the first time SSH keys will be generated for you and stored in the project or instance metadata as described in the [connecting to instances](https://cloud.google.com/compute/docs/instances/connecting-to-instance) documentation. +SSH will be used to configure the machines in the cluster. Verify that you have `root` SSH access to each machine listed in your machine database. You may need to enable root SSH access on each node by updating the sshd_config file and restarting the SSH server. -Test SSH access to the `controller-0` compute instances: +### Enable root SSH Access -``` -gcloud compute ssh controller-0 +If `root` SSH access is enabled for each of your machines you can skip this section. + +By default, a new `debian` install disables SSH access for the `root` user. This is done for security reasons as the `root` user is a well known user on Linux systems, and if a weak password is used on a machine connected to the internet, well, let's just say it's only a matter of time before your machine belongs to someone else. As mention earlier, we are going to enable `root` access over SSH in order to streamline the steps in this tutorial. Security is a tradeoff, and in this case, we are optimizing for convenience. On each machine login via SSH using your user account, then switch to the `root` user using the `su` command: + +```bash +su - root ``` -If this is your first time connecting to a compute instance SSH keys will be generated for you. Enter a passphrase at the prompt to continue: +Edit the `/etc/ssh/sshd_config` SSH daemon configuration file and the `PermitRootLogin` option to `yes`: +```bash +sed -i \ + 's/^#PermitRootLogin.*/PermitRootLogin yes/' \ + /etc/ssh/sshd_config ``` -WARNING: The public SSH key file for gcloud does not exist. -WARNING: The private SSH key file for gcloud does not exist. -WARNING: You do not have an SSH key for gcloud. -WARNING: SSH keygen will be executed to generate a key. + +Restart the `sshd` SSH server to pick up the updated configuration file: + +```bash +systemctl restart sshd +``` + +### Generate and Distribute SSH Keys + +In this section you will generate and distribute an SSH keypair to the `server`, `node-0`, and `node-1`, machines, which will be used to run commands on those machines throughout this tutorial. Run the following commands from the `jumpbox` machine. + +Generate a new SSH key: + +```bash +ssh-keygen +``` + +```text Generating public/private rsa key pair. -Enter passphrase (empty for no passphrase): -Enter same passphrase again: +Enter file in which to save the key (/root/.ssh/id_rsa): +Enter passphrase (empty for no passphrase): +Enter same passphrase again: +Your identification has been saved in /root/.ssh/id_rsa +Your public key has been saved in /root/.ssh/id_rsa.pub ``` -At this point the generated SSH keys will be uploaded and stored in your project: +Copy the SSH public key to each machine: -``` -Your identification has been saved in /home/$USER/.ssh/google_compute_engine. -Your public key has been saved in /home/$USER/.ssh/google_compute_engine.pub. -The key fingerprint is: -SHA256:nz1i8jHmgQuGt+WscqP5SeIaSy5wyIJeL71MuV+QruE $USER@$HOSTNAME -The key's randomart image is: -+---[RSA 2048]----+ -| | -| | -| | -| . | -|o. oS | -|=... .o .o o | -|+.+ =+=.+.X o | -|.+ ==O*B.B = . | -| .+.=EB++ o | -+----[SHA256]-----+ -Updating project ssh metadata...-Updated [https://www.googleapis.com/compute/v1/projects/$PROJECT_ID]. -Updating project ssh metadata...done. -Waiting for SSH key to propagate. +```bash +while read IP FQDN HOST SUBNET; do + ssh-copy-id root@${IP} +done < machines.txt ``` -After the SSH keys have been updated you'll be logged into the `controller-0` instance: +Once each key is added, verify SSH public key access is working: -``` -Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-1042-gcp x86_64) -... +```bash +while read IP FQDN HOST SUBNET; do + ssh -n root@${IP} uname -o -m +done < machines.txt ``` -Type `exit` at the prompt to exit the `controller-0` compute instance: +```text +aarch64 GNU/Linux +aarch64 GNU/Linux +aarch64 GNU/Linux +``` -``` -$USER@controller-0:~$ exit -``` -> output +## Hostnames +In this section you will assign hostnames to the `server`, `node-0`, and `node-1` machines. The hostname will be used when executing commands from the `jumpbox` to each machine. The hostname also play a major role within the cluster. Instead of Kubernetes clients using an IP address to issue commands to the Kubernetes API server, those client will use the `server` hostname instead. Hostnames are also used by each worker machine, `node-0` and `node-1` when registering with a given Kubernetes cluster. + +To configure the hostname for each machine, run the following commands on the `jumpbox`. + +Set the hostname on each machine listed in the `machines.txt` file: + +```bash +while read IP FQDN HOST SUBNET; do + CMD="sed -i 's/^127.0.1.1.*/127.0.1.1\t${FQDN} ${HOST}/' /etc/hosts" + ssh -n root@${IP} "$CMD" + ssh -n root@${IP} hostnamectl hostname ${HOST} +done < machines.txt ``` -logout -Connection to XX.XX.XX.XXX closed + +Verify the hostname is set on each machine: + +```bash +while read IP FQDN HOST SUBNET; do + ssh -n root@${IP} hostname --fqdn +done < machines.txt ``` +```text +server.kubernetes.local +node-0.kubernetes.local +node-1.kubernetes.local +``` + +## DNS + +In this section you will generate a DNS `hosts` file which will be appended to `jumpbox` local `/etc/hosts` file and to the `/etc/hosts` file of all three machines used for this tutorial. This will allow each machine to be reachable using a hostname such as `server`, `node-0`, or `node-1`. + +Create a new `hosts` file and add a header to identify the machines being added: + +```bash +echo "" > hosts +echo "# Kubernetes The Hard Way" >> hosts +``` + +Generate a DNS entry for each machine in the `machines.txt` file and append it to the `hosts` file: + +```bash +while read IP FQDN HOST SUBNET; do + ENTRY="${IP} ${FQDN} ${HOST}" + echo $ENTRY >> hosts +done < machines.txt +``` + +Review the DNS entries in the `hosts` file: + +```bash +cat hosts +``` + +```text + +# Kubernetes The Hard Way +XXX.XXX.XXX.XXX server.kubernetes.local server +XXX.XXX.XXX.XXX node-0.kubernetes.local node-0 +XXX.XXX.XXX.XXX node-1.kubernetes.local node-1 +``` + +## Adding DNS Entries To A Local Machine + +In this section you will append the DNS entries from the `hosts` file to the local `/etc/hosts` file on your `jumpbox` machine. + +Append the DNS entries from `hosts` to `/etc/hosts`: + +```bash +cat hosts >> /etc/hosts +``` + +Verify that the `/etc/hosts` file has been updated: + +```bash +cat /etc/hosts +``` + +```text +127.0.0.1 localhost +127.0.1.1 jumpbox + +# The following lines are desirable for IPv6 capable hosts +::1 localhost ip6-localhost ip6-loopback +ff02::1 ip6-allnodes +ff02::2 ip6-allrouters + + + +# Kubernetes The Hard Way +XXX.XXX.XXX.XXX server.kubernetes.local server +XXX.XXX.XXX.XXX node-0.kubernetes.local node-0 +XXX.XXX.XXX.XXX node-1.kubernetes.local node-1 +``` + +At this point you should be able to SSH to each machine listed in the `machines.txt` file using a hostname. + +```bash +for host in server node-0 node-1 + do ssh root@${host} uname -o -m -n +done +``` + +```text +server aarch64 GNU/Linux +node-0 aarch64 GNU/Linux +node-1 aarch64 GNU/Linux +``` + +## Adding DNS Entries To The Remote Machines + +In this section you will append the DNS entries from `hosts` to `/etc/hosts` on each machine listed in the `machines.txt` text file. + +Copy the `hosts` file to each machine and append the contents to `/etc/hosts`: + +```bash +while read IP FQDN HOST SUBNET; do + scp hosts root@${HOST}:~/ + ssh -n \ + root@${HOST} "cat hosts >> /etc/hosts" +done < machines.txt +``` + +At this point hostnames can be used when connecting to machines from your `jumpbox` machine, or any of the three machines in the Kubernetes cluster. Instead of using IP addresess you can now connect to machines using a hostname such as `server`, `node-0`, or `node-1`. + Next: [Provisioning a CA and Generating TLS Certificates](04-certificate-authority.md) diff --git a/docs/04-certificate-authority.md b/docs/04-certificate-authority.md index 1510993..19932cd 100644 --- a/docs/04-certificate-authority.md +++ b/docs/04-certificate-authority.md @@ -1,412 +1,106 @@ # Provisioning a CA and Generating TLS Certificates -In this lab you will provision a [PKI Infrastructure](https://en.wikipedia.org/wiki/Public_key_infrastructure) using CloudFlare's PKI toolkit, [cfssl](https://github.com/cloudflare/cfssl), then use it to bootstrap a Certificate Authority, and generate TLS certificates for the following components: etcd, kube-apiserver, kube-controller-manager, kube-scheduler, kubelet, and kube-proxy. +In this lab you will provision a [PKI Infrastructure](https://en.wikipedia.org/wiki/Public_key_infrastructure) using openssl to bootstrap a Certificate Authority, and generate TLS certificates for the following components: kube-apiserver, kube-controller-manager, kube-scheduler, kubelet, and kube-proxy. The commands in this section should be run from the `jumpbox`. ## Certificate Authority -In this section you will provision a Certificate Authority that can be used to generate additional TLS certificates. +In this section you will provision a Certificate Authority that can be used to generate additional TLS certificates for the other Kubernetes components. Setting up CA and generating certificates using `openssl` can be time-consuming, especially when doing it for the first time. To streamline this lab, I've included an openssl configuration file `ca.conf`, which defines all the details needed to generate certificates for each Kubernetes component. + +Take a moment to review the `ca.conf` configuration file: + +```bash +cat ca.conf +``` + +You don't need to understand everything in the `ca.conf` file to complete this tutorial, but you should consider it a starting point for learning `openssl` and the configuration that goes into managing certificates at a high level. + +Every certificate authority starts with a private key and root certificate. In this section we are going to create a self-signed certificate authority, and while that's all we need for this tutorial, this shouldn't be considered something you would do in a real-world production level environment. Generate the CA configuration file, certificate, and private key: -``` +```bash { - -cat > ca-config.json < ca-csr.json < admin-csr.json <`. In this section you will create a certificate for each Kubernetes worker node that meets the Node Authorizer requirements. - -Generate a certificate and private key for each Kubernetes worker node: - -``` -for instance in worker-0 worker-1 worker-2; do -cat > ${instance}-csr.json < kube-controller-manager-csr.json < kube-proxy-csr.json < kube-scheduler-csr.json < kubernetes-csr.json < The Kubernetes API server is automatically assigned the `kubernetes` internal dns name, which will be linked to the first IP address (`10.32.0.1`) from the address range (`10.32.0.0/24`) reserved for internal cluster services during the [control plane bootstrapping](08-bootstrapping-kubernetes-controllers.md#configure-the-kubernetes-api-server) lab. - -Results: - -``` -kubernetes-key.pem -kubernetes.pem -``` - -## The Service Account Key Pair - -The Kubernetes Controller Manager leverages a key pair to generate and sign service account tokens as described in the [managing service accounts](https://kubernetes.io/docs/admin/service-accounts-admin/) documentation. - -Generate the `service-account` certificate and private key: - -``` -{ - -cat > service-account-csr.json < The `kube-proxy`, `kube-controller-manager`, `kube-scheduler`, and `kubelet` client certificates will be used to generate client authentication configuration files in the next lab. diff --git a/docs/05-kubernetes-configuration-files.md b/docs/05-kubernetes-configuration-files.md index 7275597..4169ce5 100644 --- a/docs/05-kubernetes-configuration-files.md +++ b/docs/05-kubernetes-configuration-files.md @@ -4,19 +4,7 @@ In this lab you will generate [Kubernetes configuration files](https://kubernete ## Client Authentication Configs -In this section you will generate kubeconfig files for the `controller manager`, `kubelet`, `kube-proxy`, and `scheduler` clients and the `admin` user. - -### Kubernetes Public IP Address - -Each kubeconfig requires a Kubernetes API Server to connect to. To support high availability the IP address assigned to the external load balancer fronting the Kubernetes API Servers will be used. - -Retrieve the `kubernetes-the-hard-way` static IP address: - -``` -KUBERNETES_PUBLIC_ADDRESS=$(gcloud compute addresses describe kubernetes-the-hard-way \ - --region $(gcloud config get-value compute/region) \ - --format 'value(address)') -``` +In this section you will generate kubeconfig files for the `kubelet` and the `admin` user. ### The kubelet Kubernetes Configuration File @@ -24,54 +12,54 @@ When generating kubeconfig files for Kubelets the client certificate matching th > The following commands must be run in the same directory used to generate the SSL certificates during the [Generating TLS Certificates](04-certificate-authority.md) lab. -Generate a kubeconfig file for each worker node: +Generate a kubeconfig file the node-0 worker node: -``` -for instance in worker-0 worker-1 worker-2; do +```bash +for host in node-0 node-1; do kubectl config set-cluster kubernetes-the-hard-way \ - --certificate-authority=ca.pem \ + --certificate-authority=ca.crt \ --embed-certs=true \ - --server=https://${KUBERNETES_PUBLIC_ADDRESS}:6443 \ - --kubeconfig=${instance}.kubeconfig + --server=https://server.kubernetes.local:6443 \ + --kubeconfig=${host}.kubeconfig - kubectl config set-credentials system:node:${instance} \ - --client-certificate=${instance}.pem \ - --client-key=${instance}-key.pem \ + kubectl config set-credentials system:node:${host} \ + --client-certificate=${host}.crt \ + --client-key=${host}.key \ --embed-certs=true \ - --kubeconfig=${instance}.kubeconfig + --kubeconfig=${host}.kubeconfig kubectl config set-context default \ --cluster=kubernetes-the-hard-way \ - --user=system:node:${instance} \ - --kubeconfig=${instance}.kubeconfig + --user=system:node:${host} \ + --kubeconfig=${host}.kubeconfig - kubectl config use-context default --kubeconfig=${instance}.kubeconfig + kubectl config use-context default \ + --kubeconfig=${host}.kubeconfig done ``` Results: -``` -worker-0.kubeconfig -worker-1.kubeconfig -worker-2.kubeconfig +```text +node-0.kubeconfig +node-1.kubeconfig ``` ### The kube-proxy Kubernetes Configuration File Generate a kubeconfig file for the `kube-proxy` service: -``` +```bash { kubectl config set-cluster kubernetes-the-hard-way \ - --certificate-authority=ca.pem \ + --certificate-authority=ca.crt \ --embed-certs=true \ - --server=https://${KUBERNETES_PUBLIC_ADDRESS}:6443 \ + --server=https://server.kubernetes.local:6443 \ --kubeconfig=kube-proxy.kubeconfig kubectl config set-credentials system:kube-proxy \ - --client-certificate=kube-proxy.pem \ - --client-key=kube-proxy-key.pem \ + --client-certificate=kube-proxy.crt \ + --client-key=kube-proxy.key \ --embed-certs=true \ --kubeconfig=kube-proxy.kubeconfig @@ -80,13 +68,14 @@ Generate a kubeconfig file for the `kube-proxy` service: --user=system:kube-proxy \ --kubeconfig=kube-proxy.kubeconfig - kubectl config use-context default --kubeconfig=kube-proxy.kubeconfig + kubectl config use-context default \ + --kubeconfig=kube-proxy.kubeconfig } ``` Results: -``` +```text kube-proxy.kubeconfig ``` @@ -94,17 +83,17 @@ kube-proxy.kubeconfig Generate a kubeconfig file for the `kube-controller-manager` service: -``` +```bash { kubectl config set-cluster kubernetes-the-hard-way \ - --certificate-authority=ca.pem \ + --certificate-authority=ca.crt \ --embed-certs=true \ - --server=https://127.0.0.1:6443 \ + --server=https://server.kubernetes.local:6443 \ --kubeconfig=kube-controller-manager.kubeconfig kubectl config set-credentials system:kube-controller-manager \ - --client-certificate=kube-controller-manager.pem \ - --client-key=kube-controller-manager-key.pem \ + --client-certificate=kube-controller-manager.crt \ + --client-key=kube-controller-manager.key \ --embed-certs=true \ --kubeconfig=kube-controller-manager.kubeconfig @@ -113,13 +102,14 @@ Generate a kubeconfig file for the `kube-controller-manager` service: --user=system:kube-controller-manager \ --kubeconfig=kube-controller-manager.kubeconfig - kubectl config use-context default --kubeconfig=kube-controller-manager.kubeconfig + kubectl config use-context default \ + --kubeconfig=kube-controller-manager.kubeconfig } ``` Results: -``` +```text kube-controller-manager.kubeconfig ``` @@ -128,17 +118,17 @@ kube-controller-manager.kubeconfig Generate a kubeconfig file for the `kube-scheduler` service: -``` +```bash { kubectl config set-cluster kubernetes-the-hard-way \ - --certificate-authority=ca.pem \ + --certificate-authority=ca.crt \ --embed-certs=true \ - --server=https://127.0.0.1:6443 \ + --server=https://server.kubernetes.local:6443 \ --kubeconfig=kube-scheduler.kubeconfig kubectl config set-credentials system:kube-scheduler \ - --client-certificate=kube-scheduler.pem \ - --client-key=kube-scheduler-key.pem \ + --client-certificate=kube-scheduler.crt \ + --client-key=kube-scheduler.key \ --embed-certs=true \ --kubeconfig=kube-scheduler.kubeconfig @@ -147,13 +137,14 @@ Generate a kubeconfig file for the `kube-scheduler` service: --user=system:kube-scheduler \ --kubeconfig=kube-scheduler.kubeconfig - kubectl config use-context default --kubeconfig=kube-scheduler.kubeconfig + kubectl config use-context default \ + --kubeconfig=kube-scheduler.kubeconfig } ``` Results: -``` +```text kube-scheduler.kubeconfig ``` @@ -161,17 +152,17 @@ kube-scheduler.kubeconfig Generate a kubeconfig file for the `admin` user: -``` +```bash { kubectl config set-cluster kubernetes-the-hard-way \ - --certificate-authority=ca.pem \ + --certificate-authority=ca.crt \ --embed-certs=true \ --server=https://127.0.0.1:6443 \ --kubeconfig=admin.kubeconfig kubectl config set-credentials admin \ - --client-certificate=admin.pem \ - --client-key=admin-key.pem \ + --client-certificate=admin.crt \ + --client-key=admin.key \ --embed-certs=true \ --kubeconfig=admin.kubeconfig @@ -180,35 +171,40 @@ Generate a kubeconfig file for the `admin` user: --user=admin \ --kubeconfig=admin.kubeconfig - kubectl config use-context default --kubeconfig=admin.kubeconfig + kubectl config use-context default \ + --kubeconfig=admin.kubeconfig } ``` Results: -``` +```text admin.kubeconfig ``` - -## - ## Distribute the Kubernetes Configuration Files -Copy the appropriate `kubelet` and `kube-proxy` kubeconfig files to each worker instance: +Copy the `kubelet` and `kube-proxy` kubeconfig files to the node-0 instance: -``` -for instance in worker-0 worker-1 worker-2; do - gcloud compute scp ${instance}.kubeconfig kube-proxy.kubeconfig ${instance}:~/ +```bash +for host in node-0 node-1; do + ssh root@$host "mkdir /var/lib/{kube-proxy,kubelet}" + + scp kube-proxy.kubeconfig \ + root@$host:/var/lib/kube-proxy/kubeconfig \ + + scp ${host}.kubeconfig \ + root@$host:/var/lib/kubelet/kubeconfig done ``` -Copy the appropriate `kube-controller-manager` and `kube-scheduler` kubeconfig files to each controller instance: +Copy the `kube-controller-manager` and `kube-scheduler` kubeconfig files to the controller instance: -``` -for instance in controller-0 controller-1 controller-2; do - gcloud compute scp admin.kubeconfig kube-controller-manager.kubeconfig kube-scheduler.kubeconfig ${instance}:~/ -done +```bash +scp admin.kubeconfig \ + kube-controller-manager.kubeconfig \ + kube-scheduler.kubeconfig \ + root@server:~/ ``` Next: [Generating the Data Encryption Config and Key](06-data-encryption-keys.md) diff --git a/docs/06-data-encryption-keys.md b/docs/06-data-encryption-keys.md index 233bce2..be613a0 100644 --- a/docs/06-data-encryption-keys.md +++ b/docs/06-data-encryption-keys.md @@ -8,36 +8,23 @@ In this lab you will generate an encryption key and an [encryption config](https Generate an encryption key: -``` -ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64) +```bash +export ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64) ``` ## The Encryption Config File Create the `encryption-config.yaml` encryption config file: -``` -cat > encryption-config.yaml < encryption-config.yaml ``` Copy the `encryption-config.yaml` encryption config file to each controller instance: -``` -for instance in controller-0 controller-1 controller-2; do - gcloud compute scp encryption-config.yaml ${instance}:~/ -done +```bash +scp encryption-config.yaml root@server:~/ ``` Next: [Bootstrapping the etcd Cluster](07-bootstrapping-etcd.md) diff --git a/docs/07-bootstrapping-etcd.md b/docs/07-bootstrapping-etcd.md index a9ad937..574d901 100644 --- a/docs/07-bootstrapping-etcd.md +++ b/docs/07-bootstrapping-etcd.md @@ -4,125 +4,73 @@ Kubernetes components are stateless and store cluster state in [etcd](https://gi ## Prerequisites -The commands in this lab must be run on each controller instance: `controller-0`, `controller-1`, and `controller-2`. Login to each controller instance using the `gcloud` command. Example: +Copy `etcd` binaries and systemd unit files to the `server` instance: -``` -gcloud compute ssh controller-0 +```bash +scp \ + downloads/etcd-v3.4.27-linux-arm64.tar.gz \ + units/etcd.service \ + root@server:~/ ``` -### Running commands in parallel with tmux - -[tmux](https://github.com/tmux/tmux/wiki) can be used to run commands on multiple compute instances at the same time. See the [Running commands in parallel with tmux](01-prerequisites.md#running-commands-in-parallel-with-tmux) section in the Prerequisites lab. - -## Bootstrapping an etcd Cluster Member - -### Download and Install the etcd Binaries - -Download the official etcd release binaries from the [etcd](https://github.com/etcd-io/etcd) GitHub project: +The commands in this lab must be run on the `server` machine. Login to the `server` machine using the `ssh` command. Example: +```bash +ssh root@server ``` -wget -q --show-progress --https-only --timestamping \ - "https://github.com/etcd-io/etcd/releases/download/v3.4.15/etcd-v3.4.15-linux-amd64.tar.gz" -``` + +## Bootstrapping an etcd Cluster + +### Install the etcd Binaries Extract and install the `etcd` server and the `etcdctl` command line utility: -``` +```bash { - tar -xvf etcd-v3.4.15-linux-amd64.tar.gz - sudo mv etcd-v3.4.15-linux-amd64/etcd* /usr/local/bin/ + tar -xvf etcd-v3.4.27-linux-arm64.tar.gz + mv etcd-v3.4.27-linux-arm64/etcd* /usr/local/bin/ } ``` ### Configure the etcd Server -``` +```bash { - sudo mkdir -p /etc/etcd /var/lib/etcd - sudo chmod 700 /var/lib/etcd - sudo cp ca.pem kubernetes-key.pem kubernetes.pem /etc/etcd/ + mkdir -p /etc/etcd /var/lib/etcd + chmod 700 /var/lib/etcd + cp ca.crt kube-api-server.key kube-api-server.crt \ + /etc/etcd/ } ``` -The instance internal IP address will be used to serve client requests and communicate with etcd cluster peers. Retrieve the internal IP address for the current compute instance: - -``` -INTERNAL_IP=$(curl -s -H "Metadata-Flavor: Google" \ - http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/ip) -``` - Each etcd member must have a unique name within an etcd cluster. Set the etcd name to match the hostname of the current compute instance: -``` -ETCD_NAME=$(hostname -s) -``` - Create the `etcd.service` systemd unit file: -``` -cat < Remember to run the above commands on each controller node: `controller-0`, `controller-1`, and `controller-2`. - ## Verification List the etcd cluster members: -``` -sudo ETCDCTL_API=3 etcdctl member list \ - --endpoints=https://127.0.0.1:2379 \ - --cacert=/etc/etcd/ca.pem \ - --cert=/etc/etcd/kubernetes.pem \ - --key=/etc/etcd/kubernetes-key.pem +```bash +etcdctl member list ``` -> output - -``` -3a57933972cb5131, started, controller-2, https://10.240.0.12:2380, https://10.240.0.12:2379, false -f98dc20bce6225a0, started, controller-0, https://10.240.0.10:2380, https://10.240.0.10:2379, false -ffed16798470cab5, started, controller-1, https://10.240.0.11:2380, https://10.240.0.11:2379, false +```text +6702b0a34e2cfd39, started, controller, http://127.0.0.1:2380, http://127.0.0.1:2379, false ``` Next: [Bootstrapping the Kubernetes Control Plane](08-bootstrapping-kubernetes-controllers.md) diff --git a/docs/08-bootstrapping-kubernetes-controllers.md b/docs/08-bootstrapping-kubernetes-controllers.md index ede6ce0..099c15c 100644 --- a/docs/08-bootstrapping-kubernetes-controllers.md +++ b/docs/08-bootstrapping-kubernetes-controllers.md @@ -1,425 +1,179 @@ # Bootstrapping the Kubernetes Control Plane -In this lab you will bootstrap the Kubernetes control plane across three compute instances and configure it for high availability. You will also create an external load balancer that exposes the Kubernetes API Servers to remote clients. The following components will be installed on each node: Kubernetes API Server, Scheduler, and Controller Manager. +In this lab you will bootstrap the Kubernetes control plane. The following components will be installed the controller machine: Kubernetes API Server, Scheduler, and Controller Manager. ## Prerequisites -The commands in this lab must be run on each controller instance: `controller-0`, `controller-1`, and `controller-2`. Login to each controller instance using the `gcloud` command. Example: +Copy Kubernetes binaries and systemd unit files to the `server` instance: -``` -gcloud compute ssh controller-0 +```bash +scp \ + downloads/kube-apiserver \ + downloads/kube-controller-manager \ + downloads/kube-scheduler \ + downloads/kubectl \ + units/kube-apiserver.service \ + units/kube-controller-manager.service \ + units/kube-scheduler.service \ + configs/kube-scheduler.yaml \ + configs/kube-apiserver-to-kubelet.yaml \ + root@server:~/ ``` -### Running commands in parallel with tmux +The commands in this lab must be run on the controller instance: `server`. Login to the controller instance using the `ssh` command. Example: -[tmux](https://github.com/tmux/tmux/wiki) can be used to run commands on multiple compute instances at the same time. See the [Running commands in parallel with tmux](01-prerequisites.md#running-commands-in-parallel-with-tmux) section in the Prerequisites lab. +```bash +ssh root@server +``` ## Provision the Kubernetes Control Plane Create the Kubernetes configuration directory: -``` -sudo mkdir -p /etc/kubernetes/config +```bash +mkdir -p /etc/kubernetes/config ``` -### Download and Install the Kubernetes Controller Binaries - -Download the official Kubernetes release binaries: - -``` -wget -q --show-progress --https-only --timestamping \ - "https://storage.googleapis.com/kubernetes-release/release/v1.21.0/bin/linux/amd64/kube-apiserver" \ - "https://storage.googleapis.com/kubernetes-release/release/v1.21.0/bin/linux/amd64/kube-controller-manager" \ - "https://storage.googleapis.com/kubernetes-release/release/v1.21.0/bin/linux/amd64/kube-scheduler" \ - "https://storage.googleapis.com/kubernetes-release/release/v1.21.0/bin/linux/amd64/kubectl" -``` +### Install the Kubernetes Controller Binaries Install the Kubernetes binaries: -``` +```bash { - chmod +x kube-apiserver kube-controller-manager kube-scheduler kubectl - sudo mv kube-apiserver kube-controller-manager kube-scheduler kubectl /usr/local/bin/ + chmod +x kube-apiserver \ + kube-controller-manager \ + kube-scheduler kubectl + + mv kube-apiserver \ + kube-controller-manager \ + kube-scheduler kubectl \ + /usr/local/bin/ } ``` ### Configure the Kubernetes API Server -``` +```bash { - sudo mkdir -p /var/lib/kubernetes/ + mkdir -p /var/lib/kubernetes/ - sudo mv ca.pem ca-key.pem kubernetes-key.pem kubernetes.pem \ - service-account-key.pem service-account.pem \ - encryption-config.yaml /var/lib/kubernetes/ + mv ca.crt ca.key \ + kube-api-server.key kube-api-server.crt \ + service-accounts.key service-accounts.crt \ + encryption-config.yaml \ + /var/lib/kubernetes/ } ``` -The instance internal IP address will be used to advertise the API Server to members of the cluster. Retrieve the internal IP address for the current compute instance: - -``` -INTERNAL_IP=$(curl -s -H "Metadata-Flavor: Google" \ - http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/ip) -``` - -``` -REGION=$(curl -s -H "Metadata-Flavor: Google" \ - http://metadata.google.internal/computeMetadata/v1/project/attributes/google-compute-default-region) -``` - -``` -KUBERNETES_PUBLIC_ADDRESS=$(gcloud compute addresses describe kubernetes-the-hard-way \ - --region $REGION \ - --format 'value(address)') -``` - Create the `kube-apiserver.service` systemd unit file: -``` -cat < Allow up to 10 seconds for the Kubernetes API Server to fully initialize. -### Enable HTTP Health Checks - -A [Google Network Load Balancer](https://cloud.google.com/compute/docs/load-balancing/network) will be used to distribute traffic across the three API servers and allow each API server to terminate TLS connections and validate client certificates. The network load balancer only supports HTTP health checks which means the HTTPS endpoint exposed by the API server cannot be used. As a workaround the nginx webserver can be used to proxy HTTP health checks. In this section nginx will be installed and configured to accept HTTP health checks on port `80` and proxy the connections to the API server on `https://127.0.0.1:6443/healthz`. - -> The `/healthz` API server endpoint does not require authentication by default. - -Install a basic web server to handle HTTP health checks: - -``` -sudo apt-get update -sudo apt-get install -y nginx -``` - -``` -cat > kubernetes.default.svc.cluster.local < Remember to run the above commands on each controller node: `controller-0`, `controller-1`, and `controller-2`. - ## RBAC for Kubelet Authorization In this section you will configure RBAC permissions to allow the Kubernetes API Server to access the Kubelet API on each worker node. Access to the Kubelet API is required for retrieving metrics, logs, and executing commands in pods. > This tutorial sets the Kubelet `--authorization-mode` flag to `Webhook`. Webhook mode uses the [SubjectAccessReview](https://kubernetes.io/docs/admin/authorization/#checking-api-access) API to determine authorization. -The commands in this section will effect the entire cluster and only need to be run once from one of the controller nodes. +The commands in this section will affect the entire cluster and only need to be run on the controller node. -``` -gcloud compute ssh controller-0 +```bash +ssh root@server ``` Create the `system:kube-apiserver-to-kubelet` [ClusterRole](https://kubernetes.io/docs/admin/authorization/rbac/#role-and-clusterrole) with permissions to access the Kubelet API and perform most common tasks associated with managing pods: -``` -cat < The compute instances created in this tutorial will not have permission to complete this section. **Run the following commands from the same machine used to create the compute instances**. - - -### Provision a Network Load Balancer - -Create the external load balancer network resources: - -``` -{ - KUBERNETES_PUBLIC_ADDRESS=$(gcloud compute addresses describe kubernetes-the-hard-way \ - --region $(gcloud config get-value compute/region) \ - --format 'value(address)') - - gcloud compute http-health-checks create kubernetes \ - --description "Kubernetes Health Check" \ - --host "kubernetes.default.svc.cluster.local" \ - --request-path "/healthz" - - gcloud compute firewall-rules create kubernetes-the-hard-way-allow-health-check \ - --network kubernetes-the-hard-way \ - --source-ranges 209.85.152.0/22,209.85.204.0/22,35.191.0.0/16 \ - --allow tcp - - gcloud compute target-pools create kubernetes-target-pool \ - --http-health-check kubernetes - - gcloud compute target-pools add-instances kubernetes-target-pool \ - --instances controller-0,controller-1,controller-2 - - gcloud compute forwarding-rules create kubernetes-forwarding-rule \ - --address ${KUBERNETES_PUBLIC_ADDRESS} \ - --ports 6443 \ - --region $(gcloud config get-value compute/region) \ - --target-pool kubernetes-target-pool -} +```bash +kubectl apply -f kube-apiserver-to-kubelet.yaml \ + --kubeconfig admin.kubeconfig ``` ### Verification -> The compute instances created in this tutorial will not have permission to complete this section. **Run the following commands from the same machine used to create the compute instances**. - -Retrieve the `kubernetes-the-hard-way` static IP address: - -``` -KUBERNETES_PUBLIC_ADDRESS=$(gcloud compute addresses describe kubernetes-the-hard-way \ - --region $(gcloud config get-value compute/region) \ - --format 'value(address)') -``` +At this point the Kubernetes control plane is up and running. Run the following commands from the `jumpbox` machine to verify it's working: Make a HTTP request for the Kubernetes version info: -``` -curl --cacert ca.pem https://${KUBERNETES_PUBLIC_ADDRESS}:6443/version +```bash +curl -k --cacert ca.crt https://server.kubernetes.local:6443/version ``` -> output - -``` +```text { "major": "1", - "minor": "21", - "gitVersion": "v1.21.0", - "gitCommit": "cb303e613a121a29364f75cc67d3d580833a7479", + "minor": "28", + "gitVersion": "v1.28.3", + "gitCommit": "a8a1abc25cad87333840cd7d54be2efaf31a3177", "gitTreeState": "clean", - "buildDate": "2021-04-08T16:25:06Z", - "goVersion": "go1.16.1", + "buildDate": "2023-10-18T11:33:18Z", + "goVersion": "go1.20.10", "compiler": "gc", - "platform": "linux/amd64" + "platform": "linux/arm64" } ``` diff --git a/docs/09-bootstrapping-kubernetes-workers.md b/docs/09-bootstrapping-kubernetes-workers.md index 9958f88..fc4b15e 100644 --- a/docs/09-bootstrapping-kubernetes-workers.md +++ b/docs/09-bootstrapping-kubernetes-workers.md @@ -1,27 +1,60 @@ # Bootstrapping the Kubernetes Worker Nodes -In this lab you will bootstrap three Kubernetes worker nodes. The following components will be installed on each node: [runc](https://github.com/opencontainers/runc), [container networking plugins](https://github.com/containernetworking/cni), [containerd](https://github.com/containerd/containerd), [kubelet](https://kubernetes.io/docs/admin/kubelet), and [kube-proxy](https://kubernetes.io/docs/concepts/cluster-administration/proxies). +In this lab you will bootstrap two Kubernetes worker nodes. The following components will be installed: [runc](https://github.com/opencontainers/runc), [container networking plugins](https://github.com/containernetworking/cni), [containerd](https://github.com/containerd/containerd), [kubelet](https://kubernetes.io/docs/admin/kubelet), and [kube-proxy](https://kubernetes.io/docs/concepts/cluster-administration/proxies). ## Prerequisites -The commands in this lab must be run on each worker instance: `worker-0`, `worker-1`, and `worker-2`. Login to each worker instance using the `gcloud` command. Example: +Copy Kubernetes binaries and systemd unit files to each worker instance: -``` -gcloud compute ssh worker-0 +```bash +for host in node-0 node-1; do + SUBNET=$(grep $host machines.txt | cut -d " " -f 4) + sed "s|SUBNET|$SUBNET|g" \ + configs/10-bridge.conf > 10-bridge.conf + + sed "s|SUBNET|$SUBNET|g" \ + configs/kubelet-config.yaml > kubelet-config.yaml + + scp 10-bridge.conf kubelet-config.yaml \ + root@$host:~/ +done ``` -### Running commands in parallel with tmux +```bash +for host in node-0 node-1; do + scp \ + downloads/runc.arm64 \ + downloads/crictl-v1.28.0-linux-arm.tar.gz \ + downloads/cni-plugins-linux-arm64-v1.3.0.tgz \ + downloads/containerd-1.7.8-linux-arm64.tar.gz \ + downloads/kubectl \ + downloads/kubelet \ + downloads/kube-proxy \ + configs/99-loopback.conf \ + configs/containerd-config.toml \ + configs/kubelet-config.yaml \ + configs/kube-proxy-config.yaml \ + units/containerd.service \ + units/kubelet.service \ + units/kube-proxy.service \ + root@$host:~/ +done +``` -[tmux](https://github.com/tmux/tmux/wiki) can be used to run commands on multiple compute instances at the same time. See the [Running commands in parallel with tmux](01-prerequisites.md#running-commands-in-parallel-with-tmux) section in the Prerequisites lab. +The commands in this lab must be run on each worker instance: `node-0`, `node-1`. Login to the worker instance using the `ssh` command. Example: + +```bash +ssh root@node-0 +``` ## Provisioning a Kubernetes Worker Node Install the OS dependencies: -``` +```bash { - sudo apt-get update - sudo apt-get -y install socat conntrack ipset + apt-get update + apt-get -y install socat conntrack ipset } ``` @@ -29,39 +62,26 @@ Install the OS dependencies: ### Disable Swap -By default the kubelet will fail to start if [swap](https://help.ubuntu.com/community/SwapFaq) is enabled. It is [recommended](https://github.com/kubernetes/kubernetes/issues/7294) that swap be disabled to ensure Kubernetes can provide proper resource allocation and quality of service. +By default, the kubelet will fail to start if [swap](https://help.ubuntu.com/community/SwapFaq) is enabled. It is [recommended](https://github.com/kubernetes/kubernetes/issues/7294) that swap be disabled to ensure Kubernetes can provide proper resource allocation and quality of service. Verify if swap is enabled: -``` -sudo swapon --show +```bash +swapon --show ``` -If output is empthy then swap is not enabled. If swap is enabled run the following command to disable swap immediately: +If output is empty then swap is not enabled. If swap is enabled run the following command to disable swap immediately: -``` -sudo swapoff -a +```bash +swapoff -a ``` > To ensure swap remains off after reboot consult your Linux distro documentation. -### Download and Install Worker Binaries - -``` -wget -q --show-progress --https-only --timestamping \ - https://github.com/kubernetes-sigs/cri-tools/releases/download/v1.21.0/crictl-v1.21.0-linux-amd64.tar.gz \ - https://github.com/opencontainers/runc/releases/download/v1.0.0-rc93/runc.amd64 \ - https://github.com/containernetworking/plugins/releases/download/v0.9.1/cni-plugins-linux-amd64-v0.9.1.tgz \ - https://github.com/containerd/containerd/releases/download/v1.4.4/containerd-1.4.4-linux-amd64.tar.gz \ - https://storage.googleapis.com/kubernetes-release/release/v1.21.0/bin/linux/amd64/kubectl \ - https://storage.googleapis.com/kubernetes-release/release/v1.21.0/bin/linux/amd64/kube-proxy \ - https://storage.googleapis.com/kubernetes-release/release/v1.21.0/bin/linux/amd64/kubelet -``` - Create the installation directories: -``` -sudo mkdir -p \ +```bash +mkdir -p \ /etc/cni/net.d \ /opt/cni/bin \ /var/lib/kubelet \ @@ -72,242 +92,85 @@ sudo mkdir -p \ Install the worker binaries: -``` +```bash { - mkdir containerd - tar -xvf crictl-v1.21.0-linux-amd64.tar.gz - tar -xvf containerd-1.4.4-linux-amd64.tar.gz -C containerd - sudo tar -xvf cni-plugins-linux-amd64-v0.9.1.tgz -C /opt/cni/bin/ - sudo mv runc.amd64 runc + mkdir -p containerd + tar -xvf crictl-v1.28.0-linux-arm.tar.gz + tar -xvf containerd-1.7.8-linux-arm64.tar.gz -C containerd + tar -xvf cni-plugins-linux-arm64-v1.3.0.tgz -C /opt/cni/bin/ + mv runc.arm64 runc chmod +x crictl kubectl kube-proxy kubelet runc - sudo mv crictl kubectl kube-proxy kubelet runc /usr/local/bin/ - sudo mv containerd/bin/* /bin/ + mv crictl kubectl kube-proxy kubelet runc /usr/local/bin/ + mv containerd/bin/* /bin/ } ``` ### Configure CNI Networking -Retrieve the Pod CIDR range for the current compute instance: - -``` -POD_CIDR=$(curl -s -H "Metadata-Flavor: Google" \ - http://metadata.google.internal/computeMetadata/v1/instance/attributes/pod-cidr) -``` - Create the `bridge` network configuration file: -``` -cat < The `resolvConf` configuration is used to avoid loops when using CoreDNS for service discovery on systems running `systemd-resolved`. - -Create the `kubelet.service` systemd unit file: - -``` -cat < Remember to run the above commands on each worker node: `worker-0`, `worker-1`, and `worker-2`. - ## Verification -> The compute instances created in this tutorial will not have permission to complete this section. Run the following commands from the same machine used to create the compute instances. +The compute instances created in this tutorial will not have permission to complete this section. Run the following commands from the `jumpbox` machine. List the registered Kubernetes nodes: -``` -gcloud compute ssh controller-0 \ - --command "kubectl get nodes --kubeconfig admin.kubeconfig" +```bash +ssh root@server \ + "kubectl get nodes \ + --kubeconfig admin.kubeconfig" ``` -> output - ``` -NAME STATUS ROLES AGE VERSION -worker-0 Ready 22s v1.21.0 -worker-1 Ready 22s v1.21.0 -worker-2 Ready 22s v1.21.0 +NAME STATUS ROLES AGE VERSION +node-0 Ready 1m v1.28.3 +node-1 Ready 10s v1.28.3 ``` Next: [Configuring kubectl for Remote Access](10-configuring-kubectl.md) diff --git a/docs/10-configuring-kubectl.md b/docs/10-configuring-kubectl.md index 601fe28..cf90391 100644 --- a/docs/10-configuring-kubectl.md +++ b/docs/10-configuring-kubectl.md @@ -2,28 +2,45 @@ In this lab you will generate a kubeconfig file for the `kubectl` command line utility based on the `admin` user credentials. -> Run the commands in this lab from the same directory used to generate the admin client certificates. +> Run the commands in this lab from the `jumpbox` machine. ## The Admin Kubernetes Configuration File -Each kubeconfig requires a Kubernetes API Server to connect to. To support high availability the IP address assigned to the external load balancer fronting the Kubernetes API Servers will be used. +Each kubeconfig requires a Kubernetes API Server to connect to. + +You should be able to ping `server.kubernetes.local` based on the `/etc/hosts` DNS entry from a previous lap. + +```bash +curl -k --cacert ca.crt \ + https://server.kubernetes.local:6443/version +``` + +```text +{ + "major": "1", + "minor": "28", + "gitVersion": "v1.28.3", + "gitCommit": "a8a1abc25cad87333840cd7d54be2efaf31a3177", + "gitTreeState": "clean", + "buildDate": "2023-10-18T11:33:18Z", + "goVersion": "go1.20.10", + "compiler": "gc", + "platform": "linux/arm64" +} +``` Generate a kubeconfig file suitable for authenticating as the `admin` user: -``` +```bash { - KUBERNETES_PUBLIC_ADDRESS=$(gcloud compute addresses describe kubernetes-the-hard-way \ - --region $(gcloud config get-value compute/region) \ - --format 'value(address)') - kubectl config set-cluster kubernetes-the-hard-way \ - --certificate-authority=ca.pem \ + --certificate-authority=ca.crt \ --embed-certs=true \ - --server=https://${KUBERNETES_PUBLIC_ADDRESS}:6443 + --server=https://server.kubernetes.local:6443 kubectl config set-credentials admin \ - --client-certificate=admin.pem \ - --client-key=admin-key.pem + --client-certificate=admin.crt \ + --client-key=admin.key kubectl config set-context kubernetes-the-hard-way \ --cluster=kubernetes-the-hard-way \ @@ -32,35 +49,33 @@ Generate a kubeconfig file suitable for authenticating as the `admin` user: kubectl config use-context kubernetes-the-hard-way } ``` +The results of running the command above should create a kubeconfig file in the default location `~/.kube/config` used by the `kubectl` commandline tool. This also means you can run the `kubectl` command without specifying a config. + ## Verification Check the version of the remote Kubernetes cluster: -``` +```bash kubectl version ``` -> output - -``` -Client Version: version.Info{Major:"1", Minor:"21", GitVersion:"v1.21.0", GitCommit:"cb303e613a121a29364f75cc67d3d580833a7479", GitTreeState:"clean", BuildDate:"2021-04-08T16:31:21Z", GoVersion:"go1.16.1", Compiler:"gc", Platform:"linux/amd64"} -Server Version: version.Info{Major:"1", Minor:"21", GitVersion:"v1.21.0", GitCommit:"cb303e613a121a29364f75cc67d3d580833a7479", GitTreeState:"clean", BuildDate:"2021-04-08T16:25:06Z", GoVersion:"go1.16.1", Compiler:"gc", Platform:"linux/amd64"} +```text +Client Version: v1.28.3 +Kustomize Version: v5.0.4-0.20230601165947-6ce0bf390ce3 +Server Version: v1.28.3 ``` List the nodes in the remote Kubernetes cluster: -``` +```bash kubectl get nodes ``` -> output - ``` -NAME STATUS ROLES AGE VERSION -worker-0 Ready 2m35s v1.21.0 -worker-1 Ready 2m35s v1.21.0 -worker-2 Ready 2m35s v1.21.0 +NAME STATUS ROLES AGE VERSION +node-0 Ready 30m v1.28.3 +node-1 Ready 35m v1.28.3 ``` Next: [Provisioning Pod Network Routes](11-pod-network-routes.md) diff --git a/docs/11-pod-network-routes.md b/docs/11-pod-network-routes.md index b08994e..7e7cfd4 100644 --- a/docs/11-pod-network-routes.md +++ b/docs/11-pod-network-routes.md @@ -12,49 +12,67 @@ In this section you will gather the information required to create routes in the Print the internal IP address and Pod CIDR range for each worker instance: -``` -for instance in worker-0 worker-1 worker-2; do - gcloud compute instances describe ${instance} \ - --format 'value[separator=" "](networkInterfaces[0].networkIP,metadata.items[0].value)' -done +```bash +{ + SERVER_IP=$(grep server machines.txt | cut -d " " -f 1) + NODE_0_IP=$(grep node-0 machines.txt | cut -d " " -f 1) + NODE_0_SUBNET=$(grep node-0 machines.txt | cut -d " " -f 4) + NODE_1_IP=$(grep node-1 machines.txt | cut -d " " -f 1) + NODE_1_SUBNET=$(grep node-1 machines.txt | cut -d " " -f 4) +} ``` -> output - -``` -10.240.0.20 10.200.0.0/24 -10.240.0.21 10.200.1.0/24 -10.240.0.22 10.200.2.0/24 +```bash +ssh root@server < output +## Verification -``` -NAME NETWORK DEST_RANGE NEXT_HOP PRIORITY -default-route-1606ba68df692422 kubernetes-the-hard-way 10.240.0.0/24 kubernetes-the-hard-way 0 -default-route-615e3652a8b74e4d kubernetes-the-hard-way 0.0.0.0/0 default-internet-gateway 1000 -kubernetes-route-10-200-0-0-24 kubernetes-the-hard-way 10.200.0.0/24 10.240.0.20 1000 -kubernetes-route-10-200-1-0-24 kubernetes-the-hard-way 10.200.1.0/24 10.240.0.21 1000 -kubernetes-route-10-200-2-0-24 kubernetes-the-hard-way 10.200.2.0/24 10.240.0.22 1000 +```bash +ssh root@server ip route ``` -Next: [Deploying the DNS Cluster Add-on](12-dns-addon.md) +```text +default via XXX.XXX.XXX.XXX dev ens160 +10.200.0.0/24 via XXX.XXX.XXX.XXX dev ens160 +10.200.1.0/24 via XXX.XXX.XXX.XXX dev ens160 +XXX.XXX.XXX.0/24 dev ens160 proto kernel scope link src XXX.XXX.XXX.XXX +``` + +```bash +ssh root@node-0 ip route +``` + +```text +default via XXX.XXX.XXX.XXX dev ens160 +10.200.1.0/24 via XXX.XXX.XXX.XXX dev ens160 +XXX.XXX.XXX.0/24 dev ens160 proto kernel scope link src XXX.XXX.XXX.XXX +``` + +```bash +ssh root@node-1 ip route +``` + +```text +default via XXX.XXX.XXX.XXX dev ens160 +10.200.0.0/24 via XXX.XXX.XXX.XXX dev ens160 +XXX.XXX.XXX.0/24 dev ens160 proto kernel scope link src XXX.XXX.XXX.XXX +``` + + +Next: [Smoke Test](12-smoke-test.md) diff --git a/docs/12-dns-addon.md b/docs/12-dns-addon.md deleted file mode 100644 index be81ef6..0000000 --- a/docs/12-dns-addon.md +++ /dev/null @@ -1,81 +0,0 @@ -# Deploying the DNS Cluster Add-on - -In this lab you will deploy the [DNS add-on](https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/) which provides DNS based service discovery, backed by [CoreDNS](https://coredns.io/), to applications running inside the Kubernetes cluster. - -## The DNS Cluster Add-on - -Deploy the `coredns` cluster add-on: - -``` -kubectl apply -f https://storage.googleapis.com/kubernetes-the-hard-way/coredns-1.8.yaml -``` - -> output - -``` -serviceaccount/coredns created -clusterrole.rbac.authorization.k8s.io/system:coredns created -clusterrolebinding.rbac.authorization.k8s.io/system:coredns created -configmap/coredns created -deployment.apps/coredns created -service/kube-dns created -``` - -List the pods created by the `kube-dns` deployment: - -``` -kubectl get pods -l k8s-app=kube-dns -n kube-system -``` - -> output - -``` -NAME READY STATUS RESTARTS AGE -coredns-8494f9c688-hh7r2 1/1 Running 0 10s -coredns-8494f9c688-zqrj2 1/1 Running 0 10s -``` - -## Verification - -Create a `busybox` deployment: - -``` -kubectl run busybox --image=busybox:1.28 --command -- sleep 3600 -``` - -List the pod created by the `busybox` deployment: - -``` -kubectl get pods -l run=busybox -``` - -> output - -``` -NAME READY STATUS RESTARTS AGE -busybox 1/1 Running 0 3s -``` - -Retrieve the full name of the `busybox` pod: - -``` -POD_NAME=$(kubectl get pods -l run=busybox -o jsonpath="{.items[0].metadata.name}") -``` - -Execute a DNS lookup for the `kubernetes` service inside the `busybox` pod: - -``` -kubectl exec -ti $POD_NAME -- nslookup kubernetes -``` - -> output - -``` -Server: 10.32.0.10 -Address 1: 10.32.0.10 kube-dns.kube-system.svc.cluster.local - -Name: kubernetes -Address 1: 10.32.0.1 kubernetes.default.svc.cluster.local -``` - -Next: [Smoke Test](13-smoke-test.md) diff --git a/docs/13-smoke-test.md b/docs/12-smoke-test.md similarity index 53% rename from docs/13-smoke-test.md rename to docs/12-smoke-test.md index 566ace7..9a92ce8 100644 --- a/docs/13-smoke-test.md +++ b/docs/12-smoke-test.md @@ -8,48 +8,41 @@ In this section you will verify the ability to [encrypt secret data at rest](htt Create a generic secret: -``` +```bash kubectl create secret generic kubernetes-the-hard-way \ --from-literal="mykey=mydata" ``` Print a hexdump of the `kubernetes-the-hard-way` secret stored in etcd: -``` -gcloud compute ssh controller-0 \ - --command "sudo ETCDCTL_API=3 etcdctl get \ - --endpoints=https://127.0.0.1:2379 \ - --cacert=/etc/etcd/ca.pem \ - --cert=/etc/etcd/kubernetes.pem \ - --key=/etc/etcd/kubernetes-key.pem\ - /registry/secrets/default/kubernetes-the-hard-way | hexdump -C" +```bash +ssh root@server \ + 'etcdctl get /registry/secrets/default/kubernetes-the-hard-way | hexdump -C' ``` -> output - -``` +```text 00000000 2f 72 65 67 69 73 74 72 79 2f 73 65 63 72 65 74 |/registry/secret| 00000010 73 2f 64 65 66 61 75 6c 74 2f 6b 75 62 65 72 6e |s/default/kubern| 00000020 65 74 65 73 2d 74 68 65 2d 68 61 72 64 2d 77 61 |etes-the-hard-wa| 00000030 79 0a 6b 38 73 3a 65 6e 63 3a 61 65 73 63 62 63 |y.k8s:enc:aescbc| -00000040 3a 76 31 3a 6b 65 79 31 3a 97 d1 2c cd 89 0d 08 |:v1:key1:..,....| -00000050 29 3c 7d 19 41 cb ea d7 3d 50 45 88 82 a3 1f 11 |)<}.A...=PE.....| -00000060 26 cb 43 2e c8 cf 73 7d 34 7e b1 7f 9f 71 d2 51 |&.C...s}4~...q.Q| -00000070 45 05 16 e9 07 d4 62 af f8 2e 6d 4a cf c8 e8 75 |E.....b...mJ...u| -00000080 6b 75 1e b7 64 db 7d 7f fd f3 96 62 e2 a7 ce 22 |ku..d.}....b..."| -00000090 2b 2a 82 01 c3 f5 83 ae 12 8b d5 1d 2e e6 a9 90 |+*..............| -000000a0 bd f0 23 6c 0c 55 e2 52 18 78 fe bf 6d 76 ea 98 |..#l.U.R.x..mv..| -000000b0 fc 2c 17 36 e3 40 87 15 25 13 be d6 04 88 68 5b |.,.6.@..%.....h[| -000000c0 a4 16 81 f6 8e 3b 10 46 cb 2c ba 21 35 0c 5b 49 |.....;.F.,.!5.[I| -000000d0 e5 27 20 4c b3 8e 6b d0 91 c2 28 f1 cc fa 6a 1b |.' L..k...(...j.| -000000e0 31 19 74 e7 a5 66 6a 99 1c 84 c7 e0 b0 fc 32 86 |1.t..fj.......2.| -000000f0 f3 29 5a a4 1c d5 a4 e3 63 26 90 95 1e 27 d0 14 |.)Z.....c&...'..| -00000100 94 f0 ac 1a cd 0d b9 4b ae 32 02 a0 f8 b7 3f 0b |.......K.2....?.| -00000110 6f ad 1f 4d 15 8a d6 68 95 63 cf 7d 04 9a 52 71 |o..M...h.c.}..Rq| -00000120 75 ff 87 6b c5 42 e1 72 27 b5 e9 1a fe e8 c0 3f |u..k.B.r'......?| -00000130 d9 04 5e eb 5d 43 0d 90 ce fa 04 a8 4a b0 aa 01 |..^.]C......J...| -00000140 cf 6d 5b 80 70 5b 99 3c d6 5c c0 dc d1 f5 52 4a |.m[.p[.<.\....RJ| -00000150 2c 2d 28 5a 63 57 8e 4f df 0a |,-(ZcW.O..| +00000040 3a 76 31 3a 6b 65 79 31 3a 9b 79 a5 b9 49 a2 77 |:v1:key1:.y..I.w| +00000050 c0 6a c9 12 7c b4 c7 c4 64 41 37 97 4a 83 a9 c1 |.j..|...dA7.J...| +00000060 4f 14 ae 73 ab b8 38 26 11 14 0a 40 b8 f3 0e 0a |O..s..8&...@....| +00000070 f5 a7 a2 2c b6 35 b1 83 22 15 aa d0 dd 25 11 3e |...,.5.."....%.>| +00000080 c4 e9 69 1c 10 7a 9d f7 dc 22 28 89 2c 83 dd 0b |..i..z..."(.,...| +00000090 a4 5f 3a 93 0f ff 1f f8 bc 97 43 0e e5 05 5d f9 |._:.......C...].| +000000a0 ef 88 02 80 49 81 f1 58 b0 48 39 19 14 e1 b1 34 |....I..X.H9....4| +000000b0 f6 b0 9b 0a 9c 53 27 2b 23 b9 e6 52 b4 96 81 70 |.....S'+#..R...p| +000000c0 a7 b6 7b 4f 44 d4 9c 07 51 a3 1b 22 96 4c 24 6c |..{OD...Q..".L$l| +000000d0 44 6c db 53 f5 31 e6 3f 15 7b 4c 23 06 c1 37 73 |Dl.S.1.?.{L#..7s| +000000e0 e1 97 8e 4e 1a 2e 2c 1a da 85 c3 ff 42 92 d0 f1 |...N..,.....B...| +000000f0 87 b8 39 89 e8 46 2e b3 56 68 41 b8 1e 29 3d ba |..9..F..VhA..)=.| +00000100 dd d8 27 4c 7f d5 fe 97 3c a3 92 e9 3d ae 47 ee |..'L....<...=.G.| +00000110 24 6a 0b 7c ac b8 28 e6 25 a6 ce 04 80 ee c2 eb |$j.|..(.%.......| +00000120 4c 86 fa 70 66 13 63 59 03 c2 70 57 8b fb a1 d6 |L..pf.cY..pW....| +00000130 f2 58 08 84 43 f3 70 7f ad d8 30 63 3e ef ff b6 |.X..C.p...0c>...| +00000140 b2 06 c3 45 c5 d8 89 d3 47 4a 72 ca 20 9b cf b5 |...E....GJr. ...| +00000150 4b 3d 6d b4 58 ae 42 4b 7f 0a |K=m.X.BK..| 0000015a ``` @@ -61,21 +54,20 @@ In this section you will verify the ability to create and manage [Deployments](h Create a deployment for the [nginx](https://nginx.org/en/) web server: -``` -kubectl create deployment nginx --image=nginx +```bash +kubectl create deployment nginx \ + --image=nginx:latest ``` List the pod created by the `nginx` deployment: -``` +```bash kubectl get pods -l app=nginx ``` -> output - -``` -NAME READY STATUS RESTARTS AGE -nginx-f89759699-kpn5m 1/1 Running 0 10s +```bash +NAME READY STATUS RESTARTS AGE +nginx-56fcf95486-c8dnx 1/1 Running 0 8s ``` ### Port Forwarding @@ -84,46 +76,44 @@ In this section you will verify the ability to access applications remotely usin Retrieve the full name of the `nginx` pod: -``` -POD_NAME=$(kubectl get pods -l app=nginx -o jsonpath="{.items[0].metadata.name}") +```bash +POD_NAME=$(kubectl get pods -l app=nginx \ + -o jsonpath="{.items[0].metadata.name}") ``` Forward port `8080` on your local machine to port `80` of the `nginx` pod: -``` +```bash kubectl port-forward $POD_NAME 8080:80 ``` -> output - -``` +```text Forwarding from 127.0.0.1:8080 -> 80 Forwarding from [::1]:8080 -> 80 ``` In a new terminal make an HTTP request using the forwarding address: -``` +```bash curl --head http://127.0.0.1:8080 ``` -> output - -``` +```text HTTP/1.1 200 OK -Server: nginx/1.19.10 -Date: Sun, 02 May 2021 05:29:25 GMT +Server: nginx/1.25.3 +Date: Sun, 29 Oct 2023 01:44:32 GMT Content-Type: text/html -Content-Length: 612 -Last-Modified: Tue, 13 Apr 2021 15:13:59 GMT +Content-Length: 615 +Last-Modified: Tue, 24 Oct 2023 13:46:47 GMT Connection: keep-alive -ETag: "6075b537-264" +ETag: "6537cac7-267" Accept-Ranges: bytes + ``` Switch back to the previous terminal and stop the port forwarding to the `nginx` pod: -``` +```text Forwarding from 127.0.0.1:8080 -> 80 Forwarding from [::1]:8080 -> 80 Handling connection for 8080 @@ -136,15 +126,13 @@ In this section you will verify the ability to [retrieve container logs](https:/ Print the `nginx` pod logs: -``` +```bash kubectl logs $POD_NAME ``` -> output - -``` +```text ... -127.0.0.1 - - [02/May/2021:05:29:25 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.64.0" "-" +127.0.0.1 - - [01/Nov/2023:06:10:17 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.88.1" "-" ``` ### Exec @@ -153,14 +141,12 @@ In this section you will verify the ability to [execute commands in a container] Print the nginx version by executing the `nginx -v` command in the `nginx` container: -``` +```bash kubectl exec -ti $POD_NAME -- nginx -v ``` -> output - -``` -nginx version: nginx/1.19.10 +```text +nginx version: nginx/1.25.3 ``` ## Services @@ -169,52 +155,36 @@ In this section you will verify the ability to expose applications using a [Serv Expose the `nginx` deployment using a [NodePort](https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport) service: -``` -kubectl expose deployment nginx --port 80 --type NodePort +```bash +kubectl expose deployment nginx \ + --port 80 --type NodePort ``` > The LoadBalancer service type can not be used because your cluster is not configured with [cloud provider integration](https://kubernetes.io/docs/getting-started-guides/scratch/#cloud-provider). Setting up cloud provider integration is out of scope for this tutorial. Retrieve the node port assigned to the `nginx` service: -``` +```bash NODE_PORT=$(kubectl get svc nginx \ --output=jsonpath='{range .spec.ports[0]}{.nodePort}') ``` -Create a firewall rule that allows remote access to the `nginx` node port: +Make an HTTP request using the IP address and the `nginx` node port: -``` -gcloud compute firewall-rules create kubernetes-the-hard-way-allow-nginx-service \ - --allow=tcp:${NODE_PORT} \ - --network kubernetes-the-hard-way +```bash +curl -I http://node-0:${NODE_PORT} ``` -Retrieve the external IP address of a worker instance: - -``` -EXTERNAL_IP=$(gcloud compute instances describe worker-0 \ - --format 'value(networkInterfaces[0].accessConfigs[0].natIP)') -``` - -Make an HTTP request using the external IP address and the `nginx` node port: - -``` -curl -I http://${EXTERNAL_IP}:${NODE_PORT} -``` - -> output - -``` +```text HTTP/1.1 200 OK -Server: nginx/1.19.10 -Date: Sun, 02 May 2021 05:31:52 GMT +Server: nginx/1.25.3 +Date: Sun, 29 Oct 2023 05:11:15 GMT Content-Type: text/html -Content-Length: 612 -Last-Modified: Tue, 13 Apr 2021 15:13:59 GMT +Content-Length: 615 +Last-Modified: Tue, 24 Oct 2023 13:46:47 GMT Connection: keep-alive -ETag: "6075b537-264" +ETag: "6537cac7-267" Accept-Ranges: bytes ``` -Next: [Cleaning Up](14-cleanup.md) +Next: [Cleaning Up](13-cleanup.md) diff --git a/docs/13-cleanup.md b/docs/13-cleanup.md new file mode 100644 index 0000000..47a97d3 --- /dev/null +++ b/docs/13-cleanup.md @@ -0,0 +1,7 @@ +# Cleaning Up + +In this lab you will delete the compute resources created during this tutorial. + +## Compute Instances + +Delete the controller and worker compute instances. diff --git a/docs/14-cleanup.md b/docs/14-cleanup.md deleted file mode 100644 index 5ab908c..0000000 --- a/docs/14-cleanup.md +++ /dev/null @@ -1,63 +0,0 @@ -# Cleaning Up - -In this lab you will delete the compute resources created during this tutorial. - -## Compute Instances - -Delete the controller and worker compute instances: - -``` -gcloud -q compute instances delete \ - controller-0 controller-1 controller-2 \ - worker-0 worker-1 worker-2 \ - --zone $(gcloud config get-value compute/zone) -``` - -## Networking - -Delete the external load balancer network resources: - -``` -{ - gcloud -q compute forwarding-rules delete kubernetes-forwarding-rule \ - --region $(gcloud config get-value compute/region) - - gcloud -q compute target-pools delete kubernetes-target-pool - - gcloud -q compute http-health-checks delete kubernetes - - gcloud -q compute addresses delete kubernetes-the-hard-way -} -``` - -Delete the `kubernetes-the-hard-way` firewall rules: - -``` -gcloud -q compute firewall-rules delete \ - kubernetes-the-hard-way-allow-nginx-service \ - kubernetes-the-hard-way-allow-internal \ - kubernetes-the-hard-way-allow-external \ - kubernetes-the-hard-way-allow-health-check -``` - -Delete the `kubernetes-the-hard-way` network VPC: - -``` -{ - gcloud -q compute routes delete \ - kubernetes-route-10-200-0-0-24 \ - kubernetes-route-10-200-1-0-24 \ - kubernetes-route-10-200-2-0-24 - - gcloud -q compute networks subnets delete kubernetes - - gcloud -q compute networks delete kubernetes-the-hard-way -} -``` - -Delete the `kubernetes-the-hard-way` compute address: - -``` -gcloud -q compute addresses delete kubernetes-the-hard-way \ - --region $(gcloud config get-value compute/region) -``` diff --git a/docs/images/tmux-screenshot.png b/docs/images/tmux-screenshot.png deleted file mode 100644 index bf23b114d3532a298144cc38ba97f891cd522cdd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118848 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 diff --git a/downloads.txt b/downloads.txt new file mode 100644 index 0000000..f093923 --- /dev/null +++ b/downloads.txt @@ -0,0 +1,11 @@ +https://storage.googleapis.com/kubernetes-release/release/v1.28.3/bin/linux/arm64/kubectl +https://storage.googleapis.com/kubernetes-release/release/v1.28.3/bin/linux/arm64/kube-apiserver +https://storage.googleapis.com/kubernetes-release/release/v1.28.3/bin/linux/arm64/kube-controller-manager +https://storage.googleapis.com/kubernetes-release/release/v1.28.3/bin/linux/arm64/kube-scheduler +https://github.com/kubernetes-sigs/cri-tools/releases/download/v1.28.0/crictl-v1.28.0-linux-arm.tar.gz +https://github.com/opencontainers/runc/releases/download/v1.1.9/runc.arm64 +https://github.com/containernetworking/plugins/releases/download/v1.3.0/cni-plugins-linux-arm64-v1.3.0.tgz +https://github.com/containerd/containerd/releases/download/v1.7.8/containerd-1.7.8-linux-arm64.tar.gz +https://storage.googleapis.com/kubernetes-release/release/v1.28.3/bin/linux/arm64/kube-proxy +https://storage.googleapis.com/kubernetes-release/release/v1.28.3/bin/linux/arm64/kubelet +https://github.com/etcd-io/etcd/releases/download/v3.4.27/etcd-v3.4.27-linux-arm64.tar.gz diff --git a/units/containerd.service b/units/containerd.service new file mode 100644 index 0000000..fe6fb25 --- /dev/null +++ b/units/containerd.service @@ -0,0 +1,19 @@ +[Unit] +Description=containerd container runtime +Documentation=https://containerd.io +After=network.target + +[Service] +ExecStartPre=/sbin/modprobe overlay +ExecStart=/bin/containerd +Restart=always +RestartSec=5 +Delegate=yes +KillMode=process +OOMScoreAdjust=-999 +LimitNOFILE=1048576 +LimitNPROC=infinity +LimitCORE=infinity + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/units/etcd.service b/units/etcd.service new file mode 100644 index 0000000..40ec247 --- /dev/null +++ b/units/etcd.service @@ -0,0 +1,22 @@ +[Unit] +Description=etcd +Documentation=https://github.com/etcd-io/etcd + +[Service] +Type=notify +Environment="ETCD_UNSUPPORTED_ARCH=arm64" +ExecStart=/usr/local/bin/etcd \ + --name controller \ + --initial-advertise-peer-urls http://127.0.0.1:2380 \ + --listen-peer-urls http://127.0.0.1:2380 \ + --listen-client-urls http://127.0.0.1:2379 \ + --advertise-client-urls http://127.0.0.1:2379 \ + --initial-cluster-token etcd-cluster-0 \ + --initial-cluster controller=http://127.0.0.1:2380 \ + --initial-cluster-state new \ + --data-dir=/var/lib/etcd +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/units/kube-apiserver.service b/units/kube-apiserver.service new file mode 100644 index 0000000..1a0b035 --- /dev/null +++ b/units/kube-apiserver.service @@ -0,0 +1,36 @@ +[Unit] +Description=Kubernetes API Server +Documentation=https://github.com/kubernetes/kubernetes + +[Service] +ExecStart=/usr/local/bin/kube-apiserver \ + --allow-privileged=true \ + --apiserver-count=1 \ + --audit-log-maxage=30 \ + --audit-log-maxbackup=3 \ + --audit-log-maxsize=100 \ + --audit-log-path=/var/log/audit.log \ + --authorization-mode=Node,RBAC \ + --bind-address=0.0.0.0 \ + --client-ca-file=/var/lib/kubernetes/ca.crt \ + --enable-admission-plugins=NamespaceLifecycle,NodeRestriction,LimitRanger,ServiceAccount,DefaultStorageClass,ResourceQuota \ + --etcd-servers=http://127.0.0.1:2379 \ + --event-ttl=1h \ + --encryption-provider-config=/var/lib/kubernetes/encryption-config.yaml \ + --kubelet-certificate-authority=/var/lib/kubernetes/ca.crt \ + --kubelet-client-certificate=/var/lib/kubernetes/kube-api-server.crt \ + --kubelet-client-key=/var/lib/kubernetes/kube-api-server.key \ + --runtime-config='api/all=true' \ + --service-account-key-file=/var/lib/kubernetes/service-accounts.crt \ + --service-account-signing-key-file=/var/lib/kubernetes/service-accounts.key \ + --service-account-issuer=https://server.kubernetes.local:6443 \ + --service-cluster-ip-range=10.32.0.0/24 \ + --service-node-port-range=30000-32767 \ + --tls-cert-file=/var/lib/kubernetes/kube-api-server.crt \ + --tls-private-key-file=/var/lib/kubernetes/kube-api-server.key \ + --v=2 +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/units/kube-controller-manager.service b/units/kube-controller-manager.service new file mode 100644 index 0000000..0ebfda7 --- /dev/null +++ b/units/kube-controller-manager.service @@ -0,0 +1,22 @@ +[Unit] +Description=Kubernetes Controller Manager +Documentation=https://github.com/kubernetes/kubernetes + +[Service] +ExecStart=/usr/local/bin/kube-controller-manager \ + --bind-address=0.0.0.0 \ + --cluster-cidr=10.200.0.0/16 \ + --cluster-name=kubernetes \ + --cluster-signing-cert-file=/var/lib/kubernetes/ca.crt \ + --cluster-signing-key-file=/var/lib/kubernetes/ca.key \ + --kubeconfig=/var/lib/kubernetes/kube-controller-manager.kubeconfig \ + --root-ca-file=/var/lib/kubernetes/ca.crt \ + --service-account-private-key-file=/var/lib/kubernetes/service-accounts.key \ + --service-cluster-ip-range=10.32.0.0/24 \ + --use-service-account-credentials=true \ + --v=2 +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/units/kube-proxy.service b/units/kube-proxy.service new file mode 100644 index 0000000..88db1af --- /dev/null +++ b/units/kube-proxy.service @@ -0,0 +1,12 @@ +[Unit] +Description=Kubernetes Kube Proxy +Documentation=https://github.com/kubernetes/kubernetes + +[Service] +ExecStart=/usr/local/bin/kube-proxy \ + --config=/var/lib/kube-proxy/kube-proxy-config.yaml +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/units/kube-scheduler.service b/units/kube-scheduler.service new file mode 100644 index 0000000..e79ea29 --- /dev/null +++ b/units/kube-scheduler.service @@ -0,0 +1,13 @@ +[Unit] +Description=Kubernetes Scheduler +Documentation=https://github.com/kubernetes/kubernetes + +[Service] +ExecStart=/usr/local/bin/kube-scheduler \ + --config=/etc/kubernetes/config/kube-scheduler.yaml \ + --v=2 +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/units/kubelet.service b/units/kubelet.service new file mode 100644 index 0000000..a524920 --- /dev/null +++ b/units/kubelet.service @@ -0,0 +1,17 @@ +[Unit] +Description=Kubernetes Kubelet +Documentation=https://github.com/kubernetes/kubernetes +After=containerd.service +Requires=containerd.service + +[Service] +ExecStart=/usr/local/bin/kubelet \ + --config=/var/lib/kubelet/kubelet-config.yaml \ + --kubeconfig=/var/lib/kubelet/kubeconfig \ + --register-node=true \ + --v=2 +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target \ No newline at end of file