Skip to content

Blog

GitOps Project: Automating AWS infrastructure deployment with Terraform and GitHub Actions

Overview

As infrastructure becomes more complex, ensuring automation, security, and compliance is crucial. This blog post walks through setting up a GitOps pipeline that simplifies AWS infrastructure management using Terraform and GitHub Actions. Whether you're an engineer looking to automate deployments or a security-conscious DevOps professional, this guide provides a structured approach to implementing Infrastructure as Code (IaC) best practices.

This blog post documents my experience with the MoreThanCertified GitOps Minicamp, where students set up a fully automated GitOps pipeline to manage AWS infrastructure using Terraform and GitHub Actions. The goal is to implement Infrastructure as Code (IaC) best practices, enforce security policies, and automate infrastructure deployment.

This tutorial is meant as an overview "at-a-glance" summary of the steps I took to implement the pipeline from project start, to resource deployment. The MoreThanCertified GitOps Minicamp goes into a lot more detail and covers all the topics in this post in more depth. I would highly recommend checking the course out if you haven't already.

In this tutorial, I will guide you through:

  • Setting up GitHub Codespaces for development

  • Configuring a Terraform backend in AWS with CloudFormation

  • Deploying AWS resources with Terraform

  • Running security, cost, and policy checks using GitHub Actions

  • Implementing a CI/CD pipeline with GitHub Actions

Visualizing the Pipeline Architecture

The diagram below illustrates the end-to-end GitOps workflow:

  • GitHub Actions automates security scans, cost checks, and Terraform execution.
  • Terraform manages AWS infrastructure, storing state in an S3 backend.
  • OpenID Connect (OIDC) ensures secure authentication to AWS.

Basic Project Diagram

Setting Up GitHub Codespaces for Development

Prerequisites

  • Previous knowledge and use of Github is assumed.

  • A GitHub repository needs to be created where the Terraform code will be stored.

  • GitHub Codespaces enabled in your repository settings.

Enable Codespaces in GitHub

  1. Navigate to Settings > Codespaces in your repository.

  2. Ensure that Codespaces is enabled for your repository.

  3. Create a new Codespace and open it.

  4. The Codespace should open the browser based IDE, in the root of the repo you chose.

Configure Development Environment

Install Terraform in Codespaces

  • Copy, paste, and then run the following in your terminal.
Terraform install script
# Update package list and install dependencies
sudo apt-get update && sudo apt-get install -y gnupg software-properties-common

# Add HashiCorpโ€™s GPG key
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg

# Add the Terraform repository
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list

# Update and install Terraform
sudo apt-get update && sudo apt-get install -y terraform

# Verify installation
terraform version

Verify Terraform Installation

terraform -version

Configuring a Terraform Backend in AWS Using CloudFormation

Deploying an OIDC Role for GitHub Actions

To allow GitHub Actions to authenticate securely with AWS, we use an OIDC (OpenID Connect) role. This CloudFormation template sets up the necessary IAM role and OIDC provider.

Add the following code to a new template file cfn > oidc-role.yaml

oidc-role.yaml
Parameters:
  Repo:
    Description: The GitHub organization/repo for which the OIDC provider is set up
    Type: String 
Resources:
  MyOIDCProvider:
    Type: 'AWS::IAM::OIDCProvider'
    Properties:
      Url: 'https://token.actions.githubusercontent.com'
      ClientIdList:
        - sts.amazonaws.com
      ThumbprintList:
        - 6938fd4d98bab03faadb97b34396831e3780aea1
        - 1c58a3a8518e8759bf075b76b750d4f2df264fcd
  gitops2024Role:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Federated: !Sub >-
                arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com
            Action: 'sts:AssumeRoleWithWebIdentity'
            Condition:
              StringLike:
                'token.actions.githubusercontent.com:sub': !Sub 'repo:${Repo}:*'
              StringEquals:
                'token.actions.githubusercontent.com:aud': sts.amazonaws.com
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/PowerUserAccess'
Outputs:
  RoleName:
    Description: 'The name of the IAM role for GitHub Actions'
    Value:
      Ref: gitops2024Role
    Export:
      Name:
        Fn::Sub: '${AWS::StackName}-RoleName'

Prerequisites

  • You must have an AWS account set up in advance.

  • An IAM user with sufficient permissions to create S3 buckets, DynamoDB tables, and IAM roles.

  • A remote Terraform backend ensures state consistency and allows multiple users to collaborate.

Why CloudFormation?

CloudFormation is used to create the backend infrastructure as a best practice. This ensures automation and easy re-deployment.

Steps (CodeSpaces)

Add the following code to a new template file cfn > backend-resources.yaml

backend-resources.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: CloudFormation template to create S3 and DynamoDB for Terraform Backend

Parameters:
  S3BucketName:
    Type: String
    Description: The name of the S3 bucket to be created for storing Terraform state files.
    Default: gitops-tf-backend

  DynamoDBTableName:
    Type: String
    Description: The name of the DynamoDB table to be created for Terraform state locking.
    Default: GitopsTerraformLocks

Resources:
  TerraformBackendBucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: !Ref S3BucketName
      VersioningConfiguration:
        Status: Enabled

  TerraformBackendDynamoDBTable:
    Type: 'AWS::DynamoDB::Table'
    Properties:
      TableName: !Ref DynamoDBTableName
      AttributeDefinitions:
        - AttributeName: LockID
          AttributeType: S
      KeySchema:
        - AttributeName: LockID
          KeyType: HASH
      ProvisionedThroughput:
        ReadCapacityUnits: 5
        WriteCapacityUnits: 5
      SSESpecification:
        SSEEnabled: true

Outputs:
  TerraformBackendBucketName:
    Description: "S3 bucket name for the Terraform backend."
    Value: !Ref TerraformBackendBucket
    Export:
      Name: !Sub "${AWS::StackName}-TerraformBackendBucketName"

  TerraformBackendDynamoDBTableName:
    Description: "DynamoDB table name for the Terraform backend."
    Value: !Ref TerraformBackendDynamoDBTable
    Export:
      Name: !Sub "${AWS::StackName}-TerraformBackendDynamoDBTableName"

Steps (AWS Console)

  1. Log in to the AWS Management Console and navigate to the CloudFormation service.

  2. Click on Create Stack and select With new resources (standard).

  3. In the Specify template section, select Upload a template file and upload the backend-resources.yml file.

  4. Click Next, enter a Stack name (e.g., TerraformBackend), and proceed.

  5. Click Next through the stack options, ensuring the correct IAM permissions are set.

  6. Click Create stack and wait for the deployment to complete.

