Custom Resources

Configure the CustomResourceDefinitions

To define a new resource type, all you need to do is post a CustomResourceDefinition object (CRD) to the Kubernetes API server. The CustomResourceDefinition object is the description of the custom resource type. Once the CRD is posted, users can then create instances of the custom resource by posting JSON or YAML manifests to the API server, the same as with any other Kubernetes resource.

Let’s imagine you want to allow users of your Kubernetes cluster to run static websites as easily as possible, without having to deal with Pods, Services, and other Kubernetes resources. What you want to achieve is for users to create objects of type Website that contain nothing more than the website’s name and the source from which the website’s files (HTML, CSS, PNG, and others) should be obtained, eg., docker image, github, … When a user creates an instance of the Website resource, you want Kubernetes to spin up a new web server pod and expose it through a Service.

To create the Website resource, you want users to post manifests along the lines of the one shown in the following listing.

apiVersion: extensions.example.com/v1
kind: Website
metadata:
  name: kubia
spec:
  gitRepo: https://github.com/hoangph3/kubia-website-example.git

If you try posting this resource to Kubernetes, you’ll receive an error because Kubernetes doesn’t know what a Website object is yet:

$ kubectl apply -f imaginary-website.yaml
error: unable to recognize "imaginary-website.yaml": no matches for kind "Website" in version "extensions.example.com/v1"

Now you will create the CRD from the website-crd.yaml file:

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  # name must match the spec fields below, and be in the form: <plural>.<group>
  name: websites.extensions.example.com
spec:
  scope: Namespaced # either Namespaced or Cluster
  group: extensions.example.com # group name to use for REST API: /apis/<group>/<version>
  versions:
    - name: v1
      served: true # Each version can be enabled/disabled by Served flag.
      storage: true # One and only one version must be marked as the storage version.
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                gitRepo:
                  type: string
  names:
    kind: Website # kind is normally the CamelCased singular type. Your resource manifests use this.
    singular: website # singular name to be used as an alias on the CLI and for display
    plural: websites # plural name to be used in the URL: /apis/<group>/<version>/<plural>
    shortNames: # shortNames allow shorter string to match your resource on the CLI
    - gitrp
$ kubectl apply -f website-crd.yaml
customresourcedefinition.apiextensions.k8s.io/websites.extensions.example.com created

Create your Website object now:

$ kubectl apply -f imaginary-website.yaml
website.extensions.example.com/kubia created

$ kubectl get websites
NAME    AGE
kubia   35s

$ kubectl describe websites.extensions.example.com kubia 
Name:         kubia
Namespace:    default
Labels:       <none>
Annotations:  <none>
API Version:  extensions.example.com/v1
Kind:         Website
Metadata:
  Creation Timestamp:  2022-04-24T08:06:14Z
  Generation:          1
  Managed Fields:
    API Version:  extensions.example.com/v1
    Fields Type:  FieldsV1
    fieldsV1:
      f:metadata:
        f:annotations:
          .:
          f:kubectl.kubernetes.io/last-applied-configuration:
      f:spec:
        .:
        f:gitRepo:
    Manager:         kubectl-client-side-apply
    Operation:       Update
    Time:            2022-04-24T08:06:14Z
  Resource Version:  505709
  UID:               e1721dc4-e80e-4628-be20-11366aa6bffa
Spec:
  Git Repo:  https://github.com/hoangph3/kubia-website-example.git
Events:      <none>

To make your Website objects run a web server pod exposed through a Service, you’ll need to build and deploy a Website controller, which will watch the API server for the creation of Website objects and then create the Service and the web server Pod for each of them.

Firstly, build website-controller image:

$ cd website-controller && ls
deployment-template.json  Dockerfile  pkg  service-template.json

$ go mod init website-controller
go: creating new go.mod: module website-controller
go: to add module requirements and sums:
        go mod tidy

$ go get github.com/hoangph3/website-controller/pkg/v1
go: downloading github.com/hoangph3/website-controller v0.0.0-20170607104431-912c847af8cc
go: added github.com/hoangph3/website-controller v0.0.0-20170607104431-912c847af8cc

$ CGO_ENABLED=0 GOOS=linux go build -o website-controller -a pkg/website-controller.go

