A long time ago, in a job far, far away, I was tasked with switching our old-school LAMP stacks over to Kubernetes. My boss at the time, always starry-eyed for new technologies, announced the change should only take a few days—a bold statement considering we didn’t even have a grasp on how containers worked yet.
After reading the official docs and Googling around, I began to feel overwhelmed. There were too many new concepts to learn: there were the pods, the containers, and the replicas. To me, it seemed Kubernetes was reserved for a clique of sophisticated developers.
This post is what I would have liked to read at that time: a short, simple, no-nonsense guide on how the heck I go about deploying an application in Kubernetes.
I’ve put all the files we’ll be needing below. Feel to fork and clone the repository.
Semaphore CI/CD demo for Kubernetes
This is an example application and CI/CD pipeline showing how to build, test and deploy a microservice to Kubernetes using Semaphore 2.0.
- Ruby Sinatra as web framework
- RSpec for tests
- Packaged in a Docker container
- Container pushed to Docker Hub registry
- Deployed to Kubernetes
CI/CD on Semaphore
If you're new to Semaphore, feel free to fork this repository and use it to create a project.
The CI/CD pipeline is defined in
.semaphore directory and looks like this:
Local application setup
To run the microservice:
bundle install --path vendor/bundle bundle exec rackup
To run tests:
bundle exec rspec
To build and run Docker container:
docker build -t semaphore-demo-ruby-kubernetes . docker run -p 80:4567 semaphore-demo-ruby-kubernetes curl localhost > hello world :))
Copyright (c) 2019 Rendered Text
Distributed under the MIT License. See the file LICENSE.
Maybe I’m stating the obvious, but the first step is getting a Kubernetes cluster. Most cloud providers offer this service in one form or another, so shop around and see what fits your needs. The lowest-end machine and cluster size is enough to run our example app. I like starting from a three-node cluster, but you can get away with just one node.
After the cluster ready, download the kubeconfig file from your provider. Some let you download it directly from their web console, while others require a helper program. Check their documentation. We’ll need this file to connect to the cluster.
We can run anything in Kubernetes—as long as it has been packaged with Docker.
So, what does Docker do? Docker creates an isolated space, called a container, where application can run without interference. We can use Docker to put our applications in portable images that we can run anywhere without having to install libraries or dependencies.
To build a Docker image, we need the docker CLI and a Dockerfile like this:
FROM ruby:2.5 RUN apt-get update -qq && apt-get install -y build-essential ENV APP_HOME /app RUN mkdir $APP_HOME WORKDIR $APP_HOME ADD Gemfile* $APP_HOME/ RUN bundle install --without development test ADD . $APP_HOME EXPOSE 4567 CMD ["bundle", "exec", "rackup", "--host", "0.0.0.0", "-p", "4567"]
The file has the commands to build a ruby-based application image:
- Start from a pre-built ruby image.
- Install the build tools with apt-get.
- Copy Gemfile since it has all the dependencies.
- Install them with bundle.
- Copy the app source code.
- Define the listening port and the start command.
Build it and run it using:
$ docker build . -t hello-ruby $ docker run -p 4567:4567 hello-ruby
Step 3 is about putting the image where Kubernetes can find it.
Docker images are stored in Docker registries. Docker Hub is the default and provides unlimited free space for public repositories.
To upload an image to the registry:
- Connect to the Docker registry with
- Tag the image with your Docker username:
- Push it to the registry:
$ export DOCKER_USERNAME=<<YOUR USERNAME>> $ export DOCKER_PASSWORD=<<YOUR_PASSWORD>> $ docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD $ docker tag hello-ruby -t $DOCKER_USERNAME/hello-ruby $ docker push $DOCKER_USERNAME/hello-ruby
Automatic deployment is Kubernetes’ strong suit. All we need is to tell the cluster our final desired state and it will take care of the rest.
In Kubernetes we don’t manage containers directly. In truth, we work with pods. A pod is like a group of merry friends that always go together to the same places. Containers in a pod are guaranteed to run on the same node and have the same IP. They always start and stop in unison and, since they run on the same machine, they can share its resources.
To tell Kubernetes what we want we must write a manifest file. A minimal viable manifest looks like this:
# deployment.yml apiVersion: apps/v1 kind: Deployment metadata: name: hello-ruby spec: replicas: 1 selector: matchLabels: app: hello-ruby template: metadata: labels: app: hello-ruby spec: containers: - name: hello-ruby image: $DOCKER_USERNAME/hello-ruby:latest
There are several interesting things here:
Labels: resources can have a name and several labels, which are convenient to organize things.
Spec: defines the desired final state and the template used to create the pods.
Replicas: defines how many copies of the pod to create. We usually set this to the number of nodes in the cluster.
To complete the setup, we need a service. A service presents a fixed IP address to the world. We can use load balancer service to forward traffic to the pods:
# service.yml apiVersion: v1 kind: Service metadata: name: hello-ruby-lb spec: selector: app: hello-ruby type: LoadBalancer ports: - port: 80 targetPort: 4567
Kubernetes matches up the
selector with the
labels to connect services and pods.
Now to apply both files and you should get the application running in a few minutes:
$ kubectl apply -f service.yml $ kubectl apply -f deployment.yml
Check the cluster status with:
$ kubectl get deployment NAME READY UP-TO-DATE AVAILABLE AGE semaphore-demo-ruby-kubernetes 1/1 1 1 31s $ kubectl get service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.120.0.1 <none> 443/TCP 5d20h semaphore-demo-ruby-kubernetes-lb LoadBalancer 10.120.8.161 <pending> 80:31603/TCP 36s
Pssst... come near... I have to tell you a secret...
You don’t have to do all this by hand. You can use Continuous Integration and Delivery to test and deploy on your behalf.
The demo project we cloned in the beginning comes with everything you need to get started with the Semaphore CI/CD platform.
To get started with a free account, go to semaphoreci.com and sign up using your GitHub account.
Semaphore needs to know how to connect to your cluster. We can store sensitive data in Semaphore using [secrets].
Semaphore provides a secure mechanism to store sensitive information such as passwords, tokens, or keys.
In order to connect to your cluster, create a [secret] in the Semaphore website:
- On the left navigation bar, under Configuration, click on Secrets.
- Click on Create New Secret.
- The secret name is “do-k8s”.
- Upload the Kubeconfig file to
- Define any other environment variables needed to connect to your cloud.
- Click on the Save Changes button.
Create second secret to store the
dockerhub user and password:
Semaphore uses the YAML Syntax to define what the pipelines do at each step.
The pipeline files are located in the
semaphore.yml: tests the application.
docker-build.yml: build the Docker image and push it to Docker Hub.
deploy-k8s.yml: deploy the application.
deploy-k8s.yml. The basics for pipelines were discussed in a previous post so I’ll jump straight deployment job. The heart of the pipeline are blocks and jobs. We put our command in jobs, and our jobs in blocks.
The deploy block first imports the secrets we just created using the
blocks: - name: Deploy to Kubernetes task: secrets: - name: do-k8s - name: dockerhub
Then, we define the environment using the
env_vars property. You may need to add more variables.
env_vars: - name: KUBECONFIG value: /home/semaphore/.kube/dok8s.yaml
We have only one job in the block. It clones the Git repository with the checkout script, then does the declarative deployment.
jobs: - name: Deploy commands: - checkout # <PUT CLOUD-SPECIFIC LOGIN COMMANDS HERE (aws, gcloud, doctl, etc.)> - envsubst < deployment.yml | tee deployment.yml - kubectl apply -f deployment.yml
If you need cloud-specific commands to login, add them before the first kubectl command.
Semaphore now has all the information to make the deployment on our behalf.
Delete the deployment we did manually:
$ kubectl delete hello-ruby $ kubectl delete hello-ruby-lb
deployment.yml and set the number of
replicas to the number of nodes in your cluster. I’ll use three replicas.
Add the project to Semaphore:
- Go to https://semaphoreci.com
- Click on the + (plus) sign next to Projects to see a list of your repositories.
- Use the Choose button next to
Finally, make a push:
$ git add .semaphore $ git commit -m “first deployment” $ git push origin master
And watch Semaphore go:
When the “Docker Build” pipeline is complete, press on the Promote button to deploy:
We have some cloud specific tutorials here:
If you wish to learn more about how Semaphore can work Kubernetes and Docker check out these:
Did you find the post useful? Hit those ❤️ and 🦄, follow me or leave a comment below.
Interested in CI/CD and Kubernetes? We’re working on a free ebook, sign up, to receive it as soon as it’s published.
Thanks for reading!