Once the stack is successfully created, go to Resources in the CloudFormation console to confirm that the S3 bucket and DynamoDB table have been provisioned.

Once the stack is deployed, navigate to the AWS CloudFormation console (https://console.aws.amazon.com/cloudformation) and check that:

  • Navigate to the AWS IAM Console.

  • The S3 bucket is listed under AWS S3.

  • The DynamoDB table is available in AWS DynamoDB.

  • Check that a new IAM Role (gitops2024Role) has been created.

  • Confirm that an OIDC Provider exists under IAM.

GitHub Actions Workflows: Automating CI/CD for Terraform

Example Mermaid Diagram:
graph TD;
    Developer -->|Push to GitHub| GitHub_Actions;
    GitHub_Actions -->|Run Formatting Checks| TFLint;
    TFLint -->|Run Security Checks| Trivy;
    GitHub_Actions -->|Run Terraform Plan| Terraform_Plan;
    GitHub_Actions -->|Run Cost Analysis| Infracost;
    Terraform_Plan -->|Approval Needed| Manual_Approval;
    Manual_Approval -->|Deploy Resources| Terraform_Apply;
    Terraform_Apply -->|Provision AWS Resources| AWS;
    AWS -->|Destroy if needed| Terraform_Destroy;

๐Ÿ“Œ GitHub Actions Workflow Execution Order:

1๏ธโƒฃ TFLint & Trivy Security Scan โ€“ Ensures best practices & security compliance
2๏ธโƒฃ Terraform Plan โ€“ Generates a preview of infrastructure changes
3๏ธโƒฃ OPA Policy Checks & Infracost Analysis โ€“ Ensures compliance & cost awareness
4๏ธโƒฃ Terraform Apply (manual trigger) โ€“ Deploys the infrastructure
5๏ธโƒฃ Terraform Destroy (manual trigger) โ€“ Cleans up resources when no longer needed

Trivy Security Scan

  • This workflow scans the Terraform configuration for security vulnerabilities using Trivy.

TFLint Code Linter

  • Lints Terraform code for syntax errors and best practices.

Terraform Plan

  • Generates a Terraform execution plan and performs OPA policy checks.

Infracost Cost Analysis

  • Estimates the cost impact of Terraform changes.

Terraform Apply

  • Deploys infrastructure changes to AWS.

Terraform Destroy

  • Destroys all infrastructure deployed by Terraform.

Each of these workflows is triggered based on specific events and plays a key role in the CI/CD pipeline. Below is a breakdown of each workflow file, its purpose, how it works, and how it is triggered:

Trivy Security Scan

name: Trivy Security Scan

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main
      - feature
  workflow_dispatch:

permissions:
  contents: read
  pull-requests: write

jobs:
  security-scan:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
      - name: Install Trivy
        uses: aquasecurity/setup-trivy@v0.2.2
      - name: Run Trivy Terraform Security Scan
        run: |
          trivy fs --scanners misconfig --severity HIGH,CRITICAL --format table --exit-code 1 --ignorefile .trivyignore ./terraform | tee trivy-report.txt
      - name: Display Scan Report
        if: always()
        run: cat trivy-report.txt
      - name: Upload Scan Report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: tfsec-report
          path: trivy-report.txt
      - name: Post Scan Results as PR Comment
        if: always()
        uses: mshick/add-pr-comment@v2
        with:
          message: "๐Ÿšจ **Terraform Security Scan Results** ๐Ÿšจ

``
$(cat trivy-report.txt)
``

๐Ÿ“Œ **Severity Levels:** `HIGH`, `CRITICAL`
๐Ÿ” **Ignored Findings:** Defined in `.trivyignore`
๐Ÿ“„ **Full Report:** Check [tfsec-report](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})"
          repo-token: ${{ secrets.GITHUB_TOKEN }}

Purpose: Scans Terraform configuration for security vulnerabilities.

Triggers: Runs on push to main, pull_request to main or feature branches, and can be triggered manually via workflow_dispatch.

Key Steps:

  • Checks out the repository.

  • Installs Trivy security scanner.

  • Runs a scan for HIGH and CRITICAL misconfigurations.

  • Uploads scan results as an artifact and comments on PRs if issues are found.

TFLint Code Linter

name: Lint
on:
  push:
    branches: [ main ]
  pull_request:

jobs:
  tflint:
    runs-on: ${{ matrix.os }}
    defaults:
        run:
            working-directory: ./terraform

    strategy:
      matrix:
        os: [ubuntu-latest]

    steps:
    - uses: actions/checkout@v4
      name: Checkout source code

    - uses: actions/cache@v4
      name: Cache plugin dir
      with:
        path: ~/.tflint.d/plugins
        key: ${{ matrix.os }}-tflint-${{ hashFiles('.tflint.hcl') }}

    - uses: terraform-linters/setup-tflint@v4
      name: Setup TFLint
      with:
        tflint_version: v0.52.0
    - name: Show version
      run: tflint --version

    - name: Init TFLint
      run: tflint --init
      env:
        # https://github.com/terraform-linters/tflint/blob/master/docs/user-guide/plugins.md#avoiding-rate-limiting
        GITHUB_TOKEN: ${{ github.token }}

    - name: Run TFLint
      run: tflint -f compact

Purpose: Ensures Terraform code follows best practices and is formatted correctly.

Triggers: Runs on push to main and all pull_request events.

Key Steps:

  • Checks out the repository.

  • Caches TFLint plugins to optimize runs.

  • Initializes and runs TFLint to detect formatting and best-practice issues.

Terraform Plan

name: 'Plan'

on:
  push:
    branches: [ 'main' ]
  pull_request:
  workflow_dispatch:

permissions:
  contents: read
  id-token: write

