As I wanted to get back to writing some more blog posts I was annoyed by the fact that I need to build, push and deploy the updated posts everytime something changes.
As I already managed my whole blog with hugo and within a git repository hosted on my very-own instance of Gitea I decided it’s time to get into self-hosting CI/CD for my personal projects.
why drone?
I checked out multiple other solutions like Gitlab, Jenkins, Bamboo and Circle CI but all of them seemed a bit overkill for my needs.
Also drone has ready to use helm charts for both the server and the runner (I’ll explain both later on) which is great considering I already have nearly all my services running inside a kubernetes cluster.
what makes drone different
Drone is based on docker images, basically any action in your pipeline is a Docker-Container.
That makes the system incredibly flexible because you can simply build plugins with new Containers
or use existing containers for your build-steps, for example golang
to build your app.
server and runner
Basically the drone server is the component you are interacting with either via drone-cli oder the web-gui. The server also receives the webhooks from your git repo and dispatches jobs to runners which do the actual work.
Runners come in a variety of sizes and shapes, for example they can run..
- in kubernetes
- via SSH
- in Docker
- on AWS
- on DigitalOcean
..and some more you can find all of them in their docs.
Obviously I’ll be using the kubernetes runner.
deploying drone to kubernetes
I choose to deploy drone via helm3 because it’s easy and supported.
You can simply add the drone helm repos to your helm3 installation via the following commands:
helm repo add drone https://charts.drone.io
helm repo update
To deploy I’d suggest checking the chart docs over at Github. Depending on your VCS the steps may vary slightly.
.drone.yml
This is the file which tells drone how to handle your repository, it contains a pipeline with all steps needed to build your project.
This website is built using the following .drone.yml
(I redacted confidential information)
---
kind: pipeline
type: kubernetes
name: default
steps:
- name: generatetag
image: hub.docker.com/alpine
commands:
- echo "${DRONE_REPO_BRANCH}-${DRONE_COMMIT_SHA},latest" >> .tags
- name: submodules
image: hub.docker.com/alpine/git
commands:
- git submodule update --init --recursive
- name: dockerbuild
image: plugins/docker
settings:
username:
from_secret: docker-username
password:
from_secret: docker-password
repo: hub.docker.com/youruser/yourimage
registry: hub.docker.com
- name: deploy
image: myprivatedockerrepo/drone-plugins/k8s-deploy
settings:
token:
from_secret: k8stoken
server:
from_secret: k8sserver
namespace:
from_secret: k8snamespace
deployment: marschallsystems
container: marschallsystems
repo: hub.docker.com/youruser/yourimage
tag: ${DRONE_REPO_BRANCH}-${DRONE_COMMIT_SHA}
As you see the file isn`t too complicated, every step has a name and an image. The image is the Docker-Image used for the step.
At first, we are using alpine to generate a .tags
file which contains all target tags
for the resulting Image.
The Second Step is building the Dockerfile
from which this blog is created
(I will cover this in another post and put a link over here).
This step will also take the tags from .tags
and tag the image and push it.
All credentials are supplied from drone secrets, these can be either set via the
web gui or via drone-cli.
At last, the image which is used in the kubernetes deployment is updated via a little custom drone plugin. Actually this “plugin” is really only a bash one-liner in a docker container.
deployment update plugin
To update the image we just need to issue a
kubectl set image deployment/deploymentname containername=newimagename:tag
building the image
As previously stated the image is no big deal. It consists of a script.sh
and a corresponding Dockerfile
. These two files need to be in the following
places.
.
├── src
│ └── script.sh
└── Dockerfile
script.sh
#!/bin/sh
/bin/kubectl --token $PLUGIN_TOKEN --server $PLUGIN_SERVER --namespace $PLUGIN_NAMESPACE --insecure-skip-tls-verify set image deployment/$PLUGIN_DEPLOYMENT $PLUGIN_CONTAINER=$PLUGIN_REPO:$PLUGIN_TAG
Dockerfile
FROM alpine
RUN apk add --no-cache curl ca-certificates
RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl && \
chmod 755 kubectl && \
mv kubectl /bin/
ADD src/script.sh /bin/
RUN chmod +x /bin/script.sh
ENTRYPOINT /bin/script.sh
Now we can build the image.
docker build . -t hub.docker.com/youruser/yourimage:tag
kubectl auth
Obviously we need some kind of authentication for the kubectl
command to work,
and we will leverage the power of drone secrets again.
To get a token to use for kubectl
we use a service-account
.
Todo so we apply following yml in the cluster (change namespaces according to your deployments needs)
apiVersion: v1
kind: ServiceAccount
metadata:
name: drone-deploy
namespace: marschallsystems
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
name: drone-deploy
namespace: marschallsystems
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get","list","patch","update"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: drone-deploy
namespace: marschallsystems
subjects:
- kind: ServiceAccount
name: drone-deploy
namespace: marschallsystems
roleRef:
kind: Role
name: drone-deploy
apiGroup: rbac.authorization.k8s.io
This will create the service account and the corresponding role
and bind them together. Now we need to get the exact name of the
secret which kubernetes created for the service account. To do so we use kubectl get secrets -n yournamespace
.
After that we can receive the token with the following command (replace the secret name to match yours).
kubectl get secrets drone-deploy-token-tn8t2 -o=jsonpath="{.data.token}" | base64 -d -i -
Congratulations you now have a token which you can save in drone as a secret
with the name k8stoken
.
conclusion
After just a few hours we have a working CI/CD solution with very little footprint.
At first, it might seem a bit overwhelming to describe every step in the yml
but
after all it’s quite easy if you get the hang of it.
I’ve been also surprised how easy it is to make new plugins, and I just love it!
I hope I’ve been able to show you how amazing drone is. Simple, fast and easy to learn!