Jim Cheung

sample app

app.js:

const http = require('http');
const os = require('os');

console.log("Kubia server starting...");

var handler = function(request, response) {
  console.log("Received request from " + request.connection.remoteAddress);
  response.writeHead(200);
  response.end("You've hit " + os.hostname() + "\n");
};

var www = http.createServer(handler);
www.listen(8080);

Dockerfile:

FROM node:7
ADD app.js /app.js
ENTRYPOINT ["node", "app.js"]

introduction

$ kubectl cluster-info

bash completion

$ source <(kubectl completion bash)

create alias

alias k=kubectl

$ source <(kubectl completion bash | sed s/kubectl/k/g)

The simplest way to deploy your app is to use the kubectl run command, which will create all the necessary components without having to deal with JSON or YAML.

$ kubectl run kubia --image=luksa/kubia --port=8080 --generator=run/v1

To make the pod accessible from the outside, you’ll expose it through a Service object. You’ll create a special service of type LoadBalancer

By creating a LoadBalancer-type service, an external load balancer will be created and you can connect to the pod through the load balancer’s public IP.

$ kubectl expose rc kubia --type=LoadBalancer --name kubia-http

$ kubectl get svc

We’re using the abbreviation rc instead of replicationcontroller (po for pods, svc for services).

scale up

$ kubectl scale rc kubia --replicas=3

$ kubectl get rc

$ kubectl get pods
$ kubectl get pods -o wide

$ kubectl describe pod kubia-hczji

dashboard

$ kubectl cluster-info | grep dashboard

$ gcloud container clusters describe kubia | grep -E "(username|password):"

$ minikube dashboard

pods

All pods in a Kubernetes cluster reside in a single flat, shared, network-address space.

which means every pod can access every other pod at the other pod’s IP address.

No NAT (Network Address Translation) gateways exist between them.

Deciding when to use multiple containers in a pod

pod definition

three important sections are found in almost all Kubernetes resources:

more info:

$ kubectl explain pods
$ kubectl explain pod.spec

A basic pod manifest kubia-manual.yaml:

apiVersion: v1
kind: Pod
metadata:
  name: kubia-manual
spec:
  containers:
  - image: luksa/kubia
    name: kubia
    ports:
    - containerPort: 8080
      protocol: TCP
$ kubectl create -f kubia-manual.yaml

$ kubectl get po kubia-manual -o yaml
$ kubectl get pods

$ docker logs <container id>
$ kubectl logs kubia-manual
$ kubectl logs kubia-manual -c kubia

$ kubectl port-forward kubia-manual 8888:8080

$ curl localhost:8888

labels

metadata:
  name: kubia-manual-v2
  labels:
    creation_method: manual
    env: prod             
$ kubectl create -f kubia-manual-with-labels.yaml
$ kubectl get po --show-labels
$ kubectl get po -L creation_method,env

add new label:

$ kubectl label po kubia-manual creation_method=manual

update label:

$ kubectl label po kubia-manual-v2 env=debug --overwrite

listing pods using a label selector

$ kubectl get po -l creation_method=manual

$ kubectl get po -l env
$ kubectl get po -l '!env'

label nodes

$ kubectl label node gke-kubia-85f6-node-0rrx gpu=true

$ kubectl get nodes -l gpu=true

schedule pods to specific node

apiVersion: v1
kind: Pod
metadata:
  name: kubia-gpu
spec:
  nodeSelector:
    gpu: "true"
  containers:
  - image: luksa/kubia
    name: kubia

namespaces

$ kubectl get ns

$ kubectl get po --namespace kube-system

create namespace

apiVersion: v1
kind: Namespace
metadata:
  name: custom-namespace
$ kubectl create -f custom-namespace.yaml

# or 

$ kubectl create namespace custom-namespace

create pod under namespace:

$ kubectl create -f kubia-manual.yaml -n custom-namespace

deleting pod

$ kubectl delete po kubia-gpu

# delete by label