jobs:

  terraform:
    name: 'Terraform'
    runs-on: ubuntu-latest
    environment: production
    defaults:
      run:
        shell: bash
        working-directory: ./terraform
    env:
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}


    steps:
    # Checkout the repository to the GitHub Actions runner
    - name: Checkout
      uses: actions/checkout@v4

    - name: Configure AWS Credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        role-to-assume: ${{ secrets.ROLE_TO_ASSUME }}
        aws-region: eu-west-2

    # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token
    - name: Setup Terraform
      uses: hashicorp/setup-terraform@v3

    # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc.
    - name: Terraform Init
      run: terraform init

    # Checks that all Terraform configuration files adhere to a canonical format
    - name: Terraform Format
      run: terraform fmt -check

    # Terraform Plan
    - name: Terraform Plan
      id: plan
      run: |
        terraform plan -out=plan.tfplan
        terraform show -json plan.tfplan > /tmp/plan.json
        cat /tmp/plan.json

    - name: Setup OPA
      uses: open-policy-agent/setup-opa@v2
      with:
        version: latest

    - name: Run OPA Tests
      run: |
        opaout=$(opa eval --data ../policies/instance-policy.rego --input /tmp/plan.json "data.terraform.deny" | jq -r '.result[].expressions[].value[]')
        [ -z "$opaout" ] && exit 0 || echo "$opaout" && gh pr comment ${{ github.event.pull_request.number }} --body "### $opaout" && exit 1

Purpose: Generates and evaluates a Terraform execution plan before applying changes.

Triggers: Runs on push to main, pull_request, and manually via workflow_dispatch.

Key Steps:

  • Checks out the repository.

  • Configures AWS credentials using OIDC.

  • Initializes Terraform and runs terraform plan, storing the output for later review.

  • Runs OPA (Open Policy Agent) tests against the Terraform plan to enforce security policies.

Infracost Cost Analysis

name: 'Run Infracost'
on:
  pull_request:
    types: [opened, synchronize, closed]
jobs:
  infracost-pull-request-checks:
    name: Infracost Pull Request Checks
    if: github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'synchronize')
    runs-on: ubuntu-latest
    environment: production
    permissions:
      contents: read
      pull-requests: write # Required to post comments
    steps:
      - name: Setup Infracost
        uses: infracost/actions/setup@v3
        with:
          api-key: ${{ secrets.INFRACOST_API_KEY }}
      - name: Checkout base branch
        uses: actions/checkout@v4
        with:
          ref: '${{ github.event.pull_request.base.ref }}'
      - name: Generate Infracost cost estimate baseline
        run: |
          infracost breakdown --path=. \
                              --format=json \
                              --out-file=/tmp/infracost-base.json
      - name: Checkout PR branch
        uses: actions/checkout@v4
      - name: Generate Infracost diff
        run: |
          infracost diff --path=. \
                          --format=json \
                          --compare-to=/tmp/infracost-base.json \
                          --out-file=/tmp/infracost.json
      - name: Post Infracost comment
        run: |
            infracost comment github --path=/tmp/infracost.json \
                                     --repo=$GITHUB_REPOSITORY \
                                     --github-token=${{ github.token }} \
                                     --pull-request=${{ github.event.pull_request.number }} \
                                     --behavior=update \
                                     --policy-path ./policies/cost.rego

Purpose: Estimates the cost impact of Terraform changes before they are applied.

Triggers: Runs on pull_request when a PR is opened, updated, or closed.

Key Steps:

  • Sets up Infracost with an API key.

  • Runs cost analysis for the current branch and compares it with the base branch.

  • Posts a cost breakdown as a comment on the PR.

Example Output:
Name Quantity Unit Cost Monthly Cost
aws_instance.grafana 1 $8.32 $8.32
aws_s3_bucket.gitops-tf 1 $0.03 $0.03

Terraform Apply

name: 'Apply'

on: workflow_dispatch

permissions:
  contents: read
  id-token: write

jobs:

  terraform:
    name: 'Terraform'
    runs-on: ubuntu-latest

    defaults:
      run:
        shell: bash

    environment: production

    steps:
    # Checkout the repository to the GitHub Actions runner
    - name: Configure AWS Credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        role-to-assume: ${{ secrets.ROLE_TO_ASSUME }}
        aws-region: eu-west-2
    - name: Checkout
      uses: actions/checkout@v4

    # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token
    - name: Setup Terraform
      uses: hashicorp/setup-terraform@v3

    # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc.
    - name: Terraform Init
      run: terraform -chdir="./terraform" init

    # Checks that all Terraform configuration files adhere to a canonical format
    - name: Terraform Format
      run: terraform -chdir="./terraform" fmt -check

    # Generates an execution plan for Terraform
    - name: Terraform Plan
      run: terraform -chdir="./terraform" plan -input=false

    # Apply the Configuration
    - name: Terraform Apply
      run: terraform -chdir="./terraform" apply -input=false -auto-approve

Purpose: Applies Terraform changes to deploy the infrastructure.

Triggers: Runs only when manually triggered via workflow_dispatch.

Key Steps:

  • Checks out the repository.

  • Configures AWS credentials.

  • Initializes Terraform.

  • Runs terraform apply to deploy resources.

Terraform Destroy

name: 'Destroy'

on: workflow_dispatch

permissions:
  contents: read
  id-token: write

jobs:

  terraform:
    name: 'Terraform'
    runs-on: ubuntu-latest

    defaults:
      run:
        shell: bash

    environment: production

    steps:
    # Checkout the repository to the GitHub Actions runner
    - name: Configure AWS Credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        role-to-assume: ${{ secrets.ROLE_TO_ASSUME }}
        aws-region: eu-west-2
    - name: Checkout
      uses: actions/checkout@v4

    # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token
    - name: Setup Terraform
      uses: hashicorp/setup-terraform@v3

    # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc.
    - name: Terraform Init
      run: terraform -chdir="./terraform" init

    # Checks that all Terraform configuration files adhere to a canonical format
    - name: Terraform Format
      run: terraform -chdir="./terraform" fmt -check

    # Generates an execution plan for Terraform
    - name: Terraform Plan
      run: terraform -chdir="./terraform" plan -input=false

    # Apply the Configuration
    - name: Terraform Destroy
      run: terraform -chdir="./terraform" destroy -input=false -auto-approve

Purpose: Destroys deployed infrastructure when it's no longer needed.

Triggers: Runs only when manually triggered via workflow_dispatch.

Key Steps:

  • Checks out the repository.

  • Configures AWS credentials.

  • Initializes Terraform.

  • Runs terraform destroy to remove all resources.

Push Changes to a Feature Branch (in GitHub Codespaces)

Run the following commands inside your cloned GitHub repository:

# Create and switch to a new feature branch
git checkout -b feature-branch 

# Stage all modified files
git add . 

# Commit the changes with a meaningful message
git commit -m "Testing CI/CD"

