Automate everything! #2

Im ersten Teil dieser Blogserie, bin ich primär darauf eingegangen, wie wir Ansible und Renovate Bot bei /sys einsetzen um möglichst alles zu Automatisieren. In diesem Teil, möchte ich auf weitere Tools, sowie auch wichtige Arbeitsmethoden eingehen.

 

Octodns

Auch unser DNS ist komplett via IaC gemanaged. Fully Qualified Domain Names der einzelnen Maschinen werden gleich beim Deployment via Ansible gesetzt. Alle anderen Zonen managen wir via Octodns. Dies ermöglicht uns, unsere DNS-Changes wie gewohnt via Merge Requet peerreviewen zu lassen und anschliessend vollautomatisiert via Pipeline auf unsere Providers ausrollen zu lassen. Octodns unterstützt unterdessen eine riesen Bandbreite an unterschiedlichen Providers.

Das mit diesem Git

Seit gut 10 Jahren ist bei Puzzle ITC Git die Standard Sourcecode Versionierung und mit Gitlab tiptop in unsere Umgebung integriert. Dass Git sich als Standard version control system etabliert hat, da sind sich wohl die Meisten einig, wie aber mit Git in einem Team oder gar Teamübergreifend gearbeitet werden soll, da scheiden sich die Geister. Nachfolgend ein paar Schwänke aus unseren Erfahrungen:

Merge Requests / Peerreview

Die Abmachung bei uns im Team ist, dass jede Änderung an der Produktion durch ein Teamgspändli reviewed wird. Dies nicht, weil wir einander nicht vertrauen, sondern weil wir voneinander lernen wollen und weil wir auch ständig an unserer Fehlerkultur arbeiten. Da, wie wir im letzten Beitrag bereits gelernt haben, der Grossteil unserer Infrastruktur durch IaC managed wird, passiert dieses Review am einfachsten via Merge Request.

Dieser Workflow spart uns in der Regel sogar Zeit, da Fehler bereits identifiziert werden, bevor die Änderung gemerged wird. Durch diesen Gewinn hat sich dieser Workflow auch für Codebases etabliert, welche keinen produktiven Charakter haben. Gerade, wenn (zum Teil. zeitintensive) Pipelines im Spiel sind, lohnt es sich ungemein.

Restrict yourself / Protect your branches

Es ist wichtig, sich umso besser zu schützen, je mehr Pipelines man hat, da man manchmal unachtsam sein kann. Man entdeckt einen Bug, schnell die Konsole auf, Bugfix, git add, git commit, git push und schon ist alles erst recht am Ranzen, da man auf dem default-Branch war und eine Pipeline dies gleich bis in die Produktion propagiert hat. Dies passiert unseren Repos maximal einmal und anschliessend wird der Default Branch protected!

Pipelines – Where the automation begins

Unter Pipelines versteht man nichts anderes als ein Haufen von Skripts, welche in einer bestimmten Reihenfolge hintereinander oder gar parallel laufen und evtl. auch auf Grund von Returncodes die ganze Prozedur stoppen. Eine klassische Pipeline besteht aus einer oder mehreren Stages, welche wiederum einen oder mehrere Jobs beinhaltet. Dass eine Pipeline von einer Stage zur nächsten weiter läuft, müssen alle Jobs der letzten Stage erfolgreich sein. Als Beispiel möchte ich hier wiederum Teile unserer Ansible Pipeline vorstellen:

Lint / Codequality

Um ein erstes Feedback zur Codequalität zu erhalten, verwenden wir Linter. In den Ansible-Projekten ist verwenden wir Ansible-Lint mit folgender Konfiguration:

 

# Ansible-Lint configuration
# Options: https://github.com/ansible/ansible-lint#configuration-file

use_default_rules: true
offline: true

warn_list:
  - experimental

skip_list:
  - meta-no-info # we do not use meta anyhow
  - name[casing] # why would anyone wanna have that
  - name[template] # we're fine with jinja in between the Name
  - template-instead-of-copy # we are lazy bastards and won't create a template file for one-liners

