Having devoured DevOps books for the past year and a half, it was interesting to find that some of them contained the terms Infrastructure as Code and Continuous Deployment. It was amazing to learn from the DevOps Handbook that companies were already doing these things in the early 2000s. They achieved it via virtualization while nowadays many projects are doing it via containerization, yet the goals of these two concepts are still the same:
- Infrastructure as code: define all your infrastructure in files, down to the network interface level.
- Continuous deployment: make production deployments a chore. Small, frequent, and, most important: automatic.
To experiment with these two concepts, here is a Drupal demo application. You can check out the repository at https://github.com/juampynr/drupal8-do.
The below screen recording shows a change being made in the configuration (the website's title), committed, and pushed. The GitHub Actions workflow, Kubernetes dashboard, and the Drupal application are then monitored:
It works! It is far from perfect, but it is a giant step as it is an entirely different way of hosting and deploying a site. Here is the homepage running:
In the next section, you will see an overview of the setup.
The setup
Here is the root of the GitHub repository with some annotations:
- .github contains the workflow explained in the previous section.
- definitions contain the Kubernetes objects.
- Dockerfile defines how to build the project's image.
Here is a diagram that illustrates what happens behind the screens when pushing code changes to the GitHub repository:
In the above diagram, GitHub Actions takes care of building the project and creating a release. At the same time, Kubernetes is in charge of the deployment, detaching the database volume from the MySQL pod and re-attaching it to the pod containing the new configuration. The same happens for the Drupal pod, which has a volume to host public files. If you are new to Kubernetes, there is a great introduction with animated diagrams on the official site.
Below is the GitHub Actions workflow, located in the repository at .github/workflows/ci.yml:
on:
push:
branches:
- master
name: Build and deploy
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Build, push, and verify image
run: |
echo ${{ secrets.PACKAGES_TOKEN }} | docker login docker.pkg.github.com -u juampynr --password-stdin
docker build --tag docker.pkg.github.com/juampynr/drupal8-do/app:${GITHUB_SHA} .
docker push docker.pkg.github.com/juampynr/drupal8-do/app:${GITHUB_SHA}
docker pull docker.pkg.github.com/juampynr/drupal8-do/app:${GITHUB_SHA}
- name: Install doctl
uses: digitalocean/action-doctl@v2
with:
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
- name: Save cluster configuration
run: doctl kubernetes cluster kubeconfig save drupster
- name: Deploy to DigitalOcean
run: |
sed -i 's|<IMAGE>|docker.pkg.github.com/juampynr/drupal8-do/app:'${GITHUB_SHA}'|' $GITHUB_WORKSPACE/definitions/drupal-deployment.yaml
sed -i 's|<DRUPAL_CRON>|${{ secrets.DRUPAL_CRON }}|' $GITHUB_WORKSPACE/definitions/drupal-deployment.yaml
kubectl apply -k definitions
kubectl rollout status deployment/drupal
- name: Update database
run: |
POD_NAME=$(kubectl get pods -l tier=frontend -o=jsonpath='{.items[0].metadata.name}')
kubectl exec $POD_NAME -c drupal -- vendor/bin/robo files:configure
kubectl exec $POD_NAME -c drupal -- vendor/bin/robo database:update
In essence, it builds a Docker image, pushes it, and then deploys it along with any other configuration changes into a Kubernetes cluster hosted on DigitalOcean.
Debugging
While setting up the workflow, there was a time when a check seemed necessary to find out why the site was not working. A search for how to open an SSH connection with the cluster turned up nothing. Further research revealed that you don't use SSH to connect to the cluster, but instead, you use kubectl
to open a shell against a container.
Kubernetes contexts (aka cluster management)
Kubernetes uses the concept of context to manage cluster configurations. For example, here is the output of listing contexts locally:
juampy@carboncete:~:$ kubectl config get-contexts
CURRENT NAME CLUSTER AUTHINFO
* do-sfo2-drupster do-sfo2-drupster do-sfo2-drupster-admin
minikube minikube minikube
do-sfo2-drupster is the current context, referencing the DigitalOcean cluster used for this article. The second one in the list refers to a cluster created by minikube, a development tool to create and run a Kubernetes cluster locally.
When the GitHub Actions workflow in the above section runs the command doctl kubernetes cluster kubeconfig save drupster, it is downloading the cluster configuration, saving it at ~/.kube/config, and setting it as the default context. From that point, all kubectl commands will run against such cluster.
doctl is the command-line interface to interact with DigitalOcean.
Opening an interactive session against a container
With the cluster configuration in place for kubectl, here is how to run a command against the container running Drupal. First, you need to identify the pod that hosts the container that runs Drupal, which you can figure out via kubectl get pods:
juampy@carboncete:~:$ kubectl get pods
NAME READY STATUS RESTARTS AGE
drupal-5b4f9996cf-dlvtw 1/1 Running 0 29h
drupal-cron-1590506940-zxglx 0/1 Completed 0 3m10s
drupal-cron-1590507000-g25g2 0/1 Completed 0 2m10s
drupal-cron-1590507060-7vmns 0/1 Completed 0 70s
drupal-cron-1590507120-rnn6t 0/1 Completed 0 10s
drupal-mysql-6594bfd66b-9j6zd 1/1 Running 0 29h
The pod to look for is drupal-5b4f9996cf-dlvtw since the other pods are for cron runs and MySQL. With the pod identifier you can run commands against the drupal container running within it:
juampy@carboncete:~:$ kubectl exec drupal-5b4f9996cf-dlvtw -c drupal -- ls
Dockerfile Makefile README.md RoboFile.php composer.json composer.lock config definitions docker-compose.yml docs scripts traefik.yml vendor web
We just sent an ls command to the container which returned the list of files and directories of the application. If run several commands against the Drupal container is required, you can open an interactive session:
juampy@carboncete:~:$ kubectl exec drupal-5b4f9996cf-dlvtw -c drupal -i -t -- bash
root@drupal-5b4f9996cf-dlvtw:/var/www/html# ls
Dockerfile Makefile README.md RoboFile.php composer.json composer.lock config definitions docker-compose.yml docs scripts traefik.yml vendor web
As a final note, this is a learning process. If you have any tips, feedback, or want to share anything about this topic, please post it here as a comment or tweet us @lullabot or @juampynr. Thanks in advance!
The next article in this series will cover each of the GitHub Actions steps in detail.
Acknowledgments
- Inspiring articles written by Alejandro Moreno and Jeff Geerling.
- Tutorials that help you get a mental map of how to fit each of the pieces together in the puzzle:
- This presentation by Tess (socketwench) is stellar in every single way.