# Push the feature branch to GitHub
git push origin feature-branch

Deploying AWS Resources with Terraform

Terraform Configuration Breakdown

The following Terraform files define the infrastructure to be deployed. Below, we explain each file and its role in the deployment process.

versions.tf

Defines the required Terraform version and provider constraints to ensure compatibility.

terraform {
  required_version = ">= 1.3.0"

  backend "s3" {
    bucket         = "gitops-tf-backend-mpcloudlab"
    key            = "terraform.tfstate"
    region         = "eu-west-2"
    dynamodb_table = "GitopsTerraformLocks"
  }
}

providers.tf

Configures the AWS provider and region settings.

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.69.0"
    }
    http = {
      source  = "hashicorp/http"
      version = "3.4.5"
    }
  }
}

provider "aws" {
  region = var.region
}

variables.tf

Declares input variables used throughout the Terraform configuration.

variable "region" {
  description = "AWS region where resources will be deployed"
  type        = string
  default     = "eu-west-2"
}

variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t3.micro"
}

terraform.tfvars

Defines default values for input variables.

region        = "eu-west-2"
instance_type = "t3.micro"

main.tf

Defines the main infrastructure resources to be deployed.

data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"] # Canonical
}

resource "aws_vpc" "gitops_vpc" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "gitops-vpc"
  }
}

resource "aws_internet_gateway" "gitops_igw" {
  vpc_id = aws_vpc.gitops_vpc.id

  tags = {
    Name = "gitops-igw"
  }
}

resource "aws_route_table" "gitops_rt" {
  vpc_id = aws_vpc.gitops_vpc.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.gitops_igw.id
  }

  tags = {
    Name = "gitops-rt"
  }
}

resource "aws_subnet" "gitops_subnet" {
  vpc_id                  = aws_vpc.gitops_vpc.id
  cidr_block              = "10.0.1.0/24"
  map_public_ip_on_launch = true

  tags = {
    Name = "gitops-subnet"
  }
}

resource "aws_route_table_association" "gitops_rta" {
  subnet_id      = aws_subnet.gitops_subnet.id
  route_table_id = aws_route_table.gitops_rt.id
}

resource "aws_security_group" "gitops_sg" {
  name        = "gitops_sg"
  description = "Allow port 3000"
  vpc_id      = aws_vpc.gitops_vpc.id

  ingress {
    from_port   = 3000
    to_port     = 3000
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "gitops-sg"
  }
}

resource "aws_instance" "grafana_server" {
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = var.instance_type
  subnet_id              = aws_subnet.gitops_subnet.id
  vpc_security_group_ids = [aws_security_group.gitops_sg.id]
  user_data              = file("userdata.tftpl")

  root_block_device {
    encrypted = true
  }

  metadata_options {
    http_tokens = "required"
  }

  tags = {
    Name = "grafana-server"
  }
}

check "grafana_health_check" {
  data "http" "test" {
    url = "http://${aws_instance.grafana_server.public_ip}:3000"
    retry {
      attempts = 10
    }
  }
  assert {
    condition     = data.http.test.status_code == 200
    error_message = "Grafana is inaccessible on port 3000."
  }
}

userdata.tftpl

Contains startup scripts that run when the EC2 instance is launched.

#!/bin/bash
sudo apt-get install -y apt-transport-https software-properties-common wget &&
sudo mkdir -p /etc/apt/keyrings/ &&
wget -q -O - https://apt.grafana.com/gpg.key | gpg --dearmor | sudo tee /etc/apt/keyrings/grafana.gpg > /dev/null &&
echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" | sudo tee -a /etc/apt/sources.list.d/grafana.list &&
sudo apt-get update &&
sudo apt-get install -y grafana &&
sudo systemctl start grafana-server &&
sudo systemctl enable grafana-server

outputs.tf

Defines output values to retrieve important details after deployment.

output "grafana_ip" {
  value = "http://${aws_instance.grafana_server.public_ip}:3000"
}

Steps to Deploy Terraform Resources

Initialize Terraform

terraform init

Validate the configuration

terraform validate

Generate an execution plan

terraform plan

Apply the configuration

terraform apply -auto-approve

Retrieve outputs

terraform output

Verifying the Deployment

  • Check the AWS Console to confirm that resources have been created.

  • Use SSH or a browser to access the deployed EC2 instance.

Destroying the Infrastructure

When no longer needed, remove all resources:

terraform destroy -auto-approve

or use the Destroy GitHub Actions workflow

Running and Testing the Pipeline

How to Trigger Workflows

Automatic Triggers
  • Pushes and pull requests to main trigger the Plan, Security Scan, and Linter.

  • Pull requests trigger Infracost Cost Analysis.

Manual Triggers
  • terraform apply and terraform destroy require workflow_dispatch (manual execution via GitHub UI).

  • Manually trigger Apply workflow to deploy

  • Manually trigger Destroy workflow to clean up resources

Step-by-Step Execution

Push Changes to a New Feature Branch
git checkout -b feature-branch
git add .
git commit -m "Testing CI/CD"
git push origin feature-branch
Open a Pull Request
  • This will trigger security scans, cost analysis, and Terraform plan.

  • Review GitHub Actions Results

  • Check logs for security, cost, and linting errors.

  • Merge to branch when approved

Project Folder Structure

