Kubernetes For Fullstack Developers
Kubernetes For Fullstack Developers
Kubernetes For Fullstack Developers
International License.
ISBN 978-0-9997730-3-1
Kubernetes for Full-Stack Developers
2020-01
Kubernetes for Full-Stack Developers
1. About DigitalOcean
2. Preface - Getting Started with this Book
3. Introduction
4. An Introduction to Kubernetes
5. How To Create a Kubernetes Cluster Using Kubeadm on
Ubuntu 18.04
6. Webinar Series: A Closer Look at Kubernetes
7. An Introduction to Helm, the Package Manager for Kubernetes
8. How To Install Software on Kubernetes Clusters with the Helm
Package Manager
9. Architecting Applications for Kubernetes
10. Modernizing Applications for Kubernetes
11. How To Build a Node.js Application with Docker
12. Containerizing a Node.js Application for Development With
Docker Compose
13. How to Set Up DigitalOcean Kubernetes Cluster Monitoring with
Helm and Prometheus Operator
14. How To Set Up Laravel, Nginx, and MySQL with Docker
Compose
15. How To Migrate a Docker Compose Workflow to Kubernetes
16. Building Optimized Containers for Kubernetes
17. How To Scale a Node.js Application with MongoDB on
Kubernetes Using Helm
18. How To Set Up a Private Docker Registry on Top of
DigitalOcean Spaces and Use It with DigitalOcean Kubernetes
19. How To Deploy a PHP Application with Kubernetes on Ubuntu
18.04
20. How To Automate Deployments to DigitalOcean Kubernetes with
CircleCI
21. How To Set Up a CD Pipeline with Spinnaker on DigitalOcean
Kubernetes
22. Kubernetes Networking Under the Hood
23. How To Inspect Kubernetes Networking
24. An Introduction to Service Meshes
25. How To Back Up and Restore a Kubernetes Cluster on
DigitalOcean Using Velero
26. How To Set Up an Elasticsearch, Fluentd and Kibana (EFK)
Logging Stack on Kubernetes
27. How to Set Up an Nginx Ingress with Cert-Manager on
DigitalOcean Kubernetes
28. How to Protect Private Kubernetes Services Behind a GitHub
Login with oauth2_proxy
About DigitalOcean
We recommend that you begin with a set of three or more clean, new
servers to start learning about Kubernetes. You can also run Kubernetes
locally in a development environment using a tool like Minikube. Finally,
you can choose to use a managed Kubernetes solution as long as you have
administrator access to the cluster. The examples in this book will work
with any system running Ubuntu 18.04 and should also work on Debian 9
and 10 systems, from a laptop to a remote server running in a cloud
provider’s environment.
Chapter 2 of this book goes into detail about how to create a Kubernetes
cluster from three new servers. It will be helpful to prepare in advance and
ensure that you can connect to the servers that you will use to create a
cluster. To connect to your servers with a terminal, use one of these guides
based on your computer’s operating system.
You should not feel obliged to follow the topics in any particular order.
If one section is more interesting or relevant to you, explore it and come
back to the others later if you prefer. Likewise, if you are already familiar
with the concepts and tools in a given section, feel free to skip that one
and focus on other topics.
What is Kubernetes?
Kubernetes, at its basic level, is a system for running and coordinating
containerized applications across a cluster of machines. It is a platform
designed to completely manage the life cycle of containerized applications
and services using methods that provide predictability, scalability, and
high availability.
As a Kubernetes user, you can define how your applications should run
and the ways they should be able to interact with other applications or the
outside world. You can scale your services up or down, perform graceful
rolling updates, and switch traffic between different versions of your
applications to test features or rollback problematic deployments.
Kubernetes provides interfaces and composable platform primitives that
allow you to define and manage your applications with high degrees of
flexibility, power, and reliability.
Kubernetes Architecture
To understand how Kubernetes is able to provide these capabilities, it is
helpful to get a sense of how it is designed and organized at a high level.
Kubernetes can be visualized as a system built in layers, with each higher
layer abstracting the complexity found in the lower levels.
At its base, Kubernetes brings together individual physical or virtual
machines into a cluster using a shared network to communicate between
each server. This cluster is the physical platform where all Kubernetes
components, capabilities, and workloads are configured.
The machines in the cluster are each given a role within the Kubernetes
ecosystem. One server (or a small group in highly available deployments)
functions as the master server. This server acts as a gateway and brain for
the cluster by exposing an API for users and clients, health checking other
servers, deciding how best to split up and assign work (known as
“scheduling”), and orchestrating communication between other
components. The master server acts as the primary point of contact with
the cluster and is responsible for most of the centralized logic Kubernetes
provides.
The other machines in the cluster are designated as nodes: servers
responsible for accepting and running workloads using local and external
resources. To help with isolation, management, and flexibility, Kubernetes
runs applications and services in containers, so each node needs to be
equipped with a container runtime (like Docker or rkt). The node receives
work instructions from the master server and creates or destroys
containers accordingly, adjusting networking rules to route and forward
traffic appropriately.
As mentioned above, the applications and services themselves are run
on the cluster within containers. The underlying components make sure
that the desired state of the applications matches the actual state of the
cluster. Users interact with the cluster by communicating with the main
API server either directly or with clients and libraries. To start up an
application or service, a declarative plan is submitted in JSON or YAML
defining what to create and how it should be managed. The master server
then takes the plan and figures out how to run it on the infrastructure by
examining the requirements and the current state of the system. This group
of user-defined applications running according to a specified plan
represents Kubernetes’ final layer.
etcd
kube-apiserver
One of the most important master services is an API server. This is the
main management point of the entire cluster as it allows a user to
configure Kubernetes’ workloads and organizational units. It is also
responsible for making sure that the etcd store and the service details of
deployed containers are in agreement. It acts as the bridge between various
components to maintain cluster health and disseminate information and
commands.
The API server implements a RESTful interface, which means that
many different tools and libraries can readily communicate with it. A
client called kubectl is available as a default method of interacting with
the Kubernetes cluster from a local computer.
kube-controller-manager
kube-scheduler
The process that actually assigns workloads to specific nodes in the cluster
is the scheduler. This service reads in a workload’s operating
requirements, analyzes the current infrastructure environment, and places
the work on an acceptable node or nodes.
The scheduler is responsible for tracking available capacity on each host
to make sure that workloads are not scheduled in excess of the available
resources. The scheduler must know the total capacity as well as the
resources already allocated to existing workloads on each server.
cloud-controller-manager
A Container Runtime
The first component that each node must have is a container runtime.
Typically, this requirement is satisfied by installing and running Docker,
but alternatives like rkt and runc are also available.
The container runtime is responsible for starting and managing
containers, applications encapsulated in a relatively isolated but
lightweight operating environment. Each unit of work on the cluster is, at
its basic level, implemented as one or more containers that must be
deployed. The container runtime on each node is the component that
finally runs the containers defined in the workloads submitted to the
cluster.
kubelet
The main contact point for each node with the cluster group is a small
service called kubelet. This service is responsible for relaying information
to and from the control plane services, as well as interacting with the
etcd store to read configuration details or write new values.
The kubelet service communicates with the master components to
authenticate to the cluster and receive commands and work. Work is
received in the form of a manifest which defines the workload and the
operating parameters. The kubelet process then assumes responsibility
for maintaining the state of the work on the node server. It controls the
container runtime to launch or destroy containers as needed.
kube-proxy
Pods
A pod is the most basic unit that Kubernetes deals with. Containers
themselves are not assigned to hosts. Instead, one or more tightly coupled
containers are encapsulated in an object called a pod.
A pod generally represents one or more containers that should be
controlled as a single application. Pods consist of containers that operate
closely together, share a life cycle, and should always be scheduled on the
same node. They are managed entirely as a unit and share their
environment, volumes, and IP space. In spite of their containerized
implementation, you should generally think of pods as a single, monolithic
application to best conceptualize how the cluster will manage the pod’s
resources and scheduling.
Usually, pods consist of a main container that satisfies the general
purpose of the workload and optionally some helper containers that
facilitate closely related tasks. These are programs that benefit from being
run and managed in their own containers, but are tightly tied to the main
application. For example, a pod may have one container running the
primary application server and a helper container pulling down files to the
shared filesystem when changes are detected in an external repository.
Horizontal scaling is generally discouraged on the pod level because there
are other higher level objects more suited for the task.
Generally, users should not manage pods themselves, because they do
not provide some of the features typically needed in applications (like
sophisticated life cycle management and scaling). Instead, users are
encouraged to work with higher level objects that use pods or pod
templates as base components but implement additional functionality.
Often, when working with Kubernetes, rather than working with single
pods, you will instead be managing groups of identical, replicated pods.
These are created from pod templates and can be horizontally scaled by
controllers known as replication controllers and replication sets.
A replication controller is an object that defines a pod template and
control parameters to scale identical replicas of a pod horizontally by
increasing or decreasing the number of running copies. This is an easy way
to distribute load and increase availability natively within Kubernetes. The
replication controller knows how to create new pods as needed because a
template that closely resembles a pod definition is embedded within the
replication controller configuration.
The replication controller is responsible for ensuring that the number of
pods deployed in the cluster matches the number of pods in its
configuration. If a pod or underlying host fails, the controller will start
new pods to compensate. If the number of replicas in a controller’s
configuration changes, the controller either starts up or kills containers to
match the desired number. Replication controllers can also perform rolling
updates to roll over a set of pods to a new version one by one, minimizing
the impact on application availability.
Replication sets are an iteration on the replication controller design with
greater flexibility in how the controller identifies the pods it is meant to
manage. Replication sets are beginning to replace replication controllers
because of their greater replica selection capabilities, but they are not able
to do rolling updates to cycle backends to a new version like replication
controllers can. Instead, replication sets are meant to be used inside of
additional, higher level units that provide that functionality.
Like pods, both replication controllers and replication sets are rarely the
units you will work with directly. While they build on the pod design to
add horizontal scaling and reliability guarantees, they lack some of the
fine grained life cycle management capabilities found in more complex
objects.
Deployments
Stateful Sets
Stateful sets are specialized pod controllers that offer ordering and
uniqueness guarantees. Primarily, these are used to have more fine-grained
control when you have special requirements related to deployment
ordering, persistent data, or stable networking. For instance, stateful sets
are often associated with data-oriented applications, like databases, which
need access to the same volumes even if rescheduled to a new node.
Stateful sets provide a stable networking identifier by creating a unique,
number-based name for each pod that will persist even if the pod needs to
be moved to another node. Likewise, persistent storage volumes can be
transferred with a pod when rescheduling is necessary. The volumes
persist even after the pod has been deleted to prevent accidental data loss.
When deploying or adjusting scale, stateful sets perform operations
according to the numbered identifier in their name. This gives greater
predictability and control over the order of execution, which can be useful
in some cases.
Daemon Sets
Daemon sets are another specialized form of pod controller that run a copy
of a pod on each node in the cluster (or a subset, if specified). This is most
often useful when deploying pods that help perform maintenance and
provide services for the nodes themselves.
For instance, collecting and forwarding logs, aggregating metrics, and
running services that increase the capabilities of the node itself are
popular candidates for daemon sets. Because daemon sets often provide
fundamental services and are needed throughout the fleet, they can bypass
pod scheduling restrictions that prevent other controllers from assigning
pods to certain hosts. As an example, because of its unique
responsibilities, the master server is frequently configured to be
unavailable for normal pod scheduling, but daemon sets have the ability to
override the restriction on a pod-by-pod basis to make sure essential
services are running.
Jobs and Cron Jobs
Services
So far, we have been using the term “service” in the conventional, Unix-
like sense: to denote long-running processes, often network connected,
capable of responding to requests. However, in Kubernetes, a service is a
component that acts as a basic internal load balancer and ambassador for
pods. A service groups together logical collections of pods that perform
the same function to present them as a single entity.
This allows you to deploy a service that can keep track of and route to
all of the backend containers of a particular type. Internal consumers only
need to know about the stable endpoint provided by the service.
Meanwhile, the service abstraction allows you to scale out or replace the
backend work units as necessary. A service’s IP address remains stable
regardless of changes to the pods it routes to. By deploying a service, you
easily gain discoverability and can simplify your container designs.
Any time you need to provide access to one or more pods to another
application or to external consumers, you should configure a service. For
instance, if you have a set of pods running web servers that should be
accessible from the internet, a service will provide the necessary
abstraction. Likewise, if your web servers need to store and retrieve data,
you would want to configure an internal service to give them access to
your database pods.
Although services, by default, are only available using an internally
routable IP address, they can be made available outside of the cluster by
choosing one of several strategies. The NodePort configuration works by
opening a static port on each node’s external networking interface. Traffic
to the external port will be routed automatically to the appropriate pods
using an internal cluster IP service.
Alternatively, the LoadBalancer service type creates an external load
balancer to route to the service using a cloud provider’s Kubernetes load
balancer integration. The cloud controller manager will create the
appropriate resource and configure it using the internal service service
addresses.
Volumes and Persistent Volumes
Conclusion
Kubernetes is an exciting project that allows users to run scalable, highly
available containerized workloads on a highly abstracted platform. While
Kubernetes’ architecture and set of internal components can at first seem
daunting, their power, flexibility, and robust feature set are unparalleled in
the open-source world. By understanding how the basic building blocks fit
together, you can begin to design systems that fully leverage the
capabilities of the platform to run and manage your workloads at scale.
How To Create a Kubernetes Cluster
Using Kubeadm on Ubuntu 18.04
Written by bsder
In this guide, you will set up a Kubernetes cluster from scratch using
Ansible and Kubeadm, and then deploy a containerized Nginx application
to it. You will be able to use the cluster that you create in this tutorial in
subsequent tutorials.
While the first tutorial in this curriculum introduces some of the
concepts and terms that you will encounter when running an application in
Kubernetes, this tutorial focuses on the steps required to build a working
Kubernetes cluster.
This tutorial uses Ansible to automate some of the more repetitive tasks
like user creation, dependency installation, and network setup in the
cluster. If you would like to create a cluster manually, the tutorial provides
a list of resources that includes the official Kubernetes documentation,
which you can use instead of Ansible.
By the end of this tutorial you should have a functioning Kubernetes
cluster that consists of three Nodes (a master and two worker Nodes). You
will also deploy Nginx to the cluster to confirm that everything works as
intended.
The author selected the Free and Open Source Fund to receive a
donation as part of the Write for DOnations program.
Kubernetes is a container orchestration system that manages containers
at scale. Initially developed by Google based on its experience running
containers in production, Kubernetes is open source and actively
developed by a community around the world.
Note: This tutorial uses version 1.14 of Kubernetes, the official
supported version at the time of this article’s publication. For up-to-date
information on the latest version, please see the current release notes in
the official Kubernetes documentation.
Kubeadm automates the installation and configuration of Kubernetes
components such as the API server, Controller Manager, and Kube DNS. It
does not, however, create users or handle the installation of operating-
system-level dependencies and their configuration. For these preliminary
tasks, it is possible to use a configuration management tool like Ansible or
SaltStack. Using these tools makes creating additional clusters or
recreating existing clusters much simpler and less error prone.
In this guide, you will set up a Kubernetes cluster from scratch using
Ansible and Kubeadm, and then deploy a containerized Nginx application
to it.
Goals
Your cluster will include the following physical resources:
After completing this guide, you will have a cluster ready to run
containerized applications, provided that the servers in the cluster have
sufficient CPU and RAM resources for your applications to consume.
Almost any traditional Unix application including web applications,
databases, daemons, and command line tools can be containerized and
made to run on the cluster. The cluster itself will consume around 300-
500MB of memory and 10% of CPU on each node.
Once the cluster is set up, you will deploy the web server Nginx to it to
ensure that it is running workloads correctly.
Prerequisites
~/kube-cluster/hosts
[masters]
master ansible_host=master_ip ansible_user=root
[workers]
worker1 ansible_host=worker_1_ip ansible_user=root
worker2 ansible_host=worker_2_ip ansible_user=root
[all:vars]
ansible_python_interpreter=/usr/bin/python3
You may recall that inventory files in Ansible are used to specify server
information such as IP addresses, remote users, and groupings of servers
to target as a single unit for executing commands. ~/kube-
cluster/hosts will be your inventory file and you’ve added two
Ansible groups (masters and workers) to it specifying the logical structure
of your cluster.
In the masters group, there is a server entry named “master” that lists
the master node’s IP (master_ip) and specifies that Ansible should run
remote commands as the root user.
Similarly, in the workers group, there are two entries for the worker
servers (worker_1_ip and worker_2_ip) that also specify the
ansible_user as root.
The last line of the file tells Ansible to use the remote servers’ Python 3
interpreters for its management operations.
Save and close the file after you’ve added the text.
Having set up the server inventory with groups, let’s move on to
installing operating system level dependencies and creating configuration
settings.
~/kube-cluster/initial.yml
- hosts: all
become: yes
tasks:
- name: create the 'ubuntu' user
user: name=ubuntu append=yes state=present
createhome=yes shell=/bin/bash
Save and close the file after you’ve added the text.
Next, execute the playbook by locally running:
ansible-playbook -i hosts ~/kube-
cluster/initial.yml
The command will complete within two to five minutes. On completion,
you will see output similar to the following:
Output
PLAY [all] ****
~/kube-cluster/kube-dependencies.yml
- hosts: all
become: yes
tasks:
- name: install Docker
apt:
name: docker.io
state: present
update_cache: true
- hosts: master
become: yes
tasks:
- name: install kubectl
apt:
name: kubectl=1.14.0-00
state: present
force: yes
The first play in the playbook does the following:
The second play consists of a single task that installs kubectl on your
master node.
Note: While the Kubernetes documentation recommends you use the
latest stable release of Kubernetes for your environment, this tutorial uses
a specific version. This will ensure that you can follow the steps
successfully, as Kubernetes changes rapidly and the latest version may not
work with this tutorial.
Save and close the file when you are finished.
Next, execute the playbook by locally running:
ansible-playbook -i hosts ~/kube-cluster/kube-
dependencies.yml
On completion, you will see output similar to the following:
Output
PLAY [all] ****
~/kube-cluster/master.yml
- hosts: master
become: yes
tasks:
- name: initialize the cluster
shell: kubeadm init --pod-network-
cidr=10.244.0.0/16 >> cluster_initialized.txt
args:
chdir: $HOME
creates: cluster_initialized.txt
Output
PLAY [master] ****
Output
NAME STATUS ROLES AGE VERSION
master Ready master 1d v1.14.0
The output states that the master node has completed all initialization
tasks and is in a Ready state from which it can start accepting worker
nodes and executing tasks sent to the API Server. You can now add the
workers from your local machine.
~/kube-cluster/workers.yml
- hosts: master
become: yes
gather_facts: false
tasks:
- name: get join command
shell: kubeadm token create --print-join-
command
register: join_command_raw
The first play gets the join command that needs to be run on the
worker nodes. This command will be in the following
format:kubeadm join --token <token> <master-ip>:
<master-port> --discovery-token-ca-cert-hash
sha256:<hash>. Once it gets the actual command with the proper
token and hash values, the task sets it as a fact so that the next play
will be able to access that info.
The second play has a single task that runs the join command on all
worker nodes. On completion of this task, the two worker nodes will
be part of the cluster.
Output
PLAY [master] ****
Output
NAME STATUS ROLES AGE VERSION
master Ready master 1d v1.14.0
worker1 Ready <none> 1d v1.14.0
worker2 Ready <none> 1d v1.14.0
If all of your nodes have the value Ready for STATUS, it means that
they’re part of the cluster and ready to run workloads.
If, however, a few of the nodes have NotReady as the STATUS, it
could mean that the worker nodes haven’t finished their setup yet. Wait for
around five to ten minutes before re-running kubectl get nodes and
inspecting the new output. If a few nodes still have NotReady as the
status, you might have to verify and re-run the commands in the previous
steps.
Now that your cluster is verified successfully, let’s schedule an example
Nginx application on the cluster.
Output
NAME TYPE CLUSTER-IP
EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none>
443/TCP 1d
nginx NodePort 10.109.228.209 <none>
80:nginx_port/TCP 40m
From the third line of the above output, you can retrieve the port that
Nginx is running on. Kubernetes will assign a random port that is greater
than 30000 automatically, while ensuring that the port is not already
bound by another service.
To test that everything is working, visit
http://worker_1_ip:nginx_port or
http://worker_2_ip:nginx_port through a browser on your
local machine. You will see Nginx’s familiar welcome page.
If you would like to remove the Nginx application, first delete the
nginx service from the master node:
kubectl delete service nginx
Run the following to ensure that the service has been deleted:
kubectl get services
You will see the following output:
Output
NAME TYPE CLUSTER-IP
EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none>
443/TCP 1d
Then delete the deployment:
kubectl delete deployment nginx
Run the following to confirm that this worked:
kubectl get deployments
Output
No resources found.
Conclusion
In this guide, you’ve successfully set up a Kubernetes cluster on Ubuntu
18.04 using Kubeadm and Ansible for automation.
If you’re wondering what to do with the cluster now that it’s set up, a
good next step would be to get comfortable deploying your own
applications and services onto the cluster. Here’s a list of links with
further information that can guide you in the process:
Dockerizing applications - lists examples that detail how to
containerize applications using Docker.
Pod Overview - describes in detail how Pods work and their
relationship with other Kubernetes objects. Pods are ubiquitous in
Kubernetes, so understanding them will facilitate your work.
Deployments Overview - provides an overview of deployments. It is
useful to understand how controllers such as deployments work since
they are used frequently in stateless applications for scaling and the
automated healing of unhealthy applications.
Services Overview - covers services, another frequently used object
in Kubernetes clusters. Understanding the types of services and the
options they have is essential for running both stateless and stateful
applications.
Other important concepts that you can look into are Volumes, Ingresses
and Secrets, all of which come in handy when deploying production
applications.
Kubernetes has a lot of functionality and features to offer. The
Kubernetes Official Documentation is the best place to learn about
concepts, find task-specific guides, and look up API references for various
objects.
Webinar Series: A Closer Look at
Kubernetes
Prerequisites
To complete this tutorial, you should first complete the previous tutorial in
this series, Getting Started with Kubernetes.
Step 1 – Understanding Kubernetes Primitives
Kubernetes exposes an API that clients use to create, scale, and terminate
applications. Each operation targets one of more objects that Kubernetes
manages. These objects form the basic building blocks of Kubernetes.
They are the primitives through which you manage containerized
applications.
The following is a summary of the key API objects of Kubernetes:
Output
NAME STATUS ROLES AGE
VERSION
spc3c97hei-master-1 Ready master 10m
v1.8.7
spc3c97hei-worker-1 Ready <none> 4m
v1.8.7
spc3c97hei-worker-2 Ready <none> 4m
v1.8.7
kubectl get namespaces
Output
NAME STATUS AGE
default Active 11m
kube-public Active 11m
kube-system Active 11m
stackpoint-system Active 4m
When no Namespace is specified, kubectl targets the default
Namespace.
Now let’s launch an application.
Simple-Pod.yaml
apiVersion: "v1"
kind: Pod
metadata:
name: web-pod
labels:
name: web
env: dev
spec:
containers:
- name: myweb
image: nginx
ports:
- containerPort: 80
name: http
protocol: TCP
Run the following command to create a Pod.
kubectl create -f Simple-Pod.yaml
Output
pod "web-pod" created
Let’s verify the creation of the Pod.
kubectl get pods
Output
NAME READY STATUS RESTARTS AGE
web-pod 1/1 Running 0 2m
In the next step, we will make this Pod accessible to the public Internet.
Simple-Service.yaml
apiVersion: v1
kind: Service
metadata:
name: web-svc
labels:
name: web
env: dev
spec:
selector:
name: web
type: NodePort
ports:
- port: 80
name: http
targetPort: 80
protocol: TCP
The Service discovers all the Pods in the same Namespace that match
the Label with name: web. The selector section of the YAML file
explicitly defines this association.
We specify that the Service is of type NodePort through type: NodePort
declaration.
Then use kubectl to submit it to the cluster.
kubectl create -f Simple-Service.yml
You’ll see this output indicating the service was created successfully:
Output
service "web-svc" created
Let’s get the port on which the Pod is available.
kubectl get services
Output
NAME TYPE CLUSTER-IP EXTERNAL-IP
PORT(S) AGE
kubernetes ClusterIP 10.3.0.1 <none>
443/TCP 28m
web-svc NodePort 10.3.0.143 <none>
80:32097/TCP 38s
From this output, we see that the Service is available on port 32097.
Let’s try to connect to one of the Worker Nodes.
Use the DigitalOcean Console to get the IP address of one of the Worker
Nodes.
The Droplets in the DigitalOcean console associated with your Kubernetes Cluster.
Use the curl command to make an HTTP request to one of the nodes
on port 31930.
curl http://your_worker_1_ip_address:32097
You’ll see the response containing the Nginx default home page:
Output
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.
</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
You’ve defined a Pod and a Service. Now let’s look at scaling with
Replica Sets.
Output
pod "web-pod" deleted
Now create a new Replica Set declaration. The definition of the Replica
Set is identical to a Pod. The key difference is that it contains the replica
element that defines the number of Pods that need to run. Like a Pod, it
also contains Labels as metadata that help in service discovery.
Create the file Simple-RS.yml and add this code to the file:
Simple-RS.yml
apiVersion: apps/v1beta2
kind: ReplicaSet
metadata:
name: web-rs
labels:
name: web
env: dev
spec:
replicas: 3
selector:
matchLabels:
name: web
template:
metadata:
labels:
name: web
env: dev
spec:
containers:
- name: myweb
image: nginx
ports:
- containerPort: 80
name: http
protocol: TCP
Save and close the file.
Now create the Replica Set:
kubectl create -f Simple-RS.yml
Output
replicaset "web-rs" created
Then check the number of Pods:
kubectl get pods
Output
NAME READY STATUS RESTARTS AGE
web-rs-htb58 1/1 Running 0 38s
web-rs-khtld 1/1 Running 0 38s
web-rs-p5lzg 1/1 Running 0 38s
When we access the Service through the NodePort, the request will be
sent to one of the Pods managed by the Replica Set.
Let’s test the functionality of a Replica Set by deleting one of the Pods
and seeing what happens:
kubectl delete pod web-rs-p5lzg
Output
pod "web-rs-p5lzg" deleted
Look at the pods again:
kubectl get pods
Output
NAME READY STATUS
RESTARTS AGE
web-rs-htb58 1/1 Running 0
2m
web-rs-khtld 1/1 Running 0
2m
web-rs-fqh2f 0/1 ContainerCreating 0
2s
web-rs-p5lzg 1/1 Running 0
2m
web-rs-p5lzg 0/1 Terminating 0
2m
As soon as the Pod is deleted, Kubernetes has created another one to
ensure the desired count is maintained.
Now let’s look at Deployments.
Output
replicaset "web-rs" deleted
Now define a new Deployment. Create the file Simple-
Deployment.yaml and add the following code:
Simple-Deployment.yaml
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: web-dep
labels:
name: web
env: dev
spec:
replicas: 3
selector:
matchLabels:
name: web
template:
metadata:
labels:
name: web
spec:
containers:
- name: myweb
image: nginx
ports:
- containerPort: 80
Create a deployment and verify the creation.
kubectl create -f Simple-Deployment.yml
Output
deployment "web-dep" created
View the deployments:
kubectl get deployments
Output
NAME DESIRED CURRENT UP-TO-DATE
AVAILABLE AGE
web-dep 3 3 3 3
1m
Since the Deployment results in the creation of Pods, there will be three
Pods running as per the replicas declaration in the YAML file.
kubectl get pods
Output
NAME READY STATUS
RESTARTS AGE
web-dep-8594f5c765-5wmrb 1/1 Running 0
2m
web-dep-8594f5c765-6cbsr 1/1 Running 0
2m
web-dep-8594f5c765-sczf8 1/1 Running 0
2m
The Service we created earlier will continue to route the requests to the
Pods created by the Deployment. That’s because of the Labels that contain
the same values as the original Pod definition.
Clean up the resources by deleting the Deployment and Service.
kubectl delete deployment web-dep
Output
deployment "web-dep" deleted
kubectl delete service web-svc
Output
service "web-svc" deleted
For more details on Deployments, refer to the Kubernetes
documentation.
Conclusion
In this tutorial, you explored the basic building blocks of Kubernetes as
you deployed an Nginx web server using a Pod, a Service, a Replica Set,
and a Deployment.
In the next part of this series, you will learn how to package, deploy,
scale, and manage a multi-container application.
An Introduction to Helm, the Package
Manager for Kubernetes
An Overview of Helm
Most every programming language and operating system has its own
package manager to help with the installation and maintenance of
software. Helm provides the same basic feature set as many of the package
managers you may already be familiar with, such as Debian’s apt, or
Python’s pip.
Helm can:
Install software.
Automatically install software dependencies.
Upgrade software.
Configure software deployments.
Fetch software packages from repositories.
A command line tool, helm, which provides the user interface to all
Helm functionality.
A companion server component, tiller, that runs on your
Kubernetes cluster, listens for commands from helm, and handles the
configuration and deployment of software releases on the cluster.
The Helm packaging format, called charts.
An official curated charts repository with prepackaged charts for
popular open-source software projects.
Charts
Helm packages are called charts, and they consist of a few YAML
configuration files and some templates that are rendered into Kubernetes
manifest files. Here is the basic directory structure of a chart:
The helm command can install a chart from a local directory, or from a
.tar.gz packaged version of this directory structure. These packaged
charts can also be automatically downloaded and installed from chart
repositories or repos.
We’ll look at chart repositories next.
Chart Repositories
A Helm chart repo is a simple HTTP site that serves an index.yaml file
and .tar.gz packaged charts. The helm command has subcommands
available to help package charts and create the required index.yaml
file. These files can be served by any web server, object storage service, or
a static site host such as GitHub Pages.
Helm comes preconfigured with a default chart repository, referred to as
stable. This repo points to a Google Storage bucket at
https://kubernetes-charts.storage.googleapis.com.
The source for the stable repo can be found in the helm/charts Git
repository on GitHub.
Alternate repos can be added with the helm repo add command.
Some popular alternate repositories are:
The official incubator repo that contains charts that are not yet ready
for stable. Instructions for using incubator can be found on the
official Helm charts GitHub page.
Bitnami Helm Charts which provide some charts that aren’t covered
in the official stable repo.
Chart Configuration
A chart usually comes with default configuration values in its
values.yaml file. Some applications may be fully deployable with
default values, but you’ll typically need to override some of the
configuration to meet your needs.
The values that are exposed for configuration are determined by the
author of the chart. Some are used to configure Kubernetes primitives, and
some may be passed through to the underlying container to configure the
application itself.
Here is a snippet of some example values:
values.yaml
service:
type: ClusterIP
port: 3306
These are options to configure a Kubernetes Service resource. You can
use helm inspect values chart-name to dump all of the
available configuration values for a chart.
These values can be overridden by writing your own YAML file and
using it when running helm install, or by setting options individually
on the command line with the --set flag. You only need to specify those
values that you want to change from the defaults.
A Helm chart deployed with a particular configuration is called a
release. We will talk about releases next.
Releases
During the installation of a chart, Helm combines the chart’s templates
with the configuration specified by the user and the defaults in
value.yaml. These are rendered into Kubernetes manifests that are then
deployed via the Kubernetes API. This creates a release, a specific
configuration and deployment of a particular chart.
This concept of releases is important, because you may want to deploy
the same application more than once on a cluster. For instance, you may
need multiple MySQL servers with different configurations.
You also will probably want to upgrade different instances of a chart
individually. Perhaps one application is ready for an updated MySQL
server but another is not. With Helm, you upgrade each release
individually.
You might upgrade a release because its chart has been updated, or
because you want to update the release’s configuration. Either way, each
upgrade will create a new revision of a release, and Helm will allow you to
easily roll back to previous revisions in case there’s an issue.
Creating Charts
If you can’t find an existing chart for the software you are deploying, you
may want to create your own. Helm can output the scaffold of a chart
directory with helm create chart-name. This will create a folder
with the files and directories we discussed in the Charts section above.
From there, you’ll want to fill out your chart’s metadata in
Chart.yaml and put your Kubernetes manifest files into the
templates directory. You’ll then need to extract relevant configuration
variables out of your manifests and into values.yaml, then include
them back into your manifest templates using the templating system.
The helm command has many subcommands available to help you test,
package, and serve your charts. For more information, please read the
official Helm documentation on developing charts.
Conclusion
In this article we reviewed Helm, the package manager for Kubernetes. We
overviewed the Helm architecture and the individual helm and tiller
components, detailed the Helm charts format, and looked at chart
repositories. We also looked into how to configure a Helm chart and how
configurations and charts are combined and deployed as releases on
Kubernetes clusters. Finally, we touched on the basics of creating a chart
when a suitable chart isn’t already available.
For more information about Helm, take a look at the official Helm
documentation. To find official charts for Helm, check out the official
helm/charts Git repository on GitHub.
How To Install Software on Kubernetes
Clusters with the Helm Package Manager
Prerequisites
For this tutorial you will need:
A Kubernetes 1.8+ cluster with role-based access control (RBAC)
enabled.
The kubectl command-line tool installed on your local machine,
configured to connect to your cluster. You can read more about
installing kubectl in the official documentation.
You can test your connectivity with the following command:
kubectl cluster-info
If you see no errors, you’re connected to the cluster. If you access
multiple clusters with kubectl, be sure to verify that you’ve
selected the correct cluster context:
kubectl config get-contexts
[secondary_label Output]
CURRENT NAME CLUSTER
AUTHINFO NAMESPACE
* do-nyc1-k8s-example do-nyc1-k8s-
example do-nyc1-k8s-example-admin
docker-for-desktop docker-for-
desktop-cluster docker-for-desktop
In this example the asterisk (*) indicates that we are connected to the
do-nyc1-k8s-example cluster. To switch clusters run:
kubectl config use-context context-name
Output
helm installed into /usr/local/bin/helm
Run 'helm init' to configure helm.
Next we will finish the installation by installing some Helm
components on our cluster.
Output
. . .
Output
NAME READY
STATUS RESTARTS AGE
. . .
kube-dns-64f766c69c-rm9tz 3/3
Running 0 22m
kube-proxy-worker-5884 1/1
Running 1 21m
kube-proxy-worker-5885 1/1
Running 1 21m
kubernetes-dashboard-7dd4fc69c8-c4gwk 1/1
Running 0 22m
tiller-deploy-5c688d5f9b-lccsk 1/1
Running 0 40s
The Tiller pod name begins with the prefix tiller-deploy-.
Now that we’ve installed both Helm components, we’re ready to use
helm to install our first application.
Step 3 — Installing a Helm Chart
Helm software packages are called charts. Helm comes preconfigured with
a curated chart repository called stable. You can browse the available
charts in their GitHub repo. We are going to install the Kubernetes
Dashboard as an example.
Use helm to install the kubernetes-dashboard package from the
stable repo:
helm install stable/kubernetes-dashboard --name
dashboard-demo
Output
NAME: dashboard-demo
LAST DEPLOYED: Wed Aug 8 20:11:07 2018
NAMESPACE: default
STATUS: DEPLOYED
. . .
Notice the NAME line, highlighted in the above example output. In this
case we specified the name dashboard-demo. This is the name of our
release. A Helm release is a single deployment of one chart with a specific
configuration. You can deploy multiple releases of the same chart with,
each with its own configuration.
If you don’t specify your own release name using --name, Helm will
create a random name for you.
We can ask Helm for a list of releases on this cluster:
helm list
Output
NAME REVISION UPDATED
STATUS CHART NAMESPACE
dashboard-demo 1 Wed Aug 8 20:11:11
2018 DEPLOYED kubernetes-dashboard-0.7.1
default
We can now use kubectl to verify that a new service has been
deployed on the cluster:
kubectl get services
Output
NAME TYPE
CLUSTER-IP EXTERNAL-IP PORT(S) AGE
dashboard-demo-kubernetes-dashboard ClusterIP
10.32.104.73 <none> 443/TCP 51s
kubernetes ClusterIP
10.32.0.1 <none> 443/TCP 34m
Notice that by default the service name corresponding to our release is a
combination of the Helm release name and the chart name.
Now that we’ve deployed the application, let’s use Helm to change its
configuration and update the deployment.
Output
NAME TYPE CLUSTER-IP
EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.32.0.1
<none> 443/TCP 36m
dashboard ClusterIP 10.32.198.148
<none> 443/TCP 40s
Our service name has been updated to the new value.
Note: At this point you may want to actually load the Kubernetes
Dashboard in your browser and check it out. To do so, first run the
following command:
kubectl proxy
This creates a proxy that lets you access remote cluster resources from
your local computer. Based on the previous instructions your dashboard
service is named kubernetes-dashboard and it’s running in the
default namespace. You may now access the dashboard at the following
url:
http://localhost:8001/api/v1/namespaces/default/se
rvices/https:dashboard:/proxy/
If necessary, substitute your own service name and namespace for the
highlighted portions. Instructions for actually using the dashboard are out
of scope for this tutorial, but you can read the official Kubernetes
Dashboard docs for more information.
Next we’ll look at Helm’s ability to roll back releases.
Output
NAME REVISION UPDATED
STATUS CHART NAMESPACE
dashboard-demo 2 Wed Aug 8 20:13:15 2018
DEPLOYED kubernetes-dashboard-0.7.1 default
The REVISION column tells us that this is now the second revision.
Use helm rollback to roll back to the first revision:
helm rollback dashboard-demo 1
You should see the following output, indicating that the rollback
succeeded:
Output
Rollback was a success! Happy Helming!
At this point, if you run kubectl get services again, you will
notice that the service name has changed back to its previous value. Helm
has re-deployed the application with revision 1’s configuration.
Next we’ll look into deleting releases with Helm.
Output
release "dashboard-demo" deleted
Though the release has been deleted and the dashboard application is no
longer running, Helm saves all the revision information in case you want
to re-deploy the release. If you tried to helm install a new
dashboard-demo release right now, you’d get an error:
Error: a release named dashboard-demo already
exists.
If you use the --deleted flag to list your deleted releases, you’ll see
that the release is still around:
helm list --deleted
Output
NAME REVISION UPDATED
STATUS CHART NAMESPACE
dashboard-demo 3 Wed Aug 8 20:15:21
2018 DELETED kubernetes-dashboard-0.7.1
default
To really delete the release and purge all old revisions, use the --
purge flag with the helm delete command:
helm delete dashboard-demo --purge
Now the release has been truly deleted, and you can reuse the release
name.
Conclusion
In this tutorial we installed the helm command-line tool and its tiller
companion service. We also explored installing, upgrading, rolling back,
and deleting Helm charts and releases.
For more information about Helm and Helm charts, please see the
official Helm documentation.
Architecting Applications for Kubernetes
One popular methodology that can help you focus on the characteristics
that matter most when creating cloud-ready web apps is the Twelve-Factor
App philosophy. Written to help developers and operations teams
understand the core qualities shared by web services designed to run in the
cloud, the principles apply very well to software that will live in a
clustered environment like Kubernetes. While monolithic applications can
benefit from following these recommendations, microservices
architectures designed around these principles work particularly well.
A quick summary of the Twelve Factors are:
1. Codebase: Manage all code in version control systems (like Git or
Mercurial). The codebase comprehensively dictates what is deployed.
2. Dependencies: Dependencies should be managed entirely and
explicitly by the codebase, either vendored (stored with the code) or
version pinned in a format that a package manager can install from.
3. Config: Separate configuration parameters from the application and
define them in the deployment environment instead of baking them
into the application itself.
4. Backing services: Local and remote services are both abstracted as
network-accessible resources with connection details set in
configuration.
5. Build, release, run: The build stage of your application should be
completely separate from your application release and operations
processes. The build stage creates a deployment artifact from source
code, the release stage combines the artifact and configuration, and
the run stage executes the release.
6. Processes: Applications are implemented as processes that should not
rely on storing state locally. State should be offloaded to a backing
service as described in the fourth factor.
7. Port binding: Applications should natively bind to a port and listen
for connections. Routing and request forwarding should be handled
externally.
8. Concurrency: Applications should rely on scaling through the process
model. Running multiple copies of an application concurrently,
potentially across multiple servers, allows scaling without adjusting
application code.
9. Disposability: Processes should be able to start quickly and stop
gracefully without serious side effects.
10. Dev/prod parity: Your testing, staging, and production environments
should match closely and be kept in sync. Differences between
environments are opportunities for incompatibilities and untested
configurations to appear.
11. Logs: Applications should stream logs to standard output so external
services can decide how to best handle them.
12. Admin processes: One-off administration processes should be run
against specific releases and shipped with the main process code.
hardcoded_config.py
from flask import Flask
DB_HOST = 'mydb.mycloud.com'
DB_USER = 'sammy'
app = Flask(__name__)
@app.route('/')
def print_config():
output = 'DB_HOST: {} -- DB_USER:
{}'.format(DB_HOST, DB_USER)
return output
Running this simple app (consult the Flask Quickstart to learn how) and
visiting its web endpoint will display a page containing these two config
values.
Now, here’s the same example with the config values externalized to the
app’s running environment:
env_config.py
import os
app = Flask(__name__)
@app.route('/')
def print_config():
output = 'DB_HOST: {} -- DB_USER:
{}'.format(DB_HOST, DB_USER)
return output
Before running the app, we set the necessary config variables in the
local environment:
export APP_DB_HOST=mydb.mycloud.com
export APP_DB_USER=sammy
flask run
The displayed web page should contain the same text as in the first
example, but the app’s config can now be modified independently of the
application code. You can use a similar approach to read in config
parameters from a local file.
In the next section we’ll discuss moving application state outside of
containers.
env_config.py
. . .
@app.route('/')
def print_config():
output = 'DB_HOST: {} -- DB_USER:
{}'.format(DB_HOST, DB_USER)
return output
@app.route('/health')
def return_ok():
return 'Ok!', 200
A Kubernetes liveness probe that checks this path would then look
something like this:
pod_spec.yaml
. . .
livenessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 5
periodSeconds: 2
The initialDelaySeconds field specifies that Kubernetes
(specifically the Node Kubelet) should probe the /health endpoint after
waiting 5 seconds, and periodSeconds tells the Kubelet to probe
/health every 2 seconds.
To learn more about liveness and readiness probes, consult the
Kubernetes documentation.
This minimal set of metrics should give you enough data to alert on
when your application’s performance degrades. Implementing this
instrumentation along with the health checks discussed above will allow
you to quickly detect and recover from a failing application.
To learn more about signals to measure when monitoring your
applications, consult Monitoring Distributed Systems from the Google
Site Reliability Engineering book.
In addition to thinking about and designing features for publishing
telemetry data, you should also plan how your application will log in a
distributed cluster-based environment. You should ideally remove
hardcoded configuration references to local log files and log directories,
and instead log directly to stdout and stderr. You should treat logs as a
continuous event stream, or sequence of time-ordered events. This output
stream will then get captured by the container enveloping your
application, from which it can be forwarded to a logging layer like the
EFK (Elasticsearch, Fluentd, and Kibana) stack. Kubernetes provides a lot
of flexibility in designing your logging architecture, which we’ll explore
in more detail below.
Summary
Before creating a Dockerfile for your application, one of the first steps is
taking stock of the software and operating system dependencies your
application needs to run correctly. Dockerfiles allow you to explicitly
version every piece of software installed into the image, and you should
take advantage of this feature by explicitly declaring the parent image,
software library, and programming language versions.
Avoid latest tags and unversioned packages as much as possible, as
these can shift, potentially breaking your application. You may wish to
create a private registry or private mirror of a public registry to exert more
control over image versioning and to prevent upstream changes from
unintentionally breaking your image builds.
To learn more about setting up a private image registry, consult Deploy
a Registry Server from the Docker official documentation and the
Registries section below.
Inject Configuration
Dockerfile
...
ENV MYSQL_USER=my_db_user
...
Your app can then parse these values from its running environment and
configure its settings appropriately.
You can also pass in environment variables as parameters when starting
a container using docker run and the -e flag:
docker run -e MYSQL_USER='my_db_user' IMAGE[:TAG]
Finally, you can use an env file, containing a list of environment
variables and their values. To do this, create the file and use the --env-
file parameter to pass it in to the command:
docker run --env-file var_list IMAGE[:TAG]
If you’re modernizing your application to run it using a cluster manager
like Kubernetes, you should further externalize your config from the
image, and manage configuration using Kubernetes’ built-in ConfigMap
and Secrets objects. This allows you to separate configuration from image
manifests, so that you can manage and version it separately from your
application. To learn how to externalize configuration using ConfigMaps
and Secrets, consult the ConfigMaps and Secrets section below.
There are many paid continuous integration products that have built-in
integrations with popular version control services like GitHub and image
registries like Docker Hub. An alternative to these products is Jenkins, a
free and open-source build automation server that can be configured to
perform all of the functions described above. To learn how to set up a
Jenkins continuous integration pipeline, consult How To Set Up
Continuous Integration Pipelines in Jenkins on Ubuntu 16.04.
When working with containers, it’s important to think about the logging
infrastructure you will use to manage and store logs for all your running
and stopped containers. There are multiple container-level patterns you
can use for logging, and also multiple Kubernetes-level patterns.
In Kubernetes, by default containers use the json-file Docker
logging driver, which captures the stdout and stderr streams and writes
them to JSON files on the Node where the container is running.
Sometimes logging directly to stderr and stdout may not be enough for
your application container, and you may want to pair the app container
with a logging sidecar container in a Kubernetes Pod. This sidecar
container can then pick up logs from the filesystem, a local socket, or the
systemd journal, granting you a little more flexibility than simply using
the stderr and stdout streams. This container can also do some processing
and then stream enriched logs to stdout/stderr, or directly to a logging
backend. To learn more about Kubernetes logging patterns, consult the
Kubernetes logging and monitoring section of this tutorial.
How your application logs at the container level will depend on its
complexity. For simple, single-purpose microservices, logging directly to
stdout/stderr and letting Kubernetes pick up these streams is the
recommended approach, as you can then leverage the kubectl logs
command to access log streams from your Kubernetes-deployed
containers.
Similar to logging, you should begin thinking about monitoring in a
container and cluster-based environment. Docker provides the helpful
docker stats command for grabbing standard metrics like CPU and
memory usage for running containers on the host, and exposes even more
metrics through the Remote REST API. Additionally, the open-source tool
cAdvisor (installed on Kubernetes Nodes by default) provides more
advanced functionality like historical metric collection, metric data
export, and a helpful web UI for sorting through the data.
However, in a multi-node, multi-container production environment,
more complex metrics stacks like Prometheus and Grafana may help
organize and monitor your containers’ performance data.
Summary
In the next section, we’ll explore Kubernetes features that allow you to
run and scale your containerized app in a cluster.
Deploying on Kubernetes
At this point, you’ve containerized your app and implemented logic to
maximize its portability and observability in Cloud Native environments.
We’ll now explore Kubernetes features that provide simple interfaces for
managing and scaling your apps in a Kubernetes cluster.
flask_deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: flask-app
labels:
app: flask-app
spec:
replicas: 3
selector:
matchLabels:
app: flask-app
template:
metadata:
labels:
app: flask-app
spec:
containers:
- name: flask
image: sammy/flask_app:1.0
ports:
- containerPort: 8080
This Deployment launches 3 Pods that run a container called flask
using the sammy/flask_app image (version 1.0) with port 8080
open. The Deployment is called flask-app.
To learn more about Kubernetes Pods and Deployments, consult the
Pods and Deployments sections of the official Kubernetes documentation.
- name: nginx-sync
image: nginx-sync
volumeMounts:
- name: nginx-web
mountPath: /web-data
volumes:
- name: nginx-web
emptyDir: {}
We use a volumeMount for each container, indicating that we’d like to
mount the nginx-web volume containing the web page files at
/usr/share/nginx/html in the nginx container and at /web-
data in the nginx-sync container. We also define a volume called
nginx-web of type emptyDir.
In a similar fashion, you can configure Pod storage using cloud block
storage products by modifying the volume type from emptyDir to the
relevant cloud storage volume type.
The lifecycle of a Volume is tied to the lifecycle of the Pod, but not to
that of a container. If a container within a Pod dies, the Volume persists
and the newly launched container will be able to mount the same Volume
and access its data. When a Pod gets restarted or dies, so do its Volumes,
although if the Volumes consist of cloud block storage, they will simply be
unmounted with data still accessible by future Pods.
To preserve data across Pod restarts and updates, the PersistentVolume
(PV) and PersistentVolumeClaim (PVC) objects must be used.
PersistentVolumes are abstractions representing pieces of persistent
storage like cloud block storage volumes or NFS storage. They are created
separately from PersistentVolumeClaims, which are demands for pieces of
storage by developers. In their Pod configurations, developers request
persistent storage using PVCs, which Kubernetes matches with available
PV Volumes (if using cloud block storage, Kubernetes can dynamically
create PersistentVolumes when PersistentVolumeClaims are created).
If your application requires one persistent volume per replica, which is
the case with many databases, you should not use Deployments but use the
StatefulSet controller, which is designed for apps that require stable
network identifiers, stable persistent storage, and ordering guarantees.
Deployments should be used for stateless applications, and if you define a
PersistentVolumeClaim for use in a Deployment configuration, that PVC
will be shared by all the Deployment’s replicas.
To learn more about the StatefulSet controller, consult the Kubernetes
documentation. To learn more about PersistentVolumes and
PersistentVolume claims, consult the Kubernetes storage documentation.
Similar to Docker, Kubernetes provides the env and envFrom fields for
setting environment variables in Pod configuration files. Here’s a sample
snippet from a Pod configuration file that sets the HOSTNAME
environment variable in the running Pod to my_hostname :
sample_pod.yaml
...
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
env:
- name: HOSTNAME
value: my_hostname
...
This allows you to move configuration out of Dockerfiles and into Pod
and Deployment configuration files. A key advantage of further
externalizing configuration from your Dockerfiles is that you can now
modify these Kubernetes workload configurations (say, by changing the
HOSTNAME value to my_hostname_2) separately from your application
container definitions. Once you modify the Pod configuration file, you can
then redeploy the Pod using its new environment, while the underlying
container image (defined via its Dockerfile) does not need to be rebuilt,
tested, and pushed to a repository. You can also version these Pod and
Deployment configurations separately from your Dockerfiles, allowing
you to quickly detect breaking changes and further separate config issues
from application bugs.
Kubernetes provides another construct for further externalizing and
managing configuration data: ConfigMaps and Secrets.
ConfigMaps allow you to save configuration data as objects that you then
reference in your Pod and Deployment configuration files, so that you can
avoid hardcoding configuration data and reuse it across Pods and
Deployments.
Here’s an example, using the Pod config from above. We’ll first save
the HOSTNAME environment variable as a ConfigMap, and then reference
it in the Pod config:
kubectl create configmap hostname --from-
literal=HOSTNAME=my_host_name
To reference it from the Pod configuration file, we use the the
valueFrom and configMapKeyRef constructs:
sample_pod_configmap.yaml
...
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
env:
- name: HOSTNAME
valueFrom:
configMapKeyRef:
name: hostname
key: HOSTNAME
...
So the HOSTNAME environment variable’s value has been completely
externalized from configuration files. We can then update these variables
across all Deployments and Pods referencing them, and restart the Pods
for the changes to take effect.
If your applications use configuration files, ConfigMaps additionally
allow you to store these files as ConfigMap objects (using the --from-
file flag), which you can then mount into containers as configuration
files.
Secrets provide the same essential functionality as ConfigMaps, but
should be used for sensitive data like database credentials as the values are
base64-encoded.
To learn more about ConfigMaps and Secrets consult the Kubernetes
documentation.
Create Services
Once you have your application up and running in Kubernetes, every Pod
will be assigned an (internal) IP address, shared by its containers. If one of
these Pods is removed or dies, newly started Pods will be assigned
different IP addresses.
For long-running services that expose functionality to internal and/or
external clients, you may wish to grant a set of Pods performing the same
function (or Deployment) a stable IP address that load balances requests
across its containers. You can do this using a Kubernetes Service.
Kubernetes Services have 4 types, specified by the type field in the
Service configuration file:
flask_app_svc.yaml
apiVersion: v1
kind: Service
metadata:
name: flask-svc
spec:
ports:
- port: 80
targetPort: 8080
selector:
app: flask-app
type: LoadBalancer
Here we choose to expose the flask-app Deployment using this
flask-svc Service. We create a cloud load balancer to route traffic
from load balancer port 80 to exposed container port 8080.
To learn more about Kubernetes Services, consult the Services section
of the Kubernetes docs.
Conclusion
Migrating and modernizing an application so that it can efficiently run in a
Kubernetes cluster often involves non-trivial amounts of planning and
architecting of software and infrastructure changes. Once implemented,
these changes allow service owners to continuously deploy new versions
of their apps and easily scale them as necessary, with minimal amounts of
manual intervention. Steps like externalizing configuration from your app,
setting up proper logging and metrics publishing, and configuring health
checks allow you to fully take advantage of the Cloud Native paradigm
that Kubernetes has been designed around. By building portable containers
and managing them using Kubernetes objects like Deployments and
Services, you can fully use your available compute infrastructure and
development resources.
How To Build a Node.js Application with
Docker
Prerequisites
To follow this tutorial, you will need: - One Ubuntu 18.04 server, set up
following this Initial Server Setup guide. - Docker installed on your server,
following Steps 1 and 2 of How To Install and Use Docker on Ubuntu
18.04. - Node.js and npm installed, following these instructions on
installing with the PPA managed by NodeSource. - A Docker Hub account.
For an overview of how to set this up, refer to this introduction on getting
started with Docker Hub.
~/node_project/package.json
{
"name": "nodejs-image-demo",
"version": "1.0.0",
"description": "nodejs image demo",
"author": "Sammy the Shark <[email protected]>",
"license": "MIT",
"main": "app.js",
"keywords": [
"nodejs",
"bootstrap",
"express"
],
"dependencies": {
"express": "^4.16.4"
}
}
This file includes the project name, author, and license under which it is
being shared. Npm recommends making your project name short and
descriptive, and avoiding duplicates in the npm registry. We’ve listed the
MIT license in the license field, permitting the free use and distribution of
the application code.
Additionally, the file specifies: - "main": The entrypoint for the
application, app.js. You will create this file next. - "dependencies":
The project dependencies — in this case, Express 4.16.4 or above.
Though this file does not list a repository, you can add one by following
these guidelines on adding a repository to your package.json file. This
is a good addition if you are versioning your application.
Save and close the file when you’ve finished making changes.
To install your project’s dependencies, run the following command:
npm install
This will install the packages you’ve listed in your package.json
file in your project directory.
We can now move on to building the application files.
~/node_project/app.js
const express = require('express');
const app = express();
const router = express.Router();
~/node_project/app.js
...
router.use(function (req,res,next) {
console.log('/' + req.method);
next();
});
router.get('/', function(req,res){
res.sendFile(path + 'index.html');
});
router.get('/sharks', function(req,res){
res.sendFile(path + 'sharks.html');
});
The router.use function loads a middleware function that will log
the router’s requests and pass them on to the application’s routes. These
are defined in the subsequent functions, which specify that a GET request
to the base project URL should return the index.html page, while a
GET request to the /sharks route should return sharks.html.
Finally, mount the router middleware and the application’s static
assets and tell the app to listen on port 8080:
~/node_project/app.js
...
app.use(express.static(path));
app.use('/', router);
app.listen(port, function () {
console.log('Example app listening on port
8080!')
})
The finished app.js file will look like this:
~/node_project/app.js
const express = require('express');
const app = express();
const router = express.Router();
router.use(function (req,res,next) {
console.log('/' + req.method);
next();
});
router.get('/', function(req,res){
res.sendFile(path + 'index.html');
});
router.get('/sharks', function(req,res){
res.sendFile(path + 'sharks.html');
});
app.use(express.static(path));
app.use('/', router);
app.listen(port, function () {
console.log('Example app listening on port
8080!')
})
Save and close the file when you are finished.
Next, let’s add some static content to the application. Start by creating
the views directory:
mkdir views
Open the landing page file, index.html:
nano views/index.html
Add the following code to the file, which will import Boostrap and
create a jumbotron component with a link to the more detailed
sharks.html info page:
~/node_project/views/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>About Sharks</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-
width, initial-scale=1">
<link rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap
/4.1.3/css/bootstrap.min.css" integrity="sha384-
MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkF
OJwJ8ERdknLPMO" crossorigin="anonymous">
<link href="css/styles.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?
family=Merriweather:400,700" rel="stylesheet"
type="text/css">
</head>
<body>
<nav class="navbar navbar-dark bg-dark navbar-
static-top navbar-expand-md">
<div class="container">
<button type="button" class="navbar-
toggler collapsed" data-toggle="collapse" data-
target="#bs-example-navbar-collapse-1" aria-
expanded="false"> <span class="sr-only">Toggle
navigation
</button> <a class="navbar-brand"
href="#">Everything Sharks</a>
<div class="collapse navbar-collapse"
id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav mr-
auto">
<li class="active nav-item"><a
href="/" class="nav-link">Home</a>
</li>
<li class="nav-item"><a
href="/sharks" class="nav-link">Sharks</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="jumbotron">
<div class="container">
<h1>Want to Learn About Sharks?</h1>
<p>Are you ready to learn about
sharks?</p>
<br>
<p><a class="btn btn-primary btn-lg"
href="/sharks" role="button">Get Shark Info</a>
</p>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-lg-6">
<h3>Not all sharks are alike</h3>
<p>Though some are dangerous,
sharks generally do not attack humans. Out of the
500 species known to researchers, only 30 have
been known to attack humans.
</p>
</div>
<div class="col-lg-6">
<h3>Sharks are ancient</h3>
<p>There is evidence to suggest
that sharks lived up to 400 million years ago.
</p>
</div>
</div>
</div>
</body>
</html>
The top-level navbar here allows users to toggle between the Home and
Sharks pages. In the navbar-nav subcomponent, we are using
Bootstrap’s active class to indicate the current page to the user. We’ve
also specified the routes to our static pages, which match the routes we
defined in app.js:
~/node_project/views/index.html
...
<div class="collapse navbar-collapse" id="bs-
example-navbar-collapse-1">
<ul class="nav navbar-nav mr-auto">
<li class="active nav-item"><a href="/"
class="nav-link">Home</a>
</li>
<li class="nav-item"><a href="/sharks"
class="nav-link">Sharks</a>
</li>
</ul>
</div>
...
Additionally, we’ve created a link to our shark information page in our
jumbotron’s button:
~/node_project/views/index.html
...
<div class="jumbotron">
<div class="container">
<h1>Want to Learn About Sharks?</h1>
<p>Are you ready to learn about sharks?</p>
<br>
<p><a class="btn btn-primary btn-lg"
href="/sharks" role="button">Get Shark Info</a>
</p>
</div>
</div>
...
There is also a link to a custom style sheet in the header:
~/node_project/views/index.html
...
<link href="css/styles.css" rel="stylesheet">
...
We will create this style sheet at the end of this step.
Save and close the file when you are finished.
With the application landing page in place, we can create our shark
information page, sharks.html, which will offer interested users more
information about sharks.
Open the file:
nano views/sharks.html
Add the following code, which imports Bootstrap and the custom style
sheet and offers users detailed information about certain sharks:
~/node_project/views/sharks.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>About Sharks</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-
width, initial-scale=1">
<link rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap
/4.1.3/css/bootstrap.min.css" integrity="sha384-
MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkF
OJwJ8ERdknLPMO" crossorigin="anonymous">
<link href="css/styles.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?
family=Merriweather:400,700" rel="stylesheet"
type="text/css">
</head>
<nav class="navbar navbar-dark bg-dark navbar-
static-top navbar-expand-md">
<div class="container">
<button type="button" class="navbar-
toggler collapsed" data-toggle="collapse" data-
target="#bs-example-navbar-collapse-1" aria-
expanded="false"> <span class="sr-only">Toggle
navigation
</button> <a class="navbar-brand"
href="/">Everything Sharks</a>
<div class="collapse navbar-collapse"
id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav mr-auto">
<li class="nav-item"><a href="/"
class="nav-link">Home</a>
</li>
<li class="active nav-item"><a
href="/sharks" class="nav-link">Sharks</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="jumbotron text-center">
<h1>Shark Info</h1>
</div>
<div class="container">
<div class="row">
<div class="col-lg-6">
<p>
<div class="caption">Some sharks
are known to be dangerous to humans, though many
more are not. The sawshark, for example, is not
considered a threat to humans.
</div>
<img
src="https://assets.digitalocean.com/articles/dock
er_node_image/sawshark.jpg" alt="Sawshark">
</p>
</div>
<div class="col-lg-6">
<p>
<div class="caption">Other sharks
are known to be friendly and welcoming!</div>
<img
src="https://assets.digitalocean.com/articles/dock
er_node_image/sammy.png" alt="Sammy the Shark">
</p>
</div>
</div>
</div>
</html>
Note that in this file, we again use the active class to indicate the
current page.
Save and close the file when you are finished.
Finally, create the custom CSS style sheet that you’ve linked to in
index.html and sharks.html by first creating a css folder in the
views directory:
mkdir views/css
Open the style sheet:
nano views/css/styles.css
Add the following code, which will set the desired color and font for our
pages:
~/node_project/views/css/styles.css
.navbar {
margin-bottom: 0;
}
body {
background: #020A1B;
color: #ffffff;
font-family: 'Merriweather', sans-serif;
}
h1,
h2 {
font-weight: bold;
}
p {
font-size: 16px;
color: #ffffff;
}
.jumbotron {
background: #0048CD;
color: white;
text-align: center;
}
.jumbotron p {
color: white;
font-size: 26px;
}
.btn-primary {
color: #fff;
text-color: #000000;
border-color: white;
margin-bottom: 5px;
}
img,
video,
audio {
margin-top: 20px;
max-width: 80%;
}
div.caption: {
float: left;
clear: both;
}
In addition to setting font and color, this file also limits the size of the
images by specifying a max-width of 80%. This will prevent them from
taking up more room than we would like on the page.
Save and close the file when you are finished.
With the application files in place and the project dependencies
installed, you are ready to start the application.
If you followed the initial server setup tutorial in the prerequisites, you
will have an active firewall permitting only SSH traffic. To permit traffic
to port 8080 run:
sudo ufw allow 8080
To start the application, make sure that you are in your project’s root
directory:
cd ~/node_project
Start the application with node app.js:
node app.js
Navigate your browser to http://your_server_ip:8080. You
will see the following landing page:
Click on the Get Shark Info button. You will see the following
information page:
Shark Info Page
You now have an application up and running. When you are ready, quit
the server by typing CTRL+C. We can now move on to creating the
Dockerfile that will allow us to recreate and scale this application as
desired.
~/node_project/Dockerfile
FROM node:10-alpine
This image includes Node.js and npm. Each Dockerfile must begin with
a FROM instruction.
By default, the Docker Node image includes a non-root node user that
you can use to avoid running your application container as root. It is a
recommended security practice to avoid running containers as root and to
restrict capabilities within the container to only those required to run its
processes. We will therefore use the node user’s home directory as the
working directory for our application and set them as our user inside the
container. For more information about best practices when working with
the Docker Node image, see this best practices guide.
To fine-tune the permissions on our application code in the container,
let’s create the node_modules subdirectory in /home/node along
with the app directory. Creating these directories will ensure that they
have the permissions we want, which will be important when we create
local node modules in the container with npm install. In addition to
creating these directories, we will set ownership on them to our node user:
~/node_project/Dockerfile
...
RUN mkdir -p /home/node/app/node_modules && chown
-R node:node /home/node/app
For more information on the utility of consolidating RUN instructions,
see this discussion of how to manage container layers.
Next, set the working directory of the application to
/home/node/app:
~/node_project/Dockerfile
...
WORKDIR /home/node/app
If a WORKDIR isn’t set, Docker will create one by default, so it’s a good
idea to set it explicitly.
Next, copy the package.json and package-lock.json (for npm
5+) files:
~/node_project/Dockerfile
...
COPY package*.json ./
Adding this COPY instruction before running npm install or
copying the application code allows us to take advantage of Docker’s
caching mechanism. At each stage in the build, Docker will check to see if
it has a layer cached for that particular instruction. If we change
package.json, this layer will be rebuilt, but if we don’t, this
instruction will allow Docker to use the existing image layer and skip
reinstalling our node modules.
To ensure that all of the application files are owned by the non-root
node user, including the contents of the node_modules directory, switch
the user to node before running npm install:
~/node_project/Dockerfile
...
USER node
After copying the project dependencies and switching our user, we can
run npm install:
~/node_project/Dockerfile
...
RUN npm install
Next, copy your application code with the appropriate permissions to
the application directory on the container:
~/node_project/Dockerfile
...
COPY --chown=node:node . .
This will ensure that the application files are owned by the non-root
node user.
Finally, expose port 8080 on the container and start the application:
~/node_project/Dockerfile
...
EXPOSE 8080
~/node_project/Dockerfile
FROM node:10-alpine
WORKDIR /home/node/app
COPY package*.json ./
USER node
EXPOSE 8080
~/node_project/.dockerignore
node_modules
npm-debug.log
Dockerfile
.dockerignore
If you are working with Git then you will also want to add your .git
directory and .gitignore file.
Save and close the file when you are finished.
You are now ready to build the application image using the docker
build command. Using the -t flag with docker build will allow
you to tag the image with a memorable name. Because we are going to
push the image to Docker Hub, let’s include our Docker Hub username in
the tag. We will tag the image as nodejs-image-demo, but feel free to
replace this with a name of your own choosing. Remember to also replace
your_dockerhub_username with your own Docker Hub username:
docker build -t your_dockerhub_username/nodejs-
image-demo .
The . specifies that the build context is the current directory.
It will take a minute or two to build the image. Once it is complete,
check your images:
docker images
You will see the following output:
Output
REPOSITORY
TAG IMAGE ID CREATED
SIZE
your_dockerhub_username/nodejs-image-demo
latest 1c723fb2ef12 8 seconds
ago 73MB
node
10-alpine f09e7c96b6de 3 weeks
ago 70.7MB
It is now possible to create a container with this image using docker
run. We will include three flags with this command: - -p: This publishes
the port on the container and maps it to a port on our host. We will use port
80 on the host, but you should feel free to modify this as necessary if you
have another process running on that port. For more information about
how this works, see this discussion in the Docker docs on port binding. - -
d: This runs the container in the background. - --name: This allows us to
give the container a memorable name.
Run the following command to build the container:
docker run --name nodejs-image-demo -p 80:8080 -d
your_dockerhub_username/nodejs-image-demo
Once your container is up and running, you can inspect a list of your
running containers with docker ps:
docker ps
You will see the following output:
Output
CONTAINER ID IMAGE
COMMAND CREATED STATUS
PORTS NAMES
e50ad27074a7
your_dockerhub_username/nodejs-image-demo
"node app.js" 8 seconds ago Up 7
seconds 0.0.0.0:80->8080/tcp nodejs-
image-demo
With your container running, you can now visit your application by
navigating your browser to http://your_server_ip. You will see
your application landing page once again:
Application Landing Page
Now that you have created an image for your application, you can push
it to Docker Hub for future use.
Output
CONTAINER ID IMAGE
COMMAND CREATED STATUS
PORTS NAMES
e50ad27074a7
your_dockerhub_username/nodejs-image-demo "node
app.js" 3 minutes ago Up 3 minutes
0.0.0.0:80->8080/tcp nodejs-image-demo
Using the CONTAINER ID listed in your output, stop the running
application container. Be sure to replace the highlighted ID below with
your own CONTAINER ID:
docker stop e50ad27074a7
List your all of your images with the -a flag:
docker images -a
You will see the following output with the name of your image,
your_dockerhub_username/nodejs-image-demo, along with
the node image and the other images from your build:
Output
REPOSITORY
TAG IMAGE ID CREATED
SIZE
your_dockerhub_username/nodejs-image-demo
latest 1c723fb2ef12 7 minutes
ago 73MB
<none>
<none> 2e3267d9ac02 4 minutes
ago 72.9MB
<none>
<none> 8352b41730b9 4 minutes
ago 73MB
<none>
<none> 5d58b92823cb 4 minutes
ago 73MB
<none>
<none> 3f1e35d7062a 4 minutes
ago 73MB
<none>
<none> 02176311e4d0 4 minutes
ago 73MB
<none>
<none> 8e84b33edcda 4 minutes
ago 70.7MB
<none>
<none> 6a5ed70f86f2 4 minutes
ago 70.7MB
<none>
<none> 776b2637d3c1 4 minutes
ago 70.7MB
node
10-alpine f09e7c96b6de 3 weeks
ago 70.7MB
Remove the stopped container and all of the images, including unused
or dangling images, with the following command:
docker system prune -a
Type y when prompted in the output to confirm that you would like to
remove the stopped container and images. Be advised that this will also
remove your build cache.
You have now removed both the container running your application
image and the image itself. For more information on removing Docker
containers, images, and volumes, please see How To Remove Docker
Images, Containers, and Volumes.
With all of your images and containers deleted, you can now pull the
application image from Docker Hub:
docker pull your_dockerhub_username/nodejs-image-
demo
List your images once again:
docker images
You will see your application image:
Output
REPOSITORY TAG
IMAGE ID CREATED SIZE
your_dockerhub_username/nodejs-image-demo
latest 1c723fb2ef12 11 minutes
ago 73MB
You can now rebuild your container using the command from Step 3:
docker run --name nodejs-image-demo -p 80:8080 -d
your_dockerhub_username/nodejs-image-demo
List your running containers:
docker ps
Output
CONTAINER ID IMAGE
COMMAND CREATED STATUS
PORTS NAMES
f6bc2f50dff6
your_dockerhub_username/nodejs-image-demo
"node app.js" 4 seconds ago Up 3
seconds 0.0.0.0:80->8080/tcp nodejs-
image-demo
Visit http://your_server_ip once again to view your running
application.
Conclusion
In this tutorial you created a static web application with Express and
Bootstrap, as well as a Docker image for this application. You used this
image to create a container and pushed the image to Docker Hub. From
there, you were able to destroy your image and container and recreate
them using your Docker Hub repository.
If you are interested in learning more about how to work with tools like
Docker Compose and Docker Machine to create multi-container setups,
you can look at the following guides: - How To Install Docker Compose on
Ubuntu 18.04. - How To Provision and Manage Remote Docker Hosts with
Docker Machine on Ubuntu 18.04.
For general tips on working with container data, see: - How To Share
Data between Docker Containers. - How To Share Data Between the
Docker Container and the Host.
If you are interested in other Docker-related topics, please see our
complete library of Docker tutorials.
Containerizing a Node.js Application for
Development With Docker Compose
Prerequisites
To follow this tutorial, you will need: - A development server running
Ubuntu 18.04, along with a non-root user with sudo privileges and an
active firewall. For guidance on how to set these up, please see this Initial
Server Setup guide. - Docker installed on your server, following Steps 1
and 2 of How To Install and Use Docker on Ubuntu 18.04. - Docker
Compose installed on your server, following Step 1 of How To Install
Docker Compose on Ubuntu 18.04.
~/home/node_project/app.js
...
const port = 8080;
...
app.listen(port, function () {
console.log('Example app listening on port
8080!');
});
Let’s redefine the port constant to allow for dynamic assignment at
runtime using the process.env object. Make the following changes to
the constant definition and listen function:
~/home/node_project/app.js
...
const port = process.env.PORT || 8080;
...
app.listen(port, function () {
console.log(`Example app listening on
${port}!`);
});
Our new constant definition assigns port dynamically using the value
passed in at runtime or 8080. Similarly, we’ve rewritten the listen
function to use a template literal, which will interpolate the port value
when listening for connections. Because we will be mapping our ports
elsewhere, these revisions will prevent our having to continuously revise
this file as our environment changes.
When you are finished editing, save and close the file.
Next, we will modify our database connection information to remove
any configuration credentials. Open the db.js file, which contains this
information:
nano db.js
Currently, the file does the following things: - Imports Mongoose, the
Object Document Mapper (ODM) that we’re using to create schemas and
models for our application data. - Sets the database credentials as
constants, including the username and password. - Connects to the
database using the mongoose.connect method.
For more information about the file, please see Step 3 of How To
Integrate MongoDB with Your Node Application.
Our first step in modifying the file will be redefining the constants that
include sensitive information. Currently, these constants look like this:
~/node_project/db.js
...
const MONGO_USERNAME = 'sammy';
const MONGO_PASSWORD = 'your_password';
const MONGO_HOSTNAME = '127.0.0.1';
const MONGO_PORT = '27017';
const MONGO_DB = 'sharkinfo';
...
Instead of hardcoding this information, you can use the process.env
object to capture the runtime values for these constants. Modify the block
to look like this:
~/node_project/db.js
...
const {
MONGO_USERNAME,
MONGO_PASSWORD,
MONGO_HOSTNAME,
MONGO_PORT,
MONGO_DB
} = process.env;
...
Save and close the file when you are finished editing.
At this point, you have modified db.js to work with your application’s
environment variables, but you still need a way to pass these variables to
your application. Let’s create an .env file with values that you can pass
to your application at runtime.
Open the file:
nano .env
This file will include the information that you removed from db.js:
the username and password for your application’s database, as well as the
port setting and database name. Remember to update the username,
password, and database name listed here with your own information:
~/node_project/.env
MONGO_USERNAME=sammy
MONGO_PASSWORD=your_password
MONGO_PORT=27017
MONGO_DB=sharkinfo
Note that we have removed the host setting that originally appeared in
db.js. We will now define our host at the level of the Docker Compose
file, along with other information about our services and containers.
Save and close this file when you are finished editing.
Because your .env file contains sensitive information, you will want to
ensure that it is included in your project’s .dockerignore and
.gitignore files so that it does not copy to your version control or
containers.
Open your .dockerignore file:
nano .dockerignore
Add the following line to the bottom of the file:
~/node_project/.dockerignore
...
.gitignore
.env
Save and close the file when you are finished editing.
The .gitignore file in this repository already includes .env, but
feel free to check that it is there:
nano .gitignore
~~/node_project/.gitignore
...
.env
...
At this point, you have successfully extracted sensitive information
from your project code and taken measures to control how and where this
information gets copied. Now you can add more robustness to your
database connection code to optimize it for a containerized workflow.
~/node_project/db.js
...
const {
MONGO_USERNAME,
MONGO_PASSWORD,
MONGO_HOSTNAME,
MONGO_PORT,
MONGO_DB
} = process.env;
const url =
`mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${M
ONGO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}?
authSource=admin`;
~/node_project/db.js
...
const {
MONGO_USERNAME,
MONGO_PASSWORD,
MONGO_HOSTNAME,
MONGO_PORT,
MONGO_DB
} = process.env;
const options = {
useNewUrlParser: true,
reconnectTries: Number.MAX_VALUE,
reconnectInterval: 500,
connectTimeoutMS: 10000,
};
...
The reconnectTries option tells Mongoose to continue trying to
connect indefinitely, while reconnectInterval defines the period
between connection attempts in milliseconds. connectTimeoutMS
defines 10 seconds as the period that the Mongo driver will wait before
failing the connection attempt.
We can now use the new options constant in the Mongoose connect
method to fine tune our Mongoose connection settings. We will also add a
promise to handle potential connection errors.
Currently, the Mongoose connect method looks like this:
~/node_project/db.js
...
mongoose.connect(url, {useNewUrlParser: true});
Delete the existing connect method and replace it with the following
code, which includes the options constant and a promise:
~/node_project/db.js
...
mongoose.connect(url, options).then( function() {
console.log('MongoDB is connected');
})
.catch( function(err) {
console.log(err);
});
In the case of a successful connection, our function logs an appropriate
message; otherwise it will catch and log the error, allowing us to
troubleshoot.
The finished file will look like this:
~/node_project/db.js
const mongoose = require('mongoose');
const {
MONGO_USERNAME,
MONGO_PASSWORD,
MONGO_HOSTNAME,
MONGO_PORT,
MONGO_DB
} = process.env;
const options = {
useNewUrlParser: true,
reconnectTries: Number.MAX_VALUE,
reconnectInterval: 500,
connectTimeoutMS: 10000,
};
const url =
`mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${M
ONGO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}?
authSource=admin`;
~/node_project/app/wait-for.sh
#!/bin/sh
# original script:
https://github.com/eficode/wait-
for/blob/master/wait-for
TIMEOUT=15
QUIET=0
echoerr() {
if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*"
1>&2; fi
}
usage() {
exitcode="$1"
cat << USAGE >&2
Usage:
$cmdname host:port [-t timeout] [-- command
args]
-q | --quiet Do not
output any status messages
-t TIMEOUT | --timeout=timeout Timeout in
seconds, zero for no timeout
-- COMMAND ARGS Execute
command with args after the test finishes
USAGE
exit "$exitcode"
}
wait_for() {
for i in `seq $TIMEOUT` ; do
nc -z "$HOST" "$PORT" > /dev/null 2>&1
result=$?
if [ $result -eq 0 ] ; then
if [ $# -gt 0 ] ; then
exec "$@"
fi
exit 0
fi
sleep 1
done
echo "Operation timed out" >&2
exit 1
}
while [ $# -gt 0 ]
do
case "$1" in
*:* )
HOST=$(printf "%s\n" "$1"| cut -d : -f 1)
PORT=$(printf "%s\n" "$1"| cut -d : -f 2)
shift 1
;;
-q | --quiet)
QUIET=1
shift 1
;;
-t)
TIMEOUT="$2"
if [ "$TIMEOUT" = "" ]; then break; fi
shift 2
;;
--timeout=*)
TIMEOUT="${1#*=}"
shift 1
;;
--)
shift
break
;;
--help)
usage 0
;;
*)
echoerr "Unknown argument: $1"
usage 1
;;
esac
done
wait_for "$@"
Save and close the file when you are finished adding the code.
Make the script executable:
chmod +x wait-for.sh
Next, open the docker-compose.yml file:
nano docker-compose.yml
First, define the nodejs application service by adding the following
code to the file:
~/node_project/docker-compose.yml
version: '3'
services:
nodejs:
build:
context: .
dockerfile: Dockerfile
image: nodejs
container_name: nodejs
restart: unless-stopped
env_file: .env
environment:
- MONGO_USERNAME=$MONGO_USERNAME
- MONGO_PASSWORD=$MONGO_PASSWORD
- MONGO_HOSTNAME=db
- MONGO_PORT=$MONGO_PORT
- MONGO_DB=$MONGO_DB
ports:
- "80:8080"
volumes:
- .:/home/node/app
- node_modules:/home/node/app/node_modules
networks:
- app-network
command: ./wait-for.sh db:27017 --
/home/node/app/node_modules/.bin/nodemon app.js
The nodejs service definition includes the following options: -
build: This defines the configuration options, including the context
and dockerfile, that will be applied when Compose builds the
application image. If you wanted to use an existing image from a registry
like Docker Hub, you could use the image instruction instead, with
information about your username, repository, and image tag. - context:
This defines the build context for the image build — in this case, the
current project directory. - dockerfile: This specifies the
Dockerfile in your current project directory as the file Compose will
use to build the application image. For more information about this file,
please see How To Build a Node.js Application with Docker. - image,
container_name: These apply names to the image and container. -
restart: This defines the restart policy. The default is no, but we have
set the container to restart unless it is stopped. - env_file: This tells
Compose that we would like to add environment variables from a file
called .env, located in our build context. - environment: Using this
option allows you to add the Mongo connection settings you defined in the
.env file. Note that we are not setting NODE_ENV to development,
since this is Express’s default behavior if NODE_ENV is not set. When
moving to production, you can set this to production to enable view
caching and less verbose error messages. Also note that we have specified
the db database container as the host, as discussed in Step 2. - ports:
This maps port 80 on the host to port 8080 on the container. - volumes:
We are including two types of mounts here: - The first is a bind mount that
mounts our application code on the host to the /home/node/app
directory on the container. This will facilitate rapid development, since
any changes you make to your host code will be populated immediately in
the container. - The second is a named volume, node_modules. When
Docker runs the npm install instruction listed in the application
Dockerfile, npm will create a new node_modules directory on the
container that includes the packages required to run the application. The
bind mount we just created will hide this newly created node_modules
directory, however. Since node_modules on the host is empty, the bind
will map an empty directory to the container, overriding the new
node_modules directory and preventing our application from starting.
The named node_modules volume solves this problem by persisting the
contents of the /home/node/app/node_modules directory and
mounting it to the container, hiding the bind.
**Keep the following points in mind when using
this approach**:
- Your bind will mount the contents of the
`node_modules` directory on the container to the
host and this directory will be owned by `root`,
since the named volume was created by Docker.
- If you have a pre-existing `node_modules`
directory on the host, it will override the
`node_modules` directory created on the container.
The setup that we're building in this tutorial
assumes that you do **not** have a pre-existing
`node_modules` directory and that you won't be
working with `npm` on your host. This is in
keeping with a [twelve-factor approach to
application development](https://12factor.net/),
which minimizes dependencies between execution
environments.
networks: This specifies that our application service will join the
app-network network, which we will define at the bottom on the
file.
command: This option lets you set the command that should be
executed when Compose runs the image. Note that this will override
the CMD instruction that we set in our application Dockerfile.
Here, we are running the application using the wait-for script,
which will poll the db service on port 27017 to test whether or not
the database service is ready. Once the readiness test succeeds, the
script will execute the command we have set,
/home/node/app/node_modules/.bin/nodemon app.js,
to start the application with nodemon. This will ensure that any
future changes we make to our code are reloaded without our having
to restart the application.
Next, create the db service by adding the following code below the
application service definition:
~/node_project/docker-compose.yml
...
db:
image: mongo:4.1.8-xenial
container_name: db
restart: unless-stopped
env_file: .env
environment:
- MONGO_INITDB_ROOT_USERNAME=$MONGO_USERNAME
- MONGO_INITDB_ROOT_PASSWORD=$MONGO_PASSWORD
volumes:
- dbdata:/data/db
networks:
- app-network
Some of the settings we defined for the nodejs service remain the
same, but we’ve also made the following changes to the image,
environment, and volumes definitions: - image: To create this
service, Compose will pull the 4.1.8-xenial Mongo image from
Docker Hub. We are pinning a particular version to avoid possible future
conflicts as the Mongo image changes. For more information about
version pinning, please see the Docker documentation on Dockerfile best
practices. - MONGO_INITDB_ROOT_USERNAME,
MONGO_INITDB_ROOT_PASSWORD: The mongo image makes these
environment variables available so that you can modify the initialization
of your database instance. MONGO_INITDB_ROOT_USERNAME and
MONGO_INITDB_ROOT_PASSWORD together create a root user in the
admin authentication database and ensure that authentication is enabled
when the container starts. We have set
MONGO_INITDB_ROOT_USERNAME and
MONGO_INITDB_ROOT_PASSWORD using the values from our .env
file, which we pass to the db service using the env_file option. Doing
this means that our sammy application user will be a root user on the
database instance, with access to all of the administrative and operational
privileges of that role. When working in production, you will want to
create a dedicated application user with appropriately scoped privileges.
Note: Keep in mind that these variables will not take effect if you start
the container with an existing data directory in place.
~/node_project/docker-compose.yml
...
networks:
app-network:
driver: bridge
volumes:
dbdata:
node_modules:
The user-defined bridge network app-network enables
communication between our containers since they are on the same Docker
daemon host. This streamlines traffic and communication within the
application, as it opens all ports between containers on the same bridge
network, while exposing no ports to the outside world. Thus, our db and
nodejs containers can communicate with each other, and we only need to
expose port 80 for front-end access to the application.
Our top-level volumes key defines the volumes dbdata and
node_modules. When Docker creates volumes, the contents of the
volume are stored in a part of the host filesystem,
/var/lib/docker/volumes/, that’s managed by Docker. The
contents of each volume are stored in a directory under
/var/lib/docker/volumes/ and get mounted to any container that
uses the volume. In this way, the shark information data that our users will
create will persist in the dbdata volume even if we remove and recreate
the db container.
The finished docker-compose.yml file will look like this:
~/node_project/docker-compose.yml
version: '3'
services:
nodejs:
build:
context: .
dockerfile: Dockerfile
image: nodejs
container_name: nodejs
restart: unless-stopped
env_file: .env
environment:
- MONGO_USERNAME=$MONGO_USERNAME
- MONGO_PASSWORD=$MONGO_PASSWORD
- MONGO_HOSTNAME=db
- MONGO_PORT=$MONGO_PORT
- MONGO_DB=$MONGO_DB
ports:
- "80:8080"
volumes:
- .:/home/node/app
- node_modules:/home/node/app/node_modules
networks:
- app-network
command: ./wait-for.sh db:27017 --
/home/node/app/node_modules/.bin/nodemon app.js
db:
image: mongo:4.1.8-xenial
container_name: db
restart: unless-stopped
env_file: .env
environment:
- MONGO_INITDB_ROOT_USERNAME=$MONGO_USERNAME
- MONGO_INITDB_ROOT_PASSWORD=$MONGO_PASSWORD
volumes:
- dbdata:/data/db
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
dbdata:
node_modules:
Save and close the file when you are finished editing.
With your service definitions in place, you are ready to start the
application.
Output
...
Creating db ... done
Creating nodejs ... done
You can also get more detailed information about the startup processes
by displaying the log output from the services:
docker-compose logs
You will see something like this if everything has started correctly:
Output
...
nodejs | [nodemon] starting `node app.js`
nodejs | Example app listening on 8080!
nodejs | MongoDB is connected
...
db | 2019-02-22T17:26:27.329+0000 I ACCESS
[conn2] Successfully authenticated as principal
sammy on admin
You can also check the status of your containers with docker-
compose ps:
docker-compose ps
You will see output indicating that your containers are running:
Output
Name Command State
Ports
--------------------------------------------------
--------------------
db docker-entrypoint.sh mongod Up
27017/tcp
nodejs ./wait-for.sh db:27017 -- ... Up
0.0.0.0:80->8080/tcp
With your services running, you can visit
http://your_server_ip in the browser. You will see a landing page
that looks like this:
Click on the Get Shark Info button. You will see a page with an entry
form where you can enter a shark name and a description of that shark’s
general character:
Shark Info Form
In the form, add a shark of your choosing. For the purpose of this
demonstration, we will add Megalodon Shark to the Shark Name field,
and Ancient to the Shark Character field:
Filled Shark Form
Click on the Submit button. You will see a page with this shark
information displayed back to you:
Shark Output
As a final step, we can test that the data you’ve just entered will persist
if you remove your database container.
Back at your terminal, type the following command to stop and remove
your containers and network:
docker-compose down
Note that we are not including the --volumes option; hence, our
dbdata volume is not removed.
The following output confirms that your containers and network have
been removed:
Output
Stopping nodejs ... done
Stopping db ... done
Removing nodejs ... done
Removing db ... done
Removing network node_project_app-network
Recreate the containers:
docker-compose up -d
Now head back to the shark information form:
Shark Info Form
Enter a new shark of your choosing. We’ll go with Whale Shark and
Large:
Conclusion
By following this tutorial, you have created a development setup for your
Node application using Docker containers. You’ve made your project more
modular and portable by extracting sensitive information and decoupling
your application’s state from your application code. You have also
configured a boilerplate docker-compose.yml file that you can revise
as your development needs and requirements change.
As you develop, you may be interested in learning more about designing
applications for containerized and Cloud Native workflows. Please see
Architecting Applications for Kubernetes and Modernizing Applications
for Kubernetes for more information on these topics.
To learn more about the code used in this tutorial, please see How To
Build a Node.js Application with Docker and How To Integrate MongoDB
with Your Node Application. For information about deploying a Node
application with an Nginx reverse proxy using containers, please see How
To Secure a Containerized Node.js Application with Nginx, Let’s Encrypt,
and Docker Compose.
How to Set Up DigitalOcean Kubernetes
Cluster Monitoring with Helm and
Prometheus Operator
Along with tracing and logging, monitoring and alerting are essential
components of a Kubernetes observability stack. Setting up monitoring for
your Kubernetes cluster allows you to track your resource usage and
analyze and debug application errors.
A monitoring system usually consists of a time-series database that
houses metric data and a visualization layer. In addition, an alerting layer
creates and manages alerts, handing them off to integrations and external
services as necessary. Finally, one or more components generate or expose
the metric data that will be stored, visualized, and processed for alerts by
this monitoring stack.
One popular monitoring solution is the open-source Prometheus,
Grafana, and Alertmanager stack:
Prerequisites
To follow this tutorial, you will need:
custom-values.yaml
# Define persistent storage for Prometheus (PVC)
prometheus:
prometheusSpec:
storageSpec:
volumeClaimTemplate:
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: do-block-storage
resources:
requests:
storage: 5Gi
Output
NAME: doks-cluster-monitoring
LAST DEPLOYED: Mon Apr 22 10:30:42 2019
NAMESPACE: monitoring
STATUS: DEPLOYED
RESOURCES:
==> v1/PersistentVolumeClaim
NAME STATUS VOLUME
CAPACITY ACCESS MODES STORAGECLASS AGE
doks-cluster-monitoring-grafana Pending do-
block-storage 10s
==> v1/ServiceAccount
NAME
SECRETS AGE
doks-cluster-monitoring-grafana
1 10s
doks-cluster-monitoring-kube-state-metrics
1 10s
. . .
==> v1beta1/ClusterRoleBinding
NAME
AGE
doks-cluster-monitoring-kube-state-metrics
9s
psp-doks-cluster-monitoring-prometheus-node-
exporter 9s
NOTES:
The Prometheus Operator has been installed. Check
its status by running:
kubectl --namespace monitoring get pods -l
"release=doks-cluster-monitoring"
Visit https://github.com/coreos/prometheus-
operator for instructions on how
to create & configure Alertmanager and Prometheus
instances using the Operator.
This indicates that Prometheus Operator, Prometheus, Grafana, and the
other components listed above have successfully been installed into your
DigitalOcean Kubernetes cluster.
Following the note in the helm install output, check the status of
the release’s Pods using kubectl get pods:
kubectl --namespace monitoring get pods -l
"release=doks-cluster-monitoring"
You should see the following:
Output
NAME
READY STATUS RESTARTS AGE
doks-cluster-monitoring-grafana-9d7f984c5-hxnw6
2/2 Running 0 3m36s
doks-cluster-monitoring-kube-state-metrics-
dd8557f6b-9rl7j 1/1 Running 0
3m36s
doks-cluster-monitoring-pr-operator-9c5b76d78-
9kj85 1/1 Running 0 3m36s
doks-cluster-monitoring-prometheus-node-exporter-
2qvxw 1/1 Running 0 3m36s
doks-cluster-monitoring-prometheus-node-exporter-
7brwv 1/1 Running 0 3m36s
doks-cluster-monitoring-prometheus-node-exporter-
jhdgz 1/1 Running 0 3m36s
This indicates that all the monitoring components are up and running,
and you can begin exploring Prometheus metrics using Grafana and its
preconfigured dashboards.
Output
NAME
TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
AGE
alertmanager-operated
ClusterIP None <none>
9093/TCP,6783/TCP 34m
doks-cluster-monitoring-grafana
ClusterIP 10.245.105.130 <none> 80/TCP
34m
doks-cluster-monitoring-kube-state-metrics
ClusterIP 10.245.140.151 <none>
8080/TCP 34m
doks-cluster-monitoring-pr-alertmanager
ClusterIP 10.245.197.254 <none>
9093/TCP 34m
doks-cluster-monitoring-pr-operator
ClusterIP 10.245.14.163 <none>
8080/TCP 34m
doks-cluster-monitoring-pr-prometheus
ClusterIP 10.245.201.173 <none>
9090/TCP 34m
doks-cluster-monitoring-prometheus-node-exporter
ClusterIP 10.245.72.218 <none>
30206/TCP 34m
prometheus-operated
ClusterIP None <none>
9090/TCP 34m
We are going to forward local port 8000 to port 80 of the doks-
cluster-monitoring-grafana Service, which will in turn forward
to port 3000 of a running Grafana Pod. These Service and Pod ports are
configured in the stable/grafana Helm chart values file:
kubectl port-forward -n monitoring svc/doks-
cluster-monitoring-grafana 8000:80
You should see the following output:
Output
Forwarding from 127.0.0.1:8000 -> 3000
Forwarding from [::1]:8000 -> 3000
This indicates that local port 8000 is being forwarded successfully to a
Grafana Pod.
Visit http://localhost:8000 in your web browser. You should
see the following Grafana login page:
In the left-hand navigation bar, select the Dashboards button, then click
on Manage:
Grafana Dashboard Tab
Output
NAME
TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
AGE
alertmanager-operated
ClusterIP None <none>
9093/TCP,6783/TCP 34m
doks-cluster-monitoring-grafana
ClusterIP 10.245.105.130 <none> 80/TCP
34m
doks-cluster-monitoring-kube-state-metrics
ClusterIP 10.245.140.151 <none>
8080/TCP 34m
doks-cluster-monitoring-pr-alertmanager
ClusterIP 10.245.197.254 <none>
9093/TCP 34m
doks-cluster-monitoring-pr-operator
ClusterIP 10.245.14.163 <none>
8080/TCP 34m
doks-cluster-monitoring-pr-prometheus
ClusterIP 10.245.201.173 <none>
9090/TCP 34m
doks-cluster-monitoring-prometheus-node-exporter
ClusterIP 10.245.72.218 <none>
30206/TCP 34m
prometheus-operated
ClusterIP None <none>
9090/TCP 34m
We are going to forward local port 9090 to port 9090 of the doks-
cluster-monitoring-pr-prometheus Service:
kubectl port-forward -n monitoring svc/doks-
cluster-monitoring-pr-prometheus 9090:9090
You should see the following output:
Output
Forwarding from 127.0.0.1:9090 -> 9090
Forwarding from [::1]:9090 -> 9090
This indicates that local port 9090 is being forwarded successfully to a
Prometheus Pod.
Visit http://localhost:9090 in your web browser. You should
see the following Prometheus Graph page:
Prometheus Graph Page
From here you can use PromQL, the Prometheus query language, to
select and aggregate time series metrics stored in its database. To learn
more about PromQL, consult Querying Prometheus from the official
Prometheus docs.
In the Expression field, type machine_cpu_cores and hit Execute.
You should see a list of time series with the metric
machine_cpu_cores that reports the number of CPU cores on a given
node. You can see which node generated the metric and which job scraped
the metric in the metric labels.
Finally, in the top navigation bar, click on Status and then Targets to see
the list of targets Prometheus has been configured to scrape. You should
see a list of targets corresponding to the list of monitoring endpoints
described at the beginning of Step 2.
To learn more about Promtheus and how to query your cluster metrics,
consult the official Prometheus docs.
We’ll follow a similar process to connect to AlertManager, which
manages Alerts generated by Prometheus. You can explore these Alerts by
clicking into Alerts in the Prometheus top navigation bar.
To connect to the Alertmanager Pods, we will once again use kubectl
port-forward to forward a local port. If you’re done exploring
Prometheus, you can close the port-forward tunnel by hitting CTRL-C.
Alternatively you can open a new shell and port-forward connection.
Begin by listing running Services in the monitoring namespace:
kubectl get svc -n monitoring
You should see the following Services:
Output
NAME
TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
AGE
alertmanager-operated
ClusterIP None <none>
9093/TCP,6783/TCP 34m
doks-cluster-monitoring-grafana
ClusterIP 10.245.105.130 <none> 80/TCP
34m
doks-cluster-monitoring-kube-state-metrics
ClusterIP 10.245.140.151 <none>
8080/TCP 34m
doks-cluster-monitoring-pr-alertmanager
ClusterIP 10.245.197.254 <none>
9093/TCP 34m
doks-cluster-monitoring-pr-operator
ClusterIP 10.245.14.163 <none>
8080/TCP 34m
doks-cluster-monitoring-pr-prometheus
ClusterIP 10.245.201.173 <none>
9090/TCP 34m
doks-cluster-monitoring-prometheus-node-exporter
ClusterIP 10.245.72.218 <none>
30206/TCP 34m
prometheus-operated
ClusterIP None <none>
9090/TCP 34m
We are going to forward local port 9093 to port 9093 of the doks-
cluster-monitoring-pr-alertmanager Service.
kubectl port-forward -n monitoring svc/doks-
cluster-monitoring-pr-alertmanager 9093:9093
You should see the following output:
Output
Forwarding from 127.0.0.1:9093 -> 9093
Forwarding from [::1]:9093 -> 9093
This indicates that local port 9093 is being forwarded successfully to
an Alertmanager Pod.
Visit http://localhost:9093 in your web browser. You should
see the following Alertmanager Alerts page:
Alertmanager Alerts Page
From here, you can explore firing alerts and optionally silencing them.
To learn more about Alertmanager, consult the official Alertmanager
documentation.
Conclusion
In this tutorial, you installed a Prometheus, Grafana, and Alertmanager
monitoring stack into your DigitalOcean Kubernetes cluster with a
standard set of dashboards, Prometheus rules, and alerts. Since this was
done using Helm, you can use helm upgrade, helm rollback, and
helm delete to upgrade, roll back, or delete the monitoring stack. To
learn more about these functions, consult How To Install Software on
Kubernetes Clusters with the Helm Package Manager.
The prometheus-operator chart helps you get cluster monitoring
up and running quickly using Helm. You may wish to build, deploy, and
configure Prometheus Operator manually. To do so, consult the
Prometheus Operator and kube-prometheus GitHub repos.
How To Set Up Laravel, Nginx, and
MySQL with Docker Compose
Prerequisites
Before you start, you will need:
One Ubuntu 18.04 server, and a non-root user with sudo privileges.
Follow the Initial Server Setup with Ubuntu 18.04 tutorial to set this
up.
Docker installed, following Steps 1 and 2 of How To Install and Use
Docker on Ubuntu 18.04.
Docker Compose installed, following Step 1 of How To Install
Docker Compose on Ubuntu 18.04.
~/laravel-app/docker-compose.yml
version: '3'
services:
#PHP Service
app:
build:
context: .
dockerfile: Dockerfile
image: digitalocean.com/php
container_name: app
restart: unless-stopped
tty: true
environment:
SERVICE_NAME: app
SERVICE_TAGS: dev
working_dir: /var/www
networks:
- app-network
#Nginx Service
webserver:
image: nginx:alpine
container_name: webserver
restart: unless-stopped
tty: true
ports:
- "80:80"
- "443:443"
networks:
- app-network
#MySQL Service
db:
image: mysql:5.7.22
container_name: db
restart: unless-stopped
tty: true
ports:
- "3306:3306"
environment:
MYSQL_DATABASE: laravel<^>
MYSQL_ROOT_PASSWORD: your_mysql_root_password
SERVICE_TAGS: dev
SERVICE_NAME: mysql
networks:
- app-network
app network
#Docker Networks
networks:
app-network:
driver: bridge
~/laravel-app/docker-compose.yml
...
#MySQL Service
db:
...
volumes:
- dbdata:/var/lib/mysql
networks:
- app-network
...
The named volume dbdata persists the contents of the
/var/lib/mysql folder present inside the container. This allows you to
stop and restart the db service without losing data.
At the bottom of the file, add the definition for the dbdata volume:
~/laravel-app/docker-compose.yml
...
#Volumes
volumes:
dbdata:
driver: local
With this definition in place, you will be able to use this volume across
services.
Next, add a bind mount to the db service for the MySQL configuration
files you will create in Step 7:
~/laravel-app/docker-compose.yml
...
#MySQL Service
db:
...
volumes:
- dbdata:/var/lib/mysql
- ./mysql/my.cnf:/etc/mysql/my.cnf
...
This bind mount binds ~/laravel-app/mysql/my.cnf to
/etc/mysql/my.cnf in the container.
Next, add bind mounts to the webserver service. There will be two:
one for your application code and another for the Nginx configuration
definition that you will create in Step 6:
~/laravel-app/docker-compose.yml
#Nginx Service
webserver:
...
volumes:
- ./:/var/www
- ./nginx/conf.d/:/etc/nginx/conf.d/
networks:
- app-network
The first bind mount binds the application code in the ~/laravel-
app directory to the /var/www directory inside the container. The
configuration file that you will add to ~/laravel-
app/nginx/conf.d/ will also be mounted to
/etc/nginx/conf.d/ in the container, allowing you to add or modify
the configuration directory’s contents as needed.
Finally, add the following bind mounts to the app service for the
application code and configuration files:
~/laravel-app/docker-compose.yml
#PHP Service
app:
...
volumes:
- ./:/var/www
-
./php/local.ini:/usr/local/etc/php/conf.d/local.in
i
networks:
- app-network
The app service is bind-mounting the ~/laravel-app folder, which
contains the application code, to the /var/www folder in the container.
This will speed up the development process, since any changes made to
your local application directory will be instantly reflected inside the
container. You are also binding your PHP configuration file,
~/laravel-app/php/local.ini, to
/usr/local/etc/php/conf.d/local.ini inside the container.
You will create the local PHP configuration file in Step 5.
Your docker-compose file will now look like this:
~/laravel-app/docker-compose.yml
version: '3'
services:
#PHP Service
app:
build:
context: .
dockerfile: Dockerfile
image: digitalocean.com/php
container_name: app
restart: unless-stopped
tty: true
environment:
SERVICE_NAME: app
SERVICE_TAGS: dev
working_dir: /var/www
volumes:
- ./:/var/www
- ./php/local.ini:/usr/local/etc/php/conf.d/l
networks:
networks:
- app-network
#Nginx Service
webserver:
image: nginx:alpine
container_name: webserver
restart: unless-stopped
tty: true
ports:
- "80:80"
- "443:443"
volumes:
- ./:/var/www
- ./nginx/conf.d/:/etc/nginx/conf.d/
networks:
- app-network
#MySQL Service
db:
image: mysql:5.7.22
container_name: db
restart: unless-stopped
tty: true
ports:
- "3306:3306"
environment:
environment:
MYSQL_DATABASE: laravel<^>
MYSQL_ROOT_PASSWORD: your_mysql_root_password
SERVICE_TAGS: dev
SERVICE_NAME: mysql
volumes:
- dbdata:/var/lib/mysql/
- ./mysql/my.cnf:/etc/mysql/my.cnf
networks:
- app-network
#Docker Networks
networks:
app-network:
driver: bridge
#Volumes
volumes:
dbdata:
driver: local
Save the file and exit your editor when you are finished making
changes.
With your docker-compose file written, you can now build the
custom image for your application.
~/laravel-app/php/Dockerfile
FROM php:7.2-fpm
# Install dependencies
RUN apt-get update && apt-get install -y \
build-essential \
mysql-client \
libpng-dev \
libjpeg62-turbo-dev \
libfreetype6-dev \
locales \
zip \
jpegoptim optipng pngquant gifsicle \
vim \
unzip \
git \
curl
# Clear cache
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
# Install extensions
RUN docker-php-ext-install pdo_mysql mbstring zip
exif pcntl
RUN docker-php-ext-configure gd --with-gd --with-
freetype-dir=/usr/include/ --with-jpeg-
dir=/usr/include/ --with-png-dir=/usr/include/
RUN docker-php-ext-install gd
# Install composer
RUN curl -sS https://getcomposer.org/installer |
php -- --install-dir=/usr/local/bin --
filename=composer
~/laravel-app/php/local.ini
upload_max_filesize=40M
post_max_size=40M
~/laravel-app/nginx/conf.d/app.conf
server {
listen 80;
index index.php index.html;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /var/www/public;
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass app:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME
$document_root$fastcgi_script_name;
fastcgi_param PATH_INFO
$fastcgi_path_info;
}
location / {
try_files $uri $uri/ /index.php?
$query_string;
gzip_static on;
}
}
The server block defines the configuration for the Nginx web server
with the following directives: - listen: This directive defines the port
on which the server will listen to incoming requests. - error_log and
access_log: These directives define the files for writing logs. - root:
This directive sets the root folder path, forming the complete path to any
requested file on the local file system.
In the php location block, the fastcgi_pass directive specifies that
the app service is listening on a TCP socket on port 9000. This makes the
PHP-FPM server listen over the network rather than on a Unix socket.
Though a Unix socket has a slight advantage in speed over a TCP socket, it
does not have a network protocol and thus skips the network stack. For
cases where hosts are located on one machine, a Unix socket may make
sense, but in cases where you have services running on different hosts, a
TCP socket offers the advantage of allowing you to connect to distributed
services. Because our app container is running on a different host from
our webserver container, a TCP socket makes the most sense for our
configuration.
Save the file and exit your editor when you are finished making
changes.
Thanks to the bind mount you created in Step 2, any changes you make
inside the nginx/conf.d/ folder will be directly reflected inside the
webserver container.
Next, let’s look at our MySQL settings.
~/laravel-app/mysql/my.cnf
[mysqld]
general_log = 1
general_log_file = /var/lib/mysql/general.log
Output
CONTAINER ID NAMES IMAGE
STATUS PORTS
c31b7b3251e0 db
mysql:5.7.22 Up 2 seconds
0.0.0.0:3306->3306/tcp
ed5a69704580 app
digitalocean.com/php Up 2 seconds
9000/tcp
5ce4ee31d7c0 webserver
nginx:alpine Up 2 seconds
0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
The CONTAINER ID in this output is a unique identifier for each
container, while NAMES lists the service name associated with each. You
can use both of these identifiers to access the containers. IMAGE defines
the image name for each container, while STATUS provides information
about the container’s state: whether it’s running, restarting, or stopped.
You can now modify the .env file on the app container to include
specific details about your setup.
Open the file using docker-compose exec, which allows you to
run specific commands in containers. In this case, you are opening the file
for editing:
docker-compose exec app nano .env
Find the block that specifies DB_CONNECTION and update it to reflect
the specifics of your setup. You will modify the following fields: -
DB_HOST will be your db database container. - DB_DATABASE will be
the laravel database. - DB_USERNAME will be the username you will
use for your database. In this case, we will use laraveluser. -
DB_PASSWORD will be the secure password you would like to use for this
user account.
/var/www/.env
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=laraveluser
DB_PASSWORD=your_laravel_db_password
Save your changes and exit your editor.
Next, set the application key for the Laravel application with the php
artisan key:generate command. This command will generate a
key and copy it to your .env file, ensuring that your user sessions and
encrypted data remain secure:
docker-compose exec app php artisan key:generate
You now have the environment settings required to run your application.
To cache these settings into a file, which will boost your application’s load
speed, run:
docker-compose exec app php artisan config:cache
Your configuration settings will be loaded into
/var/www/bootstrap/cache/config.php on the container.
As a final step, visit http://your_server_ip in the browser. You
will see the following home page for your Laravel application:
Laravel Home Page
Output
[environment second]
+--------------------+
| Database |
+--------------------+
| information_schema |
| laravel |
| mysql |
| performance_schema |
| sys |
+--------------------+
5 rows in set (0.00 sec)
Next, create the user account that will be allowed to access this
database. Our username will be laraveluser, though you can replace
this with another name if you’d prefer. Just be sure that your username and
password here match the details you set in your .env file in the previous
step:
[environment second]
GRANT ALL ON laravel.* TO 'laraveluser'@'%'
IDENTIFIED BY 'your_laravel_db_password';
Flush the privileges to notify the MySQL server of the changes:
[environment second]
FLUSH PRIVILEGES;
Exit MySQL:
[environment second]
EXIT;
Finally, exit the container:
[environment second]
exit
You have configured the user account for your Laravel application
database and are ready to migrate your data and work with the Tinker
console.
Output
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_000000_create_users_table
Migrating:
2014_10_12_100000_create_password_resets_table
Migrated:
2014_10_12_100000_create_password_resets_table
Once the migration is complete, you can run a query to check if you are
properly connected to the database using the tinker command:
docker-compose exec app php artisan tinker
Test the MySQL connection by getting the data you just migrated:
\DB::table('migrations')->get();
You will see output that looks like this:
Output
=> Illuminate\Support\Collection {#2856
all: [
{#2862
+"id": 1,
+"migration":
"2014_10_12_000000_create_users_table",
+"batch": 1,
},
{#2865
+"id": 2,
+"migration":
"2014_10_12_100000_create_password_resets_table",
+"batch": 1,
},
],
}
You can use tinker to interact with your databases and to experiment
with services and models.
With your Laravel application in place, you are ready for further
development and experimentation. ## Conclusion
You now have a LEMP stack application running on your server, which
you’ve tested by accessing the Laravel welcome page and creating MySQL
database migrations.
Key to the simplicity of this installation is Docker Compose, which
allows you to create a group of Docker containers, defined in a single file,
with a single command. If you would like to learn more about how to do
CI with Docker Compose, take a look at How To Configure a Continuous
Integration Testing Environment with Docker and Docker Compose on
Ubuntu 16.04. If you want to streamline your Laravel application
deployment process then How to Automatically Deploy Laravel
Applications with Deployer on Ubuntu 16.04 will be a relevant resource.
How To Migrate a Docker Compose
Workflow to Kubernetes
Prerequisites
Output
1.18.0 (06a2e56)
With kompose installed and ready to use, you can now clone the
Node.js project code that you will be translating to Kubernetes.
Output
REPOSITORY TAG
IMAGE ID CREATED SIZE
your_dockerhub_username/node-kubernetes latest
9c6f897e1fbc 3 seconds ago 90MB
node 10-alpine
94f3c8956482 12 days ago 71MB
Next, log in to the Docker Hub account you created in the prerequisites:
docker login -u your_dockerhub_username
When prompted, enter your Docker Hub account password. Logging in
this way will create a ~/.docker/config.json file in your user’s
home directory with your Docker Hub credentials.
Push the application image to Docker Hub with the docker push
command. Remember to replace your_dockerhub_username with
your own Docker Hub username:
docker push your_dockerhub_username/node-kubernetes
You now have an application image that you can pull to run your
application with Kubernetes. The next step will be to translate your
application service definitions to Kubernetes objects.
~/node_project/docker-compose.yaml
...
services:
nodejs:
build:
context: .
dockerfile: Dockerfile
image: nodejs
container_name: nodejs
restart: unless-stopped
env_file: .env
environment:
- MONGO_USERNAME=$MONGO_USERNAME
- MONGO_PASSWORD=$MONGO_PASSWORD
- MONGO_HOSTNAME=db
- MONGO_PORT=$MONGO_PORT
- MONGO_DB=$MONGO_DB
ports:
- "80:8080"
volumes:
- .:/home/node/app
- node_modules:/home/node/app/node_modules
networks:
- app-network
command: ./wait-for.sh db:27017 --
/home/node/app/node_modules/.bin/nodemon app.js
...
Make the following edits to your service definition: - Use your node-
kubernetes image instead of the local Dockerfile. - Change the
container restart policy from unless-stopped to always. -
Remove the volumes list and the command instruction.
The finished service definition will now look like this:
~/node_project/docker-compose.yaml
...
services:
nodejs:
image: your_dockerhub_username/node-kubernetes
container_name: nodejs
restart: always
env_file: .env
environment:
- MONGO_USERNAME=$MONGO_USERNAME
- MONGO_PASSWORD=$MONGO_PASSWORD
- MONGO_HOSTNAME=db
- MONGO_PORT=$MONGO_PORT
- MONGO_DB=$MONGO_DB
ports:
- "80:8080"
networks:
- app-network
...
Next, scroll down to the db service definition. Here, make the following
edits: - Change the restart policy for the service to always. - Remove
the .env file. Instead of using values from the .env file, we will pass the
values for our MONGO_INITDB_ROOT_USERNAME and
MONGO_INITDB_ROOT_PASSWORD to the database container using the
Secret we will create in Step 4.
The db service definition will now look like this:
~/node_project/docker-compose.yaml
...
db:
image: mongo:4.1.8-xenial
container_name: db
restart: always
environment:
- MONGO_INITDB_ROOT_USERNAME=$MONGO_USERNAME
- MONGO_INITDB_ROOT_PASSWORD=$MONGO_PASSWORD
volumes:
- dbdata:/data/db
networks:
- app-network
...
Finally, at the bottom of the file, remove the node_modules volumes
from the top-level volumes key. The key will now look like this:
~/node_project/docker-compose.yaml
...
volumes:
dbdata:
Save and close the file when you are finished editing.
Before translating our service definitions, we will need to write the
.env file that kompose will use to create the ConfigMap with our non-
sensitive information. Please see Step 2 of Containerizing a Node.js
Application for Development With Docker Compose for a longer
explanation of this file.
In that tutorial, we added .env to our .gitignore file to ensure that
it would not copy to version control. This means that it did not copy over
when we cloned the node-mongo-docker-dev repository in Step 2 of this
tutorial. We will therefore need to recreate it now.
Create the file:
nano .env
kompose will use this file to create a ConfigMap for our application.
However, instead of assigning all of the variables from the nodejs service
definition in our Compose file, we will add only the MONGO_DB database
name and the MONGO_PORT. We will assign the database username and
password separately when we manually create a Secret object in Step 4.
Add the following port and database name information to the .env file.
Feel free to rename your database if you would like:
~/node_project/.env
MONGO_PORT=27017
MONGO_DB=sharkinfo
Save and close the file when you are finished editing.
You are now ready to create the files with your object specs. kompose
offers multiple options for translating your resources. You can: - Create
yaml files based on the service definitions in your docker-
compose.yaml file with kompose convert. - Create Kubernetes
objects directly with kompose up. - Create a Helm chart with kompose
convert -c.
For now, we will convert our service definitions to yaml files and then
add to and revise the files kompose creates.
Convert your service definitions to yaml files with the following
command:
kompose convert
You can also name specific or multiple Compose files using the -f flag.
After you run this command, kompose will output information about the
files it has created:
Output
INFO Kubernetes file "nodejs-service.yaml" created
INFO Kubernetes file "db-deployment.yaml" created
INFO Kubernetes file "dbdata-
persistentvolumeclaim.yaml" created
INFO Kubernetes file "nodejs-deployment.yaml"
created
INFO Kubernetes file "nodejs-env-configmap.yaml"
created
These include yaml files with specs for the Node application Service,
Deployment, and ConfigMap, as well as for the dbdata
PersistentVolumeClaim and MongoDB database Deployment.
These files are a good starting point, but in order for our application’s
functionality to match the setup described in Containerizing a Node.js
Application for Development With Docker Compose we will need to make
a few additions and changes to the files kompose has generated.
~/node_project/secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: mongo-secret
data:
MONGO_USERNAME: your_encoded_username
MONGO_PASSWORD: your_encoded_password
We have named the Secret object mongo-secret, but you are free to
name it anything you would like.
Save and close this file when you are finished editing. As you did with
your .env file, be sure to add secret.yaml to your .gitignore file
to keep it out of version control.
With secret.yaml written, our next step will be to ensure that our
application and database Pods both use the values we added to the file.
Let’s start by adding references to the Secret to our application
Deployment.
Open the file called nodejs-deployment.yaml:
nano nodejs-deployment.yaml
The file’s container specifications include the following environment
variables defined under the env key:
~/node_project/nodejs-deployment.yaml
apiVersion: extensions/v1beta1
kind: Deployment
...
spec:
containers:
- env:
- name: MONGO_DB
valueFrom:
configMapKeyRef:
key: MONGO_DB
name: nodejs-env
- name: MONGO_HOSTNAME
value: db
- name: MONGO_PASSWORD
- name: MONGO_PORT
valueFrom:
configMapKeyRef:
key: MONGO_PORT
name: nodejs-env
- name: MONGO_USERNAME
We will need to add references to our Secret to the MONGO_USERNAME
and MONGO_PASSWORD variables listed here, so that our application will
have access to those values. Instead of including a configMapKeyRef
key to point to our nodejs-env ConfigMap, as is the case with the values
for MONGO_DB and MONGO_PORT, we’ll include a secretKeyRef key
to point to the values in our mongo-secret secret.
Add the following Secret references to the MONGO_USERNAME and
MONGO_PASSWORD variables:
~/node_project/nodejs-deployment.yaml
apiVersion: extensions/v1beta1
kind: Deployment
...
spec:
containers:
- env:
- name: MONGO_DB
valueFrom:
configMapKeyRef:
key: MONGO_DB
name: nodejs-env
- name: MONGO_HOSTNAME
value: db
- name: MONGO_PASSWORD
valueFrom:
secretKeyRef:
name: mongo-secret
key: MONGO_PASSWORD
- name: MONGO_PORT
valueFrom:
configMapKeyRef:
key: MONGO_PORT
name: nodejs-env
- name: MONGO_USERNAME
valueFrom:
secretKeyRef:
name: mongo-secret
key: MONGO_USERNAME
Save and close the file when you are finished editing.
Next, we’ll add the same values to the db-deployment.yaml file.
Open the file for editing:
nano db-deployment.yaml
In this file, we will add references to our Secret for following variable
keys: MONGO_INITDB_ROOT_USERNAME and
MONGO_INITDB_ROOT_PASSWORD. The mongo image makes these
variables available so that you can modify the initialization of your
database instance. MONGO_INITDB_ROOT_USERNAME and
MONGO_INITDB_ROOT_PASSWORD together create a root user in the
admin authentication database and ensure that authentication is enabled
when the database container starts.
Using the values we set in our Secret ensures that we will have an
application user with root privileges on the database instance, with access
to all of the administrative and operational privileges of that role. When
working in production, you will want to create a dedicated application user
with appropriately scoped privileges.
Under the MONGO_INITDB_ROOT_USERNAME and
MONGO_INITDB_ROOT_PASSWORD variables, add references to the
Secret values:
~/node_project/db-deployment.yaml
apiVersion: extensions/v1beta1
kind: Deployment
...
spec:
containers:
- env:
- name: MONGO_INITDB_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mongo-secret
key: MONGO_PASSWORD
- name: MONGO_INITDB_ROOT_USERNAME
valueFrom:
secretKeyRef:
name: mongo-secret
key: MONGO_USERNAME
image: mongo:4.1.8-xenial
...
Save and close the file when you are finished editing.
With your Secret in place, you can move on to creating your database
Service and ensuring that your application container only attempts to
connect to the database once it is fully set up and initialized.
~/node_project/db-service.yaml
apiVersion: v1
kind: Service
metadata:
annotations:
kompose.cmd: kompose convert
kompose.version: 1.18.0 (06a2e56)
creationTimestamp: null
labels:
io.kompose.service: db
name: db
spec:
ports:
- port: 27017
targetPort: 27017
selector:
io.kompose.service: db
status:
loadBalancer: {}
The selector that we have included here will match this Service
object with our database Pods, which have been defined with the label
io.kompose.service: db by kompose in the db-
deployment.yaml file. We’ve also named this service db.
Save and close the file when you are finished editing.
Next, let’s add an Init Container field to the containers array in
nodejs-deployment.yaml. This will create an Init Container that we
can use to delay our application container from starting until the db
Service has been created with a Pod that is reachable. This is one of the
possible uses for Init Containers; to learn more about other use cases,
please see the official documentation.
Open the nodejs-deployment.yaml file:
nano nodejs-deployment.yaml
Within the Pod spec and alongside the containers array, we are going
to add an initContainers field with a container that will poll the db
Service.
Add the following code below the ports and resources fields and
above the restartPolicy in the nodejs containers array:
~/node_project/nodejs-deployment.yaml
apiVersion: extensions/v1beta1
kind: Deployment
...
spec:
containers:
...
name: nodejs
ports:
- containerPort: 8080
resources: {}
initContainers:
- name: init-db
image: busybox
command: ['sh', '-c', 'until nc -z
db:27017; do echo waiting for db; sleep 2; done;']
restartPolicy: Always
...
This Init Container uses the BusyBox image, a lightweight image that
includes many UNIX utilities. In this case, we’ll use the netcat utility to
poll whether or not the Pod associated with the db Service is accepting
TCP connections on port 27017.
This container command replicates the functionality of the wait-for
script that we removed from our docker-compose.yaml file in Step 3.
For a longer discussion of how and why our application used the wait-
for script when working with Compose, please see Step 4 of
Containerizing a Node.js Application for Development with Docker
Compose.
Init Containers run to completion; in our case, this means that our Node
application container will not start until the database container is running
and accepting connections on port 27017. The db Service definition
allows us to guarantee this functionality regardless of the exact location of
the database container, which is mutable.
Save and close the file when you are finished editing.
With your database Service created and your Init Container in place to
control the startup order of your containers, you can move on to checking
the storage requirements in your PersistentVolumeClaim and exposing your
application service using a LoadBalancer.
Output
NAME PROVISIONER
AGE
do-block-storage (default)
dobs.csi.digitalocean.com 76m
If you are not working with a DigitalOcean cluster, you will need to
create a StorageClass and configure a provisioner of your choice. For
details about how to do this, please see the official documentation.
When kompose created dbdata-persistentvolumeclaim.yaml,
it set the storage resource to a size that does not meet the minimum
size requirements of our provisioner. We will therefore need to modify
our PersistentVolumeClaim to use the minimum viable DigitalOcean Block
Storage unit: 1GB. Please feel free to modify this to meet your storage
requirements.
Open dbdata-persistentvolumeclaim.yaml:
nano dbdata-persistentvolumeclaim.yaml
Replace the storage value with 1Gi:
~/node_project/dbdata-persistentvolumeclaim.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
creationTimestamp: null
labels:
io.kompose.service: dbdata
name: dbdata
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
status: {}
Also note the accessMode: ReadWriteOnce means that the volume
provisioned as a result of this Claim will be read-write only by a single
node. Please see the documentation for more information about different
access modes.
Save and close the file when you are finished.
Next, open nodejs-service.yaml:
nano nodejs-service.yaml
We are going to expose this Service externally using a DigitalOcean
Load Balancer. If you are not using a DigitalOcean cluster, please consult
the relevant documentation from your cloud provider for information about
their load balancers. Alternatively, you can follow the official Kubernetes
documentation on setting up a highly available cluster with kubeadm, but
in this case you will not be able to use PersistentVolumeClaims to
provision storage.
Within the Service spec, specify LoadBalancer as the Service type:
~/node_project/nodejs-service.yaml
apiVersion: v1
kind: Service
...
spec:
type: LoadBalancer
ports:
...
When we create the nodejs Service, a load balancer will be
automatically created, providing us with an external IP where we can
access our application.
Save and close the file when you are finished editing.
With all of our files in place, we are ready to start and test the
application.
Output
service/nodejs created
deployment.extensions/nodejs created
configmap/nodejs-env created
service/db created
deployment.extensions/db created
persistentvolumeclaim/dbdata created
secret/mongo-secret created
To check that your Pods are running, type:
kubectl get pods
You don’t need to specify a Namespace here, since we have created our
objects in the default Namespace. If you are working with multiple
Namespaces, be sure to include the -n flag when running this command,
along with the name of your Namespace.
You will see the following output while your db container is starting and
your application Init Container is running:
Output
NAME READY STATUS
RESTARTS AGE
db-679d658576-kfpsl 0/1 ContainerCreating
0 10s
nodejs-6b9585dc8b-pnsws 0/1 Init:0/1
0 10s
Once that container has run and your application and database containers
have started, you will see this output:
Output
NAME READY STATUS
RESTARTS AGE
db-679d658576-kfpsl 1/1 Running 0
54s
nodejs-6b9585dc8b-pnsws 1/1 Running 0
54s
The Running STATUS indicates that your Pods are bound to nodes and
that the containers associated with those Pods are running. READY
indicates how many containers in a Pod are running. For more information,
please consult the documentation on Pod lifecycles.
Note: If you see unexpected phases in the STATUS column, remember
that you can troubleshoot your Pods with the following commands:
kubectl describe pods your_pod
kubectl logs your_pod
With your containers running, you can now access the application. To get
the IP for the LoadBalancer, type:
kubectl get svc
You will see the following output:
Output
NAME TYPE CLUSTER-IP
EXTERNAL-IP PORT(S) AGE
db ClusterIP 10.245.189.250 <none>
27017/TCP 93s
kubernetes ClusterIP 10.245.0.1 <none>
443/TCP 25m12s
nodejs LoadBalancer 10.245.15.56
your_lb_ip 80:30729/TCP 93s
The EXTERNAL_IP associated with the nodejs service is the IP
address where you can access the application. If you see a <pending>
status in the EXTERNAL_IP column, this means that your load balancer is
still being created.
Once you see an IP in that column, navigate to it in your browser:
http://your_lb_ip.
You should see the following landing page:
Click on the Get Shark Info button. You will see a page with an entry
form where you can enter a shark name and a description of that shark’s
general character:
Shark Info Form
Click on the Submit button. You will see a page with this shark
information displayed back to you:
Shark Output
You now have a single instance setup of a Node.js application with a
MongoDB database running on a Kubernetes cluster.
Conclusion
The files you have created in this tutorial are a good starting point to build
from as you move toward production. As you develop your application, you
can work on implementing the following: - Centralized logging and
monitoring. Please see the relevant discussion in Modernizing Applications
for Kubernetes for a general overview. You can also look at How To Set Up
an Elasticsearch, Fluentd and Kibana (EFK) Logging Stack on Kubernetes
to learn how to set up a logging stack with Elasticsearch, Fluentd, and
Kibana. Also check out An Introduction to Service Meshes for information
about how service meshes like Istio implement this functionality. - Ingress
Resources to route traffic to your cluster. This is a good alternative to a
LoadBalancer in cases where you are running multiple Services, which
each require their own LoadBalancer, or where you would like to
implement application-level routing strategies (A/B & canary tests, for
example). For more information, check out How to Set Up an Nginx
Ingress with Cert-Manager on DigitalOcean Kubernetes and the related
discussion of routing in the service mesh context in An Introduction to
Service Meshes. - Backup strategies for your Kubernetes objects. For
guidance on implementing backups with Velero (formerly Heptio Ark) with
DigitalOcean’s Kubernetes product, please see How To Back Up and
Restore a Kubernetes Cluster on DigitalOcean Using Heptio Ark.
Building Optimized Containers for
Kubernetes
Docker creates a new image layer each time it executes a RUN, COPY, or
ADD instruction. If you build the image again, the build engine will check
each instruction to see if it has an image layer cached for the operation. If
it finds a match in the cache, it uses the existing image layer rather than
executing the instruction again and rebuilding the layer.
This process can significantly shorten build times, but it is important to
understand the mechanism used to avoid potential problems. For file
copying instructions like COPY and ADD, Docker compares the checksums
of the files to see if the operation needs to be performed again. For RUN
instructions, Docker checks to see if it has an existing image layer cached
for that particular command string.
While it might not be immediately obvious, this behavior can cause
unexpected results if you are not careful. A common example of this is
updating the local package index and installing packages in two separate
steps. We will be using Ubuntu for this example, but the basic premise
applies equally well to base images for other distributions:
Containerizing by Function
In Kubernetes, pods are the smallest unit that can be directly managed by
the control plane. Pods consist of one or more containers along with
additional configuration data to tell the platform how those components
should be run. The containers within a pod are always scheduled on the
same worker node in the cluster and the system automatically restarts
failed containers. The pod abstraction is very useful, but it introduces
another layer of decisions about how to bundle together the components of
your applications.
Like container images, pods also become less flexible when too much
functionality is bundled into a single entity. Pods themselves can be scaled
using other abstractions, but the containers within cannot be managed or
scaled independently. So, to continue using our previous example, the
separate Nginx and Gunicorn containers should probably not be bundled
together into a single pod so that they can be controlled and deployed
separately.
However, there are scenarios where it does make sense to combine
functionally different containers as a unit. In general, these can be
categorized as situations where an additional container supports or
enhances the core functionality of the main container or helps it adapt to
its deployment environment. Some common patterns are:
As you might have noticed, each of these patterns support the strategy
of building standard, generic primary container images that can then be
deployed in a variety contexts and configurations. The secondary
containers help bridge the gap between the primary container and the
specific deployment environment being used. Some sidecar containers can
also be reused to adapt multiple primary containers to the same
environmental conditions. These patterns benefit from the shared
filesystem and networking namespace provided by the pod abstraction
while still allowing independent development and flexible deployment of
standardized containers.
Conclusion
In this guide, we’ve covered some important considerations to keep in
mind when running containerized applications in Kubernetes. To reiterate,
some of the suggestions we went over were:
Prerequisites
To complete this tutorial, you will need: - A Kubernetes 1.10+ cluster with
role-based access control (RBAC) enabled. This setup will use a
DigitalOcean Kubernetes cluster, but you are free to create a cluster using
another method. - The kubectl command-line tool installed on your local
machine or development server and configured to connect to your cluster.
You can read more about installing kubectl in the official documentation.
- Helm installed on your local machine or development server and Tiller
installed on your cluster, following the directions outlined in Steps 1 and 2 of
How To Install Software on Kubernetes Clusters with the Helm Package
Manager. - Docker installed on your local machine or development server. If
you are working with Ubuntu 18.04, follow Steps 1 and 2 of How To Install
and Use Docker on Ubuntu 18.04; otherwise, follow the official
documentation for information about installing on other operating systems.
Be sure to add your non-root user to the docker group, as described in Step
2 of the linked tutorial. - A Docker Hub account. For an overview of how to
set this up, refer to this introduction to Docker Hub.
~/node_project/db.js
...
const {
MONGO_USERNAME,
MONGO_PASSWORD,
MONGO_HOSTNAME,
MONGO_PORT,
MONGO_DB
} = process.env;
...
const url =
`mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MON
GO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}?
authSource=admin`;
...
In keeping with a 12FA approach, we do not want to hard code the
hostnames of our replica instances or our replica set name into this URI
string. The existing MONGO_HOSTNAME constant can be expanded to include
multiple hostnames — the members of our replica set — so we will leave
that in place. We will need to add a replica set constant to the options
section of the URI string, however.
Add MONGO_REPLICASET to both the URI constant object and the
connection string:
~/node_project/db.js
...
const {
MONGO_USERNAME,
MONGO_PASSWORD,
MONGO_HOSTNAME,
MONGO_PORT,
MONGO_DB,
MONGO_REPLICASET
} = process.env;
...
const url =
`mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MON
GO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}?
replicaSet=${MONGO_REPLICASET}&authSource=admin`;
...
Using the replicaSet option in the options section of the URI allows
us to pass in the name of the replica set, which, along with the hostnames
defined in the MONGO_HOSTNAME constant, will allow us to connect to the
set members.
Save and close the file when you are finished editing.
With your database connection information modified to work with replica
sets, you can now package your application, build the image with the
docker build command, and push it to Docker Hub.
Build the image with docker build and the -t flag, which allows you
to tag the image with a memorable name. In this case, tag the image with
your Docker Hub username and name it node-replicas or a name of
your own choosing:
docker build -t your_dockerhub_username/node-
replicas .
The . in the command specifies that the build context is the current
directory.
It will take a minute or two to build the image. Once it is complete, check
your images:
docker images
You will see the following output:
Output
REPOSITORY TAG
IMAGE ID CREATED SIZE
your_dockerhub_username/node-replicas latest
56a69b4bc882 7 seconds ago 90.1MB
node 10-alpine
aa57b0242b33 6 days ago 71MB
Next, log in to the Docker Hub account you created in the prerequisites:
docker login -u your_dockerhub_username
When prompted, enter your Docker Hub account password. Logging in
this way will create a ~/.docker/config.json file in your non-root
user’s home directory with your Docker Hub credentials.
Push the application image to Docker Hub with the docker push
command. Remember to replace your_dockerhub_username with your
own Docker Hub username:
docker push your_dockerhub_username/node-replicas
You now have an application image that you can pull to run your
replicated application with Kubernetes. The next step will be to configure
specific parameters to use with the MongoDB Helm chart.
Output
secret/keyfilesecret created
Remove key.txt:
rm key.txt
Alternatively, if you would like to save the file, be sure restrict its
permissions and add it to your .gitignore file to keep it out of version
control.
Next, create the Secret for your MongoDB admin user. The first step will
be to convert your desired username and password to base64.
Convert your database username:
echo -n 'your_database_username' | base64
Note down the value you see in the output.
Next, convert your password:
echo -n 'your_database_password' | base64
Take note of the value in the output here as well.
Open a file for the Secret:
nano secret.yaml
Note: Kubernetes objects are typically defined using YAML, which
strictly forbids tabs and requires two spaces for indentation. If you would
like to check the formatting of any of your YAML files, you can use a linter
or test the validity of your syntax using kubectl create with the --
dry-run and --validate flags:
kubectl create -f your_yaml_file.yaml --dry-run --
validate=true
In general, it is a good idea to validate your syntax before creating
resources with kubectl.
Add the following code to the file to create a Secret that will define a
user and password with the encoded values you just created. Be sure to
replace the dummy values here with your own encoded username and
password:
~/node_project/secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: mongo-secret
data:
user: your_encoded_username
password: your_encoded_password
Here, we’re using the key names that the mongodb-replicaset chart
expects: user and password. We have named the Secret object mongo-
secret, but you are free to name it anything you would like.
Save and close the file when you are finished editing.
Create the Secret object with the following command:
kubectl create -f secret.yaml
You will see the following output:
Output
secret/mongo-secret created
Again, you can either remove secret.yaml or restrict its permissions
and add it to your .gitignore file.
With your Secret objects created, you can move on to specifying the
parameter values you will use with the mongodb-replicaset chart and
creating the MongoDB deployment.
Output
NAME PROVISIONER
AGE
do-block-storage (default)
dobs.csi.digitalocean.com 21m
If you are not working with a DigitalOcean cluster, you will need to create
a StorageClass and configure a provisioner of your choice. For details
about how to do this, please see the official documentation.
Now that you have ensured that you have a StorageClass configured, open
mongodb-values.yaml for editing:
nano mongodb-values.yaml
You will set values in this file that will do the following: - Enable
authorization. - Reference your keyfilesecret and mongo-secret
objects. - Specify 1Gi for your PersistentVolumes. - Set your replica set
name to db. - Specify 3 replicas for the set. - Pin the mongo image to the
latest version at the time of writing: 4.1.9.
Paste the following code into the file:
~/node_project/mongodb-values.yaml
replicas: 3
port: 27017
replicaSetName: db
podDisruptionBudget: {}
auth:
enabled: true
existingKeySecret: keyfilesecret
existingAdminSecret: mongo-secret
imagePullSecrets: []
installImage:
repository: unguiculus/mongodb-install
tag: 0.7
pullPolicy: Always
copyConfigImage:
repository: busybox
tag: 1.29.3
pullPolicy: Always
image:
repository: mongo
tag: 4.1.9
pullPolicy: Always
extraVars: {}
metrics:
enabled: false
image:
repository: ssalaues/mongodb-exporter
tag: 0.6.1
pullPolicy: IfNotPresent
port: 9216
path: /metrics
socketTimeout: 3s
syncTimeout: 1m
prometheusServiceDiscovery: true
resources: {}
podAnnotations: {}
securityContext:
enabled: true
runAsUser: 999
fsGroup: 999
runAsNonRoot: true
init:
resources: {}
timeout: 900
resources: {}
nodeSelector: {}
affinity: {}
tolerations: []
extraLabels: {}
persistentVolume:
enabled: true
#storageClass: "-"
accessModes:
- ReadWriteOnce
size: 1Gi
annotations: {}
serviceAnnotations: {}
terminationGracePeriodSeconds: 30
tls:
enabled: false
configmap: {}
readinessProbe:
initialDelaySeconds: 5
timeoutSeconds: 1
failureThreshold: 3
periodSeconds: 10
successThreshold: 1
livenessProbe:
initialDelaySeconds: 30
timeoutSeconds: 5
failureThreshold: 3
periodSeconds: 10
successThreshold: 1
The persistentVolume.storageClass parameter is commented
out here: removing the comment and setting its value to "-" would disable
dynamic provisioning. In our case, because we are leaving this value
undefined, the chart will choose the default provisioner — in our case,
dobs.csi.digitalocean.com.
Also note the accessMode associated with the persistentVolume
key: ReadWriteOnce means that the provisioned volume will be read-
write only by a single node. Please see the documentation for more
information about different access modes.
To learn more about the other parameters included in the file, see the
configuration table included with the repo.
Save and close the file when you are finished editing.
Before deploying the mongodb-replicaset chart, you will want to
update the stable repo with the helm repo update command:
helm repo update
This will get the latest chart information from the stable repository.
Finally, install the chart with the following command:
helm install --name mongo -f mongodb-values.yaml
stable/mongodb-replicaset
Note: Before installing a chart, you can run helm install with the --
dry-run and --debug options to check the generated manifests for your
release:
helm install --name your_release_name -f
your_values_file.yaml --dry-run --debug your_chart
Note that we are naming the Helm release mongo. This name will refer to
this particular deployment of the chart with the configuration options we’ve
specified. We’ve pointed to these options by including the -f flag and our
mongodb-values.yaml file.
Also note that because we did not include the --namespace flag with
helm install, our chart objects will be created in the default
namespace.
Once you have created the release, you will see output about its status,
along with information about the created objects and instructions for
interacting with them:
Output
NAME: mongo
LAST DEPLOYED: Tue Apr 16 21:51:05 2019
NAMESPACE: default
STATUS: DEPLOYED
RESOURCES:
==> v1/ConfigMap
NAME DATA AGE
mongo-mongodb-replicaset-init 1 1s
mongo-mongodb-replicaset-mongodb 1 1s
mongo-mongodb-replicaset-tests 1 0s
...
You can now check on the creation of your Pods with the following
command:
kubectl get pods
You will see output like the following as the Pods are being created:
Output
NAME READY STATUS
RESTARTS AGE
mongo-mongodb-replicaset-0 1/1 Running 0
67s
mongo-mongodb-replicaset-1 0/1 Init:0/3 0
8s
The READY and STATUS outputs here indicate that the Pods in our
StatefulSet are not fully ready: the Init Containers associated with the Pod’s
containers are still running. Because StatefulSet members are created in
sequential order, each Pod in the StatefulSet must be Running and Ready
before the next Pod will be created.
Once the Pods have been created and all of their associated containers are
running, you will see this output:
Output
NAME READY STATUS
RESTARTS AGE
mongo-mongodb-replicaset-0 1/1 Running 0
2m33s
mongo-mongodb-replicaset-1 1/1 Running 0
94s
mongo-mongodb-replicaset-2 1/1 Running 0
36s
The Running STATUS indicates that your Pods are bound to nodes and
that the containers associated with those Pods are running. READY indicates
how many containers in a Pod are running. For more information, please
consult the documentation on Pod lifecycles.
Note: If you see unexpected phases in the STATUS column, remember that
you can troubleshoot your Pods with the following commands:
kubectl describe pods your_pod
kubectl logs your_pod
Each of the Pods in your StatefulSet has a name that combines the name of
the StatefulSet with the ordinal index of the Pod. Because we created three
replicas, our StatefulSet members are numbered 0-2, and each has a stable
DNS entry comprised of the following elements: $(statefulset-
name)-$(ordinal).$(service
name).$(namespace).svc.cluster.local.
In our case, the StatefulSet and the Headless Service created by the
mongodb-replicaset chart have the same names:
kubectl get statefulset
Output
NAME READY AGE
mongo-mongodb-replicaset 3/3 4m2s
kubectl get svc
Output
NAME TYPE
CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP
10.245.0.1 <none> 443/TCP 42m
mongo-mongodb-replicaset ClusterIP None
<none> 27017/TCP 4m35s
mongo-mongodb-replicaset-client ClusterIP None
<none> 27017/TCP 4m35s
This means that the first member of our StatefulSet will have the
following DNS entry:
mongo-mongodb-replicaset-0.mongo-mongodb-
replicaset.default.svc.cluster.local
Because we need our application to connect to each MongoDB instance,
it’s essential that we have this information so that we can communicate
directly with the Pods, rather than with the Service. When we create our
custom application Helm chart, we will pass the DNS entries for each Pod to
our application using environment variables.
With your database instances up and running, you are ready to create the
chart for your Node application.
~/node_project/nodeapp/values.yaml
# Default values for nodeapp.
# This is a YAML-formatted file.
# Declare variables to be passed into your
templates.
replicaCount: 3
image:
repository: your_dockerhub_username/node-replicas
tag: latest
pullPolicy: IfNotPresent
nameOverride: ""
fullnameOverride: ""
service:
type: LoadBalancer
port: 80
targetPort: 8080
...
Save and close the file when you are finished editing.
Next, open a secret.yaml file in the nodeapp/templates
directory:
nano nodeapp/templates/secret.yaml
In this file, add values for your MONGO_USERNAME and
MONGO_PASSWORD application constants. These are the constants that your
application will expect to have access to at runtime, as specified in db.js,
your database connection file. As you add the values for these constants,
remember to the use the base64-encoded values that you used earlier in Step
2 when creating your mongo-secret object. If you need to recreate those
values, you can return to Step 2 and run the relevant commands again.
Add the following code to the file:
~/node_project/nodeapp/templates/secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: {{ .Release.Name }}-auth
data:
MONGO_USERNAME: your_encoded_username
MONGO_PASSWORD: your_encoded_password
The name of this Secret object will depend on the name of your Helm
release, which you will specify when you deploy the application chart.
Save and close the file when you are finished.
Next, open a file to create a ConfigMap for your application:
nano nodeapp/templates/configmap.yaml
In this file, we will define the remaining variables that our application
expects: MONGO_HOSTNAME, MONGO_PORT, MONGO_DB, and
MONGO_REPLICASET. Our MONGO_HOSTNAME variable will include the
DNS entry for each instance in our replica set, since this is what the
MongoDB connection URI requires.
According to the Kubernetes documentation, when an application
implements liveness and readiness checks, SRV records should be used when
connecting to the Pods. As discussed in Step 3, our Pod SRV records follow
this pattern: $(statefulset-name)-$(ordinal).$(service
name).$(namespace).svc.cluster.local. Since our MongoDB
StatefulSet implements liveness and readiness checks, we should use these
stable identifiers when defining the values of the MONGO_HOSTNAME
variable.
Add the following code to the file to define the MONGO_HOSTNAME,
MONGO_PORT, MONGO_DB, and MONGO_REPLICASET variables. You are
free to use another name for your MONGO_DB database, but your
MONGO_HOSTNAME and MONGO_REPLICASET values must be written as
they appear here:
~/node_project/nodeapp/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-config
data:
MONGO_HOSTNAME: "mongo-mongodb-replicaset-0.mongo-
mongodb-replicaset.default.svc.cluster.local,mongo-
mongodb-replicaset-1.mongo-mongodb-
replicaset.default.svc.cluster.local,mongo-mongodb-
replicaset-2.mongo-mongodb-
replicaset.default.svc.cluster.local"
MONGO_PORT: "27017"
MONGO_DB: "sharkinfo"
MONGO_REPLICASET: "db"
Because we have already created the StatefulSet object and replica set, the
hostnames that are listed here must be listed in your file exactly as they
appear in this example. If you destroy these objects and rename your
MongoDB Helm release, then you will need to revise the values included in
this ConfigMap. The same applies for MONGO_REPLICASET, since we
specified the replica set name with our MongoDB release.
Also note that the values listed here are quoted, which is the expectation
for environment variables in Helm.
Save and close the file when you are finished editing.
With your chart parameter values defined and your Secret and ConfigMap
manifests created, you can edit the application Deployment template to use
your environment variables.
Step 5 — Integrating Environment Variables into Your Helm
Deployment
With the files for our application Secret and ConfigMap in place, we will
need to make sure that our application Deployment can use these values. We
will also customize the liveness and readiness probes that are already
defined in the Deployment manifest.
Open the application Deployment template for editing:
nano nodeapp/templates/deployment.yaml
Though this is a YAML file, Helm templates use a different syntax from
standard Kubernetes YAML files in order to generate manifests. For more
information about templates, see the Helm documentation.
In the file, first add an env key to your application container
specifications, below the imagePullPolicy key and above ports:
~/node_project/nodeapp/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
...
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{
.Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy
}}
env:
ports:
Next, add the following keys to the list of env variables:
~/node_project/nodeapp/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
...
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{
.Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy
}}
env:
- name: MONGO_USERNAME
valueFrom:
secretKeyRef:
key: MONGO_USERNAME
name: {{ .Release.Name }}-auth
- name: MONGO_PASSWORD
valueFrom:
secretKeyRef:
key: MONGO_PASSWORD
name: {{ .Release.Name }}-auth
- name: MONGO_HOSTNAME
valueFrom:
configMapKeyRef:
key: MONGO_HOSTNAME
name: {{ .Release.Name }}-config
- name: MONGO_PORT
valueFrom:
configMapKeyRef:
key: MONGO_PORT
name: {{ .Release.Name }}-config
- name: MONGO_DB
valueFrom:
configMapKeyRef:
key: MONGO_DB
name: {{ .Release.Name }}-config
- name: MONGO_REPLICASET
valueFrom:
configMapKeyRef:
key: MONGO_REPLICASET
name: {{ .Release.Name }}-config
Each variable includes a reference to its value, defined either by a
secretKeyRef key, in the case of Secret values, or configMapKeyRef
for ConfigMap values. These keys point to the Secret and ConfigMap files
we created in the previous Step.
Next, under the ports key, modify the containerPort definition to
specify the port on the container where our application will be exposed:
~/node_project/nodeapp/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
...
spec:
containers:
...
env:
...
ports:
- name: http
containerPort: 8080
protocol: TCP
...
Next, let’s modify the liveness and readiness checks that are included in
this Deployment manifest by default. These checks ensure that our
application Pods are running and ready to serve traffic: - Readiness probes
assess whether or not a Pod is ready to serve traffic, stopping all requests to
the Pod until the checks succeed. - Liveness probes check basic application
behavior to determine whether or not the application in the container is
running and behaving as expected. If a liveness probe fails, Kubernetes will
restart the container.
For more about both, see the relevant discussion in Architecting
Applications for Kubernetes.
In our case, we will build on the httpGet request that Helm has provided
by default and test whether or not our application is accepting requests on
the /sharks endpoint. The kubelet service will perform the probe by
sending a GET request to the Node server running in the application Pod’s
container and listening on port 8080. If the status code for the response is
between 200 and 400, then the kubelet will conclude that the container is
healthy. Otherwise, in the case of a 400 or 500 status, kubelet will either
stop traffic to the container, in the case of the readiness probe, or restart the
container, in the case of the liveness probe.
Add the following modification to the stated path for the liveness and
readiness probes:
~/node_project/nodeapp/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
...
spec:
containers:
...
env:
...
ports:
- name: http
containerPort: 8080
protocol: TCP
livenessProbe:
httpGet:
path: /sharks
port: http
readinessProbe:
httpGet:
path: /sharks
port: http
Save and close the file when you are finished editing.
You are now ready to create your application release with Helm. Run the
following helm install command, which includes the name of the
release and the location of the chart directory:
helm install --name nodejs ./nodeapp
Remember that you can run helm install with the --dry-run and -
-debug options first, as discussed in Step 3, to check the generated
manifests for your release.
Again, because we are not including the --namespace flag with helm
install, our chart objects will be created in the default namespace.
You will see the following output indicating that your release has been
created:
Output
NAME: nodejs
LAST DEPLOYED: Wed Apr 17 18:10:29 2019
NAMESPACE: default
STATUS: DEPLOYED
RESOURCES:
==> v1/ConfigMap
NAME DATA AGE
nodejs-config 4 1s
==> v1/Deployment
NAME READY UP-TO-DATE AVAILABLE AGE
nodejs-nodeapp 0/3 3 0 1s
...
Again, the output will indicate the status of the release, along with
information about the created objects and how you can interact with them.
Check the status of your Pods:
kubectl get pods
Output
NAME READY STATUS
RESTARTS AGE
mongo-mongodb-replicaset-0 1/1 Running
0 57m
mongo-mongodb-replicaset-1 1/1 Running
0 56m
mongo-mongodb-replicaset-2 1/1 Running
0 55m
nodejs-nodeapp-577df49dcc-b5fq5 1/1 Running
0 117s
nodejs-nodeapp-577df49dcc-bkk66 1/1 Running
0 117s
nodejs-nodeapp-577df49dcc-lpmt2 1/1 Running
0 117s
Once your Pods are up and running, check your Services:
kubectl get svc
Output
NAME TYPE
CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP
10.245.0.1 <none> 443/TCP 96m
mongo-mongodb-replicaset ClusterIP
None <none> 27017/TCP 58m
mongo-mongodb-replicaset-client ClusterIP
None <none> 27017/TCP 58m
nodejs-nodeapp LoadBalancer
10.245.33.46 your_lb_ip 80:31518/TCP
3m22s
The EXTERNAL_IP associated with the nodejs-nodeapp Service is
the IP address where you can access the application from outside of the
cluster. If you see a <pending> status in the EXTERNAL_IP column, this
means that your load balancer is still being created.
Once you see an IP in that column, navigate to it in your browser:
http://your_lb_ip.
You should see the following landing page:
Application Landing Page
Now that your replicated application is working, let’s add some test data
to ensure that replication is working between members of the replica set.
Click on the Get Shark Info button. You will see a page with an entry form
where you can enter a shark name and a description of that shark’s general
character:
Shark Info Form
Click on the Submit button. You will see a page with this shark
information displayed back to you:
Shark Output
Now head back to the shark information form by clicking on Sharks in the
top navigation bar:
Enter a new shark of your choosing. We’ll go with Whale Shark and
Large:
Enter New Shark
Once you click Submit, you will see that the new shark has been added to
the shark collection in your database:
Output
NAME READY STATUS
RESTARTS AGE
mongo-mongodb-replicaset-0 1/1 Running
0 74m
mongo-mongodb-replicaset-1 1/1 Running
0 73m
mongo-mongodb-replicaset-2 1/1 Running
0 72m
nodejs-nodeapp-577df49dcc-b5fq5 1/1 Running
0 5m4s
nodejs-nodeapp-577df49dcc-bkk66 1/1 Running
0 5m4s
nodejs-nodeapp-577df49dcc-lpmt2 1/1 Running
0 5m4s
To access the mongo shell on your Pods, you can use the kubectl
exec command and the username you used to create your mongo-secret
in Step 2. Access the mongo shell on the first Pod in the StatefulSet with the
following command:
kubectl exec -it mongo-mongodb-replicaset-0 -- mongo
-u your_database_username -p --
authenticationDatabase admin
When prompted, enter the password associated with this username:
Output
MongoDB shell version v4.1.9
Enter password:
You will be dropped into an administrative shell:
Output
MongoDB server version: 4.1.9
Welcome to the MongoDB shell.
...
db:PRIMARY>
Though the prompt itself includes this information, you can manually
check to see which replica set member is the primary with the
rs.isMaster() method:
rs.isMaster()
You will see output like the following, indicating the hostname of the
primary:
Output
db:PRIMARY> rs.isMaster()
{
"hosts" : [
"mongo-mongodb-replicaset-0.mongo-
mongodb-replicaset.default.svc.cluster.local:27017",
"mongo-mongodb-replicaset-1.mongo-
mongodb-replicaset.default.svc.cluster.local:27017",
"mongo-mongodb-replicaset-2.mongo-
mongodb-replicaset.default.svc.cluster.local:27017"
],
...
"primary" : "mongo-mongodb-replicaset-
0.mongo-mongodb-
replicaset.default.svc.cluster.local:27017",
...
Next, switch to your sharkinfo database:
use sharkinfo
Output
switched to db sharkinfo
List the collections in the database:
show collections
Output
sharks
Output the documents in the collection:
db.sharks.find()
You will see the following output:
Output
{ "_id" : ObjectId("5cb7702c9111a5451c6dc8bb"),
"name" : "Megalodon Shark", "character" : "Ancient",
"__v" : 0 }
{ "_id" : ObjectId("5cb77054fcdbf563f3b47365"),
"name" : "Whale Shark", "character" : "Large", "__v"
: 0 }
Exit the MongoDB Shell:
exit
Now that we have checked the data on our primary, let’s check that it’s
being replicated to a secondary. kubectl exec into mongo-mongodb-
replicaset-1 with the following command:
kubectl exec -it mongo-mongodb-replicaset-1 -- mongo
-u your_database_username -p --
authenticationDatabase admin
Once in the administrative shell, we will need to use the
db.setSlaveOk() method to permit read operations from the secondary
instance:
db.setSlaveOk(1)
Switch to the sharkinfo database:
use sharkinfo
Output
switched to db sharkinfo
Permit the read operation of the documents in the sharks collection:
db.setSlaveOk(1)
Output the documents in the collection:
db.sharks.find()
You should now see the same information that you saw when running this
method on your primary instance:
Output
db:SECONDARY> db.sharks.find()
{ "_id" : ObjectId("5cb7702c9111a5451c6dc8bb"),
"name" : "Megalodon Shark", "character" : "Ancient",
"__v" : 0 }
{ "_id" : ObjectId("5cb77054fcdbf563f3b47365"),
"name" : "Whale Shark", "character" : "Large", "__v"
: 0 }
This output confirms that your application data is being replicated
between the members of your replica set.
Conclusion
You have now deployed a replicated, highly-available shark information
application on a Kubernetes cluster using Helm charts. This demo
application and the workflow outlined in this tutorial can act as a starting
point as you build custom charts for your application and take advantage of
Helm’s stable repository and other chart repositories.
As you move toward production, consider implementing the following: -
Centralized logging and monitoring. Please see the relevant discussion in
Modernizing Applications for Kubernetes for a general overview. You can
also look at How To Set Up an Elasticsearch, Fluentd and Kibana (EFK)
Logging Stack on Kubernetes to learn how to set up a logging stack with
Elasticsearch, Fluentd, and Kibana. Also check out An Introduction to
Service Meshes for information about how service meshes like Istio
implement this functionality. - Ingress Resources to route traffic to your
cluster. This is a good alternative to a LoadBalancer in cases where you are
running multiple Services, which each require their own LoadBalancer, or
where you would like to implement application-level routing strategies (A/B
& canary tests, for example). For more information, check out How to Set
Up an Nginx Ingress with Cert-Manager on DigitalOcean Kubernetes and the
related discussion of routing in the service mesh context in An Introduction
to Service Meshes. - Backup strategies for your Kubernetes objects. For
guidance on implementing backups with Velero (formerly Heptio Ark) with
DigitalOcean’s Kubernetes product, please see How To Back Up and Restore
a Kubernetes Cluster on DigitalOcean Using Heptio Ark.
To learn more about Helm, see An Introduction to Helm, the Package
Manager for Kubernetes, How To Install Software on Kubernetes Clusters
with the Helm Package Manager, and the Helm documentation.
How To Set Up a Private Docker Registry
on Top of DigitalOcean Spaces and Use It
with DigitalOcean Kubernetes
Written by Savic
In this tutorial, you’ll deploy a private Docker registry to your
Kubernetes cluster using Helm. A self-hosted Docker Registry lets you
privately store, distribute, and manage your Docker images. While this
tutorial focuses on using DigitalOcean’s Kubernetes and Spaces products,
the principles of running your own Registry in a cluster apply to any
Kubernetes stack.
At the end of this tutorial, you’ll have a secure, private Docker registry
that uses DigitalOcean Spaces (or another S3-compatible object storage
system) to store your images. Your Kubernetes cluster will be configured
to use the self-hosted registry so that your containerized applications
remain private and secure.
The author selected the Free and Open Source Fund to receive a
donation as part of the Write for DOnations program.
A Docker registry is a storage and content delivery system for named
Docker images, which are the industry standard for containerized
applications. A private Docker registry allows you to securely share your
images within your team or organization with more flexibility and control
when compared to public ones. By hosting your private Docker registry
directly in your Kubernetes cluster, you can achieve higher speeds, lower
latency, and better availability, all while having control over the registry.
The underlying registry storage is delegated to external drivers. The
default storage system is the local filesystem, but you can swap this for a
cloud-based storage driver. DigitalOcean Spaces is an S3-compatible
object storage designed for developer teams and businesses that want a
scalable, simple, and affordable way to store and serve vast amounts of
data, and is very suitable for storing Docker images. It has a built-in CDN
network, which can greatly reduce latency when frequently accessing
images.
In this tutorial, you’ll deploy your private Docker registry to your
DigitalOcean Kubernetes cluster using Helm, backed up by DigitalOcean
Spaces for storing data. You’ll create API keys for your designated Space,
install the Docker registry to your cluster with custom configuration,
configure Kubernetes to properly authenticate with it, and test it by
running a sample deployment on the cluster. At the end of this tutorial,
you’ll have a secure, private Docker registry installed on your
DigitalOcean Kubernetes cluster.
Prerequisites
Before you begin this tutorial, you’ll need:
Docker installed on the machine that you’ll access your cluster from.
For Ubuntu 18.04 visit How To Install and Use Docker on Ubuntu
18.04. You only need to complete the first step. Otherwise visit
Docker’s website for other distributions.
A DigitalOcean Kubernetes cluster with your connection
configuration configured as the kubectl default. Instructions on
how to configure kubectl are shown under the Connect to your
Cluster step shown when you create your cluster. To learn how to
create a Kubernetes cluster on DigitalOcean, see Kubernetes
Quickstart.
A DigitalOcean Space with API keys (access and secret). To learn
how to create a DigitalOcean Space and API keys, see How To Create
a DigitalOcean Space and API Key.
The Helm package manager installed on your local machine, and
Tiller installed on your cluster. Complete steps 1 and 2 of the How To
Install Software on Kubernetes Clusters with the Helm Package
Manager. You only need to complete the first two steps.
The Nginx Ingress Controller and Cert-Manager installed on the
cluster. For a guide on how to do this, see How to Set Up an Nginx
Ingress with Cert-Manager on DigitalOcean Kubernetes.
A domain name with two DNS A records pointed to the DigitalOcean
Load Balancer used by the Ingress. If you are using DigitalOcean to
manage your domain’s DNS records, consult How to Manage DNS
Records to create A records. In this tutorial, we’ll refer to the A
records as registry.example.com and k8s-
test.example.com.
chart_values.yaml
ingress:
enabled: true
hosts:
- registry.example.com
annotations:
kubernetes.io/ingress.class: nginx
certmanager.k8s.io/cluster-issuer:
letsencrypt-prod
nginx.ingress.kubernetes.io/proxy-body-size:
"30720m"
tls:
- secretName: letsencrypt-prod
hosts:
- registry.example.com
storage: s3
secrets:
htpasswd: ""
s3:
accessKey: "your_space_access_key"
secretKey: "your_space_secret_key"
s3:
region: your_space_region
regionEndpoint:
your_space_region.digitaloceanspaces.com
secure: true
bucket: your_space_name
The first block, ingress, configures the Kubernetes Ingress that will
be created as a part of the Helm chart deployment. The Ingress object
makes outside HTTP/HTTPS routes point to internal services in the
cluster, thus allowing communication from the outside. The overridden
values are:
Then, you set the file system storage to s3 — the other available option
would be filesystem. Here s3 indicates using a remote storage system
compatible with the industry-standard Amazon S3 API, which
DigitalOcean Spaces fulfills.
In the next block, secrets, you configure keys for accessing your
DigitalOcean Space under the s3 subcategory. Finally, in the s3 block,
you configure the parameters specifying your Space.
Save and close your file.
Now, if you haven’t already done so, set up your A records to point to
the Load Balancer you created as part of the Nginx Ingress Controller
installation in the prerequisite tutorial. To see how to set your DNS on
DigitalOcean, see How to Manage DNS Records.
Next, ensure your Space isn’t empty. The Docker registry won’t run at
all if you don’t have any files in your Space. To get around this, upload a
file. Navigate to the Spaces tab, find your Space, click the Upload File
button, and upload any file you’d like. You could upload the configuration
file you just created.
Output
NAME: docker-registry
...
NAMESPACE: default
STATUS: DEPLOYED
RESOURCES:
==> v1/ConfigMap
NAME DATA AGE
docker-registry-config 1 1s
==> v1/Pod(related)
NAME READY STATUS
RESTARTS AGE
docker-registry-54df68fd64-l26fb 0/1
ContainerCreating 0 1s
==> v1/Secret
NAME TYPE DATA AGE
docker-registry-secret Opaque 3 1s
==> v1/Service
NAME TYPE CLUSTER-IP
EXTERNAL-IP PORT(S) AGE
docker-registry ClusterIP 10.245.131.143 <none>
5000/TCP 1s
==> v1beta1/Deployment
NAME READY UP-TO-DATE AVAILABLE AGE
docker-registry 0/1 1 0 1s
==> v1beta1/Ingress
NAME HOSTS ADDRESS
PORTS AGE
docker-registry registry.example.com 80, 443 1s
NOTES:
1. Get the application URL by running these
commands:
https://registry.example.com/
Helm lists all the resources it created as a result of the Docker registry
chart deployment. The registry is now accessible from the domain name
you specified earlier.
You’ve configured and deployed a Docker registry on your Kubernetes
cluster. Next, you will test the availability of the newly deployed Docker
registry.
Output
Using default tag: latest
latest: Pulling from library/mysql
27833a3ba0a5: Pull complete
...
e906385f419d: Pull complete
Digest:
sha256:a7cf659a764732a27963429a87eccc8457e6d4af0ee
9d5140a3b56e74986eed7
Status: Downloaded newer image for mysql:latest
You now have the image available locally. To inform Docker where to
push it, you’ll need to tag it with the host name, like so:
sudo docker tag mysql registry.example.com/mysql
Then, push the image to the new registry:
sudo docker push registry.example.com/mysql
This command will run successfully and indicate that your new registry
is properly configured and accepting traffic — including pushing new
images. If you see an error, double check your steps against steps 1 and 2.
To test pulling from the registry cleanly, first delete the local mysql
images with the following command:
sudo docker rmi registry.example.com/mysql && sudo
docker rmi mysql
Then, pull it from the registry:
sudo docker pull registry.example.com/mysql
This command will take a few seconds to complete. If it runs
successfully, that means your registry is working correctly. If it shows an
error, double check what you have entered against the previous commands.
You can list Docker images available locally by running the following
command:
sudo docker images
You’ll see output listing the images available on your local machine,
along with their ID and date of creation.
Your Docker registry is configured. You’ve pushed an image to it and
verified you can pull it down. Now let’s add authentication so only certain
people can access the code.
chart_values.yaml
htpasswd: ""
Edit it to match the following, replacing
htpasswd\_file\_contents with the contents you copied from the
htpasswd_file:
chart_values.yaml
htpasswd: |-
htpasswd_file_contents
Be careful with the indentation, each line of the file contents must have
four spaces before it.
Once you’ve added your contents, save and close the file.
To propagate the changes to your cluster, run the following command:
helm upgrade docker-registry stable/docker-
registry -f chart_values.yaml
The output will be similar to that shown when you first deployed your
Docker registry:
Output
Release "docker-registry" has been upgraded. Happy
Helming!
LAST DEPLOYED: ...
NAMESPACE: default
STATUS: DEPLOYED
RESOURCES:
==> v1/ConfigMap
NAME DATA AGE
docker-registry-config 1 3m8s
==> v1/Pod(related)
NAME READY STATUS
RESTARTS AGE
docker-registry-6c5bb7ffbf-ltnjv 1/1 Running
0 3m7s
==> v1/Secret
NAME TYPE DATA AGE
docker-registry-secret Opaque 4 3m8s
==> v1/Service
NAME TYPE CLUSTER-IP
EXTERNAL-IP PORT(S) AGE
docker-registry ClusterIP 10.245.128.245 <none>
5000/TCP 3m8s
==> v1beta1/Deployment
NAME READY UP-TO-DATE AVAILABLE AGE
docker-registry 1/1 1 1
3m8s
==> v1beta1/Ingress
NAME HOSTS ADDRESS
PORTS AGE
docker-registry registry.example.com
159.89.215.50 80, 443 3m8s
NOTES:
1. Get the application URL by running these
commands:
https://registry.example.com/
This command calls Helm and instructs it to upgrade an existing
release, in your case docker-registry, with its chart defined in
stable/docker-registry in the chart repository, after applying the
chart_values.yaml file.
Now, you’ll try pulling an image from the registry again:
sudo docker pull registry.example.com/mysql
The output will look like the following:
Output
Using default tag: latest
Error response from daemon: Get
https://registry.example.com/v2/mysql/manifests/la
test: no basic auth credentials
It correctly failed because you provided no credentials. This means that
your Docker registry authorizes requests correctly.
To log in to the registry, run the following command:
sudo docker login registry.example.com
Remember to replace registry.example.com with your domain
address. It will prompt you for a username and password. If it shows an
error, double check what your htpasswd_file contains. You must
define the username and password combination in the htpasswd_file,
which you created earlier in this step.
To test the login, you can try to pull again by running the following
command:
sudo docker pull registry.example.com/mysql
The output will look similar to the following:
Output
Using default tag: latest
latest: Pulling from mysql
Digest:
sha256:f2dc118ca6fa4c88cde5889808c486dfe94bccecd01
ca626b002a010bb66bcbe
Status: Image is up to date for
registry.example.com/mysql:latest
You’ve now configured Docker and can log in securely. To configure
Kubernetes to log in to your registry, run the following command:
sudo kubectl create secret generic regcred --from-
file=.dockerconfigjson=/home/sammy/.docker/config.
json --type=kubernetes.io/dockerconfigjson
You will see the following output:
Output
secret/regcred created
This command creates a secret in your cluster with the name regcred,
takes the contents of the JSON file where Docker stores the credentials,
and parses it as dockerconfigjson, which defines a registry
credential in Kubernetes.
You’ve used htpasswd to create a login config file, configured the
registry to authenticate requests, and created a Kubernetes secret
containing the login credentials. Next, you will test the integration
between your Kubernetes cluster and registry.
hello-world.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: hello-kubernetes-ingress
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: k8s-test.example.com
http:
paths:
- path: /
backend:
serviceName: hello-kubernetes
servicePort: 80
---
apiVersion: v1
kind: Service
metadata:
name: hello-kubernetes
spec:
type: NodePort
ports:
- port: 80
targetPort: 8080
selector:
app: hello-kubernetes
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-kubernetes
spec:
replicas: 3
selector:
matchLabels:
app: hello-kubernetes
template:
metadata:
labels:
app: hello-kubernetes
spec:
containers:
- name: hello-kubernetes
image:
registry.example.com/paulbouwer/hello-
kubernetes:1.5
ports:
- containerPort: 8080
imagePullSecrets:
- name: regcred
First, you define the Ingress for the Hello World deployment, which you
will route through the Load Balancer that the Nginx Ingress Controller
owns. Then, you define a service that can access the pods created in the
deployment. In the actual deployment spec, you specify the image as the
one located in your registry and set imagePullSecrets to regcred,
which you created in the previous step.
Save and close the file. To deploy this to your cluster, run the following
command:
kubectl apply -f hello-world.yaml
You’ll see the following output:
Output
ingress.extensions/hello-kubernetes-ingress
created
service/hello-kubernetes created
deployment.apps/hello-kubernetes created
You can now navigate to your test domain — the second A record, k8s-
test.example.com in this tutorial. You will see the Kubernetes Hello
world! page.
Hello World page
The Hello World page lists some environment information, like the
Linux kernel version and the internal ID of the pod the request was served
from. You can also access your Space via the web interface to see the
images you’ve worked with in this tutorial.
If you want to delete this Hello World deployment after testing, run the
following command:
kubectl delete -f hello-world.yaml
You’ve created a sample Hello World deployment to test if Kubernetes
is properly pulling images from your private registry.
Conclusion
You have now successfully deployed your own private Docker registry on
your DigitalOcean Kubernetes cluster, using DigitalOcean Spaces as the
storage layer underneath. There is no limit to how many images you can
store, Spaces can extend infinitely, while at the same time providing the
same security and robustness. In production, though, you should always
strive to optimize your Docker images as much as possible, take a look at
the How To Optimize Docker Images for Production tutorial.
How To Deploy a PHP Application with
Kubernetes on Ubuntu 18.04
Prerequisites
php_service.yaml
apiVersion: v1
kind: Service
Name the service php since it will provide access to PHP-FPM:
php_service.yaml
...
metadata:
name: php
You will logically group different objects with labels. In this tutorial,
you will use labels to group the objects into “tiers”, such as frontend or
backend. The PHP pods will run behind this service, so you will label it as
tier: backend.
php_service.yaml
...
labels:
tier: backend
A service determines which pods to access by using selector labels.
A pod that matches these labels will be serviced, independent of whether
the pod was created before or after the service. You will add labels for
your pods later in the tutorial.
Use the tier: backend label to assign the pod into the back-end
tier. You will also add the app: php label to specify that this pod runs
PHP. Add these two labels after the metadata section.
php_service.yaml
...
spec:
selector:
app: php
tier: backend
Next, specify the port used to access this service. You will use port
9000 in this tutorial. Add it to the php_service.yaml file under
spec:
php_service.yaml
...
ports:
- protocol: TCP
port: 9000
Your completed php_service.yaml file will look like this:
php_service.yaml
apiVersion: v1
kind: Service
metadata:
name: php
labels:
tier: backend
spec:
selector:
app: php
tier: backend
ports:
- protocol: TCP
port: 9000
Hit CTRL + O to save the file, and then CTRL + X to exit nano.
Now that you’ve created the object definition for your service, to run
the service you will use the kubectl apply command along with the -
f argument and specify your php_service.yaml file.
Create your service:
kubectl apply -f php_service.yaml
This output confirms the service creation:
Output
service/php created
Verify that your service is running:
kubectl get svc
You will see your PHP-FPM service running:
Output
NAME TYPE CLUSTER-IP EXTERNAL-
IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none>
443/TCP 10m
php ClusterIP 10.100.59.238 <none>
9000/TCP 5m
There are various service types that Kubernetes supports. Your php
service uses the default service type, ClusterIP. This service type
assigns an internal IP and makes the service reachable only from within
the cluster.
Now that the PHP-FPM service is ready, you will create the Nginx
service. Create and open a new file called nginx_service.yaml with
the editor:
nano nginx_service.yaml
This service will target Nginx pods, so you will name it nginx. You
will also add a tier: backend label as it belongs in the back-end tier:
nginx_service.yaml
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
tier: backend
Similar to the php service, target the pods with the selector labels
app: nginx and tier: backend. Make this service accessible on
port 80, the default HTTP port.
nginx_service.yaml
...
spec:
selector:
app: nginx
tier: backend
ports:
- protocol: TCP
port: 80
The Nginx service will be publicly accessible to the internet from your
Droplet’s public IP address. your_public_ip can be found from your
DigitalOcean Control Panel. Under spec.externalIPs, add:
nginx_service.yaml
...
spec:
externalIPs:
- your_public_ip
Your nginx_service.yaml file will look like this:
nginx_service.yaml
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
tier: backend
spec:
selector:
app: nginx
tier: backend
ports:
- protocol: TCP
port: 80
externalIPs:
- your_public_ip
Save and close the file. Create the Nginx service:
kubectl apply -f nginx_service.yaml
You will see the following output when the service is running:
Output
service/nginx created
You can view all running services by executing:
kubectl get svc
You will see both the PHP-FPM and Nginx services listed in the output:
Output
NAME TYPE CLUSTER-IP EXTERNAL-
IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none>
443/TCP 13m
nginx ClusterIP 10.102.160.47
your_public_ip 80/TCP 50s
php ClusterIP 10.100.59.238 <none>
9000/TCP 8m
Please note, if you want to delete a service you can run:
kubectl delete svc/service_name
Now that you’ve created your PHP-FPM and Nginx services, you will
need to specify where to store your application code and configuration
files.
secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: digitalocean
namespace: kube-system
Instead of a spec key, a Secret uses a data or stringData key to
hold the required information. The data parameter holds base64 encoded
data that is automatically decoded when retrieved. The stringData
parameter holds non-encoded data that is automatically encoded during
creation or updates, and does not output the data when retrieving Secrets.
You will use stringData in this tutorial for convenience.
Add the access-token as stringData:
secret.yaml
...
stringData:
access-token: your-api-token
Save and exit the file.
Your secret.yaml file will look like this:
secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: digitalocean
namespace: kube-system
stringData:
access-token: your-api-token
Create the secret:
kubectl apply -f secret.yaml
You will see this output upon Secret creation:
Output
secret/digitalocean created
You can view the secret with the following command:
kubectl -n kube-system get secret digitalocean
The output will look similar to this:
Output
NAME TYPE DATA AGE
digitalocean Opaque 1 41s
The Opaque type means that this Secret is read-only, which is standard
for stringData Secrets. You can read more about it on the Secret design
spec. The DATA field shows the number of items stored in this Secret. In
this case, it shows 1 because you have a single key stored.
Now that your Secret is in place, install the DigitalOcean block storage
plug-in:
kubectl apply -f
https://raw.githubusercontent.com/digitalocean/csi
-
digitalocean/master/deploy/kubernetes/releases/csi
-digitalocean-v1.1.0.yaml
You will see output similar to the following:
Output
csidriver.storage.k8s.io/dobs.csi.digitalocean.com
created
customresourcedefinition.apiextensions.k8s.io/volu
mesnapshotclasses.snapshot.storage.k8s.io created
customresourcedefinition.apiextensions.k8s.io/volu
mesnapshotcontents.snapshot.storage.k8s.io created
customresourcedefinition.apiextensions.k8s.io/volu
mesnapshots.snapshot.storage.k8s.io created
storageclass.storage.k8s.io/do-block-storage
created
statefulset.apps/csi-do-controller created
serviceaccount/csi-do-controller-sa created
clusterrole.rbac.authorization.k8s.io/csi-do-
provisioner-role created
clusterrolebinding.rbac.authorization.k8s.io/csi-
do-provisioner-binding created
clusterrole.rbac.authorization.k8s.io/csi-do-
attacher-role created
clusterrolebinding.rbac.authorization.k8s.io/csi-
do-attacher-binding created
clusterrole.rbac.authorization.k8s.io/csi-do-
snapshotter-role created
clusterrolebinding.rbac.authorization.k8s.io/csi-
do-snapshotter-binding created
daemonset.apps/csi-do-node created
serviceaccount/csi-do-node-sa created
clusterrole.rbac.authorization.k8s.io/csi-do-node-
driver-registrar-role created
clusterrolebinding.rbac.authorization.k8s.io/csi-
do-node-driver-registrar-binding created
error: unable to recognize
"https://raw.githubusercontent.com/digitalocean/cs
i-
digitalocean/master/deploy/kubernetes/releases/csi
-digitalocean-v1.1.0.yaml": no matches for kind
"VolumeSnapshotClass" in version
"snapshot.storage.k8s.io/v1alpha1"
For this tutorial, it is safe to ignore the errors.
Now that you have installed the DigitalOcean storage plug-in, you can
create block storage to hold your application code and configuration files.
code_volume.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: code
The spec for a PVC contains the following items:
code_volume.yaml
...
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
Next, specify the storage class that Kubernetes will use to provision the
volumes. You will use the do-block-storage class created by the
DigitalOcean block storage plug-in.
code_volume.yaml
...
storageClassName: do-block-storage
Your code_volume.yaml file will look like this:
code_volume.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: code
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: do-block-storage
Save and exit the file.
Create the code PVC using kubectl:
kubectl apply -f code_volume.yaml
The following output tells you that the object was successfully created,
and you are ready to mount your 1GB PVC as a volume.
Output
persistentvolumeclaim/code created
To view available Persistent Volumes (PV):
kubectl get pv
You will see your PV listed:
Output
NAME
CAPACITY ACCESS MODES RECLAIM POLICY STATUS
CLAIM STORAGECLASS REASON AGE
pvc-ca4df10f-ab8c-11e8-b89d-12331aa95b13 1Gi
RWO Delete Bound
default/code do-block-storage 2m
The fields above are an overview of your configuration file, except for
Reclaim Policy and Status. The Reclaim Policy defines what
is done with the PV after the PVC accessing it is deleted. Delete
removes the PV from Kubernetes as well as the DigitalOcean
infrastructure. You can learn more about the Reclaim Policy and
Status from the Kubernetes PV documentation.
You’ve successfully created a Persistent Volume using the DigitalOcean
block storage plug-in. Now that your Persistent Volume is ready, you will
create your pods using a Deployment.
index.php
<?php
echo phpinfo();
To create your Deployment, open a new file called
php_deployment.yaml with your editor:
nano php_deployment.yaml
This Deployment will manage your PHP-FPM pods, so you will name
the Deployment object php. The pods belong to the back-end tier, so you
will group the Deployment into this group by using the tier: backend
label:
php_deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: php
labels:
tier: backend
For the Deployment spec, you will specify how many copies of this
pod to create by using the replicas parameter. The number of
replicas will vary depending on your needs and available resources.
You will create one replica in this tutorial:
php_deployment.yaml
...
spec:
replicas: 1
This Deployment will manage pods that match the app: php and
tier: backend labels. Under selector key add:
php_deployment.yaml
...
selector:
matchLabels:
app: php
tier: backend
Next, the Deployment spec requires the template for your pod’s
object definition. This template will define specifications to create the pod
from. First, you will add the labels that were specified for the php service
selectors and the Deployment’s matchLabels. Add app: php and
tier: backend under template.metadata.labels:
php_deployment.yaml
...
template:
metadata:
labels:
app: php
tier: backend
A pod can have multiple containers and volumes, but each will need a
name. You can selectively mount volumes to a container by specifying a
mount path for each volume.
First, specify the volumes that your containers will access. You created
a PVC named code to hold your application code, so name this volume
code as well. Under spec.template.spec.volumes, add the
following:
php_deployment.yaml
...
spec:
volumes:
- name: code
persistentVolumeClaim:
claimName: code
Next, specify the container you want to run in this pod. You can find
various images on the Docker store, but in this tutorial you will use the
php:7-fpm image.
Under spec.template.spec.containers, add the following:
php_deployment.yaml
...
containers:
- name: php
image: php:7-fpm
Next, you will mount the volumes that the container requires access to.
This container will run your PHP code, so it will need access to the code
volume. You will also use mountPath to specify /code as the mount
point.
Under spec.template.spec.containers.volumeMounts,
add:
php_deployment.yaml
...
volumeMounts:
- name: code
mountPath: /code
Now that you have mounted your volume, you need to get your
application code on the volume. You may have previously used FTP/SFTP
or cloned the code over an SSH connection to accomplish this, but this
step will show you how to copy the code using an Init Container.
Depending on the complexity of your setup process, you can either use a
single initContainer to run a script that builds your application, or
you can use one initContainer per command. Make sure that the
volumes are mounted to the initContainer.
In this tutorial, you will use a single Init Container with busybox to
download the code. busybox is a small image that contains the wget
utility that you will use to accomplish this.
Under spec.template.spec, add your initContainer and
specify the busybox image:
php_deployment.yaml
...
initContainers:
- name: install
image: busybox
Your Init Container will need access to the code volume so that it can
download the code in that location. Under
spec.template.spec.initContainers, mount the volume code
at the /code path:
php_deployment.yaml
...
volumeMounts:
- name: code
mountPath: /code
Each Init Container needs to run a command. Your Init Container will
use wget to download the code from Github into the /code working
directory. The -O option gives the downloaded file a name, and you will
name this file index.php.
Note: Be sure to trust the code you’re pulling. Before pulling it to your
server, inspect the source code to ensure you are comfortable with what
the code does.
Under the install container in
spec.template.spec.initContainers, add these lines:
php_deployment.yaml
...
command:
- wget
- "-O"
- "/code/index.php"
- https://raw.githubusercontent.com/do-
community/php-kubernetes/master/index.php
Your completed php_deployment.yaml file will look like this:
php_deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: php
labels:
tier: backend
spec:
replicas: 1
selector:
matchLabels:
app: php
tier: backend
template:
metadata:
labels:
app: php
tier: backend
spec:
volumes:
- name: code
persistentVolumeClaim:
claimName: code
containers:
- name: php
image: php:7-fpm
volumeMounts:
- name: code
mountPath: /code
initContainers:
- name: install
image: busybox
volumeMounts:
- name: code
mountPath: /code
command:
- wget
- "-O"
- "/code/index.php"
- https://raw.githubusercontent.com/do-
community/php-kubernetes/master/index.php
Save the file and exit the editor.
Create the PHP-FPM Deployment with kubectl:
kubectl apply -f php_deployment.yaml
You will see the following output upon Deployment creation:
Output
deployment.apps/php created
To summarize, this Deployment will start by downloading the specified
images. It will then request the PersistentVolume from your
PersistentVolumeClaim and serially run your initContainers.
Once complete, the containers will run and mount the volumes to the
specified mount point. Once all of these steps are complete, your pod will
be up and running.
You can view your Deployment by running:
kubectl get deployments
You will see the output:
Output
NAME DESIRED CURRENT UP-TO-DATE
AVAILABLE AGE
php 1 1 1 0
19s
This output can help you understand the current state of the
Deployment. A Deployment is one of the controllers that maintains a
desired state. The template you created specifies that the DESIRED
state will have 1 replicas of the pod named php. The CURRENT field
indicates how many replicas are running, so this should match the
DESIRED state. You can read about the remaining fields in the Kubernetes
Deployments documentation.
You can view the pods that this Deployment started with the following
command:
kubectl get pods
The output of this command varies depending on how much time has
passed since creating the Deployment. If you run it shortly after creation,
the output will likely look like this:
Output
NAME READY STATUS
RESTARTS AGE
php-86d59fd666-bf8zd 0/1 Init:0/1 0
9s
The columns represent the following information:
Output
NAME READY STATUS
RESTARTS AGE
php-86d59fd666-lkwgn 0/1 podInitializing
0 39s
This means the Init Containers have finished and the containers are
initializing. If you run the command when all of the containers are
running, you will see the pod status change to Running.
Output
NAME READY STATUS
RESTARTS AGE
php-86d59fd666-lkwgn 1/1 Running 0
1m
You now see that your pod is running successfully. If your pod doesn’t
start, you can debug with the following commands:
nginx_configMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-config
labels:
tier: backend
Next, you will add the data for the ConfigMap. Name the key config
and add the contents of your Nginx configuration file as the value. You can
use the example Nginx configuration from this tutorial.
Because Kubernetes can route requests to the appropriate host for a
service, you can enter the name of your PHP-FPM service in the
fastcgi_pass parameter instead of its IP address. Add the following to
your nginx_configMap.yaml file:
nginx_configMap.yaml
...
data:
config : |
server {
index index.php index.html;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /code;
location / {
try_files $uri $uri/ /index.php?
$query_string;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)
(/.+)$;
fastcgi_pass php:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME
$document_root$fastcgi_script_name;
fastcgi_param PATH_INFO
$fastcgi_path_info;
}
}
Your nginx_configMap.yaml file will look like this:
nginx_configMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-config
labels:
tier: backend
data:
config : |
server {
index index.php index.html;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /code;
location / {
try_files $uri $uri/ /index.php?
$query_string;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)
(/.+)$;
fastcgi_pass php:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME
$document_root$fastcgi_script_name;
fastcgi_param PATH_INFO
$fastcgi_path_info;
}
}
Save the file and exit the editor.
Create the ConfigMap:
kubectl apply -f nginx_configMap.yaml
You will see the following output:
Output
configmap/nginx-config created
You’ve finished creating your ConfigMap and can now build your Nginx
Deployment.
Start by opening a new nginx_deployment.yaml file in the editor:
nano nginx_deployment.yaml
Name the Deployment nginx and add the label tier: backend:
nginx_deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
labels:
tier: backend
Specify that you want one replicas in the Deployment spec. This
Deployment will manage pods with labels app: nginx and tier:
backend. Add the following parameters and values:
nginx_deployment.yaml
...
spec:
replicas: 1
selector:
matchLabels:
app: nginx
tier: backend
Next, add the pod template. You need to use the same labels that you
added for the Deployment selector.matchLabels. Add the
following:
nginx_deployment.yaml
...
template:
metadata:
labels:
app: nginx
tier: backend
Give Nginx access to the code PVC that you created earlier. Under
spec.template.spec.volumes, add:
nginx_deployment.yaml
...
spec:
volumes:
- name: code
persistentVolumeClaim:
claimName: code
Pods can mount a ConfigMap as a volume. Specifying a file name and
key will create a file with its value as the content. To use the ConfigMap,
set path to name of the file that will hold the contents of the key. You
want to create a file site.conf from the key config. Under
spec.template.spec.volumes, add the following:
nginx_deployment.yaml
...
- name: config
configMap:
name: nginx-config
items:
- key: config
path: site.conf
Warning: If a file is not specified, the contents of the key will replace
the mountPath of the volume. This means that if a path is not explicitly
specified, you will lose all content in the destination folder.
Next, you will specify the image to create your pod from. This tutorial
will use the nginx:1.7.9 image for stability, but you can find other
Nginx images on the Docker store. Also, make Nginx available on the port
80. Under spec.template.spec add:
nginx_deployment.yaml
...
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
Nginx and PHP-FPM need to access the file at the same path, so mount
the code volume at /code:
nginx_deployment.yaml
...
volumeMounts:
- name: code
mountPath: /code
The nginx:1.7.9 image will automatically load any configuration
files under the /etc/nginx/conf.d directory. Mounting the config
volume in this directory will create the file
/etc/nginx/conf.d/site.conf. Under volumeMounts add the
following:
nginx_deployment.yaml
...
- name: config
mountPath: /etc/nginx/conf.d
Your nginx_deployment.yaml file will look like this:
nginx_deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
labels:
tier: backend
spec:
replicas: 1
selector:
matchLabels:
app: nginx
tier: backend
template:
metadata:
labels:
app: nginx
tier: backend
spec:
volumes:
- name: code
persistentVolumeClaim:
claimName: code
- name: config
configMap:
name: nginx-config
items:
- key: config
path: site.conf
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
volumeMounts:
- name: code
mountPath: /code
- name: config
mountPath: /etc/nginx/conf.d
Save the file and exit the editor.
Create the Nginx Deployment:
kubectl apply -f nginx_deployment.yaml
The following output indicates that your Deployment is now created:
Output
deployment.apps/nginx created
List your Deployments with this command:
kubectl get deployments
You will see the Nginx and PHP-FPM Deployments:
Output
NAME DESIRED CURRENT UP-TO-DATE
AVAILABLE AGE
nginx 1 1 1 0
16s
php 1 1 1 1
7m
List the pods managed by both of the Deployments:
kubectl get pods
You will see the pods that are running:
Output
NAME READY STATUS
RESTARTS AGE
nginx-7bf5476b6f-zppml 1/1 Running 0
32s
php-86d59fd666-lkwgn 1/1 Running 0
7m
Now that all of the Kubernetes objects are active, you can visit the
Nginx service on your browser.
List the running services:
kubectl get services -o wide
Get the External IP for your Nginx service:
Output
NAME TYPE CLUSTER-IP EXTERNAL-
IP PORT(S) AGE SELECTOR
kubernetes ClusterIP 10.96.0.1 <none>
443/TCP 39m <none>
nginx ClusterIP 10.102.160.47
your_public_ip 80/TCP 27m
app=nginx,tier=backend
php ClusterIP 10.100.59.238 <none>
9000/TCP 34m app=php,tier=backend
On your browser, visit your server by typing in
http://your_public_ip. You will see the output of php_info()
and have confirmed that your Kubernetes services are up and running.
Conclusion
In this guide, you containerized the PHP-FPM and Nginx services so that
you can manage them independently. This approach will not only improve
the scalability of your project as you grow, but will also allow you to
efficiently use resources as well. You also stored your application code on
a volume so that you can easily update your services in the future.
How To Automate Deployments to
DigitalOcean Kubernetes with CircleCI
Prerequisites
To follow this tutorial, you’ll need to have:
For this tutorial, you will use Kubernetes version 1.13.5 and
kubectl version 1.10.7.
Step 1 — Creating Your DigitalOcean Kubernetes Cluster
Note: You can skip this section if you already have a running
DigitalOcean Kubernetes cluster.
In this first step, you will create the DigitalOcean Kubernetes (DOKS)
cluster from which you will deploy your sample application. The
kubectl commands executed from your local machine will change or
retrieve information directly from the Kubernetes cluster.
Go to the Kubernetes page on your DigitalOcean account.
Click Create a Kubernetes cluster, or click the green Create button at the
top right of the page and select Clusters from the dropdown menu.
The next page is where you are going to specify the details of your
cluster. On Select a Kubernetes version pick version 1.13.5-do.0. If this
one is not available, choose a higher one.
For Choose a datacenter region, choose the region closest to you. This
tutorial will use San Francisco - 2.
You then have the option to build your Node pool(s). On Kubernetes, a
node is a worker machine, which contains the services necessary to run
pods. On DigitalOcean, each node is a Droplet. Your node pool will consist
of a single Standard node. Select the 2GB/1vCPU configuration and
change to 1 Node on the number of nodes.
You can add extra tags if you want; this can be useful if you plan to use
DigitalOcean API or just to better organize your node pools.
On Choose a name, for this tutorial, use kubernetes-
deployment-tutorial. This will make it easier to follow throughout
while reading the next sections. Finally, click the green Create Cluster
button to create your cluster.
After cluster creation, there will be a button on the UI to download a
configuration file called Download Config File. This is the file you will be
using to authenticate the kubectl commands you are going to run
against your cluster. Download it to your kubectl machine.
The default way to use that file is to always pass the --kubeconfig
flag and the path to it on all commands you run with kubectl. For
example, if you downloaded the config file to Desktop, you would run
the kubectl get pods command like this:
kubectl --kubeconfig ~/Desktop/kubernetes-
deployment-tutorial-kubeconfig.yaml get pods
This would yield the following output:
Output
No resources found.
This means you accessed your cluster. The No resources found.
message is correct, since you don’t have any pods on your cluster.
If you are not maintaining any other Kubernetes clusters you can copy
the kubeconfig file to a folder on your home directory called .kube.
Create that directory in case it does not exist:
mkdir -p ~/.kube
Then copy the config file into the newly created .kube directory and
rename it config:
cp current_kubernetes-deployment-tutorial-
kubeconfig.yaml_file_path ~/.kube/config
The config file should now have the path ~/.kube/config. This is
the file that kubectl reads by default when running any command, so
there is no need to pass --kubeconfig anymore. Run the following:
kubectl get pods
You will receive the following output:
Output
No resources found.
Now access the cluster with the following:
kubectl get nodes
You will receive the list of nodes on your cluster. The output will be
similar to this:
Output
NAME STATUS
ROLES AGE VERSION
kubernetes-deployment-tutorial-1-7pto Ready
<none> 1h v1.13.5
In this tutorial you are going to use the default namespace for all
kubectl commands and manifest files, which are files that define the
workload and operating parameters of work in Kubernetes. Namespaces
are like virtual clusters inside your single physical cluster. You can change
to any other namespace you want; just make sure to always pass it using
the --namespace flag to kubectl, and/or specifying it on the
Kubernetes manifests metadata field. They are a great way to organize the
deployments of your team and their running environments; read more
about them in the official Kubernetes overview on Namespaces.
By finishing this step you are now able to run kubectl against your
cluster. In the next step, you will create the local Git repository you are
going to use to house your sample application.
~/kube-general/cicd-service-account.yml
apiVersion: v1
kind: ServiceAccount
metadata:
name: cicd
namespace: default
This is a YAML file; all Kubernetes resources are represented using one.
In this case you are saying this resource is from Kubernetes API version
v1 (internally kubectl creates resources by calling Kubernetes HTTP
APIs), and it is a ServiceAccount.
The metadata field is used to add more information about this
resource. In this case, you are giving this ServiceAccount the name
cicd, and creating it on the default namespace.
You can now create this Service Account on your cluster by running
kubectl apply, like the following:
kubectl apply -f ~/kube-general/
You will recieve output similar to the following:
Output
serviceaccount/cicd created
To make sure your Service Account is working, try to log in to your
cluster using it. To do that you first need to obtain their respective access
token and store it in an environment variable. Every Service Account has
an access token which Kubernetes stores as a Secret.
You can retrieve this secret using the following command:
TOKEN=$(kubectl get secret $(kubectl get secret |
grep cicd-token | awk '{print $1}') -o
jsonpath='{.data.token}' | base64 --decode)
Some explanation on what this command is doing:
$(kubectl get secret | grep cicd-token | awk
'{print $1}')
This is used to retrieve the name of the secret related to our cicd
Service Account. kubectl get secret returns the list of secrets on
the default namespace, then you use grep to search for the lines related to
your cicd Service Account. Then you return the name, since it is the first
thing on the single line returned from the grep.
kubectl get secret preceding-command -o
jsonpath='{.data.token}' | base64 --decode
This will retrieve only the secret for your Service Account token. You
then access the token field using jsonpath, and pass the result to
base64 --decode. This is necessary because the token is stored as a
Base64 string. The token itself is a JSON Web Token.
You can now try to retrieve your pods with the cicd Service Account.
Run the following command, replacing server-from-kubeconfig-
file with the server URL that can be found after server: in
~kube/config. This command will give a specific error that you will
learn about later in this tutorial:
kubectl --insecure-skip-tls-verify --
kubeconfig="/dev/null" --server=server-from-
kubeconfig-file --token=$TOKEN get pods
--insecure-skip-tls-verify skips the step of verifying the
certificate of the server, since you are just testing and do not need to verify
this. --kubeconfig="/dev/null" is to make sure kubectl does
not read your config file and credentials but instead uses the token
provided.
The output should be similar to this:
Output
Error from server (Forbidden): pods is forbidden:
User "system:serviceaccount:default:cicd" cannot
list resource "pods" in API group "" in the
namespace "default"
This is an error, but it shows us that the token worked. The error you
received is about your Service Account not having the neccessary
authorization to list the resource secrets, but you were able to access
the server itself. If your token had not worked, the error would have been
the following one:
Output
error: You must be logged in to the server
(Unauthorized)
Now that the authentication was a success, the next step is to fix the
authorization error for the Service Account. You will do this by creating a
role with the necessary permissions and binding it to your Service
Account.
~/kube-general/cicd-role.yml
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: cicd
namespace: default
rules:
- apiGroups: ["", "apps", "batch", "extensions"]
resources: ["deployments", "services",
"replicasets", "pods", "jobs", "cronjobs"]
verbs: ["*"]
This YAML has some similarities with the one you created previously,
but here you are saying this resource is a Role, and it’s from the
Kubernetes API rbac.authorization.k8s.io/v1. You are naming
your role cicd, and creating it on the same namespace you created your
ServiceAccount, the default one.
Then you have the rules field, which is a list of resources this role has
access to. In Kubernetes resources are defined based on the API group they
belong to, the resource kind itself, and what actions you can do on then,
which is represented by a verb. Those verbs are similar to the HTTP ones.
In our case you are saying that your Role is allowed to do everything,
*, on the following resources: deployments, services,
replicasets, pods, jobs, and cronjobs. This also applies to those
resources belonging to the following API groups: "" (empty string),
apps, batch, and extensions. The empty string means the root API
group. If you use apiVersion: v1 when creating a resource it means
this resource is part of this API group.
A Role by itself does nothing; you must also create a RoleBinding,
which binds a Role to something, in this case, a ServiceAccount.
Create the file ~/kube-general/cicd-role-binding.yml and
open it:
nano ~/kube-general/cicd-role-binding.yml
Add the following lines to the file:
~/kube-general/cicd-role-binding.yml
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: cicd
namespace: default
subjects:
- kind: ServiceAccount
name: cicd
namespace: default
roleRef:
kind: Role
name: cicd
apiGroup: rbac.authorization.k8s.io
Your RoleBinding has some specific fields that have not yet been
covered in this tutorial. roleRef is the Role you want to bind to
something; in this case it is the cicd role you created earlier. subjects
is the list of resources you are binding your role to; in this case it’s a
single ServiceAccount called cicd.
Note: If you had used a ClusterRole, you would have to create a
ClusterRoleBinding instead of a RoleBinding. The file would be
almost the same. The only difference would be that it would have no
namespace field inside the metadata.
With those files created you will be able to use kubectl apply
again. Create those new resources on your Kubernetes cluster by running
the following command:
kubectl apply -f ~/kube-general/
You will receive output similar to the following:
Output
rolebinding.rbac.authorization.k8s.io/cicd created
role.rbac.authorization.k8s.io/cicd created
serviceaccount/cicd created
Now, try the command you ran previously:
kubectl --insecure-skip-tls-verify --
kubeconfig="/dev/null" --server=server-from-
kubeconfig-file --token=$TOKEN get pods
Since you have no pods, this will yield the following output:
Output
No resources found.
In this step, you gave the Service Account you are going to use on
CircleCI the necessary authorization to do meaningful actions on your
cluster like listing, creating, and updating resources. Now it’s time to
create your sample application.
~/do-sample-app/Dockerfile
FROM nginx:1.14
~/do-sample-app/index.html
<!DOCTYPE html>
<title>DigitalOcean</title>
<body>
Kubernetes Sample Application
</body>
This HTML will display a simple message that will let you know if your
application is working.
You can test if the image is correct by building and then running it.
First, build the image with the following command, replacing
dockerhub-username with your own Docker Hub username. You must
specify your username here so when you push it later on to Docker Hub it
will just work:
docker build ~/do-sample-app/ -t dockerhub-
username/do-kubernetes-sample-app
Now run the image. Use the following command, which starts your
image and forwards any local traffic on port 8080 to the port 80 inside
the image, the port Nginx listens to by default:
docker run --rm -it -p 8080:80 dockerhub-
username/do-kubernetes-sample-app
The command prompt will stop being interactive while the command is
running. Instead you will see the Nginx access logs. If you open
localhost:8080 on any browser it should show an HTML page with
the content of ~/do-sample-app/index.html. In case you don’t
have a browser available, you can open a new terminal window and use the
following curl command to fetch the HTML from the webpage:
curl localhost:8080
You will receive the following output:
Output
<!DOCTYPE html>
<title>DigitalOcean</title>
<body>
Kubernetes Sample Application
</body>
Stop the container (CTRL + C on the terminal where it’s running), and
submit this image to your Docker Hub account. To do this, first log in to
Docker Hub:
docker login
Fill in the required information about your Docker Hub account, then
push the image with the following command (don’t forget to replace the
dockerhub-username with your own):
docker push dockerhub-username/do-kubernetes-
sample-app
You have now pushed your sample application image to your Docker
Hub account. In the next step, you will create a Deployment on your
DOKS cluster from this image.
~/do-sample-app/kube/do-sample-deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: do-kubernetes-sample-app
namespace: default
labels:
app: do-kubernetes-sample-app
spec:
replicas: 1
selector:
matchLabels:
app: do-kubernetes-sample-app
template:
metadata:
labels:
app: do-kubernetes-sample-app
spec:
containers:
- name: do-kubernetes-sample-app
image: dockerhub-username/do-kubernetes-
sample-app:latest
ports:
- containerPort: 80
name: http
Kubernetes deployments are from the API group apps, so the
apiVersion of your manifest is set to apps/v1. On metadata you
added a new field you have not used previously, called
metadata.labels. This is useful to organize your deployments. The
field spec represents the behavior specification of your deployment. A
deployment is responsible for managing one or more pods; in this case it’s
going to have a single replica by the spec.replicas field. That is, it’s
going to create and manage a single pod.
To manage pods, your deployment must know which pods it’s
responsible for. The spec.selector field is the one that gives it that
information. In this case the deployment will be responsible for all pods
with tags app=do-kubernetes-sample-app. The
spec.template field contains the details of the Pod this deployment
will create. Inside the template you also have a
spec.template.metadata field. The labels inside this field must
match the ones used on spec.selector. spec.template.spec is
the specification of the pod itself. In this case it contains a single
container, called do-kubernetes-sample-app. The image of that
container is the image you built previously and pushed to Docker Hub.
This YAML file also tells Kubernetes that this container exposes the
port 80, and gives this port the name http.
To access the port exposed by your Deployment, create a Service.
Make a file named ~/do-sample-app/kube/do-sample-
service.yml and open it with your favorite editor:
nano ~/do-sample-app/kube/do-sample-service.yml
Next, add the following lines to the file:
~/do-sample-app/kube/do-sample-service.yml
apiVersion: v1
kind: Service
metadata:
name: do-kubernetes-sample-app
namespace: default
labels:
app: do-kubernetes-sample-app
spec:
type: ClusterIP
ports:
- port: 80
targetPort: http
name: http
selector:
app: do-kubernetes-sample-app
This file gives your Service the same labels used on your
deployment. This is not required, but it helps to organize your applications
on Kubernetes.
The service resource also has a spec field. The spec.type field is
responsible for the behavior of the service. In this case it’s a ClusterIP,
which means the service is exposed on a cluster-internal IP, and is only
reachable from within your cluster. This is the default spec.type for
services. spec.selector is the label selector criteria that should be
used when picking the pods to be exposed by this service. Since your pod
has the tag app: do-kubernetes-sample-app, you used it here.
spec.ports are the ports exposed by the pod’s containers that you want
to expose from this service. Your pod has a single container which exposes
port 80, named http, so you are using it here as targetPort. The
service exposes that port on port 80 too, with the same name, but you
could have used a different port/name combination than the one from the
container.
With your Service and Deployment manifest files created, you can
now create those resources on your Kubernetes cluster using kubectl:
kubectl apply -f ~/do-sample-app/kube/
You will receive the following output:
Output
deployment.apps/do-kubernetes-sample-app created
service/do-kubernetes-sample-app created
Test if this is working by forwarding one port on your machine to the
port that the service is exposing inside your Kubernetes cluster. You can do
that using kubectl port-forward:
kubectl port-forward $(kubectl get pod --
selector="app=do-kubernetes-sample-app" --output
jsonpath='{.items[0].metadata.name}') 8080:80
The subshell command $(kubectl get pod --
selector="app=do-kubernetes-sample-app" --output
jsonpath='{.items[0].metadata.name}') retrieves the name
of the pod matching the tag you used. Otherwise you could have retrieved
it from the list of pods by using kubectl get pods.
After you run port-forward, the shell will stop being interactive,
and will instead output the requests redirected to your cluster:
Output
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80
Opening localhost:8080 on any browser should render the same
page you saw when you ran the container locally, but it’s now coming
from your Kubernetes cluster! As before, you can also use curl in a new
terminal window to check if it’s working:
curl localhost:8080
You will receive the following output:
Output
<!DOCTYPE html>
<title>DigitalOcean</title>
<body>
Kubernetes Sample Application
</body>
Next, it’s time to push all the files you created to your GitHub
repository. To do this you must first create a repository on GitHub called
digital-ocean-kubernetes-deploy.
In order to keep this repository simple for demonstration purposes, do
not initialize the new repository with a README, license, or
.gitignore file when asked on the GitHub UI. You can add these files
later on.
With the repository created, point your local repository to the one on
GitHub. To do this, press CTRL + C to stop kubectl port-forward
and get the command line back, then run the following commands to add a
new remote called origin:
cd ~/do-sample-app/
git remote add origin https://github.com/your-
github-account-username/digital-ocean-kubernetes-
deploy.git
There should be no output from the preceding command.
Next, commit all the files you created up to now to the GitHub
repository. First, add the files:
git add --all
Next, commit the files to your repository, with a commit message in
quotation marks:
git commit -m "initial commit"
This will yield output similar to the following:
Output
[master (root-commit) db321ad] initial commit
4 files changed, 47 insertions(+)
create mode 100644 Dockerfile
create mode 100644 index.html
create mode 100644 kube/do-sample-deployment.yml
create mode 100644 kube/do-sample-service.yml
Finally, push the files to GitHub:
git push -u origin master
You will be prompted for your username and password. Once you have
entered this, you will see output like this:
Output
Counting objects: 7, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (7/7), done.
Writing objects: 100% (7/7), 907 bytes | 0
bytes/s, done.
Total 7 (delta 0), reused 0 (delta 0)
To github.com:your-github-account-
username/digital-ocean-kubernetes-deploy.git
* [new branch] master -> master
Branch master set up to track remote branch master
from origin.
If you go to your GitHub repository page you will now see all the files
there. With your project up on GitHub, you can now set up CircleCI as
your CI/CD tool.
circleci-home-page
You are using GitHub, so click the green Sign Up with GitHub button.
CircleCI will redirect to an authorization page on GitHub. CircleCI
needs some permissions on your account to be able to start building your
projects. This allows CircleCI to obtain your email, deploy keys and
permission to create hooks on your repositories, and add SSH keys to your
account. If you need more information on what CircleCI is going to do
with your data, check their documentation about GitHub integration.
circleci-github-authorization
circleci-project-dashboard
Next, set up your GitHub repository in CircleCI. Click on Set Up New
Projects from the CircleCI Dashboard, or as a shortcut, open the following
link changing the highlighted text with your own GitHub username:
https://circleci.com/setup-project/gh/your-github-
username/digital-ocean-kubernetes-deploy.
After that press Start Building. Do not create a config file in your
repository just yet, and don’t worry if the first build fails.
circleci-start-building
~/do-sample-app/.circleci/config.yml
version: 2.1
jobs:
build:
docker:
- image: circleci/buildpack-deps:stretch
environment:
IMAGE_NAME: dockerhub-username/do-
kubernetes-sample-app
working_directory: ~/app
steps:
- checkout
- setup_remote_docker
- run:
name: Build Docker image
command: |
docker build -t $IMAGE_NAME:latest .
- run:
name: Push Docker Image
command: |
echo "$DOCKERHUB_PASS" | docker login
-u "$DOCKERHUB_USERNAME" --password-stdin
docker push $IMAGE_NAME:latest
workflows:
version: 2
build-master:
jobs:
- build:
filters:
branches:
only: master
This sets up a Workflow with a single job, called build, that runs for
every commit to the master branch. This job is using the image
circleci/buildpack-deps:stretch to run its steps, which is an
image from CircleCI based on the official buildpack-deps Docker
image, but with some extra tools installed, like Docker binaries
themselves.
The workflow has four steps:
checkout retrieves the code from GitHub.
setup_remote_docker sets up a remote, isolated environment
for each build. This is required before you use any docker
command inside a job step. This is necessary because as the steps are
running inside a docker image, setup_remote_docker allocates
another machine to run the commands there.
The first run step builds the image, as you did previously locally.
For that you are using the environment variable you declared in
environment:, IMAGE_NAME (remember to change the
highlighted section with your own information).
The last run step pushes the image to Dockerhub, using the
environment variables you configured on the project settings to
authenticate.
Commit the new file to your repository and push the changes upstream:
cd ~/do-sample-app/
git add .circleci/
git commit -m "add CircleCI config"
git push
This will trigger a new build on CircleCI. The CircleCI workflow is
going to correctly build and push your image to Docker Hub.
CircleCI build page with success build info
Now that you have created and tested your CircleCI workflow, you can
set your DOKS cluster to retrieve the up-to-date image from Docker Hub
and deploy it automatically when changes are made.
~/do-sample-app/.circleci/config.yml:16-22
...
- run:
name: Push Docker Image
command: |
echo "$DOCKERHUB_PASS" | docker login
-u "$DOCKERHUB_USERNAME" --password-stdin
docker tag $IMAGE_NAME:latest
$IMAGE_NAME:$CIRCLE_SHA1
docker push $IMAGE_NAME:latest
docker push $IMAGE_NAME:$CIRCLE_SHA1
...
Save and exit the file.
CircleCI has some special environment variables set by default. One of
them is CIRCLE_SHA1, which contains the hash of the commit it’s
building. The changes you made to ~/do-sample-
app/.circleci/config.yml will use this environment variable to
tag your image with the commit it was built from, always tagging the most
recent build with the latest tag. That way, you always have specific images
available, without overwriting them when you push something new to your
repository.
Next, change your deployment manifest file to point to that file. This
would be simple if inside ~/do-sample-app/kube/do-sample-
deployment.yml you could set your image as dockerhub-
username/do-kubernetes-sample-app:$COMMIT_SHA1, but
kubectl doesn’t do variable substitution inside the manifests when you
use kubectl apply. To account for this, you can use envsubst.
envsubst is a cli tool, part of the GNU gettext project. It allows you to
pass some text to it, and if it finds any variable inside the text that has a
matching environment variable, it’s replaced by the respective value. The
resulting text is then returned as their output.
To use this, you will create a simple bash script which will be
responsible for your deployment. Make a new folder called scripts
inside ~/do-sample-app/:
mkdir ~/do-sample-app/scripts/
Inside that folder create a new bash script called ci-deploy.sh and
open it with your favorite text editor:
nano ~/do-sample-app/scripts/ci-deploy.sh
Inside it write the following bash script:
~/do-sample-app/scripts/ci-deploy.sh
#! /bin/bash
# exit script when any command ran here returns
with non-zero exit code
set -e
COMMIT_SHA1=$CIRCLE_SHA1
./kubectl \
--kubeconfig=/dev/null \
--server=$KUBERNETES_SERVER \
--certificate-authority=cert.crt \
--token=$KUBERNETES_TOKEN \
apply -f ./kube/
Let’s go through this script, using the comments in the file. First, there
is the following:
set -e
This line makes sure any failed command stops the execution of the
bash script. That way if one command fails, the next ones are not
executed.
COMMIT_SHA1=$CIRCLE_SHA1
export COMMIT_SHA1=$COMMIT_SHA1
These lines export the CircleCI $CIRCLE_SHA1 environment variable
with a new name. If you had just declared the variable without exporting it
using export, it would not be visible for the envsubst command.
envsubst <./kube/do-sample-deployment.yml
>./kube/do-sample-deployment.yml.out
mv ./kube/do-sample-deployment.yml.out ./kube/do-
sample-deployment.yml
envsubst cannot do in-place substitution. That is, it cannot read the
content of a file, replace the variables with their respective values, and
write the output back to the same file. Therefore, you will redirect the
output to another file and then overwrite the original file with the new one.
echo "$KUBERNETES_CLUSTER_CERTIFICATE" | base64 --
decode > cert.crt
The environment variable $KUBERNETES_CLUSTER_CERTIFICATE
you created earlier on CircleCI’s project settings is in reality a Base64
encoded string. To use it with kubectl you must decode its contents and
save it to a file. In this case you are saving it to a file named cert.crt
inside the current working directory.
./kubectl \
--kubeconfig=/dev/null \
--server=$KUBERNETES_SERVER \
--certificate-authority=cert.crt \
--token=$KUBERNETES_TOKEN \
apply -f ./kube/
Finally, you are running kubectl. The command has similar
arguments to the one you ran when you were testing your Service Account.
You are calling apply -f ./kube/, since on CircleCI the current
working directory is the root folder of your project. ./kube/ here is your
~/do-sample-app/kube folder.
Save the file and make sure it’s executable:
chmod +x ~/do-sample-app/scripts/ci-deploy.sh
Now, edit ~/do-sample-app/kube/do-sample-
deployment.yml:
nano ~/do-sample-app/kube/do-sample-deployment.yml
Change the tag of the container image value to look like the following
one:
~/do-sample-app/kube/do-sample-deployment.yml
# ...
containers:
- name: do-kubernetes-sample-app
image: dockerhub-username/do-kubernetes-
sample-app:$COMMIT_SHA1
ports:
- containerPort: 80
name: http
Save and close the file. You must now add some new steps to your CI
configuration file to update the deployment on Kubernetes.
Open ~/do-sample-app/.circleci/config.yml on your
favorite text editor:
nano ~/do-sample-app/.circleci/config.yml
Write the following new steps, right below the Push Docker Image
one you had before:
~/do-sample-app/.circleci/config.yml
...
- run:
name: Install envsubst
command: |
sudo apt-get update && sudo apt-get -y
install gettext-base
- run:
name: Install kubectl
command: |
curl -LO
https://storage.googleapis.com/kubernetes-
release/release/$(curl -s
https://storage.googleapis.com/kubernetes-
release/release/stable.txt)/bin/linux/amd64/kubect
l
chmod u+x ./kubectl
- run:
name: Deploy Code
command: ./scripts/ci-deploy.sh
The first two steps are installing some dependencies, first envsubst,
and then kubectl. The Deploy Code step is responsible for running
our deploy script.
To make sure the changes are really going to be reflected on your
Kubernetes deployment, edit your index.html. Change the HTML to
something else, like:
~/do-sample-app/index.html
<!DOCTYPE html>
<title>DigitalOcean</title>
<body>
Automatic Deployment is Working!
</body>
Once you have saved the above change, commit all the modified files to
the repository, and push the changes upstream:
cd ~/do-sample-app/
git add --all
git commit -m "add deploy script and add new steps
to circleci config"
git push
You will see the new build running on CircleCI, and successfully
deploying the changes to your Kubernetes cluster.
Wait for the build to finish, then run the same command you ran
previously:
kubectl port-forward $(kubectl get pod --
selector="app=do-kubernetes-sample-app" --output
jsonpath='{.items[0].metadata.name}') 8080:80
Make sure everything is working by opening your browser on the URL
localhost:8080 or by making a curl request to it. It should show the
updated HTML:
curl localhost:8080
You will receive the following output:
Output
<!DOCTYPE html>
<title>DigitalOcean</title>
<body>
Automatic Deployment is Working!
</body>
Congratulations, you have set up automated deployment with CircleCI!
Conclusion
This was a basic tutorial on how to do deployments to DigitalOcean
Kubernetes using CircleCI. From here, you can improve your pipeline in
many ways. The first thing you can do is create a single build job for
multiple deployments, each one deploying to different Kubernetes clusters
or different namespaces. This can be extremely useful when you have
different Git branches for development/staging/production environments,
ensuring that the deployments are always separated.
You could also build your own image to be used on CircleCI, instead of
using buildpack-deps. This image could be based on it, but could
already have kubectl and envsubst dependencies installed.
If you would like to learn more about CI/CD on Kubernetes, check out
the tutorials for our CI/CD on Kubernetes Webinar Series, or for more
information about apps on Kubernetes, see Modernizing Applications for
Kubernetes.
How To Set Up a CD Pipeline with
Spinnaker on DigitalOcean Kubernetes
Written by Savic
In this tutorial, you’ll deploy Spinnaker, an open-source resource
management and continuous delivery application, to your Kubernetes
cluster. Spinnaker enables automated application deployments to many
platforms and can integrate with other DevOps tools, like Jenkins and
TravisCI. Additionally, it can be configured to monitor code repositories
and Docker registries for completely automated Continuous Delivery
development and deployment processes.
By the end of this tutorial you will be able to manage applications and
development processes on your Kubernetes cluster using Spinnaker. You
will automate the start of your deployment pipelines using triggers, such
as, when a new Docker image has been added to your private registry, or
when new code is pushed to a git repository.
The author selected the Free and Open Source Fund to receive a
donation as part of the Write for DOnations program.
Spinnaker is an open-source resource management and continuous
delivery application for fast, safe, and repeatable deployments, using a
powerful and customizable pipeline system. Spinnaker allows for
automated application deployments to many platforms, including
DigitalOcean Kubernetes. When deploying, you can configure Spinnaker
to use built-in deployment strategies, such as Highlander and Red/black,
with the option of creating your own deployment strategy. It can integrate
with other DevOps tools, like Jenkins and TravisCI, and can be configured
to monitor GitHub repositories and Docker registries.
Spinnaker is managed by Halyard, a tool specifically built for
configuring and deploying Spinnaker to various platforms. Spinnaker
requires external storage for persisting your application’s settings and
pipelines. It supports different platforms for this task, like DigitalOcean
Spaces.
In this tutorial, you’ll deploy Spinnaker to DigitalOcean Kubernetes
using Halyard, with DigitalOcean Spaces as the underlying back-end
storage. You’ll also configure Spinnaker to be available at your desired
domain, secured using Let’s Encrypt TLS certificates. Then, you will
create a sample application in Spinnaker, create a pipeline, and deploy a
Hello World app to your Kubernetes cluster. After testing it, you’ll
introduce authentication and authorization via GitHub Organizations. By
the end, you will have a secured and working Spinnaker deployment in
your Kubernetes cluster.
Note: This tutorial has been specifically tested with Spinnaker 1.13.5.
Prerequisites
Output
+ Get current deployment
Success
+ Edit the kubernetes provider
Success
Problems in default.provider.kubernetes:
- WARNING Provider kubernetes is enabled, but no
accounts have been
configured.
+ Successfully enabled kubernetes
Halyard logged all the steps it took to enable the Kubernetes provider,
and warned that no accounts are defined yet.
Next, you’ll create a Kubernetes service account for Spinnaker, along
with RBAC. A service account is a type of account that is scoped to a
single namespace. It is used by software, which may perform various tasks
in the cluster. RBAC (Role Based Access Control) is a method of
regulating access to resources in a Kubernetes cluster. It limits the scope
of action of the account to ensure that no important configurations are
inadvertently changed on your cluster.
Here, you will grant Spinnaker cluster-admin permissions to allow
it to control the whole cluster. If you wish to create a more restrictive
environment, consult the official Kubernetes documentation on RBAC.
First, create the spinnaker namespace by running the following
command:
kubectl create ns spinnaker
The output will look like:
Output
namespace/spinnaker created
Run the following command to create a service account named
spinnaker-service-account:
kubectl create serviceaccount spinnaker-service-
account -n spinnaker
You’ve used the -n flag to specify that kubectl create the service
account in the spinnaker namespace. The output will be:
Output
serviceaccount/spinnaker-service-account created
Then, bind it to the cluster-admin role:
kubectl create clusterrolebinding spinnaker-
service-account --clusterrole cluster-admin --
serviceaccount=spinnaker:spinnaker-service-account
You will see the following output:
Output
clusterrolebinding.rbac.authorization.k8s.io/spinn
aker-service-account created
Halyard uses the local kubectl to access the cluster. You’ll need to
configure it to use the newly created service account before deploying
Spinnaker. Kubernetes accounts authenticate using usernames and tokens.
When a service account is created, Kubernetes makes a new secret and
populates it with the account token. To retrieve the token for the
spinnaker-service-account, you’ll first need to get the name of
the secret. You can fetch it into a console variable, named
TOKEN_SECRET, by running:
TOKEN_SECRET=$(kubectl get serviceaccount -n
spinnaker spinnaker-service-account -o
jsonpath='{.secrets[0].name}')
This gets information about the spinnaker-service-account
from the namespace spinnaker, and fetches the name of the first secret
it contains by passing in a JSON path.
Fetch the contents of the secret into a variable named TOKEN by
running:
TOKEN=$(kubectl get secret -n spinnaker
$TOKEN_SECRET -o jsonpath='{.data.token}' | base64
--decode)
You now have the token available in the environment variable TOKEN.
Next, you’ll need to set credentials for the service account in kubectl:
kubectl config set-credentials spinnaker-token-
user --token $TOKEN
You will see the following output:
Output
User "spinnaker-token-user" set.
Then, you’ll need to set the user of the current context to the newly
created spinnaker-token-user by running the following command:
kubectl config set-context --current --user
spinnaker-token-user
By setting the current user to spinnaker-token-user, kubectl is
now configured to use the spinnaker-service-account, but
Halyard does not know anything about that. Add an account to its
Kubernetes provider by executing:
hal config provider kubernetes account add
spinnaker-account --provider-version v2
The output will look like this:
Output
+ Get current deployment
Success
+ Add the spinnaker-account account
Success
+ Successfully added account spinnaker-account for
provider
kubernetes.
This commmand adds a Kubernetes account to Halyard, named
spinnaker-account, and marks it as a service account.
Generally, Spinnaker can be deployed in two ways: distributed
installation or local installation. Distributed installation is what you’re
completing in this tutorial—you’re deploying it to the cloud. Local
installation, on the other hand, means that Spinnaker will be downloaded
and installed on the machine Halyard runs on. Because you’re deploying
Spinnaker to Kubernetes, you’ll need to mark the deployment as
distributed, like so:
hal config deploy edit --type distributed --
account-name spinnaker-account
Since your Spinnaker deployment will be building images, it is
necessary to enable artifacts in Spinnaker. You can enable them by
running the following command:
hal config features edit --artifacts true
Here you’ve enabled artifacts to allow Spinnaker to store more
metadata about the objects it creates.
You’ve added a Kubernetes account to Spinnaker, via Halyard. You
enabled the Kubernetes provider, configured RBAC roles, and added the
current kubectl config to Spinnaker, thus adding an account to the
provider. Now you’ll set up your back-end storage.
Output
+ Get current deployment
Success
+ Get persistent store
Success
+ Edit persistent store
Success
+ Successfully edited persistent store "s3".
Now that you’ve configured s3 storage, you’ll ensure that your
deployment will use this as its storage by running the following command:
hal config storage edit --type s3
The output will look like this:
Output
+ Get current deployment
Success
+ Get persistent storage settings
Success
+ Edit persistent storage settings
Success
+ Successfully edited persistent storage.
You’ve set up your Space as the underlying storage that your instance of
Spinnaker will use. Now you’ll deploy Spinnaker to your Kubernetes
cluster and expose it at your domains using the Nginx Ingress Controller.
Output
+ Get current deployment
Success
+ Get API security settings
Success
+ Edit API security settings
Success
...
To set the UI endpoint to your domain, which is where you will access
Spinnaker, run:
hal config security ui edit --override-base-url
https://spinnaker.example.com
The output will look like:
Output
+ Get current deployment
Success
+ Get UI security settings
Success
+ Edit UI security settings
Success
+ Successfully updated UI security settings.
Remember to replace spinnaker-api.example.com and
spinnaker.example.com with your domains. These are the domains
you have pointed to the Load Balancer that you created during the Nginx
Ingress Controller prerequisite.
You’ve created and secured Spinnaker’s Kubernetes account, configured
your Space as its underlying storage, and set its UI and API endpoints to
your domains. Now you can list the available Spinnaker versions:
hal version list
Your output will show a list of available versions. At the time of writing
this article 1.13.5 was the latest version:
Output
+ Get current deployment
Success
+ Get Spinnaker version
Success
+ Get released versions
Success
+ You are on version "", and the following are
available:
- 1.11.12 (Cobra Kai):
Changelog: https://gist.GitHub.com/spinnaker-
release/29a01fa17afe7c603e510e202a914161
Published: Fri Apr 05 14:55:40 UTC 2019
(Requires Halyard >= 1.11)
- 1.12.9 (Unbreakable):
Changelog: https://gist.GitHub.com/spinnaker-
release/7fa9145349d6beb2f22163977a94629e
Published: Fri Apr 05 14:11:44 UTC 2019
(Requires Halyard >= 1.11)
- 1.13.5 (BirdBox):
Changelog: https://gist.GitHub.com/spinnaker-
release/23af06bc73aa942c90f89b8e8c8bed3e
Published: Mon Apr 22 14:32:29 UTC 2019
(Requires Halyard >= 1.17)
To select a version to install, run the following command:
hal config version edit --version 1.13.5
It is recommended to always select the latest version, unless you
encounter some kind of regression.
You will see the following output:
Output
+ Get current deployment
Success
+ Edit Spinnaker version
Success
+ Spinnaker has been configured to update/install
version "version".
Deploy this version of Spinnaker with `hal
deploy apply`.
You have now fully configured Spinnaker’s deployment. You’ll deploy
it with the following command:
hal deploy apply
This command could take a few minutes to finish.
The final output will look like this:
Output
+ Get current deployment
Success
+ Prep deployment
Success
+ Preparation complete... deploying Spinnaker
+ Get current deployment
Success
+ Apply deployment
Success
+ Deploy spin-redis
Success
+ Deploy spin-clouddriver
Success
+ Deploy spin-front50
Success
+ Deploy spin-orca
Success
+ Deploy spin-deck
Success
+ Deploy spin-echo
Success
+ Deploy spin-gate
Success
+ Deploy spin-rosco
Success
...
Halyard is showing you the deployment status of each of Spinnaker’s
microservices. Behind the scenes, it calls kubectl to install them.
Kubernetes will take some time—ten minutes on average—to bring all
of the containers up, especially for the first time. You can watch the
progress by running the following command:
kubectl get pods -n spinnaker -w
You’ve deployed Spinnaker to your Kubernetes cluster, but it can’t be
accessed beyond your cluster.
You’ll be storing the ingress configuration in a file named
spinnaker-ingress.yaml. Create it using your text editor:
nano spinnaker-ingress.yaml
Add the following lines:
spinnaker-ingress.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: spinnaker-ingress
namespace: spinnaker
annotations:
kubernetes.io/ingress.class: nginx
certmanager.k8s.io/cluster-issuer:
letsencrypt-prod
spec:
tls:
- hosts:
- spinnaker-api.example.com
- spinnaker.example.com
secretName: spinnaker
rules:
- host: spinnaker-api.example.com
http:
paths:
- backend:
serviceName: spin-gate
servicePort: 8084
- host: spinnaker.example.com
http:
paths:
- backend:
serviceName: spin-deck
servicePort: 9000
Remember to replace spinnaker-api.example.com with your
API domain, and spinnaker.example.com with your UI domain.
The configuration file defines an ingress called spinnaker-
ingress. The annotations specify that the controller for this ingress will
be the Nginx controller, and that the letsencrypt-prod cluster issuer
will generate the TLS certificates, defined in the prerequisite tutorial.
Then, it specifies that TLS will secure the UI and API domains. It sets
up routing by directing the API domain to the spin-gate service
(Spinnaker’s API containers), and the UI domain to the spin-deck
service (Spinnaker’s UI containers) at the appropriate ports 8084 and
9000.
Save and close the file.
Create the Ingress in Kubernetes by running:
kubectl create -f spinnaker-ingress.yaml
You’ll see the following output:
Output
ingress.extensions/spinnaker-ingress created
Wait a few minutes for Let’s Encrypt to provision the TLS certificates,
and then navigate to your UI domain, spinnaker.example.com, in a
browser. You will see Spinnaker’s user interface.
You can now navigate to the domain you defined in the configuration.
You will see the Hello World app, which Spinnaker just deployed.
Hello World App
Output
+ Get current deployment
Success
+ Get authentication settings
Success
+ Edit oauth2 authentication settings
Success
+ Successfully edited oauth2 method.
To set up OAuth authentication with GitHub, you’ll need to create an
OAuth application for your Organization. To do so, navigate to your
Organization on GitHub, go to Settings, click on Developer Settings, and
then select OAuth Apps from the left-hand menu. Afterward, click the New
OAuth App button on the right. You will see the Register a new OAuth
application form.
Creating a new OAuth App on GitHub
Output
+ Get current deployment
Success
+ Get authentication settings
Success
+ Edit oauth2 authentication settings
Success
Problems in default.security.authn:
- WARNING An authentication method is fully or
partially
configured, but not enabled. It must be enabled
to take effect.
Output
+ Get current deployment
Success
+ Edit oauth2 authentication settings
Success
+ Successfully enabled oauth2
You’ve configured and enabled GitHub OAuth authentication. Now
users will be forced to log in via GitHub in order to access Spinnaker.
However, right now, everyone who has a GitHub account can log in, which
is not what you want. To overcome this, you’ll configure Spinnaker to
restrict access to members of your desired Organization.
You’ll need to set this up semi-manually via local config files, because
Halyard does not yet have a command for setting this. During deployment,
Halyard will use the local config files to override the generated
configuration.
Halyard looks for custom configuration under
~/.hal/default/profiles/. Files named service-name-
*.yml are picked up by Halyard and used to override the settings of a
particular service. The service that you’ll override is called gate, and
serves as the API gateway for the whole of Spinnaker.
Create a file under ~/.hal/default/profiles/ named gate-
local.yml:
nano ~/.hal/default/profiles/gate-local.yml
Add the following lines:
gate-local.yml
security:
oauth2:
providerRequirements:
type: GitHub
organization: your_organization_name
Replace your_organization_name with the name of your GitHub
Organization. Save and close the file.
With this bit of configuration, only members of your GitHub
Organization will be able to access Spinnaker.
Note: Only those members of your GitHub Organization whose
membership is set to Public will be able to log in to Spinnaker. This
setting can be changed on the member list page of your Organization.
Now, you’ll integrate Spinnaker with an even more particular access-
rule solution: GitHub Teams. This will enable you to specify which
Team(s) will have access to resources created in Spinnaker, such as
applications.
To achieve this, you’ll need to have a GitHub Personal Access Token for
an admin account in your Organization. To create one, visit Personal
Access Tokens and press the Generate New Token button. On the next
page, give it a description of your choice and be sure to check the read:org
scope, located under admin:org. When you are done, press Generate token
and note it down when it appears—you won’t be able to see it again.
To configure GitHub Teams role authorization in Spinnaker, run the
following command:
hal config security authz github edit --
accessToken access_token --organization
organization_name --baseUrl https://api.github.com
Be sure to replace access_token with your personal access token
you generated and replace organization_name with the name of the
Organization.
The output will be:
Output
+ Get current deployment
Success
+ Get GitHub group membership settings
Success
+ Edit GitHub group membership settings
Success
+ Successfully edited GitHub method.
You’ve updated your GitHub group settings. Now, you’ll set the
authorization provider to GitHub by running the following command:
hal config security authz edit --type github
The output will look like:
Output
+ Get current deployment
Success
+ Get group membership settings
Success
+ Edit group membership settings
Success
+ Successfully updated roles.
After updating these settings, enable them by running:
hal config security authz enable
You’ll see the following output:
Output
+ Get current deployment
Success
+ Edit authorization settings
Success
+ Successfully enabled authorization
With all the changes in place, you can now apply the changes to your
running Spinnaker deployment. Execute the following command to do
this:
hal deploy apply
Once it has finished, wait for Kubernetes to propagate the changes. This
can take quite some time—you can watch the progress by running:
kubectl get pods -n spinnaker -w
When all the pods’ states become Running and availability 1/1,
navigate to your Spinnaker UI domain. You will be redirected to GitHub
and asked to log in, if you’re not already. If the account you logged in with
is a member of the Organization, you will be redirected back to Spinnaker
and logged in. Otherwise, you will be denied access with a message that
looks like this:
{"error":"Unauthorized", "message":"Authentication
The effect of GitHub Teams integration is that Spinnaker now translates
them into roles. You can use these roles in Spinnaker to incorporate
additional restrictions to access for members of particular teams. If you
try to add another application, you’ll notice that you can now also specify
permissions, which combine the level of access—read only or read and
write—with a role, for that application.
You’ve set up GitHub authentication and authorization. You have also
configured Spinnaker to restrict access to members of your Organization,
learned about roles and permissions, and considered the place of GitHub
Teams when integrated with Spinnaker.
Conclusion
You have successfully configured and deployed Spinnaker to your
DigitalOcean Kubernetes cluster. You can now manage and use your cloud
resources more easily, from a central place. You can use triggers to
automatically start a pipeline; for example, when a new Docker image has
been added to the registry. To learn more about Spinnaker’s terms and
architecture, visit the official documentation. If you wish to deploy a
private Docker registry to your cluster to hold your images, visit How To
Set Up a Private Docker Registry on Top of DigitalOcean Spaces and Use
It with DO Kubernetes.
Kubernetes Networking Under the Hood
Pod Networking
In Kubernetes, a pod is the most basic unit of organization: a group of
tightly-coupled containers that are all closely related and perform a single
function or service.
Networking-wise, Kubernetes treats pods similar to a traditional virtual
machine or a single bare-metal host: each pod receives a single unique IP
address, and all containers within the pod share that address and
communicate with each other over the lo loopback interface using the
localhost hostname. This is achieved by assigning all of the pod’s
containers to the same network stack.
This situation should feel familiar to anybody who has deployed
multiple services on a single host before the days of containerization. All
the services need to use a unique port to listen on, but otherwise
communication is uncomplicated and has low overhead.
Pod to Pod Networking
Most Kubernetes clusters will need to deploy multiple pods per node. Pod
to pod communication may happen between two pods on the same node, or
between two different nodes.
On a single node you can have multiple pods that need to communicate
directly with each other. Before we trace the route of a packet between
pods, let’s inspect the networking setup of a node. The following diagram
provides an overview, which we will walk through in detail:
Because each pod in a cluster has a unique IP, and every pod can
communicate directly with all other pods, a packet moving between pods
on two different nodes is very similar to the previous scenario.
Let’s trace a packet from pod1 to pod3, which is on a different node:
Now that we are familiar with how packets are routed via pod IP
addresses, let’s take a look at Kubernetes services and how they build on
top of this infrastructure.
IPs
When the packet returns to node1 the VIP to pod IP translation will be
reversed, and the packet will be back through the bridge and virtual
interface to the correct pod.
Conclusion
In this article we’ve reviewed the internal networking infrastructure of a
Kubernetes cluster. We’ve discussed the building blocks that make up the
network, and detailed the hop-by-hop journey of packets in different
scenarios.
For more information about Kubernetes, take a look at our Kubernetes
tutorials tag and the official Kubernetes documentation.
How To Inspect Kubernetes Networking
Getting Started
This tutorial will assume that you have a Kubernetes cluster, with
kubectl installed locally and configured to connect to the cluster.
The following sections contain many commands that are intended to be
run on a Kubernetes node. They will look like this:
echo 'this is a node command'
Commands that should be run on your local machine will have the
following appearance:
echo 'this is a local command'
Note: Most of the commands in this tutorial will need to be run as the
root user. If you instead use a sudo-enabled user on your Kubernetes
nodes, please add sudo to run the commands when necessary.
Output
NAME READY STATUS
RESTARTS AGE IP NODE
hello-world-5b446dd74b-7c7pk 1/1 Running
0 22m 10.244.18.4 node-one
hello-world-5b446dd74b-pxtzt 1/1 Running
0 22m 10.244.3.4 node-two
The IP column will contain the internal cluster IP address for each pod.
If you don’t see the pod you’re looking for, make sure you’re in the
right namespace. You can list all pods in all namespaces by adding the flag
--all-namespaces.
Finding a Service’s IP
We can find a Service IP using kubectl as well. In this case we will list
all services in all namespaces:
kubectl get service --all-namespaces
Output
NAMESPACE NAME TYPE
CLUSTER-IP EXTERNAL-IP PORT(S) AGE
default kubernetes ClusterIP
10.32.0.1 <none> 443/TCP 6d
kube-system csi-attacher-doplugin ClusterIP
10.32.159.128 <none> 12345/TCP 6d
kube-system csi-provisioner-doplugin ClusterIP
10.32.61.61 <none> 12345/TCP 6d
kube-system kube-dns ClusterIP
10.32.0.10 <none> 53/UDP,53/TCP 6d
kube-system kubernetes-dashboard ClusterIP
10.32.226.209 <none> 443/TCP 6d
The service IP can be found in the CLUSTER-IP column.
Output
CONTAINER ID IMAGE
COMMAND CREATED
STATUS PORTS NAMES
173ee46a3926 gcr.io/google-samples/node-
hello "/bin/sh -c 'node se…" 9 days ago
Up 9 days k8s_hello-
world_hello-world-5b446dd74b-
pxtzt_default_386a9073-7e35-11e8-8a3d-
bae97d2c1afd_0
11ad51cb72df k8s.gcr.io/pause-amd64:3.1
"/pause" 9 days ago Up 9
days k8s_POD_hello-
world-5b446dd74b-pxtzt_default_386a9073-7e35-11e8-
8a3d-bae97d2c1afd_0
. . .
Find the container ID or name of any container in the pod you’re
interested in. In the above output we’re showing two containers:
The first container is the hello-world app running in the hello-
world pod
The second is a pause container running in the hello-world pod.
This container exists solely to hold onto the pod’s network namespace
Output
14552
A process ID (or PID) will be output. Now we can use the nsenter
program to run a command in that process’s network namespace:
nsenter -t your-container-pid -n ip addr
Be sure to use your own PID, and replace ip addr with the command
you’d like to run inside the pod’s network namespace.
Note: One advantage of using nsenter to run commands in a pod’s
namespace – versus using something like docker exec – is that you
have access to all of the commands available on the node, instead of the
typically limited set of commands installed in containers.
Output
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc
noqueue state UNKNOWN group default qlen 1
link/loopback 00:00:00:00:00:00 brd
00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
10: eth0@if11: <BROADCAST,MULTICAST,UP,LOWER_UP>
mtu 1450 qdisc noqueue state UP group default
link/ether 02:42:0a:f4:03:04 brd
ff:ff:ff:ff:ff:ff link-netnsid 0
inet 10.244.3.4/24 brd 10.244.3.255 scope
global eth0
valid_lft forever preferred_lft forever
The command will output a list of the pod’s interfaces. Note the if11
number after eth0@ in the example output. This means this pod’s eth0
is linked to the node’s 11th interface. Now run ip addr in the node’s
default namespace to list out its interfaces:
ip addr
Output
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc
noqueue state UNKNOWN group default qlen 1
link/loopback 00:00:00:00:00:00 brd
00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
. . .
7: veth77f2275@if6:
<BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc
noqueue master docker0 state UP group default
link/ether 26:05:99:58:0d:b9 brd
ff:ff:ff:ff:ff:ff link-netnsid 0
inet6 fe80::2405:99ff:fe58:db9/64 scope link
valid_lft forever preferred_lft forever
9: vethd36cef3@if8:
<BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc
noqueue master docker0 state UP group default
link/ether ae:05:21:a2:9a:2b brd
ff:ff:ff:ff:ff:ff link-netnsid 1
inet6 fe80::ac05:21ff:fea2:9a2b/64 scope link
valid_lft forever preferred_lft forever
11: veth4f7342d@if10:
<BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc
noqueue master docker0 state UP group default
link/ether e6:4d:7b:6f:56:4c brd
ff:ff:ff:ff:ff:ff link-netnsid 2
inet6 fe80::e44d:7bff:fe6f:564c/64 scope link
valid_lft forever preferred_lft forever
The 11th interface is veth4f7342d in this example output. This is the
virtual ethernet pipe to the pod we’re investigating.
/var/log/syslog
Jul 12 15:32:11 worker-528 kernel: nf_conntrack:
table full, dropping packet.
There is a sysctl setting for the maximum number of connections to
track. You can list out your current value with the following command:
sysctl net.netfilter.nf_conntrack_max
Output
net.netfilter.nf_conntrack_max = 131072
To set a new value, use the -w flag:
sysctl -w net.netfilter.nf_conntrack_max=198000
To make this setting permanent, add it to the sysctl.conf file:
/etc/sysctl.conf
. . .
net.ipv4.netfilter.ip_conntrack_max = 198000
Output
Chain KUBE-SERVICES (2 references)
target prot opt source
destination
KUBE-SVC-TCOU7JCQXEZGVUNU udp -- anywhere
10.32.0.10 /* kube-system/kube-dns:dns
cluster IP */ udp dpt:domain
KUBE-SVC-ERIFXISQEP7F7OF4 tcp -- anywhere
10.32.0.10 /* kube-system/kube-dns:dns-
tcp cluster IP */ tcp dpt:domain
KUBE-SVC-XGLOHA7QRQ3V22RZ tcp -- anywhere
10.32.226.209 /* kube-system/kubernetes-
dashboard: cluster IP */ tcp dpt:https
. . .
Output
NAME TYPE CLUSTER-IP EXTERNAL-IP
PORT(S) AGE
kube-dns ClusterIP 10.32.0.10 <none>
53/UDP,53/TCP 15d
The cluster IP is highlighted above. Next we’ll use nsenter to run
dig in the a container namespace. Look at the section Finding and
Entering Pod Network Namespaces for more information on this:
nsenter -t 14346 -n dig
kubernetes.default.svc.cluster.local @10.32.0.10
This dig command looks up the Service’s full domain name of service-
name.namespace.svc.cluster.local and specifics the IP of the cluster DNS
service IP (@10.32.0.10).
Output
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight
ActiveConn InActConn
TCP 100.64.0.10:53 rr
-> 100.96.1.3:53 Masq 1 0
0
-> 100.96.1.4:53 Masq 1 0
0
Conclusion
In this article we’ve reviewed some commands and techniques for
exploring and inspecting the details of your Kubernetes cluster’s
networking. For more information about Kubernetes, take a look at our
Kubernetes tutorials tag and the official Kubernetes documentation.
An Introduction to Service Meshes
Service discovery
Routing and traffic configuration
Encryption and authentication/authorization
Metrics and monitoring
Service Discovery
In a distributed framework, it’s necessary to know how to connect to
services and whether or not they are available. Service instance locations
are assigned dynamically on the network and information about them is
constantly changing as containers are created and destroyed through
autoscaling, upgrades, and failures.
Historically, there have been a few tools for doing service discovery in a
microservice framework. Key-value stores like etcd were paired with other
tools like Registrator to offer service discovery solutions. Tools like
Consul iterated on this by combining a key-value store with a DNS
interface that allows users to work directly with their DNS server or node.
Taking a similar approach, Kubernetes offers DNS-based service
discovery by default. With it, you can look up services and service ports,
and do reverse IP lookups using common DNS naming conventions. In
general, an A record for a Kubernetes service matches this pattern:
service.namespace.svc.cluster.local. Let’s look at how this
works in the context of the Bookinfo application. If, for example, you
wanted information on the details service from the Bookinfo app, you
could look at the relevant entry in the Kubernetes dashboard:
Details Service in Kubernetes Dash
This will give you relevant information about the Service name,
namespace, and ClusterIP, which you can use to connect with your
Service even as individual containers are destroyed and recreated.
A service mesh like Istio also offers service discovery capabilities. To
do service discovery, Istio relies on communication between the
Kubernetes API, Istio’s own control plane, managed by the traffic
management component Pilot, and its data plane, managed by Envoy
sidecar proxies. Pilot interprets data from the Kubernetes API server to
register changes in Pod locations. It then translates that data into a
canonical Istio representation and forwards it onto the sidecar proxies.
This means that service discovery in Istio is platform agnostic, which
we can see by using Istio’s Grafana add-on to look at the details
service again in Istio’s service dashboard:
Details Service Istio Dash
Similarly, you can use a visualization tool like Weave Scope to see the
relationship between your Services at a given time. The Bookinfo
application without advanced routing might look like this:
Conclusion
Microservice architectures are designed to make application development
and deployment fast and reliable. Yet an increase in inter-service
communication has changed best practices for certain administrative
tasks. This article discusses some of those tasks, how they are handled in a
Kubernetes-native context, and how they can be managed using a service
mesh — in this case, Istio.
For more information on some of the Kubernetes topics covered here,
please see the following resources: - How to Set Up an Nginx Ingress with
Cert-Manager on DigitalOcean Kubernetes. - How To Set Up an
Elasticsearch, Fluentd and Kibana (EFK) Logging Stack on Kubernetes. -
An Introduction to the Kubernetes DNS Service.
Additionally, the Kubernetes and Istio documentation hubs are great
places to find detailed information about the topics discussed here.
How To Back Up and Restore a
Kubernetes Cluster on DigitalOcean
Using Velero
Prerequisites
Before you begin this tutorial, you should have the following available to
you:
On your local computer: - The kubectl command-line tool,
configured to connect to your cluster. You can read more about installing
and configuring kubectl in the official Kubernetes documentation. - The
git command-line utility. You can learn how to install git in Getting
Started with Git.
In your DigitalOcean account: - A DigitalOcean Kubernetes cluster, or a
Kubernetes cluster (version 1.7.5 or later) on DigitalOcean Droplets. - A
DNS server running inside of your cluster. If you are using DigitalOcean
Kubernetes, this is running by default. To learn more about configuring a
Kubernetes DNS service, consult Customizing DNS Service from the
official Kuberentes documentation. - A DigitalOcean Space that will store
your backed-up Kubernetes objects. To learn how to create a Space,
consult the Spaces product documentation. - An access key pair for your
DigitalOcean Space. To learn how to create a set of access keys, consult
How to Manage Administrative Access to Spaces . - A personal access
token for use with the DigitalOcean API. To learn how to create a personal
access token, consult How to Create a Personal Access Token. Ensure that
the token you create or use has Read/Write permissions or snapshots
will not work.
Once you have all of this set up, you’re ready to begin with this guide.
Step 1 — Installing the Velero Client
The Velero backup tool consists of a client installed on your local
computer and a server that runs in your Kubernetes cluster. To begin, we’ll
install the local Velero client.
In your web browser, navigate to the Velero GitHub repo releases page,
find the release corresponding to your OS and system architecture, and
copy the link address. For the purposes of this guide, we’ll use an Ubuntu
18.04 server on an x86-64 (or AMD64) processor as our local machine,
and the Velero v1.2.0 release.
Note: To follow this guide, you should download and install v1.2.0 of
the Velero client.
Then, from the command line on your local computer, navigate to the
temporary /tmp directory and cd into it:
cd /tmp
Use wget and the link you copied earlier to download the release
tarball:
wget https://link_copied_from_release_page
Once the download completes, extract the tarball using tar (note the
filename may differ depending on the release version and your OS):
tar -xvzf velero-v1.2.0-linux-amd64.tar.gz
The /tmp directory should now contain the extracted velero-
v1.2.0-linux-amd64 directory as well as the tarball you just
downloaded.
Verify that you can run the velero client by executing the binary:
./velero-v1.2.0-linux-amd64/velero help
You should see the following help output:
Output
Velero is a tool for managing disaster recovery,
specifically for Kubernetes
cluster resources. It provides a simple,
configurable, and operationally robust
way to back up your application state and
associated data.
Usage:
velero [command]
Available Commands:
backup Work with backups
backup-location Work with backup storage
locations
bug Report a Velero bug
client Velero client related commands
completion Output shell completion code
for the specified shell (bash or zsh)
create Create velero resources
delete Delete velero resources
describe Describe velero resources
get Get velero resources
help Help about any command
install Install Velero
plugin Work with plugins
restic Work with restic
restore Work with restores
schedule Work with schedules
snapshot-location Work with snapshot locations
version Print the velero version and
associated image
. . .
At this point you should move the velero executable out of the
temporary /tmp directory and add it to your PATH. To add it to your
PATH on an Ubuntu system, simply copy it to /usr/local/bin:
sudo mv velero-v1.2.0-linux-amd64/velero
/usr/local/bin/velero
You’re now ready to configure secrets for the Velero server and then
deploy it to your Kubernetes cluster.
/tmp/velero-plugin-1.0.0/examples/cloud-credentials
[default]
aws_access_key_id=<AWS_ACCESS_KEY_ID>
aws_secret_access_key=<AWS_SECRET_ACCESS_KEY>
Edit the <AWS_ACCESS_KEY_ID> and
<AWS_SECRET_ACCESS_KEY> placeholders to use your DigitalOcean
Spaces keys. Be sure to remove the < and > characters.
The next step is to edit the 01-velero-secret.patch.yaml file
so that it includes your DigitalOcean API token. Open the file in your
favourite editor:
nano examples/01-velero-secret.patch.yaml
It should look like this:
---
apiVersion: v1
kind: Secret
stringData:
digitalocean_token: <DIGITALOCEAN_API_TOKEN>
type: Opaque
Change the entire <DIGITALOCEAN_API_TOKEN> placeholder to use
your DigitalOcean personal API token. The line should look something
like digitalocean_token: 18a0d730c0e0..... Again, make
sure to remove the < and > characters.
Once you are ready with the appropriate bucket and backup location
settings, it is time to install Velero. Run the following command,
substituting your values where required:
velero install \
--provider velero.io/aws \
--bucket velero-backups \
--plugins velero/velero-plugin-for-
aws:v1.0.0,digitalocean/velero-plugin:v1.0.0 \
--backup-location-config
s3Url=https://nyc3.digitaloceanspaces.com,region=n
yc3 \
--use-volume-snapshots=false \
--secret-file=./examples/cloud-credentials
You should see the following output:
Output
CustomResourceDefinition/backups.velero.io:
attempting to create resource
CustomResourceDefinition/backups.velero.io:
created
CustomResourceDefinition/backupstoragelocations.ve
lero.io: attempting to create resource
CustomResourceDefinition/backupstoragelocations.ve
lero.io: created
CustomResourceDefinition/deletebackuprequests.vele
ro.io: attempting to create resource
CustomResourceDefinition/deletebackuprequests.vele
ro.io: created
CustomResourceDefinition/downloadrequests.velero.i
o: attempting to create resource
CustomResourceDefinition/downloadrequests.velero.i
o: created
CustomResourceDefinition/podvolumebackups.velero.i
o: attempting to create resource
CustomResourceDefinition/podvolumebackups.velero.i
o: created
CustomResourceDefinition/podvolumerestores.velero.
io: attempting to create resource
CustomResourceDefinition/podvolumerestores.velero.
io: created
CustomResourceDefinition/resticrepositories.velero
.io: attempting to create resource
CustomResourceDefinition/resticrepositories.velero
.io: created
CustomResourceDefinition/restores.velero.io:
attempting to create resource
CustomResourceDefinition/restores.velero.io:
created
CustomResourceDefinition/schedules.velero.io:
attempting to create resource
CustomResourceDefinition/schedules.velero.io:
created
CustomResourceDefinition/serverstatusrequests.vele
ro.io: attempting to create resource
CustomResourceDefinition/serverstatusrequests.vele
ro.io: created
CustomResourceDefinition/volumesnapshotlocations.v
elero.io: attempting to create resource
CustomResourceDefinition/volumesnapshotlocations.v
elero.io: created
Waiting for resources to be ready in cluster...
Namespace/velero: attempting to create resource
Namespace/velero: created
ClusterRoleBinding/velero: attempting to create
resource
ClusterRoleBinding/velero: created
ServiceAccount/velero: attempting to create
resource
ServiceAccount/velero: created
Secret/cloud-credentials: attempting to create
resource
Secret/cloud-credentials: created
BackupStorageLocation/default: attempting to
create resource
BackupStorageLocation/default: created
Deployment/velero: attempting to create resource
Deployment/velero: created
Velero is installed! ⛵ Use 'kubectl logs
deployment/velero -n velero' to view the status.
You can watch the deployment logs using the kubectl command from
the output. Once your deploy is ready, you can proceed to the next step,
which is configuring the server. A successful deploy will look like this
(with a different AGE column):
kubectl get deployment/velero --namespace velero
Output
NAME READY UP-TO-DATE AVAILABLE AGE
velero 1/1 1 1 2m
At this point you have installed the server component of Velero into
your Kubernetes cluster as a Deployment. You have also registered your
Spaces keys with Velero using a Kubernetes Secret.
Note: You can specify the kubeconfig that the velero command
line tool should use with the --kubeconfig flag. If you don’t use this
flag, velero will check the KUBECONFIG environment variable and
then fall back to the kubectl default (~/.kube/config).
Output
Snapshot volume location "default" configured
successfully.
Step 5 — Adding an API token
In the previous step we created block storage and object storage objects in
the Velero server. We’ve registered the digitalocean/velero-
plugin:v1.0.0 plugin with the server, and installed our Spaces secret
keys into the cluster.
The final step is patching the cloud-credentials Secret that we
created earlier to use our DigitalOcean API token. Without this token the
snapshot plugin will not be able to authenticate with the DigitalOcean API.
We could use the kubectl edit command to modify the Velero
Deployment object with a reference to the API token. However, editing
complex YAML objects by hand can be tedious and error prone. Instead,
we’ll use the kubectl patch command since Kubernetes supports
patching objects. Let’s take a quick look at the contents of the patch files
that we’ll apply.
The first patch file is the examples/01-velero-
secret.patch.yaml file that you edited earlier. It is designed to add
your API token to the secrets/cloud-credentials Secret that
already contains your Spaces keys. cat the file:
cat examples/01-velero-secret.patch.yaml
It should look like this (with your token in place of the
<DIGITALOCEAN_API_TOKEN> placeholder):
examples/01-velero-secret.patch.yaml
. . .
---
apiVersion: v1
kind: Secret
stringData:
digitalocean_token: <DIGITALOCEAN_API_TOKEN>
type: Opaque
Now let’s look at the patch file for the Deployment:
cat examples/02-velero-deployment.patch.yaml
You should see the following YAML:
examples/02-velero-deployment.patch.yaml
. . .
---
apiVersion: v1
kind: Deployment
spec:
template:
spec:
containers:
- args:
- server
command:
- /velero
env:
- name: DIGITALOCEAN_TOKEN
valueFrom:
secretKeyRef:
key: digitalocean_token
name: cloud-credentials
name: velero
This file indicates that we’re patching a Deployment’s Pod spec that is
called velero. Since this is a patch we do not need to specify an entire
Kubernetes object spec or metadata. In this case the Velero Deployment is
already configured using the cloud-credentials secret because the
velero install command created it for us. So all that this patch
needs to do is register the digitalocean_token as an environment
variable with the already deployed Velero Pod.
Let’s apply the first Secret patch using the kubectl patch
command:
kubectl patch secret/cloud-credentials -p "$(cat
examples/01-velero-secret.patch.yaml)" --namespace
velero
You should see the following output:
Output
secret/cloud-credentials patched
Finally we will patch the Deployment. Run the following command:
kubectl patch deployment/velero -p "$(cat
examples/02-velero-deployment.patch.yaml") --
namespace velero
You will see the following if the patch is successful:
Output
deployment.apps/velero patched
Let’s verify the patched Deployment is working using kubectl get
on the velero Namespace:
kubectl get deployment/velero --namespace velero
You should see the following output:
Output
NAME READY UP-TO-DATE AVAILABLE AGE
velero 1/1 1 1 12s
At this point Velero is running and fully configured, and ready to back
up and restore your Kubernetes cluster objects and Persistent Volumes to
DigitalOcean Spaces and Block Storage.
In the next section, we’ll run a quick test to make sure that the backup
and restore functionality works as expected.
Output
. . .
---
apiVersion: v1
kind: Namespace
metadata:
name: nginx-example
labels:
app: nginx
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: nginx-logs
namespace: nginx-example
labels:
app: nginx
spec:
storageClassName: do-block-storage
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deploy
namespace: nginx-example
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
volumes:
- name: nginx-logs
persistentVolumeClaim:
claimName: nginx-logs
containers:
- image: nginx:stable
name: nginx
ports:
- containerPort: 80
volumeMounts:
- mountPath: "/var/log/nginx"
name: nginx-logs
readOnly: false
---
apiVersion: v1
kind: Service
metadata:
labels:
app: nginx
name: nginx-svc
namespace: nginx-example
spec:
ports:
- port: 80
targetPort: 80
selector:
app: nginx
type: LoadBalancer
In this file, we observe specs for:
Output
NAME READY UP-TO-DATE AVAILABLE
AGE
nginx-deploy 1/1 1 1
1m23s
Once Available reaches 1, fetch the Nginx load balancer’s external
IP using kubectl get:
kubectl get services --namespace=nginx-example
You should see both the internal CLUSTER-IP and EXTERNAL-IP for
the my-nginx Service:
Output
NAME TYPE CLUSTER-IP
EXTERNAL-IP PORT(S) AGE
nginx-svc LoadBalancer 10.245.147.61
159.203.48.191 80:30232/TCP 3m1s
Note the EXTERNAL-IP and navigate to it using your web browser.
You should see the following NGINX welcome page:
Nginx Welcome Page
This indicates that your Nginx Deployment and Service are up and
running.
Before we simulate our disaster scenario, let’s first check the Nginx
access logs (stored on a Persistent Volume attached to the Nginx Pod):
Fetch the Pod’s name using kubectl get:
kubectl get pods --namespace nginx-example
Output
NAME READY STATUS
RESTARTS AGE
nginx-deploy-694c85cdc8-vknsk 1/1 Running
0 4m14s
Now, exec into the running Nginx container to get a shell inside of it:
kubectl exec -it nginx-deploy-694c85cdc8-vknsk --
namespace nginx-example -- /bin/bash
Once inside the Nginx container, cat the Nginx access logs:
[environment second]
cat /var/log/nginx/access.log
You should see some Nginx access entries:
[environment second]
[secondary_label Output]
10.244.0.119 - - [03/Jan/2020:04:43:04 +0000] "GET
/ HTTP/1.1" 200 612 "-" "Mozilla/5.0 (X11; Linux
x86_64; rv:72.0) Gecko/20100101 Firefox/72.0" "-"
10.244.0.119 - - [03/Jan/2020:04:43:04 +0000] "GET
/favicon.ico HTTP/1.1" 404 153 "-" "Mozilla/5.0
(X11; Linux x86_64; rv:72.0) Gecko/20100101
Firefox/72.0" "-"
Note these down (especially the timestamps), as we will use them to
confirm the success of the restore procedure. Exit the pod:
exit
We can now perform the backup procedure to copy all nginx
Kubernetes objects to Spaces and take a Snapshot of the Persistent Volume
we created when deploying Nginx.
We’ll create a backup called nginx-backup using the velero
command line client:
velero backup create nginx-backup --selector
app=nginx
The --selector app=nginx instructs the Velero server to only
back up Kubernetes objects with the app=nginx Label Selector.
You should see the following output:
Output
Backup request "nginx-backup" submitted
successfully.
Run `velero backup describe nginx-backup` or
`velero backup logs nginx-backup` for more
details.
Running velero backup describe nginx-backup --
details should provide the following output after a short delay:
Output
Name: nginx-backup
Namespace: velero
Labels: velero.io/backup=nginx-backup
velero.io/pv=pvc-6b7f63d7-752b-4537-
9bb0-003bed9129ca
velero.io/storage-location=default
Annotations: <none>
Phase: Completed
Namespaces:
Included: *
Excluded: <none>
Resources:
Included: *
Excluded: <none>
Cluster-scoped: auto
TTL: 720h0m0s
Hooks: <none>
Resource List:
apps/v1/Deployment:
- nginx-example/nginx-deploy
apps/v1/ReplicaSet:
- nginx-example/nginx-deploy-694c85cdc8
v1/Endpoints:
- nginx-example/nginx-svc
v1/Namespace:
- nginx-example
v1/PersistentVolume:
- pvc-6b7f63d7-752b-4537-9bb0-003bed9129ca
v1/PersistentVolumeClaim:
- nginx-example/nginx-logs
v1/Pod:
- nginx-example/nginx-deploy-694c85cdc8-vknsk
v1/Service:
- nginx-example/nginx-svc
Persistent Volumes:
pvc-6b7f63d7-752b-4537-9bb0-003bed9129ca:
Snapshot ID: dfe866cc-2de3-11ea-9ec0-
0a58ac14e075
Type: ext4
Availability Zone:
IOPS: <N/A>
This output indicates that nginx-backup completed successfully.
The list of resources shows each of the Kubernetes objects that was
included in the backup. The final section shows the PersistentVolume was
also backed up using a filesystem snapshot.
To confirm from within the DigitalOcean Cloud Control Panel, navigate
to the Space containing your Kubernetes backup files.
You should see a new directory called nginx-backup containing the
Velero backup files.
Using the left-hand navigation bar, go to Images and then Snapshots.
Within Snapshots, navigate to Volumes. You should see a Snapshot
corresponding to the PVC listed in the above output.
We can now test the restore procedure.
Let’s first delete the nginx-example Namespace. This will delete
everything in the Namespace, including the Load Balancer and Persistent
Volume:
kubectl delete namespace nginx-example
Verify that you can no longer access Nginx at the Load Balancer
endpoint, and that the nginx-example Deployment is no longer
running:
kubectl get deployments --namespace=nginx-example
Output
No resources found in nginx-example namespace.
We can now perform the restore procedure, once again using the
velero client:
velero restore create --from-backup nginx-backup
Here we use create to create a Velero Restore object from the
nginx-backup object.
You should see the following output:
Output
Restore request "nginx-backup-20200102235032"
submitted successfully.
Run `velero restore describe nginx-backup-
20200102235032` or `velero restore logs nginx-
backup-20200102235032` for more details.
Check the status of the restored Deployment:
kubectl get deployments --namespace=nginx-example
Output
NAME READY UP-TO-DATE AVAILABLE
AGE
nginx-deploy 1/1 1 1
58s
Check for the creation of a Persistent Volume:
kubectl get pvc --namespace=nginx-example
Output
NAME STATUS VOLUME
CAPACITY ACCESS MODES STORAGECLASS AGE
nginx-logs Bound pvc-6b7f63d7-752b-4537-9bb0-
003bed9129ca 5Gi RWO do-block-
storage 75s
The restore also created a LoadBalancer. Sometimes the Service will be
re-created with a new IP address. You will need to find the EXTERNAL-IP
address again:
kubectl get services --namespace nginx-example
Output
NAME TYPE CLUSTER-IP
EXTERNAL-IP PORT(S) AGE
nginx-svc LoadBalancer 10.245.15.83
159.203.48.191 80:31217/TCP 97s
Navigate to the Nginx Service’s external IP once again to confirm that
Nginx is up and running.
Finally, check the logs on the restored Persistent Volume to confirm that
the log history has been preserved post-restore.
To do this, once again fetch the Pod’s name using kubectl get:
kubectl get pods --namespace nginx-example
Output
NAME READY STATUS
RESTARTS AGE
nginx-deploy-694c85cdc8-vknsk 1/1 Running
0 2m20s
Then exec into it:
kubectl exec -it nginx-deploy-694c85cdc8-vknsk --
namespace nginx-example -- /bin/bash
Once inside the Nginx container, cat the Nginx access logs:
[environment second]
cat /var/log/nginx/access.log
[environment second]
[secondary_label Output]
10.244.0.119 - - [03/Jan/2020:04:43:04 +0000] "GET
/ HTTP/1.1" 200 612 "-" "Mozilla/5.0 (X11; Linux
x86_64; rv:72.0) Gecko/20100101 Firefox/72.0" "-"
10.244.0.119 - - [03/Jan/2020:04:43:04 +0000] "GET
/favicon.ico HTTP/1.1" 404 153 "-" "Mozilla/5.0
(X11; Linux x86_64; rv:72.0) Gecko/20100101
Firefox/72.0" "-"
You should see the same pre-backup access attempts (note the
timestamps), confirming that the Persistent Volume restore was
successful. Note that there may be additional attempts in the logs if you
visited the Nginx landing page after you performed the restore.
At this point, we’ve successfully backed up our Kubernetes objects to
DigitalOcean Spaces, and our Persistent Volumes using Block Storage
Volume Snapshots. We simulated a disaster scenario, and restored service
to the test Nginx application.
Conclusion
In this guide we installed and configured the Velero Kubernetes backup
tool on a DigitalOcean-based Kubernetes cluster. We configured the tool to
back up Kubernetes objects to DigitalOcean Spaces, and back up Persistent
Volumes using Block Storage Volume Snapshots.
Velero can also be used to schedule regular backups of your Kubernetes
cluster for disaster recovery. To do this, you can use the velero
schedule command. Velero can also be used to migrate resources from
one cluster to another.
To learn more about DigitalOcean Spaces, consult the official Spaces
documentation. To learn more about Block Storage Volumes, consult the
Block Storage Volume documentation.
This tutorial builds on the README found in StackPointCloud’s ark-
plugin-digitalocean GitHub repo.
How To Set Up an Elasticsearch, Fluentd
and Kibana (EFK) Logging Stack on
Kubernetes
Prerequisites
Before you begin with this guide, ensure you have the following available
to you:
Once you have these components set up, you’re ready to begin with this
guide.
Output
NAME STATUS AGE
default Active 5m
kube-system Active 5m
kube-public Active 5m
The default Namespace houses objects that are created without
specifying a Namespace. The kube-system Namespace contains objects
created and used by the Kubernetes system, like kube-dns, kube-
proxy, and kubernetes-dashboard. It’s good practice to keep this
Namespace clean and not pollute it with your application and
instrumentation workloads.
The kube-public Namespace is another automatically created
Namespace that can be used to store objects you’d like to be readable and
accessible throughout the whole cluster, even to unauthenticated users.
To create the kube-logging Namespace, first open and edit a file
called kube-logging.yaml using your favorite editor, such as nano:
nano kube-logging.yaml
Inside your editor, paste the following Namespace object YAML:
kube-logging.yaml
kind: Namespace
apiVersion: v1
metadata:
name: kube-logging
Then, save and close the file.
Here, we specify the Kubernetes object’s kind as a Namespace
object. To learn more about Namespace objects, consult the Namespaces
Walkthrough in the official Kubernetes documentation. We also specify
the Kubernetes API version used to create the object (v1), and give it a
name, kube-logging.
Once you’ve created the kube-logging.yaml Namespace object
file, create the Namespace using kubectl create with the -f
filename flag:
kubectl create -f kube-logging.yaml
You should see the following output:
Output
namespace/kube-logging created
You can then confirm that the Namespace was successfully created:
kubectl get namespaces
At this point, you should see the new kube-logging Namespace:
Output
NAME STATUS AGE
default Active 23m
kube-logging Active 1m
kube-public Active 23m
kube-system Active 23m
We can now deploy an Elasticsearch cluster into this isolated logging
Namespace.
elasticsearch_svc.yaml
kind: Service
apiVersion: v1
metadata:
name: elasticsearch
namespace: kube-logging
labels:
app: elasticsearch
spec:
selector:
app: elasticsearch
clusterIP: None
ports:
- port: 9200
name: rest
- port: 9300
name: inter-node
Then, save and close the file.
We define a Service called elasticsearch in the kube-
logging Namespace, and give it the app: elasticsearch label. We
then set the .spec.selector to app: elasticsearch so that the
Service selects Pods with the app: elasticsearch label. When we
associate our Elasticsearch StatefulSet with this Service, the Service will
return DNS A records that point to Elasticsearch Pods with the app:
elasticsearch label.
We then set clusterIP: None, which renders the service headless.
Finally, we define ports 9200 and 9300 which are used to interact with
the REST API, and for inter-node communication, respectively.
Create the service using kubectl:
kubectl create -f elasticsearch_svc.yaml
You should see the following output:
Output
service/elasticsearch created
Finally, double-check that the service was successfully created using
kubectl get:
kubectl get services --namespace=kube-logging
You should see the following:
Output
NAME TYPE CLUSTER-IP EXTERNAL-
IP PORT(S) AGE
elasticsearch ClusterIP None <none>
9200/TCP,9300/TCP 26s
Now that we’ve set up our headless service and a stable
.elasticsearch.kube-logging.svc.cluster.local domain
for our Pods, we can go ahead and create the StatefulSet.
elasticsearch_statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: es-cluster
namespace: kube-logging
spec:
serviceName: elasticsearch
replicas: 3
selector:
matchLabels:
app: elasticsearch
template:
metadata:
labels:
app: elasticsearch
In this block, we define a StatefulSet called es-cluster in the
kube-logging namespace. We then associate it with our previously
created elasticsearch Service using the serviceName field. This
ensures that each Pod in the StatefulSet will be accessible using the
following DNS address: es-cluster-
[0,1,2].elasticsearch.kube-
logging.svc.cluster.local, where [0,1,2] corresponds to the
Pod’s assigned integer ordinal.
We specify 3 replicas (Pods) and set the matchLabels selector to
app: elasticseach, which we then mirror in the
.spec.template.metadata section. The
.spec.selector.matchLabels and
.spec.template.metadata.labels fields must match.
We can now move on to the object spec. Paste in the following block of
YAML immediately below the preceding block:
elasticsearch_statefulset.yaml
. . .
spec:
containers:
- name: elasticsearch
image:
docker.elastic.co/elasticsearch/elasticsearch:7.2.
0
resources:
limits:
cpu: 1000m
requests:
cpu: 100m
ports:
- containerPort: 9200
name: rest
protocol: TCP
- containerPort: 9300
name: inter-node
protocol: TCP
volumeMounts:
- name: data
mountPath: /usr/share/elasticsearch/data
env:
- name: cluster.name
value: k8s-logs
- name: node.name
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: discovery.seed_hosts
value: "es-cluster-0.elasticsearch,es-
cluster-1.elasticsearch,es-cluster-
2.elasticsearch"
- name: cluster.initial_master_nodes
value: "es-cluster-0,es-cluster-1,es-
cluster-2"
- name: ES_JAVA_OPTS
value: "-Xms512m -Xmx512m"
Here we define the Pods in the StatefulSet. We name the containers
elasticsearch and choose the
docker.elastic.co/elasticsearch/elasticsearch:7.2.
0 Docker image. At this point, you may modify this image tag to
correspond to your own internal Elasticsearch image, or a different
version. Note that for the purposes of this guide, only Elasticsearch
7.2.0 has been tested.
We then use the resources field to specify that the container needs at
least 0.1 vCPU guaranteed to it, and can burst up to 1 vCPU (which limits
the Pod’s resource usage when performing an initial large ingest or dealing
with a load spike). You should modify these values depending on your
anticipated load and available resources. To learn more about resource
requests and limits, consult the official Kubernetes Documentation.
We then open and name ports 9200 and 9300 for REST API and inter-
node communication, respectively. We specify a volumeMount called
data that will mount the PersistentVolume named data to the container
at the path /usr/share/elasticsearch/data. We will define the
VolumeClaims for this StatefulSet in a later YAML block.
Finally, we set some environment variables in the container:
elasticsearch_statefulset.yaml
. . .
initContainers:
- name: fix-permissions
image: busybox
command: ["sh", "-c", "chown -R 1000:1000
/usr/share/elasticsearch/data"]
securityContext:
privileged: true
volumeMounts:
- name: data
mountPath: /usr/share/elasticsearch/data
- name: increase-vm-max-map
image: busybox
command: ["sysctl", "-w",
"vm.max_map_count=262144"]
securityContext:
privileged: true
- name: increase-fd-ulimit
image: busybox
command: ["sh", "-c", "ulimit -n 65536"]
securityContext:
privileged: true
In this block, we define several Init Containers that run before the main
elasticsearch app container. These Init Containers each run to
completion in the order they are defined. To learn more about Init
Containers, consult the official Kubernetes Documentation.
The first, named fix-permissions, runs a chown command to
change the owner and group of the Elasticsearch data directory to
1000:1000, the Elasticsearch user’s UID. By default Kubernetes mounts
the data directory as root, which renders it inaccessible to Elasticsearch.
To learn more about this step, consult Elasticsearch’s “Notes for
production use and defaults.”
The second, named increase-vm-max-map, runs a command to
increase the operating system’s limits on mmap counts, which by default
may be too low, resulting in out of memory errors. To learn more about
this step, consult the official Elasticsearch documentation.
The next Init Container to run is increase-fd-ulimit, which runs
the ulimit command to increase the maximum number of open file
descriptors. To learn more about this step, consult the “Notes for
Production Use and Defaults” from the official Elasticsearch
documentation.
Note: The Elasticsearch Notes for Production Use also mentions
disabling swapping for performance reasons. Depending on your
Kubernetes installation or provider, swapping may already be disabled. To
check this, exec into a running container and run cat /proc/swaps
to list active swap devices. If you see nothing there, swap is disabled.
Now that we’ve defined our main app container and the Init Containers
that run before it to tune the container OS, we can add the final piece to
our StatefulSet object definition file: the volumeClaimTemplates.
Paste in the following volumeClaimTemplate block:
elasticsearch_statefulset.yaml
. . .
volumeClaimTemplates:
- metadata:
name: data
labels:
app: elasticsearch
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: do-block-storage
resources:
requests:
storage: 100Gi
In this block, we define the StatefulSet’s volumeClaimTemplates.
Kubernetes will use this to create PersistentVolumes for the Pods. In the
block above, we name it data (which is the name we refer to in the
volumeMounts defined previously), and give it the same app:
elasticsearch label as our StatefulSet.
We then specify its access mode as ReadWriteOnce, which means
that it can only be mounted as read-write by a single node. We define the
storage class as do-block-storage in this guide since we use a
DigitalOcean Kubernetes cluster for demonstration purposes. You should
change this value depending on where you are running your Kubernetes
cluster. To learn more, consult the Persistent Volume documentation.
Finally, we specify that we’d like each PersistentVolume to be 100GiB
in size. You should adjust this value depending on your production needs.
The complete StatefulSet spec should look something like this:
elasticsearch_statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: es-cluster
namespace: kube-logging
spec:
serviceName: elasticsearch
replicas: 3
selector:
matchLabels:
app: elasticsearch
template:
metadata:
labels:
app: elasticsearch
spec:
containers:
- name: elasticsearch
image:
docker.elastic.co/elasticsearch/elasticsearch:7.2.
0
resources:
limits:
cpu: 1000m
requests:
cpu: 100m
ports:
- containerPort: 9200
name: rest
protocol: TCP
- containerPort: 9300
name: inter-node
protocol: TCP
volumeMounts:
- name: data
mountPath: /usr/share/elasticsearch/data
env:
- name: cluster.name
value: k8s-logs
- name: node.name
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: discovery.seed_hosts
value: "es-cluster-0.elasticsearch,es-
cluster-1.elasticsearch,es-cluster-
2.elasticsearch"
- name: cluster.initial_master_nodes
value: "es-cluster-0,es-cluster-1,es-
cluster-2"
- name: ES_JAVA_OPTS
value: "-Xms512m -Xmx512m"
initContainers:
- name: fix-permissions
image: busybox
command: ["sh", "-c", "chown -R 1000:1000
/usr/share/elasticsearch/data"]
securityContext:
privileged: true
volumeMounts:
- name: data
mountPath: /usr/share/elasticsearch/data
- name: increase-vm-max-map
image: busybox
command: ["sysctl", "-w",
"vm.max_map_count=262144"]
securityContext:
privileged: true
- name: increase-fd-ulimit
image: busybox
command: ["sh", "-c", "ulimit -n 65536"]
securityContext:
privileged: true
volumeClaimTemplates:
- metadata:
name: data
labels:
app: elasticsearch
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: do-block-storage
resources:
requests:
storage: 100Gi
Once you’re satisfied with your Elasticsearch configuration, save and
close the file.
Now, deploy the StatefulSet using kubectl:
kubectl create -f elasticsearch_statefulset.yaml
You should see the following output:
Output
statefulset.apps/es-cluster created
You can monitor the StatefulSet as it is rolled out using kubectl
rollout status:
kubectl rollout status sts/es-cluster --
namespace=kube-logging
You should see the following output as the cluster is rolled out:
Output
Waiting for 3 pods to be ready...
Waiting for 2 pods to be ready...
Waiting for 1 pods to be ready...
partitioned roll out complete: 3 new pods have
been updated...
Once all the Pods have been deployed, you can check that your
Elasticsearch cluster is functioning correctly by performing a request
against the REST API.
To do so, first forward the local port 9200 to the port 9200 on one of
the Elasticsearch nodes (es-cluster-0) using kubectl port-
forward:
kubectl port-forward es-cluster-0 9200:9200 --
namespace=kube-logging
Then, in a separate terminal window, perform a curl request against
the REST API:
curl http://localhost:9200/_cluster/state?pretty
You shoulds see the following output:
Output
{
"cluster_name" : "k8s-logs",
"compressed_size_in_bytes" : 348,
"cluster_uuid" : "QD06dK7CQgids-GQZooNVw",
"version" : 3,
"state_uuid" : "mjNIWXAzQVuxNNOQ7xR-qg",
"master_node" : "IdM5B7cUQWqFgIHXBp0JDg",
"blocks" : { },
"nodes" : {
"u7DoTpMmSCixOoictzHItA" : {
"name" : "es-cluster-1",
"ephemeral_id" : "ZlBflnXKRMC4RvEACHIVdg",
"transport_address" : "10.244.8.2:9300",
"attributes" : { }
},
"IdM5B7cUQWqFgIHXBp0JDg" : {
"name" : "es-cluster-0",
"ephemeral_id" : "JTk1FDdFQuWbSFAtBxdxAQ",
"transport_address" : "10.244.44.3:9300",
"attributes" : { }
},
"R8E7xcSUSbGbgrhAdyAKmQ" : {
"name" : "es-cluster-2",
"ephemeral_id" : "9wv6ke71Qqy9vk2LgJTqaA",
"transport_address" : "10.244.40.4:9300",
"attributes" : { }
}
},
...
This indicates that our Elasticsearch cluster k8s-logs has
successfully been created with 3 nodes: es-cluster-0, es-
cluster-1, and es-cluster-2. The current master node is es-
cluster-0.
Now that your Elasticsearch cluster is up and running, you can move on
to setting up a Kibana frontend for it.
kibana.yaml
apiVersion: v1
kind: Service
metadata:
name: kibana
namespace: kube-logging
labels:
app: kibana
spec:
ports:
- port: 5601
selector:
app: kibana
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: kibana
namespace: kube-logging
labels:
app: kibana
spec:
replicas: 1
selector:
matchLabels:
app: kibana
template:
metadata:
labels:
app: kibana
spec:
containers:
- name: kibana
image:
docker.elastic.co/kibana/kibana:7.2.0
resources:
limits:
cpu: 1000m
requests:
cpu: 100m
env:
- name: ELASTICSEARCH_URL
value: http://elasticsearch:9200
ports:
- containerPort: 5601
Then, save and close the file.
In this spec we’ve defined a service called kibana in the kube-
logging namespace, and gave it the app: kibana label.
We’ve also specified that it should be accessible on port 5601 and use
the app: kibana label to select the Service’s target Pods.
In the Deployment spec, we define a Deployment called kibana and
specify that we’d like 1 Pod replica.
We use the docker.elastic.co/kibana/kibana:7.2.0
image. At this point you may substitute your own private or public Kibana
image to use.
We specify that we’d like at the very least 0.1 vCPU guaranteed to the
Pod, bursting up to a limit of 1 vCPU. You may change these parameters
depending on your anticipated load and available resources.
Next, we use the ELASTICSEARCH_URL environment variable to set
the endpoint and port for the Elasticsearch cluster. Using Kubernetes DNS,
this endpoint corresponds to its Service name elasticsearch. This
domain will resolve to a list of IP addresses for the 3 Elasticsearch Pods.
To learn more about Kubernetes DNS, consult DNS for Services and Pods.
Finally, we set Kibana’s container port to 5601, to which the kibana
Service will forward requests.
Once you’re satisfied with your Kibana configuration, you can roll out
the Service and Deployment using kubectl:
kubectl create -f kibana.yaml
You should see the following output:
Output
service/kibana created
deployment.apps/kibana created
You can check that the rollout succeeded by running the following
command:
kubectl rollout status deployment/kibana --
namespace=kube-logging
You should see the following output:
Output
deployment "kibana" successfully rolled out
To access the Kibana interface, we’ll once again forward a local port to
the Kubernetes node running Kibana. Grab the Kibana Pod details using
kubectl get:
kubectl get pods --namespace=kube-logging
Output
NAME READY STATUS
RESTARTS AGE
es-cluster-0 1/1 Running 0
55m
es-cluster-1 1/1 Running 0
54m
es-cluster-2 1/1 Running 0
54m
kibana-6c9fb4b5b7-plbg2 1/1 Running 0
4m27s
Here we observe that our Kibana Pod is called kibana-
6c9fb4b5b7-plbg2.
Forward the local port 5601 to port 5601 on this Pod:
kubectl port-forward kibana-6c9fb4b5b7-plbg2
5601:5601 --namespace=kube-logging
You should see the following output:
Output
Forwarding from 127.0.0.1:5601 -> 5601
Forwarding from [::1]:5601 -> 5601
Now, in your web browser, visit the following URL:
http://localhost:5601
If you see the following Kibana welcome page, you’ve successfully
deployed Kibana into your Kubernetes cluster:
Kibana Welcome Screen
You can now move on to rolling out the final component of the EFK
stack: the log collector, Fluentd.
fluentd.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: fluentd
namespace: kube-logging
labels:
app: fluentd
Here, we create a Service Account called fluentd that the Fluentd
Pods will use to access the Kubernetes API. We create it in the kube-
logging Namespace and once again give it the label app: fluentd.
To learn more about Service Accounts in Kubernetes, consult Configure
Service Accounts for Pods in the official Kubernetes docs.
Next, paste in the following ClusterRole block:
fluentd.yaml
. . .
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: fluentd
labels:
app: fluentd
rules:
- apiGroups:
- ""
resources:
- pods
- namespaces
verbs:
- get
- list
- watch
Here we define a ClusterRole called fluentd to which we grant the
get, list, and watch permissions on the pods and namespaces
objects. ClusterRoles allow you to grant access to cluster-scoped
Kubernetes resources like Nodes. To learn more about Role-Based Access
Control and Cluster Roles, consult Using RBAC Authorization from the
official Kubernetes documentation.
Now, paste in the following ClusterRoleBinding block:
fluentd.yaml
. . .
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: fluentd
roleRef:
kind: ClusterRole
name: fluentd
apiGroup: rbac.authorization.k8s.io
subjects:
- kind: ServiceAccount
name: fluentd
namespace: kube-logging
In this block, we define a ClusterRoleBinding called fluentd
which binds the fluentd ClusterRole to the fluentd Service Account.
This grants the fluentd ServiceAccount the permissions listed in the
fluentd Cluster Role.
At this point we can begin pasting in the actual DaemonSet spec:
fluentd.yaml
. . .
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd
namespace: kube-logging
labels:
app: fluentd
Here, we define a DaemonSet called fluentd in the kube-logging
Namespace and give it the app: fluentd label.
Next, paste in the following section:
fluentd.yaml
. . .
spec:
selector:
matchLabels:
app: fluentd
template:
metadata:
labels:
app: fluentd
spec:
serviceAccount: fluentd
serviceAccountName: fluentd
tolerations:
- key: node-role.kubernetes.io/master
effect: NoSchedule
containers:
- name: fluentd
image: fluent/fluentd-kubernetes-
daemonset:v1.4.2-debian-elasticsearch-1.1
env:
- name: FLUENT_ELASTICSEARCH_HOST
value: "elasticsearch.kube-
logging.svc.cluster.local"
- name: FLUENT_ELASTICSEARCH_PORT
value: "9200"
- name: FLUENT_ELASTICSEARCH_SCHEME
value: "http"
- name: FLUENTD_SYSTEMD_CONF
value: disable
Here, we match the app: fluentd label defined in
.metadata.labels and then assign the DaemonSet the fluentd
Service Account. We also select the app: fluentd as the Pods
managed by this DaemonSet.
Next, we define a NoSchedule toleration to match the equivalent taint
on Kubernetes master nodes. This will ensure that the DaemonSet also
gets rolled out to the Kubernetes masters. If you don’t want to run a
Fluentd Pod on your master nodes, remove this toleration. To learn more
about Kubernetes taints and tolerations, consult “Taints and Tolerations”
from the official Kubernetes docs.
Next, we begin defining the Pod container, which we call fluentd.
We use the official v1.4.2 Debian image provided by the Fluentd
maintainers. If you’d like to use your own private or public Fluentd image,
or use a different image version, modify the image tag in the container
spec. The Dockerfile and contents of this image are available in Fluentd’s
fluentd-kubernetes-daemonset Github repo.
Next, we configure Fluentd using some environment variables:
fluentd.yaml
. . .
resources:
limits:
memory: 512Mi
requests:
cpu: 100m
memory: 200Mi
volumeMounts:
- name: varlog
mountPath: /var/log
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
terminationGracePeriodSeconds: 30
volumes:
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
Here we specify a 512 MiB memory limit on the FluentD Pod, and
guarantee it 0.1vCPU and 200MiB of memory. You can tune these resource
limits and requests depending on your anticipated log volume and
available resources.
Next, we mount the /var/log and
/var/lib/docker/containers host paths into the container using
the varlog and varlibdockercontainers volumeMounts.
These volumes are defined at the end of the block.
The final parameter we define in this block is
terminationGracePeriodSeconds, which gives Fluentd 30
seconds to shut down gracefully upon receiving a SIGTERM signal. After
30 seconds, the containers are sent a SIGKILL signal. The default value
for terminationGracePeriodSeconds is 30s, so in most cases this
parameter can be omitted. To learn more about gracefully terminating
Kubernetes workloads, consult Google’s “Kubernetes best practices:
terminating with grace.”
The entire Fluentd spec should look something like this:
fluentd.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: fluentd
namespace: kube-logging
labels:
app: fluentd
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: fluentd
labels:
app: fluentd
rules:
- apiGroups:
- ""
resources:
- pods
- namespaces
verbs:
- get
- list
- watch
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: fluentd
roleRef:
kind: ClusterRole
name: fluentd
apiGroup: rbac.authorization.k8s.io
subjects:
- kind: ServiceAccount
name: fluentd
namespace: kube-logging
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd
namespace: kube-logging
labels:
app: fluentd
spec:
selector:
matchLabels:
app: fluentd
template:
metadata:
labels:
app: fluentd
spec:
serviceAccount: fluentd
serviceAccountName: fluentd
tolerations:
- key: node-role.kubernetes.io/master
effect: NoSchedule
containers:
- name: fluentd
image: fluent/fluentd-kubernetes-
daemonset:v1.4.2-debian-elasticsearch-1.1
env:
- name: FLUENT_ELASTICSEARCH_HOST
value: "elasticsearch.kube-
logging.svc.cluster.local"
- name: FLUENT_ELASTICSEARCH_PORT
value: "9200"
- name: FLUENT_ELASTICSEARCH_SCHEME
value: "http"
- name: FLUENTD_SYSTEMD_CONF
value: disable
resources:
limits:
memory: 512Mi
requests:
cpu: 100m
memory: 200Mi
volumeMounts:
- name: varlog
mountPath: /var/log
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
terminationGracePeriodSeconds: 30
volumes:
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
Once you’ve finished configuring the Fluentd DaemonSet, save and
close the file.
Now, roll out the DaemonSet using kubectl:
kubectl create -f fluentd.yaml
You should see the following output:
Output
serviceaccount/fluentd created
clusterrole.rbac.authorization.k8s.io/fluentd
created
clusterrolebinding.rbac.authorization.k8s.io/fluen
td created
daemonset.extensions/fluentd created
Verify that your DaemonSet rolled out successfully using kubectl:
kubectl get ds --namespace=kube-logging
You should see the following status output:
Output
NAME DESIRED CURRENT READY UP-TO-DATE
AVAILABLE NODE SELECTOR AGE
fluentd 3 3 3 3
3 <none> 58s
This indicates that there are 3 fluentd Pods running, which
corresponds to the number of nodes in our Kubernetes cluster.
We can now check Kibana to verify that log data is being properly
collected and shipped to Elasticsearch.
With the kubectl port-forward still open, navigate to
http://localhost:5601.
Click on Discover in the left-hand navigation menu:
Kibana Discover
This allows you to define the Elasticsearch indices you’d like to explore
in Kibana. To learn more, consult Defining your index patterns in the
official Kibana docs. For now, we’ll just use the logstash-* wildcard
pattern to capture all the log data in our Elasticsearch cluster. Enter
logstash-* in the text box and click on Next step.
You’ll then be brought to the following page:
Kibana Index Pattern Settings
This allows you to configure which field Kibana will use to filter log
data by time. In the dropdown, select the @timestamp field, and hit Create
index pattern.
Now, hit Discover in the left hand navigation menu.
You should see a histogram graph and some recent log entries:
Kibana Incoming Logs
At this point you’ve successfully configured and rolled out the EFK
stack on your Kubernetes cluster. To learn how to use Kibana to analyze
your log data, consult the Kibana User Guide.
In the next optional section, we’ll deploy a simple counter Pod that
prints numbers to stdout, and find its logs in Kibana.
counter.yaml
apiVersion: v1
kind: Pod
metadata:
name: counter
spec:
containers:
- name: count
image: busybox
args: [/bin/sh, -c,
'i=0; while true; do echo "$i:
$(date)"; i=$((i+1)); sleep 1; done']
Save and close the file.
This is a minimal Pod called counter that runs a while loop,
printing numbers sequentially.
Deploy the counter Pod using kubectl:
kubectl create -f counter.yaml
Once the Pod has been created and is running, navigate back to your
Kibana dashboard.
From the Discover page, in the search bar enter
kubernetes.pod_name:counter. This filters the log data for Pods
named counter.
You should then see a list of log entries for the counter Pod:
You can click into any of the log entries to see additional metadata like
the container name, Kubernetes node, Namespace, and more.
Conclusion
In this guide we’ve demonstrated how to set up and configure
Elasticsearch, Fluentd, and Kibana on a Kubernetes cluster. We’ve used a
minimal logging architecture that consists of a single logging agent Pod
running on each Kubernetes worker node.
Before deploying this logging stack into your production Kubernetes
cluster, it’s best to tune the resource requirements and limits as indicated
throughout this guide. You may also want to set up X-Pack to enable built-
in monitoring and security features.
The logging architecture we’ve used here consists of 3 Elasticsearch
Pods, a single Kibana Pod (not load-balanced), and a set of Fluentd Pods
rolled out as a DaemonSet. You may wish to scale this setup depending on
your production use case. To learn more about scaling your Elasticsearch
and Kibana stack, consult Scaling Elasticsearch.
Kubernetes also allows for more complex logging agent architectures
that may better suit your use case. To learn more, consult Logging
Architecture from the Kubernetes docs.
How to Set Up an Nginx Ingress with
Cert-Manager on DigitalOcean
Kubernetes
Prerequisites
Before you begin with this guide, you should have the following available
to you:
echo1.yaml
apiVersion: v1
kind: Service
metadata:
name: echo1
spec:
ports:
- port: 80
targetPort: 5678
selector:
app: echo1
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: echo1
spec:
selector:
matchLabels:
app: echo1
replicas: 2
template:
metadata:
labels:
app: echo1
spec:
containers:
- name: echo1
image: hashicorp/http-echo
args:
- "-text=echo1"
ports:
- containerPort: 5678
In this file, we define a Service called echo1 which routes traffic to
Pods with the app: echo1 label selector. It accepts TCP traffic on port
80 and routes it to port 5678,http-echo’s default port.
We then define a Deployment, also called echo1, which manages Pods
with the app: echo1 Label Selector. We specify that the Deployment
should have 2 Pod replicas, and that the Pods should start a container
called echo1 running the hashicorp/http-echo image. We pass in
the text parameter and set it to echo1, so that the http-echo web
server returns echo1. Finally, we open port 5678 on the Pod container.
Once you’re satisfied with your dummy Service and Deployment
manifest, save and close the file.
Then, create the Kubernetes resources using kubectl apply with the
-f flag, specifying the file you just saved as a parameter:
kubectl apply -f echo1.yaml
You should see the following output:
Output
service/echo1 created
deployment.apps/echo1 created
Verify that the Service started correctly by confirming that it has a
ClusterIP, the internal IP on which the Service is exposed:
kubectl get svc echo1
You should see the following output:
Output
NAME TYPE CLUSTER-IP EXTERNAL-IP
PORT(S) AGE
echo1 ClusterIP 10.245.222.129 <none>
80/TCP 60s
This indicates that the echo1 Service is now available internally at
10.245.222.129 on port 80. It will forward traffic to containerPort
5678 on the Pods it selects.
Now that the echo1 Service is up and running, repeat this process for
the echo2 Service.
Create and open a file called echo2.yaml:
echo2.yaml
apiVersion: v1
kind: Service
metadata:
name: echo2
spec:
ports:
- port: 80
targetPort: 5678
selector:
app: echo2
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: echo2
spec:
selector:
matchLabels:
app: echo2
replicas: 1
template:
metadata:
labels:
app: echo2
spec:
containers:
- name: echo2
image: hashicorp/http-echo
args:
- "-text=echo2"
ports:
- containerPort: 5678
Here, we essentially use the same Service and Deployment manifest as
above, but name and relabel the Service and Deployment echo2. In
addition, to provide some variety, we create only 1 Pod replica. We ensure
that we set the text parameter to echo2 so that the web server returns
the text echo2.
Save and close the file, and create the Kubernetes resources using
kubectl:
kubectl apply -f echo2.yaml
You should see the following output:
Output
service/echo2 created
deployment.apps/echo2 created
Once again, verify that the Service is up and running:
kubectl get svc
You should see both the echo1 and echo2 Services with assigned
ClusterIPs:
Output
NAME TYPE CLUSTER-IP
EXTERNAL-IP PORT(S) AGE
echo1 ClusterIP 10.245.222.129 <none>
80/TCP 6m6s
echo2 ClusterIP 10.245.128.224 <none>
80/TCP 6m3s
kubernetes ClusterIP 10.245.0.1 <none>
443/TCP 4d21h
Now that our dummy echo web services are up and running, we can
move on to rolling out the Nginx Ingress Controller.
Output
namespace/ingress-nginx created
configmap/nginx-configuration created
configmap/tcp-services created
configmap/udp-services created
serviceaccount/nginx-ingress-serviceaccount
created
clusterrole.rbac.authorization.k8s.io/nginx-
ingress-clusterrole created
role.rbac.authorization.k8s.io/nginx-ingress-role
created
rolebinding.rbac.authorization.k8s.io/nginx-
ingress-role-nisa-binding created
clusterrolebinding.rbac.authorization.k8s.io/nginx
-ingress-clusterrole-nisa-binding created
deployment.apps/nginx-ingress-controller created
This output also serves as a convenient summary of all the Ingress
Controller objects created from the mandatory.yaml manifest.
Next, we’ll create the Ingress Controller LoadBalancer Service, which
will create a DigitalOcean Load Balancer that will load balance and route
HTTP and HTTPS traffic to the Ingress Controller Pod deployed in the
previous command.
To create the LoadBalancer Service, once again kubectl apply a
manifest file containing the Service definition:
kubectl apply -f
https://raw.githubusercontent.com/kubernetes/ingre
ss-nginx/nginx-
0.26.1/deploy/static/provider/cloud-generic.yaml
You should see the following output:
Output
service/ingress-nginx created
Confirm that the Ingress Controller Pods have started:
kubectl get pods --all-namespaces -l
app.kubernetes.io/name=ingress-nginx
Output
NAMESPACE NAME
READY STATUS RESTARTS AGE
ingress-nginx nginx-ingress-controller-
7fb85bc8bb-lnm6z 1/1 Running 0
2m42s
Now, confirm that the DigitalOcean Load Balancer was successfully
created by fetching the Service details with kubectl:
kubectl get svc --namespace=ingress-nginx
After several minutes, you should see an external IP address,
corresponding to the IP address of the DigitalOcean Load Balancer:
Output
NAME TYPE CLUSTER-IP
EXTERNAL-IP PORT(S) AGE
ingress-nginx LoadBalancer 10.245.247.67
203.0.113.0 80:32486/TCP,443:32096/TCP 20h
Note down the Load Balancer’s external IP address, as you’ll need it in
a later step.
Note: By default the Nginx Ingress LoadBalancer Service has
service.spec.externalTrafficPolicy set to the value Local,
which routes all load balancer traffic to nodes running Nginx Ingress Pods.
The other nodes will deliberately fail load balancer health checks so that
Ingress traffic does not get routed to them. External traffic policies are
beyond the scope of this tutorial, but to learn more you can consult A Deep
Dive into Kubernetes External Traffic Policies and Source IP for Services
with Type=LoadBalancer from the official Kubernetes docs.
This load balancer receives traffic on HTTP and HTTPS ports 80 and
443, and forwards it to the Ingress Controller Pod. The Ingress Controller
will then route the traffic to the appropriate backend Service.
We can now point our DNS records at this external Load Balancer and
create some Ingress Resources to implement traffic routing rules.
echo_ingress.yaml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: echo-ingress
spec:
rules:
- host: echo1.example.com
http:
paths:
- backend:
serviceName: echo1
servicePort: 80
- host: echo2.example.com
http:
paths:
- backend:
serviceName: echo2
servicePort: 80
When you’ve finished editing your Ingress rules, save and close the file.
Here, we’ve specified that we’d like to create an Ingress Resource
called echo-ingress, and route traffic based on the Host header. An
HTTP request Host header specifies the domain name of the target server.
To learn more about Host request headers, consult the Mozilla Developer
Network definition page. Requests with host echo1.example.com will be
directed to the echo1 backend set up in Step 1, and requests with host
echo2.example.com will be directed to the echo2 backend.
You can now create the Ingress using kubectl:
kubectl apply -f echo_ingress.yaml
You’ll see the following output confirming the Ingress creation:
Output
ingress.extensions/echo-ingress created
To test the Ingress, navigate to your DNS management service and
create A records for echo1.example.com and
echo2.example.com pointing to the DigitalOcean Load Balancer’s
external IP. The Load Balancer’s external IP is the external IP address for
the ingress-nginx Service, which we fetched in the previous step. If
you are using DigitalOcean to manage your domain’s DNS records,
consult How to Manage DNS Records to learn how to create A records.
Once you’ve created the necessary echo1.example.com and
echo2.example.com DNS records, you can test the Ingress Controller
and Resource you’ve created using the curl command line utility.
From your local machine, curl the echo1 Service:
curl echo1.example.com
You should get the following response from the echo1 service:
Output
echo1
This confirms that your request to echo1.example.com is being
correctly routed through the Nginx ingress to the echo1 backend Service.
Now, perform the same test for the echo2 Service:
curl echo2.example.com
You should get the following response from the echo2 Service:
Output
echo2
This confirms that your request to echo2.example.com is being
correctly routed through the Nginx ingress to the echo2 backend Service.
At this point, you’ve successfully set up a minimal Nginx Ingress to
perform virtual host-based routing. In the next step, we’ll install cert-
manager to provision TLS certificates for our Ingress and enable the more
secure HTTPS protocol.
Output
customresourcedefinition.apiextensions.k8s.io/cert
ificaterequests.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/cert
ificates.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/chal
lenges.acme.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/clus
terissuers.cert-manager.io created
. . .
deployment.apps/cert-manager-webhook created
mutatingwebhookconfiguration.admissionregistration
.k8s.io/cert-manager-webhook created
validatingwebhookconfiguration.admissionregistrati
on.k8s.io/cert-manager-webhook created
To verify our installation, check the cert-manager Namespace for
running pods:
kubectl get pods --namespace cert-manager
Output
NAME READY
STATUS RESTARTS AGE
cert-manager-5c47f46f57-jknnx 1/1
Running 0 27s
cert-manager-cainjector-6659d6844d-j8cbg 1/1
Running 0 27s
cert-manager-webhook-547567b88f-qks44 1/1
Running 0 27s
This indicates that the cert-manager installation succeeded.
Before we begin issuing certificates for our Ingress hosts, we need to
create an Issuer, which specifies the certificate authority from which
signed x509 certificates can be obtained. In this guide, we’ll use the Let’s
Encrypt certificate authority, which provides free TLS certificates and
offers both a staging server for testing your certificate configuration, and a
production server for rolling out verifiable TLS certificates.
Let’s create a test Issuer to make sure the certificate provisioning
mechanism is functioning correctly. Open a file named
staging_issuer.yaml in your favorite text editor:
nano staging_issuer.yaml
Paste in the following ClusterIssuer manifest:
staging_issuer.yaml
apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
namespace: cert-manager
spec:
acme:
# The ACME server URL
server: https://acme-staging-
v02.api.letsencrypt.org/directory
# Email address used for ACME registration
email: your_email_address_here
# Name of a secret used to store the ACME
account private key
privateKeySecretRef:
name: letsencrypt-staging
# Enable the HTTP-01 challenge provider
solvers:
- http01:
ingress:
class: nginx
Here we specify that we’d like to create a ClusterIssuer object called
letsencrypt-staging, and use the Let’s Encrypt staging server.
We’ll later use the production server to roll out our certificates, but the
production server may rate-limit requests made against it, so for testing
purposes it’s best to use the staging URL.
We then specify an email address to register the certificate, and create a
Kubernetes Secret called letsencrypt-staging to store the ACME
account’s private key. We also enable the HTTP-01 challenge mechanism.
To learn more about these parameters, consult the official cert-manager
documentation on Issuers.
Roll out the ClusterIssuer using kubectl:
kubectl create -f staging_issuer.yaml
You should see the following output:
Output
clusterissuer.cert-manager.io/letsencrypt-staging
created
Now that we’ve created our Let’s Encrypt staging Issuer, we’re ready to
modify the Ingress Resource we created above and enable TLS encryption
for the echo1.example.com and echo2.example.com paths.
Open up echo_ingress.yaml once again in your favorite editor:
nano echo_ingress.yaml
Add the following to the Ingress Resource manifest:
echo_ingress.yaml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: echo-ingress
annotations:
kubernetes.io/ingress.class: "nginx"
cert-manager.io/cluster-issuer: "letsencrypt-
staging"
spec:
tls:
- hosts:
- echo1.hjdo.net
- echo2.hjdo.net
secretName: echo-tls
rules:
- host: echo1.hjdo.net
http:
paths:
- backend:
serviceName: echo1
servicePort: 80
- host: echo2.hjdo.net
http:
paths:
- backend:
serviceName: echo2
servicePort: 80
Here we add some annotations to specify the ingress.class, which
determines the Ingress Controller that should be used to implement the
Ingress Rules. In addition, we define the cluster-issuer to be
letsencrypt-staging, the certificate Issuer we just created.
Finally, we add a tls block to specify the hosts for which we want to
acquire certificates, and specify a secretName. This secret will contain
the TLS private key and issued certificate.
When you’re done making changes, save and close the file.
We’ll now update the existing Ingress Resource using kubectl
apply:
kubectl apply -f echo_ingress.yaml
You should see the following output:
Output
ingress.networking.k8s.io/echo-ingress configured
You can use kubectl describe to track the state of the Ingress
changes you’ve just applied:
kubectl describe ingress
Output
Events:
Type Reason Age
From Message
---- ------ ---- ---
- -------
Normal CREATE 14m
nginx-ingress-controller Ingress default/echo-
ingress
Normal CreateCertificate 67s cert-manager
Successfully created Certificate "echo-tls"
Normal UPDATE 53s nginx-ingress-
controller Ingress default/echo-ingress
Once the certificate has been successfully created, you can run an
additional describe on it to further confirm its successful creation:
kubectl describe certificate
You should see the following output in the Events section:
Output
Events:
Type Reason Age From
Message
---- ------ ---- ---- -----
--
Normal GeneratedKey 2m12s cert-manager
Generated a new private key
Normal Requested 2m12s cert-manager
Created new CertificateRequest resource "echo-tls-
3768100355"
Normal Issued 47s cert-manager
Certificate issued successfully
This confirms that the TLS certificate was successfully issued and
HTTPS encryption is now active for the two domains configured.
We’re now ready to send a request to a backend echo server to test that
HTTPS is functioning correctly.
Run the following wget command to send a request to
echo1.example.com and print the response headers to STDOUT:
wget --save-headers -O- echo1.example.com
You should see the following output:
Output
. . .
HTTP request sent, awaiting response... 308
Permanent Redirect
. . .
ERROR: cannot verify echo1.example.com's
certificate, issued by ‘CN=Fake LE Intermediate
X1’:
Unable to locally verify the issuer's authority.
To connect to echo1.example.com insecurely, use `-
-no-check-certificate'.
This indicates that HTTPS has successfully been enabled, but the
certificate cannot be verified as it’s a fake temporary certificate issued by
the Let’s Encrypt staging server.
Now that we’ve tested that everything works using this temporary fake
certificate, we can roll out production certificates for the two hosts
echo1.example.com and echo2.example.com.
prod_issuer.yaml
apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
namespace: cert-manager
spec:
acme:
# The ACME server URL
server: https://acme-
v02.api.letsencrypt.org/directory
# Email address used for ACME registration
email: your_email_address_here
# Name of a secret used to store the ACME
account private key
privateKeySecretRef:
name: letsencrypt-prod
# Enable the HTTP-01 challenge provider
solvers:
- http01:
ingress:
class: nginx
Note the different ACME server URL, and the letsencrypt-prod
secret key name.
When you’re done editing, save and close the file.
Now, roll out this Issuer using kubectl:
kubectl create -f prod_issuer.yaml
You should see the following output:
Output
clusterissuer.cert-manager.io/letsencrypt-prod
created
Update echo_ingress.yaml to use this new Issuer:
nano echo_ingress.yaml
Make the following change to the file:
echo_ingress.yaml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: echo-ingress
annotations:
kubernetes.io/ingress.class: "nginx"
cert-manager.io/cluster-issuer: "letsencrypt-
prod"
spec:
tls:
- hosts:
- echo1.hjdo.net
- echo2.hjdo.net
secretName: echo-tls
rules:
- host: echo1.hjdo.net
http:
paths:
- backend:
serviceName: echo1
servicePort: 80
- host: echo2.hjdo.net
http:
paths:
- backend:
serviceName: echo2
servicePort: 80
Here, we update the ClusterIssuer name to letsencrypt-prod.
Once you’re satisfied with your changes, save and close the file.
Roll out the changes using kubectl apply:
kubectl apply -f echo_ingress.yaml
Output
ingress.networking.k8s.io/echo-ingress configured
Wait a couple of minutes for the Let’s Encrypt production server to
issue the certificate. You can track its progress using kubectl
describe on the certificate object:
kubectl describe certificate echo-tls
Once you see the following output, the certificate has been issued
successfully:
Output
Events:
Type Reason Age From
Message
---- ------ ---- ----
-------
Normal GeneratedKey 8m10s cert-
manager Generated a new private key
Normal Requested 8m10s cert-
manager Created new CertificateRequest resource
"echo-tls-3768100355"
Normal Requested 35s cert-
manager Created new CertificateRequest resource
"echo-tls-4217844635"
Normal Issued 10s (x2 over 6m45s) cert-
manager Certificate issued successfully
We’ll now perform a test using curl to verify that HTTPS is working
correctly:
curl echo1.example.com
You should see the following:
Output
<html>
<head><title>308 Permanent Redirect</title></head>
<body>
<center><h1>308 Permanent Redirect</h1></center>
<hr><center>nginx/1.15.9</center>
</body>
</html>
This indicates that HTTP requests are being redirected to use HTTPS.
Run curl on https://echo1.example.com:
curl https://echo1.example.com
You should now see the following output:
Output
echo1
You can run the previous command with the verbose -v flag to dig
deeper into the certificate handshake and to verify the certificate
information.
At this point, you’ve successfully configured HTTPS using a Let’s
Encrypt certificate for your Nginx Ingress.
Conclusion
In this guide, you set up an Nginx Ingress to load balance and route
external requests to backend Services inside of your Kubernetes cluster.
You also secured the Ingress by installing the cert-manager certificate
provisioner and setting up a Let’s Encrypt certificate for two host paths.
There are many alternatives to the Nginx Ingress Controller. To learn
more, consult Ingress controllers from the official Kubernetes
documentation.
For a guide on rolling out the Nginx Ingress Controller using the Helm
Kubernetes package manager, consult How To Set Up an Nginx Ingress on
DigitalOcean Kubernetes Using Helm.
How to Protect Private Kubernetes
Services Behind a GitHub Login with
oauth2_proxy
Prerequisites
To complete this tutorial, you’ll need:
Output
NAME TYPE CLUSTER-IP
EXTERNAL-IP PORT(S) AGE
ingress-nginx LoadBalancer 10.245.247.67
203.0.113.0 80:32486/TCP,443:32096/TCP 20h
Copy the external IP address to your clipboard. Browse to your DNS
management service and locate the A records for echo1-
2.your_domain to point to that external IP address. If you are using
DigitalOcean to manage your DNS records, see How to Manage DNS
Records for instructions.
Delete the records for echo1 and echo2. Add a new A record for the
hostname *.int.your_domain and point it to the External IP address
of the ingress.
Now any request to any subdomain under *.int.your_domain will
be routed to the Nginx ingress, so you can use these subdomains within
your cluster.
Next you’ll configure GitHub as your login provider.
Output
secret/oauth2-proxy-creds created
Next, create a new file named oauth2-proxy-config.yaml which
will contain the configuration for oauth2_proxy:
nano oauth2-proxy-config.yaml
The values you’ll set in this file will override the Helm chart’s defaults.
Add the following code to the file:
oauth2-proxy-config.yaml
config:
existingSecret: oauth2-proxy-creds
extraArgs:
whitelist-domain: .int.your_domain
cookie-domain: .int.your_domain
provider: github
authenticatedEmailsFile:
enabled: true
restricted_access: |-
[email protected]
[email protected]
ingress:
enabled: true
path: /
hosts:
- auth.int.your_domain
annotations:
kubernetes.io/ingress.class: nginx
certmanager.k8s.io/cluster-issuer:
letsencrypt-prod
tls:
- secretName: oauth2-proxy-https-cert
hosts:
- auth.int.your_domain
This code does the following:
Now that you have the secret and configuration file ready, you can
install oauth2_proxy. Run the following command:
helm repo update \
&& helm upgrade oauth2-proxy --install
stable/oauth2-proxy \
--reuse-values \
--values oauth2-proxy-config.yaml
It might take a few minutes for the Let’s Encrypt certificate to be issued
and installed.
To test that the deployment was successful, browse to
https://auth.int.your_domain. You’ll see a page that prompts
you to log in with GitHub.
With oauth2_proxy set up and running, all that is left is to require
authentication on your services.
Conclusion
In this tutorial, you set up oauth2_proxy on your Kubernetes cluster and
protected a private service behind a GitHub login. For any other services
you need to protect, simply follow the instructions outlined in Step 4.
oauth2_proxy supports many different providers other than GitHub. To
learn more about different providers, see the official documentation.
Additionally, there are many configuration parameters that you might
need to adjust, although the defaults will suit most needs. For a list of
parameters, see the Helm chart’s documentation and oauth2_proxy’s
documentation.