werf: deploy apps to Kubernetes

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).

Join Our Newsletter

Share this article:

Table of Contents