โ”œโ”€โ”€ .github                  # GitHub Actions workflows
โ”‚   โ””โ”€โ”€ workflows
โ”‚       โ”œโ”€โ”€ apply.yml       # GitHub Actions workflow for applying Terraform changes
โ”‚       โ”œโ”€โ”€ destroy.yml     # GitHub Actions workflow for destroying Terraform resources
โ”‚       โ”œโ”€โ”€ infracost.yml   # Workflow for running Infracost to estimate Terraform costs
โ”‚       โ”œโ”€โ”€ plan.yml        # Workflow for running Terraform Plan
โ”‚       โ”œโ”€โ”€ tflint.yml      # Workflow for running TFLint to check Terraform syntax
โ”‚       โ””โ”€โ”€ tfsec.yml       # Workflow for running tfsec to check Terraform security
โ”œโ”€โ”€ .gitignore              # Specifies files and directories to ignore in Git
โ”œโ”€โ”€ .trivyignore            # Ignore file for Trivy security scanning
โ”œโ”€โ”€ README.md               # Project documentation and setup instructions
โ”œโ”€โ”€ cfn                     # CloudFormation templates for infrastructure
โ”‚   โ”œโ”€โ”€ backend-resources.yaml  # Defines S3 and DynamoDB resources for Terraform backend
โ”‚   โ””โ”€โ”€ oidc-role.yaml      # CloudFormation template to create OIDC role for GitHub Actions
โ”œโ”€โ”€ install terraform.txt   # Instructions for installing Terraform
โ”œโ”€โ”€ policies                # Policies for security and cost analysis
โ”‚   โ”œโ”€โ”€ cost.rego           # OPA policy for Infracost cost enforcement
โ”‚   โ”œโ”€โ”€ instance-policy.rego # OPA policy for Terraform instance compliance
โ”‚   โ””โ”€โ”€ plan.json           # JSON representation of Terraform Plan for policy validation
โ””โ”€โ”€ terraform               # Terraform configuration files
    โ”œโ”€โ”€ main.tf             # Defines core infrastructure (VPC, EC2, Security Groups, etc.)
    โ”œโ”€โ”€ outputs.tf          # Specifies Terraform output values
    โ”œโ”€โ”€ providers.tf        # Configures Terraform providers (AWS, HTTP, etc.)
    โ”œโ”€โ”€ terraform.tfvars    # Defines Terraform input variable values
    โ”œโ”€โ”€ userdata.tftpl      # Cloud-init script for configuring EC2 instances
    โ”œโ”€โ”€ variables.tf        # Declares Terraform input variables
    โ””โ”€โ”€ versions.tf         # Specifies required Terraform and provider versions

Conclusion

By implementing this GitOps pipeline, we achieve:

๐Ÿš€ Automation โ€“ Eliminates manual deployments ๐Ÿ”’ Security โ€“ Enforces compliance using OPA & Trivy ๐Ÿ’ฐ Cost Awareness โ€“ Monitors infrastructure costs via Infracost

This approach provides scalability, consistency, and security for managing AWS infrastructure.

If you have questions, feedback, or suggestions, feel free to reach out!

Share on Share on

Customizing Your MkDocs Blog ๐ŸŽจ

Once you've set up your MkDocs blog, it's time to personalize it. In this post, I'll cover various customizations, including social media sharing hooks, changing the blog icon and favicon, adding authors, and using tags. These modifications will make your blog more interactive and visually appealing.

All of the steps covered in this guide are available in the MKDocs Documentation or via the superb video tutorials created by James Willett

Adding Social Media Sharing Hooks ๐Ÿ”„

To allow users to share your blog posts easily, you can create a socialmedia.py hook. This script appends sharing buttons to each post.

Create the Hook File

Inside your project, create a folder named hooks/ if it doesn't already exist, and then add a file called socialmedia.py:

mkdir hooks
nano hooks/socialmedia.py

Add the Social Media Sharing Code

Paste the following into socialmedia.py:

from textwrap import dedent
import urllib.parse
import re

x_intent = "https://x.com/intent/tweet"
fb_sharer = "https://www.facebook.com/sharer/sharer.php"
include = re.compile(r"posts/.*")

def on_page_markdown(markdown, **kwargs):
    page = kwargs['page']
    config = kwargs['config']
    if not include.match(page.url):
        return markdown

    page_url = config.site_url + page.url
    page_title = urllib.parse.quote(page.title + '\n')

    return markdown + dedent(f"""
    [Share on :simple-x:]({x_intent}?text={page_title}&url={page_url}){{ .md-button }}
    [Share on :simple-facebook:]({fb_sharer}?u={page_url}){{ .md-button }}
    """)

How the Code Works:

  • The script identifies blog post pages using a regular expression (posts/.*)
  • It gets the current page URL and title from MkDocs
  • It adds formatted markdown buttons at the end of your content
  • The buttons link to X (Twitter) and Facebook with pre-filled sharing information

Enable the Hook in mkdocs.yml

Modify mkdocs.yml to include the hook:

hooks:
  - hooks/socialmedia.py

This will append social media sharing buttons to your posts.

Changing the Blog Icon and Favicon ๐Ÿ–ผ๏ธ

Updating your blog's favicon and site icon enhances branding.

Prepare the Icons

Save your icons in docs/images/ as:

  • favicon.ico (16x16 or 32x32 pixels) - Used by browsers in tabs and bookmarks
  • logo.png (recommended 512x512 pixels) - Displayed in your site header/navigation

Update mkdocs.yml

extra:
  logo: images/logo.png
  favicon: images/favicon.ico

Note: These paths are relative to your docs/ directory, and both settings should be nested under the extra: key in your configuration file.

Restart the Server

Run mkdocs serve to preview the changes.

Adding Authors ๐Ÿ‘ฅ

To attribute posts to different authors, create an authors.yml file.

Create authors.yml

In the docs/ directory of your project, create a file named authors.yml:

matthew:
  name: "Matthew Pollock"
  email: "matthew@example.com"
  website: "https://matthewblog.com"

team:
  name: "Blog Team"
  website: "https://teamwebsite.com"

squidfunk:
  name: "SquidFunk"
  website: "https://squidfunk.github.io/"

Modify each post's metadata:

authors:
  - matthew
  - team

Adding Tags ๐Ÿท๏ธ

Tags help categorize your blog posts.

Create tags.md

Create a docs/tags.md file. Below are the tags used in this blog:

# Tags
tags:
  - technology
  - learning

Enable Tags in mkdocs.yml

Modify mkdocs.yml:

plugins:
  - tags:
      tags_file: tags.md

Now, you can tag posts like this:

tags:
  - technology
  - learning

Enabling Comments on Blog Posts ๐Ÿ’ฌ

If you want to enable comments on blog posts, follow these steps:

Create the Comments Template

Create a directory overrides/partials/ if it doesn't exist, then add a file called comments.html with your Disqus integration code:

<div id="disqus_thread"></div>
<script>
  var disqus_config = function () {
    this.page.url = window.location.href;
    this.page.identifier = document.title;
  };
  (function() {
    var d = document, s = d.createElement('script');
    s.src = 'https://your-disqus-name.disqus.com/embed.js';
    s.setAttribute('data-timestamp', +new Date());
    (d.head || d.body).appendChild(s);
  })();
</script>

Note: Replace your-disqus-name with your Disqus shortname, which you can find in your Disqus admin panel after creating a site.

Enable Comments in mkdocs.yml

extra:
  comments: true

