diff --git a/ansible/files/quicknotes b/ansible/files/quicknotes new file mode 100644 index 000000000..897a2d74e Binary files /dev/null and b/ansible/files/quicknotes differ diff --git a/ansible/files/seed.json b/ansible/files/seed.json new file mode 100644 index 000000000..ecf4fd2ed --- /dev/null +++ b/ansible/files/seed.json @@ -0,0 +1,26 @@ +[ + { + "id": 1, + "title": "Welcome to QuickNotes", + "body": "This is the project you'll containerize, deploy, monitor, and harden across all 10 labs.", + "created_at": "2026-01-15T10:00:00Z" + }, + { + "id": 2, + "title": "Read app/main.go first", + "body": "Start by understanding the entry point — env vars, signal handling, graceful shutdown.", + "created_at": "2026-01-15T10:05:00Z" + }, + { + "id": 3, + "title": "DevOps mantra", + "body": "If it hurts, do it more often.", + "created_at": "2026-01-15T10:10:00Z" + }, + { + "id": 4, + "title": "Endpoint cheat-sheet", + "body": "GET /notes GET /notes/{id} POST /notes DELETE /notes/{id} GET /health GET /metrics", + "created_at": "2026-01-15T10:15:00Z" + } +] diff --git a/ansible/inventory.ini b/ansible/inventory.ini new file mode 100644 index 000000000..c58f6cf26 --- /dev/null +++ b/ansible/inventory.ini @@ -0,0 +1,8 @@ +# WSL setup (run once): +# cp /mnt/c/Users/Selysecr/.vagrant.d/insecure_private_keys/vagrant.key.rsa ~/.ssh/vagrant-lab5 +# chmod 600 ~/.ssh/vagrant-lab5 +# +# Requires lab5 VM with port 2223 forwarded (0.0.0.0) — vagrant reload on feature/lab5. +# Windows host IP: ip route show default | awk '{print $3}' +[quicknotes_vms] +lab5-vm ansible_host=172.18.160.1 ansible_port=2223 ansible_user=vagrant ansible_ssh_private_key_file=/home/selysecr/.ssh/vagrant-lab5 ansible_ssh_common_args='-o StrictHostKeyChecking=no -o PubkeyAcceptedKeyTypes=+ssh-rsa -o HostKeyAlgorithms=+ssh-rsa' diff --git a/ansible/inventory.local.ini b/ansible/inventory.local.ini new file mode 100644 index 000000000..f79c4c8cb --- /dev/null +++ b/ansible/inventory.local.ini @@ -0,0 +1,3 @@ +# Used by ansible-pull on the VM (self-reconcile via local connection). +[quicknotes_vms] +127.0.0.1 ansible_connection=local diff --git a/ansible/playbook.yaml b/ansible/playbook.yaml new file mode 100644 index 000000000..15fa4cb1e --- /dev/null +++ b/ansible/playbook.yaml @@ -0,0 +1,111 @@ +--- +- name: Deploy QuickNotes to Lab 5 VM + hosts: quicknotes_vms + become: true + gather_facts: false + + vars: + quicknotes_user: quicknotes + quicknotes_group: quicknotes + quicknotes_data_dir: /var/lib/quicknotes + quicknotes_listen_addr: ":9191" + quicknotes_data_path: "{{ quicknotes_data_dir }}/notes.json" + quicknotes_seed_path: "{{ quicknotes_data_dir }}/seed.json" + ansible_pull_repo_url: "https://github.com/selysecr332/DevOps-Intro.git" + ansible_pull_branch: "feature/lab7" + ansible_pull_checkout_dir: "/var/lib/ansible-pull/devops-intro" + + handlers: + - name: restart quicknotes + ansible.builtin.systemd: + name: quicknotes + state: restarted + daemon_reload: true + + tasks: + - name: Ensure quicknotes system user exists + ansible.builtin.user: + name: "{{ quicknotes_user }}" + system: true + shell: /usr/sbin/nologin + create_home: false + + - name: Ensure data directory exists + ansible.builtin.file: + path: "{{ quicknotes_data_dir }}" + state: directory + owner: "{{ quicknotes_user }}" + group: "{{ quicknotes_group }}" + mode: "0750" + + - name: Install QuickNotes binary + ansible.builtin.copy: + src: files/quicknotes + dest: /usr/local/bin/quicknotes + owner: root + group: root + mode: "0755" + notify: restart quicknotes + + - name: Install seed data file + ansible.builtin.copy: + src: files/seed.json + dest: "{{ quicknotes_seed_path }}" + owner: "{{ quicknotes_user }}" + group: "{{ quicknotes_group }}" + mode: "0640" + + - name: Install systemd unit + ansible.builtin.template: + src: quicknotes.service.j2 + dest: /etc/systemd/system/quicknotes.service + owner: root + group: root + mode: "0644" + notify: restart quicknotes + + - name: Enable and start QuickNotes service + ansible.builtin.systemd: + name: quicknotes + enabled: true + state: started + daemon_reload: true + + - name: Install Ansible and Git for ansible-pull + ansible.builtin.apt: + name: + - ansible + - git + state: present + update_cache: true + + - name: Ensure ansible-pull checkout directory exists + ansible.builtin.file: + path: "{{ ansible_pull_checkout_dir }}" + state: directory + owner: root + group: root + mode: "0755" + + - name: Install ansible-pull systemd service + ansible.builtin.template: + src: ansible-pull.service.j2 + dest: /etc/systemd/system/ansible-pull.service + owner: root + group: root + mode: "0644" + + - name: Install ansible-pull systemd timer + ansible.builtin.template: + src: ansible-pull.timer.j2 + dest: /etc/systemd/system/ansible-pull.timer + owner: root + group: root + mode: "0644" + + - name: Enable and start ansible-pull timer + ansible.builtin.systemd: + name: ansible-pull.timer + enabled: true + state: started + daemon_reload: true diff --git a/ansible/templates/ansible-pull.service.j2 b/ansible/templates/ansible-pull.service.j2 new file mode 100644 index 000000000..90c8650dc --- /dev/null +++ b/ansible/templates/ansible-pull.service.j2 @@ -0,0 +1,8 @@ +[Unit] +Description=GitOps reconcile QuickNotes via ansible-pull +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +ExecStart=/usr/bin/ansible-pull -U {{ ansible_pull_repo_url }} -C {{ ansible_pull_branch }} -d {{ ansible_pull_checkout_dir }} -i ansible/inventory.local.ini ansible/playbook.yaml diff --git a/ansible/templates/ansible-pull.timer.j2 b/ansible/templates/ansible-pull.timer.j2 new file mode 100644 index 000000000..94e936d0b --- /dev/null +++ b/ansible/templates/ansible-pull.timer.j2 @@ -0,0 +1,10 @@ +[Unit] +Description=Run ansible-pull every 5 minutes + +[Timer] +OnBootSec=1min +OnUnitActiveSec=5min +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/ansible/templates/quicknotes.service.j2 b/ansible/templates/quicknotes.service.j2 new file mode 100644 index 000000000..bcf75e31f --- /dev/null +++ b/ansible/templates/quicknotes.service.j2 @@ -0,0 +1,19 @@ +[Unit] +Description=QuickNotes API +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User={{ quicknotes_user }} +Group={{ quicknotes_group }} +WorkingDirectory={{ quicknotes_data_dir }} +Environment=ADDR={{ quicknotes_listen_addr }} +Environment=DATA_PATH={{ quicknotes_data_path }} +Environment=SEED_PATH={{ quicknotes_seed_path }} +ExecStart=/usr/local/bin/quicknotes +Restart=on-failure +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/submissions/lab7.md b/submissions/lab7.md new file mode 100644 index 000000000..0c7af98dc --- /dev/null +++ b/submissions/lab7.md @@ -0,0 +1,229 @@ +# Lab 7 — Configuration Management: Deploy QuickNotes via Ansible + +Mahmoud Hassan (`selysecr332`) +**Environment:** Windows 11 + WSL Ansible 2.16.3 + Lab 5 Vagrant VM (`quicknotes-lab5`) + +--- + +## Task 1 — Idempotent deploy to Lab 5 VM + +### Layout + +```text +ansible/ +├── inventory.ini +├── inventory.local.ini +├── playbook.yaml +├── files/ +│ ├── quicknotes +│ └── seed.json +└── templates/ + ├── quicknotes.service.j2 + ├── ansible-pull.service.j2 + └── ansible-pull.timer.j2 +``` + +### `playbook.yaml` / `inventory.ini` / template + +See [`ansible/`](../ansible/) directory. + +### First run PLAY RECAP + +```text +PLAY [Deploy QuickNotes to Lab 5 VM] ******************************************************************* + +TASK [Ensure quicknotes system user exists] ************************************************************ +changed: [lab5-vm] + +TASK [Ensure data directory exists] ******************************************************************** +changed: [lab5-vm] + +TASK [Install QuickNotes binary] *********************************************************************** +changed: [lab5-vm] + +TASK [Install seed data file] ************************************************************************** +changed: [lab5-vm] + +TASK [Install systemd unit] **************************************************************************** +changed: [lab5-vm] + +TASK [Enable and start QuickNotes service] ************************************************************* +changed: [lab5-vm] + +RUNNING HANDLER [restart quicknotes] ******************************************************************* +changed: [lab5-vm] + +PLAY RECAP ********************************************************************************************* +lab5-vm : ok=7 changed=7 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +### Health check from host + +```powershell +PS> Invoke-RestMethod http://127.0.0.1:18080/health + +notes status +----- ------ + 4 ok +``` + +### Design questions (Task 1) + +**a) `command:` vs dedicated modules?** + +Dedicated modules (`user`, `file`, `copy`, `template`, `systemd`) check desired state before acting — they report `ok` when nothing needs changing. Raw `command:`/`shell:` always run unless wrapped with `creates:`/`removes:`. Idempotency matters because re-runs are safe deploys, not one-off scripts. + +**b) `notify:` and handlers?** + +Handlers fire **once at the end of the play**, only if a notifying task reports `changed`. If the task is `ok` (already converged), the handler does **not** run. That's correct: restart only when binary or unit file actually changed. + +**c) Variable hierarchy — top 3 for this lab?** + +1. **Play `vars:`** — defaults for this deploy (`quicknotes_listen_addr`, paths) visible in one file +2. **`group_vars/quicknotes_vms/`** — host-group overrides if inventory grows +3. **Extra vars (`-e`)** — one-off overrides for Task 2 demo (`listen_addr` tweak) without editing the playbook + +**d) `gather_facts: true` default — need it here?** + +No. This playbook uses only explicit variables and static paths — no `ansible_distribution` or package facts. `gather_facts: false` skips the fact-gathering SSH round-trip (~1–2 s per host). + +--- + +## Task 2 — Idempotency + selective re-run + +### Second run (`changed=0`) + +```text +PLAY RECAP ********************************************************************* +lab5-vm : ok=6 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +### Variable tweak (`listen_addr` → `:9090`) + +```bash +ansible-playbook -i ansible/inventory.ini ansible/playbook.yaml -e quicknotes_listen_addr=:9090 +``` + +```text +TASK [Install systemd unit] **************************************************** +changed: [lab5-vm] + +RUNNING HANDLER [restart quicknotes] ******************************************* +changed: [lab5-vm] + +PLAY RECAP ********************************************************************* +lab5-vm : ok=7 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +### `--check --diff` preview + +```bash +ansible-playbook -i ansible/inventory.ini ansible/playbook.yaml -e quicknotes_listen_addr=:8080 --check --diff +``` + +```diff +--- before: /etc/systemd/system/quicknotes.service ++++ after: .../quicknotes.service.j2 +@@ -8,7 +8,7 @@ +-Environment=ADDR=:9090 ++Environment=ADDR=:8080 +``` + +### Design questions (Task 2) + +**e) Why `changed=0` on second run?** + +`copy` compares checksums; `template` compares rendered content; `file` checks path/mode/owner. If all match desired state, Ansible reports `ok` not `changed`. + +**f) `shell: echo ... > unit file` instead of `template:`?** + +Every run would rewrite the file (or need manual idempotency guards). Handlers wouldn't fire reliably; drift is invisible; partial failures leave a broken unit. `template` + `notify` is declarative. + +**g) `--check` vs `--check --diff`?** + +`--check` says *whether* something would change. `--diff` shows *what* would change (e.g. `ADDR=:9090` in the unit). You catch wrong variable values before applying. + +--- + +## Bonus — `ansible-pull` GitOps loop + +### Added files + +```text +ansible/ +├── inventory.local.ini # local connection for pull on VM +└── templates/ + ├── ansible-pull.service.j2 + └── ansible-pull.timer.j2 +``` + +Playbook vars: `ansible_pull_repo_url`, `ansible_pull_branch`, `ansible_pull_checkout_dir`. + +### `systemctl list-timers | grep ansible-pull` + +```text +Wed 2026-06-24 23:02:39 UTC 3min 31s left Wed 2026-06-24 22:55:17 UTC 3min 50s ago ansible-pull.timer ansible-pull.service +``` + +Timer is `active`; service unit runs `ansible-pull -U https://github.com/selysecr332/DevOps-Intro.git -C feature/lab7`. + +### Convergence timeline (`listen_addr` :8080 → :9191) + +| Event | Timestamp (UTC) | +|-------|-----------------| +| Git commit + push `761551e` (`quicknotes_listen_addr: ":9191"`) | **2026-06-24 23:38:29** | +| Next timer fire (`ansible-pull.service` started) | **2026-06-24 23:38:48** | +| VM reconciled (`changed=2`, template + handler) | **2026-06-24 23:39:36** | + +Verified on VM after timer (no manual `ansible-playbook` from host): + +```text +$ grep ADDR /etc/systemd/system/quicknotes.service +Environment=ADDR=:9191 +``` + +Journal excerpt: + +```text +2026-06-24T23:39:36 quicknotes-vm ansible-pull[11038]: 127.0.0.1 : ok=12 changed=2 ... +2026-06-24T23:39:36 quicknotes-vm systemd[1]: Finished GitOps reconcile QuickNotes via ansible-pull. +``` + +**Elapsed push → reconcile: ~67 seconds** (next 5-minute timer window). + +**h) Security benefit of pull vs push?** + +Pull mode: VM initiates outbound HTTPS to Git — no inbound SSH from a control node, smaller attack surface, works behind NAT. + +**i) Kubernetes equivalent?** + +**GitOps** tools like **Argo CD** or **Flux** — cluster pulls desired state from Git and reconciles. `ansible-pull` + systemd timer is the same loop at VM scale. + +--- + +## Lab 7 completion checklist + +### Task 1 (6 pts) + +- [x] Playbook deploys to Lab 5 VM +- [x] `curl :18080/health` works +- [x] First-run PLAY RECAP captured +- [x] Design questions a–d answered + +### Task 2 (4 pts) + +- [x] Second run `changed=0` +- [x] Variable tweak + handler demo +- [x] `--check --diff` captured +- [x] Design questions e–g answered + +### Bonus (2 pts) + +- [x] ansible-pull timer active +- [x] Push → VM converges ≤ 5 min +- [x] Design questions h–i answered + +### Submission + +- [x] Course PR (`feature/lab7` → `inno-devops-labs/main`) +- [x] Fork PR (`feature/lab7-fork` → `selysecr332/main`)