$ ls -la
total 6564
drwxr-xr-x 3 ph3 ph3    4096 Apr 24 07:09 .
drwxr-xr-x 4 ph3 ph3    4096 Apr 24 06:51 ..
-rw-r--r-- 1 ph3 ph3    1786 Apr 24 05:51 deployment-template.json
-rw-r--r-- 1 ph3 ph3     125 Apr 24 05:49 Dockerfile
-rw-r--r-- 1 ph3 ph3     266 Jun  7  2017 .gitignore
-rw-r--r-- 1 ph3 ph3     130 Apr 24 06:45 go.mod
-rw-r--r-- 1 ph3 ph3     251 Apr 24 06:45 go.sum
drwx------ 3 ph3 ph3    4096 Jun  7  2017 pkg
-rw-r--r-- 1 ph3 ph3     342 Jun  7  2017 service-template.json
-rwxr-xr-x 1 ph3 ph3 6680760 Apr 24 07:09 website-controller

$ docker build -t website-controller . 
Sending build context to Docker daemon  6.696MB
Step 1/5 : FROM scratch
 ---> 
Step 2/5 : ADD website-controller /
 ---> 6f3fd9ca8612
Step 3/5 : ADD deployment-template.json /
 ---> 77a642a5c4af
Step 4/5 : ADD service-template.json /
 ---> 7886a1be5b6c
Step 5/5 : CMD ["/website-controller"]
 ---> Running in e0677b637e27
Removing intermediate container e0677b637e27
 ---> 757ada991a0c
Successfully built 757ada991a0c
Successfully tagged kube-website-controller:latest

Then build ambassador kubectl-proxy image used for communication with the API server:

$ cd kubectl-proxy && ls
Dockerfile  kubectl-proxy.sh

$ docker build -t kubectl-proxy .      
Sending build context to Docker daemon  3.072kB
Step 1/5 : FROM python:3.8-slim-buster
 ---> 9f9436d44487
Step 2/5 : RUN apt -y update && apt -y install curl telnet nano && curl -L -O https://dl.k8s.io/v1.23.5/kubernetes-client-linux-amd64.tar.gz && tar zvxf kubernetes-client-linux-amd64.tar.gz kubernetes/client/bin/kubectl && mv kubernetes/client/bin/kubectl / && rm -rf kubernetes && rm -f kubernetes-client-linux-amd64.tar.gz
 ---> Using cache
 ---> 6e63691a26b0
Step 3/5 : ADD kubectl-proxy.sh /kubectl-proxy.sh
 ---> 8188274c8f2c
Step 4/5 : RUN chmod +x kubectl-proxy.sh
 ---> Running in f18dacc8be53
Removing intermediate container f18dacc8be53
 ---> 2619234d7639
Step 5/5 : ENTRYPOINT /kubectl-proxy.sh
 ---> Running in 5ab5b47a9706
Removing intermediate container 5ab5b47a9706
 ---> fd2d8e870acf
Successfully built fd2d8e870acf
Successfully tagged kubectl-proxy:latest

Create kubernetes website controller:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: website-controller
  labels:
    app: website-controller
spec:
  replicas: 1
  selector:
    matchLabels:
      app: website-controller
  template:
    metadata:
      name: website-controller
      labels:
        app: website-controller
    spec:
      serviceAccountName: website-controller
      containers:
      - name: main
        image: website-controller
        imagePullPolicy: Never
      - name: proxy
        image: kubectl-proxy
        imagePullPolicy: Never

Note that you need create ServiceAccount website-controller first:

$ kubectl create serviceaccount website-controller
serviceaccount/website-controller created

Then create the controller:

$ kubectl apply -f website-controller.yaml
deployment.apps/website-controller created

Bind ClusterRole cluster-admin to ServiceAccount:

$ kubectl create clusterrolebinding website-controller --clusterrole=cluster-admin --serviceaccount=default:website-controller
clusterrolebinding.rbac.authorization.k8s.io/website-controller created

With the controller now running, create the Website resource again:

$ kubectl apply -f imaginary-website.yaml
website.extensions.example.com/kubia created

Now, let’s check the controller’s logs and lists all Deployments, Services and Pods:

