Learn how to easily deploy applications to Kubernetes using werf, a powerful open source tool.
In this lab, you will build a container image with an example application, deploy it to Kubernetes, and modify the configuration and code of the deployed application.
We will use a tiny shell script as a demo. It returns pong in response to a request to the /ping
endpoint. You will set up an environment to use werf and deploy the application to a local Kubernetes cluster based on Minikube. Finally, you will scale the running application, modify its code, and see how werf re-deploys it to the K8s cluster.
About werf
werf is an Open Source tool for building CI/CD processes. It uses the Git repository as a single source of truth (this principle is called Giterminism). With werf, you can build app containers, publish them to the registry, deploy Helm charts to Kubernetes clusters and track the status of the deployment process until it successfully completes.
werf follows the Infrastructure as Code approach by describing the infrastructure declaratively. As a result, the basic pattern of werf-based application deployment is to store both the infrastructure configuration and the app code in the same Git repository. This renders the system fully deterministic and ensures that the resulting system state is identical to the one defined in Git. The changes in the Git repository are automatically propagated to the target environment. werf can work equally well with both remote and local environments.
Preparing the environment
Please note: after starting the lab, you will have to wait a couple of minutes for all the necessary components (Docker and Kubernetes) to be installed!
We will be using git in this lab, so let’s configure it:
git config --global user.email "YOUR E-MAIL"
git config --global user.name "YOUR NAME"
Installing werf
Follow the instructions on the werf homepage to install it. First, download the installer:
curl -sSLO https://werf.io/install.sh && chmod +x install.sh
Install werf:
sudo su -
./install.sh --version 1.2 --channel stable
When prompted, press Enter to use the default installation parameters.
source "$(~/bin/trdl use werf 1.2 stable)"
Check that werf is installed and ready to run:
werf version
Configuring the Container Registry
werf stores the built images in the container registry. We will use the one provided by Docker Hub.
Authorizing in Docker Hub
Create a Docker Hub ID and a private repository named werf-guide-app
to store the built images.
Use the docker login
command to log in to the new repository; enter your Docker Hub username and password:
docker login
Creating a Secret
To pull images from the private container registry, you have to create a Secret containing user credentials. Note that the Secret must be located in the same namespace as the application.
So first, we need to create a namespace for our application:
kubectl create namespace werf-guide-app
Set the default Namespace so that you don’t have to specify it every time you invoke kubectl
:
kubectl config set-context --current --namespace=werf-guide-app
Next, create a Secret named registrysecret
in the current namespace:
kubectl create secret docker-registry registrysecret \ --docker-server='https://index.docker.io/v1/' \ --docker-username='<DOCKER HUB USERNAME>' \ --docker-password='<DOCKER HUB PASSWORD>'
Don’t forget to replace the placeholders with your username and password!
If you made a mistake when creating a secret, you would have to create it again. But first, delete the existing Secret using the following command:
kubectl delete secret registrysecret
Building an image
Our sample application’s code and all necessary configurations are stored in a tarball. Extract them:
tar -xvzf examples.tar.gz
Navigate to the first example directory:
cd ~/examples/001
We’ve prepared everything for you here; however, keep in mind that in real life, you will need a git repository in your project directory and have to commit your changes!
About the app
The application directory contains the following files:
. ├── Dockerfile ├── start.sh └── werf.yaml
start.sh
is the script that returns the response when requested:
#!/bin/sh RESPONSE="pong" while true; do printf "HTTP/1.1 200 OK\n\n$RESPONSE\n" | ncat -lp 8000 done
Dockerfile
contains all the steps required to build an application image:
FROM alpine:3.14 WORKDIR /app # Install app dependencies. RUN apk add --no-cache --update nmap-ncat # Add to the image a script to run the echo server and set the permission to execution. COPY start.sh . RUN chmod +x start.sh
The primary werf configuration file, werf.yaml
, specifies Dockerfile
to use when building an application image using werf:
project: werf-guide-app configVersion: 1 --- image: app dockerfile: Dockerfile
The werf.yaml
file can describe the assembly of multiple images. There are also some additional settings for building an image. You can learn more about them in the documentation.
Building using werf
Initiate the build using the werf build
command:
werf build
Starting the application
You can run the container locally using the built image via the werf run
command:
werf run app --docker-options="-ti --rm -p 8000:8000" -- /app/start.sh
Here, the --docker-options
flag sets the Docker parameters, while the command to run in the container is specified at the end (after two hyphens).
Open a new terminal and check if the application is running:
curl http://127.0.0.1:8000/ping
You should see /pong
in response.
Deploying the application
Navigate to the next example directory:
cd ~/examples/002
About the app
This directory contains several extra files, Helm templates. They define the Kubernetes resources that will be used to deploy our app to the K8s cluster.
. ├── .dockerignore ├── .helm │ └── templates │ ├── deployment.yaml │ ├── ingress.yaml │ └── service.yaml ├── Dockerfile ├── start.sh └── werf.yaml
The Deployment resource (deployment.yaml
) creates a set of resources for launching the application. Here are its contents:
apiVersion: apps/v1 kind: Deployment metadata: name: werf-guide-app spec: replicas: 1 selector: matchLabels: app: werf-guide-app template: metadata: labels: app: werf-guide-app spec: imagePullSecrets: - name: registrysecret containers: - name: app image: {{ .Values.werf.image.app }} command: ["/app/start.sh"] ports: - containerPort: 8000
{{ .Values.werf.image.app }}
means that the template engine inserts the full name of the app Docker image into the manifest. Note that you must use the component name specified in werf.yaml
(app in our case) to access this value.
werf automatically inserts full image names it is going to build and other service data into the Helm Chart values (.Values
). You can access them using the werf
key.
werf only rebuilds images if the added files (those used in the Dockerfile COPY/ADD
instructions) are changed or if werf.yaml
itself is changed. The image tag will also change during a rebuild resulting in the Deployment update. If no changes were made to the files mentioned above, werf would not initiate the rebuild. The Deployment and the resources created will not be redeployed since the latest application version is already running in the cluster.
The Service resource (service.yaml
) allows other applications in the cluster to connect to your application:
apiVersion: v1 kind: Service metadata: name: werf-guide-app spec: selector: app: werf-guide-app ports: - name: http port: 8000
The Ingress resource (ingress.yaml
) manages external access to the cluster (unlike a Service in Kubernetes, which defines the application access policy within the cluster). The Ingress configuration defines what Service to use for external traffic coming to the werf-guide-app.test
domain.
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: kubernetes.io/ingress.class: nginx name: werf-guide-app spec: rules: - http: paths: - path: /ping pathType: Exact backend: service: name: werf-guide-app port: number: 8000
Deploying to Kubernetes
The werf converge
command builds the application image and deploys it to Kubernetes:
git add . && git commit -m WIP
werf converge --repo <DOCKER HUB USERNAME>/werf-guide-app
Don’t forget to replace <DOCKER HUB USERNAME> with your username.
Let’s check that the application is running and for this, there are two ways to access the application:
- First, you have to find out the application service IP address:
kubectl get -n werf-guide-app services
Now, sending a request to the application should result in a pong
response.
curl http://XXX.XXX.XXX.XXX:8000/ping
- Second, you can access the application through ingress, click on the
app-80
URL under the Lab URLs section and add/ping
at the end of the URL for accessing the application.
kubectl get ingress -n werf-guide-app
You will receive result in a pong
response.
Making changes
In this section, you will make changes to a running application and its infrastructure and learn how the infrastructure-as-code (IaC) approach works.
Navigate to the next example directory:
cd ~/examples/003
Scaling
The web server is a part of the werf-guide-app
Deployment. Let’s see how many its replicas are running:
kubectl get pods
Now, change the number of replicas to 4 right in the Kubernetes configuration:
kubectl edit deployment werf-guide-app
The above command will launch the text editor (vim). Set spec.replicas=4
(press i
and edit the number of replicas), save the file, and close the editor (press ESC
, then :wq
). Let’s see how many replicas are running now:
kubectl get pods
As you can see, the cluster has been scaled manually. Now, running werf converge
will bring our Kubernetes cluster back to its original state – the one specified in our Kubernetes resource configuration files (manifests):
git add . && git commit -m WIP
werf converge --repo <DOCKER HUB USERNAME>/werf-guide-app
Don’t forget to replace <DOCKER HUB USERNAME> with the username you created earlier.
Check the number of running replicas:
kubectl get pods
The number of replicas matches the one specified in the Git repository. As you can see, werf brought the cluster to the state described in the current Git commit. This principle is called Giterminism.
But how does one respect Giterminism and scale the cluster in the right fashion? Well, you have to edit the deployment.yaml
file and commit the changes to the repository.
vim .helm/templates/deployment.yaml
apiVersion: apps/v1 kind: Deployment metadata: name: werf-guide-app spec: replicas: 4 # edit and change replicas to 4 selector: matchLabels: app: werf-guide-app template: metadata: labels: app: werf-guide-app spec: imagePullSecrets: - name: registrysecret containers: - name: app image: {{ .Values.werf.image.app }} command: ["/app/start.sh"] ports: - containerPort: 8000
Now, commit the changes and rebuild the application:
git add . && git commit -m WIP
werf converge --repo <DOCKER HUB USERNAME>/werf-guide-app
Don’t forget to replace <DOCKER HUB USERNAME> with the username created earlier.
Let’s check how many replicas are running now:
kubectl get pods
What if we decrease their number back to one? Well, let’s find out! Edit the deployment.yaml
file:
apiVersion: apps/v1 kind: Deployment metadata: name: werf-guide-app spec: replicas: 1 # edit and change replicas to 1 selector: matchLabels: app: werf-guide-app template: metadata: labels: app: werf-guide-app spec: imagePullSecrets: - name: registrysecret containers: - name: app image: {{ .Values.werf.image.app }} command: ["/app/start.sh"] ports: - containerPort: 8000
Commit the changes and rebuild the application:
git add . && git commit -m WIP
werf converge --repo <DOCKER HUB USERNAME>/werf-guide-app
Don’t forget to replace <DOCKER HUB USERNAME> with the username created earlier.
Making changes to the application
Our application is a basic echo server. When requested using
curl http://XXX.XXX.XXX.XXX:8000/ping
… it responds with the pong
string.
or through the ingress, click on the app-80
URL under the Lab URLs section and add /ping
at the end of URL for accessing the application.
kubectl get ingress -n werf-guide-app
you can find IP of application using kubectl get -n werf-guide-app services
Let’s change the response and redeploy the updated application to the cluster. Open start.sh
in the text editor and replace the response with something else (e.g., Hello world
):
vim start.sh
#!/bin/sh RESPONSE="Hello world" while true; do printf "HTTP/1.1 200 OK\n\n$RESPONSE\n" | ncat -lp 8000 done
Commit the changes and rebuild the application:
git add . && git commit -m WIP
werf converge --repo <DOCKER HUB USERNAME>/werf-guide-app
Don’t forget to replace <DOCKER HUB USERNAME> with the username created earlier.
Check the result:
curl http://XXX.XXX.XXX.XXX:8000/ping
or through the ingress, click on the app-80
URL under the Lab URLs section and add /ping
at the end of URL for accessing the application.
kubectl get ingress -n werf-guide-app
The server will respond with Hello world
. Congratulations, you did it!
Conclusion
This concludes our lab. In it, you learned how to use werf to organize local application development and speed up the process of deploying applications to a cluster.
Feel free to ask your questions or share ideas and suggestions in our werf chatroom. Note that werf also features detailed guides for different programming languages/frameworks with app source code examples and related infrastructure configurations (IaC).