$ kubectl delete po -l creation_method=manual

$ kubectl delete po -l rel=canary

# delete by namespace

$ kubectl delete ns custom-namespace

# delete all under current namespace

$ kubectl delete po --all

Replication and other controllers

liveness probes

Kubernetes can probe a container using one of the three mechanisms:

http get probe:

apiVersion: v1
kind: pod
metadata:
  name: kubia-liveness
spec:
  containers:
  - image: luksa/kubia-unhealthy
    name: kubia
    livenessProbe:
      httpGet:
        path: /
        port: 8080
$ kubectl get po kubia-liveness

$ kubectl logs mypod --previous

$ kubectl describe po kubia-liveness

delay probe

livenessProbe:
     httpGet:
       path: /
       port: 8080
     initialDelaySeconds: 15

If you don’t set the initial delay, the prober will start probing the container as soon as it starts.

even if you set the failure threshold to 1, Kubernetes will retry the probe several times before considering it a single failed attempt. Therefore, implementing your own retry loop into the probe is wasted effort.

If you’re running a Java app in your container, be sure to use an HTTP GET liveness probe instead of an Exec probe, where you spin up a whole new JVM to get the liveness information. The same goes for any JVM-based or similar applications, whose start-up procedure requires considerable computational resources.

ReplicationController

A ReplicationController has three essential parts:

Moving pods in and out of the scope of a ReplicationController

If you change a pod’s labels so they no longer match a ReplicationController’s label selector, the pod becomes like any other manually created pod. It’s no longer managed by anything.

$ kubectl label pod kubia-dmdck type=special
$ kubectl label pod kubia-dmdck app=foo --overwrite

Changing the pod template

$ kubectl edit rc kubia

You can tell kubectl to use a text editor of your choice by setting the KUBE_EDITOR environment variable.

Horizontally scaling pods

$ kubectl scale rc kubia --replicas=10

or $ kubectl edit rc kubia

spec:
  replicas: 3
  selector:
    app: kubia

delete rc but keep its pods running:

$ kubectl delete rc kubia --cascade=false

ReplicaSet

ReplicaSet is a new generation of ReplicationController and replaces it completely.

A ReplicaSet behaves exactly like a ReplicationController, but it has more expressive pod selectors.

Whereas a ReplicationController’s label selector only allows matching pods that include a certain label, a ReplicaSet’s selector also allows matching pods that lack a certain label or pods that include a certain label key, regardless of its value.

apiVersion: apps/v1beta2
kind: ReplicaSet
metadata:
  name: kubia
spec:
  replicas: 3
  selector:
    matchLabels:
      app: kubia
  template:
    metadata:
      labels:
        app: kubia
    spec:
      containers:
      - name: kubia
        image: luksa/kubia

ReplicaSets aren't part of the v1 API, but belong to the apps API group and version v1beta2.

$ kubectl get rs

$ kubectl describe rs

matchExpressions:

selector:
  matchExpressions:
    - key: app
      operator: In
      values:
        - kubia

each expression must contain a key, an operator, and possibly (depending on the operator) a list of values.

You’ll see four valid operators:

Running exactly one pod on each node with DaemonSets

A DaemonSet makes sure it creates as many pods as there are nodes and deploys each one on its own node,

If a node goes down, the DaemonSet doesn’t cause the pod to be created elsewhere. But when a new node is added to the cluster, the DaemonSet immediately deploys a new pod instance to it.

apiVersion: apps/v1beta2
kind: DaemonSet
metadata:
  name: ssd-monitor
spec:
  selector:
    matchLabels:
      app: ssd-monitor
  template:
    metadata:
      labels:
        app: ssd-monitor
    spec:
      nodeSelector:
        disk: ssd
      containers:
      - name: main
        image: luksa/ssd-monitor
$ kubectl create -f ssd-monitor-daemonset.yaml

$ kubectl get ds

$ kubectl label node minikube disk=ssd

Running pods that perform a single completable task