exclude_paths:
  - roles/_example
  - inventories/host_vars/localhost.yml

var_naming_pattern: "^[a-z0-9]*_[a-z0-9_]*$"

enable_list:
  - no-log-password
  - no-same-owner

Als Erstes aktivieren wir die Standard-Regeln des Linters und mit offline: true halten wir den Linter davon ab, dass er Collections und Rollen nicht erneut runterlädt vor dem Prüfen. Dies, weil wir diese bereits im Image resp. in der Entwicklungsumgebung installiert haben und nicht zum linten nochmals neu installiert brauchen. Die nächste Konfiguration ermöglicht uns experimentelle Rules bereits frühzeitig zu erkennen, jedoch die Pipeline nicht failen zu lassen, wenn eine solche Rule anschlägt.
Anschliessend überspringen wir gewisse Regeln, welche wir intern nicht verwenden wollen, die Begründung wieso erfassen wir gleich als Kommentar.
Als Nächstes werden Pfade übersprungen, welche nicht geprüft werden sollen.
Mit der Option var_naming_pattern versuchen wir die Convention, dass alle Variablen mit den Rollennamen prefixed sein sollten, zu enforcen.
Zu guter Letzt werden noch gewisse Rules enabled, welche per Default nicht im Linter enabled sind.

Inventory Check

In unseren Ansible Repos, checken wir die Inventories mit folgendem Code auf Fehler:

ansible-inventory --list -i inventories/cloud_production   >/dev/null 2>> /tmp/warnings
wc -l /tmp/warnings | grep '^0 ' || { cat /tmp/warnings; exit 1; }

Beispiel Pipeline

Als Beispiel Pipeline möchte ich wiederum unser Haupt Ansible Repository genauer beleuchten.

Visualisierung der Pipeline
Visualisierung der Pipeline

Da das komplette .gitlab-ci.yml etwas über 400 Zeilen hat, werde ich hier wiederum nur Ausschnitte aufführen.

build:
  stage: build
  tags:
    - docker_builder
  script:
    - docker login -u "puzzle+gitlab" -p "$QUAY_PUZZLE_GITLAB_PASSWORD" registry.puzzle.ch
    - VERSION_TAG="renovate-$CI_COMMIT_SHORT_SHA"
    - echo $VERSION_TAG
    - docker build . -t registry.puzzle.ch/puzzle/ansible:$VERSION_TAG --rm --label quay.expires-after=1d
    - docker run registry.puzzle.ch/puzzle/ansible:$VERSION_TAG ansible --version
    - echo "VERSION_TAG=$VERSION_TAG" > version_tag.env
    - docker push registry.puzzle.ch/puzzle/ansible:$VERSION_TAG
  artifacts:
    reports:
      dotenv: version_tag.env
  only:
    refs:
      - merge_requests
    changes:
      - Dockerfile
      - Pipfile
      - requirements.yml
  allow_failure: true

Als erste Stage in unserer Pipeline bauen wir uns das Image zusammen, welches wir dann in den nächsten Stages verwenden werden. Diese Stage wird nur dann getriggert, wenn eine der drei Dateien verändert wird, welche einen Einfluss auf das Image haben. (siehe dazu: Dockerfile in letztem Blog verlinken)
Etwas speziell ist, dass wir das Image jeweils mit dem Git-Commit-Hash taggen und jeweils nach 24h wieder aus unserer Registry löschen. Dies hat jedoch den grossen Vorteil, dass wir unsere komplette Pipeline jeweils mit einem neuen Image testen können und dann in der letzten Stage in die Produktion überführen können:

lock:
  stage: update
  script:
    - git config --global user.email "systems@puzzle.ch"
    - git config --global user.name "Pipeline Zwiseli"
    - git remote add api-origin https://oauth2:${OAUTH2_TOKEN}@gitlab.puzzle.ch/pitc_ansible/ansible-puzzle.git
    - git checkout $CI_COMMIT_REF_NAME
    - pipenv lock
    - git add Pipfile.lock
    - git commit -m 'updated lockfile'
    - git push -u api-origin $CI_COMMIT_REF_NAME
  only:
    refs:
      - merge_requests
    changes:
      - Pipfile
  when: manual