Restart MkDocs and test:

mkdocs serve

Theme Customization ๐ŸŽญ

The Material theme offers extensive customization options for colors, fonts, and more.

Custom Color Scheme

Add this to your mkdocs.yml:

theme:
  name: material
  palette:
    # Light mode
    - media: "(prefers-color-scheme: light)"
      scheme: default
      primary: indigo
      accent: indigo
      toggle:
        icon: material/toggle-switch-off-outline
        name: Switch to dark mode
    # Dark mode
    - media: "(prefers-color-scheme: dark)"
      scheme: slate
      primary: blue
      accent: blue
      toggle:
        icon: material/toggle-switch
        name: Switch to light mode

Custom Fonts

theme:
  font:
    text: Roboto
    code: Roboto Mono

Analytics Integration ๐Ÿ“Š

Add analytics to track your blog's performance.

Google Analytics

extra:
  analytics:
    provider: google
    property: G-XXXXXXXXXX

Plausible Analytics

extra:
  analytics:
    provider: plausible
    domain: yourdomain.com

SEO Optimization ๐Ÿ”

Improve your blog's search engine visibility.

Add Meta Tags

plugins:
  - meta

Then in each post, add:

meta:
  description: "A detailed guide to customizing MkDocs blogs"
  keywords: mkdocs, blog, customization, web development
  robots: index, follow
  og:image: /assets/social-card.png

Performance Optimization โšก

Keep your blog fast and responsive.

Image Optimization

  1. Compress all images before adding them to your blog
  2. Use modern formats like WebP
  3. Specify image dimensions in HTML to prevent layout shifts

Lazy Loading

Enable lazy loading of images using the loading="lazy" attribute:

![Alt text](image.jpg){ loading=lazy }

Understanding Your MkDocs Project Structure ๐Ÿ“

Once you have created an MkDocs project and added the components listed in the two posts in this series, you'll see a folder structure similar to this:

project-blog/
โ”œโ”€โ”€ docs/                 # Documentation files (Markdown content)
โ”‚   โ”œโ”€โ”€ index.md          # Homepage of your site
โ”‚   โ”œโ”€โ”€ tags.md           # Tags page for blog posts
โ”‚   โ”œโ”€โ”€ authors.yml       # Defines author metadata
โ”‚   โ”œโ”€โ”€ posts/            # Blog post storage
โ”‚   โ”‚   โ”œโ”€โ”€ firstpost.md  
โ”‚   โ”‚   โ”œโ”€โ”€ secondpost.md  
โ”‚   โ”‚   โ”œโ”€โ”€ thirdpost.md  
โ”‚   โ”œโ”€โ”€ images/           # Store all your images here
โ”‚   โ”‚   โ”œโ”€โ”€ logo.png  
โ”‚   โ”‚   โ”œโ”€โ”€ favicon.ico
โ”œโ”€โ”€ hooks/                # Custom MkDocs hooks (like social media sharing)
โ”œโ”€โ”€ overrides/            # Custom HTML overrides for Material theme
โ”‚   โ”œโ”€โ”€ partials/comments.html  # Comment system (if enabled)
โ”œโ”€โ”€ mkdocs.yml            # Configuration file for MkDocs
โ”œโ”€โ”€ requirements.txt      # Python dependencies
โ”œโ”€โ”€ .gitignore            # Files to exclude from Git

This structure keeps content organized, making it easy to scale your documentation or blog.

Deploying Updates ๐Ÿš€

Whenever you make changes, redeploy your site:

mkdocs gh-deploy

This command builds your site and pushes it to the gh-pages branch of your repository.

Troubleshooting Tips ๐Ÿ”ง

Social Media Buttons Not Showing

  • Ensure your mkdocs.yml has the site_url property set correctly
  • Verify the hook is correctly installed in the hooks/ directory

Favicon Not Displaying

  • Clear your browser cache
  • Ensure the path in mkdocs.yml is correct relative to the docs/ directory

Comments Not Loading

  • Check your browser console for JavaScript errors
  • Verify your Disqus shortname is correct
  • Ensure extra.comments is set to true in mkdocs.yml

Deployment Issues

If mkdocs gh-deploy fails:

# Ensure you have the latest version of MkDocs
pip install --upgrade mkdocs mkdocs-material

# Check your git configuration
git config --global user.name "Your Name"
git config --global user.email "your.email@example.com"

Conclusion ๐ŸŽ‰

With these customizations, your MkDocs blog will be more interactive and visually engaging. The additions of social sharing, comments, better visualization with icons, and proper author attribution will make your blog more professional and user-friendly.

In our next post, we'll cover advanced MkDocs features including content reuse, advanced search configuration, and integration with other tools in your workflow.

Stay tuned for more tips!


๐Ÿ“Œ Published on: 2025-03-09
โณ Read time: 8 min

Share on Share on

Enhancing My MkDocs Blog with Custom Features ๐Ÿš€

Once I set up my MkDocs blog, I wanted to personalize it by adding navigation links, social media icons, an announcement bar, and a custom footer. These enhancements improve user experience, branding, and site functionality. This post walks through each customization step with code examples and configurations.

To provide quick access to key profiles and resources, I added navigation links.

๐Ÿ“ Update mkdocs.yml

nav:
  - Home: index.md
  - Blog: blog.md
  - About: about.md
  - Contact: contact.md
  - GitHub: https://github.com/cloudlabmp
  - LinkedIn: https://linkedin.com/in/matthew-pollock-76831920/
  - Website: https://profile.pollockweb.com

This allows visitors to access my GitHub, LinkedIn, and personal site from the navigation menu.


๐ŸŒ Adding Social Media Icons to the Header

Instead of plain text links, I enabled social media icons in the header.

๐Ÿ“ Update mkdocs.yml

extra:
  social:
    - icon: fontawesome/brands/github
      link: https://github.com/cloudlabmp
    - icon: fontawesome/brands/linkedin
      link: https://linkedin.com/in/matthew-pollock-76831920/
    - icon: fontawesome/solid/globe
      link: https://profile.pollockweb.com

These icons now appear in the top-right of the header.


๐Ÿ“ข Enabling the Announcement Bar

A dismissible announcement bar allows for important updates.

๐Ÿ“ Update mkdocs.yml

theme:
  name: material
  features:
    - announce.dismiss

๐Ÿ“ Customize overrides/main.html

{% extends "base.html" %}

