|
| 1 | +# Kubernetes Controller Example |
| 2 | + |
| 3 | +This is an example application that uses Kubernetes Custom Resource Definitions to keep a list of TODO actions, but stored in Kubernetes. |
| 4 | + |
| 5 | +The application is a custom resource definition, namespaced. |
| 6 | + |
| 7 | +The code in this repository **is not production ready**. It's merely an example repository for training purposes. |
| 8 | + |
| 9 | + |
| 10 | + |
| 11 | +## Prerequisites |
| 12 | + |
| 13 | +The following apps are required: |
| 14 | + |
| 15 | +* Kubernetes `kubectl` |
| 16 | +* Docker |
| 17 | +* Kind |
| 18 | + |
| 19 | +The stack uses [Kind](https://kind.sigs.k8s.io/) to launch a cluster locally, then deploy the controller there. Since this controller relies on a Docker container, see below for instructions on how to send the Docker image once built to the Kubernetes Kind cluster. |
| 20 | + |
| 21 | +To launch a cluster with a single node mappings port 80 and 443 to 80 and 443 on given node, use [`kind/cluster.yaml`](kind/cluster.yaml): |
| 22 | + |
| 23 | +```bash |
| 24 | +kind create cluster --config kind/cluster.yaml |
| 25 | +``` |
| 26 | + |
| 27 | +If using a different cluster engine other than Kind, ensure you perform the appropriate modifications to the [`controller/controller.yaml`](controller/controller.yaml) file so it uses the proper Docker image. See below for more details. |
| 28 | + |
| 29 | +## Application structure |
| 30 | + |
| 31 | +```text |
| 32 | +. |
| 33 | +├── controller |
| 34 | +│ ├── controller.yaml |
| 35 | +│ ├── Dockerfile |
| 36 | +│ ├── go.mod |
| 37 | +│ ├── homepage.tmpl |
| 38 | +│ ├── kubernetes.go |
| 39 | +│ ├── main.go |
| 40 | +│ ├── req_logger.go |
| 41 | +│ ├── res_utils.go |
| 42 | +│ └── utils.go |
| 43 | +├── kind |
| 44 | +│ └── cluster.yaml |
| 45 | +├── kubernetes |
| 46 | +│ ├── crd-use.yaml |
| 47 | +│ └── crd.yaml |
| 48 | +└── README.md |
| 49 | +``` |
| 50 | + |
| 51 | +## Kubernetes & CRD |
| 52 | + |
| 53 | +The [`kubernetes`](kubernetes) folder contains two files: |
| 54 | + |
| 55 | +* The CRD itself, named [`crd.yaml`](kubernetes/crd.yaml) which is used to create a resource named `Todo` in Kubernetes. |
| 56 | +* The usage of given CRD, called [`crd-use.yaml`](kubernetes/crd-use.yaml). This is used to create an initial TODO object. |
| 57 | + |
| 58 | +## Go Application |
| 59 | + |
| 60 | +The [`controller`](controller) folder includes a Go application that reads from the Kubernetes API server any resource typed `Todo`. |
| 61 | + |
| 62 | +The application **does not use the `kubernetes/client-go` to interact with the Kubernetes API**. Since the idea of this repository is to provide a baseline for training, it uses instead pure HTTP calls. It does ensure, however: |
| 63 | + |
| 64 | +* That the HTTP client is configured to talk to the Kubernetes API using the CA certificate from the `ServiceAccount`. |
| 65 | +* That the token and current namespace are captured from the `ServiceAccount` details automatically mounted in the container. |
| 66 | + |
| 67 | +It's possible to use a sidecar container to avoid having all this logic here: the sidecar container can run `kubectl proxy` and this app can interact then with the sidecar's host and port to perform preauthenticated requests against the API. |
| 68 | + |
| 69 | +The Go application uses a very simple Vue UI which loads both the namespace the controller is running at, as well as any `Todo` in it, and returns it back to the UI using Javascript's `fetch()`. |
| 70 | + |
| 71 | +### Compiling the application |
| 72 | + |
| 73 | +The application uses a Docker multi-stage build to compile the application and avoid having to build it locally. You can perform simple tests by running this app in your own machine rather than in the cluster, if at the same time you use `kubectl proxy` to allow for unauthenticated local access to the Kubernetes API. Then, simply configure the environment variables so the controller knows where to connect to the Kubernetes API. In this case, those environment variables are in [`controller/.env`](controller/.env): |
| 74 | + |
| 75 | +```bash |
| 76 | +KUBERNETES_API_HOST=http://localhost:8080 |
| 77 | +KUBERNETES_CONTROLLER_PORT=8081 |
| 78 | +``` |
| 79 | + |
| 80 | +Run the application with these environment variables set, and in another terminal, run: |
| 81 | + |
| 82 | +```bash |
| 83 | +kubectl proxy --port 8080 |
| 84 | +``` |
| 85 | + |
| 86 | +That way, the controller connects to your `kubectl proxy` on port `8080`, but the app itself with the UI is available in `localhost:8081`. |
| 87 | + |
| 88 | +### Running the application |
| 89 | + |
| 90 | +In order for the application to run in a cluster, you need: |
| 91 | + |
| 92 | +* To have the Docker container either stored in a Registry or loaded into the Cluster |
| 93 | +* To allow the controller to have access to the Kubernetes API, and specifically, to the `Todo` resource in the current namespace |
| 94 | + |
| 95 | +To achieve the first point while using Kind, you can build the Docker image locally, then load it into the cluster using the `kind` CLI: |
| 96 | + |
| 97 | +```bash |
| 98 | +$ docker build -q -t demo-controller -f controller/Dockerfile controller/ |
| 99 | +sha256:4a711a67ac8cb79f555bf736f72bf7eff3febf918895499b7f2c2093f3ca1cbe |
| 100 | + |
| 101 | +$ kind load docker-image demo-controller:latest --name testing-cluster |
| 102 | +Image: "demo-controller:latest" with ID "sha256:4a711a67ac8cb79f555bf736f72bf7eff3febf918895499b7f2c2093f3ca1cbe" not yet present on node "testing-cluster-control-plane", loading... |
| 103 | +``` |
| 104 | + |
| 105 | +You can use the image now. If you're using Kind, make sure the `imagePullPolicy` is set to `IfNotPresent` to ensure it uses the copy of the image we just loaded rather than grabbing one from Internet. |
| 106 | + |
| 107 | +In order to have a full controller, a deployment has been provided using the [`controller/controller.yaml`](controller/controller.yaml) file. This file will: |
| 108 | + |
| 109 | +* Create a service account named `example-controller-service-account` |
| 110 | +* Create a Role which has access to list `todos` |
| 111 | +* Bind the Service account to the Role using a Role Binding |
| 112 | +* Finally, deploy the controller using a Deployment, which maps port `8080` as `web` |
| 113 | + |
| 114 | +You can install this manifest using: |
| 115 | + |
| 116 | +```bash |
| 117 | +$ kubectl apply -f controller/controller.yaml |
| 118 | +serviceaccount/example-controller-service-account created |
| 119 | +role.rbac.authorization.k8s.io/example-controller-role created |
| 120 | +rolebinding.rbac.authorization.k8s.io/example-controller-rolebinding created |
| 121 | +deployment.apps/example-controller created |
| 122 | +``` |
| 123 | + |
| 124 | +You can access the application running here by using Kubernetes port-forward. The example below will use `awk` to find the appropriate pod then expose it on port `1234`: |
| 125 | + |
| 126 | +```bash |
| 127 | +$ kubectl get pods --show-labels | awk '/example\-controller/ { print $1 }' | xargs -I{} kubectl port-forward --address 0.0.0.0 pod/{} 1234:8080 |
| 128 | +Forwarding from 0.0.0.0:1234 -> 8080 |
| 129 | +``` |
| 130 | + |
| 131 | +Alternatively, you can build a `Service` out of it and expose it, or top it off with an `Ingress`. The sky is the limit. |
0 commit comments