Naroj site automation¶
Site content publishment using the GitOps way of working combined with event driven partial updates. This allows us to perform CRUD on a single blog article and have Git/Pipelines and events control the actual site updates based on just a new git tag.
Event-driven architecture (EDA) is an architecture pattern built from decoupled services that publish / route events. In our case this enables us to exchange site updates between build pipelines and the hosting infrastructure which runs this site.
Site repository | CI config | Builds | Dockerfile | Container repository
High-level¶
- Change a blog article (like this one)
- Push a git tag
- Gitlab pipeline will run and build + publish a new container image using that git tag
- Send a HTTP POST to a webhook receiver from the container host
- The ansible-rule book listening for webhooks will receive the new image tag
- An ansible playbook executed by the rule book will instruct the container runtime control socket to run the new version
- Content is published
Components¶
- Readthedocs this is a very simple static site generator
- GIT version control of content
- GitLab pipelines pipeline triggers (our automation runtime)
- Container host a simple linux server with a container runtime
- Event receiver we use ansible-rule books to receive events
- Ansible playbook an ansible playbook which is executed by the event receiver to load a new image version
Steps¶
Getting from a changed blog article to a new version of the site containing this new changes takes a few steps.
0. Prerequisites¶
Beyond the obvious we need to have mkdocs installed as a python module in order to generate an actual site from markdown files. Make sure you have python-pip installed. Pip modules;
mkdocs 1.6.1
mkdocs-material 9.5.49
mkdocs-material-extensions 1.3.1
If you run this on a local machine I recommend you setup a virtualenv, distrobox environment or Docker container to install your dependencies so we keep the base OS clean and untouched. When you would choose for a virtualenv it would take the following steps to set this up;
# create new virtual env
python3 -m venv ~/.venv
# install dependencies
~/.venv/bin/pip install mkdocs mkdocs-material mkdocs-material-extensions
# initiate new readthedocs project
~/.venv/bin/mkdocs new my-site
1. Change a blog article¶
A blog article (like this one) is a very simple markdown file. Files are part of a little hierarchy which makes up the site structure.
├── index.md
├── Blog
│ ├── ansible_hosted_vault_service.md
│ ├── dnsdist.md
│ ├── site_automation.md
├── images
│ ├── contact_me.png
│ ├── material.png
│ └── shell.png
├── ISO-27001
│ ├── continuously_monitoring.md
│ └── risks_to_information.md
├── manifest.json
├── Tech
│ ├── containerize.md
│ └── pipelines.md
└── training
└── index.md
Bare bones git actions to get the entire automation to spin a new version based on a content update.
git add Blog/site_automation.md
git commit -m "making progress on site automation article"
git push
git tag 1.5
git push --tags
VIM is my editor of choice but any plain text editor will work.
2. Local test¶
Before publishing a new version of the site we would like to make sure it looks the way we expect. In order to do that we run a new local version on our machine. Currently this site is too small for a full staging environment so this will be done on the local work station.
Running the serve command from the project directory will spin up a development http server serving local files.
➜ naroj.eu git:(main) ✗ ~/.venv/bin/mkdocs serve --config-file mkdocs.yml
INFO - Building documentation...
INFO - Cleaning site directory
INFO - Doc file 'Blog/dnsdist.md' contains an absolute link '/../images/DNSDistOutgoingDoH1.png', it was left as is.
INFO - Doc file 'Blog/dnsdist.md' contains an absolute link '/../images/dnsdist_metrics.png', it was left as is.
INFO - Documentation built in 0.40 seconds
INFO - [21:55:23] Watching paths for changes: 'readthedocs', 'mkdocs.yml'
INFO - [21:55:23] Serving on http://127.0.0.1:8000/
INFO - [21:55:35] Browser connected: http://localhost:8000/
INFO - [21:55:37] Browser connected: http://localhost:8000/
3. GitLab pipelines¶
Using GitLab's pipelines we automate the whole process of changing a local file and getting it finally published on our domain name. Pipelines respond to receiving a new git tag which indicate a new version of the site is desired.
Variables we need to run our build;
variables:
REGISTRY_NAME: registry.gitlab.com/naroj
IMAGE_NAME: naroj.eu
CONTAINER_NAME: naroj-eu
CONTAINER_NETWORK: backend
WHOOPER_ACTION: update-container
WHOOPER_ENDPOINT: https://naroj.eu/event
Build site stage¶
Build a new version of the site in the pipeline to validate all content is valid. This will render markdown into plain HTML/CSS in the site directory.
build-mkdocs:
stage: build
image: python:3.11-slim-buster
script:
- echo "Installing MkDocs..."
pip install -r requirements.txt
- echo "Building MkDocs site..."
python3 -m mkdocs build --config-file mkdocs.yml
artifacts:
paths:
- site/
Build docker image stage¶
The docker image build takes the generated HTML & CSS from the previous step and adds it to an OCI image we can run in about any container runtime. Dockerfile needed to build the image.
build-docker-image:
stage: push
script:
- echo "Logging in to GitLab Container Registry..."
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
- echo "Building Docker image..."
- docker build -t $CI_REGISTRY/naroj/$IMAGE_NAME:$CI_COMMIT_TAG .
- docker tag $CI_REGISTRY/naroj/$IMAGE_NAME:$CI_COMMIT_TAG $CI_REGISTRY/naroj/$IMAGE_NAME:latest
#- echo "Pushing Docker image..."
- docker push $CI_REGISTRY/naroj/$IMAGE_NAME:$CI_COMMIT_TAG
- docker push $CI_REGISTRY/naroj/$IMAGE_NAME:latest
only:
- tags
Activate docker image stage¶
In this step we send an event using the 'https://naroj.eu/event' endpoint which tells our hosting infrastructure a new version of the site is ready to be deployed. This event receiver (called event-whooper) will take this event in after authentication and uses ansible-rulebook logic to talk to the container runtime API to make sure our desired version will be running in an idempotent manner.
activate-docker-image:
stage: post
image: archlinux:base
script:
- |
curl -v -u "pipeline:$EVENT_PASS" -H 'content-type: application/json' \
-d "{\"action\": \"$WHOOPER_ACTION\", \"name\": \"$CONTAINER_NAME\", \"registry\": \"$REGISTRY_NAME\", \"image\": \"$IMAGE_NAME\", \"tag\": \"$CI_COMMIT_TAG\", \"network\": \"$CONTAINER_NETWORK\"}" \
$WHOOPER_ENDPOINT
only:
- tags
Event payload structure
{
"action": "(update-container)",
"name": "(name of our container process)",
"registry": "(container registry)",
"image": "(newly build container image)",
"tag": "(git tag pushed after last content change)",
"network": "(network our container is running in)"
}
4. Ansible rule book¶
Ansible rule book will receive a pipe line event containing the instruction to update a running container with a new version.
Ansible Rulebooks are a feature of Event-Driven Ansible (EDA) that enable automation based on events. They define a set of rules written in YAML, which specify event sources (e.g., webhooks, message queues) and actions to take when specific conditions are met. These rulebooks allow users to dynamically respond to real-time events, integrating Ansible's automation capabilities with event-driven architectures for tasks like scaling, notifications, or configuration management.
Rule book
- name: Rulebook to control containers on locahost
hosts: localhost
gather_facts: false
sources:
- ansible.eda.webhook:
host: 0.0.0.0
port: 6000
rules:
- name: Launch playbook on update-container payload
condition: |
event.payload.action == "update-container"
and event.payload.image is defined
and event.payload.name is defined
and event.payload.tag is defined
action:
run_playbook:
name: update_container.yml
extra_vars:
payload: "{{ event.payload }}"
Payload data structure
{
"action": "update-container",
"image": "naroj.eu",
"name": "naroj-eu",
"registry": "registry.gitlab.com/naroj",
"tag": "1.5",
"network": "backend"
}
The action will be executed when the input payload structure matches the eda condition. This acion executes the docker ansible module which needs exposure to the local docker API. The action playbook (update_container.yml) invokes the docker_container module which idempotently ensures the container image is running with the specified version passed by the payload definition.
- name: Web blog
community.docker.docker_container:
name: "{{ payload.name }}"
state: started
restart_policy: always
recreate: true
image: "{{ payload.image }}:{{ payload.tag }}"
networks:
- "{{ payload.network }}"
Rule book logs
********************************************************************************
2025-01-12 23:02:08,259 - ansible_rulebook.rule_set_runner - DEBUG - Posting data to ruleset Rulebook to control containers on locahost => {'payload': {'action': 'update-container', 'name': 'naroj-eu', 'registry': 'registry.gitlab.com/naroj', 'image': 'naroj.eu', 'tag': '1.5', 'network': 'backend'}, 'meta': {'endpoint': '', 'headers': {'Host': 'naroj.eu', 'User-Agent': 'curl/8.11.1', 'Content-Length': '148', 'Accept': '*/*', 'Content-Type': 'application/json', 'X-Forwarded-For': 'XX.229.XX.XX', 'X-Forwarded-Host': 'naroj.eu', 'X-Forwarded-Proto': 'https', 'Accept-Encoding': 'gzip'}, 'source': {'name': 'ansible.eda.webhook', 'type': 'ansible.eda.webhook'}, 'received_at': '2025-01-12T23:02:08.254428Z', 'uuid': 'b00c0a2e-607a-419c-b74b-a2ceec3b1798'}}
2025-01-12 23:02:08 265 [main] DEBUG org.drools.ansible.rulebook.integration.api.rulesengine.RegisterOnlyAgendaFilter - Activation of effective rule "Launch playbook on update-container payload" with facts: {m={payload={action=update-container, name=naroj-eu, registry=registry.gitlab.com/naroj, image=naroj.eu, tag=1.5, network=backend}, meta={endpoint=, headers={Host=naroj.eu, User-Agent=curl/8.11.1, Content-Length=148, Accept=*/*, Content-Type=application/json, X-Forwarded-For=XX.229.XX.XX, X-Forwarded-Host=naroj.eu, X-Forwarded-Proto=https, Accept-Encoding=gzip}, source={name=ansible.eda.webhook, type=ansible.eda.webhook}, received_at=2025-01-12T23:02:08.254428Z, uuid=b00c0a2e-607a-419c-b74b-a2ceec3b1798}}}
2025-01-12 23:02:08,267 - drools.ruleset - DEBUG - Calling rule : Launch playbook on update-container payload in session: 1
2025-01-12 23:02:08,267 - ansible_rulebook.rule_generator - DEBUG - callback calling Launch playbook on update-container payload
2025-01-12 23:02:08,268 - ansible_rulebook.rule_set_runner - DEBUG - Creating action task action::run_playbook::Rulebook to control containers on locahost::Launch playbook on update-container payload
2025-01-12 23:02:08,268 - ansible_rulebook.rule_set_runner - DEBUG - call_action run_playbook
2025-01-12 23:02:08,268 - ansible_rulebook.rule_set_runner - DEBUG - substitute_variables [{'name': 'update_container.yml', 'extra_vars': {'payload': '{{ event.payload }}'}}] [{'event': {'payload': {'action': 'update-container', 'name': 'naroj-eu', 'registry': 'registry.gitlab.com/naroj', 'image': 'naroj.eu', 'tag': '1.5', 'network': 'backend'}, 'meta': {'endpoint': '', 'headers': {'Host': 'naroj.eu', 'User-Agent': 'curl/8.11.1', 'Content-Length': '148', 'Accept': '*/*', 'Content-Type': 'application/json', 'X-Forwarded-For': 'XX.229.XX.X1', 'X-Forwarded-Host': 'naroj.eu', 'X-Forwarded-Proto': 'https', 'Accept-Encoding': 'gzip'}, 'source': {'name': 'ansible.eda.webhook', 'type': 'ansible.eda.webhook'}, 'received_at': '2025-01-12T23:02:08.254428Z', 'uuid': 'b00c0a2e-607a-419c-b74b-a2ceec3b1798'}}}]
2025-01-12 23:02:08,271 - ansible_rulebook.rule_set_runner - DEBUG - action args: {'name': 'update_container.yml', 'extra_vars': {'payload': {'action': 'update-container', 'name': 'naroj-eu', 'registry': 'registry.gitlab.com/naroj', 'image': 'naroj.eu', 'tag': '1.5', 'network': 'backend'}}}