diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 000000000..1ab9f6c3a --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +GO_VERSION = "1.24.5" +APP_GUEST_PATH = "/opt/quicknotes/app" + +Vagrant.configure("2") do |config| + config.vm.box = "generic/ubuntu2204" + config.vm.boot_timeout = 900 + config.vm.hostname = "quicknotes-vm" + + config.vm.network "forwarded_port", + guest: 8080, + host: 18080, + host_ip: "127.0.0.1", + auto_correct: false + + config.vm.synced_folder "./app", + APP_GUEST_PATH, + type: "virtualbox", + owner: "vagrant", + group: "vagrant", + SharedFoldersEnableSymlinksCreate: false, + mount_options: ["dmode=775", "fmode=664"] + + config.vm.provider "virtualbox" do |vb| + vb.name = "quicknotes-lab5" + vb.cpus = 2 + vb.memory = 1024 + end + + config.vm.provision "shell", + privileged: true, + env: { + "APP_DIR" => APP_GUEST_PATH, + "GO_VERSION" => GO_VERSION + }, + inline: <<-'SHELL' + set -euxo pipefail + export DEBIAN_FRONTEND=noninteractive + + apt-get update + apt-get install -y --no-install-recommends ca-certificates curl build-essential + + case "$(dpkg --print-architecture)" in + amd64) GO_ARCH="amd64" ;; + arm64) GO_ARCH="arm64" ;; + *) echo "unsupported CPU architecture"; exit 1 ;; + esac + + if ! /usr/local/go/bin/go version 2>/dev/null | grep -q "go${GO_VERSION} "; then + rm -rf /usr/local/go + curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz" -o /tmp/go.tgz + tar -C /usr/local -xzf /tmp/go.tgz + rm -f /tmp/go.tgz + fi + + cat >/etc/profile.d/go.sh <<'EOF' +export PATH=/usr/local/go/bin:$PATH +EOF + + install -d -o vagrant -g vagrant /var/lib/quicknotes + + cd "${APP_DIR}" + /usr/local/go/bin/go build -o /usr/local/bin/quicknotes . + + cat >/etc/systemd/system/quicknotes.service < APP_GUEST_PATH, + "GO_VERSION" => GO_VERSION + }, + inline: <<-'SHELL' + set -euxo pipefail + export DEBIAN_FRONTEND=noninteractive + + apt-get update + apt-get install -y --no-install-recommends ca-certificates curl build-essential + + case "$(dpkg --print-architecture)" in + amd64) GO_ARCH="amd64" ;; + arm64) GO_ARCH="arm64" ;; + *) echo "unsupported CPU architecture"; exit 1 ;; + esac + + if ! /usr/local/go/bin/go version 2>/dev/null | grep -q "go${GO_VERSION} "; then + rm -rf /usr/local/go + curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz" -o /tmp/go.tgz + tar -C /usr/local -xzf /tmp/go.tgz + rm -f /tmp/go.tgz + fi + + cat >/etc/profile.d/go.sh <<'EOF' +export PATH=/usr/local/go/bin:$PATH +EOF + + install -d -o vagrant -g vagrant /var/lib/quicknotes + + cd "${APP_DIR}" + /usr/local/go/bin/go build -o /usr/local/bin/quicknotes . + + cat >/etc/systemd/system/quicknotes.service < default: Box 'generic/ubuntu2204' could not be found. Attempting to find and install... + default: Box Provider: virtualbox + default: Box Version: >= 0 +==> default: Loading metadata for box 'generic/ubuntu2204' + default: URL: https://vagrantcloud.com/api/v2/vagrant/generic/ubuntu2204 +==> default: Adding box 'generic/ubuntu2204' (v4.3.12) for provider: virtualbox (amd64) + default: Downloading: https://vagrantcloud.com/generic/boxes/ubuntu2204/versions/4.3.12/providers/virtualbox/amd64/vagrant.box + default: Calculating and comparing box checksum... +==> default: Successfully added box 'generic/ubuntu2204' (v4.3.12) for 'virtualbox (amd64)'! +``` + +Provisioning and service excerpt: + +```text +==> default: Machine booted and ready! +==> default: Mounting shared folders... + default: C:/GitProjects/DevOps-Intro/app => /opt/quicknotes/app +==> default: Running provisioner: shell... + default: ++ curl -fsSL https://go.dev/dl/go1.24.5.linux-amd64.tar.gz -o /tmp/go.tgz + default: ++ /usr/local/go/bin/go build -o /usr/local/bin/quicknotes . + default: ++ systemctl enable --now quicknotes.service + default: Created symlink /etc/systemd/system/multi-user.target.wants/quicknotes.service -> /etc/systemd/system/quicknotes.service. + default: Active: active (running) since Wed 2026-06-17 11:14:20 UTC +``` + +Validation: + +```text +$ vagrant validate +Vagrantfile validated successfully. +``` + +Go version inside the VM: + +```text +$ vagrant ssh -c 'go version' +go version go1.24.5 linux/amd64 +``` + +QuickNotes health from inside the VM: + +```text +$ vagrant ssh -c 'curl -s http://localhost:8080/health' +{"notes":4,"status":"ok"} +``` + +QuickNotes health from the host through the forwarded port: + +```text +$ curl -s http://localhost:18080/health +{"notes":4,"status":"ok"} +``` + +Service status: + +```text +$ vagrant ssh -c 'systemctl --no-pager --full status quicknotes.service | sed -n "1,8p"' +quicknotes.service - QuickNotes API + Loaded: loaded (/etc/systemd/system/quicknotes.service; enabled; vendor preset: enabled) + Active: active (running) since Wed 2026-06-17 11:14:20 UTC; 29s ago + Main PID: 10088 (quicknotes) + Tasks: 7 (limit: 1012) + Memory: 1.6M + CPU: 24ms +``` + +### Design questions + +a) I used the `virtualbox` synced-folder type. For this Windows + VirtualBox setup it avoids an extra host-side `rsync` dependency and gives immediate bidirectional edits between the host `app/` directory and `/opt/quicknotes/app` in the VM. The trade-off is that VirtualBox shared folders depend on Guest Additions compatibility and can be slower than native filesystem access or one-way `rsync` for large trees. + +b) The VM uses NAT, which is Vagrant's default VirtualBox network mode. The only host exposure is a forwarded port bound to `127.0.0.1:18080`, so only the local machine can reach QuickNotes. That is safer than a bridged interface for a course exercise because bridged networking puts the VM directly on the LAN, where classmates, campus devices, or other local network clients could reach the service if firewalling is wrong. + +c) I used the shell provisioner. Installing one pinned Go toolchain, building one binary, and writing one systemd unit is simple enough that shell is the least moving part; Ansible would be useful later, but Lab 7 is where the course starts targeting this VM with Ansible. + +d) Pinning `1.24.5` instead of only `1.24` makes the build reproducible. A floating minor alias can move to a new patch release with different compiler behavior, security fixes, or cache contents, while a point release lets every student and CI run use the same toolchain. + +## Task 2 - Snapshots: Save, Break, Restore + +### Commands run + +```bash +vagrant snapshot save quicknotes-clean +vagrant snapshot list +vagrant ssh -c 'sudo mv /usr/local/go /usr/local/go.broken' +vagrant ssh -c 'go version' +time vagrant snapshot restore quicknotes-clean +vagrant ssh -c 'go version' +vagrant ssh -c 'curl -s http://localhost:8080/health' +curl -s http://localhost:18080/health +``` + +### Output + +```text +==> default: Snapshotting the machine as 'quicknotes-clean'... +==> default: Snapshot saved! You can restore the snapshot at any time by +==> default: using `vagrant snapshot restore`. You can delete it using +==> default: `vagrant snapshot delete`. + +quicknotes-clean + +bash: line 1: go: command not found + +RESTORE_SECONDS=19.523 + +go version go1.24.5 linux/amd64 +{"notes":4,"status":"ok"} +{"notes":4,"status":"ok"} +``` + +The deliberate break was moving `/usr/local/go` out of the way. That made `go version` fail with `command not found`, proving the VM was broken. Restoring `quicknotes-clean` recovered both the Go toolchain and the running QuickNotes service. + +### Snapshot design questions + +e) Snapshots are not backups because they usually live on the same host and storage pool as the VM. If the laptop disk dies, the VM directory is deleted, ransomware encrypts the host, or the VirtualBox metadata is corrupted, the snapshot is lost with the VM. A backup must be independent, recoverable elsewhere, and protected from the same failure domain. + +f) Copy-on-write means the snapshot initially stores metadata and only keeps blocks that change after the snapshot point. Ten snapshots do not immediately cost ten full VM disks, but disk usage grows as the VM writes new blocks and as each snapshot chain keeps older versions alive. Long chains can become surprisingly large even if each individual snapshot looked cheap when created. + +g) Snapshotting is an antipattern when snapshots become long-lived operational state. Long chains make restore behavior harder to reason about, can hurt performance, and encourage pet-server habits instead of rebuilding from code and configuration. For production-like systems, backups, image builds, configuration management, and disposable rebuilds are safer than keeping many old snapshots around. + +## Bonus Task + +### B.1 Vagrant VM baseline + +The VM was already provisioned, so the boot measurement below is a normal boot, not the first box download/provisioning run. + +```text +$ time vagrant halt +VM_HALT_SECONDS=8.901 + +$ time vagrant up --provider=virtualbox +VM_UP_SECONDS=36.339 + +$ vagrant ssh -c 'go version' +go version go1.24.5 linux/amd64 + +$ vagrant ssh -c 'curl -s http://localhost:8080/health' +{"notes":4,"status":"ok"} + +$ curl -s http://localhost:18080/health +{"notes":4,"status":"ok"} +``` + +Idle RAM: + +```text +$ vagrant ssh -c 'free -h' + total used free shared buff/cache available +Mem: 957Mi 179Mi 470Mi 0.0Ki 307Mi 638Mi +Swap: 2.0Gi 0B 2.0Gi +``` + +Process count: + +```text +$ vagrant ssh -c 'ps -A --no-headers | wc -l' +113 +``` + +Disk size: + +```text +$ du -sh "C:\Users\bear_\VirtualBox VMs\quicknotes-lab5" +6.44 GiB +``` + +### B.2 Docker container baseline + +I used the lab-provided `golang:1.24` container command, mounted the same `app/` directory, and exposed it on host port `28080`. The first `docker run` pulled the image; that pull time is not counted as cold start. + +```text +$ docker run -d -p 28080:8080 -v 'C:\GitProjects\DevOps-Intro\app:/src' -w /src golang:1.24 sh -c 'go build -o /tmp/qn && /tmp/qn' +CONTAINER_ID=f21e3a1e8178680ab9f9082354d4a33c2e4e328df1333270ab6ab3e56cc51fa1 + +$ curl -s http://localhost:28080/health +{"notes":6,"status":"ok"} + +$ docker stop f21e3a1e8178 +$ time docker start f21e3a1e8178 +DOCKER_START_SECONDS=0.282 + +$ curl -s http://localhost:28080/health +{"notes":6,"status":"ok"} +``` + +Idle RAM: + +```text +$ docker stats --no-stream +8.23MiB / 7.682GiB +``` + +Process count: + +```text +$ docker top f21e3a1e8178 +DOCKER_PROCESS_COUNT=2 +``` + +Image size: + +```text +$ docker images golang:1.24 --format '{{.Size}}' +1.32GB +``` + +### B.3 Comparison + +| Dimension | Vagrant VM | Docker container | +|-----------------------|-----------:|-----------------:| +| Cold start | 36.339s | 0.282s | +| Idle RAM | 179Mi | 8.23MiB | +| On-disk size | 6.44 GiB | 1.32GB | +| Process count (guest) | 113 | 2 | + +The biggest gap is startup time: the VM has to boot a full guest OS, while Docker only restarts an already-created container process tree. RAM is also much lower for the container because it shares the host kernel instead of carrying a full init system, SSH service, journald, networking stack, and other guest daemons. A VM is the right tool when kernel isolation, full OS behavior, or cross-OS testing matters; a container is the better fit for packaging and running one stateless service quickly. This data explains why containers won the 2014-2020 stateless microservice era: the operational unit became smaller, faster to replace, and cheaper to pack densely on the same hardware. VMs still matter for stronger boundaries and complete machine simulation, but they are heavier than needed for a simple QuickNotes API process.