{% block announce %}
  <div class="announcement-content">
    <p>Welcome to my blog! Connect with me:</p>
    <a href="https://github.com/cloudlabmp" target="_blank">
      <i class="fab fa-github fa-2x"></i>
    </a>
    <a href="https://www.linkedin.com/in/matthew-pollock-76831920/" target="_blank">
      <i class="fab fa-linkedin fa-2x"></i>
    </a>
  </div>
{% endblock %}

To personalize the footer, I added a copyright notice and aligned social media icons.

๐Ÿ“ Customize overrides/partials/footer.html

{% block content %}
  <div class="custom-footer">
    <div class="custom-footer-left">
      <p>Copyright &copy; 2025 Matthew Pollock</p>
    </div>
    <div class="custom-footer-right">
      <a href="https://github.com/cloudlabmp" target="_blank">
        <i class="fab fa-github"></i>
      </a>
      <a href="https://linkedin.com/in/matthew-pollock-76831920/" target="_blank">
        <i class="fab fa-linkedin"></i>
      </a>
      <a href="https://profile.pollockweb.com" target="_blank">
        <i class="fas fa-globe"></i>
      </a>
    </div>
  </div>
{% endblock %}
/* Custom footer styling */
.custom-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px 20px;
  width: 100%;
  background: var(--md-default-bg-color);
  border-top: 1px solid var(--md-default-fg-color--light);
}

.custom-footer-left {
  text-align: left;
  font-size: 0.9em;
  color: var(--md-default-fg-color);
}

.custom-footer-right {
  display: flex;
  gap: 15px;
}

.custom-footer-right a {
  font-size: 1.8em;
  color: var(--md-default-fg-color);
  transition: transform 0.2s ease-in-out;
}

.custom-footer-right a:hover {
  transform: scale(1.2);
  color: #673AB7; /* Deep Purple (Accent Color) */
}

/* Add padding to the bottom of the page */
.md-content {
  padding-bottom: 40px;
}

๐ŸŽ‰ Final Results

โœ” Clickable social media icons in the header and footer
โœ” A dismissible announcement bar for updates
โœ” Navigation links to external sites
โœ” A fully customized footer with copyright and icons
โœ” Refined color scheme using deep purple accents

Each of these enhancements has made my MkDocs blog more functional, visually appealing, and user-friendly. If youโ€™re looking to implement similar customizations, these steps should get you started! ๐Ÿš€


Share on Share on

Setting Up MkDocs for Your Blog ๐Ÿ“

If you're looking for a simple yet powerful way to create and manage your documentation or blog, MkDocs is a fantastic option. MkDocs is a fast, static site generator that's geared towards building project documentation but works wonderfully for blogs too! In this post, I'll walk you through the steps to set up MkDocs with the popular Material theme and get it hosted on GitHub Pages.

All of the steps covered in this guide are available in the MkDocs Documentation or via the superb video tutorials created by James Willett.

Why I Chose MkDocs ๐Ÿค”

After exploring several blogging platforms, I settled on MkDocs for its simplicity, flexibility, and Markdown support. Unlike WordPress or Ghost, MkDocs is lightweight and doesn't require a database. The Material theme provides beautiful out-of-the-box styling, and since everything is in Markdown, I can easily version control my content using Git.

Prerequisites ๐Ÿ› ๏ธ

Before we begin, ensure you have the following installed:

  • Python 3.x โ€“ MkDocs is a Python-based tool. You can check if Python is installed by running:
python --version

If Python is not installed, download it from python.org and follow the installation instructions.

  • pip โ€“ Python's package manager. It usually comes with Python, but you can verify it with:
pip --version
  • Git โ€“ To manage version control and push changes to GitHub. You can install Git from git-scm.com.

Setting Up Your Environment ๐ŸŒฑ

It's a good practice to use a virtual environment (venv) when working with Python projects to avoid dependency conflicts:

  1. Navigate to your project directory:
cd /path/to/your/project
  1. Create a virtual environment:
python -m venv venv
  1. Activate the virtual environment:

Choose your OS

venv\Scripts\activate
source venv/bin/activate

Installing MkDocs

Once your virtual environment is activated, install MkDocs:

pip install mkdocs

To verify the installation, run:

mkdocs --version

If you see the installed version displayed, the installation was successful.

Creating a New MkDocs Project ๐Ÿ—๏ธ

Navigate to the directory where you want to create your blog and run:

mkdocs new my-blog
cd my-blog

This creates a basic MkDocs project structure, including:

  • A default mkdocs.yml configuration file
  • A docs/ directory containing an index.md file

Installing Dependencies โš™๏ธ

To enhance your MkDocs site with additional features, create a requirements.txt file in the root of your project:

# Requirements for core
jinja2~=3.0
markdown~=3.2
mkdocs~=1.6
mkdocs-material~=9.5.46
mkdocs-material-extensions~=1.3
pygments~=2.16
pymdown-extensions~=10.2

# Requirements for plugins
babel~=2.10
colorama~=0.4
paginate~=0.5
regex>=2022.4
requests~=2.26

# Additional Material and MkDocs plugins
mkdocs-glightbox~=0.4.0
mkdocs-get-deps~=0.2.0
mkdocs-minify-plugin~=0.8.0
mkdocs-git-committers-plugin-2~=2.4.1
mkdocs-git-revision-date-localized-plugin~=1.3.0
mkdocs-rss-plugin~=1.16.0

Install these dependencies with:

pip install -r requirements.txt

Customizing Your Blog โœจ

Basic Configuration

The mkdocs.yml file is the configuration file for your blog. Open it in a text editor and modify it:

site_name: My Tech Blog
site_description: A blog documenting my projects and insights
site_author: Your Name
repo_url: https://github.com/yourusername/my-blog

Adding the Material Theme

The Material theme provides a clean, responsive design for your blog:

theme:
  name: material
  palette:
    primary: indigo
    accent: indigo
  features:
    - navigation.tabs
    - navigation.top
    - search.suggest
    - search.highlight

Setting Up Blog Features

To turn your MkDocs site into a proper blog, add the blog plugin configuration:

plugins:
  - blog:
      blog_dir: blog
      post_date_format: yyyy-MM-dd
      post_url_format: "{date}/{slug}"
  - search
  - rss:
      match_path: blog/posts/.*
      date_from_meta:
        as_creation: date
      categories:
        - categories

Creating Blog Posts ๐Ÿ“ฐ