Job resource

apiVersion: batch/v1
kind: Job
metadata:
  name: batch-job
spec:
  template:
    metadata:
      labels:
        app: batch-job
    spec:
      restartPolicy: OnFailure
      containers:
      - name: main
        image: luksa/batch-job

Job pods can’t use the default policy, because they’re not meant to run indefinitely. Therefore, you need to explicitly set the restart policy to either OnFailure or Never.

$ kubectl get jobs

# after job is done
$ kubectl get po -a
$ kubectl logs batch-job-28qf4

Running multiple pod instances in a Job

run five pods sequentially

apiVersion: batch/v1
kind: Job
metadata:
  name: multi-completion-batch-job
spec:
  completions: 5
  template:
    <template is the same as above>

running job pods in parallel:

apiVersion: batch/v1
kind: Job
metadata:
  name: multi-completion-batch-job
spec:
  completions: 5
  parallelism: 2
  template:
    <same as above>

Scaling a Job

You can even change a Job’s parallelism property while the Job is running.

$ kubectl scale job multi-completion-batch-job --replicas 3

limiting the time allowed for a Job pod to complete

A pod’s time can be limited by setting the activeDeadlineSeconds property in the pod spec.

You can configure how many times a Job can be retried before it is marked as failed by specifying the spec.backoffLimit field in the Job manifest. If you don’t explicitly specify it, it defaults to 6.

scheduling Jobs to run periodically or once in the future

Creating a CronJob:

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: batch-job-every-fifteen-minutes
spec:
  schedule: "0,15,30,45 * * * *"
  jobTemplate:
    spec:
      template:
        metadata:
          labels:
            app: periodic-batch-job
        spec:
          restartPolicy: OnFailure
          containers:
          - name: main
            image: luksa/batch-job

It may happen that the Job or pod is created and run relatively late.

You may have a hard requirement for the job to not be started too far over the scheduled time. In that case, you can specify a deadline by specifying the startingDeadlineSeconds field.

apiVersion: batch/v1beta1
kind: CronJob
spec:
  schedule: "0,15,30,45 * * * *"
  startingDeadlineSeconds: 15
  ...

Services

apiVersion: v1
kind: Service
metadata:
  name: kubia
spec:
  ports:
  - port: 80
    targetPort: 8080
  selector:
    app: kubia
$ kubectl get svc
NAME         CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
kubernetes   10.111.240.1     <none>        443/TCP   30d
kubia        10.111.249.153   <none>        80/TCP    6m

$ kubectl exec kubia-7nog1 -- curl -s http://10.111.249.153

you want all requests made by a certain client to be redirected to the same pod every time, you can set the service’s sessionAffinity property to ClientIP (instead of None, which is the default)

apiVersion: v1
kind: Service
spec:
  sessionAffinity: ClientIP
  ...

Exposing multiple ports in the same service

apiVersion: v1
kind: Service
metadata:
  name: kubia
spec:
  ports:
  - name: http
    port: 80
    targetPort: 8080
  - name: https
    port: 443
    targetPort: 8443
  selector:
    app: kubia

When creating a service with multiple ports, you must specify a name for each port.

Discovering services through environment variables

When a pod is started, Kubernetes initializes a set of environment variables pointing to each service that exists at that moment.

$ kubectl exec kubia-3inly env
...
KUBIA_SERVICE_HOST=10.111.249.153
KUBIA_SERVICE_PORT=80
...

Discovering services through DNS

Whether a pod uses the internal DNS server or not is configurable through the dnsPolicy property in each pod’s spec.

root@kubia-3inly:/# curl http://kubia.default.svc.cluster.local
You've hit kubia-5asi2

root@kubia-3inly:/# curl http://kubia.default
You've hit kubia-3inly

root@kubia-3inly:/# curl http://kubia
You've hit kubia-8awf3

You can omit the namespace and the svc.cluster.local suffix because of how the DNS resolver inside each pod’s container is configured:

root@kubia-3inly:/# cat /etc/resolv.conf
search default.svc.cluster.local svc.cluster.local cluster.local ...

Endpoints resource

$ kubectl get endpoints kubia

Manually configuring service endpoints

apiVersion: v1
kind: Service
metadata:
  name: external-service
spec:
  ports:
  - port: 80

Creating an Endpoints resource for a service without a selector

apiVersion: v1
kind: Endpoints
metadata:
  name: external-service
subsets:
  - addresses:
    - ip: 11.11.11.11
    - ip: 22.22.22.22
    ports:
    - port: 80

Creating an ExternalName service

apiVersion: v1
kind: Service
metadata:
  name: external-service
spec:
  type: ExternalName
  externalName: someapi.somecompany.com
  ports:
  - port: 80

After the service is created, pods can connect to the external service through the external-service.default.svc.cluster.local domain name (or even external-service) instead of using the service’s actual FQDN.

ExternalName services are implemented solely at the DNS level—a simple CNAME DNS record is created for the service. Therefore, clients connecting to the service will connect to the external service directly, bypassing the service proxy completely. For this reason, these types of services don’t even get a cluster IP.

Exposing services to external clients

You have a few ways to make a service accessible externally:

NodePort service

apiVersion: v1
kind: Service
metadata:
  name: kubia-nodeport
spec:
  type: NodePort
  ports:
  - port: 80
    targetPort: 8080
    nodePort: 30123
  selector:
    app: kubia
$ kubectl get svc kubia-nodeport

Using JSONPath to get the IPs of all your nodes

$ kubectl get nodes -o jsonpath='{.items[*].status.addresses[?(@.type=="ExternalIP")].address}'

Exposing a service through an external load balancer

If Kubernetes is running in an environment that doesn’t support LoadBalancer services, the load balancer will not be provisioned, but the service will still behave like a NodePort service.

apiVersion: v1
kind: Service
metadata:
  name: kubia-loadbalancer
spec:
  type: LoadBalancer
  ports:
  - port: 80
    targetPort: 8080
  selector:
    app: kubia
$ kubectl get svc kubia-loadbalancer

browser is using keep-alive connections and sends all its requests through a single connection, whereas curl opens a new connection every time.

Services work at the connection level, so when a connection to a service is first opened, a random pod is selected and then all network packets belonging to that connection are all sent to that single pod.

Even if session affinity is set to None, users will always hit the same pod (until the connection is closed).

preventing unnecessary network hops

configuring the service to redirect external traffic only to pods running on the node that received the connection

spec:
  externalTrafficPolicy: Local
  ...

If a service definition includes this setting and an external connection is opened through the service’s node port, the service proxy will choose a locally running pod.

If no local pods exist, the connection will hang. You therefore need to ensure the load balancer forwards connections only to nodes that have at least one such pod.

Using this annotation also has other drawbacks. Normally, connections are spread evenly across all the pods, but when using this annotation, that’s no longer the case.

It also affects the preservation of the client’s IP, because there’s no additional hop between the node receiving the connection and the node hosting the target pod (SNAT isn’t performed).

Exposing services externally through an Ingress resource

One important reason is that each LoadBalancer service requires its own load balancer with its own public IP address, whereas an Ingress only requires one, even when providing access to dozens of services.

an Ingress controller needs to be running in the cluster.

Enabling the Ingress add-on in Minikube

$ minikube addons list

$ minikube addons enable ingress

$ kubectl get po --all-namespaces

creating an Ingress resource

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: kubia
spec:
  rules:
  - host: kubia.example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: kubia-nodeport
          servicePort: 80

Ingress controllers on cloud providers (in GKE, for example) require the Ingress to point to a NodePort service. But that’s not a requirement of Kubernetes itself.

$ kubectl get ingresses

Exposing multiple services through the same Ingress

You can map multiple paths on the same host to different services

