Continuous Deployment, Infrastructure as Code, and Drupal: Part 1

Continuous Staircase

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.

Changes to Master Branch
https://github.com/juampynr/drupal8-do/actions

 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:

The homepage running on a Kubernetes cluster
The homepage running on a Kubernetes cluster

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.
Repository root
The repository root

Here is a diagram that illustrates what happens behind the screens when pushing code changes to the GitHub repository:

CD workflow
CD workflow

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

Juampy NR

Juampy Headshot
Loves optimizing development workflows. Publishes articles, books, and code.