0% found this document useful (0 votes)
47 views291 pages

01 First Steps.v2

Uploaded by

milukyone
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
47 views291 pages

01 First Steps.v2

Uploaded by

milukyone
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 291

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.

You might also like