Folder Structure

Create the following structure for your blog posts:

docs/
โ”œโ”€โ”€ blog/
โ”‚   โ”œโ”€โ”€ posts/
โ”‚   โ”‚   โ”œโ”€โ”€ 2025-03-01-hello-world.md
โ”‚   โ”‚   โ””โ”€โ”€ 2025-03-09-setting-up-mkdocs.md
โ”‚   โ””โ”€โ”€ index.md
โ””โ”€โ”€ index.md

Post Frontmatter

Each blog post should have frontmatter at the top, like this:

---
title: "Your Post Title"
date: 2025-03-09
authors:
  - yourname
description: "A brief description of your post."
categories:
  - Category1
  - Category2
tags:
  - tag1
  - tag2
---

# Your Post Title

Content goes here...

Adding Images and Media

To include images in your posts:

  1. Create an assets folder in your docs directory:
docs/
โ”œโ”€โ”€ assets/
โ”‚   โ””โ”€โ”€ images/
โ”‚       โ””โ”€โ”€ screenshot.png
  1. Reference the image in your Markdown:
![Screenshot of MkDocs site](../assets/images/screenshot.png)

Running MkDocs Locally ๐Ÿ–ฅ๏ธ

To preview your blog locally and check how it looks before publishing, run:

mkdocs serve

This will start a local web server. Open your browser and go to:

http://127.0.0.1:8000/

to view your blog. The server will automatically reload when you make changes to your files.

Deploying to GitHub Pages ๐Ÿš€

Initialize a Git Repository

First, navigate to your project folder and initialize a Git repository:

git init
git add .
git commit -m "Initial commit"

Create a GitHub Repository

  1. Go to GitHub and log in.
  2. Click the "+" button in the top-right and select "New repository".
  3. Enter a Repository name (e.g., my-blog).
  4. Choose Public or Private, based on your preference.
  5. DO NOT initialize with a README, .gitignore, or license (since we are pushing an existing project).
  6. Click "Create repository".

After creating the repository, copy the repository URL (e.g., https://github.com/yourusername/my-blog.git).

Now, link your local project to the GitHub repository:

git remote add origin https://github.com/yourusername/my-blog.git
git branch -M main
git push -u origin main

Deploy Your Blog to GitHub Pages

To publish your blog on GitHub Pages, run:

mkdocs gh-deploy

This command builds your MkDocs project and pushes the static files to the gh-pages branch of your repository.

Enable GitHub Pages in Repository Settings

  1. Go to your GitHub repository.
  2. Navigate to Settings > Pages.
  3. Under Branch, select gh-pages and click Save.
  4. Your site will be live at https://yourusername.github.io/my-blog/ (Note: It may take a few minutes for your site to appear live after deployment).

Enhancing Your Development Experience ๐Ÿ’ป

For an easier development experience, I recommend using Visual Studio Code (VS Code). You can install it from code.visualstudio.com.

  • Python (for virtual environment support)
  • Markdown Preview Enhanced (for writing and previewing Markdown files)
  • YAML (for editing mkdocs.yml)
  • Material Theme Icons (for a nicer file tree visualization)

Troubleshooting Common Issues ๐Ÿ”ง

Site Not Deploying

If your site isn't appearing after running mkdocs gh-deploy:

  • Check if you've configured GitHub Pages in your repository settings
  • Ensure you've pushed your changes to the correct branch
  • Wait a few minutes as GitHub Pages deployment can take time

Styling Issues

If your theme isn't applying correctly:

  • Verify the theme is installed (pip install mkdocs-material)
  • Check for syntax errors in your mkdocs.yml file
  • Try clearing your browser cache

Conclusion ๐ŸŽ‰

Setting up MkDocs is straightforward, and with GitHub Pages, you can host your blog for free. The Material theme provides excellent styling out of the box, and with the right plugins, you can create a fully-featured blog with minimal effort.

In future posts, I'll cover more customizations, themes, and plugins to enhance your MkDocs blog. Stay tuned!


Share on Share on

๐Ÿ“Œ Life Update: Projects, Learning, and the Usual Balancing Act

๐Ÿ“ท My Workspace

Life has been ticking along โ€” work, learning, personal projects. Nothing dramatic, but I thought Iโ€™d put down some thoughts on what Iโ€™ve been up to. Mostly for my own benefit, but if anyone else finds it useful, all the better.


โœ๏ธ Documenting Projects Properly (Finally)

For a while now, Iโ€™ve been meaning to get better at documenting my work. Not in an overly polished or performative way, just something structured enough to be useful. I finally set up this MkDocs blog to keep track of things.

Itโ€™s already proving helpful. Writing things down forces me to clarify my thinking, and itโ€™s nice to have a reference point when revisiting old work. If someone else stumbles across it and finds it useful, thatโ€™s fine too.


โš™๏ธ Work, Learning, and Trying to Keep Up

Keeping up with work and continuous learning at the same time is an ongoing challenge. Thereโ€™s always more to read, more to test, more to refine. Lately, Iโ€™ve been focused on:

๐Ÿ”น Automation โ€“ making things run themselves where possible.
๐Ÿ”น Security โ€“ because the landscape never stops shifting.
๐Ÿ”น Cloud optimization โ€“ getting the most out of whatโ€™s already in place.

I also need to push myself to do more hands-on work in areas I havenโ€™t explored as much. Itโ€™s easy to keep circling around familiar topics, but I want to force some variety into my learning.


๐Ÿšดโ€โ™‚๏ธ Getting Outside (Because Screens Arenโ€™t Everything)

๐Ÿ“ท Out and About

I spend enough time at a desk as it is, so Iโ€™ve been continuing to prioritise getting outside. Cycling, running, gardening and birdwatching have been good ways to clear my head.

Itโ€™s a reminder that productivity isnโ€™t just about doing moreโ€”itโ€™s about doing things well, which is harder when youโ€™re constantly in work mode. Some distance helps.


๐Ÿ”ฎ Next Steps

No major changes on the horizon, but things I want to focus on:

โœ… Expanding this blog with more useful content.
โœ… Continuing to refine workflows and knowledge-sharing.
โœ… Dedicating time to learning and experimenting with new tech.

Thatโ€™s about it. Nothing groundbreaking, just a steady progression of projects and ideas. If youโ€™re also juggling work, learning, and trying to maintain some kind of balance, Iโ€™d be interested to hear how you manage it.


Share on Share on