tag:
  stage: update
  tags:
    - docker_builder
  script:
    - VERSION="$(cat Pipfile.lock | jq .default.ansible.version -r | tr -d '=')"
    - echo $VERSION
    - docker build . -t registry.puzzle.ch/puzzle/ansible:$VERSION
    - docker push registry.puzzle.ch/puzzle/ansible:$VERSION
    - git config --global user.email "systems@puzzle.ch"
    - git config --global user.name "Pipeline Zwiseli"
    - git remote add api-origin https://oauth2:${OAUTH2_TOKEN}@gitlab.puzzle.ch/pitc_ansible/ansible-puzzle.git || true
    - git checkout $CI_COMMIT_REF_NAME
    - >
      sed -i "s/^  VERSION_TAG:.*/  VERSION_TAG: \"${VERSION}\"  # default image tag to use/" .gitlab-ci.yml
    - git add .gitlab-ci.yml
    - git commit -m "updated dockerfile to ${VERSION}"
    - git push -u api-origin $CI_COMMIT_REF_NAME
  only:
    refs:
      - merge_requests
    changes:
      - Dockerfile
      - Pipfile
      - requirements.yml
  when: manual

Der Lock-Task locked unser Pipfile auf den aktuellen Stand und commited dieses wiederum in den aktuell offenen Merge Request. Der Tag-Task liest die Ansible Version aus dem Lock-File aus und setzt diese anschliessend als Tag auf das Docker Image und pushed dies (diesmal ohne Ablaufdatum). Zusätzlich ersetzt dieser Task gleich das Default Image im .gitlab-ci.yml, damit nach dem Merge das neue Image verwendet wird.

Die Lint-Stage führt wie bereits oben erwähnt bei jedem Merge Request sowohl ein ansible-lint als auch ein Inventory Check aus.

# Common configs to be used as extend in ansible steps
.ansible-common:
  image: registry.puzzle.ch/puzzle/ansible:$VERSION_TAG
  tags:
    - cloudscale_prod
  variables:
    ANSIBLE_DISPLAY_OK_HOSTS: 'No'
    ANSIBLE_DISPLAY_SKIPPED_HOSTS: 'No'
    PLAYBOOK_ARGUMENTS: "-i inventories/cloud_staging --diff --check"
  script:
    - ansible-playbook plays/ansible_sshkey.yml
    - ansible-playbook plays/site.yml $PLAYBOOK_ARGUMENTS
  dependencies:
    - build

# Site configs to be used as extend in ansible steps
.ansible-site:
  extends: .ansible-common
  script:
    - ansible-playbook plays/ansible_sshkey.yml
    - ansible-playbook plays/site.yml $PLAYBOOK_ARGUMENTS

check_cloud_production:
  stage: checkmode
  image: registry.puzzle.ch/puzzle/ansible:$VERSION_TAG
  extends: .ansible-site
  variables:
    PLAYBOOK_ARGUMENTS: "-i inventories/cloud_production --diff --check"
  only:
    - merge_requests
  when: manual
  allow_failure: true

Die Checkmode-Stage erlaubt uns, Ansible auf jede unserer Umgebung mit --diff --check auszuführen um potentielle Änderungen bereits zu sehen.

play_cloud_production:
  stage: deploy
  extends: .ansible-site
  environment:
    name: cloud_production
  variables:
    PLAYBOOK_ARGUMENTS: "-i inventories/cloud_production --diff"
  only:
    - main
    - schedules
  except:
    variables:
      - $JOB == "ldap_sshkey_sync"
      - $JOB == "play_cloud_staging"

Und schlussendlich führt die Deploy-Stage Ansible «scharf» aus. Hier als Beispiel auf unsere Cloudscale Umgebung. Andere Tasks werden z. T. auch über Schedules gestartet und erledigen spezifische Aufgaben wie zum Beispiel das Ausrollen von Zertifikaten auf unserer Staging Umgebung.

Kommentare sind geschlossen.