(FEAT): Added the Sedaro submission
All checks were successful
Deploy to Server / deploy (push) Successful in 23s
All checks were successful
Deploy to Server / deploy (push) Successful in 23s
This commit is contained in:
parent
bacbe620d4
commit
ef022579e6
520
src/journal/DevOps Pipeline Submission.md
Normal file
520
src/journal/DevOps Pipeline Submission.md
Normal file
@ -0,0 +1,520 @@
|
||||
Date: 2025/10/17
|
||||
Desc: My implementation of a CI/CD pipeline.
|
||||
|
||||
# DevOps Pipeline Submission
|
||||
###### Author: Hayden Hargreaves
|
||||
|
||||
###### Published: 10/17/2025
|
||||
|
||||
|
||||
# Sedaro Software Engineering Internship Take-Home Project
|
||||
|
||||
##### Hayden Hargreaves, October 10th 2025.
|
||||
|
||||
###### This project is not to be copied or used by any entity other than the intended audience, Sedaro
|
||||
|
||||
|
||||
|
||||
## Write Up
|
||||
|
||||
This article was written 100% by hand. With the ever-growing use of AI, it is important to show
|
||||
integrity in the workplace, and this submission is no exception. Furthermore, I love writing "articles"
|
||||
like this, as you can see from my own personal [blog](https://hhargreaves.net/journal).
|
||||
|
||||
This article can also be found on my website/blog with better formatting at this [link](https://hhargreaves.net/journal).
|
||||
As the Sedaro team has requested that these assets remain private, I will only leave the live resources
|
||||
available to the public for 7 days (from the time of submission) or until I have received feedback.
|
||||
If this does not work, please let me know, and I will take down any live applications that are
|
||||
currently running!
|
||||
|
||||
### Overview
|
||||
|
||||
To provide a short TL;DR of this submission, I implemented the following: A complete CI/CD pipeline
|
||||
which tests, lints, builds and deploys the checked-in code as soon as it is pushed. This allows
|
||||
software engineers to focus their efforts on building quality software and not on making sure the
|
||||
deployments are working.
|
||||
|
||||
By requiring checked-in code to meet standards (linters and formatters), the quality and readability
|
||||
of the code are improved. Automated testing allows the engineers the ability to write code, run some small
|
||||
tests locally, and then execute the entire test suite in the cloud while they continue to write code
|
||||
and solve problems.
|
||||
|
||||
For business, automated CI/CD pipelines save money and time, but they also allow for quicker time-to-market
|
||||
which grants them an edge over competitors. Deployments are less risky, smaller and more frequent.
|
||||
All of which allows the business a chance to advertise new features and begin user testing as soon as
|
||||
a feature has passed the defined rules.
|
||||
|
||||
### Demo
|
||||
|
||||
Since this pipeline is custom-built for my own personal server infrastructure, it may be hard to replicate
|
||||
the **exact** setup that I have. The only place you may run into an issue is in the server
|
||||
deployment using Cloudflare. I am not comfortable pushing my production server keys and details to
|
||||
a public submission or repository, so I have left that piece out of the script.
|
||||
|
||||
However, I know that the Cloudflare step is critical to the success of this pipeline, so I have created
|
||||
a deployment that exists on my server at [demo.gophernest.net](https://demo.gophernest.net). I will also work on submitting a
|
||||
video article showing the pipeline in action, just in case the team at Sedaro is unable (or does not
|
||||
want to) wire up all the pieces. I would not blame you!
|
||||
|
||||
The live copy of the Git repository can be found [here](https://git.gophernest.net/azpect/puny.card). Just like the other live
|
||||
elements, I will leave this repository public for exactly 7 days from submission. If more time is
|
||||
needed, do not hesitate to ask! Furthermore, if there are any issues accessing the repository, please
|
||||
let me know. It is also hosted on my own personal Git server, but migration to GitHub is seamless.
|
||||
|
||||
### Setup
|
||||
|
||||
However, if running it locally is the goal, here are the steps I followed during implementation.
|
||||
|
||||
##### Setup a Git repo
|
||||
|
||||
You may clone my project (as defined above in the **Demo** section), or copy the files provided
|
||||
in this `zip` file and push them to a Git repository. Once that is done, the magic should begin.
|
||||
|
||||
For best success, the working branch should be called `main`. All of the workflows execute on events
|
||||
triggered on the `main` branch.
|
||||
|
||||
Once the code is checked in and the repository is set up, the last thing you will need to do is define
|
||||
a few secrets. Inside the `.github/workflows/backend-build.yml` and `.github/workflows/frontend-build.yml`
|
||||
workflow files, you will notice there are two variables:
|
||||
|
||||
- **DOCKER_USERNAME**: Username for the working Docker hub account
|
||||
- **DOCKER_TOKEN**: Password or token for the working Docker hub account
|
||||
|
||||
These should be defined in your actions secrets tab before continuing. Otherwise, the build workflows
|
||||
will fail to build and deploy a new image.
|
||||
|
||||
##### Setting up Ansible
|
||||
|
||||
This submission utilizes **Ansible** as a solution for infrastructure management. To configure access to your server,
|
||||
you will need to update the values in the `infrastructure/inventory/hosts.yml` and
|
||||
`infrastructure/inventory/host_vars/*.yml`. The changes will be described below. If you encounter any
|
||||
issues, Ansible is well-documented, but I can also be of assistance if needed.
|
||||
|
||||
```yml
|
||||
# infrastructure/inventory/hosts.yml
|
||||
servers:
|
||||
hosts:
|
||||
server_name: # Whatever you would like to call your server! Remember this for later
|
||||
|
||||
```
|
||||
|
||||
```yml
|
||||
# infrastructure/inventory/host_vars/server_name.yml
|
||||
|
||||
# It is CRITICAL that this file has the same name as the server_name defined above.
|
||||
# Otherwise, Ansible will be unable to locate the values.
|
||||
|
||||
# Connection details
|
||||
ansible_user: username # Update to reflect your server's username
|
||||
ansible_connection: ssh
|
||||
key_file: ~/.ssh/keyfile # Pre-configured SSH key for key-based authentication
|
||||
|
||||
# Use my personal server for testing
|
||||
domain: yourserver.com # Server domain
|
||||
hostname: yourdomain # Server hostname
|
||||
|
||||
```
|
||||
|
||||
Once these updates have been made, you can execute the Ansible scripts as follows:
|
||||
|
||||
```bash
|
||||
cd infrastructure # must be run from ./infrastructure
|
||||
ansible-playbook ./playbooks/install-packages.yml -i ./inventory/hosts.yml --ask-become-pass
|
||||
ansible-playbook ./playbooks/system-update.yml -i ./inventory/hosts.yml --ask-become-pass
|
||||
ansible-playbook ./playbooks/deploy-application.yml -i ./inventory/hosts.yml --ask-become-pass
|
||||
```
|
||||
|
||||
These scripts will be later explained, but for now, running them in this order will yield a working
|
||||
output on the server. You will be prompted for a **BECOME PASS**; this is your server's sudo password.
|
||||
|
||||
|
||||
### Motivation
|
||||
|
||||
I was once told that DevOps is a dying field and that software engineers were taking over the roles
|
||||
of DevOps engineers. While that breaks my heart to hear, I was very inspired when I saw this position
|
||||
open, hiring DevOps engineers specifically. I am a software engineer by trade (and study), but I
|
||||
have been working for months, even years, on perfecting my own personal home server. This server
|
||||
is the host to all of my applications, a Git server, file servers, custom web apps, and so much more.
|
||||
However, in doing so, I learned a great deal about DevOps and automation.
|
||||
|
||||
All of my DevOps experience is a result of hands-on practice working on my server and creating countless
|
||||
pipelines. Each of my web applications can be found on my **gophernest** (the name of the server, I like Golang),
|
||||
domain. All of which are featuring a fully automated CI/CD pipeline, very similar to the one I have
|
||||
implemented for this submission.
|
||||
|
||||
Furthermore, I have minimal experience working in Rust, which unfortunately made it very hard to contribute
|
||||
to the compiler team. I mentioned in my interview that I am interested in working on
|
||||
a compiler or systems alike, but with my lack of knowledge, that did not seem feasible for this project.
|
||||
|
||||
From this point onward, I will begin describing the technical aspects of this project and the decisions
|
||||
I have made along way. It would take *days* to explain every detail, so I have only selected a few
|
||||
larger decisions to highlight in this article, but I would love to talk more about anything you may question
|
||||
in my submission!
|
||||
|
||||
### Nightly Builds vs. Continuous Integration (CI)
|
||||
|
||||
A common debate in the DevOps space is the use of **nightly builds** or **continuous integration.**
|
||||
A nightly build is exactly what it sounds like -- a defined time that occurs each day (usually at night)
|
||||
when the application is built based on the current state of the repository. A common pitfall of this
|
||||
solution is the "rush to submit" code before the build time. This practice of rushing can lead to bugs,
|
||||
poor software quality, skipped tests and overall, worsened performance. While it is definitely a step
|
||||
above many other practices, it is not my go-to choice.
|
||||
|
||||
|
||||
**Continuous integration** is the practice of building and deploying an application as soon as it is
|
||||
checked in and submitted to the master repository. For example, when code is merged into the main branch,
|
||||
it is instantly built and deployed to the production server. This instant deployment solves a lot of
|
||||
rushing issues that are commonly experienced with nightly builds.
|
||||
|
||||
In my experience working on many personal projects, continuous integration is a more straightforward solution,
|
||||
as it allows me to view my changes on my live site, minutes (or even seconds) after I merge them into
|
||||
the repository. This is a huge time saver and allows patches and fixes to be rolled out instantly
|
||||
upon resolution.
|
||||
|
||||
Henceforth, this submission implements **continue integration.**
|
||||
#### Linting and Formatting
|
||||
|
||||
This pipeline enforces the contributors to write "higher quality" code by requiring code to pass
|
||||
a lint check and a format check. Before the applications are built, they must first run through a
|
||||
series of linters and formatting tools to ensure the code submitted adheres to the defined guidelines.
|
||||
|
||||
This process is done using **GitHub workflows**. Since this application is built using many languages,
|
||||
many different tasks need to be executed; the Rust project, for example, uses `clippy`
|
||||
as a linter and `rustfmt` as a formatter. Before the code can be passed to the next step, the following
|
||||
workflows are executed and required to pass.
|
||||
|
||||
|
||||
```bash
|
||||
.github/workflows/black-check.yml # Python formatter
|
||||
.github/workflows/clippy.yml # Rust linter
|
||||
.github/workflows/eslint.yml # JS/TS linter
|
||||
.github/workflows/prettier-check.yml # JS/TS formatter
|
||||
.github/workflows/pylint.yml # Python linter
|
||||
.github/workflows/rustfmt-check.yml # Rust formatter
|
||||
```
|
||||
|
||||
Here is an example workflow:
|
||||
|
||||
```yml
|
||||
# .github/workflows/rustfmt-check.yml
|
||||
name: Rust Format Check
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
format-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
components: rustfmt
|
||||
- name: Check Rust formatting with rustfmt
|
||||
run: |
|
||||
cd queries
|
||||
cargo fmt -- --check
|
||||
```
|
||||
|
||||
>
|
||||
> Disclaimer! The apps in this project do not pass the linter or formatting check, so I was forced
|
||||
> to disable the requirement for these to pass. However, this is a straightforward fix.
|
||||
>
|
||||
|
||||
|
||||
#### Testing
|
||||
|
||||
Testing is a crucial part of software development. It helps ensure that existing code continues
|
||||
to work and that new code does not impact working functionality. Furthermore, effective testing
|
||||
helps engineers find and prevent bugs, which improves the quality and reliability of the software.
|
||||
|
||||
This application did not include many tests, only one, to be exact. Which was not very well defined;
|
||||
however, I was able to make it work. The pipeline I designed has a **test step** which occurs after
|
||||
the checking step above has passed. The final build step can not occur until after the tests have
|
||||
passed.
|
||||
|
||||
To implement this, a new **Dockerfile** was required. This file can be seen here: `./app/Dockerfile.test`.
|
||||
To run this Dockerfile, another workflow is needed. A sample can be seen below. The key part of this
|
||||
workflow is the `needs: []` directive, which defines that this workflow cannot run until the defined
|
||||
workflows have run and passed.
|
||||
|
||||
```yml
|
||||
# .github/workflows/backend-tests.yml
|
||||
name: Execute Dockerized Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
run_docker_tests:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# You should include the 'needs' keyword here to ensure linters and format checks pass first.
|
||||
needs:
|
||||
[
|
||||
backend-tests,
|
||||
black-check,
|
||||
clippy,
|
||||
eslint,
|
||||
prettier-check,
|
||||
pylint,
|
||||
rustfmt-check,
|
||||
]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build Test Docker Image
|
||||
run: docker build -f app/Dockerfile.test -t sedaro-backend-test:latest .
|
||||
|
||||
- name: Run Tests in Container
|
||||
run: docker run sedaro-backend-test:latest
|
||||
```
|
||||
|
||||
>
|
||||
> Disclaimer! The apps in this project do not pass the linter or formatting check, so I was forced
|
||||
> to disable the requirement for them to pass before this workflow executes.
|
||||
>
|
||||
|
||||
### Continuous Delivery vs. Continuous Deployment
|
||||
|
||||
Remember the difference between nightly builds and continuous integration? A similar debate exists
|
||||
between two *very* similar concepts: continuous **delivery** and continuous **deployment**. Before I discuss
|
||||
differences, let's talk about similarities. Both versions of **CD** define the automation of the build
|
||||
process and deployment process. i.e., the **build step** in this working example. However, there is one
|
||||
key difference that sets these two apart. When using continuous delivery, the build is completed
|
||||
automatically, but it is not deployed to the production server until it is manually approved. Continuous
|
||||
deployment is exactly as it sounds: code is built and deployed as soon as the automation occurs.
|
||||
|
||||
These significant difference allows teams to create very different workflows and environments. Teams using
|
||||
continuous delivery often find themselves with extra control over deployment time and can wait
|
||||
for an external action to trigger deployment. Teams that employ continuous deployment often find that
|
||||
users are able to use the most recent build of the application at all times. The most recent approved
|
||||
set of changes is deployed live for users as soon as it is approved.
|
||||
|
||||
In most of my personal projects, I use continuous deployment because it fits my needs. However, both
|
||||
sides have a distinct goal and use case in mind. This project features a **continuous deployment** pattern,
|
||||
as it is my preference.
|
||||
|
||||
### Implementation
|
||||
|
||||
Implementing a continuous deployment system is very simple. I used a tool called "Watchtower" in parallel
|
||||
with a simple Docker Compose setup. **Watchtower** is a tool that scans the Dockerhub registry for updates
|
||||
to a specific Docker image. With this configured to listen for all running images with a polling interval
|
||||
of **60**, we can see live changes with 60 seconds of fidelity.
|
||||
|
||||
To view the entire implementation, the *Docker* role used by the Ansible playbooks can be referenced
|
||||
here: `infrastructure/roles/docker/tasks/main.yml`. I will cover the Ansible setup right after this!
|
||||
|
||||
The last piece needed to finalize the CD portion of this project is a docker-compose file. The template
|
||||
defined below is configured to pull my two images, which are built during the CI phase, and Watchtower
|
||||
will poll them each every 60 seconds, and if there is an update, the images will be updated live during
|
||||
run-time.
|
||||
|
||||
>
|
||||
> Note! Notice the `restart: always` directive; this is crucial for ensuring the images start up when
|
||||
> the server is rebooted!
|
||||
>
|
||||
|
||||
```yml
|
||||
# infrastructure/roles/web/templates/docker-compose.yml.j2
|
||||
services:
|
||||
backend:
|
||||
image: azpect3120/puny.card.backend:latest
|
||||
container_name: backend
|
||||
ports:
|
||||
- "8000:8000"
|
||||
|
||||
restart: always
|
||||
|
||||
frontend:
|
||||
image: azpect3120/puny.card.frontend:latest
|
||||
container_name: frontend
|
||||
ports:
|
||||
- "3030:3030"
|
||||
|
||||
restart: always
|
||||
```
|
||||
|
||||
*You may notice that this file is not actually a `.yml` file; instead, it is a `.yml.j2` file. This is simply
|
||||
so Ansible can convert the file to a **template** which can be used to render variables in dynamically.
|
||||
In this project, there was no need for variables here, but it is best practice to support them just
|
||||
in case.*
|
||||
|
||||
|
||||
### Infrastructure as Code
|
||||
|
||||
|
||||
#### Ansible
|
||||
|
||||
To keep this short, I will provide the definition provided by Red Hat, the owners of Ansible.
|
||||
|
||||
>
|
||||
> "Ansible is an open source IT automation engine that automates provisioning, configuration
|
||||
> management, application deployment, orchestration, and many other IT processes. It is free to use,
|
||||
> and the project benefits from the experience and intelligence of its thousands of contributors."
|
||||
> ~Red Hat
|
||||
>
|
||||
|
||||
More information can be found [here](https://www.redhat.com/en/ansible-collaborative).
|
||||
|
||||
This submission uses Ansible in a straightforward way **to enable continuous deployment.** The entire
|
||||
CI process was implemented using only GitHub workflows, but we need a server to deploy this
|
||||
code! Ansible is the tool that I am most familiar with to configure servers. My home server is entirely
|
||||
configured using Ansible!
|
||||
|
||||
It is possible to duplicate a working server using the three **playbooks** provided in this submission.
|
||||
They should be run in this order:
|
||||
|
||||
```bash
|
||||
|
||||
cd infrastructure # must be run from ./infrastructure
|
||||
ansible-playbook ./playbooks/install-packages.yml -i ./inventory/hosts.yml --ask-become-pass
|
||||
ansible-playbook ./playbooks/system-update.yml -i ./inventory/hosts.yml --ask-become-pass
|
||||
ansible-playbook ./playbooks/deploy-application.yml -i ./inventory/hosts.yml --ask-become-pass
|
||||
```
|
||||
|
||||
*To keep this project simple, I did not include scripts to configure the entire server, such as users
|
||||
and Linux setup, so it is expected that your target server has a suitable user with root permissions
|
||||
set up beforehand.*
|
||||
|
||||
|
||||
#### Directory Setup
|
||||
|
||||
Those who are experienced with Ansible may recognize the directory structure defined below, but just
|
||||
in case this is your first time seeing it, I have left some short descriptions below that should help
|
||||
you understand what is going on.
|
||||
|
||||
When you actually *run* Ansible, you will run the **playbooks**. These playbooks call the roles, which
|
||||
are what tell Ansible what to do to the server and what state to leave it in. This setup is straightforward
|
||||
and allows for extreme portability and reuse.
|
||||
|
||||
```bash
|
||||
infrastructure
|
||||
├── Ansible.cfg # Ansible configuration
|
||||
├── inventory
|
||||
│ ├── hosts.yml # Host definitions
|
||||
│ └── host_vars # Host-specific variables
|
||||
├── playbooks
|
||||
│ ├── deploy-application.yml # Deploy the live demo
|
||||
│ ├── install-packages.yml # Install required packages on hosts
|
||||
│ └── system-update.yml # Update the system and reboot
|
||||
├── requirements.yml # Ansible requirements
|
||||
└── roles
|
||||
├── docker # Install docker
|
||||
├── packages # Install packages
|
||||
├── system # System configuration
|
||||
└── web # Install and deploy demo application
|
||||
```
|
||||
|
||||
### Application Changes
|
||||
|
||||
This submission is DevOps-focused; as a result, I did not work too much on the actual application itself.
|
||||
However, I did have to make a few small changes to some settings in the applications themselves, namely
|
||||
**CORS** configurations. In the backend application, a simple change was needed to accept connections
|
||||
from the deployed frontend.
|
||||
|
||||
```py
|
||||
# app/app.py
|
||||
|
||||
# ~line 22
|
||||
CORS(app, origins=["http://localhost:3030", "https://demo.gophernest.net"])
|
||||
```
|
||||
|
||||
In the frontend application, a very similar change was needed to make sure the requests are sent to
|
||||
the proper backend and not the local host. This can be done as follows:
|
||||
|
||||
```typescript
|
||||
// web/src/SimulateForm.tsx
|
||||
|
||||
// ~line 56
|
||||
const response = await fetch('https://demob.gophernest.net/simulation', {...});
|
||||
|
||||
// ./web/src/App.tsx
|
||||
|
||||
// ~line 31
|
||||
const response = await fetch('https://demob.gophernest.net/simulation');
|
||||
```
|
||||
|
||||
|
||||
### Hosting with Cloudflare
|
||||
|
||||
I will cover this section pretty briefly, to leave some things up for conversation during the interview,
|
||||
but also because I did not provide any of the code required to achieve this portion. Cloudflare offers
|
||||
a reverse proxy or **tunnel** which allows my server to deploy an application running locally to the live
|
||||
web without opening any ports or adjusting my firewall rules. This tool is 100% free and has been a
|
||||
lifesaver for all of my applications.
|
||||
|
||||
As such, I have implemented a detailed system that can deploy new DNS records and attach applications
|
||||
to them with minimal effort. However, these scripts contain lots of personal details that would
|
||||
give anyone access to my account and the ability to manage (and cause harm) to my tunnel infrastructure.
|
||||
Therefore, this part is not included in the final submission; however, I will attach a small code snippet
|
||||
below to *inspire* you to give it a try!
|
||||
|
||||
```yml
|
||||
...
|
||||
|
||||
- name: Configure cloudflare Tunnel DNS Record (CNAMEs) for gophernest.net
|
||||
community.general.cloudflare_dns:
|
||||
zone: "gophernest.net"
|
||||
record: "gophernest.net"
|
||||
type: "CNAME"
|
||||
value: "{{ tunnel_id }}.cfargotunnel.com"
|
||||
state: present
|
||||
proxied: true
|
||||
api_token: "{{ cloudflare_api_key }}"
|
||||
tags:
|
||||
- cloudflared
|
||||
- cnames
|
||||
- gophernest.net
|
||||
|
||||
- name: Configure cloudflare Tunnel DNS Record (CNAMEs) for *.gophernest.net
|
||||
community.general.cloudflare_dns:
|
||||
zone: "gophernest.net"
|
||||
record: "{{ item }}.gophernest.net"
|
||||
type: "CNAME"
|
||||
value: "{{ tunnel_id }}.cfargotunnel.com"
|
||||
state: present
|
||||
proxied: true
|
||||
api_token: "{{ cloudflare_api_key }}"
|
||||
loop: "{{ gophernest_cnames }}"
|
||||
tags:
|
||||
- cloudflared
|
||||
- cnames
|
||||
- gophernest.net
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
### Result
|
||||
|
||||
Finally, you made it all the way through! I hope you enjoyed reading this short story I wrote. I really
|
||||
enjoyed working on this project and writing this detailed report!
|
||||
|
||||
To sum up: this submission includes an entire, working **CI/CD** pipeline which runs linters,
|
||||
formatters, tests, builds and even deploys the application. All without any manual intervention. The
|
||||
pipeline enforces strict code quality and assures that reliable software is created to prevent bugs,
|
||||
crashes and application failure.
|
||||
|
||||
I want to thank the Sedaro team for giving me this opportunity to create something so detailed, and I
|
||||
look forward to meeting and discussing this project!
|
||||
|
||||
### How I Would Improve
|
||||
|
||||
There are two things I would improve if I had more time available to work on this project. The first
|
||||
being **updating the application code to pass the check stages.** The simple fact that the code provided
|
||||
does not pass a linter means that the entire pipeline would fail to run. To combat this, I was forced
|
||||
to remove the requirements for each step, which is not safe in a production environment.
|
||||
|
||||
The last thing I would like to have implemented was different environments: **production**, **staging**
|
||||
and **development**. These would exist as different branches in the repository and execute different
|
||||
actions. For example, on each branch, the application would be deployed to a different live application.
|
||||
This would allow developers to see a live demo of the application without impacting production.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user