$ kubectl get pods
NAME                                  READY   STATUS    RESTARTS       AGE
kubia-website-7d94854d9f-clqg5        2/2     Running   0              5s
website-controller-5dd4555d87-rwsz7   2/2     Running   1 (2m4s ago)   2m5s

$ kubectl logs -f website-controller-5dd4555d87-4rl2x -c main
2022/04/25 01:45:17 website-controller started.
2022/04/25 01:45:30 Received watch event: ADDED: kubia: https://github.com/hoangph3/kubia-website-example.git
2022/04/25 01:45:30 Creating services with name kubia-website in namespace default
2022/04/25 01:45:30 response Status: 201 Created
2022/04/25 01:45:30 Creating deployments with name kubia-website in namespace default
2022/04/25 01:45:30 response Status: 201 Created

$ kubectl get deploy,svc,po
NAME                                 READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/kubia-website        0/1     1            0           4s
deployment.apps/website-controller   1/1     1            1           19s

NAME                    TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
service/kubernetes      ClusterIP   10.96.0.1       <none>        443/TCP        88d
service/kubia-website   NodePort    10.104.14.151   <none>        80:32522/TCP   4s

NAME                                      READY   STATUS              RESTARTS      AGE
pod/kubia-website-7d94854d9f-jxzbh        0/2     ContainerCreating   0             4s
pod/website-controller-5dd4555d87-rwsz7   2/2     Running             1 (18s ago)   19s

$ minikube service list
|---------------|------------------------------------|--------------|---------------------------|
|   NAMESPACE   |                NAME                | TARGET PORT  |            URL            |
|---------------|------------------------------------|--------------|---------------------------|
| default       | kubernetes                         | No node port |
| default       | kubia-service                      |           80 | http://192.168.49.2:32522 |
| ingress-nginx | ingress-nginx-controller           | http/80      | http://192.168.49.2:31300 |
|               |                                    | https/443    | http://192.168.49.2:30830 |
| ingress-nginx | ingress-nginx-controller-admission | No node port |
| kube-system   | kube-dns                           | No node port |
|---------------|------------------------------------|--------------|---------------------------|

$ curl http://192.168.49.2:32522
<html>
<body>
Hello there.
</body>
</html>

If you want to delete this website:

$ kubectl delete -f imaginary-website.yaml
website.extensions.example.com "kubia" deleted

$ kubectl get deploy,svc,po               
NAME                                 READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/website-controller   1/1     1            1           4m6s

NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
service/kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   88d

NAME                                      READY   STATUS    RESTARTS       AGE
pod/website-controller-5dd4555d87-rwsz7   2/2     Running   1 (4m5s ago)   4m6s

$ kubectl logs -f website-controller-5dd4555d87-rwsz7 -c main
2022/04/25 01:45:17 website-controller started.
2022/04/25 01:45:30 Received watch event: ADDED: kubia: https://github.com/hoangph3/kubia-website-example.git
2022/04/25 01:45:30 Creating services with name kubia-website in namespace default
2022/04/25 01:45:30 response Status: 201 Created
2022/04/25 01:45:30 Creating deployments with name kubia-website in namespace default
2022/04/25 01:45:30 response Status: 201 Created
2022/04/25 01:46:01 Received watch event: DELETED: kubia: https://github.com/hoangph3/kubia-website-example.git
2022/04/25 01:46:01 Deleting services with name kubia-website in namespace default
2022/04/25 01:46:01 response Status: 200 OK
2022/04/25 01:46:01 Deleting deployments with name kubia-website in namespace default
2022/04/25 01:46:01 response Status: 200 OK

GPU Scheduling

Note: Support 1 local machine with nvidia-gpu, and minikube on it

Step 1: setup gpu (secureBoot must be off)

Step 2: install minikube (v1.25.2)

$ curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube_latest_amd64.deb
$ sudo dpkg -i minikube_latest_amd64.deb
$ minikube version
minikube version: v1.25.2
commit: 362d5fdc0a3dbee389b3d3f1034e8023e72bd3a7

Step 3: install nvidia-docker

$ wget http://mirror.cs.uchicago.edu/nvidia-docker/libnvidia-container/stable/ubuntu18.04/amd64/libnvidia-container1_1.9.0-1_amd64.deb (libnvidia-container1)
$ wget http://mirror.cs.uchicago.edu/nvidia-docker/libnvidia-container/stable/ubuntu18.04/amd64/libnvidia-container-tools_1.9.0-1_amd64.deb (libnvidia-container-tools)

