Frontend Automation
I’ve completed the DevOps mod for Chunk 1 of the Cloud Resume Challenge. I’ve learned so much, especially that there are still things I don’t know, and I’m excited to continue this project and learn a lot more!
Here is what I accomplished in this chunk:
- I created a static website using the Hugo static site generator.
- I hosted the site using Azure Storage and Azure CDN.
- I created a basic workflow using GitHub Actions, including:
- Deploy Azure infrastructure using Terraform,
- Build the Hugo static site each time I push a change to my repository,
- Upload the new website files to Azure Storage, and
- Clear the Azure CDN cache.
- I built a simple test battery using Cypress and appended it to the GitHub Actions workflow.
Challenges⌗
Uploading Files⌗
The first major challenge that I had to overcome was automating the uploading of my website files. I struggled to get this working, constantly getting this error:
ERROR: incorrect usage: source must be an existing directory
The solution was that I had to get my permissions straight for GitHub Actions and in Azure. On the Azure side, the security principle I created for automation needed to have Contributor permissions, and on GitHub, workflow permissions needed to be read and write, “Allow GitHub to create and approve pull requests” had to be checked, and my fine-grained personal access token needed to also be given read and write access to code.
Cypress Testing⌗
Configuring Cypress testing was much more difficult to figure out. I though I had it set up in GitHub Actions correctly, but every time I ran the workflow I would get one of various errors, including the following:
Cannot find module 'cypress'
Require stack:
- /home/runner/work/_actions/cypress-io/github-action/v6/dist/index.js
Action failed. Missing package manager lockfile. Expecting one of package-lock.json (npm), pnpm-lock.yaml (pnpm) or yarn.lock (yarn) in working-directory /home/runner/work/techno-literate-blog/techno-literate-blog
I’m sure some of you reading this can already see the problem, but it wasn’t obvious to me and I didn’t find any documentation that directly stated what the cause was. In short, I hadn’t actually installed Cypress! The first step was to install Node.js locally, and run npm init
in my local git repository. Then, I was able to use npm install cypress --save-dev
to properly install Cypress.
Cypress appears to be quite powerful and capable, and I’m not sure I’ve entirely wrapped my brain around it yet. I wanted my Cypress tests to just loop through my website and run on every single page, but I don’t think that is how the software is intended to be used. I finally ended up making a couple very specific tests to run every time I update the site: First, make sure every page in the sitemap generated by Hugo is accessible, and second, make sure CSS is rendering properly on the home page (a problem I had with Hugo early on).
Final Thoughts and Code⌗
I’m very proud of myself for getting through this chunk, and especially for tackling the optional DevOps mod. As I stated before, I can tell that there are still many things for me to learn, but I feel much more confident in my ability to do so after successfully getting this site online!
Here is the most important code from this chunk.
Github Actions workflow⌗
name: CI/CD Release Pipeline
on:
push:
branches:
- main
jobs:
terraform:
name: 'Terraform'
env:
ARM_CLIENT_ID: ${{ secrets.AZURE_TF_CLIENT_ID }}
ARM_CLIENT_SECRET: ${{ secrets.AZURE_TF_CLIENT_SECRET }}
ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
ARM_TENANT_ID: ${{ secrets.AZURE_TF_TENANT_ID }}
TF_VERSION: 1.6.4
runs-on: ubuntu-latest
environment: production
defaults:
run:
shell: bash
working-directory: ./terraform
steps:
- name: Checkout
uses: actions/checkout@v4
- name: 'Setup Terraform'
uses: hashicorp/setup-terraform@v2
with:
terraform_version: ${{ env.TF_VERSION }}
- name: 'Terraform fmt'
id: fmt
run: terraform fmt -check
- name: 'Terraform init'
id: init
run: |
set -a
source ../.env.backend
terraform init \
-backend-config="resource_group_name=$TF_VAR_state_resource_group_name" \
-backend-config="storage_account_name=$TF_VAR_state_storage_account_name"
- name: Terraform validate
id: validate
run: terraform validate -no-color
- name: Terraform Plan
id: plan
run: terraform plan -no-color -var-file="variables.tfvars"
- name: Terraform apply
id: apply
run: terraform apply -auto-approve -var-file="variables.tfvars"
build-hugo:
runs-on: ubuntu-latest
needs: terraform
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: write
steps:
- name: Git Checkout
uses: actions/checkout@v4
with:
submodules: true
fetch-depth: 0
token: ${{ secrets.TOKEN }}
- name: Setup Hugo
uses: peaceiris/actions-hugo@v2
with:
hugo-version: 'latest'
extended: true
- name: Build Project
run: hugo --minify
- name: Release Assets
uses: peaceiris/actions-gh-pages@v3
if: github.ref == 'refs/heads/main'
with:
personal_token: ${{ secrets.TOKEN }}
publish_dir: ./public
- name: Azure Login
uses: azure/[email protected]
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Upload to Azure Storage
uses: azure/CLI@v1
with:
inlineScript: |
az storage blob upload-batch -d "\$web" -s ./public --connection-string "${{ secrets.AZURE_STORAGE_CONNECTION_STRING }}" --overwrite true
- name: Purge Azure CDN Resources
run:
az cdn endpoint purge -n ${{ secrets.AZURE_CDN_ENDPOINT }} --profile-name ${{ secrets.AZURE_CDN_PROFILE_NAME }} --content-paths "/*" --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} --no-wait
- name: Dispose Azure Service Principal Session
run: |
az logout
cypress-run:
name: Cypress run
needs: build-hugo
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Cypress run
uses: cypress-io/github-action@v6
Terraform Configuration⌗
resource "azurerm_resource_group" "res-0" {
location = "westus2"
name = "production_storage"
}
resource "azurerm_cdn_endpoint_custom_domain" "res-1" {
cdn_endpoint_id = var.endpoint_id
host_name = var.domain_name
name = "resume-techno-literate-com"
cdn_managed_https {
certificate_type = "Dedicated"
protocol_type = "ServerNameIndication"
}
depends_on = [
azurerm_cdn_endpoint.res-10,
]
}
resource "azurerm_storage_account" "res-3" {
account_replication_type = "LRS"
account_tier = "Standard"
allow_nested_items_to_be_public = false
cross_tenant_replication_enabled = false
location = "westus2"
name = "tlprodstore"
resource_group_name = "production_storage"
custom_domain {
name = "resume.techno-literate.com"
}
static_website {
error_404_document = "404.html"
index_document = "index.html"
}
}
resource "azurerm_storage_container" "res-5" {
name = "$web"
storage_account_name = "tlprodstore"
}
resource "azurerm_cdn_profile" "res-9" {
location = "global"
name = "technoliterate-cdn"
resource_group_name = "production_storage"
sku = "Standard_Microsoft"
}
resource "azurerm_cdn_endpoint" "res-10" {
is_compression_enabled = true
location = "global"
name = "techno-literate-cdn"
origin_host_header = var.endpoint_origin_hostname
profile_name = "technoliterate-cdn"
resource_group_name = "production_storage"
origin {
host_name = var.endpoint_origin_hostname
name = var.endpoint_origin_name
}
depends_on = [
azurerm_cdn_profile.res-9,
]
}
Cypress Tests⌗
describe('Sitemap Test', () => {
let urls = [];
before(async () => {
const response = await cy.request('https://resume.techno-literate.com/sitemap.xml');
urls = Cypress.$(response.body).find('loc').toArray().map(el => el.innerText);
})
it('should load each page successfully', () => {
urls.forEach(cy.visit);
});
});
describe('CSS test', () => {
it('should have CSS', () => {
cy.visit('https://resume.techno-literate.com/').get('.blue').should('have.css','background-color','rgb(29, 33, 44)');
});
});