Drone — CI Pipeline Engine
Drone a Continuous Integration platform helps the DevOps engineer a lot in building the pipelines for application easily. As stated it empowers the developers in many ways.
A DevOps pipeline is a set of steps that are executed in sequence or parallel to validate the basic functionality of the project is not broken. The primary steps can be to run the test cases or to build the output binary. For a cloud-native-based application, we can also have additional steps which include building the docker image, pushing it to the Docker registry, and then deploying it for integration testing. Whenever a new commit or change is pushed to a project in Source Control Management(SCM) the pipeline is triggered to validate that the new change wouldn’t break any existing functionality.
In this blog, we will explore Drone to build a basic pipeline for a sample golang application. The Drone platform primarily contains a drone server and a runner.
The drone server runs as a docker container that integrates with Github or Gitlab or Gitea or other supported SCM providers. Once integrated using the drone UI we can sync any one of the existing projects in the SCM with the drone to build a pipeline. A drone runner runs as a daemon that integrates with the drone server to execute the pipeline. It supports different types of runners like Docker, Kubernetes, ssh, exec, and others. Based on the user’s requirement, the runner would be selected and configured.
Once we have the drone server and runner in place, we define a configuration(usually .drone.yml) file which lists various steps that would be part of the pipeline. This YAML file should be part of the same repository or project for which the pipeline is built for. It would then be used by the drone server to execute the pipeline with the help of the drone runner.
The current blog outlines the steps to install a drone server with a private Gitlab SCM and then configure a drone runner with Kubernetes. Next, we will sync and build a pipeline for a sample go-lang project from Gitlab with the drone. The pipeline would define a sequence of steps that involve the execution of test-cases, building binary, building a docker image, and publish it to a private repository and finally, it also deploys the application on the Kubernetes cluster.
The use case is being executed on the linux platform with the following specifications:
- Ubuntu 20.04
- Docker 20.10.2
- Kubernetes v1.21.2 — kubeadm single node cluster with calico CNI
- GitLab Community Edition 13.12.3 — Private Gitlab running on the same host
Please note: As this is a testing environment so there is no DNS server to resolve DNS entries, hence all the configuration is done based on the IPs. And also all the applications — Gitlab, drone, Kubernetes are running on the same host, so some port re-configurations are done respectively.
Private Gitlab-ce installation
Below are some commands which help installation of gitlab-ce on ubuntu server. Detailed steps are available here.
$ sudo apt install curl openssh-server ca-certificates postfix
$ curl -sS https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh | sudo bash
$ sudo apt install gitlab-ce
$ vim /etc/gitlab/gitlab.rb == change external_url 'http://<ip>'
$ sudo gitlab-ctl reconfigure
In this setup, we are using a private SCM, but a similar use case can be extended to other SCM hosted publicly provided the drone server has connectivity to it. This would require running the drone platform on a public cloud.
Install Drone
The following steps illustrate the installation of drone server and drone runner in integration with private gitlab-ce.
Drone server Installation
Here we will discuss the installation of the drone server as a docker container. More info can be found here.
Step 1: Create an application entry of the drone server in Gitlab. Login to Gitlab server and navigate to user-> preferences-> applications and configure drone server as shown below. In this setup, we would be running the drone server at http://192.168.1.102:8081 correspondingly configure where you would be running your drone server in your setup. Also, allow API and read user access to the server.
Step 2: Once created, it generates an application-id and secret-key. Copy these ids to use when launching the drone server.
Step 3: Generate shared secret to be used by drone server and drone runner.
$ openssl rand -hex 16
Step 4: Create a drone server using the ids from step 2 and step 3. Configure DRONE_SERVER_HOST
with the IP and Port on which the server will be running, here it would be <192.168.1.102:8081> the same details provided in step 1. Also, configure the host port mapping in the publish
argument, it is 8081 in this case.
$ docker run \
--volume=/var/lib/drone:/data \
--env=DRONE_GITLAB_SERVER=http://192.168.1.102 \
--env=DRONE_GITLAB_CLIENT_ID=<application_id_from_step_2> \
--env=DRONE_GITLAB_CLIENT_SECRET=<secret_from_step_2> \
--env=DRONE_RPC_SECRET=servet_from_step_3 \
--env=DRONE_SERVER_HOST=<Ip_and_port_for_the_server_to_run> \
--env=DRONE_SERVER_PROTO=http \
--env=DRONE_USER_CREATE=username:root,admin:true \
--env=DRONE_LOGS_TRACE=true \
--env=DRONE_LOGS_DEBUG=true \
--env=DRONE_LOGS_PRETTY=true \
--publish=<port_of_drone_server>:80 \
--publish=443:443 \
--restart=always \
--detach=true \
--name=drone \
drone/drone# Check logs to see if drone server is running$ docker logs drone
{"interval":"30m0s","level":"info","msg":"starting the cron scheduler","time":"2021-06-24T08:37:39Z"}
{"interval":"24h0m0s","level":"info","msg":"starting the zombie build reaper","time":"2021-06-24T08:37:39Z"}
{"acme":false,"host":"192.168.1.102:8081","level":"info","msg":"starting the http server","port":":80","proto":"http","time":"2021-06-24T08:37:39Z","url":"http://192.168.1.102:8081"}
Step 5: Configure the drone server with Gitlab. Access the drone server on browser http://192.168.1.102:8081 and then follow the below steps
Click continue to redirect to Gitlab page.
Click on authorize. After authorization, you will be again redirected to drone server page.
After registering you would be redirected to the drone server dashboard which would list the projects available in Gitlab. This completes the drone server setup.
Drone runner Installation
Next, we move to the installation of the drone runner with the Kubernetes plugin. More info can be found here.
Drone runner will be running as a kind deployment in the Kubernetes cluster and integrate with drone server to launch the pipeline on the Kubernetes cluster. It helps in executing the steps defined in the pipeline. We will create the deployment in the default namespace.
Before creating the drone runner, edit the yaml file with the drone server details according to your setup. The following environment fields have to be updated in the deploy.yaml
file.
- DRONE_RPC_HOST — Drone server hostname and port
- DRONE_RPC_PROTO — Drone server protocol — http/https
- DRONE_RPC_SECRET — openssl secret created from step 3 above.
# deploy.yamlapiVersion: apps/v1
kind: Deployment
metadata:
name: drone
labels:
app.kubernetes.io/name: drone
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: drone
template:
metadata:
labels:
app.kubernetes.io/name: drone
spec:
containers:
- name: runner
image: drone/drone-runner-kube:latest
ports:
- containerPort: 3000
env:
- name: DRONE_RPC_HOST
value: 192.168.1.102:8081
- name: DRONE_RPC_PROTO
value: http
- name: DRONE_RPC_SECRET
value: 887a80e28b7183912e788173dbd5d07f$ kubectl apply -f deploy.yaml
deployment.apps/drone created
After creating the deployment, check the pod logs to see if the connection is established successfully with drone server.
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
drone-5bdf497677-srhkw 1/1 Running 0 36s$ kubectl logs drone-5bdf497677-srhkw
time="2021-06-28T10:38:54Z" level=info msg="starting the server" addr=":3000"
time="2021-06-28T10:38:54Z" level=info msg="successfully pinged the remote server"
time="2021-06-28T10:38:54Z" level=info msg="polling the remote server" capacity=100 endpoint="http://192.168.1.102:8081" kind=pipeline type=kubernetes
This completes the installation of drone platform. Next steps would be to define pipeline for the project which require testing, building and deploying the application.
Sample project creation
Here we will discuss the creation of a sample project in Gitlab and integrating it with drone.
Login to Gitlab and create a sample project.
Next open the drone dashboard and click on sync button on top right to sync the projects.
Click on the golang-app project to activate the repository.
This completes the project sync between Gitlab and drone server
Pipeline Creation
We will now discuss how to enable the pipeline for the created sample project. The pipeline definition for a project is done in a .yml file which is committed as a file and is part of the same repository.
As we are building a sample golang application that can be deployed as a microservice below are the steps we will execute as part of the pipeline
- Test & build — to run golang test and to build golang binary
- Publish — builds docker image for the application and push it to quay repository
- Deploy — Finally deploys the application with the latest built image
Here we are using a part of the commit id to tag the image while pushing it to quay and it is later used for deployment.
If you are not deploying or pushing the application to any private repository ignore the below steps.
From the above listed steps, we can see that there are some parameters required by the drone server before executing the pipeline. These include docker username and password to publish the image to quay and Kubernetes endpoint, API server key, and cert for deploying the application as microservice in the Kubernetes. These configurations can be specified as secrets in the drone server as shown below and are later used in the pipeline .yml file.
Navigate to project Settings->Secrets ->New Secret
Similarly create k8s_cert, k8s_token, docker_username, and docker_password secrets in the drone CI.
To retrieve the k8s server, cert, and token details for the server on which the application will be deployed using the below commands.
# To retrieve k8s server details
$ kubectl config view -o jsonpath="{.clusters[?(@.name==\"kubernetes\")].cluster.server}"# To get cert and token, first retrieve the secret associated with the service account desired. Here we are using the default sa in the default namespace.
$ kubectl get secret
NAME TYPE DATA AGE
default-token-4mc89 kubernetes.io/service-account-token 3 2d21h# Get token and cert
$ kubectl get secret default-token-4mc89 -o jsonpath='{.data.token}' | base64 --decode && echo
$ kubectl get secret default-token-4mc89 -o jsonpath='{.data.ca\.crt}' && echo
Using the above details create secrets in the drone, also create the private image repository credentials as secrets in the drone UI.
As we are using the default service account from the default namespace to deploy the application as a microservice in Kubernetes, we should also take care that the service account has proper RBAC policies associated with it so that the application can be deployed. Here we create role, cluster role, role binding, and cluster role binding for the default service account to enable it with proper RBAC permissions. Here is the yaml file used.
$ kubectl apply -f rbac.yaml
role.rbac.authorization.k8s.io/drone created
rolebinding.rbac.authorization.k8s.io/drone created
clusterrole.rbac.authorization.k8s.io/drone created
clusterrolebinding.rbac.authorization.k8s.io/drone created
YAML file creation
In this section, we will discuss how to define and commit the pipeline .yml file for the sample project repository.
First clone the repository. And add the golang files related to our project. In this application, we are running an http server that has 2 endpoints with some sample responses. The application go code and related files used are available here.
$ git clone https://github.com/SirishaGopigiri/drone-example.git$ git clone http://192.168.1.102/root/golang-app.git
Cloning into 'golang-app'...
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (3/3), done.$ cd golang-app# Copy files from github sample project
$ cp -r ../drone-example/golang/* .
Next, create the sample pipeline .yml file with the steps discussed.
# .drone.ymlkind: pipeline
type: kubernetes
name: test-go
steps:
- name: test
image: golang:alpine
commands:
- "apk add build-base"
- "go mod download"
- "go build -o app ."
- "go test -v"
- name: publish
image: plugins/docker
settings:
registry: quay.io
repo: quay.io/sirishagopigiri/golang-app
tags: [ "${DRONE_COMMIT_SHA:0:7}","latest" ]
username:
from_secret: docker_username
password:
from_secret: docker_password
- name: deliver
image: sinlead/drone-kubectl
settings:
kubernetes_server:
from_secret: k8s_server
kubernetes_cert:
from_secret: k8s_cert
kubernetes_token:
from_secret: k8s_token
commands:
- sed -i "s|golang-app:v1|golang-app:${DRONE_COMMIT_SHA:0:7}|g" deployment.yaml
- kubectl apply -f deployment.yaml
Below are some definitions for the keys in the pipeline .yml file
- type — defines the drone type runner for the pipeline, here as we are using Kubernetes
- steps — list of contains that would be created in the drone type runner, each executing the commands defined in that step
- Some steps have pre-defined logic built for them, example
plugins/docker
automatically builds and pushes the image - settings — are the environment variables passed to the container when launched.
Copy the drone.yml
file defined above to the repository and then commit the repository. Please note that the file name should match the configuration file name defined in the settings page for the project in the drone server.
# Copy the .drone.yml file to the repo
$ cp ../drone-example/golang-app/.drone.yml
Commit to trigger the pipeline
We will now commit and push the changes, which will automatically trigger the pipeline in drone which in turn will execute the steps in the pipeline.
$ ls -al
total 48
drwxrwxr-x 3 sirisha sirisha 4096 Jun 28 16:20 .
drwxrwxr-x 6 sirisha sirisha 4096 Jun 28 16:18 ..
-rw-rw-r-- 1 sirisha sirisha 434 Jun 28 16:18 app.go
-rw-rw-r-- 1 sirisha sirisha 1282 Jun 28 16:18 app_test.go
-rw-rw-r-- 1 sirisha sirisha 720 Jun 28 16:18 deployment.yaml
-rw-rw-r-- 1 sirisha sirisha 287 Jun 28 16:18 Dockerfile
-rw-rw-r-- 1 sirisha sirisha 819 Jun 28 16:20 .drone.yml
drwxrwxr-x 8 sirisha sirisha 4096 Jun 28 16:30 .git
-rw-rw-r-- 1 sirisha sirisha 69 Jun 28 16:18 go.mod
-rw-rw-r-- 1 sirisha sirisha 4285 Jun 28 16:18 go.sum
-rw-rw-r-- 1 sirisha sirisha 214 Jun 28 16:18 README.md$ git add .$ git commit -m "Initial commit"
[master e3292a4] Initial commit
8 files changed, 231 insertions(+), 2 deletions(-)
create mode 100644 .drone.yml
create mode 100644 Dockerfile
create mode 100644 app.go
create mode 100644 app_test.go
create mode 100644 deployment.yaml
create mode 100644 go.mod
create mode 100644 go.sum$ git push origin master
Enumerating objects: 12, done.
Counting objects: 100% (12/12), done.
Delta compression using up to 8 threads
Compressing objects: 100% (10/10), done.
Writing objects: 100% (10/10), 4.30 KiB | 4.30 MiB/s, done.
Total 10 (delta 0), reused 0 (delta 0)
To 192.168.1.102:root/golang-app.git
85d2248..e3292a4 master -> master
After pushing the changes, check the drone UI to see if the pipeline is triggered.
Alternatively, we can also see that there would be a pod created in Kubernetes default cluster namespace, which is managed by the drone runner running in the same namespace.
$ kubectl get pod --show-labels
NAME READY STATUS RESTARTS AGE LABELS
drone-1rxlonglx22qecnz1sii 1/4 NotReady 4 4m35s io.drone.build.event=push,io.drone.build.number=1,io.drone.name=drone-1rxlonglx22qecnz1sii,io.drone.repo.name=golang-app,io.drone.repo.namespace=root,io.drone=true
drone-5bdf497677-srhkw 1/1 Running 0 29m app.kubernetes.io/name=drone,pod-template-hash=5bdf497677
After the execution of the pipeline, we can access the logs for each step and verify that the execution has been successful or not. Below are the screenshots of logs for each step. Please note that clone is a default step added by drone to pull the latest commit changes.
From the final logs we can observe that the application has been deployed successfully as a microservice in the Kubernetes cluster. Checking the same using kubectl.
$ kubectl get deploy -n test
NAME READY UP-TO-DATE AVAILABLE AGE
golangapp-deploy 1/1 1 1 85s$ kubectl get pods -n test
NAME READY STATUS RESTARTS AGE
golangapp-deploy-6689fdc49b-9gcgd 1/1 Running 0 77s$ kubectl get svc -n test
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
golangapp-service ClusterIP 10.109.111.59 <none> 5000/TCP 88s
We will now try to access the application and observe the responses.
$ kubectl -n test run -i -t nginx --rm=true --image=nginx -- bash
If you don't see a command prompt, try pressing enter.
root@nginx:/# curl -X GET http://golangapp-service:5000
{"message":"hello world!!"}root@nginx:/#
root@nginx:/# curl -X GET http://golangapp-service:5000/app_version
{"message":"Sample golang app version 1.0 !!"}
This verifies that the pipeline execution and the application deployment have been successful.
Pushing another commit
Now that we have the pipeline built, we will now push another commit with some golang changes and see if how the pipeline executes. Below are the changes done to the golang application, we have added another API endpoint and its corresponding test cases.
--- a/app.go
+++ b/app.go
@@ -14,6 +14,11 @@ func RunServer() *gin.Engine {
"message": "Sample golang app version 1.0 !!",
})
})
+ r.GET("/testing", func(c *gin.Context) {
+ c.JSON(200, gin.H{
+ "message": "New api added to the application !!",
+ })
+ })
return r
}
diff --git a/app_test.go b/app_test.go
index 3beb302..a729119 100644
--- a/app_test.go
+++ b/app_test.go
@@ -51,3 +51,25 @@ func TestServeHTTPVersion(t *testing.T) {
}
}
+func TestServeHTTPNewAPI(t *testing.T) {
+ server := httptest.NewServer(RunServer())
+ defer server.Close()
+
+
+ resp, err := http.Get(server.URL+"/testing")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if resp.StatusCode != 200 {
+ t.Fatalf("Received non-200 response: %d\n", resp.StatusCode)
+ }
+ expected := `{"message":"New api added to the application !!"}`
+ actual, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if expected != string(actual) {
+ t.Errorf("Expected the message '%s' but got '%s'\n", expected,actual)
+ }
+}
+
We will now commit the code to trigger the pipeline.
$ git add .
$ git commit -m "Update go code"
[master 3d3aef0] Update go code
2files changed, 28 insertions(+), 4 deletions(-)
$ git push origin master
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 8 threads
Compressing objects: 100% (5/5), done.
Writing objects: 100% (5/5), 596 bytes | 596.00 KiB/s, done.
Total 5 (delta 4), reused 0 (delta 0)
To 192.168.1.102:root/golang-app.git
e3292a4..3d3aef0 master -> master
We can see that the pipeline has been triggered.
The clone, build and publish steps have the same logs as seen before, but in the deliver step we will see that the application will now be configured with the latest image. The namespace and the service object remains the same.
Checking the application using kubectl
$ kubectl get pods -n test
NAME READY STATUS RESTARTS AGE
golangapp-deploy-54c89c7d56-7hnj6 1/1 Running 0 2m20s$ kubectl get deploy -n test
NAME READY UP-TO-DATE AVAILABLE AGE
golangapp-deploy 1/1 1 1 22m$ kubectl -n test run -i -t nginx --rm=true --image=nginx -- bash
If you don't see a command prompt, try pressing enter.
root@nginx:/# curl -X GET http://golangapp-service:5000
{"message":"hello world!!"}root@nginx:/#
root@nginx:/# curl -X GET http://golangapp-service:5000/testing
{"message":"New api added to the application !!"}root@nginx:/#
root@nginx:/# curl -X GET http://golangapp-service:5000/app_version
{"message":"Sample golang app version 1.0 !!"}
We can observe that a new pod has been created for the existing deployment with the latest tag. This new image supports the new API that has been added.
Failed git push
Here we will try to push a commit which has failed test cases and observe the pipeline behaviour. Below are the changes listed.
diff --git a/app_test.go b/app_test.go
index a729119..dde377d 100644
--- a/app_test.go
+++ b/app_test.go
@@ -63,7 +63,7 @@ func TestServeHTTPNewAPI(t *testing.T) {
if resp.StatusCode != 200 {
t.Fatalf("Received non-200 response: %d\n", resp.StatusCode)
}
- expected := `{"message":"New api added to the application !!"}`
+ expected := `{"message":"New api added to the application"}`
actual, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
Commit and push the changes
We can observe that since the test case execution failed the pipeline is marked as failed and is not deployed in the cluster.
Conclusion
The drone CI platform helps in building an effective pipeline to help identify issues before deployment. Drone has support to integrate with different types of SCM like Gitea, Github, Gitlab, bitbucket, etc. It also has various runners support to run like exec, docker, Kubernetes, ssh etc. And since the drone pipeline steps are simple which involves specifying the docker container we can build various custom plugins according to our requirements.
Integrating the project with Drone helps in easy maintenance of the projects. It also makes debugging the problems and issues easy in case if something fails. As the drone pipeline is built using simple YAML file configuration it helps the DevOps engineer in an effortless configuration.