Copyright © 2021 by Learnk8s
All rights reserved.
No part of this book may be reproduced or used in any
manner without written permission of the copyright owner
except for the use of quotations in a book review. For more
information, address: hello@learnk8s.io.
Acknowledgements
This book is dedicated to:
My wife and daughter — you are the love of my life.
The younger myself. I wish you could have read this book
instead of spending countless hours reading internet
tutorials.
All the instructors at Learnk8s that took the time to
discuss and offer constructive criticism.
All students that listened to my workshops and asked
questions. You made me realise how much there is to learn.
Sasha. You are the best dog I've ever had.
4
Table of contents
1. Preface 13
Prerequisite knowledge 15
In this journey, you are not alone 16
2. Infrastructure, the past, present and future 17
Managing infrastructure at scale 18
Virtual machines 20
Resources utilisation and efficiency 23
Poor resource allocation 27
Packaging and distribution 33
Recap 40
3. The containers revolution 42
Docker and containers 44
Linux containers 47
Packaging and distributing Docker images 52
Linux containers vs virtual machines 56
Containers and development lifecycle 59
5
Recap 61
4. Managing containers at scale 64
Managing containers allocations & placements 68
Containers, networking and service discovery 70
Running containers at scale at Google 71
Data centre as a computer 73
Kubernetes as a scheduler 76
Kubernetes as an API 77
Kubernetes, what it is and what it is not 81
From containers to Kubernetes 83
5. Deploying apps on Kubernetes 84
Watching over Pods 91
Creating resources in Kubernetes 93
Kubernetes resources in YAML 95
Recap 98
6. Setting up a local environment 100
Your first deployment 106
6
Inspecting YAML resources 110
Exposing the application 118
1. Creating a Kubectl-portward tunnel 124
2. Creating a tunnel with minikube tunnel 124
3. Accessing the cluster IP address 126
Exploring the cluster with Kubectl 127
Recap 131
7. Self-healing and scaling 132
Practising chaos engineering 133
Scaling the application 138
Scaling with the Service 142
Recap 143
8. Creating the app end-to-end 145
Creating the application 147
Containerising the application 148
Running the container 153
Uploading the container image to a container 155
registry
7
Sharing resources 159
Deploying the app in Kubernetes 166
Defining a Service 172
Defining an Ingress 178
Deploying the application 181
1. Creating a Kubectl-portward tunnel 184
2. Creating a tunnel with minikube tunnel 184
3. Accessing the cluster IP address 185
Recap and tidy-up 186
9. Practising deployments 188
Version 2.0.0 189
Deployment 191
Service 191
Ingress 192
Namespace 193
Version 3.0.0 193
If you are stuck 195
8
10. Making sense of Deployments, Services and 196
Ingresses
Connecting Deployment and Service 200
Connecting Service and Ingress 205
Recap 209
11. Installing Docker, Minikube and kubectl 210
macOS 211
Installing Homebrew 211
Installing Docker on macOS 212
Testing your Docker installation 213
Installing minikube on macOS 214
Windows 10 216
PowerShell prerequisites 216
Installing Chocolatey 217
Installing Docker on Windows 10 217
Installing minikube on Windows 10 222
Ubuntu 224
Installing kubectl on Ubuntu 224
9
Installing minikube on Ubuntu 225
12. Mastering YAML 228
Indentation 229
YAML maps 230
YAML lists 232
YAML data types 234
Strings 234
Numeric types 236
Boolean types 237
Null 238
Multiple documents 239
Comments 240
Snippets 241
Debugging tips 242
Navigating paths 243
Navigating maps 244
Navigating lists 245
10
yq 246
Merging YAML files 250
13. Kubectl tips and tricks 254
1. Save typing with command completion 255
How command completion works 255
Bash on Linux 256
Bash on macOS 259
Zsh 262
2. Quickly look up resource specifications 263
3. Use the custom columns output format 265
JSONPath expressions 268
Example applications 269
4. Switch between clusters and namespaces with ease 272
Kubeconfig files 273
Use kubectx 278
5. Save typing with auto-generated aliases 280
Installation 283
11
Completion 283
Enable completion for aliases in Bash 284
6. Extend kubectl with plugins 288
Installing plugins 289
Finding and installing plugins with krew 290
Kubernetes first steps
by Daniele Polencic
Learnk8s
© 2021
Chapter 1
Preface
14
Welcome to Learnk8s' Kubernetes first steps course!
This book is designed to take you on a journey and teach
you why containers and Kubernetes are dominating modern
development and deployment of applications at scale.
The book doesn't make any assumptions and covers the
basics as well as advanced topics.
You will learn:
What Kubernetes and containers are meant to replace
(Chapter 1).
How containers solve the problem of packaging,
distributing and running apps reliably (Chapter 2).
What is a container orchestrator and why you might
need one (Chapter 3).
The basic concepts necessary to deploy your apps on
Kubernetes (Chapter 4).
How to use a Kubernetes cluster (Chapter 5).
What happens when an app is deleted or crashes in the
cluster (Chapter 6).
The full end-to-end journey of creating apps, packaging
them as containers and deploying them on Kubernetes
(Chapter 7).
How different Kubernetes objects relate to each other
and how you can debug them (Chapter 8).
That's the high-level plan.
15
Each chapter dives into more details.
The book is designed to be hands-on, so you can read it to
the end, and re-read it while practising the command with a
real cluster.
There are also hands-on challenges in Chapter 8 (with
solutions).
When you complete all the challenges, you will be awarded a
certificate of completion!
The book has three special chapters at the end:
1. A section designed to help you install the prerequisites.
2. A mastering YAML course — everything you need to
master the configuration language used by Kubernetes.
3. Tips and tricks to become more efficient with kubectl —
the Kubernetes command-line tool.
Prerequisite knowledge
This book tries to make as few assumptions as possible.
However, it's hard to cover everything singly and concisely.
To make the most out of this book, you should be familiar
16
with:
A shell environment like Bash or Powershell. You won't
use complex commands, so a basic working knowledge is
enough. If you haven't used Bash before and you want to
start now, I recommend checking out Learn Bash the
Hard Way by Ian Miell.
Virtual machines and virtualisation. If you've used tools
such as VirtualBox or Parallels to run other operating
systems such as Windows, then you already know
everything there is to know.
Web servers such as Nginx, HAProxy, Apache, IIS, etc.
If you've used any of these to host websites, that is
enough to understand Kubernetes. If this is the first
time you have heard about them, you can check out this
introduction about Nginx.
In this journey, you are not alone
If at any time you are stuck or find a concept difficult and
confusing, you can get in touch with me or any of the
instructors at Learnk8s.
You can find us on the official Learnk8s Slack channel.
You can request an invite at this link.
Chapter 2
Infrastructure,
the past,
present and
future
18
In the past few years, the industry has experienced a shift
towards developing smaller and more focused applications.
It comes as no surprise that more and more companies are
breaking down their static apps into a set of decoupled and
independent components.
And rightly so.
Apps that are smaller in scope are:
1. Quicker to deploy — because you create and release
them in smaller chunks.
2. Easier to iterate on — since adding features happens
independently.
3. Resilient — the overall service can still function despite
one of the apps not being available.
Smaller services are excellent from a product and
development perspective.
But how does that cultural shift impact the infrastructure?
Managing infrastructure at scale
Developing services out of smaller components introduces a
different challenge.
Imagine being tasked with migrating a single app into a
19
collection of services.
When, for every application, you can refactor the same app
in a collection of four components, you have three more
apps to develop, package and release.
1 2
Fig. 1Applications packaged as a single unit are usually
referred to as monoliths.
Fig. 2You might have developed applications as single
units and then decided to break them down into smaller
components.
20
During the transition, you could have created three
Fig. 3
more apps that should be developed and deployed
independently.
And it doesn't end there.
If you test and integrate your apps into separate
environments, you might need to provision more copies of
your environments.
For example, with only three environments such as
development, staging and production, you might need to
provision 12 environments — 9 more than you had to
provision with a single app.
And that's still a conservative number.
It's common, even for smaller companies, to have dozens of
components such as a front-end, a backend API, an
authorisation server, an admin UI, etc.
Virtual machines
Your applications are usually deployed on a server.
However, it's not always practical to provision a new
21
machine for every deployment.
Instead, it's more cost-effective to buy or rent a large
computer and partition it into discrete units.
Each unit could run one of your apps isolated from the
others.
Virtual machines are the primary example of such a
mechanism.
With virtual machines, you can create virtual servers within
the same server and keep your workloads isolated.
Each virtual machine behaves like a real server, and it has an
operating system just like the real one.
When you develop apps that are smaller in scope, you might
see a proliferation of virtual machines in your infrastructure.
22
1 2
3 4
Fig. 1 Each server could run several virtual machines.
Fig. 2As soon as you break the application into smaller
components, you might wonder if it is wise to keep them
in the same virtual machine.
Perhaps it's best to isolate the apps and define clear
Fig. 3
boundaries between them.
You might want to package and run those apps into
Fig. 4
separate virtual machines.
23
Virtual machines are widely used because they offer strong
guarantees of isolation.
If you deploy an app in a virtual machine that shares a server
with several others, the only thing you can see is the current
virtual machine.
You can't tell that you are sharing the same CPU and
memory with other apps.
But virtual machines have trade-offs.
Resources utilisation and efficiency
Each virtual machine comes with an operating system that
consumes part of the memory and CPU resources allocated
to it.
1 2
3
24
Fig. 1 Consider the following virtual machine.
Parts of the compute resources are used for the
Fig. 2
operating system.
The application can use the rest of the CPU and
Fig. 3
memory.
When you create a t2.micro virtual machine with 1GB of
memory and 1 vCPU in Amazon Web Services, you are left
with 70% of the resources after you account for the CPU
and memory used by the operating system.
Similarly, when you request for one larger instance such as a
t2.xlarge with 4 vCPU and 16 GiB of memory, you will end
up wasting about 2% of the resources for the operating
system.
For large virtual machines, the overhead adds up to about 2
to 3% — not a lot.
25
1 2
Fig. 1When you develop fewer and larger apps, you have a
limited number of virtual machines deployed in your
infrastructure. They also tend to use compute resources
with more memory and CPU.
The percentage of CPU and memory used by the
Fig. 2
operating system in a large virtual machine is minimal —
between 2 and 3%.
However, that's not true for smaller virtual
Fig. 3
machines.
26
If you have four applications and decide to refactor them in
a collection of 4 smaller components each, you have 16
virtual machines deployed in your infrastructure.
When each virtual machine has an operating system that
uses 300MiB of memory and 0.2 vCPU, the total overhead
is 4.8GiB of memory (300MiB times 16 apps) and 3.2
vCPU (0.2 vCPU times 16 apps).
That means you’re paying for resources, sometimes 6 to 10%
of which you can't use.
1 2
3
27
If you assume that you can break each app down
Fig. 1
into four components, you should now have 16 virtual
machines and operating systems.
Fig. 2Smaller components have modest CPU and
memory requirements. The CPU and memory used by
the operating system aren't negligible anymore since the
virtual machine is smaller too.
The overhead in running operating systems with
Fig. 3
smaller virtual machines is more significant.
However, the operating system overhead is only part of the
issue.
Poor resource allocation
You have probably realised that when you break your service
into smaller components, each of them comes with different
resource requirements for CPU and memory.
Some components, such as data processing and data mining
applications, are CPU intensive.
Others, such as servers for real-time applications, might use
28
more memory than CPU.
1 2
3 4
When you develop apps as a collection of smaller
Fig. 1
components, you realise that no two apps are alike.
They all look different. Some of them are CPU
Fig. 2
intensive; others require more memory. And you might
have apps requiring specific hardware such as GPUs.
You can imagine being able to profile those apps.
Fig. 3
Some of them could use more CPU than others.
29
Or you could have components that use similar
Fig. 4
CPU resources but a lot more memory.
Ideally, you should strive to use the right virtual machine
that fits your app's requirements.
If you have an application that uses 800MiB of memory, you
don't want to use a virtual machine that has 2GiB.
You're probably fine with one that has 1GiB.
In practice, it's easier said than done.
It's more practical to select a single virtual machine that is
good enough in 80% of the cases and use it all the time.
The result?
You waste hundreds of gigabytes of RAM and plenty of
CPU cycles in underutilised hardware.
30
1 2
3 4
You can deploy an application that uses only 1GiB
Fig. 1
of memory and 1 vCPU in a 4GB memory virtual
machine.
Fig. 2 However, you're wasting 3/4 of the resources.
Fig. 3 It's common to use the same virtual machine's spec
for all apps. Sometimes you might be lucky and minimise
the waste in CPU and memory.
Fig. 4In this case there are still resources left, but they are
negligible.
31
Companies are utilising as little as 10% of their allocated
resources.
Can you imagine allocating hundreds of servers, but
effectively using only a dozen of them?
What you need is something to break free from the fixed
resource allocation of a virtual machine and regain the
resources that you don't use.
If you don't use the CPU and memory, you should be able
to claim it back and use it for other workloads.
But that's not the only waste of resources.
A virtual machine emulates a real computer.
When you create one, you can decide what kind of CPU you
wish to emulate, as well as what networking device to use,
storage, etc.
If you have hundreds of virtual machines, each of them will
have their own virtual network interface — even if some of
them share the same network connection on the same server.
32
1 2
3 4
Virtual machines emulate network devices to
Fig. 1
connect to the internet.
Fig. 2 They also emulate graphic cards.
Fig. 3If you wish to do so, you could run a CPU with a
different architecture from your computer.
Emulating hardware is costly, particularly when you
Fig. 4
run virtual machines at scale.
The other challenge you might face is the packaging and
33
distribution of your virtual machines.
Packaging and distribution
A virtual machine is just a regular server, so you have to
install, run and maintain an operating system.
You also need to provide the environment with the right
dependencies.
If you develop Java apps, you might need the JVM
(runtime) as well as external dependencies (a good example
is the Java Cryptographic Extension which has to be
installed separately).
Similarly, Node.js applications require the Node.js runtime
as well as Python and the C/C++ toolchain to compile
native add-ons.
1 2
34
Fig. 1If you plan on running a Spring application, you
might need to provision a virtual machine with the JVM
installed.
If you wish to run a Node.js app, you might need to
Fig. 2
provision the environment accordingly.
You should cater to all of those requirements when you
create the virtual machine and before the service is deployed.
If it sounds like a time-consuming and complex task, it is.
Fortunately, there's an entire set of tools designed to
configure and provision environments such as Puppet,
Ansible, Chef, Salt, etc.
There isn't a standard way to provision environment, so you
can pick the tool that works best for you.
But even if you automate the configuration, it is often not
enough.
Launching a virtual machine, waiting for the operating
system to boot, and installing all of the dependencies could
take some time.
35
1 2
3 4
Provisioning a virtual machine is time-consuming.
Fig. 1
Waiting for it to be created and ready could take a few
minutes.
Even if you automate the configuration,
Fig. 2
downloading and installing packages could take several
minutes.
At the end of the process, you still have to
Fig. 3
download the code for your app and run it.
36
Fig. 4Since that could take time too, provisioning
environments from scratch could take several minutes
and isn't best practice.
It's also error-prone.
What if one of the packages could not be downloaded?
You have to start from scratch.
When working with virtual machines, it's usually a good
idea to provision the environment once, take a snapshot and
save it for later.
When you need a new environment, you can retrieve the
snapshot, make a copy and deploy the application.
1 2
Once you provision the virtual machine, you don't
Fig. 1
have to run the environment immediately.
37
Instead, you could save the image and use it to
Fig. 2
generate copies the next time you need one.
Taking snapshots of virtual machines is a popular option
since you can speed up the deployment process significantly.
1 2
3 4
38
When you wish to create an environment, you
Fig. 1
could copy the snapshot instead of reprovisioning a blank
virtual machine.
As soon as the virtual machine boots, the
Fig. 2
environment is preconfigured.
You might still need to apply configurations to it,
Fig. 3
such as setting the proper environment variables.
However, it takes significantly less time to create a
Fig. 4
new environment.
Unfortunately, there is no common standard for creating
snapshots.
If you use open source tools such a Virtual Box, you might
use the vbox format.
Amazon Web Service expects virtual machines to use the
Amazon Machine Image (AMI).
Azure expects those snapshots to be packaged in a
completely different format too.
While you might be able to convert from one format to
another, it's not always straightforward to do so.
And you also have to version and keep track of snapshots as
you patch the operating system or upgrade the
dependencies.
39
Every change to the virtual machine has to go through a new
cycle of provisioning and snapshotting.
If you deploy applications with diverse languages and
runtimes, you might find yourself automating the process of
creating, patching and deployments for each app.
With dozens or hundreds of applications, you can already
imagine how much effort is involved in creating and
maintaining such a system.
As an example, let's consider the following set-up:
One Node.js application.
One Java application.
Two environments: development and production.
If you decide to adopt a production set-up based on virtual
machines, you might need:
A generic script that can provision the environment.
The script should install all the dependencies before the
snapshot is taken.
A repository where you can store and tag snapshots for
later retrieval.
A script to provision environment-specific settings such
as environment variables.
A script that retrieves the latest snapshot, creates a
virtual machine and deploys the app.
The above should be repeated twice — one for each app.
40
Every organisation has a slight variation of the above steps.
Some might skip steps like the snapshotting, for example.
Others might integrate with more advanced checks and
optimisation such as scanning snapshots for common
vulnerabilities.
What's important to recognise is that there isn't a widely
adopted standard or common way to do things.
As a consequence, there's fragmentation in tools and most
of the time the code is vendor-specific.
The automation that you build to deploy apps in Amazon
Web Service cannot be reused in Azure unless you make
changes.
Recap
Managing applications at scale is challenging.
Maintaining and running infrastructure for thousands of
applications can be even more challenging.
Virtual machines are an excellent mechanism to isolate
workloads, but they have limitations:
1. Resources are allocated upfront. Every CPU cycle or
megabyte of memory that you don't use is lost.
41
2. Each virtual machine runs an operating system that
consumes memory and CPU. The more virtual
machines, the more resources you have to spend to keep
operating systems running.
3. Virtual machines emulate even the hardware — even if
you don't need it. A virtual machine is a virtual
computer that emulates network drivers, CPU, storage,
etc.
4. You should invest in tooling and processes to create, run
and maintain virtual machines. The industry hasn't
settled on a single tool or strategy, so there is little by way
of an off the shelf solution that you can leverage.
But are virtual machines the only mechanism to isolate
workloads?
In the next chapter you will learn how you can isolate apps
without using virtual machines.
Chapter 3
The
containers
revolution
43
Not long ago, the shipping industry went through a
revolution.
The industry’s objective has always been to ship goods
around the world efficiently and reliably.
However, not all goods are the same.
How do you pack individual items of all shapes and sizes
efficiently?
Shipping a phone overseas doesn't present the same
challenges as sending a car.
They have different shapes, sizes, weights, etc.
You could put them in a box, but how big should the box
be?
Also, how do you know which items belong to the owner?
What you need is a box that has a standard size and label.
So the shipping industry turned to containers.
A container is more convenient to load and deliver because it
has a standard size.
The shipper doesn't worry about what's in the container —
they only care about how many containers they should
move.
Shipping containers are similar to virtual machines:
They are isolated.
There's a standard size that is allocated upfront.
But, contrary to virtual machines, containers are an
44
industry-wide standard.
Every port in the world can load and unload containers.
Containers can also be loaded on to trucks and trains.
All of which is possible because there is an agreement on the
standard size for a container.
The shipping industry realised that by standardising the
packaging, they could have a broader and more versatile
distribution network.
The IT industry went through a similar revolution in 2013
when a small company unveiled Docker — a technology
designed to encapsulate processes.
Docker and containers
dotCloud (the company behind Docker) was in the business
of hosting apps.
They were looking for an efficient way to package and
deploy workloads.
As a hosting business, you can offer a more competitive
product if you can pack more apps in a single server than the
competition.
So instead of using virtual machines, dotCloud developed
an abstraction that isolated processes.
45
Imagine launching the same app three times on your
computer:
bash
$ ./my-app &
$ ./my-app &
$ ./my-app &
Please note that the & is used to launch the
processes in the background.
If the apps read and write the same files, they could
overwrite each other.
But what if you could isolate the process and pretend that
each of them has a dedicated filesystem?
That's precisely what dotCloud did.
They developed a mechanism to define how many resources
a process can use and how to partition them.
So even if you have a single network interface, you could use
a part of it and still pretend it's the full networking interface.
In other words, they developed a mechanism to wrap the
process so that it can be isolated and controlled.
bash
$ ./run-and-isolate my-app &
$ ./run-and-isolate my-app &
46
$ ./run-and-isolate my-app &
dotCloud's solution was different from virtual machines.
They didn't create a full operating system; they just isolated
the processes.
They also recognised that packaging and distribution of
apps was a challenge.
Users deploying on their platform had to package the app as
well as all of the host dependencies.
So they came up with a standard of what should be included
in a snapshot.
Any customer interested in deploying on their platform
could use a standard format for packaging the app and the
dependencies.
Once deployed, dotCloud unpacked the bundle and ran the
process isolated from the others but without a virtual
machine.
You probably guessed it already, dotCloud is the former
name of Docker Inc. — the company behind Docker (the
tool).
Docker popularised the term container — a mechanism to
package, run and manage isolated processes on a single host.
However, containers existed well before Docker.
47
Linux containers
How could you isolate processes on a single host?
When a process runs on your operating system, it makes
requests to the kernel using system calls (also known as
syscalls).
If the process wants to initiate a network connection, it has
to start a system call.
Does it want to connect to the hardware through a driver?
It has to execute another system call.
Writing on the file system?
Again, a system call.
1 2
3
48
Fig. 1When your Java application accesses a file on the
filesystem, it initiates a syscall to the Linux kernel.
The kernel knows the steps necessary to read the file
Fig. 2
from the disk.
If your Node.js app has to initiate a network
Fig. 3
connection, it still has to go through the kernel with a
system call.
If you want to inspect all system calls on Linux; you
might want to check out this interactive map.
If you want to limit or isolate resources, the best place to do
so is in the kernel — where all the system calls are received.
The Linux kernel has two primitives to limit what a process
can do: control groups and namespaces.
Control groups are a convenient way to limit resources such
as CPU or memory that a particular process can use.
As an example, you could say that your process should use
only 2GB of memory and one of your four CPU cores.
49
1 2
3 4
Control groups are not only restricted to memory
Fig. 1
though.
You can create a control group that limits access to
Fig. 2
CPU, memory, network bandwidth, etc.
Fig. 3 Each process can have its control group.
You can fine-tune the settings for each control
Fig. 4
group and further restrict the available resources for that
process.
50
Namespaces, on the other hand, are in charge of isolating
the process and limiting what it can see.
As an example, the process can only see the network packets
that are directly related to it.
It won't be able to see all of the network packets flowing
through the network adapter.
Or you could isolate the filesystem and let the process
believe that it has access to all of it.
1 2
3 4
51
Since kernel version 5.6, there are eight kinds of
Fig. 1
namespaces and the mount namespace is one of them.
Fig. 2With the mount namespace, you can let the process
believe that it has access to all directories on the host
when in fact it has not.
The mount namespace is designed to isolate
Fig. 3
resources — in this case, the filesystem.
Each process can see the same file system, while still
Fig. 4
being isolated from the others.
Control groups and namespaces are low-level primitives.
With time, developers created more and more layers of
abstractions to make it easier to control those kernel
features.
One of the first abstractions was LXC, but it wasn't until
Docker that control groups and namespaces gained traction
amongst the community.
But Docker wasn't just a better abstraction for low-level
kernel primitives.
Docker offered a convenient mechanism to package
processes into bundles called container images.
52
Packaging and distributing Docker
images
When using virtual machines with snapshots, you usually
combine the following tools:
A tool to provision the environment such as Chef,
Puppet, Ansible, etc.
A tool to snapshot the virtual machine such as Packer.
An object storage service to store the snapshots such as
Amazon S3.
Even if you use containers, you still need to provide all of the
dependencies to your app.
Should you use the same tools?
You don't have to.
Docker realised that they could simplify the process by
building three components:
1. An efficient way to store incremental updates for the
snapshots.
2. A build system to create the snapshots.
3. Tooling to manage and run snapshots.
The build system is based on a Dockerfile — a recipe that
describes what should be included in the bundle.
53
Dockerfile
FROM ubuntu
RUN apt-get update
RUN apt-get -y install openssh
Once you submit the file, Docker executes the instructions
line by line and creates a mini snapshot for every instruction.
The final snapshot is the sum of all snapshots combined.
The mini snapshots are usually referred to as layers, whereas
the final snapshot is called an image.
1 2
3 4
54
Fig. 1If you ask Docker to build the image above, it will
start executing the instructions in the Dockerfile one
by one.
Fig. 2 For each instruction, Docker creates a new layer.
A layer is a snapshot of the current filesystem
Fig. 3
identified by a SHA.
Once all the instructions are executed, you are left
Fig. 4
with a container image.
Saving the image in layers has its benefits, since changes to a
single layer require building and downloading only that
layer.
A significant improvement compared to virtual machines
already.
In practical terms, the image is just a tar archive that
contains all dependencies and the instructions on how to
start the process.
Once Docker gained popularity, an ecosystem of tools grew
around it.
To preserve and foster even more innovation, Docker open-
sourced the format in which they package the tar archive.
The OCI image is now the standard to package, share and
run containers.
55
The other side effect of standardisation is that more people
found it useful to share and store images.
That led to container registries — apps that could store
versioned images.
As of now, container registries are ubiquitous. Every major
cloud provider offers one, and you can also install and host
your own.
If you compare virtual machines and containers, you can
already notice how containers standardised common good
practices.
There's no need to agree on a provisioning tool. Docker
has a build system that can create standard container
images.
Since the images are standard, any container registry can
store your image for later use.
And any container runtime that can read OCI images
can also run the process.
Standardisation pushed innovation to higher limits, to the
point that Docker isn't as relevant as it used to be.
The industry developed more abstractions similar to Docker
to package, run and maintain containers.
In your Kubernetes journey, you might have heard of the
following tools:
Containerd
CRI-O
56
rkt (defunct)
They all fulfil the same task: running containers.
With so many tools and benefits, is there any reason to still
use virtual machines?
Linux containers vs virtual
machines
You might think that containers are lighter virtual machines.
But they are not virtual machines at all!
Containers are just processes that share the same kernel of
the operating system.
When one of the processes causes a kernel panic and crashes,
the rest of the containers and operating system will crash as
well.
In contrast, virtual machines emulate a full server —
hardware included.
If there's a kernel panic in the operating system in the virtual
machine, it will not affect the other virtual machines.
57
1 2
3 4
Your computer runs a single operating system and
Fig. 1
kernel.
However, you can use Linux containers to partition
Fig. 2
resources and assign them to processes. If there's a kernel
panic, everything is affected.
Fig. 3If you partition your computer with virtual
machines, each of them has an operating system. If
there's a kernel panic, the failure is isolated.
58
Notice how virtualisation and containerisation are
Fig. 4
not mutually exclusive and you could combine to isolate
processes further.
It's also important to remember that containers do not
supersede virtual machines.
In general, virtualisation creates a much stronger security
boundary than container isolation.
And the risk of an attacker escaping a container (process) is
much higher than its chance of escaping a virtual machine.
As an example, when you inspect the running processes on
your computer, a container process looks just like any other
process.
You could have tools or programs that ignore control groups
and namespaces and interact with the processes directly.
Should you stop using containers?
It depends.
Containers are lightweight processes, and if you can find a
way to secure them, they offer an excellent alternative to
virtual machines.
However, if you have to deal with untrusted code, perhaps
virtual machines are a safer choice.
You can also combine virtual machines and containers!
59
Projects such as Katacontainers mix the two technologies
and offer a hybrid virtual machine that looks like a
container.
Here's a recap of containers and virtual machines:
Virtual machines Containers
Limited performance Native performance
All containers share the host
Each VM runs in its OS
OS
Hardware-level virtualisation OS virtualisation
Startup time in minutes Startup time in milliseconds
Fixed resource allocation Dynamic resource allocation
Virtualised hardware
Namespace isolation
isolation
There's another aspect of containers worth discussing: how
they simplify day to day development.
Containers and development
lifecycle
60
When developing applications, it's usually a good idea to
develop in a production-like environment.
Before containers were popular, often developers used to
run a virtual machine provisioned with the same
dependencies as production.
However, virtual machines are slow to boot and tend to use
a lot of memory.
As a consequence, developers used to run several apps and
databases in the same virtual machine.
There was no real isolation between components, but at
least you could have the same environment as production.
With containers, the development workflow changed
dramatically.
Since containers are processes with a restricted view of the
operating system, they have low overheads.
When you are developing a front-end application, you can
launch your dependent components such as an API or a
database as containers and connect to those directly.
Since containers are cheaper to run, it's easy to have a dozen
of them running locally — unlike virtual machines.
61
1 2
Running a development environment in a virtual
Fig. 1
machine is slow, and the isolation is poor.
Instead, containers offer a lightweight solution for
Fig. 2
running multiple processes isolated on the same host.
And you are now developing and integrating your code with
the same containers that are deployed into production.
So you should expect fewer surprises and regressions when
your code is published live.
Recap
Virtual machines are not the only technology capable of
isolating and segregating apps.
62
Linux containers are a lightweight alternative that restrict
what a process can do.
Containers leverage primitives in the Linux kernel to isolate
processes on the same operating system.
Docker is a tool that builds on top of those abstractions and
offers a developer-friendly interface to run processes as
containers.
It also offer a mechanism to build a container image, as well
as to share them.
Running containers doesn't require as many resources as
running a virtual machine since there's no hardware to
virtualise.
Developers can benefit from containers since they can run
the same dependency as production, but locally.
No wonder more and more applications are packaged as
containers!
However, what happens when there are so many
applications packaged as containers that you can't run them
in a single server?
Could you have containers deployed across several hosts?
Unfortunately, Linux containers are designed to encapsulate
processes only.
They don't define how two processes on two different
servers should talk to each other.
When you want to manage a large number of containers
across several servers, you should consider using a container
63
orchestrator.
In the next chapter, you will learn about container
orchestrators — including Kubernetes.
Chapter 4
Managing
containers at
scale
65
Deploying containers is convenient because:
1. They offer a consistent mechanism to package and
distribute applications.
2. They are built according to a standard. You can run and
manage them with any tool that supports that standard.
If you are in charge of deploying them, you don't have to
learn how to develop or run Ruby, Java or .NET
applications.
You can just run the container as you would any other
processes on your computer.
66
1 2
3 4
5 6
Fig. 1In the past, you might have deployed applications
that required an environment to be provisioned upfront.
67
You had to have intimate knowledge of the app
Fig. 2
deployed: the start command, the environment variables,
host dependencies, etc.
Fig. 3If you managed more than one app or
programming language, you might have faced the
challenge on a larger scale.
Fig. 4Containers simplified running several apps on a
single host. You only need a computer that can support
the control group and namespaces.
All the applications are packaged in a standard
Fig. 5
format that can be distributed and run in the same way.
Running containers doesn't require any particular
Fig. 6
knowledge on what's inside the container.
Containers are an excellent abstraction if you wish to share
resources and isolate processes on the same host.
But they don't solve the problem of running processes across
several hosts.
68
Managing containers allocations &
placements
When you have hundreds if not thousands of containers,
you should find a way to run containers on multiple servers.
And it would be best if you could spread the load.
When you can distribute the load across several nodes, you
can prevent a single failure from taking down the entire
service.
You can also scale your application as you receive more
traffic.
1 2
When you have a single server, you don't get to
Fig. 1
choose where your containers run.
69
But as soon as you have more than one computer,
Fig. 2
you might ask yourself — what is the best way to launch
containers in different servers?
But keeping track of where every container is deployed in
your infrastructure is time-consuming and complicated.
You could write a program to do it for you.
The algorithm could be smart enough to pack containers
efficiently to maximise server density, but also intelligent
enough to minimise disruption if a server becomes
unavailable.
Allocating and balancing containers in your data centre isn't
the only challenge, though.
If you have a diverse set of servers, you want your containers
for data-intensive processing — such as machine learning
jobs — to always be scheduled on servers with dedicated
GPUs.
Similarly, if a container is mounting a volume on a server,
you don't want that container to be moved somewhere else
because you could lose the data.
You could tweak your scheduling algorithm and make sure
that placements are taken into consideration before
launching containers.
The algorithm isn't as simple as you envisioned, but it's
70
doable and promising.
You also have to solve the challenge with networking and
service discovery.
Containers, networking and service
discovery
When you run multiple servers, the containers should be
able to connect and exchange messages to each other.
However, Linux containers are designed only to restrict
what a process can do.
They don't prescribe a way to connect multiple processes
across several hosts.
1 2
71
When you have a single server, you can use the host
Fig. 1
network to connect the containers.
When you have multiple servers, you can't benefit
Fig. 2
from the host network. You should find a way to connect
multiple networks and hosts.
Managing containers on a single server is a different
challenge from running containers at scale.
And Google knows that well.
Running containers at scale at
Google
Google runs servers globally.
They guard against revealing the secrets of their locations,
but you can imagine them having an extensive fleet of servers
that spans several regions.
Gartner estimates that Google had 2.5 million servers in
2006.
Could you imagine running and maintaining containers
manually in such an infrastructure?
72
Impossible.
Google had to find an efficient way to schedule workloads
that could span hundreds of thousands of servers.
They knew that their products could not fit a single server
and they had to scale the load using several computers.
Also, they had to find a way to allocate containers efficiently
and automatically.
Machine learning jobs had to be assigned to a computer
with accelerated hardware such as GPUs or TPUs, whereas
databases had to use I/O optimised computers.
The system could only work if they could find a way to
create a unified network that could span multiple servers and
regions.
In the end, they decided to write a platform that can
automatically analyse resource utilisation, schedule and
deploy containers.
Later on, a few Googlers decided to restart the project as an
open-source effort.
And Kubernetes was born.
You can think of Kubernetes as three things:
1. A single computer that abstracts your data centre.
2. A scheduler.
3. A unified API layer over your infrastructure.
Let's dive into it.
73
Data centre as a computer
Dealing with a single computer is more straightforward than
coordinating deployments across a fleet of servers.
You had an example in the previous chapter.
Managing placements and allocations, as well as networking
and service discovery, were non-problems with a single
server.
Also, with a single server, you didn't have to worry about
which server your application ran on.
But how can you use a single server for all of your apps?
You can't.
But you could create an abstraction.
You could pretend to make a more powerful computer from
merging smaller compute resources.
And that's what Kubernetes does.
Kubernetes embraces the idea of treating your servers as a
single unit and abstracts away how individual compute
resources operate.
From a collection of three servers, Kubernetes makes a single
cluster that behaves like one.
74
1 2
3 4
When you install Kubernetes in your infrastructure,
Fig. 1
all servers are connected.
The servers are abstracted from the users. When
Fig. 2
you use Kubernetes, you don't have to worry about
placements or allocations.
You deploy your containers in the Kubernetes
Fig. 3
cluster as you would deploy them into a single computer.
Kubernetes takes care of finding the best server for
Fig. 4
your containers.
75
Kubernetes isn't picky.
You can give it any computer that has memory and CPU,
and it will use them to run your workloads.
If you're into embedded systems, there's an active
community interested in running Kubernetes
cluster on Raspberry Pis.
1 2
You can have any servers joining the cluster. It
Fig. 1
doesn't matter if they are not of the same size as long as
they can offer CPU and memory.
The cluster has the CPU and memory of all the
Fig. 2
other servers combined.
CPU and memory for each compute resource are stored in
76
Kubernetes and used to decide how to allocate containers.
Kubernetes as a scheduler
When you deploy an application, Kubernetes identifies the
memory requirements for your container and finds the best
place to deploy your application.
You don't decide which server the application is deployed
into.
Kubernetes has a scheduler that decides it for you.
The scheduler is optimised to pack containers efficiently:
Initially, it spreads containers over the servers to
maximise resiliency.
As time goes on, the scheduler will try to fill containers
in the same host, while still trying to balance resource
utilisation.
You can think about Kubernetes scheduler as a skilled Tetris
player.
Containers are the blocks, servers are the boards, and
Kubernetes is the player.
77
Fig. Kubernetes as a skilled Tetris player
Kubernetes keeps playing the game every time you want to
deploy a new container.
Kubernetes as an API
78
When you wish to deploy a container in Kubernetes, you
end up calling a REST API.
It's as straightforward as sending a POST request to
Kubernetes with the container that you want to run.
Kubernetes receives the request and, as soon as the scheduler
identifies the right place, a container is deployed.
If you wish to expose your application to the public,
Kubernetes exposes a REST API endpoint for that too.
There are REST API endpoints for things such as
provisioning storage, autoscaling, managing access and
more.
You always interact with Kubernetes through an API.
That's advantageous because:
You can create scripts and daemons that interact with
the API programmatically.
The API is versioned; when you upgrade your cluster,
you can keep using the old API and gradually migrate.
You can install Kubernetes in any cloud provider or data
centre, and you can leverage the same API.
Starting with Kubernetes 1.10, the Kubernetes API
serves an OpenAPI spec. You should take your time
and inspect the API.
You could think of Kubernetes as a layer on top of your
79
infrastructure.
When you issue a command through a REST API, you
don't worry about the underlying infrastructure.
Kubernetes acts as a translator between you and the cloud
provider (or data centre).
Even if there are differences in implementation between
Azure and Amazon Web Services, you can still rely on
Kubernetes to abstract those away.
And since this layer is generic and can be installed anywhere,
you can always take it with you.
Even if you migrate from your on-premise cluster to
Amazon Web Services or Azure.
80
Fig.Kubernetes abstracts the underlying resources and
exposes a consistent API
Now that you know why Kubernetes exists, let's take a look
at what it is capable of.
81
Kubernetes, what it is and what it is
not
Kubernetes is a platform to run distributed applications
resiliently.
If you have a collection of servers and a few apps you wish to
deploy, this is what Kubernetes can do for you:
1. It can run and look after your apps packaged as
containers. If a container crashes or is deleted,
Kubernetes automatically respawns a new one.
2. It can schedule containers efficiently and maximise CPU
and memory utilisation of your servers.
3. It provides a unified network across your fleet of servers
so that containers can communicate no matter where
they are deployed.
4. It provides the basic building blocks to build scalable
services such as autoscalers and load balancers to
distribute the traffic.
5. It bundles a mechanism to roll out updates with zero
downtime. If the deployment fails, Kubernetes can roll
back your deployment to the previous working version.
As your application increases in scope, Kubernetes can do
even more:
82
1. It offers a standard storage interface that containers can
consume to save and retrieve data.
2. It lets you store and manage sensitive information
securely, such as passwords, tokens, and keys.
3. It can segregate access to resources and assign roles to
users and apps.
4. It can inspect, validate and reject deployments based on
custom and internal rules.
5. It can collect and aggregate metrics for monitoring and
reporting.
The list goes on, but you’ve got the gist.
Kubernetes is a vast subject and can accomplish a wide range
of tasks.
In this book, you will focus on deployments of applications
using containers.
You might want to check out the Learnk8s website
to learn other topics such as Kubernetes internal
components and its architecture, Kubernetes
networking, etc.
Before jumping into Kubernetes and getting your hands
dirty, it's worth doing a recap of what the workflow is so far.
83
From containers to Kubernetes
When you develop applications locally, you want your
environment to mimic production as much as possible to
avoid late integration of your code.
If you work with dependent services, you can use containers
to start the processes locally and emulate a real environment.
Once you're ready to release your code, you package your
app as a container and store it in a registry.
The registry is a convenient repository with the current and
previous version of your app.
Kubernetes can download the container from the registry
and run it.
Kubernetes looks after the container: when it stops, it
restarts it.
When it's deleted, it spawns a new one.
The rest of the book will focus on deploying containers on
Kubernetes.
Chapter 5
Deploying
apps on
Kubernetes
85
Kubernetes isn't the only platform that can deploy and scale
applications.
Engineers used to deploy at scale even before Kubernetes.
Let's take the following web application as an example.
It's a static page with a red background.
Fig. A static red page
If you don't expect your application to be popular, you can
deploy a single instance of it.
86
Of course, you need a server to deploy the application.
You could rent one from any cloud provider such as
Amazon Web Services, Google Cloud Platform, Azure,
Digital Ocean, etc.
If you log in to the server, you can download the code and
start the app, and you're done.
Anyone who has access to the IP address of the server can
visit your app.
The setup is straightforward, but it has some limitations.
As soon as more users visit your app, you could overload the
single instance, and you might need to provision a second
copy of it.
But how should you divide the traffic equally between the
two instances?
In the past, you might have used a load balancer.
1 2
87
Fig. 1 A user can visit the website by going directly to the
app.
Fig. 2However, as soon as you scale the app to two
instances, there's no easy way to distribute the traffic
unless you use a load balancer.
Your infrastructure is more scalable, but now you have to
keep track of where each application is deployed and
connect the load balancer to every new instance.
To further increase reliability and scalability, you decide to
split the app into smaller components that are more
focussed.
So you end up with a backend and a front-end app.
The backend exposes a private API that the front-end can
consume and a public API for anyone who wants to
consume the data.
The server has a single IP address, so how should you expose
both apps?
You should design the system so that:
All the requests starting with / are routed to the front-
end app.
All the requests starting with /api are forwarded to the
backend app.
88
You probably need a component that can inspect the HTTP
requests and make intelligent routing decisions.
The router works almost like a load balancer.
It still distributes the traffic to both apps, but instead of
trying to balance the load, it routes the traffic based on the
path.
1 2
If you wish to expose both apps, you have to find a
Fig. 1
way to distribute the traffic.
Fig. 2A router could distribute the traffic to the other
load balancers.
You're finally done.
You have:
Two front-end apps with a load balancer to distribute
89
the traffic.
One backend app. There's a load balancer, in case you
need to scale it.
One load balancer which acts as a router. It inspects the
HTTP requests and forwards the traffic to the right app.
There are two load balancers, but they fulfil different duties.
The load balancer next to the apps is always internal.
The load balancer that forwards the traffic is the external
load balancer.
How does the above setup translate into Kubernetes?
In Kubernetes, you have the same structure.
You have apps, internal load balancers and routers.
But instead of calling them by their names, Kubernetes has
particular words to define them.
1. The apps are called Pods.
2. The internal load balancers are called Services.
3. The front-facing load balancer which acts as a router is
called Ingress.
90
Fig. Pods, Services and Ingresses in Kubernetes
The traffic that flows to your Pods has to be processed first
by the router and then by the internal load balancer.
There's a fourth Kubernetes object that is fundamental to
deploying Pods.
91
Watching over Pods
Pods are rarely deployed manually.
When an application has a memory leak and crashes, you'd
expect it to be restarted.
If the application is deleted because of a glitch, you probably
want it to be respawned.
But who is watching and restarting those apps?
In Kubernetes, you can create a Deployment resource that
monitors and respawns Pods.
In the Deployment, you define what Pods and how many
you need.
Kubernetes automatically creates the Pods and respawns
them when one is lost.
92
1 2
3 4
A Deployment is a recipe to create more Pods. You
Fig. 1
define what the Pod should look like and how many
instances you wish to have.
Fig. 2 The Deployment monitors the Pods.
Fig. 3 There might be cases where the Pod is deleted.
The Deployment automatically creates a new Pod
Fig. 4
to match the number of instances requested.
A Deployment is a convenient way to deploy apps.
93
And it's also convenient if you want to scale your apps —
just increase the number of instances, and Kubernetes
automatically creates more copies.
Pods, Deployments, Services and Ingresses are the basic
building blocks for deploying apps in Kubernetes.
Now that you've mastered the theory, let's dive into how you
create these resources.
Creating resources in Kubernetes
When you want to create a Deployment, Service or Ingress,
you describe what you want, and Kubernetes figures out the
necessary steps to reach this state.
The "language" that you use to communicate with
Kubernetes consists of so-called Kubernetes resources.
So instead of telling Kubernetes to create a Deployment
with two instances, you submit a request saying that your
app uses a particular container and you want two copies of
it.
The difference is subtle but essential.
Imagine ordering a coffee at your favourite coffee shop.
You can go there and:
1. ask for a latte or
94
2. ask for two espresso shots, two cups of milk and one
spoon of sugar. You also tell the barista how they should
be mixed.
In the first example, you say what you want.
The barista figures out how many shots of coffee or how
much milk they should pour into it.
In the other example, you tell the barista exactly how to
make your drink.
Kubernetes is your trusted barista, you ask for what
resources you want — not how you want them.
There are many different Kubernetes resources — each is
responsible for a specific aspect of your application.
You can find the full list of Kubernetes resources in
the Kubernetes API reference.
Kubernetes resources are defined in YAML files and
submitted to the cluster through the Kubernetes API.
Kubernetes resource definitions are also sometimes
called "resource manifests" or "resource
configurations".
95
Kubernetes resources in YAML
YAML, which stands for Yet Another Markup Language, is
a human-readable text-based format for specifying
configuration-type information.
YAML is a superset of JSON, which means that any valid
JSON file is also a valid YAML file.
Why not use JSON then?
JSON is a repetitive format, and all values are wrapped into
quotes " .
A format such as YAML is more concise and more
comfortable to type.
Let's have a look at an example.
Pay attention to the following JSON example:
example.json
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "my-first-pod"
},
"spec": {
"containers": [
{
"name": "web",
"image": "my-container",
"ports": [
{
96
"containerPort": 80
}
]
}
]
}
}
Now have a look at the same definition in YAML:
example.yaml
apiVersion: v1
kind: Pod
metadata:
name: my-first-pod
spec:
containers:
- name: web
image: my-container
ports:
- containerPort: 80
The format is less tedious and requires less indentation and
fewer quotes " .
Using YAML for Kubernetes objects gives you several
advantages, including:
Convenience — you don't have to add all of your
parameters to the command line.
Maintenance — YAML files can be added to source
control so that you can track changes.
Flexibility — you can create complex structures.
97
If you are familiar with YAML or need a refresher, you will
find additional material at the end of this book.
The YAML file above is the definition of a Pod in
Kubernetes.
The manifest can be intimidating at first.
But there are only four parts worth remembering:
pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: my-first-pod
spec:
containers:
- name: web
image: my-container
ports:
- containerPort: 80
In the YAML definition:
1. kind describes the schema of the manifest. In this case,
you created a Pod.
2. metadata.name is the name of the resource.
3. A pod is an application that you want to deploy and run
in the cluster. The app should be packaged as a container
and spec.containers[0].image is where you define the
image.
4. If you develop a web application, you might need to
define which port the container is exposing in
98
4. spec.containers[*].ports[0].containerPort .
How do you know which fields are available for a Pod?
What about Deployments, Services, Ingresses, etc.?
You can find the full list of Kubernetes resources and what
fields are available in the Kubernetes API reference.
There's also a convenient command in kubectl where you
can navigate the same documentation programmatically:
bash
$ kubectl explain resource[.field]...
But what is kubectl ?
Also, how should you submit resources to Kubernetes?
And how do you create a Kubernetes cluster?
The next chapter will answer all of these questions and help
you set up your local environment.
Recap
There are four components that are crucial in deploying
apps in Kubernetes:
Pods (the applications/containers)
99
Services (the internal load balancers)
Ingresses (the external local balancers)
Deployments (the component that monitors the Pods)
The components are defined using YAML — a human-
friendly configuration language — and submitted to the
Kubernetes API.
Chapter 6
Setting up a
local
environment
101
There are several ways to create a Kubernetes cluster:
Using a managed Kubernetes service like Google
Kubernetes Service (GKE), Azure Kubernetes Service
(AKS), or Amazon Elastic Kubernetes Service (EKS).
Installing Kubernetes yourself on cloud or on-premises
infrastructure with a Kubernetes installation tool like
kubeadm or kops.
Creating a Kubernetes cluster on your local machine
with a tool like Minikube, MicroK8s, or k3s.
Minikube is an excellent option if you want to explore
Kubernetes because it comes prepackaged with all the most
common components that you might need and has a
friendly and polished interface.
A Minikube cluster is only intended for testing purposes,
not for production.
But in the previous chapters, you already learned that any
resource that you create for a single cluster could be
redeployed to any other cluster.
So you can practise and perfect your technique locally and
deploy to a production-ready cluster once you are confident.
You should download and install minikube from the official
website.
102
If you find the installation difficult, you can find a
few helpful resources at the back of this book.
With minikube installed, you can create a cluster and start
deploying your YAML manifests.
But how should you submit the YAML to the cluster?
Enter kubectl.
kubectl is the primary Kubernetes CLI — you use it for all
interactions with a Kubernetes cluster, no matter how the
cluster was created.
That means that the same binary can be used with
minikube, but also Microk8s, K3s, Azure Kubernetes
Service, Amazon EKS, etc.
From a user's point of view, kubectl allows you to perform
every possible Kubernetes operation.
From a technical point of view, kubectl is a client for the
Kubernetes API.
The Kubernetes API is an HTTP REST API and it is the
real Kubernetes user interface.
Kubernetes can be controlled in full through this API with
kubectl.
103
1 2
Kubectl is what you use to submit requests to the
Fig. 1
cluster.
Kubectl works as a translator between your requests
Fig. 2
and the Kubernetes API.
As a prerequisite to interacting with the cluster, you should
install kubectl.
You can find the instruction on the official documentation
or at the back of this book.
With minikube and kubectl installed, you can finally create
your first cluster:
bash
$ minikube start --driver=docker
👍 Starting control plane node minikube in cluster minikube
🐳 Preparing Kubernetes v1.23.0 ...
🔎 Verifying Kubernetes components...
🌟 Enabled addons: default-storageclass,
104
storage-provisioner
🏄 Done! kubectl is now configured to use "minikube" by default
The command creates a Kubernetes cluster as a Docker
container.
Starting the virtual machine and cluster may take a couple of
minutes, so please be patient!
When the command completes, minikube automatically sets
up with the credentials to connect to the cluster.
You can verify that the cluster is created successfully with:
bash
$ kubectl cluster-info
Kubernetes master is running at https://192.168.64.20:8443
CoreDNS is running at <long url>
To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
You have a fully-functioning Kubernetes cluster on your
machine now.
Before moving forward, it's worth learning a few tricks to
troubleshoot your setup.
When things go wrong with your minikube, and you think
it might be broken, you can delete and start fresh with:
bash
$ minikube delete
105
🔥 Deleting "minikube" ...
💀 Removed all traces of the "minikube" cluster.
$ minikube start --driver=docker
👍 Starting control plane node minikube in cluster minikube
🐳 Preparing Kubernetes v1.23.0 ...
🔎 Verifying Kubernetes components...
🌟 Enabled addons: default-storageclass, storage-provisioner
🏄 Done! kubectl is now configured to use "minikube" by default
Since kubectl makes requests to the Kubernetes API, it's
worth checking that you are not using an older version of
kubectl.
bash
$ kubectl version
Client Version: version.Info{Major:"1", Minor:"22", GitVersion:"v1.22.4"}
Server Version: version.Info{Major:"1", Minor:"23", GitVersion:"v1.23.0"}
The Kubernetes API supports up to two older versions for
backwards compatibility.
So you could have the client (kubectl) on version 1.16 and
the server on 1.18.
But you cannot use kubectl on version 1.15 with a cluster
that runs on 1.19.
In the rest of the book, you will practise deploying, running
and scaling applications in minikube with kubectl, so you
should take time to set them up properly.
If you need help, you can refer to the official documentation
or at the end of this book.
106
Your first deployment
Podinfo is a web application that showcases best practices of
running apps in Kubernetes.
As soon as you launch it, this is what it looks like:
107
Podinfo is a tiny web application made with Go that
Fig.
showcases best practices of running microservices in
Kubernetes.
The container is available publicly on Docker Hub.
You can deploy PodInfo on Kubernetes with:
INSTRUCTIONS FOR BASH
bash
108
$ kubectl run podinfo \
--restart=Never \
--image=stefanprodan/podinfo:6.0.3 \
--port=9898
pod/podinfo created
INSTRUCTIONS FOR POWERSHELL
PowerShell — □ 𝖷
PS> kubectl run podinfo `
--restart=Never `
--image=stefanprodan/podinfo:6.0.3 `
--port=9898
pod/podinfo created
Let's review the command:
kubectl run podinfo deploys an app in the cluster and
gives it the name podinfo .
--restart=Never is used not to restart the app when it
crashes.
--image=stefanprodan/podinfo:6.0.3 and --
port=9898 are the name of the image and the port
exposed on the container.
Please note that 9898 is the port exposed in the
container. If you make a mistake, you shouldn't
increment the port; you can still use port 9898. If
you make a mistake, you can execute kubectl
delete pod app and start again.
109
If the command was successful, there is a good chance that
Kubernetes deployed your application.
You should check that the cluster has a new Pod with:
bash
$ kubectl get pods
NAME READY STATUS RESTARTS
podinfo 1/1 Running 0
Great!
But how can you reach that Pod and visit it in a browser?
You should expose it.
You can expose the podinfo Pod with a Service:
bash
$ kubectl expose pod podinfo --port=9898 --type=NodePort
service/podinfo exposed
Let's review the command:
kubectl expose pod podinfo exposed the Pod named
podinfo .
--port=9898 is the port exposed by the container.
--type=NodePort is the type of Service. Kubernetes has
four different kinds of Services. NodePort is the Service
that exposes apps to external traffic.
You should check if Kubernetes created the Service with:
110
bash
$ kubectl get services
NAME TYPE CLUSTER-IP PORT(S)
podinfo NodePort 10.111.188.196 9898:32036/TCP
To obtain the URL to visit your application, you should
run:
bash
$ minikube service --url podinfo
http://192.168.64.18:32036
If you visit that URL in the browser (yours might be slightly
different), you should see the app in full.
What did kubectl run and kubectl expose do?
Inspecting YAML resources
Kubernetes created a few resources when you typed
kubectl run , and kubectl expose .
When you ran kubectl run podinfo it created a Pod
object.
You can verify that with:
111
bash
$ kubectl get pods
NAME READY STATUS RESTARTS
podinfo 1/1 Running 0
However, you learned that:
1. Kubernetes has a declarative interface — you describe
what you want, not how you get there. In this instance,
you are telling Kubernetes how to run your pod.
2. All workloads are defined in YAML. But you haven't
written any YAML so far, and deployment worked
anyway.
kubectl run and kubectl expose are two shortcuts that
are useful for creating resources quickly.
However, they are rarely used in a production environment.
When you type kubectl run , a YAML definition is
automatically created, populated and submitted to the
cluster.
You can inspect the YAML submitted to the cluster with:
bash
$ kubectl get pod podinfo --output yaml
apiVersion: v1
kind: Pod
metadata:
labels:
run: podinfo
112
name: podinfo
namespace: default
# truncated output
spec:
containers:
- image: stefanprodan/podinfo:6.0.3
imagePullPolicy: Always
name: podinfo
ports:
- containerPort: 9898
protocol: TCP
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: default-token-lvsst
readOnly: true
# truncated output
status:
# truncated output
The output is overwhelming!
How can I short command like kubectl run generate over
150 lines of YAML code?
The response includes:
The status of the resource in status .
The schema of the resource in
metadata.managedFields .
The propriety of the pod in spec .
Most of the fields are set to their default value, and some of
them are automatically included by your cluster (and may
vary).
113
With time, you will learn all of those, but for now, let's focus
on the basics.
The long output can be shortened to this:
pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: podinfo
spec:
containers:
- name: podinfo
image: stefanprodan/podinfo:6.0.3
ports:
- containerPort: 9898
The name of the Pod, the image and the container port are
the same values that you provided to kubectl run .
The command generated the above YAML and submitted
to the cluster for you.
As you can imagine, kubectl expose works in a similar
way.
As you type the command, kubectl generates a Service.
Let's explore the YAML stored in the cluster with:
bash
$ kubectl get services podinfo --output yaml
apiVersion: v1
kind: Service
metadata:
labels:
114
run: podinfo
name: podinfo
namespace: default
# truncated output
spec:
clusterIP: 10.111.188.196
externalTrafficPolicy: Cluster
ports:
- nodePort: 32036
port: 9898
protocol: TCP
targetPort: 9898
selector:
run: podinfo
sessionAffinity: None
type: NodePort
status:
loadBalancer: {}
The output is verbose, and there are a few fields that are
automatically set to their defaults.
The above YAML shortened could be rewritten as:
service.yaml
apiVersion: v1
kind: Service
metadata:
name: podinfo
spec:
selector:
run: podinfo
ports:
- port: 9898
targetPort: 9898
nodePort: 32036
The YAML definition includes:
115
The name of the resource in metadata.name .
The port expose by the Service as spec.ports[0].port .
The port used to forward traffic to the Pods
spec.ports[0].targetPort .
The port that is exposed to public traffic
spec.ports[0].nodePort .
The following diagram is a visual representation of the
Service's ports.
1 2
3 4
116
The Service above exposes three ports:
Fig. 1 port ,
targetPort and nodePort .
The targetPort is used to forward traffic to the
Fig. 2
Pods and should always match the containerPort in the
Pod resource.
The nodePort is a convenient way to bypass the
Fig. 3
Ingress and expose the Service directly from the cluster.
The port is used by any service in the cluster to
Fig. 4
communicate with the Pods. It's also used by the Ingress
to locate the Service.
Object definitions for Pods and Services are written in
YAML.
But YAML isn't the only option.
You can retrieve the same Pod in JSON format with:
bash
$ kubectl get pod app --output json
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"creationTimestamp": "2020-10-23T01:51:52Z",
"labels": {
"run": "podinfo"
},
# truncated output
117
And you can do the same with the Service too:
bash
$ kubectl get service app --output json
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"creationTimestamp": "2020-10-23T01:55:20Z",
"labels": {
"run": "podinfo"
},
# truncated output
How do you remember all the available properties for
objects such as Services and Pods?
There are two useful strategies to explore all available
configuration options.
The first is the official Kubernetes API.
The second is the kubectl explain command.
You can use the command to query the same description
that you can read from the API like this:
bash
$ kubectl explain pod.spec.containers
KIND: Pod
VERSION: v1
RESOURCE: containers <[]Object>
DESCRIPTION:
List of containers belonging to the pod. Containers cannot currently be
added or removed. There must be at least one container in a Pod. Cannot be
updated.
118
A single application container that you want to run within a pod.
Kubectl is probably one of your most-used tools in
Kubernetes.
Whenever you spend a lot of time working with a specific
tool, it is worth to get to know it very well and learn how to
use it efficiently.
At the back of this book, you will find some bonus material
on how to get more proficient with it.
Exposing the application
Exposing applications to the public using Services is not
common.
Services are similar to internal load balancers and can't route
traffic depending on the path, for example.
And that's a frequent requirement.
Imagine having two apps:
The first should handle the traffic to /store .
The second should serve requests for /api .
How do you split the traffic and route it to the appropriate
Pod?
119
Fig. Routing the traffic based on paths
You might have used a tool such as Nginx to route the traffic
to several apps.
You define rules, such as the domain or the path, and the
backend that should receive the traffic.
Nginx inspects every incoming request against the rules and
forwards it to the right backend.
Routing traffic in such manner is popular even in
120
Kubernetes.
Hence there's a specific object called Ingress where you can
define routing rules.
Routing the traffic based on paths in Kubernetes
Fig.
with the Ingress
Instead of using a Service, you should create an Ingress to
expose your application to the public internet.
You explored kubectl run and kubectl expose that
121
created YAML resources on your behalf.
This time, you will create the Ingress resource by hand.
Please note that creating resource manually in
YAML is considered best practice. Commands such
as kubectl run and kubectl expose are not
popular in the Kubernetes community, but they are
an excellent start to familiarise with Kubernetes.
Create an ingress.yaml file with the following content:
ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: podinfo
spec:
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: podinfo
port:
number: 9898
Let's break down that YAML definition.
122
Every time the incoming request matches the path / , the
traffic is forwarded to the Pods targeted by the podinfo
Service on port 9898.
ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: podinfo
spec:
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: podinfo
port:
number: 9898
Before you submit the resource to the cluster, you should
enable the Ingress controller in minikube :
bash
$ minikube addons enable ingress
🔎 Verifying ingress addon...
🌟 The 'ingress' addon is enabled
Please note that the Ingress could take some time to
download. You can track the progress with kubectl
get pods --all-namespaces .
123
You can submit the Ingress manifest with:
bash
$ kubectl apply --filename ingress.yaml
ingress.networking.k8s.io/podinfo created
The command is usually shortened to kubectl
apply -f .
You should verify that Kubernetes created the resource with:
bash
$ kubectl get ingress podinfo
NAME CLASS HOSTS ADDRESS PORTS
podinfo <none> * 192.168.64.18 80
If you wish to inspect the resource stored in the cluster, you
can do so with:
bash
$ kubectl get ingress podinfo --output yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
# truncated output
Does it work?
124
Let's find out.
You can access the cluster in three different ways:
1. Via a tunnel with kubectl port-forward .
2. Via a tunnel with minikube tunnel .
3. Via the cluster IP address.
Why so many option?
Minikube integrates with Windows, Linux and macOS but
it has to account for the operating systems' differences.
In a cloud environment, you will likely expose the nodes via
a cloud load balancer.
Unfortunately, this is a bit more complex to run locally.
1. Creating a Kubectl-portward tunnel
Create a tunnel with:
bash
$ kubectl port-forward service/ingress-nginx-controller -n ingress-nginx 8080:80
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80
Visit your app at http://localhost:8080/ in your browser.
Well done!
It's the same app, but this time it's served by the Ingress.
2. Creating a tunnel with minikube tunnel
125
minikube tunnel runs as a process that creates a network
route to link your computer to the cluster.
It is similar to kubectl port-forward but it exposes all
Services and Ingress, not just a single pod.
Please note that this option is viable only for macOS and
Windows setups.
If you are using a Linux distribution, you can execute the
command, but it won't have any effect.
In another terminal, execute the following command:
bash
$ minikube tunnel
Password:
Status:
machine: minikube
pid: 39087
route: 10.96.0.0/12 -> 192.168.64.194
minikube: Running
services: [podinfo]
errors:
minikube: no errors
router: no errors
loadbalancer emulator: no errors
# truncated output
Now you can retrieve the IP address of the Ingress with:
bash
$ kubectl get ingress podinfo
NAME CLASS HOSTS ADDRESS PORTS
podinfo <none> * 192.168.64.18 80
126
Pay attention to the address column.
Visit your app at http://[ADDRESS COLUMN]/ in your
browser.
Congrats!
It's the same app, but this time it's served by the Ingress.
3. Accessing the cluster IP address
This is the simplest option and closer to a bare metal setup.
It only works if you started your minikube cluster with
minikube start --vm .
First, retrieve the IP address of minikube with:
bash
$ minikube ip
192.168.64.18
Visit your app at http://[replace with minikube ip]/ in
your browser.
Done!
Congratulations!
It's the same app, but this time it's served by the Ingress.
Notice how the traffic goes to port 80 on the Minikube's IP
address and then to the Pod.
If you recall, the Service used a random port in the range of
30000.
127
Exploring the cluster with Kubectl
You learned how to create resources with kubectl apply
and how to retrieve them from the cluster with kubectl
get .
It's time to explore a few more commands that can help you
make sense of your deployments.
Kubernetes generally leverages standard RESTful
terminology to describe the API concepts:
All resource types have a schema called kind .
A single instance of a kind is called a resource.
A list of instances of a resource is known as a collection.
When designing RESTful APIs, there's a strict convention
on how you should use the HTTP methods:
128
Verb Action
POST Create
GET Read
PUT Update/Replace
PATCH Update/Modify
DELETE Delete
So if you wish to retrieve a resource you could:
GET /resource-name
Kubectl works similarly.
If you want to retrieve a Pod, you can use the get verb:
bash
$ kubectl get pod <pod-name>
If you wish to delete a resource, you can use the command
delete:
bash
129
$ kubectl delete pod <pod-name>
You can explore all the commands available with kubectl -
h , but there are two worth discussing now: describe and
logs .
kubectl describe is similar to kubectl get --output
yaml .
However, instead of display the YAML of the resource,
kubectl describe summarises the properties of the
resource in a human-readable format.
bash
$ kubectl describe pod podinfo
Name: podinfo
Namespace: default
Priority: 0
Node: minikube/192.168.64.18
Start Time: Fri, 23 Oct 2020 09:51:52 +0800
Labels: run=podinfo
Annotations: <none>
Status: Running
IP: 172.17.0.10
# truncated output
It's worth noting that most actions such as get , describe ,
delete , etc. can be applied to all resources in the cluster.
So you could describe the Ingress with:
bash
130
$ kubectl describe ingress podinfo
Name: podinfo
Namespace: default
Address: 192.168.64.18
Default backend: default-http-backend:80
Rules:
Host Path Backends
---- ---- --------
*
/ podinfo:9898 (172.17.0.10:9898)
Events:
Type Reason From Message
---- ------ ---- -------
Normal CREATE nginx-ingress-controller Ingress default/app
Normal UPDATE nginx-ingress-controller Ingress default/app
Some commands are specific to particular resources.
As an example, kubectl logs retrieves the logs from a Pod.
The command can only be applied to a Pod, so there's no
need to specific the resource type:
bash
$ kubectl logs podinfo
{"level":"info","msg":"Starting podinfo","port":"9898"}
With practise, you will notice that some commands are
more frequently used than others.
However, you can make your life easier using the cheat sheet
attached to this book.
131
Recap
A quick recap of what you've done so far:
1. You created a single Pod.
2. You exposed the Pod using a Service.
3. You exposed the application to the public using an
Ingress.
The deployment works, but you could provide better
reliability and scalability with a Deployment object.
Instead of deploying the Pod manually, you could wrap the
definition in a Deployment and have Kubernetes creating it
on your behalf.
As a benefit, Kubernetes monitors and restarts the Pod.
You can also scale the number of instances by changing the
value in the deployment.
In the next chapter, you will explore the Deployment
resource in Kubernetes.
Chapter 7
Self-healing
and scaling
133
At this point you should have a local Kubernetes cluster
with:
A single Pod running
A Service that routes traffic to a Pod
An Ingress that exposes the Pod to external sources
Having a single Pod is usually not enough.
What happens when the Pod is accidentally deleted?
It's time to find out.
Practising chaos engineering
You can delete the Pod with:
bash
$ kubectl delete pod podinfo
pod "podinfo" deleted
If you visit the app does it still work?
"503 Service Temporarily Unavailable".
But why?
You deployed a single Pod in isolation.
There's no process looking after and respawning it when it's
134
deleted.
As you can imagine, a single Pod that isn't recreated is of
limited use.
It'd be better if there could be a mechanism to watch Pods
and restart them when they are deleted.
Kubernetes has an abstraction designed to solve that specific
challenge: the Deployment object.
Here's an example for a Deployment definition:
deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: first-deployment
spec:
selector:
matchLabels:
run: podinfo
template:
metadata:
labels:
run: podinfo
spec:
containers:
- name: podinfo
image: stefanprodan/podinfo
ports:
- containerPort: 9898
Notice that, since the Deployment watches over a Pod, it
also embeds the definition of a Pod.
If you pay close attention, spec.template is the definition
of a Pod.
135
deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: first-deployment
spec:
selector:
matchLabels:
run: podinfo
template:
metadata:
labels:
run: podinfo
spec:
containers:
- name: podinfo
image: stefanprodan/podinfo
ports:
- containerPort: 9898
The spec.selector field is used to select which Pods the
Deployment should look after.
Kubernetes uses labels and selectors to link the Deployments
and the Pods.
Please note how the label is a key-value pair.
In this case, the Deployment monitors all the pods with a
run: podinfo label.
The label is repeated in the Pod definition.
deployment.yaml
136
apiVersion: apps/v1
kind: Deployment
metadata:
name: first-deployment
spec:
selector:
matchLabels:
run: podinfo
template:
metadata:
labels:
run: podinfo
spec:
containers:
- name: podinfo
image: stefanprodan/podinfo
ports:
- containerPort: 9898
You can copy the content of the above file and save it as
deployment.yaml .
You can create the Deployment in Kubernetes with:
bash
$ kubectl apply -f deployment.yaml
deployment.apps/first-deployment created
The Deployment should create a Pod that has the same label
as the one that was deleted earlier.
Can you see the app now?
Yes, it worked.
Did the Deployment create a Pod?
Let's find out:
137
bash
$ kubectl get pods
NAME READY STATUS RESTARTS
first-deployment-w4wsp 1/1 Running 0
The Deployment created a single Pod.
What happens when you delete it again?
bash
$ kubectl delete pod <replace_with_pod_id_above>
The Deployment immediately respawns another Pod.
You can verify that is the case by visiting the URL in your
browser or with:
bash
$ kubectl get pods
NAME READY STATUS RESTARTS
first-deployment-uer2x 1/1 Running 0
However, you still have a single Pod running.
How could you serve more traffic when the application
becomes more popular?
138
Scaling the application
You could ask Kubernetes to scale your Deployment and
create more copies of your Pod.
Open a new terminal and watch the number of Pods with:
bash
$ kubectl get pods --watch
NAME READY STATUS RESTARTS
first-deployment-uer2x 1/1 Running 0
In the previous terminal scale your Deployment to five
replicas with:
bash
$ kubectl scale --replicas=5 deployment first-deployment
deployment.apps/first-deployment scaled
You should see four more Pods in the terminal running the
command kubectl get pods --watch :
bash
$ kubectl get pods --watch
139
NAME READY STATUS RESTARTS
first-deployment-uer2x 1/1 Running 0
first-deployment-smd6z 1/1 Running 0
first-deployment-smq9t 1/1 Running 0
first-deployment-w4wsp 1/1 Running 0
first-deployment-w7sv4 1/1 Running 0
Unfortunately, kubectl scale is one of those imperative
commands that is not very popular in the Kubernetes
community.
You can try to edit the YAML and submit it to the cluster.
Let's scale up your deployment to ten instances by editing
the YAML configuration in place:
bash
$ kubectl edit deployment first-deployment
And change the spec.replicas field to 10.
You should look at the other terminal and notice that
Kubernetes spawned more Pods as a result.
So which command should you use, kubectl edit or
kubectl scale ?
If you can, you should avoid both.
In Kubernetes, you will create resources, change
configurations and delete resources with YAML.
So it's generally recommended to modify the YAML file on
the filesystem and resubmit it to the cluster.
140
In this particular instance, the right thing to do is to edit the
deployment.yaml as follows:
deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: first-deployment
spec:
replicas: 10
selector:
matchLabels:
run: podinfo
template:
metadata:
labels:
run: podinfo
spec:
containers:
- name: podinfo
image: stefanprodan/podinfo
ports:
- containerPort: 9898
You can resubmit the resource to the cluster with:
bash
$ kubectl apply -f deployment.yaml
Why not use kubectl scale ?
You can keep using kubectl scale , but it's something else
that you have to remember on top of all the other
commands.
141
Editing YAML is universal, and it's using the familiar
kubectl apply -f command.
Why not use kubectl edit ?
kubectl edit modifies the resource directly in the cluster.
While it's technically similar to kubectl apply -f it also
has a drawback.
You don't have a record of the change.
When you use kubectl apply you are forced to have the
file locally.
That file can be stored in version control and kept as a
reference.
There are several benefits to this pattern:
You can share your work with other colleagues.
You know who committed the change and when.
You can always revert the change to a previous version.
Since this is also a best practice, you will stop using
commands such as kubectl run and kubectl expose and
start writing YAML files from now on.
Now that you have ten replicas, the deployment can handle
ten times more requests.
But who is distributing those requests to the ten Pods?
142
Scaling with the Service
The Service is in charge of distributing requests to Pods.
Services are convenient when you have two apps in the
cluster that want to communicate.
Instead of one Pod directly talking to another Pod, you have
them going through a middleman: the Service.
In turn, the Service forwards the request to one of the Pods.
The Service is convenient because if a Pod is deleted, the
request is forwarded to another Pod.
You don't need to know the specific Pod that receives the
request; the Service chooses it for you.
Also, it doesn't have to know how many Pods are behind
that Service.
1 2
3
143
The red Pod issues a request to an internal (beige)
Fig. 1
component. Instead of choosing one of the Pods as the
destination, the red Pod issues the request to the Service.
Fig. 2 The Service forwards the traffic to one of the Pods.
Fig. 3Notice how the red Pod doesn't know how many
replicas are hidden behind the Service.
Recap
Your deployment in Kubernetes is rock solid.
When a Pod is deleted, it is automatically respawned.
You can change how many instances of your app are
running with a line change in a YAML file.
You have a solid framework that can be copy-pasted and
adapted to deployment.
You can reuse the same YAML file, and template the same
values with different names.
In your Kubernetes journey, you will
Not create Pods manually, but use Deployments instead.
Often create pairs of Deployment and Service.
Occasionally create Ingress resources to expose a few
144
selected Services in the cluster.
Until now, the Deployment you created used the premade
container image stefanprodan/podinfo .
What if you wish to deploy your image?
In the next chapter, you will code, package and deploy a
simple application with what you've learned so far.
Chapter 8
Creating the
app end-to-
end
146
Applications deployed in Kubernetes are usually packaged as
containers.
So let's explore the workflow to build and package
applications to deploy them in Kubernetes.
Since the focus is not on development, you will deploy the
following static web page served by Nginx.
Fig. A static web page served by Nginx
147
Creating the application
You should create a directory where you can save the files:
bash
$ mkdir -p first-k8s-app
$ cd first-k8s-app
Next, you should create an index.html file with the
following content:
<html style="background-color:#FF003C">
<head>
<title>v1.0.0</title>
</head>
<body style="display:flex;
align-items:center;
justify-content:center;
color:#FFFFFF;
font-family:sans-serif;
font-size:6rem;margin:0;
letter-spacing:-0.1em">
<h1>v1.0.0</h1>
</body>
</html>
You can open the file in your browser of choice and see what
it looks like.
It's a boring HTML page!
148
A static HTML file doesn't present the same challenges as a
fully-fledged application written in Python, Node.js or Java.
It doesn't connect to a database or use CPUs to process data-
intensive workloads.
However, it's enough to get an idea of what's necessary to
upgrade your development workflow with Kubernetes.
You have the app, so it's time to package it as a container
image.
Containerising the application
First of all, you have to install the Docker Community
Edition (CE).
You can follow the instructions in the official Docker
documentation.
If you need help, you will find the instructions on how to
install Docker at the back of this book.
You can verify that Docker is installed correctly with the
following command:
bash
$ docker run hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.
149
You're now ready to build Docker containers.
Docker containers are built from Dockerfile s.
A Dockerfile is like a recipe — it defines what goes in a
container.
A Dockerfile consists of a sequence of commands.
You can find the full list of commands in the
Dockerfile reference.
Here is a Dockerfile that packages your app into a
container image:
Dockerfile
FROM nginx:1.21.4
EXPOSE 80
COPY index.html /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"]
The file above reads as follows:
FROM nginx:1.21.4 is the base image. The image
contains a minimal version of the Debian operating
system with Nginx installed.
EXPOSE 80 is a comment and doesn't expose port 80.
It's only a friendly reminder that the process exposes
port 80.
150
COPY index.html /usr/share/nginx/html copies the
HTML file in the container.
CMD ["nginx", "-g", "daemon off;"] launches the
Nginx process without making it a background process.
You can build the image with:
bash
$ docker build -t first-k8s-app .
Sending build context to Docker daemon 3.072kB
Step 1/4 : FROM nginx:1.21.4
---> 5a8dfb2ca731
Step 2/4 : EXPOSE 80
---> Using cache
---> fcda535bf090
Step 3/4 : COPY index.html /usr/share/nginx/html
---> Using cache
---> 3581fd7eed08
Step 4/4 : CMD ["nginx", "-g", "daemon off;"]
---> Using cache
---> 253e0d2e6293
Successfully built 253e0d2e6293
Successfully tagged first-k8s-app:latest
Note the following about this command:
-t app defines the name ("tag") of your container — in
this case, your container is just called app .
. is the location of the Dockerfile and application code
— in this case, it's the current directory.
The command executes the steps outlined in the
Dockerfile , one by one:
151
Docker creates a layer for every instruction in the
Fig.
Dockerfile.
The output is a Docker image.
What is a Docker image?
A Docker image is an archive containing all the files that go
in a container.
You can create many Docker containers from the same
Docker image:
152
From the same `Dockerfile` you always obtain the
Fig.
same Docker image. You can instantiate many Docker
containers from the same image
Don't believe that Docker images are archives? Save
the image locally with docker save app > app.tar
and inspect it.
You can list all the images on your system with the following
153
command:
bash
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
first-k8s-app latest dc2a8fd35e2e 30 seconds ago 165MB
nginx 1.17.9 d9bfca6c7741 2 weeks ago 150MB
You should see the app image that you built.
You should also see the nginx:1.21.4 which is the base
layer of your app image — it is just an ordinary image as
well, and the docker build command downloaded it
automatically from Docker Hub.
Docker Hub is a container registry — a place to
distribute and share container images.
You packaged your app as a Docker image — let's run it as a
container.
Running the container
You can run your app as follows:
154
bash
$ docker run -ti --rm -p 8080:80 first-k8s-app
Note the following about this command:
--rm automatically cleans up the container and
removes the file system when the container exits.
-p 8080:80 publishes port 80 of the container to port
8080 of your local machine. That means, if you now
access port 8080 on your computer, the request is
forwarded to port 80 of the app. You can use the
forwarding to access the app from your local machine.
-ti attached the output of the terminal to the current
terminal session.
You can test that the container runs successfully with:
bash
$ curl localhost:8080
<html style="background-color:#FF003C">
<head>
<title>v1.0.0</title>
</head>
<body style="display:flex;
align-items:center;
justify-content:center;
color:#FFFFFF;
font-family:sans-serif;
font-size:6rem;margin:0;
letter-spacing:-0.1em">
<h1>v1.0.0</h1>
</body>
</html>
155
You can display all running containers with the following
command:
bash
$ docker ps
CONTAINER ID IMAGE COMMAND PORTS NAMES
41b50740a920 first-k8s-app "nginx -g 'daemon of…" 0.0.0.0:8080->80/tcp busy_allen
Great!
It's time to test your application!
You can visit http://localhost:8080 with your browser to
inspect the containerised app.
When you're done experimenting, stop the container with
Ctrl+C.
Uploading the container image to a
container registry
Imagine you want to share your app with a friend — how
would you go about sharing your container image?
Sure, you could save the image to disk and send it to your
friend.
But there is a better way.
When you used the Nginx as a base image, you specified its
156
Docker Hub ID ( nginx ), and Docker automatically
downloaded it.
Perhaps you could create your image and upload it to
Docker Hub?
If your friend doesn't have the image locally, Docker
automatically pulls the image from Docker Hub.
Other public container registries exist, such as Quay
— however, Docker Hub is the default registry used
by Docker.
To use Docker Hub, you first have to create a Docker ID.
A Docker ID is your Docker Hub username.
Once you have your Docker ID, you have to authorise
Docker to connect to the Docker Hub account:
bash
$ docker login
Before you can upload your image, there is one last thing to
do.
Images uploaded to Docker Hub must have a name of the
form username/image:tag :
username is your Docker ID.
image is the name of the image.
157
tag is an optional additional attribute — often it is
used to indicate the version of the image.
To rename your image according to this format, run the
following command:
bash
$ docker tag app <username>/first-k8s-app:1.0.0
Please replace <username> with your Docker ID.
Now you can upload your image to Docker Hub:
bash
$ docker push <username>/first-k8s-app:1.0.0
Your image is now publicly available as
<username>/app:1.0.0 on Docker Hub and everybody can
download and run it.
To verify this, you can re-run your app, but this time using
the new image name.
158
Please note that the command below runs the
learnk8s/first-k8s-app:1.0.0 image. If you wish
to use yours, replace learnk8s with your Docker
ID.
bash
$ docker run -ti --rm -p 8080:80 learnk8s/first-k8s-app:1.0.0
Everything should work exactly as before.
Note that now everybody in the world can run your
application by executing the above two commands.
And the app will run on their machine precisely as it runs
on yours — without installing any dependencies.
This is the power of containerisation!
Once you're done testing your app, you can stop and remove
the containers with Ctrl+C.
In this section, you learned how to package your app as a
Docker image and run it as a container.
In the next section, you will learn how to run your
containerised application on Kubernetes!
But before deploying the application, let's talk about sharing
resources.
159
Sharing resources
You might have noticed that the Pod you created isn't the
only Pod in the cluster.
You can list all the Pods in the cluster with:
bash
$ kubectl get pods --all-namespaces
NAMESPACE NAME READY STATUS
kube-system coredns-74ff55c5b-t444w 1/1 Running
kube-system etcd-minikube 1/1 Running
kube-system kube-apiserver-minikube 1/1 Running
kube-system kube-controller-manager-minikube 1/1 Running
kube-system kube-proxy-98q84 1/1 Running
kube-system kube-scheduler-minikube 1/1 Running
kube-system storage-provisioner 1/1 Running
But why are the other Pods not visible by default?
Why are you using --all-namespaces ?
Kubernetes clusters could have hundreds of different Pods
and serve dozens of different teams.
There should be a way to segregate and isolate resources.
As an example, you might want to group all the resources
that belong to the backend API of your website.
Those resources could include:
Deployments for all the microservices.
Services.
160
Ingress to expose the APIs.
You can create logical sets of resources using a feature called
Namespaces.
A Namespace is like a basket where you can store your
resources.
Fig. You can group Kubernetes resources in namespaces
When you create, list, update or retrieve a resource,
kubectl executes the current command in the current
161
Namespace.
If you create a Pod, the Pod is created in the current
Namespace.
Unless, of course, you override the request and select a
different Namespace:
bash
$ kubectl get pods --namespace kube-system
NAME READY STATUS RESTARTS AGE
coredns-f9fd979d6-vnqnb 0/1 Running 0 32s
etcd 0/1 Running 0 24s
kube-apiserver 1/1 Running 0 24s
kube-controller-manager 0/1 Running 0 24s
kube-proxy-m2929 1/1 Running 0 32s
kube-scheduler 0/1 Running 0 24s
storage-provisioner 1/1 Running 0 26s
Please note that you can customise the current
Namespace for kubectl by changing your context
with kubectl config set-context --current --
namespace=<insert-namespace-name-here> . If you
don't customise it, the Namespace default is
selected by default.
Kubernetes has a few Namespaces already.
You can list all Namespaces with:
bash
162
$ kubectl get namespaces
NAME STATUS AGE
default Active 107s
kube-node-lease Active 111s
kube-public Active 111s
kube-system Active 111s
And you can create your Namespace too.
First of all, create a folder named kube in your application
directory:
bash
$ mkdir kube
The purpose of this folder is to hold all the Kubernetes
YAML files that you will create.
It's best practice to group all resource definitions for
an application in the same folder because this allows
you to submit them to the cluster with a single
command.
Create a namespace.yaml file:
kube/namespace.yaml
apiVersion: v1
kind: Namespace
163
metadata:
name: first-k8s-app
You can submit the resource to the cluster with:
bash
$ kubectl apply -f kube/namespace.yaml
namespace/first-k8s-app created
You can verify that the Namespace was created with:
bash
$ kubectl get namespaces
NAME STATUS AGE
default Active 3m1s
first-k8s-app Active 10s
kube-node-lease Active 3m5s
kube-public Active 3m5s
kube-system Active 3m5s
The Namespace first-k8s-app should be part of that list.
Is there any Pod in that Namespace?
You can check it with:
$ kubectl get pods --namespace first-k8s-app
No resources found in first-k8s-app namespace.
No, there isn't.
164
But you will create one now!
Since you will create all of the resources in the first-k8s-
app Namespace, it's a good idea to configure your kubectl
command line to always use that Namespace instead of the
default .
You can do so with:
bash
$ kubectl config set-context --current --namespace=first-k8s-app
Context "minikube" modified.
A word of caution about Namespaces.
Even if your resources or team are organised in Namespaces,
there's no built-in security mechanism.
Pods can still talk to other Pods even if they are in different
Namespaces.
If a Pod abuses the resources of the cluster, all Pods might be
affected, even if they are in different namespaces.
165
Fig. Namespaces are not designed to isolate workloads
Namespaces are useful for grouping resources logically.
However, they are not designed to provide rigid boundaries
for your workloads.
Let's package and deploy the app in the new Namespace.
166
Deploying the app in Kubernetes
The first time you deployed an app in Kubernetes, you used
the commands kubectl run , and kubectl expose .
kubectl run created a Pod template, replaced the values
you passed as arguments and submitted it to the cluster.
You also learned that:
1. Creating Pods manually isn't always a good idea. If the
Pod is deleted, there is no one to recreate a new one.
2. If you use kubectl run you can't save the YAML
resource locally.
Instead of using kubectl run , this time, you will create a
Deployment definition and use kubectl apply to submit
the changes.
Let's get started.
Create a Deployment file.
Here is the definition:
kube/app.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
167
replicas: 1
selector:
matchLabels:
name: app
template:
metadata:
labels:
name: app
spec:
containers:
- name: app
image: learnk8s/first-k8s-app:1.0.0
ports:
- containerPort: 80
imagePullPolicy: Always
That looks complicated, but we will break it down and
explain it in detail.
For now, save the above content in a file named app.yaml in
the kube folder.
Please note that the command below runs the
learnk8s/first-k8s-app:1.0.0 image. If you wish
to use yours, replace learnk8s with your Docker
ID.
You must be wondering how you can find out about the
structure of a Kubernetes resource.
The answer is, in the Kubernetes API reference.
The Kubernetes API reference contains the specification for
every Kubernetes resource, including all the available fields,
168
their data types, default values, required fields, and so on.
Here is the specification of the Deployment resource.
If you prefer to work in the command-line, there's an even
better way.
The kubectl explain command can print the specification
of every Kubernetes resource directly in your terminal:
bash
$ kubectl explain deployment
KIND: Deployment
VERSION: apps/v1
DESCRIPTION:
Deployment enables declarative updates for Pods and ReplicaSets.
# truncated output
The command outputs precisely the same information as
the web-based API reference.
To drill down to a specific field use:
bash
$ kubectl explain deployment.spec.replicas
KIND: Deployment
VERSION: apps/v1
FIELD: replicas <integer>
DESCRIPTION:
Number of desired pods. This is a pointer to distinguish between explicit
zero and not specified. Defaults to 1.
Now that you know how to look up the documentation of
Kubernetes resources, let's turn back to the Deployment.
169
The first four lines define the type of resource
( Deployment ), the version of this resource type ( apps/v1 ),
and the name of this specific resource ( app ):
kube/app.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
replicas: 1
selector:
matchLabels:
name: app
template:
metadata:
labels:
name: app
spec:
containers:
- name: app
image: learnk8s/first-k8s-app:1.0.0
ports:
- containerPort: 80
imagePullPolicy: Always
Next, you have the desired number of replicas of your Pod:
1.
You don't usually talk about containers in Kubernetes.
Instead, you talk about Pods.
What is a Pod?
A Pod is a wrapper around one or more containers.
Most often, a Pod contains only a single container —
however, for advanced use cases, a Pod may have multiple
170
containers.
If a Pod contains multiple containers, they are treated by
Kubernetes as a unit — for example, they are started and
stopped together and executed on the same node.
A Pod is the smallest unit of deployment in Kubernetes —
you never work with containers directly, but with Pods that
wrap containers.
Technically, a Pod is a Kubernetes resource, like a
Deployment or Service.
Let's turn back to the Deployment resource.
The next part ties together the Deployment resource with
the Pod replicas:
kube/app.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
replicas: 1
selector:
matchLabels:
name: app
template:
metadata:
labels:
name: app
spec:
containers:
- name: app
image: learnk8s/first-k8s-app:1.0.0
ports:
- containerPort: 80
imagePullPolicy: Always
171
The template.metadata:labels field defines a label for the
Pods that wraps your container ( name: app ).
The selector.matchLabels field selects those Pods with a
name: app label to belong to this Deployment resource.
Note that there must be at least one shared label
between these two fields.
The next part in the Deployment defines the actual
container that you want to run:
kube/app.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
replicas: 1
selector:
matchLabels:
name: app
template:
metadata:
labels:
name: app
spec:
containers:
- name: app
image: learnk8s/first-k8s-app:1.0.0
ports:
- containerPort: 80
imagePullPolicy: Always
It defines the following things:
172
A name for the container ( app ).
The name of the Docker image ( learnk8s/first-k8s-
app:1.0.0 or <username>/first-k8s-app:1.0.0 if
you're using your image).
The port that the container listens on (80).
The above arguments should look familiar to you: you used
similar ones when you ran your app with docker run in the
previous section.
That's not a coincidence.
When you submit a Deployment resource to the cluster, you
can imagine Kubernetes executing docker run and
launching your container in one of the computers.
The container specification also defines an
imagePullPolicy of Always — the instruction forces the
Docker image to be downloaded, even if it was already
downloaded.
A Deployment defines how to run an app in the cluster, but
it doesn't make it available to other apps.
To expose your app, you need a Service.
However, instead of using kubectl expose , this time, you
will create a Service YAML definition.
Defining a Service
173
A Service resource makes Pods accessible to other Pods or
users inside and outside the cluster.
Fig. Services in Kubernetes
In this regard, a Service is akin to a load balancer.
Here is the definition of a Service that makes your Pod
accessible to other Pods or users:
kube/app.yaml
174
apiVersion: v1
kind: Service
metadata:
name: app
spec:
selector:
name: app
ports:
- port: 8080
targetPort: 80
Again, to find out about the available fields of a
Service, look it up in the API reference, or, even
better, use kubectl explain service .
Where should you save the above definition?
It is best-practice to save resource definitions that belong to
the same application in the same YAML file.
To do so, paste the above content at the beginning of your
existing app.yaml file, and separate the Service and
Deployment resources with three dashes like this:
kube/app.yaml
# ... Deployment YAML definition
---
# ... Service YAML definition
175
You can find the final YAML files for this section in
this repository.
Let's break down the Service resource.
It consists of three crucial parts.
The first part is the selector:
kube/app.yaml
apiVersion: v1
kind: Service
metadata:
name: app
spec:
selector:
name: app
ports:
- port: 8080
targetPort: 80
It selects the Pods to expose according to their labels.
In this case, all Pods that have a label of name: app will be
exposed by the Service.
Note how this label corresponds exactly to what you
specified for the Pods in the Deployment resource:
kube/app.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
176
spec:
# ...
template:
metadata:
labels:
name: app
# ...
It is this label that ties your Service to your Deployment
resource.
The next important part is the port:
kube/app.yaml
apiVersion: v1
kind: Service
metadata:
name: app
spec:
selector:
name: app
ports:
- port: 8080
targetPort: 80
In this case, the Service listens for requests on port 8080 and
forwards them to port 80 of the target Pods:
177
Fig. Service and ports
Beyond exposing your containers, a Service also ensures
continuous availability for your app.
If one of the Pods crashes and is restarted, the Service makes
sure not to route traffic to this container until it is ready
again.
Also, when the Pod is restarted, and a new IP address is
assigned, the Service automatically handles the update too.
178
Furthermore, if you decide to scale your Deployment to 2, 3,
4, or 100 replicas, the Service keeps track of all of these Pods.
The last resource that you need is the Ingress.
Defining an Ingress
The Ingress is the component that exposes your Pods to the
public internet.
The definition for the resource is similar to what you used in
the previous chapter:
kube/app.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- http:
paths:
- path: /second
pathType: Prefix
backend:
service:
name: app
port:
number: 8080
179
You can paste the above content at the end of your existing
app.yaml file, after Deployment and Service like this:
kube/app.yaml
# ... Deployment YAML definition
---
# ... Service YAML definition
---
# ... Ingress YAML definition
Let's break down the Ingress resource.
It consists of three crucial parts.
The first part is the annotation
nginx.ingress.kubernetes.io/rewrite-target: / .
Annotations are arbitrary values that you can assign to
Kubernetes resources.
You can create and add your annotations and Kubernetes
will gladly accept them.
Annotations are often used to decorate resources with extra
arguments.
In this case, the annotation is meant to signal that the base
path is rewritten to / .
The next part that you should pay attention to is the
spec.rules .
kube/app.yaml
apiVersion: networking.k8s.io/v1
180
kind: Ingress
metadata:
name: app
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- http:
paths:
- path: /second
pathType: Prefix
backend:
service:
name: app
port:
number: 8080
spec.rules is a collection of rules on how the traffic should
be routed to your Services.
A single Ingress manifest could have several rules and target
different Services.
Each rule can be customised to match:
A particular path — in this case /second .
The target service — in this case, the Service is app , and
the port is 8080.
A hostname — for example, only traffic from
example.com should go to the Service.
And many more.
Here's a visual recap of your deployment:
181
Fig. Ingress, Service and Pod ports
This completes the description of your app — a
Deployment, a Service, and an Ingress are all you need.
Deploying the application
182
So far, you have created a few YAML files with resource
definitions.
You didn't yet touch the cluster.
But now comes the big moment!
You are going to submit your resource definitions to
Kubernetes.
And Kubernetes will bring your application to life.
First of all, make sure that you have an app.yaml in the
kube directory:
bash
$ tree .
.
├── Dockerfile
├── index.html
└── kube
├── app.yaml
└── namespace.yaml
You can find these files also in this repository.
Also, make sure that your Minikube cluster is running:
bash
$ minikube status
minikube
type: Control Plane
host: Running
kubelet:
183
Running
apiserver: Running
kubeconfig: Configured
Then submit your resource definitions to Kubernetes with
the following command:
bash
$ kubectl apply -f kube
deployment.apps/app created
service/app created
ingress.networking.k8s.io/app created
namespace/first-k8s-app unchanged
This command submits all the YAML files in the kube
directory to Kubernetes.
The -f flag accepts either a single filename or a
directory. In the latter case, all YAML files in the
directory are submitted.
As soon as Kubernetes receives your resources, it creates the
Pods.
You can watch your Pods coming alive with:
bash
$ kubectl get pods --watch
NAME READY STATUS RESTARTS
184
AGE
app-5459b555fd-gdbx8 0/1 ContainerCreating 0 36s
You should see the Pods transitioning from Pending to
ContainerCreating to Running.
As soon as the Pod is in the Running state, your application
is ready.
As discussed previously, it's time to connect to the cluster.
You have three options:
1. kubectl port-forward .
2. minikube tunnel .
3. Via the cluster IP address.
1. Creating a Kubectl-portward tunnel
You can reuse the same tunnel as before.
If you've stopped it, you can create it with:
bash
$ kubectl port-forward service/ingress-nginx-controller -n ingress-nginx 8080:80
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80
Visit your app at http://localhost:8080/second in your
browser.
2. Creating a tunnel with minikube tunnel
185
You can reuse the same tunnel as before.
If you've stopped it, you can create it with:
bash
$ minikube tunnel
Password:
Status:
machine: minikube
pid: 39087
route: 10.96.0.0/12 -> 192.168.64.194
minikube: Running
services: [podinfo]
errors:
minikube: no errors
router: no errors
loadbalancer emulator: no errors
# truncated output
Now you can retrieve the IP address of the Ingress with:
bash
$ kubectl get ingress podinfo
NAME CLASS HOSTS ADDRESS PORTS
appp <none> * 192.168.64.18 80
Pay attention to the address column.
Visit your app at http://[ADDRESS COLUMN]/second in your
browser.
3. Accessing the cluster IP address
Retrieve the IP of Minikube with:
186
bash
$ minikube ip
192.168.64.18
Visit the URL http://<replace with minikube
ip>/second in your browser.
Congratulations!
Recap and tidy-up
You:
1. Wrote a small application.
2. Packaged it as a container.
3. Deployed in Kubernetes.
When you're done testing the app, you can remove it from
the cluster with the following command:
bash
$ kubectl delete -f kube
deployment.apps "app" deleted
service "app" deleted
ingress.networking.k8s.io "app" deleted
namespace "first-k8s-app" deleted
187
The command deletes all the resources that were created by
kubectl apply .
In this section, you learned how to deploy an application to
Kubernetes.
In the next section, you will be practising deploying
applications in Kubernetes.
Deploying an application in Kubernetes requires you to:
1. Create the app and package it as a container.
2. If you use Docker, you can define how to build the
container image with a Dockerfile .
3. Container images are tar archives. You can share them
with friends and colleagues with container registries
such as Docker Hub.
4. Any Kubernetes cluster can download and run the same
images.
5. You deploy the container to Kubernetes using a
Deployment, Service and Ingress.
6. You can define the Kubernetes resources in YAML in a
single file using --- as a separator.
7. You can use kubectl apply to submit all the resources
at once.
8. If you change the context of kubectl, you can deploy the
resources into a different Namespace.
Chapter 9
Practising
deployments
189
In the previous chapters, you learned how to:
1. Package an application in a Docker container.
2. Deploy your container with a Deployment.
3. Expose your Pods with Services.
4. Expose the Pods to external traffic using an Ingress.
5. Group sets of resources using Namespaces.
You also learned that commands such as kubectl run and
kubectl expose should be dismissed in favour of YAML
files.
It's time to practise what you learned so far and deploy a few
more applications.
If, at any time, you need help, the solutions are also
available in this repository.
Version 2.0.0
You should deploy in Minikube an app that uses Nginx and
serves a static HTML file.
The content of the HTML file is:
190
index.html
<html style="background-color:#FABE28">
<head>
<title>v2.0.0</title>
</head>
<body style="display:flex;
align-items:center;
justify-content:center;
color:#000000;
font-family:sans-serif;
font-size:6rem;
margin:0;
letter-spacing:-0.1em">
<h1>v2.0.0</h1>
</body>
</html>
You should:
Namespace all resources in Kubernetes.
Package the application as a Docker container.
Deploy it in Minikube.
Scale the Deployment to two replicas.
Expose the app with a Service.
Expose the app with an Ingress with path /v2 .
Use YAML resource definitions (please don't use
kubectl run , kubectl expose or kubectl scale ).
In particular, you should create:
a Namespace resource.
a Deployment resource.
a Service resource.
191
an Ingress resource.
If you're not sure how to create those resources, you can
have a look at the samples below.
Deployment
You can find the official documentation for Deployments
here.
A Deployment looks like this:
deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-name
labels:
key: pod-label
spec:
selector:
matchLabels:
key: pod-label
template:
metadata:
labels:
key: pod-label
spec:
containers:
- name: container-name
image: container-image
ports:
- containerPort: 3000
Service
You can find the official documentation for Services here.
192
A Service looks like this:
service.yaml
apiVersion: v1
kind: Service
metadata:
name: service-name
spec:
selector:
key: value
ports:
- port: 3000
targetPort: 9376
Ingress
You can find the official documentation for the Ingress here
An Ingress manifest looks like this:
ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-name
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: service-name
port:
number: 8888
193
Namespace
You can find the official documentation for the Namespace
here
A Namespace looks like this:
namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: namespace-name
Version 3.0.0
How does the Deployment change when using an app
written in Python?
Let's assume you created a python directory and saved the
following HTML file as index.html :
index.html
<html style="background-color:#00C176">
<head>
<title>API</title>
</head>
<body style="display:flex;
align-items:center;
justify-content:center;
color:#FFFFFF;
194
font-family:sans-serif;
font-size:6rem;
margin:0;
letter-spacing:-0.1em">
<h1>API</h1>
</body>
</html>
Python can be used to serve files located in the current
directory with:
bash
$ python -m http.server
The command assumes you use Python 3.
The server will be available on http://localhost:8000.
Can you package the above Python web server and static
HTML file as a container?
Also, can you create a Deployment?
You should:
Package the application as a Docker container. You
might need to customise the Dockerfile and a different
base image.
Deploy it in Minikube.
Scale the Deployment to two replicas.
195
Expose the app with a Service.
Expose the app with an Ingress with the domain name
k8s-is-great.com .
Use YAML resource definitions.
Group the resources.
Please note that you might need to use curl to test the
domain-based routing of the Ingress.
You can use the following command to send a request with a
specific domain name:
bash
$ curl --header 'Host: k8s-is-great.com' http://<replace with minikube ip>
If you are stuck
You can get in touch with the Learnk8s instructor on Slack.
Chapter 10
Making
sense of
Deployments,
Services and
Ingresses
197
When you deployed the application in Kubernetes, you
defined three components:
a Deployment — which is a recipe for creating copies of
your application called Pods.
a Service — an internal load balancer that routes the
traffic to Pods.
an Ingress — a description of how the traffic should flow
from outside the cluster to your Service.
Here's a quick visual recap.
198
Pods, Deployments, Services and Ingresses in
Fig.
Kubernetes
The YAML for such an application is similar to this:
hello-world.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-hello-world
labels:
199
track: canary
spec:
selector:
matchLabels:
run: pod-hello-world
template:
metadata:
labels:
run: pod-hello-world
spec:
containers:
- name: cont1
image: learnk8s/app:1.0.0
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: service-hello-world
spec:
ports:
- port: 80
targetPort: 8080
selector:
run: pod-hello-world
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-hello-world
spec:
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: service-hello-world
port:
number: 80
You can deploy the above app with:
200
bash
$ kubectl apply -f hello-world.yaml
deployment.apps/deployment-hello-world created
service/service-hello-world created
ingress.networking.k8s.io/ingress-hello-world created
Let's focus on the YAML definition as it's easy to overlook
how the components relate to each other.
At this point, you might have a few questions:
When should you use port 80 and when port 8080?
Should you create a new port for every Service so that
they don't clash?
Do label names matter? Should it be the same
everywhere?
To answer these questions, it's crucial to understand how
the three components link to each other.
Let's start with Deployment and Service.
Connecting Deployment and
Service
The surprising news is that Service and Deployment aren't
connected at all.
201
Instead, the Service points to the Pods directly and skips the
Deployment altogether.
So what you should pay attention to is how the Pods and the
Service are related to each other.
You should remember three things:
1. The Service selector should match at least one label of
the Pod.
2. The Service targetPort should match the
containerPort of the container inside the Pod.
3. The Service port can be any number. Multiple Services
can use the same port because they have different IP
addresses assigned.
The following diagram summarises how to connect the
ports:
202
Fig. Connecting Pods to Services
If you look at the YAML, the labels and
ports / targetPort should match:
hello-world.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-hello-world
labels:
203
track: canary
spec:
selector:
matchLabels:
run: pod-hello-world
template:
metadata:
labels:
run: pod-hello-world
spec:
containers:
- name: cont1
image: learnk8s/app:1.0.0
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: service-hello-world
spec:
ports:
- port: 80
targetPort: 8080
selector:
run: pod-hello-world
What about the track: canary label at the top of the
Deployment?
Should that match too?
That label belongs to the deployment, and it's not used by
the Service's selector to route traffic.
In other words, you can safely remove it or assign it a
different value.
And what about the matchLabels selector?
It always has to match the Pod labels and it's used by the
204
Deployment to track the Pods.
Assuming that you made the correct change, how do you
test it?
You can check if the Pods have the right label with the
following command:
bash
$ kubectl get pods --show-labels
NAME READY LABELS
deployment-hello-world 1/1 run=pod-hello-world
Or if you have Pods belonging to several applications:
bash
$ kubectl get pods --selector run=pod-hello-world --show-labels
Where run=pod-hello-world is the label run: pod-hello-
world .
Still having issues?
You can also connect to the Pod!
You can use the port-forward command in kubectl to
connect to the Service and test the connection.
bash
$ kubectl port-forward service/service-hello-world 8080:80
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080
205
Where:
service/service-hello-world is the name of the
service.
8080 is the port that you wish to open on your
computer.
80 is the port exposed by the Service in the port field.
If you can connect, the setup is correct.
If you can't, you most likely misplaced a label or the port
doesn't match.
Connecting Service and Ingress
The next step in exposing your app is to configure the
Ingress.
The Ingress has to know how to reach the Service to then
forward the traffic to the Pods.
The Ingress retrieves the right Service by name and port.
Two things should match in the Ingress and Service:
1. The servicePort of the Ingress should match the
port of the Service.
2. The serviceName of the Ingress should match the
name of the Service.
206
The following diagram summarises how to connect the
ports:
Fig. Connecting the Ingress to the Service
In practice, you should look at these lines:
hello-world.yaml
apiVersion: v1
kind: Service
metadata:
207
name: service-hello-world
spec:
ports:
- port: 80
targetPort: 8080
selector:
run: pod-hello-world
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-hello-world
spec:
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: service-hello-world
port:
number: 80
How do you test that the Ingress works?
You can use the same strategy as before with kubectl port-
forward , but instead of connecting to a service, you should
connect to the Ingress controller.
First, retrieve the Pod name for the Ingress controller with:
bash
$ kubectl get pods --all-namespaces
NAMESPACE NAME READY STATUS
kube-system coredns-5644d7b6d9-jn7cq 1/1 Running
kube-system etcd-minikube 1/1 Running
kube-system kube-apiserver-minikube 1/1 Running
kube-system kube-controller-manager-minikube 1/1 Running
kube-system kube-proxy-zvf2h 1/1
208
Running
kube-system kube-scheduler-minikube 1/1 Running
kube-system nginx-ingress-controller-6fc5bcc 1/1 Running
Identify the Ingress Pod (which might be in a different
Namespace) and describe it to retrieve the port:
bash
$ kubectl describe pod nginx-ingress-controller-6fc5bcc \
--namespace kube-system \
| grep Ports
Ports: 80/TCP, 443/TCP, 18080/TCP
Finally, connect to the Pod:
bash
$ kubectl port-forward nginx-ingress-controller-6fc5bcc 8080:80 \
--namespace kube-system
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80
At this point, every time you visit port 8080 on your
computer, the request is forwarded to port 80 on the Ingress
controller Pod.
If you visit http://localhost:8080, you should find the app
serving a web page.
209
Recap
Here's a quick recap on which ports and labels should
match:
1. The Service selector should match the label of the Pod.
2. The Service targetPort should match the
containerPort of the container inside the Pod.
3. The Service port can be any number. Multiple Services
can use the same port because they have different IP
addresses assigned.
4. The servicePort of the Ingress should match the
port in the Service.
5. The name of the Service should match the field
serviceName in the Ingress.
Chapter 11
Installing
Docker,
Minikube and
kubectl
211
Docker, minikube and kubectl are the only prerequisites
necessary for following the examples presented in this book.
Below you can find the instructions on how to install them.
macOS
Windows 10
Ubuntu
macOS
Installing Homebrew
Homebrew is a package manager for macOS.
You tell it what binaries you wish to install, and Homebrew
installs them on your behalf.
While you can install all the dependencies manually, it's
easier to install them with Homebrew.
Installing Homebrew is easy.
You can find the full instructions on the official website.
But, in a nutshell, this is what you have to do.
Start a terminal session and execute this command:
212
bash
$ /usr/bin/ruby -e \
"$(curl -fsSL https://raw.github.com/Homebrew/install/HEAD/install)"
Now let's verify that Homebrew is set up correctly.
Execute this command:
bash
$ brew doctor
Your system is ready to brew.
Installing Docker on macOS
You can download Docker for Mac with:
bash
$ brew install --cask docker
==> Downloading …
####################################################### 100.0%
==> Installing Cask docker
==> Moving App 'Docker.app' to '/Applications/Docker.app'.
🍺 docker was successfully installed!
You should start Docker. It will ask permission to install the
networking component.
213
Fig. Docker networking component
You should proceed.
You can find Docker in your Applications folder.
Testing your Docker installation
You should test that your Docker installation was successful.
In your terminal type:
bash
$ docker run hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.
Congratulations!
The installation was successful.
214
Installing minikube on macOS
You can download minikube with:
bash
$ brew install minikube
==> Downloading …
####################################################### 100.0%
==> Installing dependencies for minikube: kubernetes-cli
==> Installing minikube dependency: kubernetes-cli
==> Pouring kubernetes-cli.tar.gz
==> Summary
🍺 /usr/local/Cellar/kubernetes-cli: 246 files, 46.1MB
==> Installing minikube
==> Pouring minikube.tar.gz
==> Summary
🍺 /usr/local/Cellar/minikube: 8 files, 62.4MB
Please note how kubectl is automatically installed
when you install minikube .
You can test your minikube installation with:
Please note that running minikube as root is
discouraged. Please run minikube as a standard user
and not sudo .
bash
215
$ minikube start
👍 Starting control plane node minikube in cluster minikube
🐳 Preparing Kubernetes v1.23.0 ...
🔎 Verifying Kubernetes components...
🌟 Enabled addons: default-storageclass, storage-provisioner
🏄 Done! kubectl is now configured to use "minikube" by default
If for any reason minikube fails to start up, you can debug it
with:
bash
$ minikube delete
🔥 Deleting "minikube" ...
💀 Removed all traces of the "minikube" cluster.
$ minikube start --v=7 --alsologtostderr
The extra verbose logging should help you get to the issue.
It's time to test if your installation is successful.
Before you do so, in the command prompt type:
bash
$ kubectl get nodes
NAME STATUS ROLES AGE
minikube Ready master 88s
One last thing.
Since kubectl makes requests to the Kubernetes API, it's
worth checking that you are not using an older version of
kubectl.
216
bash
$ kubectl version
Client Version: version.Info{Major:"1", Minor:"19", GitVersion:"v1.18.3"}
Server Version: version.Info{Major:"1", Minor:"18", GitVersion:"v1.18.3"}
The Kubernetes API supports up to two older versions for
backwards compatibility.
So you could have the client (kubectl) on version 1.16 and
the server on 1.18.
But you cannot use kubectl on version 1.15 with a cluster
that runs on 1.19.
Windows 10
PowerShell prerequisites
You should have the Powershell installed (5.0 or above).
You can check the version of your PowerShell with:
PowerShell — □ 𝖷
PS> $PSVersionTable.PSVersion
Major Minor Build Revision
----- ----- ----- --------
5 1 17134 858
217
You will be using it to install and run the code.
Please notice that PowerShell ISE is not
recommended.
Installing Chocolatey
Chocolatey is a package manager for Windows.
You tell it what binaries you wish to install, and Chocolatey
installs them on your behalf.
While you can install all the dependencies manually, it's
easier to install them with Chocolatey.
Chocolatey automatically sources and installs dependencies,
sets the right PATH for the executables and can manage
updates as well as versions.
Installing Chocolatey is easy.
You can find the full instructions on the official website.
Installing Docker on Windows 10
You can download Docker for Windows with:
PowerShell — □ 𝖷
PS> choco install docker-desktop -y
218
Installing the following packages:
docker-desktop
By installing you accept licenses for the packages.
# truncated output
Installing docker-desktop...
docker-desktop has been installed.
You should be aware that Docker requires VT-X/AMD-v
virtual hardware extension to be enabled before you can run
any container. Depending on your computer, you may need
to reboot and enable it in your BIOS.
If you're unsure VT-X/AMD-v was enabled, don't worry. If
you don't have it, Docker will greet you with the following
error message:
Hardware assisted virtualization and data execution
protection must be enabled in the BIOS.
219
Fig. You should enable VT-X/AMD-v
Another common error has to do with the Hyper-V
hypervisor not being enabled. If you experience this error:
Unable to start: The running command stopped
because the preference variable
"ErrorActionPreference" or common parameter is
set to Stop: 'MobyLinuxVM' failed to start. Failed to
start the virtual machine 'MobyLinuxVM' because
one of the Hyper-V components is not running.
220
You should enable Hyper-V with Docker for
Fig.
Windows
You should enable Hyper-V.
Open a new command prompt as an administrator and type
the following:
PowerShell — □ 𝖷
221
PS> bcdedit /set hypervisorlaunchtype auto
You should reboot your machine and Docker should finally
start.
But how do you know if Docker is working?
Open a new command prompt and type:
PowerShell — □ 𝖷
PS> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
You can verify that Docker is installed correctly with the
following command:
PowerShell — □ 𝖷
PS> docker run hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.
If your Docker daemon isn't running, you're probably
banging your head against this error:
PowerShell — □ 𝖷
PS> docker ps
error during connect: Get http://%2F%2F.%2Fpipe%2Fdocker_engine/v1.37/containers/json:
open //./pipe/docker_engine: The system cannot find the file specified. In the default
daemon configuration on Windows, the docker client must be run elevated to connect. This
error may also indicate that the docker daemon is not running.
222
The error above is suggesting that your Docker installation
is not behaving normally and wasn't able to start.
You should start your Docker daemon before you connect
to it.
Installing minikube on Windows 10
You can download minikube with:
PowerShell — □ 𝖷
PS> choco install minikube -y
You can test your minikube installation with:
PowerShell — □ 𝖷
PS> minikube start
👍 Starting control plane node minikube in cluster minikube
🐳 Preparing Kubernetes v1.23.0 ...
🔎 Verifying Kubernetes components...
🌟 Enabled addons: default-storageclass, storage-provisioner
🏄 Done! kubectl is now configured to use "minikube" by default
PowerShell — □ 𝖷
PS> minikube delete
223
🔥 Deleting "minikube" ...
💀 Removed all traces of the "minikube" cluster.
And create a new one with:
PowerShell — □ 𝖷
PS> minikube start --v=7 --alsologtostderr
The --v=7 flag increases the logging level, and you should
be able to spot the error in the terminal output.
The extra verbose logging should help you get to the issue.
In a particular case, minikube used to fail with:
"E0427 09:19:10.114873 10012 start.go:159] Error
starting host: Error starting stopped host: exit status
1."
Not very helpful. After enabling verbose logging, the issue
was more obvious:
PowerShell — □ 𝖷
Hyper-V\Start-VM minikube
~~~~~~~~~~~~~~~~~~~~~~~~~
CategoryInfo : FromStdErr: (:) [Start-VM], VirtualizationException
FullyQualifiedErrorId : OutOfMemory,Microsoft.HyperV.PowerShell.Commands.StartVM
I was running out of memory!
224
It's time to test if your installation has been successful.
Before you do so, in the command prompt type:
PowerShell — □ 𝖷
PS> kubectl get nodes
NAME STATUS ROLES AGE
minikube Ready master 88s
You should be able to see a single node in Kubernetes.
Ubuntu
Installing kubectl on Ubuntu
Add the Kubernetes repository to apt with:
bash
$ curl -s \
https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
OK
$ sudo tee /etc/apt/sources.list.d/kubernetes.list > /dev/null <<EOF
deb http://apt.kubernetes.io/ kubernetes-xenial main
EOF
You can install kubectl with:
bash
225
$ sudo apt-get -qq update && sudo apt-get -qqy install kubectl
$ Unpacking kubectl ...
Setting up kubectl ...
Installing minikube on Ubuntu
You can download minikube with:
bash
$ curl -Lo minikube \
https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
% Total % Received % Xferd Average Speed Time Time Current
Dload Upload Total Spent Speed
100 55.2M 100 55.2M 0 0 11.4M 0 0:00:04 0:00:04 15.3M
You can install it with:
bash
$ chmod +x minikube && sudo mv minikube /usr/local/bin/
You can test your minikube installation with:
Please note that running minikube as root is
discouraged. Please run minikube as a standard user
and not sudo .
bash
$ minikube start --vm
226
👍 Starting control plane node minikube in cluster minikube
🐳 Preparing Kubernetes v1.23.0 ...
🔎 Verifying Kubernetes components...
🌟 Enabled addons: default-storageclass, storage-provisioner
🏄 Done! kubectl is now configured to use "minikube" by default
VirtualBox isn't the default driver for Ubuntu, but
it's the most stable.
If for any reason minikube fails to start up, you can debug it
with:
PowerShell — □ 𝖷
PS> minikube delete
🔥 Deleting "minikube" ...
💀 Removed all traces of the "minikube" cluster.
And create a new one with:
PowerShell — □ 𝖷
PS> minikube start --v=7 --alsologtostderr
The --v=7 flag increases the logging level, and you should
be able to spot the error in the terminal output.
It's time to test if your installation was successful. In the
command prompt type:
227
bash
$ kubectl get nodes
NAME STATUS ROLES AGE
minikube Ready master 88s
Great!
Chapter 12
Mastering
YAML
229
Though YAML syntax may seem daunting and abrupt at
first, there are only three rules to remember when writing
YAML:
1. Indentation.
2. Maps.
3. Lists.
Indentation
YAML uses a fixed indentation scheme to represent
relationships between data layers.
A YAML file uses spaces as indentation; you can use 2 or 4
spaces for indentation, but no tab. In other words, tab
indentation is forbidden.
Why does YAML forbid tabs?
Tabs have been outlawed since they are treated differently by
different editors and tools.
And since indentation is critical to the proper interpretation
of YAML, this issue is too tricky even to attempt.
You can find the same question on the official YAML
website.
230
YAML maps
Maps let you associate key-value pairs.
Keys are represented in YAML as strings terminated by a
trailing colon.
Values are represented by either a string following the colon,
separated by a space.
For example, you might have a config file that starts like this:
pod.yaml
apiVersion: v1
kind: Pod
You can think of it in terms of its JSON equivalent:
pod.json
{
"apiVersion": "v1",
"kind": "Pod"
}
You can also specify more complicated structures by creating
a key that maps to another map, as in:
pod.yaml
231
apiVersion: v1
kind: Pod
metadata:
name: example-pod
labels:
app: app
In this case, you have a key, metadata , that has as its value a
map with two more keys, name and labels .
The labels key itself has a map as its value.
You can nest these as far as you want to.
The YAML processor knows how all of these pieces relate to
each other because you indented the lines.
It's common practice to use two spaces for readability, but
the number of spaces doesn't matter — as long as it's at least
1, and as long as you're consistent.
The following YAML is equivalent to the previous but uses
four spaces for indentation:
pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: example-pod
labels:
app: app
If you decide to translate the YAML into JSON, both
definitions are equivalent to:
232
pod.json
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "example-pod",
"labels": {
"app": "app"
}
}
}
Let's have a look at lists next.
YAML lists
YAML lists are a sequence of objects.
For example:
args.yaml
args:
- sleep
- "1000"
You can have virtually any number of items in a list, defined
as items that start with a dash - indented from the parent.
The YAML file translates to JSON as:
args.json
{
"args": ["sleep", "1000"]
233
Please note that members of the list can also be maps:
pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: example-pod
labels:
app: web
spec:
containers:
- name: app
image: nginx
ports:
- containerPort: 88
So as you can see here, we have a list of containers —
"objects" — each of which consists of:
a name
an image , and
a list of ports
Each list item under ports is a map that lists the
containerPort and its value.
For completeness, let's quickly look at the JSON equivalent.
Copy the content from above and try to convert it using this
online editor.
As you can see, the definition is more complicated, but
234
YAML is making it more readable.
What can you have as list items or values in a map?
YAML data types
Maps and lists are the basic building blocks of any YAML
file.
Any value part of a list or a map's value can be a string, a
number, a boolean, null , or another dictionary.
Strings
In most cases, strings are automatically recognised as such.
There's no need to wrap them in quotes.
normal: this is a normal string
new_line: "this is a string with a new line\n"
String values can span more than one line.
With the greater than ( > ) character, you can specify a string
in a block.
235
long_string: >
this is not a normal string it
spans more than
one line
There's another way to have a string with multiple lines
using the pipe ( | ) operator.
long_string: |
this is not a normal string it
spans more than
one line
What's the difference?
The > operator ignores new lines and concatenates the
string into a single line.
The | operator keeps the new line as is.
Please note that the > and | will trim whitespaces and
new lines from the beginning or the end of the string.
long_string: |
this is not a normal string it
spans more than
one line
# ^ this line is omitted because it's empty.
236
You can preserve any whitespace or new line by adding a +
sign.
long_string: |+
this is not a normal string it
spans more than
one line
# ^ this line is preserved.
Numeric types
YAML recognises numeric types.
Here's an example:
integer:
decimal: 10
hexadecimal: 0x12d4
octal: 023332
floating:
fixed: 10.4
exponential: 12.3015e+05
not_a_number: .NAN
infinity: .inf
negative_infinity: -.Inf
When using Kubernetes, you might find that you want to
use decimal numbers in most cases.
Ironically, that could lead to some issues such as this one:
237
replicas: 10
mysql_version: 5.5
The field mysql_version is converted into a float rather
than a string.
The correct representation is:
replicas: 10
mysql_version: "5.5"
Boolean types
YAML indicates boolean values with the keywords True ,
On and Yes for true.
False is indicated with False , Off , or No .
foo: True
bar: False
light: On
TV: Off
Having a lot of options for booleans is a con rather than a
pro.
Consider the following example:
238
countries:
united_kingdom: uk
italy: it
norway: no
The last no is evaluated as a false .
The surprising behaviour above is sometimes
described as the Norway problem.
The correct representation is:
countries:
united_kingdom: uk
italy: it
norway: "no"
Null
A null value indicates the lack of a value.
You can define null s with a tilde ( ~ ) or the unquoted
null string literal:
239
foo: ~
bar: null
You will probably never have to write a YAML definition
that includes YAML.
However, you could see null values when you retrieve
YAML definitions from the cluster.
bash
$ kubectl get clusterversions v1 -o yaml
apiVersion: config.openshift.io/v1
kind: ClusterVersion
# truncated
status:
availableUpdates: null
# truncated
Multiple documents
YAML definitions in the same document can be separated
using the --- operator.
first: document
---
240
second: document
It's considered best practice in Kubernetes to store your
Kubernetes resources into a single YAML file separated by -
-- .
# deployment
---
# service
---
# ingress
Comments
Comments begin with a hash # sign.
They can appear after a document value or take up an entire
line.
# This is a full-line comment
foo: bar # this is a comment too
Comments are for humans.
YAML processors will discard them.
241
Snippets
In YAML, you can define structures and label them using
the & operator.
You can recall the structure with the * operator and the
label name when you wish to reuse it.
Let's have a look at an example:
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-hello-world
spec:
selector:
matchLabels: &pod-label
run: pod-hello-world
template:
metadata:
labels: *pod-label
spec:
containers:
- name: cont1
image: learnk8s/app:1.0.0
ports:
- containerPort: 8080
The map run: pod-hello-world is assigned to the label
pod-label .
You reuse the same structure every time you use *pod-
label .
The above YAML expands to:
242
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-hello-world
spec:
selector:
matchLabels:
run: pod-hello-world
template:
metadata:
labels:
run: pod-hello-world
spec:
containers:
- name: cont1
image: learnk8s/app:1.0.0
ports:
- containerPort: 8080
Debugging tips
What if you make a mistake?
How do you know if the YAML file is valid?
You could use an online YAML linter or a command line
tool to verify your YAML files.
If you're using Visual Studio Code, you might want to
install the YAML extension from Red Hat, which
autocompletes the Kubernetes YAML as you type.
243
Navigating paths
YAML path specifies the element (or a set of elements) in a
YAML file.
Consider the following YAML definition:
apiVersion: v1
kind: Pod
metadata:
name: example-pod
labels:
app: app
You can refer to the Pod's label with the path
metadata.labels.app .
A YAML Path consists of segments.
Each segment indicates part of the navigation instructions
through the source data.
Some segment types select a single precise node, while others
are capable of selecting more than one.
As an example, the following Pod has two containers:
apiVersion: v1
kind: Pod
244
metadata:
labels:
run: pod-hello-world
spec:
containers:
- name: cont1
image: learnk8s/app:1.0.0
ports:
- containerPort: 8080
- name: cont2
image: nginx
ports:
- containerPort: 80
You could refer to both containers with
spec.containers[*] .
If you wish to refer to both images, you could use
spec.containers[*].image .
The YAML path follows a similar syntax to JSONPath.
Here's a summary of the fundamental operators you can
use:
Navigating maps
You can drill down into the YAML definition with dots . .
# YAML path: "a.b.c"
---
a:
b:
c: thing0 # MATCHES
d:
c: thing1
245
If you want to match more than one path, you can use the
star * operator.
# YAML path: "a.*.c"
---
a:
b:
c: thing0 # MATCHES
d:
c: thing1 # MATCHES
Navigating lists
You can select a particular element in a list using its index.
# YAML path: "a.b[1].c"
---
a:
b:
- c: thing0
- c: thing1 # MATCHES
- d: thing2
You can use the star * operator if you wish to match all
elements.
# YAML path: "a.b[*].c"
---
a:
246
b:
- c: thing0 # MATCHES
- c: thing1 # MATCHES
- d: thing2
A complete list of operators for the YAML path is available
here.
YAML path helps identify values in your YAML when
talking to a friend or colleague.
But it's also helpful if you wish to have tooling automatically
parsing, amending and deleting values.
Let's take yq as an example.
yq
yq is a command-line tool designed to transform YAML.
yq is similar to another more popular tool called
jq that focuses on JSON instead of YAML.
yq takes a YAML file as input and can:
1. read values from the file
2. add new values
247
3. update existing values
4. generate new YAML files
5. convert YAML into JSON
6. merge two or more YAML files
Let's have a look at the following Pod definition:
pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: test-pod
spec:
containers:
- name: test-container
image: k8s.gcr.io/busybox
env:
- name: DB_URL
value: postgres://db_url:5432
You can read the value for the environment variable ENV
with:
bash
$ yq ".spec.containers[0].env[0].value"
postgres://db_url:5432
The command works as follows:
pod.yaml is the file path of the YAML that you want to
read.
248
.spec.containers[0].env[0].value is the query path.
If you want to try out yq , you can install yq on macOS
with:
bash
$ brew install yq
On Linux with:
bash
$ sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CC86BB64
sudo add-apt-repository ppa:rmescandon/yq
sudo apt update
sudo apt install yq -y
If you don't have the add-apt-repository
command installed, you can install it with apt-get
install software-properties-common .
If you're on Windows, you can download the executable
from Github.
yq isn't just for reading values, though.
You can also update values.
Let's imagine that you want to update the image in the
following Pod:
249
pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: test-pod
spec:
containers:
- name: test-container
image: k8s.gcr.io/busybox
The path for the image name is
spec.containers[0].image .
You can use the following command to amend the YAML:
bash
$ yq '.spec.containers[0].image = "learnk8s/app:1.0.0"' pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: test-pod
spec:
containers:
- name: test-container
image: learnk8s/app:1.0.0
You should notice that yq printed the result on the
standard output.
If you prefer to edit the YAML in place, you should add the
-i flag.
Since yq understands YAML, there is also a mechanism to
create a more complex structure from smaller YAML
definitions.
250
Merging YAML files
With yq you can merge multiple YAML files into one.
Consider the following two YAML files:
one.yaml
a: simple
b: [1, 2]
and
two.yaml
a: other
c:
test: 1
You can merge the two files with:
bash
$ yq '. * load("one.yaml")' two.yaml
a: other
b: [1, 2]
c:
test: 1
The output of that command is the two YAML files
combined.
251
Notice how the last a field has overwritten the first.
In this case, the:
The . references the current file ( two.yaml ).
load operator is used to read another yaml file.
* is the merge operator.
Let's have a look at another example.
In Kubernetes, a Pod can contain multiple containers.
You might need to add the same container to all Pods in the
cluster.
Instead of repeating the definition for the container in every
Pod, you could define it and merge it with yq .
Consider the two Pod definitions:
log-container.yaml
apiVersion: v1
kind: Pod
metadata:
name: log
spec:
containers:
- name: fluentd
image: fluentd
ports:
- containerPort: 80
The Pod above defines the container for fluentd — a log
collector.
252
pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: test-pod
spec:
containers:
- name: test-container
image: k8s.gcr.io/busybox
If you want to merge the fluentd container with your test-
pod you can do so with:
bash
$ yq '. *+ load("log-container.yaml")' pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: log
spec:
containers:
- name: test-container
image: k8s.gcr.io/busybox
- name: fluentd
image: fluentd
ports:
- containerPort: 80
What's *= ?
yq doesn't know when to override or append values, so it
needs some hints.
The + flag after * instructs yq to append existing values
to the array.
What happens when you omit the + ?
253
bash
$ yq '. * load("log-container.yaml")' pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: log
spec:
containers:
- name: fluentd
image: fluentd
ports:
- containerPort: 80
The original container was overwritten.
There are more flags to control how values are merged in an
object.
yq is an excellent utility to make minor amendments to
your YAML and read fields programmatically.
If you need more complex templating, you might want to
have a look at Kustomize or Helm, which are explicitly
designed to work with Kubernetes.
Chapter 13
Kubectl tips
and tricks
255
If you work with Kubernetes, you can't escape using
kubectl .
Whenever you spend a lot of time working with a specific
tool, it is worth getting to know it very well and learning
how to use it efficiently.
This section contains a series of tips and tricks to make your
usage of kubectl more efficient and effective.
1. Save typing with command
completion
One of the most useful, but often overlooked, tricks to
boost your kubectl productivity is command completion.
Command completion allows you to auto-complete
individual parts of kubectl commands with the Tab key.
This works for sub-commands, options and arguments,
including hard-to-type things like resource names.
Command completion is available for the Bash and Zsh
shells.
The official documentation contains detailed instructions
for setting up command completion, but the following
sections provide a recap for you.
How command completion works
256
In general, command completion is a shell feature that
works by the means of a completion script.
A completion script is a shell script that defines the
completion behaviour for a specific command.
Sourcing a completion script enables completion for the
corresponding command.
Kubectl can automatically generate and print out the
completion scripts for Bash and Zsh with the following
commands:
bash
$ kubectl completion bash
# or
$ kubectl completion zsh
In theory, sourcing the output of this command in the
appropriate shell enables kubectl command completion.
However, in practice, the details differ for Bash (including a
difference between Linux and macOS) and Zsh. The
following sections explain all these cases:
Set up command completion for Bash on Linux
Set up command completion for Bash on macOS
Set up command completion for Zsh
Bash on Linux
The completion script for Bash depends on the
257
bash-completion project, so you have to install that first.
You can install bash-completion with various package
managers.
For example:
bash
$ sudo apt-get install bash-completion
# or
$ yum install bash-completion
You can test if bash-completion is correctly installed with
the following command:
bash
$ type _init_completion
If this outputs the code of shell function, then bash-
completion has been correctly installed. If the command
outputs a not found error, you have to add the following
line to your ~/.bashrc file:
bash
$ source /usr/share/bash-completion/bash_completion
258
Whether you have to add this line to your
~/.bashrc file or not, depends on the package
manager you used to install bash-completion. For
APT it's necessary, for yum not.
Once bash-completion is installed, you have to set things up
so that the kubectl completion script gets sourced in all your
shell sessions.
One way to do this is to add the following line to your
~/.bashrc file:
.bashrc
source <(kubectl completion bash)
Another possibility is to add the kubectl completion script
to the /etc/bash_completion.d directory (create it, if it
doesn't exist):
bash
$ kubectl completion bash >/etc/bash_completion.d/kubectl
All completion scripts in the
/etc/bash_completion.d directory are
automatically sourced by bash-completion.
259
Both approaches are equivalent.
After reloading your shell, kubectl command completion
should be working!
Bash on macOS
With macOS, there is a slight complication.
The reason is that the default version of Bash on macOS is
3.2, which is quite outdated.
The kubectl completion script unfortunately requires at
least Bash 4.1 and therefore it doesn't work with Bash 3.2.
The reason that Apple includes an outdated version
of Bash in macOS is that newer versions use the
GPLv3 license, which Apple doesn't support.
That means that, to use kubectl command completion on
macOS, you have to install a newer version of Bash.
You can even make it your new default shell, which will save
you a lot of this kind of trouble in the future.
It's actually not difficult, and you can find instructions in
this article.
Before continuing, make sure that you are now using Bash
4.1 or newer (find out with bash --version ).
The completion script for Bash depends on the bash-
completion project, so you have to install that first.
260
You can install bash-completion with Homebrew:
bash
$ brew install bash-completion@2
The @2 stands for bash-completion v2. The kubectl
completion script requires bash-completion v2, and
bash-completion v2 requires at least Bash 4.1. This is
the reason that you can't use the kubectl completion
script on Bash versions lower than 4.1.
The output of the brew install command includes a
"Caveats" section with instructions to add the following
lines to your ~/.bash_profile file:
.bash_profile
export BASH_COMPLETION_COMPAT_DIR=/usr/local/etc/bash_completion.d
[[ -r "/usr/local/etc/profile.d/bash_completion.sh" ]] && . "/usr/local/etc/profile.d/bash_completion.sh"
You have to do this in order to complete the installation of
bash-completion.
However, you should add those lines to your ~/.bashrc
instead of the ~/.bash_profile file.
This ensures that bash-completion is also available in sub-
shells.
After reloading your shell, you can test if bash-completion is
261
correctly installed with the following command:
bash
$ type _init_completion
If this outputs the code of a shell function, then you're all
set.
Now you have to set things up so that the kubectl
completion script gets sourced in all your shell sessions.
One way to do this is to add the following line to your
~/.bashrc file:
.bashrc
source <(kubectl completion bash)
Another possibility is to add the kubectl completion script
to the /usr/local/etc/bash_completion.d directory:
bash
$ kubectl completion bash >/usr/local/etc/bash_completion.d/kubectl
This only works if you installed bash-completion
with Homebrew. In that case, bash-completion
sources all completion scripts in this directory.
262
In case you also installed kubectl with Homebrew, you
don't even have to do the above step, because the
completion script should already have been put in the
/usr/local/etc/bash_completion.d directory by the
kubectl Homebrew formula.
In that case, kubectl completion should just start working
automatically after installing bash-completion.
In the end, all of these approaches are equivalent.
After reloading your shell, kubectl completion should be
working!
Zsh
The completion script for Zsh doesn't have any
dependencies.
All you have to do is set things up so it gets sourced in all
your shell sessions.
You can do this by adding the following line to your
~/.zshrc file:
.zshrc
source <(kubectl completion zsh)
In case you get a command not found: compdef error after
reloading your shell, you have to enable the compdef
builtin, which you can do by adding the following to the
263
beginning of your ~/.zshrc file:
.zshrc
autoload -Uz compinit
compinit
2. Quickly look up resource
specifications
When you create YAML resource definitions, you need to
know the fields and the meanings of these resources.
One location to look up this information is in the API
reference, which contains the full specifications of all
resources.
However, switching to a web browser each time you need to
look something up is tedious.
Therefore, kubectl provides the kubectl explain
command, which can print out the resource specifications
of all resources right in your terminal.
The usage of kubectl explain is as follows:
bash
$ kubectl explain resource[.field]...
264
The command outputs the specification of the requested
resource or field.
The information shown by kubectl explain is identical to
the information in the API reference.
bash
$ kubectl explain deployment.spec.replicas
KIND: Deployment
VERSION: apps/v1
FIELD: replicas <integer>
DESCRIPTION:
Number of desired pods. This is a pointer to distinguish between explicit
zero and not specified. Defaults to 1.
By default, kubectl explain displays only a single level of
fields. You can display the entire tree of fields with the --
recursive flag:
bash
$ kubectl explain deployment.spec --recursive
In case you're not sure about which resource names you can
use with kubectl explain , you can display all of them with
the following command:
bash
$ kubectl api-resources
NAME SHORTNAMES KIND
configmaps cm
265
ConfigMap
endpoints ep Endpoints
events ev Event
namespaces ns Namespace
nodes no Node
pods po Pod
# truncated output
This command displays the resource names in their plural
form (e.g. deployments instead of deployment ).
It also displays the shortname (e.g. deploy ) for those
resources that have one.
Don't worry about these differences.
All of these name variants are equivalent for kubectl.
That is, you can use any of them for kubectl explain .
For example, all of the following commands are equivalent:
bash
$ kubectl explain deployments.spec
# or
$ kubectl explain deployment.spec
# or
$ kubectl explain deploy.spec
3. Use the custom columns output
format
266
The default output format of the kubectl get command
(for reading resources) is as follows:
bash
$ kubectl get pods
NAME READY STATUS AGE
engine-544b6b6467-22qr6 1/1 Running 78d
engine-544b6b6467-lw5t8 1/1 Running 78d
engine-544b6b6467-tvgmg 1/1 Running 78d
web-ui-6db964458-8pdw4 1/1 Running 78d
That's a nice human-readable format, but it contains only a
limited amount of information.
As you can see, just a few fields (compared to the full
resource definitions) are shown for each resource.
That's where the custom columns output format comes in.
It lets you freely define the columns and the data to display
in them.
You can choose any field of a resource to be displayed as a
separate column in the output.
The usage of the custom columns output option is as
follows:
-o custom-columns=<header>:<jsonpath>[,<header>:<jsonpath>]...
You have to define each output column as a <header>:
<jsonpath> pair:
267
<header> is the name of the column, you can choose
anything you want.
<jsonpath> is an expression that selects a resource field
(explained in more detail below).
Let's look at a simple example:
bash
$ kubectl get pods -o custom-columns='NAME:metadata.name'
NAME
engine-544b6b6467-22qr6
engine-544b6b6467-lw5t8
engine-544b6b6467-tvgmg
web-ui-6db964458-8pdw4
Here, the output consists of a single column displaying the
names of all Pods.
The expression selecting the Pod names is metadata.name .
The reason for this is that the name of a Pod is defined in the
name field of the metadata field of a Pod resource (you can
look this up in the API reference or with kubectl explain
pod.metadata.name ).
Now, imagine you want to add an additional column to the
output, for example, showing the node that each Pod is
running on.
To do so, you can just add an appropriate column
specification to the custom columns option:
bash
268
$ kubectl get pods \
-o custom-columns='NAME:metadata.name,NODE:spec.nodeName'
NAME NODE
engine-544b6b6467-22qr6 ip-10-0-80-67.ec2.internal
engine-544b6b6467-lw5t8 ip-10-0-36-80.ec2.internal
engine-544b6b6467-tvgmg ip-10-0-118-34.ec2.internal
web-ui-6db964458-8pdw4 ip-10-0-118-34.ec2.internal
The expression selecting the node name is spec.nodeName .
This is because the node a Pod has been scheduled to is saved
in the spec.nodeName field of a Pod (see kubectl explain
pod.spec.nodeName ).
Note that Kubernetes resource fields are case-
sensitive.
You can set any field of a resource as an output column in
that way.
Just browse the resource specifications and try it out with
any fields you like!
But first, let's have a closer look at these field selection
expressions.
JSONPath expressions
The expressions for selecting resource fields are based on
JSONPath.
JSONPath is a language to extract data from JSON
269
documents (it is similar to XPath for XML).
Selecting a single field is the most basic usage of JSONPath.
It has a lot of features, like list selectors, filters, and more.
However, with kubectl explain , only a subset of the
JSONPath capabilities is supported.
The following summarises these supported features with
example usages:
bash
# Select all elements of a list
$ kubectl get pods -o custom-columns='DATA:spec.containers[*].image'
# Select a specific element of a list
$ kubectl get pods -o custom-columns='DATA:spec.containers[0].image'
# Select those elements of a list that match a filter expression
$ kubectl get pods -o custom-columns='DATA:spec.containers[?(@.image!="nginx")].image'
# Select all fields under a specific location, regardless of their name
$ kubectl get pods -o custom-columns='DATA:metadata.*'
# Select all fields with a specific name, regardless of their location
$ kubectl get pods -o custom-columns='DATA:..image'
Of particular importance is the [] operator.
Many fields of Kubernetes resources are lists, and this
operator allows you to select items from these lists.
It is often used with a wildcard as [*] to select all items of
the list.
Below you will find some examples that use this notation.
Example applications
The possibilities for using the custom columns output
270
format are endless, as you can display any field, or
combination of fields, of a resource in the output.
Here are some example applications, but feel free to explore
on your own and find applications that are useful to you!
Tip: if you end up using one of these commands
frequently, you can create a shell alias for it.
Display container images of Pods
bash
$ kubectl get pods \
-o custom-columns='NAME:metadata.name,IMAGES:spec.containers[*].image'
NAME IMAGES
engine-544b6b6467-22qr6 rabbitmq:3.7.8-management,nginx
engine-544b6b6467-lw5t8 rabbitmq:3.7.8-management,nginx
engine-544b6b6467-tvgmg rabbitmq:3.7.8-management,nginx
web-ui-6db964458-8pdw4 wordpress
This command displays the names of all the container
images of each Pod.
Remember that a Pod may contain more than one
container. In that case, the container images of a
single Pod are displayed as a comma-separated list in
the same column.
Display availability zones of nodes
271
bash
$ kubectl get nodes \
-o custom-columns='NAME:metadata.name,ZONE:metadata.labels.failure-domain\.beta\.kubernetes\.io/zone'
NAME ZONE
ip-10-0-118-34.ec2.internal us-east-1b
ip-10-0-36-80.ec2.internal us-east-1a
ip-10-0-80-67.ec2.internal us-east-1b
This command can be useful if your Kubernetes cluster is
deployed on a public cloud infrastructure (such as AWS,
Azure, or GCP).
It displays the availability zone that each node is in.
The availability zone is a cloud concept that denotes
a point of replication within a geographical region.
The availability zones for each node are obtained through
the special failure-domain.beta.kubernetes.io/zone
label.
If the cluster runs on a public cloud infrastructure, then this
label is automatically created and its value is set to the name
of the availability zone of the node.
Labels are not part of the Kubernetes resource
specifications, so you can't find the above label in the API
reference.
However, you can see it (as well as all other labels), if you
output the nodes as YAML or JSON:
bash
272
$ kubectl get nodes -o yaml
# or
$ kubectl get nodes -o json
This is generally a good way to discover even more
information about your resources, in addition to exploring
the resource specifications.
4. Switch between clusters and
namespaces with ease
When kubectl has to make a request to the Kubernetes API,
it reads the so-called kubeconfig file on your system to get all
the connection parameters it needs to access and make a
request to the API server.
The default kubeconfig file is ~/.kube/config .
This file is usually automatically created or updated
by some command (for example, aws eks update-
kubeconfig or gcloud container clusters get-
credentials , if you use managed Kubernetes
services).
When you work with multiple clusters, then you have
273
connection parameters for multiple clusters configured in
your kubeconfig file.
This means that you need a way to tell kubectl which of
these clusters you want it to connect to.
Within a cluster, you can set up multiple namespaces (a
namespace is a kind of "virtual" cluster within a physical
cluster).
Kubectl determines which namespace to use for a request
from the kubeconfig file as well.
So, you need a way to tell kubectl which of these namespaces
you want it to use.
This section explains how this works and how you can do it
effortlessly.
Note that you can also have multiple kubeconfig
files by listing them in the KUBECONFIG environment
variable. In that case, all these files will be merged
into a single effective configuration at execution
time. You can also overwrite the default kubeconfig
file with the --kubeconfig option for every kubectl
command. See the official documentation.
Kubeconfig files
Let's see what a kubeconfig file actually contains:
274
Fig. A kubeconfig file contains a set of contexts
As you can see, a kubeconfig file consists of a set of contexts.
A context contains the following three elements:
Cluster: URL of the API server of a cluster
User: authentication credentials for a specific user of the
cluster
Namespace: the namespace to use when connecting to
the cluster
275
In practice, people often use a single context per
cluster in their kubeconfig file. However, you could
also have multiple contexts per cluster, differing in
their user or namespace. But this seems to be less
common so that often there is one-to-one mapping
between clusters and contexts.
At any given time, one of these contexts is set as the current
context (through a dedicated field in the kubeconfig file):
276
One of the contexts in a kubeconfig file is set as the
Fig.
current context
When kubectl reads a kubeconfig file, it always uses the
information from the current context.
Thus, in the above example, kubectl would connect to the
dev cluster.
Consequently, to switch to another cluster, you can just
change the current context in the kubeconfig file.
277
Fig. You can switch contexts with kubectl
In the example above, the current context was changed to
the test cluster and API Namespace.
278
Note that kubectl also provides the --cluster , --
user , --namespace , and --context options which
allow you to overwrite individual elements and the
current context itself, regardless of what is set in the
kubeconfig file. See kubectl options .
In theory, you could do these changes by manually editing
the kubeconfig file.
But of course this is tedious.
Kubectl provides a few commands for doing this too.
In particular, the kubectl config command provides sub-
commands for editing kubeconfig files.
Here are some of them:
kubectl config get-contexts : list all contexts
kubectl config current-context : get the current
context
kubectl config use-context : change the current
context
kubectl config set-context : change an element of a
context
However, using these commands directly is not very
convenient, because they are long to type.
Use kubectx
279
A very popular tool for switching between clusters and
namespaces is kubectx.
This tool provides the kubectx and kubens commands
that allow you to change the current context and
namespace, respectively.
As mentioned, changing the current context means
changing the cluster, if you have only a single
context per cluster.
To install kubectx, just follow the instructions on the
GitHub page.
Both kubectx and kubens provide command completion
through a completion script.
This allows you to auto-complete the context names and
namespaces so that you don't have to fully type them.
You can find the instructions to set up completion on the
GitHub page as well.
Another useful feature of kubectx is the interactive mode.
This works in conjunction with the fzf tool, which you have
to install separately (in fact, installing fzf automatically
enables kubectx interactive mode).
The interactive mode allows you to select the target context
or namespace through an interactive fuzzy search interface
(provided by fzf).
280
5. Save typing with auto-generated
aliases
Shell aliases are generally a good way to save typing.
The kubectl-aliases project takes this idea to heart and
provides about 800 aliases for common kubectl commands.
You might wonder how you could possibly remember 800
aliases!
Actually, you don't need to remember them, because they
are all generated according to a simple scheme which is
shown below, together with some example aliases.
281
Fig. Programmatically generated handy kubectl aliases
As you can see, the aliases consist of components, each
standing for a specific element of a kubectl command.
Each alias can have one component for the base command,
operation, and resource, and multiple components for the
options, and you just "fill in" these components from left to
right according to the above scheme.
282
Please note that the current and fully detailed
scheme is on the GitHub page. There you can also
find the full list of aliases.
For example, the alias kgpooyamlall stands for the
command kubectl get pods -o yaml --all-namespaces .
Fig. kubectl get pods -o yaml --all-namespaces
283
Note that the relative order of most option components
doesn't matter. So, kgpooyamlall is equivalent to
kgpoalloyaml .
You don't need to use all the components for an alias.
For example, k , kg , klo , ksys , or kgpo are valid aliases
too.
Furthermore, you can combine aliases with other words on
the command-line.
For example, you could use k proxy for running kubectl
proxy .
Or you could use kg roles for running kubectl get
roles (an alias component for the Roles resource doesn't
currently exist).
Installation
To install kubectl-aliases, you just have to download the
.kubectl-aliases file from GitHub and source it in your
~/.bashrc or ~/.zshrc file:
.bashrc
source ~/.kubectl_aliases
That's it! After reloading your shell, you should be able to
use all the 800 kubectl aliases!
Completion
284
As you have seen, you often append further words to an alias
on the command-line. For example:
bash
$ kgpooyaml test-pod-d4b77b989
If you use kubectl command completion, then you're
probably used to auto-completing things like resource
names. But can you still do that when you use the aliases?
That's an important question because if it didn’t work, that
would undo some of the benefits of these aliases!
The answer depends on which shell you use.
For Zsh, completion works out-of-the-box for aliases.
For Bash, unfortunately, completion doesn't work by default
for aliases. The good news is that it can be made to work
with some extra steps. The next section explains how to do
this.
Enable completion for aliases in Bash
The problem with Bash is that it attempts completion
(whenever you press Tab) on the alias name, instead of on
the aliased command (like Zsh).
Since you don't have a completion script for all the 800
aliases, this doesn't work.
The complete-alias project provides a general solution to this
285
problem.
It taps into the completion mechanism for an alias,
internally expands the alias to the aliased command, and
returns the completion suggestion for the expanded
command.
This means it makes completion for an alias behave exactly
the same as for the aliased command.
In the following, you will install complete-alias, and then
configure it to enable completion for all of the kubectl
aliases.
Install complete-alias
First of all, complete-alias depends on bash-completion.
So you need to ensure that bash-completion is installed
before installing complete-alias.
Instructions for this have previously been given for Linux
and MacOS.
286
Important note for macOS users: like the kubectl
completion script, complete-alias does not work
with Bash 3.2, which is the default version of Bash
on macOS. In particular, complete-alias depends on
bash-completion v2 ( brew install bash-
completion@2 ), which requires at least Bash 4.1.
That means, to use complete-alias on macOS, you
need to install a newer version of Bash.
To install complete-alias, you just have to download the
bash_completion.sh script from the GitHub repository,
and source it in your ~/.bashrc file:
.bashrc
source ~/bash_completion.sh
After reloading your shell, complete-alias should be correctly
installed.
Enable completion for kubectl aliases
Technically, complete-alias provides the _complete_alias
shell function.
This function inspects an alias and returns the completion
suggestions for the aliased command.
To hook this up with a specific alias, you have to use the
287
complete Bash builtin to set _complete_alias as the
completion function of the alias.
As an example, let's take the k alias that stands for the
kubectl command.
To set _complete_alias as the completion function for this
alias, you have to execute the following command:
bash
$ complete -F _complete_alias k
The effect of this is that whenever you auto-complete on the
k alias, the _complete_alias function is invoked, which
inspects the alias and returns the completion suggestions for
the kubectl command.
As another example, let's take the kg alias that stands for
kubectl get :
bash
$ complete -F _complete_alias kg
Similarly, the effect of this is that when you auto-complete
on kg , you get the same completion suggestions that you
would get for kubectl get .
288
Note that you can use complete-alias in this way for
any alias on your system.
Consequently, to enable completion for all the kubectl
aliases, you just have to run the above command for each of
them.
The following snippet does exactly that (assuming you
installed kubectl-aliases to ~/.kubectl-aliases ):
.bashrc
for _a in $(sed '/^alias /!d;s/^alias //;s/=.*$//' ~/.kubectl_aliases); do
complete -F _complete_alias "$_a"
done
Just add this snippet to your ~/.bashrc file, reload your
shell, and now you should be able to use completion for all
the 800 kubectl aliases!
6. Extend kubectl with plugins
Since version 1.12, kubectl includes a plugin mechanism
that allows you to extend kubectl with custom commands.
289
In case you're familiar with it, the kubectl plugin
mechanism closely follows the design of the Git
plugin mechanism.
This section will show you how to install plugins, where you
can find existing plugins, and how to create your own
plugins.
Installing plugins
Kubectl plugins are distributed as simple executable files
with a name of the form kubectl-x . The prefix kubectl-
is mandatory, and what follows is the new kubectl sub-
command that allows invoking the plugin.
For example, an hello plugin should be distributed as a file
named kubectl-hello .
To install a plugin, you just have to copy the kubectl-x file
to any directory in your PATH and make it executable (for
example, with chmod +x ).
Immediately after that, you can invoke the plugin with
kubectl x .
You can use the following command to list all the plugins
that are currently installed on your system:
bash
$ kubectl plugin list
290
This command also displays warnings if you have multiple
plugins with the same name, or if there is a plugin file that is
not executable.
Finding and installing plugins with krew
Kubectl plugins lend themselves to being shared and reused
like software packages.
But where can you find plugins that have been shared by
others?
The krew project aims to provide a unified solution for
sharing, finding, installing and managing kubectl plugins.
The project refers to itself as a "package manager for kubectl
plugins" (the name krew is a hint at brew).
Krew is centred around an index of kubectl plugins from
which you can choose and install.
Krew itself is a kubectl plugin.
That means, installing krew works in essence like installing
any other kubectl plugin.
You can find the detailed installation instructions for krew
on the official page.
The most important krew commands are as follows:
bash
# Search the krew index (with an optional search query)
$ kubectl krew search [<query>]
# Display information about a plugin
291
$ kubectl krew info <plugin>
# Install a plugin
$ kubectl krew install <plugin>
# Upgrade all plugins to the newest versions
$ kubectl krew upgrade
# List all plugins that have been installed with krew
$ kubectl krew list
# Uninstall a plugin
$ kubectl krew remove <plugin>
Note that installing plugins with krew does not prevent
installing plugins the traditional way.
Even if you use krew, you can still install plugins that you
find elsewhere (or create yourself) by other means.
Note that the kubectl krew list command only
lists the plugins that have been installed with krew,
whereas the kubectl plugin list command lists
all plugins, that is, those installed with krew and
those installed in other ways.