$ sudo dpkg -i libnvidia-container1_1.9.0-1_amd64.deb libnvidia-container-tools_1.9.0-1_amd64.deb
$ wget http://mirror.cs.uchicago.edu/nvidia-docker/libnvidia-container/stable/ubuntu18.04/amd64/nvidia-container-runtime_3.9.0-1_all.deb (nvidia-container-runtime)
$ wget http://mirror.cs.uchicago.edu/nvidia-docker/libnvidia-container/stable/ubuntu18.04/amd64/nvidia-container-toolkit_1.9.0-1_amd64.deb (nvidia-container-toolkit)
$ wget http://mirror.cs.uchicago.edu/nvidia-docker/libnvidia-container/stable/ubuntu18.04/amd64/nvidia-docker2_2.10.0-1_all.deb (nvidia-docker2)

$ sudo dpkg -i nvidia-container-runtime_3.9.0-1_all.deb nvidia-container-toolkit_1.9.0-1_amd64.deb nvidia-docker2_2.10.0-1_all.deb

Step 4: config daemon docker in /etc/docker/daemon.json

{
    "runtimes": {
        "nvidia": {
            "path": "nvidia-container-runtime",
            "runtimeArgs": []
        }
    },
    "default-runtime": "nvidia"
}

Step 5: download kube-system images from https://hub.docker.com/ (because the proxy server has no connection to https://k8s.gcr.io/)

List the images of kubernetes cluster:

$ kubeadm config images list
W0618 11:26:15.229688   15772 version.go:103] could not fetch a Kubernetes version from the internet: unable to get URL "https://dl.k8s.io/release/stable-1.txt": Get "https://dl.k8s.io/release/stable-1.txt": x509: certificate signed by unknown authority
W0618 11:26:15.229716   15772 version.go:104] falling back to the local client version: v1.24.2

k8s.gcr.io/kube-apiserver:v1.24.2
k8s.gcr.io/kube-controller-manager:v1.24.2
k8s.gcr.io/kube-scheduler:v1.24.2
k8s.gcr.io/kube-proxy:v1.24.2
k8s.gcr.io/pause:3.7
k8s.gcr.io/etcd:3.5.3-0
k8s.gcr.io/coredns/coredns:v1.8.6

Now we will pull images (stable version v1.23.3), also we need to pull the storage-provider image:

# api-server
$ docker pull k8simage/kube-apiserver:v1.23.3

# controller-manager
$ docker pull k8simage/kube-controller-manager:v1.23.3

# scheduler
$ docker pull k8simage/kube-scheduler:v1.23.3

# proxy
$ docker pull k8simage/kube-proxy:v1.23.3

# pause
$ docker pull k8simage/pause:3.6

# etcd
$ docker pull k8simage/etcd:3.5.1-0

# coredns
$ docker pull k8simage/coredns:v1.8.6

# storage-provisioner
$ docker pull yedward/gcr.io.k8s-minikube.storage-provisioner:v1.8.1

Then re-tag image to ensure that the image’s name same as the image name will pulled when run minikube start:

# api-server
$ docker tag k8simage/kube-apiserver:v1.23.3 k8s.gcr.io/kube-apiserver:v1.23.3

# controller-manager
$ docker tag k8simage/kube-controller-manager:v1.23.3 k8s.gcr.io/kube-controller-manager:v1.23.3

# scheduler
$ docker tag k8simage/kube-scheduler:v1.23.3 k8s.gcr.io/kube-scheduler:v1.23.3

# proxy
$ docker tag k8simage/kube-proxy:v1.23.3 k8s.gcr.io/kube-proxy:v1.23.3

# pause
$ docker tag k8simage/pause:3.6 k8s.gcr.io/pause:3.6

# etcd
$ docker tag k8simage/etcd:3.5.1-0 k8s.gcr.io/etcd:3.5.1-0

# coredns
$ docker tag k8simage/coredns:v1.8.6 k8s.gcr.io/coredns/coredns:v1.8.6

# storage-provisioner
$ docker tag yedward/gcr.io.k8s-minikube.storage-provisioner:v1.8.1 gcr.io/k8s-minikube/storage-provisioner:v5