...
  - host: kubia.example.com
    http:
      paths:
      - path: /kubia
        backend:
          serviceName: kubia
          servicePort: 80
      - path: /foo
        backend:
          serviceName: bar
          servicePort: 80

Similarly, you can use an Ingress to map to different services based on the host in the HTTP request instead of (only) the path

spec:
  rules:
  - host: foo.example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: foo
          servicePort: 80
  - host: bar.example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: bar
          servicePort: 80

Configuring Ingress to handle TLS traffic

When a client opens a TLS connection to an Ingress controller, the controller terminates the TLS connection.

The application running in the pod doesn’t need to support TLS.

create the private key and certificate:

$ openssl genrsa -out tls.key 2048
$ openssl req -new -x509 -key tls.key -out tls.cert -days 360 -subj /CN=kubia.example.com

$ kubectl create secret tls tls-secret --cert=tls.cert --key=tls.key

Instead of signing the certificate ourselves, you can get the certificate signed by creating a CertificateSigningRequest (CSR) resource.

$ kubectl certificate approve <name of the CSR>

The private key and the certificate are now stored in the Secret called tls-secret

ingress manifest:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: kubia
spec:
  tls:
  - hosts:
    - kubia.example.com
    secretName: tls-secret
  rules:
  - host: kubia.example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: kubia-nodeport
          servicePort: 80

Instead of deleting the Ingress and re-creating it from the new file, you can invoke kubectl apply -f kubia-ingress-tls.yaml, which updates the Ingress resource with what’s specified in the file.

$ curl -k -v https://kubia.example.com/kubia

Although they currently support only L7 (HTTP/HTTPS) load balancing, support for L4 load balancing is also planned.

Signaling when a pod is ready to accept connections

readiness probe is invoked periodically and determines whether the specific pod should receive client requests or not.

Types of readiness probes

Like liveness probes, 3 types of readiness probes exist:

When a container is started, Kubernetes can be configured to wait for a configurable amount of time to pass before performing the first readiness check. After that, it invokes the probe periodically and acts based on the result of the readiness probe. If a pod reports that it’s not ready, it’s removed from the service.

Unlike liveness probes, if a container fails the readiness check, it won’t be killed or restarted. This is an important distinction between liveness and readiness probes.

Liveness probes keep pods healthy by killing off unhealthy containers and replacing them with new, healthy ones,

whereas readiness probes make sure that only pods that are ready to serve requests receive them.

Adding a readiness probe to the pod template

$ kubectl edit rc kubia
apiVersion: v1
kind: ReplicationController
...
spec:
  ...
  template:
    ...
    spec:
      containers:
      - name: kubia
        image: luksa/kubia
        readinessProbe:
          exec:
            command:
            - ls
            - /var/ready
        ...
$ kubectl get po

$ kubectl exec kubia-2r1qb -- touch /var/ready

The readiness probe is checked periodically—every 10 seconds by default.

Understanding what real-world readiness probes should do

Manually removing pods from services should be performed by either deleting the pod or changing the pod’s labels instead of manually flipping a switch in the probe.

If you want to add or remove a pod from a service manually, add enabled=true as a label to your pod and to the label selector of your service. Remove the label when you want to remove the pod from the service.

You should always define a readiness probe, even if it’s as simple as sending an HTTP request to the base URL.

Don’t include pod shutdown logic into your readiness probes, because Kubernetes removes the pod from all services as soon as you delete the pod.

Creating a headless service

Setting the clusterIP field in a service spec to None makes the service headless, as Kubernetes won’t assign it a cluster IP through which clients could connect to the pods backing it

apiVersion: v1
kind: Service
metadata:
  name: kubia-headless
spec:
  clusterIP: None
  ports:
  - port: 80
    targetPort: 8080
  selector:
    app: kubia
$ kubectl exec <pod name> -- touch /var/ready
$ kubectl run dnsutils --image=tutum/dnsutils --generator=run-pod/v1 --command -- sleep infinity

$ kubectl exec dnsutils nslookup kubia-headless