Search Results
Search Results
App Development
GOLD EN GUIDE TO
KUB ERN ETES APPLICATIO N DEVELOPMENT
By Mat thew Pa l m er
Preamble 5
Reference materials 6
Physical implementation diagram 6
Conceptual diagram 7
Glossary 8
Installation 12
Installing Kubernetes and Docker 12
Installation guide 12
Come together 13
Containers 18
Docker and containers 18
Pods 22
Pods in detail 34
Overview 34
Pod lifecycle 34
Advanced configuration 36
Custom command and arguments 36
Environment variables 37
Liveness and Readiness 38
Security Contexts 42
Multi-container pod design patterns 43
Sidecar pattern 44
Adapter pattern 46
Deployments 54
Overview 54
Deployment YAML 56
Rolling updates and rolling back 57
Scaling and autoscaling 60
Services 62
Overview 62
How is a request to a service routed through Kubernetes? 65
Storage 72
Volumes 72
Types of Volumes 73
Persistent Volumes 75
Persistent Volumes 75
Persistent Volume Claims 75
Lifecycle 77
Configuration 78
ConfigMaps 78
Secrets 81
Jobs 84
Overview 84
Jobs 84
CronJobs 87
Resource Quotas 89
Service Accounts 95
Network Policies 96
Networking Overview 96
Network policies 96
The upgraded package gets you to the next level of Kubernetes expertise
through:
• a thirty question, three hour practice exam in the style of the CNCF
CKAD exam
• access to a private GitHub repository where you can view complete
code samples, ask the author questions directly, and participate in a
community of developers learning Kubernetes
• access to full source code, high-res diagrams, and solutions to
exercises
If you originally purchased the base package and would like to upgrade,
email the author at [email protected].
kubectl node
A command-line utility with A worker machine in the
commands to control the Kubernetes cluster, responsible for
Kubernetes cluster actually running pods.
Minikube kubelet
A single-node Kubernetes cluster A process that runs on each
designed to be run locally while worker node, and takes
learning and developing on responsibility for containers that
Kubernetes run on that node.
Cluster kube-proxy
A cluster is a collection of A networking proxy that runs on
computers coordinated to work as each worker node, enables pod-
a single unit. In Kubernetes this to-pod, pod-to-service
consists of a master node and communication.
worker nodes.
cAdvisor
master A process that runs on each
A machine in the Kubernetes worker node that tracks
cluster that directs work to be monitoring and resource usage
done on the worker nodes. information.
etcd YAML
A distributed key-value store used A markup language used to define
in Kubernetes to store configuration for Kubernetes
configuration data for the cluster. objects.
Controller Containerization
Controllers are responsible for The process of packaging an
updating resources in Kubernetes application, its dependencies, and
based on changes to data in etcd. its configuration into a single
deployable unit.
scheduler
A module in the Kubernetes Image
master that selects which worker A template for how to create
node a pod should run on, based running instances of an application
on resource requirements. or system.
Pod ConfigMap
The smallest object that An object used to inject
Kubernetes interacts with. It is a configuration data into containers.
layer of abstraction around the
container, allowing for containers Secret
to be colocated and share An object used to inject secret or
filesystem and network resources. sensitive configuration data into
containers.
Service
A Kubernetes object used to Liveness Probe
expose a dynamic set of pods to A process that checks if a
the network behind a single container is still alive or if it needs
interface. to be restarted.
These instructions written and updated for the latest version of macOS. If
you’re running Linux, you’re probably better than me at installing software
manually, and can figure out—with the help of the internet—how to install
Kubernetes on your specific system.
Installation guide
The only pre-requisite for this guide is that you have Homebrew installed.
Homebrew is a package manager for the Mac. You’ll also need Homebrew
Cask, which you can install after Homebrew by running brew tap
caskroom/cask in your Terminal.
1. Install Docker for Mac. Docker is used to create, manage, and run
our containers. It lets us construct containers that will run in
Kubernetes Pods.
2. Install VirtualBox for Mac using Homebrew. Run brew cask install
virtualbox in your Terminal. VirtualBox lets you run virtual machines
on your Mac (like running Windows inside macOS, except for a
Kubernetes cluster.)
3. Install kubectl for Mac. This is the command-line interface that lets
you interact with Kubernetes. Run brew install kubectl in your
Terminal.
4. Install Minikube via the Installation > OSX instructions from the latest
release. At the time of writing, this meant running the following
command in Terminal: curl -Lo minikube https://
storage.googleapis.com/minikube/releases/v0.27.0/
minikube-darwin-amd64 && chmod +x minikube && sudo mv
minikube /usr/local/bin/
5. Everything should work! Start your Minikube cluster with minikube
start. Minikube will run a Kubernetes cluster with a single node.
Then run kubectl api-versions.
<resource> is how you identify the entity you want the command to be
executed on. You will see the resource identifier written in many different
ways, and each resource type has an abbreviation (pod is po, service is
svc, etc.). All of the following are equivalent, retrieving information about a
pod called example-c7f6:
• kubectl get pods example-c7f6 – the full identifier, useful
because executing kubectl get pods will list all pods, in case you
forget the name of the resource
• kubectl get po example-c7f6 – uses the abbreviation alias po,
meaning pod
• kubectl get pod/example-c7f6 – most useful when copying and
pasting between commands
When learning Kubernetes for the first time, there are a few
extraordinarily useful commands for exploring the Kubernetes API and
YAML options without digging through online resources.
Of that list, you really only take responsibility for your source code. When
you deploy a new version of your application, you just swap out the old
source code for the newer version. The operating system and Node.js
stays in place.
But alarm bells might be ringing: why aren't these images huge and
expensive to run? The image includes the whole operating system and
the Node.js runtime!
There are two analogies that might help depending on your familiarity
with other technologies. React—the JavaScript framework—re-renders all
your UI whenever your application's state changes. Like including an
operating system in your application deployment, this seems like it
should be really expensive. But React gets smart at the other end—it
determines the difference in DOM output and then only changes what is
necessary.
The other analogy is the git version control system, which captures the
difference between one commit and the previous commit so that you can
effectively get a snapshot of your entire project at any point in time.
Docker, React, and git take what should be an expensive operation and
make it practical by capturing the difference between states.
Let's create a Docker image to see how this works in practice. Start a new
directory, and save the following in a file called Dockerfile.
# index.js
var http = require('http');
server.listen(3000, function() {
console.log('Server running on port 3000');
});
In the directory, open a new shell and build the Docker image.
Now that we've built and tagged the Docker image, we can run a
container instantiated from the image using our local Docker engine.
$ docker ps
CONTAINER ID IMAGE COMMAND PORTS
a7afe78a7213 node-welcome-app "node index.js" 0.0.0.0:32772->3000
$ curl 'http://localhost:32772'
Welcome to the Golden Guide to Kubernetes Application Development!
In the previous chapter, we ran our Node.js web server using the docker
run command. Let's do the equivalent with Kubernetes.
First, let's create a simple web server using Node.js. When a request is
made to localhost:3000, it responds with a welcome message. Save this
in a file called index.js.
# index.js
var http = require('http');
server.listen(3000, function() {
console.log('Server running on port 3000');
});
# Dockerfile
FROM node:carbon
WORKDIR /app
COPY . .
CMD [ "node", "index.js" ]
Now we've got to build this image. Take note that we need to make sure
this image is available to the Docker engine in our cluster. If you're
running a cluster locally with Minikube, you'll need to configure your
Docker settings to point at the Minikube Docker engine rather than your
local (host) Docker engine. This means that when we do docker build, the
image will be added to Minikube's image cache and available to
The -t flag specifies the tag for an image—by convention it's the name of
your image plus the version number, separated by a colon.
Now that we've built and tagged an image, we can run it in Kubernetes by
declaring a pod that uses it. Kubernetes lets you declare your object
configuration in YAML or JSON. This is really beneficial for communicating
your deployment environment and tracking changes. If you're not familiar
with YAML, it's not complicated—search online to find a tutorial and you
can learn it in fifteen minutes.
Save this configuration in a file called pod.yaml. We'll cover each field in
detail in the coming chapters, but for now the most important thing to
note is that we have a Kubernetes pod called my-first-pod that runs the
my-first-image:1.0.0 Docker image we just built. It instantiates this
image to run in a container called my-first-container.
While you can create things directly on the command line with kubectl,
one of the biggest benefits of Kubernetes is that you can declare your
deployment environments explicitly and clearly through YAML. These are
simple text files that can be added to your source control repository, and
changes to them can be easily tracked throughout your application's
history. For this reason, we prefer writing our Kubernetes resources'
configuration into a YAML file, and then creating those resources from the
file.
Great! Our pod looks like it's running. You might be tempted to try to
access our container via http://localhost:3000 like we did when we ran
the container directly on our local Docker engine. But this wouldn't work.
Remember that Kubernetes has taken our pod and run its containers on
the Kubernetes cluster—Minikube—not our local machine. So to access
our Node.js server, we need to be inside the cluster. We'll cover
networking and exposing pods to the wider world in the coming chapters.
The kubectl exec command lets you execute a command in one of the
containers in a running pod. The argument -it allows you to interact with
the container. We'll start bash shell on the container we just created
(conceptually, this is kind of similar to SSHing in to your pod). Then we'll
make a request to our web server using curl.
Now let's take a step back and take in the bigger picture of how
Kubernetes works.
Kubernetes unlocks the ability for you to treat many machines as a single
resource—Kubernetes takes control of the cluster so that you can focus
on your application. If a machine within the cluster dies while one of your
containers is doing something, Kubernetes will get a new machine
running, migrate your container onto it, and make sure your container runs
properly.
Components
As an application developer, you need to have a conceptual
understanding of how Kubernetes works.
The master
Other components watch this store for changes—if the actual state
doesn't match the desired state, Kubernetes controllers will make
changes in the system to reconcile them.
You ask two natural questions based on this design pattern. How did the
current state of the cluster and the ideal state diverge? How do I get to
the ideal state? Everything else you need to understand flows from
answering these two questions.
First—how did the current state and the ideal state diverge? Two ways:
the actual state changed, or the ideal state changed.
The ideal state of the cluster changes because you, the application
developer, request it, or because one of the nodes in your cluster
requests it. Kubernetes is told to run a new pod, scale a deployment, or
maybe to add more storage. The request is made through the API server
(kube-apiserver), a process inside the master that gives access to the
Kubernetes API via a HTTP REST API. The API Server will update etcd to
reflect what the new ideal state is.
The actual state of the cluster might change without you requesting it
because a node dies, a container crashes, or some other failure. This is
Kubernetes' power—in the event of a failure, Kubernetes can see what the
state of the system is meant to be, and tries to get back to that state.
if actual_state != ideal_state:
make_changes()
The other core component that runs in master is the scheduler. kube-
scheduler determines which node should run a pod. It finds new pods
that don't have a node assigned, looks at the cluster's overall resource
utilisation, hardware and software policies, node affinity, and deadlines,
and then decides which node should run that pod.
The master maintains the actual and desired state of the cluster using
etcd, lets users and nodes change the desired state via the kube-
apiserver, runs controllers that reconcile these states, and the kube-
scheduler assigns pods to a node to run.
The nodes
While there is only one master machine, there are many node machines
to run pods on. These are the workers—the machines where containers
are deployed and actual work is done. Every node in a Kubernetes cluster
has a container runtime, a Kubernetes node agent, a networking proxy,
and a resource monitoring service.
Every Kubernetes node also runs cAdvisor, a simple agent that monitors
the performance and resource usage of containers on the node.
Objects
Your Kubernetes environment is defined by a collection of objects.
Kubernetes provides many types of objects, all with unique
characteristics, that you combine to perform specific tasks.
All of this configuration information is stored inside etcd, and if the actual
state of Kubernetes diverges from the state declared by this
configuration, work will be done to reconcile them.
The following example explains each field on a pod, the simplest object
in Kubernetes that runs containers.
What is the correct value for this field? It depends! The most up-to-date
version will change as Kubernetes releases new versions, and different
objects move through alpha, beta, and into different API groups. To help
you navigate the changes to the apiVersion field, I've created an up-to-
date web page that lists the correct apiVersion for each object.
While you can run multiple containers in a pod, in reality you often use
one container per pod. When designing your pods, don't force multiple
containers into the same pod—we will discuss a few of the specific
reasons to have multiple containers in a pod in this chapter.
Pod lifecycle
In its lifetime, a pod transitions through several stages. A pod's current
stage is tracked in the status field, which has a subfield called phase.
When a pod's state changes, the kubelet process on that node updates
Pending
The API Server has validated the pod and created an entry for it in etcd,
but its containers haven't been created or scheduled yet.
Running
All of a pod's containers have been created, the pod has been scheduled
on a node in the cluster, and it has running containers.
Succeeded
Every container in the pod has finished executing without returning errors
or failing. The pod is considered to have completed successfully. None of
the containers are going to be restarted.
Failed
Every container has finished executing, but some of them have exited
with a failure code.
Unknown
The pod's status could not be obtained.
In the output of kubectl get pods -o yaml, we see the pod is in the
Pending phase of its lifecycle—it has been accepted by Kubernetes but
hasn't had any of its containers created yet or been successfully
scheduled. In the event logs from kubectl describe pods, Kubernetes
has tried to schedule the pod but failed to get its image—the image has
an invalid name that can't be found anywhere. This gets reflected into the
pod's status.containerStatuses[0].state field, which captures the
last error from Kubernetes.
Advanced configuration
args replaces the image's CMD. This list of arguments is passed to the
command specified in the previous field. Often in this book, you'll see args
set to ["-c", "<some command>"]. This allows us to define shell
commands in YAML that are executed when the container runs. Since
pods are only alive for as long as the first command is running, these will
often run in an infinite loop so that we can inspect the running containers
later. This is a useful debugging tool in case you don't have a suitable
Docker image to deploy.
kind: Pod
apiVersion: v1
metadata:
name: custom-command-pod
spec:
containers:
- name: command-container
image: alpine
command: ["/bin/sh"]
args: ["-c", "while true; do date; sleep 5; done"]
Environment variables
When running your application, you're likely to want to use environment
variables to dynamically inject configuration settings into your containers.
While it's also recommended to use ConfigMaps to accomplish this,
which are covered in a later chapter, you can also set these values
directly in YAML.
Container probes are small processes that run periodically. The result of
this process determines Kubernetes' view of the container's state—the
result of the probe is one of Success, Failed, or Unknown.
You will most often use container probes through Liveness and Readiness
probes. Liveness probes are responsible for determining if a container is
running or when it needs to be restarted. Readiness probes indicate that a
container is ready to accept traffic. Once all of its containers indicate they
are ready to accept traffic, the pod containing them can accept requests.
There are three ways to implement these probes. One way is to use HTTP
requests, which look for a successful status code in response to making a
request to a defined endpoint. Another method is to use TCP sockets,
which returns a failed status if the TCP connection cannot be established.
The final, most flexible, way is to define a custom command, whose exit
code determines whether the check is successful.
Readiness Probes
In this example, we've got a simple web server that starts serving traffic
after some delay while the application starts up. If we didn't configure our
readiness probe, Kubernetes would either start sending traffic to the
kind: Pod
apiVersion: v1
metadata:
name: liveness-readiness-pod
spec:
containers:
- name: server
image: python:2.7-alpine
Start the pod, and we'll inspect its event log—notice how Kubernetes
thinks the pod is unhealthy. In reality, we just have an application that
takes a little while to boot up!
Let's build off the previous example and add a liveness probe for our web
server container. Instead of using an httpGet request to probe the
container, this time we'll use a command whose exit code indicates
whether the container is dead or alive.
Now, delete and recreate the new pod. Then, inspect its event log. We'll
see that our changes to the readiness probe have worked, but then the
liveness probe finds that the container doesn't have the required file so
the container will be killed and restarted.
Security Contexts
A little-used but powerful feature of Kubernetes pods is that you can
declare its security context, an object that configures roles and privileges
for containers. A security context can be defined at the pod level or at the
container level. If the container doesn't declare its own security context, it
will inherit from the parent pod.
Security contexts generally configure two fields: the user ID that should
be used to run the pod or container, and the group ID that should be used
for filesystem access. These options are useful when modifying files in a
volume that is mounted and shared between containers, or in a persistent
volume that's shared between pods. We'll cover volumes in detail in a
coming chapter, but for now think of them like a regular directory on your
computer's filesystem.
To illustrate, let's create a pod with a container that writes the current
date to a file in a mounted volume every five seconds.
Notice that the created file has the user ID 45 and the group ID 231, both
taken from the security context. This is useful for auditing, reviewing
where files come from, and also to control access to files and directories.
Sidecar pattern
The sidecar pattern consists of a main application—i.e. your web
application—plus a helper container with a responsibility that is useful to
your application, but is not necessarily part of the application itself. The
most common sidecar containers are logging utilities, sync services,
watchers, and monitoring agents. It wouldn't make sense for a logging
container to run while the application itself isn't running, so we create a
pod composed of the main application and the sidecar container. Moving
the logging work to another container means that any faults are isolated
to that container—failure won't bring down the main application.
# Mount the pod's shared log file into the app container.
volumeMounts:
- name: shared-logs
mountPath: /var/log
# Sidecar container
- name: sidecar-container
# Simple sidecar: display log files using nginx.
image: nginx:1.7.9
ports:
- containerPort: 80
The monitoring agent can only accept output in the format [RUBY|NODE]
- [HOST] - [DATE] - [DURATION]. We could force the applications to
write output in the format we need, but that burdens the application
developer, and there might be other things depending on this format. The
better alternative is to provide adapter containers that adjust the output
into the desired format. Then the application developer can simply
update the pod definition to add the adapter container and they get this
monitoring for free.
containers:
# Adapter container
- name: adapter-container
# This sidecar container takes the output format of
# the application, simplifies and reformats it for
# the monitoring service to come and collect.
# In this example, our monitoring service requires
# status files to have the date, then memory usage,
# then CPU percentage each on a new line.
One of the best use-cases for the ambassador pattern is for providing
access to a database. When developing locally, you probably want to use
your local database, while your test and production deployments want
different databases again. Managing which database you connect to
could be done through environment variables, but will mean your
application changes connection URLs depending on the environment. A
better solution is for the application to always connect to localhost, and
let the responsibility of mapping this connecting to the right database fall
to an ambassador container. In other situations, the ambassador could be
sending requests to different shards of the database—the application
itself still doesn't need to worry.
Namespaces
The first useful tool for managing access to resources in your cluster is
namespaces. Namespaces allow you to subdivide a single physical
cluster into several virtual clusters, each of which looks like the original
but is completely isolated from the others. One of these virtual clusters is
called a namespace.
Why subdivide a cluster like this? You can define resource limits on each
namespace, which prevents users in that namespace from exceeding
CPU, disk, or object limits. Namespaces also isolate resources, which
means that resources created by one team in your company won't be
affected by resources created by a different team.
Using namespaces
If you use namespaces very rarely, or you want to be explicit when using
them, simply append the --namespace=NAMESPACE_NAME argument to the
kubectl command you want to execute. This is a simple, explicit, and easy
to remember approach.
Labels
Kubernetes lets you associate key-value pairs to objects to help you
identify and query them later. These key-value pairs are called labels.
While they might not seem important at first, they’re vital for grouping
objects in Kubernetes. For example, if you want to group pods together
into a service, you need labels and a label selector. In your system, expect
that many objects will carry the same labels.
Set labels for an object inside the labels dictionary in the object's
metadata property. Like dictionary entries in programming languages,
Kubernetes labels consist of both a key and a value.
spec:
# ...
Selectors
Selectors are the counterpart to labels that make labels useful. They are
the mechanism for performing queries based on objects' labels. Selectors
let you select a group of objects based on the key-value pairs that have
been set as their labels. You can think of selectors as the WHERE part of a
SELECT * from pods WHERE <labels> = <values>. You use label
selectors from the command line or in an object’s YAML when it needs to
select other objects.
Label queries specify a key and value to use for matching objects. You
can query for things that have that value (the = operator) or things that do
not have that value (the != operator). Combine multiple filters using a
comma (,), which is the equivalent of && in programming languages.
Label queries can filter on a set of values using the in, notin, and exists
keywords.
# Set-based filters
kubectl get all --selector='provider in (kubernetes,utils,mail)'
Selectors in YAML
For application developers, there are two important uses for label
selectors: in services and in deployments.
We'll cover services in depth in a coming chapter, but one of the most
common uses of labels and selectors is to group pods into a service. The
selector field in a service's spec defines which pods receive requests
sent to that service. The service runs a query using the key-value label
pairs specified in this field to find pods it can use. If your pods' labels are
incorrectly defined, your services won't send traffic to the correct pods!
In the following example, the service will route requests to pods that have
key equal to value, and key2 equal to value2. Using the command-line
syntax, this is equivalent to kubectl get pods --selector='key =
value, key2 = value2'.
Annotations
Labels and selectors have a functional purpose. Labels are key-value
pairs set on an object. Selectors are queries that search for objects based
on those key-value pairs. Because they're used in queries and sorting,
labels need to be short and simple.
apiVersion: v1
kind: Pod
metadata:
name: my-pod
labels:
# ...
annotations:
commit: 6da32faca
logs: 'http://logservice.com/oirewnj'
contact: 'Matthew Palmer <[email protected]>'
spec:
# ...
Now simulate one of the application pods failing, and watch the
Kubernetes deployment created a new pod in its place.
Deployment YAML
One of the main appeals of Kubernetes is that you can declare your
desired system state through configuration files which can be tracked,
versioned, and easily understood. Like pods, deployments are declared
through YAML files.
At first, a deployment's YAML file looks more confusing than the example
YAML configuration we've seen so far. But this is only because it nests a
pod specification inside the deployment's configuration—the
deployment.spec.template field is in fact the same configuration we've
seen for all the pods we've created so far.
kind: Deployment
apiVersion: extensions/v1beta1
metadata:
name: nginx-deployment
spec:
# A deployment's specification really only
# has a few useful options
Recreate
With the Recreate strategy, all running pods in the deployment are killed,
and new pods are created in their place. This does not guarantee zero-
downtime, but can be useful in case you are not able to have multiple
versions of your application running simultaneously.
RollingUpdate
The preferred and more commonly used strategy is RollingUpdate. This
gracefully updates pods one at a time to prevent your application from
going down. The strategy gradually brings pods with the new
configuration online, while killing old pods as the new configuration
scales up.
Rolling back
A deployment's entire rollout and configuration history is tracked in
Kubernetes, allowing for powerful undo and redo functionality. You can
easily rollback to a previous version of your deployment at any time.
kind: Deployment
apiVersion: extensions/v1beta1
metadata:
name: nginx-deployment
spec:
replicas: 5
strategy:
type: RollingUpdate
rollingUpdate:
# Allow up to seven pods to be running
# during the update process
maxSurge: 2
# Require at least four pods to be running
# during the update process
maxUnavailable: 1
selector:
matchLabels:
app: nginx
template:
metadata:
name: nginx
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
Now create the deployment, and check out the deployment revision log
using kubectl rollout.
We can see that the first version of our deployment has been deployed
and rolled out.
Let's update our deployment to use a more modern version of nginx, the
Docker image nginx:1.15.0. Watch as Kubernetes scales down the old
replicas of the pod, and brings online replicas of the pod that has the
updated version of nginx.
Let's first inspect our current revision, then compare it to the previous
revision, and finally undo the new deployment.
Manual scaling
Autoscaling
The Horizontal Pod Autoscaler is a new resource in modern versions of
Kubernetes that manages the number of replicas in your deployment
automatically. The autoscaler automatically scales the number of pods
based on resource utilization. This was originally limited to tracking CPU
utilization, but recently gained the ability to scale based on memory
usage and custom metrics (one common use-case is to scale pods based
on the number of items in a work queue).
kind: Service
apiVersion: v1
metadata:
name: hostname-service
spec:
# Expose the service on a static port on each node
# so that we can access the service from outside the cluster
type: NodePort
selector:
app: echo-hostname
ports:
# Three types of ports for a service
# nodePort - a static port assigned on each the node
# port - port exposed internally in the cluster
# targetPort - the container port to send requests to
- nodePort: 30163
port: 8080
targetPort: 80
Now, we'll create two pods that the service will use to handle network
traffic. Pay particular attention to the relationship between the service's
selector field and the pods' labels—this is how the service discovers the
pods it can use.
kind: Pod
apiVersion: v1
metadata:
name: hostname-pod-101
labels:
# These labels are used by the service to select
# pods that can handle its network requests
app: echo-hostname
app-version: v101
spec:
containers:
- name: nginx-hostname
image: kubegoldenguide/nginx-hostname:1.0.1
ports:
# nginx runs on port 80 inside its container
- containerPort: 80
---
kind: Pod
apiVersion: v1
metadata:
name: hostname-pod-102
labels:
app: echo-hostname
app-version: v102
spec:
containers:
- name: nginx-hostname
image: kubegoldenguide/nginx-hostname:1.0.2
ports:
- containerPort: 80
$ curl 'http://192.168.99.100:30163'
Hostname: dc534047acbb
Now that we know it's working, let's backtrack and understand what
happens when we're making these requests.
If you are accessing the service from outside the cluster (say, using curl
from a normal shell on your computer):
• 10.100.213.239:8080, 10.100.213.239:30163, or anything using the
10.100.213.239 IP address will not work. This is because that IP
address is internal to the cluster—it will only work from a pod (or
directly on a node) inside the cluster.
• You need to find the cluster's IP address, and access the service
through that. Running a local Kubernetes cluster with minikube, you
can use minikube dashboard --url to find the IP address of your
cluster. In the default scenario, this is 192.168.99.100.
• The only address that works for accessing the service from outside
the cluster is 192.168.99.100:30163. This is the cluster's IP address
and the external NodePort that was set up when Kubernetes
created the service.
Let's create a deployment and service that work together. We'll create
two deployments of the nginx-hostname application from the previous
example. The first deployment is the original application version, 1.0.1,
scaled to three replicas, and the second deployment is a single replica of
version 1.0.2 of the application.
Now, create the service, which will forward network requests to the
service to any pod with the label app set to hostname. In our case, we
have four such pods created via our deployments—three of them with the
original application version, and one with the new application version.
We've now created three replicas of the first version of our application, a
single replica of the second version, and exposed all of these to the
network through a single unified interface. Let's access our deployed
$ curl 'http://192.168.99.100:30412'
Hostname: dc534047acbb, date: Sat Jun 16 21:57:15 UTC 2018
$ curl 'http://192.168.99.100:30412'
Hostname: dc534047acbb
How does Kubernetes solve this so you can keep data across container
restarts? Pods can define their volumes—filesystems that exist as long as
the pod lives. These are mounted into a container, which provides a
directory the container can read and write to.
A volume provides storage to your pod and the containers running inside
it. The storage will live for as long as the pod lives—data will survive
across container restarts but will be lost when the pod is destroyed.
Further, this storage can be shared across containers, so if two containers
are running in the same pod, they will be able to share files using the
volume.
Within the container, volumes are mounted at a specific path. It's like a
symbolic link from the container to the volume within the pod. A volume
can also define permissions around read and write access. From there,
the container can use that directory however they like.
kind: Pod
apiVersion: v1
metadata:
name: simple-volume-pod
spec:
# Volumes are declared at the pod level. They share its
# lifecycle and are communal across containers inside it.
volumes:
# Volumes have a name and configuration
# based on the type of volume.
# In this example, we use the emptyDir volume type
- name: simple-vol
emptyDir: {}
Types of Volumes
In the above example, we used the simple emptyDir volume type. There
are dozens of types of volumes you can use—each with different settings
—that you can use as-needed. Every application developer uses a few
key volume types in their career.
emptyDir
An emptyDir volume is an empty directory created in the pod. Containers
can read and write files in this directory. The directory's contents are
removed once the pod is deleted. This is often the right volume type for
nfs
nfs volumes are network file system shares that are mounted into the
pod. An nfs volume's contents might exist beyond the pod's lifetime—
Kubernetes doesn't do anything in regards to the management of the nfs
—so it can be a good solution for sharing, pre-populating, or preserving
data.
hostPath
hostPath volumes directly mount the node's filesystem into the pod.
While these are often not used in production since—as developers—we
prefer not to worry about the underlying node, they can be useful for
getting files from the underlying machine into a pod.
Persistent Volumes
A persistent volume is a piece of storage in your Kubernetes cluster, often
provisioned by a Kubernetes administrator. Its defining characteristic is
that it is storage that persists beyond containers, pods, and node restarts.
In the same way that a node is a resource, a persistent volume is also a
resource: it can be consumed by pods via Persistent Volume Claims.
When it comes to use the persistent volume, a pod does so via the
persistentVolumeClaim volume type. From there, it works like any other
volume!
image: alpine
command: ["/bin/sh"]
args: ["-c", "while true; do date >> /var/forever/
file.txt; sleep 5; done"]
ConfigMaps
A ConfigMap is a key-value dictionary whose data is injected into your
container's environment when it runs. The first step to using a ConfigMap
is to create the ConfigMap resource inside your Kubernetes cluster. The
next step is for a pod to consume it by mounting it as a volume or getting
its data injected through environment variables.
Creating a ConfigMap
ConfigMaps are very flexible in how they are created and consumed. You
can,
• create a ConfigMap directly in YAML — this is our preferred solution
• create a ConfigMap from a directory of files
• create a ConfigMap from a single file
• create a ConfigMap from literal values on the command line
If your application has specific needs, it is well worth searching online for
alternative usages of ConfigMaps. In our case, we prefer to keep our
configuration settings tracked in version control and consistent with the
rest of our Kubernetes resources by defining them directly in YAML.
We can then create this ConfigMap like any other Kubernetes resource—
kubectl apply -f configmap.yaml.
kind: Pod
apiVersion: v1
metadata:
name: pod-using-configmap
spec:
# Add the ConfigMap as a volume to the Pod
volumes:
# `name` here must match the name
# specified in the volume mount
- name: example-configmap-volume
# Populate the volume with config map data
configMap:
# `name` here must match the name
# specified in the ConfigMap's YAML
name: example-configmap
containers:
- name: container-configmap
image: nginx:1.7.9
# Mount the volume that contains the configuration data
# into your container filesystem
volumeMounts:
# `name` here must match the name
# from the volumes section of this pod
- name: example-configmap-volume
mountPath: /etc/config
kind: Pod
apiVersion: v1
metadata:
name: pod-env-var
spec:
containers:
- name: env-var-configmap
image: nginx:1.7.9
envFrom:
- configMapRef:
name: example-configmap
We can access and view the injected environment variables once the pod
is running.
Secrets
If you understand creating and consuming ConfigMaps, you also
understand how to use Secrets. The primary difference between the two
is that Secrets are designed to hold sensitive data—things like keys,
tokens, and passwords.
Kubernetes will only send Secrets to a node when one of the node's pods
explicitly requires it, and removes the Secret if that pod is removed. Plus,
a Secret is only ever visible inside the Pod that requested it.
Creating a Secret
As we did with a ConfigMap, let's create a YAML file to configure secrets
for an API access key and token. When adding Secrets via YAML, they
must be encoded in base64. base64 is not an encryption method and
does not provide any security for what is encoded—it's simply a way of
presenting a string in a different format. Do not commit your base64-
encoded secrets to source control or share them publicly.
Encode strings in base64 using the base64 command in your shell, or with
an online tool like base64encode.org.
Now, add these encoded strings to the YAML file for our Secret.
kind: Secret
apiVersion: v1
metadata:
name: api-authentication-secret
type: Opaque
data:
key: T1VSX0FQSV9BQ0NFU1NfS0VZCg==
token: U0VDUkVUXzd0NDgzNjM3OGVyd2RzZXIzNAo=
Using the Secret is then the same as using a ConfigMap, just with slightly
different field names.
kind: Pod
apiVersion: v1
metadata:
name: pod-using-secret
spec:
# Add the Secret as a volume to the Pod
volumes:
# `name` here must match the name
# specified in the volume mount
- name: api-secret-volume
# Populate the volume with config map data
secret:
# `secretName` here must match the name
# specified in the secret's YAML
secretName: api-authentication-secret
containers:
- name: container-secret
image: nginx:1.7.9
volumeMounts:
# `name` here must match the name
# from the volumes section of this pod
- name: api-secret-volume
mountPath: /etc/secret
Jobs
Jobs are the core mechanism for executing finite tasks in Kubernetes. A
Job creates one or more Pods, and guarantees that a specified number of
them complete successfully. Common real-world use-cases of Jobs are
batch processing, long-running calculations, backup and file sync
operations, processing items from a work queue, and file upload to
external services (for example, a monitoring or log collection service).
There are three types of jobs: non-parallel, parallel with fixed completion
count, and parallel with a work queue. These are characterised by the
number of pods they create, what their "completed" state looks like, and
the degree of parallelism when they execute.
Using Jobs
Let's create a job that illustrates how Kubernetes ensures your tasks will
run successfully. Our job simulates a dice roll, and if we get a six, the job
is successful. Any other number means the task has failed.
# The spec for the pod that is created when the job runs.
template:
metadata:
name: dice-pod
spec:
# Pods used in jobs *must* use a restart
# policy of "OnFailure" or "Never"
# OnFailure = Re-run the container in the same pod
# Never = Run the container in a new pod
restartPolicy: Never
Now let's create the job and live-tail the output to see how Kubernetes
will automatically recreate pods until we get one whose dice roll was a six
and the pod had status Completed.
One caveat for CronJobs is that, in rare cases, more than one Job might
get created for a single CronJob task. Ensure your CronJobs can be run
repeatedly but produce the same result each time.
cron syntax
Don't worry if you find cron syntax strange or confusing. Just have a basic
understanding and then use online tools like cronmaker.com to write your
scheduling configuration for you. (Although, if you're taking the Certified
Kubernetes Application Developer Exam, it's worth knowing some cron
syntax since you can't use online tools in the exam environment.)
Using CronJobs
Let's create a CronJob that'll ping Google every minute and check that
their site hasn't gone down. Notice that a CronJob includes two nested
configuration templates. First, it reuses the Job spec configuration, and
that Job spec configuration reuses the Pod spec configuration.
Now let's create the CronJob and watch it work. Don't forget to kill the
CronJob afterwards!
A resource quota limits the amount of resources that namespace can use.
Resource quotas can limit anything in the namespace, including the total
count of each type of object, the total storage used, and the total memory
or CPU usage of containers in the namespace.
When the Kubernetes scheduler selects a node for a pod to run in, it
looks at the sum of CPU and memory requests for each container in that
pod to see if it will be able to run. As a result, it is a good practice to set
both of these for your pods, even if you expect your pod's resource
consumption to be relatively light. This will allow Kubernetes to
intelligently schedule pods; if you don't, some pods might never be
successfully scheduled because other pods are consuming more than
they strictly need.
kind: ResourceQuota
apiVersion: v1
metadata:
name: red-team-resource-quota
# Important! Define the resource quota for the namespace.
namespace: red-team
spec:
# The set of hard limits for each resource
# This is a map of resource type to request/limit
hard:
# Resource constrains can be set up for "counts" of objects.
# In this case, limit the count of pods to five.
pods: 5
kind: Pod
apiVersion: v1
metadata:
name: pod-illegal
# Important! Create this pod in the right namespace.
namespace: red-team
spec:
# This pod will fail to be created because it doesn't have
# the `request` or `limit` property set -- this is required by
# our resource quota.
containers:
- name: nginx-illegal
image: nginx:1.7.9
We can see that containers must define their resource limits if the
resource quota is defined. Let's create two pods, the first will use the
majority of the resource quota.
Now create the pod, and then inspect our resource quota to see the
effects.
The second pod requests at least 512MB of memory to run. When this
request is added to the other requests in the namespace—namely the
768MB required by the first pod—it will exceed the memory requests limit
defined by the resource quota.
It is also useful to look at the "events" section of the pod that is being
created—it can contain useful information as to why scheduling might
have failed.
One final command that gives an overview of the entire cluster is kubectl
describe nodes. This lets you inspect all of the nodes running in your
cluster, and see what limits might be breached. This is an extremely
useful debugging tool because it displays the CPU and memory
resources across all namespaces—it might be that the node itself doesn't
have space for your pod to be scheduled because of what's happening in
other namespaces.
Every pod in your Kubernetes cluster has a Service Account it uses. Most
of the time, this is set to default. If you need to change the Service
Account, simply set the pod's spec.serviceAccountName field to the
name of the Service Account you want to use.
kind: ServiceAccount
apiVersion: v1
metadata:
name: my-service-account
kind: Pod
apiVersion: v1
metadata:
name: use-service-account-pod
spec:
# Set the service account containers in this pod
# use when they make requests to the API server
serviceAccountName: my-service-account
containers:
- name: container-service-account
image: nginx:1.7.9
To see which service account a pod is using, run kubectl get pods
<pod> -o yaml and look for the pod's spec.serviceAccount field.
The other important detail to note is that service accounts are defined
per-namespace, while user accounts are not namespaced.
1. Containers on the same pod can talk to each other via localhost
2. Pods on the same node can talk to each other via a pod's IP address
3. Pods on different nodes can talk to each other via a pod's IP address
Network policies
By default, every pod can communicate directly with every other pod via
an IP address. This is one of the major benefits of Kubernetes' networking
model. However, you might want to restrict how groups of pods can
communicate with each other. Network Policies let you group pods
Debugging
Debugging your Kubernetes object configuration relies heavily on using a
few key commands combined with the experience of having seen things
break in a similar way in the past. The benefit of Kubernetes—compared
to ad-hoc deployment systems—is that it breaks in a predictable way.
Once you've seen a certain type of failure and you know how to fix it, you
can expect that resolution to work in other situations.
kubectl logs and kubectl logs --previous get you the output of a
given pod, and the output of a previously-run pod respectively.
Finally, given that a pod is running, kubectl exec -it <pod> sh gets
you shell access inside that pod. From there, you'll often find yourself
curling localhost, other containers, other pods and other services to
check that they're running.
Pods
If the pod isn't running…
• Run kubectl describe pod <pod> and kubectl get pods. Check
the pod's status.
• CrashLoopBackOff means that the pod runs a container that
immediately exits. This is commonly caused by a misconfiguration or
invalid image.
Services
If a service doesn't seem to work…
• Make sure you understand and are using the correct service type—
the default is clusterIP, where the service is only exposed inside
the cluster
• Use kubectl exec -it <pod> sh to get a shell in a pod inside the
cluster, then try to curl one of the pod's directly. kubectl get pods
-o wide gets you the IP address of all the pods in the cluster. Then
try to curl the service. This helps you diagnose if the pod is
misconfigured or if the service is misconfigured.
Monitoring
Monitoring for your Kubernetes cluster needs to be set up at the
container, pod, service, node, and cluster level.
Please note that I can’t give out specific details because of confidentiality
agreements with the CNCF signed before taking the exam. I’ll do my best
to share my experience in a suitable way.
If you purchased the upgraded version of this book, you have access to
the Github repo with a practice exam, code samples, and complete
solutions.
Background
The CKAD exam is a two hour practical exam that tests your knowledge,
skill, and ability as a developer using Kubernetes. The exam is oriented
towards testing this ability. Questions focus on using, debugging,
monitoring, and accessing Kubernetes.
Exam style
The test involves interacting with a live Kubernetes cluster via the
command line. This occurs in a web-based command-line environment
where you need to execute kubectl commands, operate a Unix shell, and
edit YAML files.
As you’d expect, the exam follows the syllabus and curriculum closely.
There are roughly twenty topics in the curriculum, and roughly that many
questions in the exam. The exam covers almost every topic in the
syllabus—make sure you’re familiar with everything when preparing.
Assumed skills
First of all, you will need to be comfortable navigating a Unix command
line environment. The entire test takes place in a web application that
simulates a Unix command line. You will need to be able to navigate a
Unix shell, know a command-line text editor, and use Unix programs to
complete questions.
One large annoyance for me was that I don’t usually edit my YAML files in
Vim. This was a problem when I wanted to bulk-indent fifteen lines but
couldn’t remember how to do it. I had to type space-space fifteen times—
a huge time-sink for very little value. Make sure you’re fast at navigating
Unix and editing YAML in a terminal text editor.
You will need to be able to create Kubernetes objects through both YAML
and via kubectl commands!
Content
There were no content-related surprises to my exam—what’s in the
curriculum is what gets tested. If something seems like something that an
administrator would set up on a cluster (for example, a resource quota),
it’s more likely that you’ll need to consume or use that resource, rather
than needing to know how to create it yourself. Remember that this is an
exam for application developers.
I think there are three priority tiers of content to learn for the exam. The
first tier requires deep understanding and experience, and will be tested
multiple times. The second tier you need to understand and use, but it is
likely to appear fewer times. The third tier are simpler concepts that you
are likely to simply use or consume, and they only appear once in the
exam.
Sample questions
I can’t give out any specific questions, but to give you a sense of what the
questions are like, here are a couple of sample questions. More sample
questions and complete solutions are available in the Github repo for the
upgraded package.
Note that these sample questions are easier than what’s in the exam, but
they give you a hint of the style of the questions.
Sample question 1
Create a deployment of version 1.7.1 of the nginx image. This deployment
should have five pods running the nginx image. Create the deployment in
the question-four namespace. Save the YAML configuration to create this
deployment in a file called deploy.yaml.
Sample question 2
A pod called question-five-pod is failing to run in our Kubernetes cluster
in the question-five namespace. Query the Kubernetes cluster to
determine why the pod isn’t starting, and save any relevant events to a file
called query-results.txt. Then, fix the pod’s configuration so that it starts
correctly.
Other advice
Use the practice exam
Purchasers of the upgraded package of this book have access to a Github
repo with a practice exam. Make sure you attempt this exam in a realistic
environment: use only the command line, kubectl, and kubernetes.io,
complete it under a time limit. Afterwards, check your solutions and re-do
anything you weren't sure about.
While navigating the docs is important, the faster (and my preferred) way
is to use kubectl’s inbuilt documentation to quickly find information you
need.
The terminal environment you have to use is pretty terrible. It can be quite
laggy and doesn’t have the autocompletion functionality you might be
used to. Try to find the best internet connection you can to take the exam
—a laggy environment is really disruptive.
Practice exam
If you've purchased the upgraded book package, you have access to a
Github repository containing a practice exam to help you pass your official
Certified Kubernetes Application Developer exam.
This is a practice exam designed to help you prepare for the CKAD exam.
It's a collection of sample questions around the topics in the CKAD exam
curriculum.
I've talked to many people who have taken the exam, and the number
one thing they struggle with is time. Following that, navigating the
environment and using command-line editors is a pain point.
This practice exam is based around the publicly available CKAD exam
curriculum. I cannot and won't share specific questions from the real
exam, it's simply a set of questions I've put together based on talking to
people who are taking the exam. I cannot and will not help you cheat.
This practice exam has thirty questions. You should aim to complete them
all in three hours. The real exam only goes for two hours, and you should
expect around twenty questions.
You might find this time limit restricting—this is intentional. Most people
struggle with time constraints when they take the real exam. Do your best
to work through each question and don't cheat—if you aren't moving fast
enough, you want to find out now, not in the middle of your exam!
The solutions are not the only way to complete the question, just the way
that I would have done it. I'd love for you to post more solutions in the
Github repo!