Step 6: start minikube cluster

Initialize the cluster as root privileges:

$ sudo -i
$ minikube start --driver=none --apiserver-ips 127.0.0.1 --apiserver-name localhost

Change the permission to $USER:

$ sudo mv /root/.kube /root/.minikube $HOME
$ sudo chown -R $USER $HOME/.kube $HOME/.minikube

Edit kubernetes config file, change certificate-authority, client-certificate, and client-key from root path to $USER path:

apiVersion: v1
clusters:
- cluster:
    certificate-authority: /home/hoang/.minikube/ca.crt
    extensions:
    - extension:
        last-update: Fri, 17 Jun 2022 17:10:56 +07
        provider: minikube.sigs.k8s.io
        version: v1.25.2
      name: cluster_info
    server: https://localhost:8443
  name: minikube
contexts:
- context:
    cluster: minikube
    extensions:
    - extension:
        last-update: Fri, 17 Jun 2022 17:10:56 +07
        provider: minikube.sigs.k8s.io
        version: v1.25.2
      name: context_info
    namespace: default
    user: minikube
  name: minikube
current-context: minikube
kind: Config
preferences: {}
users:
- name: minikube
  user:
    client-certificate: /home/hoang/.minikube/profiles/minikube/client.crt
    client-key: /home/hoang/.minikube/profiles/minikube/client.key

Step 7: install NVIDIA’s device plugin

Create the manifest yaml file nvidia-device-plugin.yml:

# Copyright (c) 2019, NVIDIA CORPORATION.  All rights reserved.
#
# 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: apps/v1
kind: DaemonSet
metadata:
  name: nvidia-device-plugin-daemonset
  namespace: kube-system
spec:
  selector:
    matchLabels:
      name: nvidia-device-plugin-ds
  updateStrategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        name: nvidia-device-plugin-ds
    spec:
      tolerations:
      - key: nvidia.com/gpu
        operator: Exists
        effect: NoSchedule
      # Mark this pod as a critical add-on; when enabled, the critical add-on
      # scheduler reserves resources for critical add-on pods so that they can
      # be rescheduled after a failure.
      # See https://kubernetes.io/docs/tasks/administer-cluster/guaranteed-scheduling-critical-addon-pods/
      priorityClassName: "system-node-critical"
      containers:
      - image: nvidia/k8s-device-plugin
        name: nvidia-device-plugin-ctr
        env:
          - name: FAIL_ON_INIT_ERROR
            value: "false"
        securityContext:
          allowPrivilegeEscalation: false
          capabilities:
            drop: ["ALL"]
        volumeMounts:
          - name: device-plugin
            mountPath: /var/lib/kubelet/device-plugins
      volumes:
        - name: device-plugin
          hostPath:
            path: /var/lib/kubelet/device-plugins

Create the daemonset:

$ kubectl create -f nvidia-device-plugin.yml

Step 8: testing

Create the manifest yaml file gpu-demo.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gpu-demo
spec:
  selector:
    matchLabels:
      app: gpu
  replicas: 2
  template:
    metadata:
      labels:
        app: gpu
    spec:
      containers:
      - name: gpu-demo
        image: nvidia/cuda:10.1-cudnn7-runtime-ubuntu18.04
        command: ["/bin/sh", "-c"]
        args: ["nvidia-smi && tail -f /dev/null"]
        ports:
        - containerPort: 80

Create the deployment:

$ kubectl apply -f gpu-demo.yaml
deployment.apps/gpu-demo created

Tracking pods:

$ kubectl get pods
NAME                        READY   STATUS    RESTARTS   AGE
gpu-demo-78b68dbfb6-wdxns   1/1     Running   0          58s
gpu-demo-78b68dbfb6-wkk25   1/1     Running   0          58s

$ kubectl logs -f gpu-demo-78b68dbfb6-wdxns
Fri Jun 17 11:45:07 2022
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 515.43.04    Driver Version: 515.43.04    CUDA Version: 11.7     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  NVIDIA GeForce ...  On   | 00000000:01:00.0  On |                  N/A |
| 30%   38C    P8    35W / 350W |    544MiB / 24576MiB |     12%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
+-----------------------------------------------------------------------------+

You can get full source code here: custom-resources, gpus.