diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..d3bb44554 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,98 @@ +name: CI + +on: + push: + branches: [ main ] + paths: + - 'app/**' + - '.github/workflows/ci.yml' + pull_request: + branches: [ main ] + paths: + - 'app/**' + - '.github/workflows/ci.yml' + +permissions: + contents: read + +jobs: + vet: + name: vet (${{ matrix.go-version }}) + runs-on: ubuntu-24.04 + strategy: + matrix: + go-version: [ '1.23', '1.24' ] + fail-fast: false + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2 + + - name: Set up Go ${{ matrix.go-version }} + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.3.0 + with: + go-version: ${{ matrix.go-version }} + cache: true + cache-dependency-path: app/go.sum + + - name: go vet + working-directory: app + run: go vet ./... + + test: + name: test (${{ matrix.go-version }}) + runs-on: ubuntu-24.04 + strategy: + matrix: + go-version: [ '1.23', '1.24' ] + fail-fast: false + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2 + + - name: Set up Go ${{ matrix.go-version }} + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.3.0 + with: + go-version: ${{ matrix.go-version }} + cache: true + cache-dependency-path: app/go.sum + + - name: go test -race + working-directory: app + run: go test -race -count=1 ./... + + lint: + name: lint + runs-on: ubuntu-24.04 + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2 + + - name: Set up Go + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.3.0 + with: + go-version: '1.24' + cache: true + cache-dependency-path: app/go.sum + + - name: Install golangci-lint v2.5.0 + working-directory: app + run: | + go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0 + + - name: Run golangci-lint + working-directory: app + run: golangci-lint run + + ci-ok: + name: ci-ok + if: always() + needs: [ vet, test, lint ] + runs-on: ubuntu-24.04 + steps: + - name: Check results + run: | + if [ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "true" ]; then + echo "Some jobs failed or were cancelled" + exit 1 + fi + echo "All jobs passed" \ No newline at end of file diff --git a/.github/workflows/ci.yml.backup b/.github/workflows/ci.yml.backup new file mode 100644 index 000000000..d3bb44554 --- /dev/null +++ b/.github/workflows/ci.yml.backup @@ -0,0 +1,98 @@ +name: CI + +on: + push: + branches: [ main ] + paths: + - 'app/**' + - '.github/workflows/ci.yml' + pull_request: + branches: [ main ] + paths: + - 'app/**' + - '.github/workflows/ci.yml' + +permissions: + contents: read + +jobs: + vet: + name: vet (${{ matrix.go-version }}) + runs-on: ubuntu-24.04 + strategy: + matrix: + go-version: [ '1.23', '1.24' ] + fail-fast: false + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2 + + - name: Set up Go ${{ matrix.go-version }} + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.3.0 + with: + go-version: ${{ matrix.go-version }} + cache: true + cache-dependency-path: app/go.sum + + - name: go vet + working-directory: app + run: go vet ./... + + test: + name: test (${{ matrix.go-version }}) + runs-on: ubuntu-24.04 + strategy: + matrix: + go-version: [ '1.23', '1.24' ] + fail-fast: false + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2 + + - name: Set up Go ${{ matrix.go-version }} + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.3.0 + with: + go-version: ${{ matrix.go-version }} + cache: true + cache-dependency-path: app/go.sum + + - name: go test -race + working-directory: app + run: go test -race -count=1 ./... + + lint: + name: lint + runs-on: ubuntu-24.04 + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2 + + - name: Set up Go + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.3.0 + with: + go-version: '1.24' + cache: true + cache-dependency-path: app/go.sum + + - name: Install golangci-lint v2.5.0 + working-directory: app + run: | + go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0 + + - name: Run golangci-lint + working-directory: app + run: golangci-lint run + + ci-ok: + name: ci-ok + if: always() + needs: [ vet, test, lint ] + runs-on: ubuntu-24.04 + steps: + - name: Check results + run: | + if [ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "true" ]; then + echo "Some jobs failed or were cancelled" + exit 1 + fi + echo "All jobs passed" \ No newline at end of file diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 000000000..9b33c0f3a --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,25 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +Vagrant.configure("2") do |config| + config.vm.box = "ubuntu/jammy64" + config.vm.hostname = "quicknotes-vm" + config.vm.network "forwarded_port", guest: 8080, host: 18080, host_ip: "127.0.0.1" + config.vm.synced_folder "./app", "/home/vagrant/app" + + config.vm.provider "virtualbox" do |vb| + vb.memory = "1024" + vb.cpus = 2 + end + + config.vm.provision "shell", inline: <<-SHELL + apt-get update + apt-get install -y wget git + wget -q https://go.dev/dl/go1.24.5.linux-amd64.tar.gz + tar -C /usr/local -xzf go1.24.5.linux-amd64.tar.gz + echo 'export PATH=$PATH:/usr/local/go/bin' >> /home/vagrant/.bashrc + echo 'export PATH=$PATH:/usr/local/go/bin' >> /root/.bashrc + export PATH=$PATH:/usr/local/go/bin + go version + SHELL +end \ No newline at end of file diff --git a/Vagrantfile.txt b/Vagrantfile.txt new file mode 100644 index 000000000..0c40ac5be --- /dev/null +++ b/Vagrantfile.txt @@ -0,0 +1,35 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +Vagrant.configure("2") do |config| + # 1. Базовый образ Ubuntu 22.04 (работает быстрее, чем 24.04) + config.vm.box = "ubuntu/jammy64" + + # 2. Хостнейм + config.vm.hostname = "quicknotes-vm" + + # 3. Проброс порта 18080 хост → 8080 гость + config.vm.network "forwarded_port", guest: 8080, host: 18080, host_ip: "127.0.0.1" + + # 4. Синхронизация папки ./app → /home/vagrant/app + config.vm.synced_folder "./app", "/home/vagrant/app" + + # 5. Ресурсы: 2 vCPU, 1024 MB RAM + config.vm.provider "virtualbox" do |vb| + vb.memory = "1024" + vb.cpus = 2 + end + + # 6. Provisioning: установка Go 1.24.5 + config.vm.provision "shell", inline: <<-SHELL + apt-get update + apt-get install -y wget git + wget -q https://go.dev/dl/go1.24.5.linux-amd64.tar.gz + tar -C /usr/local -xzf go1.24.5.linux-amd64.tar.gz + echo 'export PATH=$PATH:/usr/local/go/bin' >> /home/vagrant/.bashrc + echo 'export PATH=$PATH:/usr/local/go/bin' >> /root/.bashrc + # для текущей сессии + export PATH=$PATH:/usr/local/go/bin + go version + SHELL +end \ No newline at end of file diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 000000000..291fdf123 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,4 @@ +[ssh_connection] +control_path = /dev/null +control_master = false +scp_if_ssh = True diff --git a/ansible/files/quicknotes b/ansible/files/quicknotes new file mode 100644 index 000000000..8cb022abd Binary files /dev/null and b/ansible/files/quicknotes differ diff --git a/ansible/inventory.ini b/ansible/inventory.ini new file mode 100644 index 000000000..45bc127c6 --- /dev/null +++ b/ansible/inventory.ini @@ -0,0 +1,2 @@ +[quicknotes-vm] +quicknotes-vm ansible_host=127.0.0.1 ansible_port=2222 ansible_user=vagrant ansible_private_key_file=/c/Users/kudin/Desktop/DevOpsCourse/Lab1/DevOps-Intro/DevOps-Intro-new/.vagrant/machines/default/virtualbox/private_key ansible_ssh_extra_args="-o StrictHostKeyChecking=no -o ControlMaster=no -o ControlPersist=no" diff --git a/ansible/playbook.yaml b/ansible/playbook.yaml new file mode 100644 index 000000000..155e397f9 --- /dev/null +++ b/ansible/playbook.yaml @@ -0,0 +1,63 @@ +--- +- name: Deploy QuickNotes to Lab 5 VM + hosts: quicknotes-vm + become: true + gather_facts: false + + vars: + listen_addr: ":9090" + data_path: "/var/lib/quicknotes/data.json" + seed_path: "/var/lib/quicknotes/seed.json" + binary_path: "/usr/local/bin/quicknotes" + data_dir: "/var/lib/quicknotes" + user: "quicknotes" + group: "quicknotes" + + tasks: + - name: Create system user quicknotes + ansible.builtin.user: + name: "{{ user }}" + system: true + shell: /usr/sbin/nologin + create_home: false + state: present + + - name: Ensure data directory exists + ansible.builtin.file: + path: "{{ data_dir }}" + state: directory + owner: "{{ user }}" + group: "{{ group }}" + mode: '0750' + + - name: Copy QuickNotes binary + ansible.builtin.copy: + src: files/quicknotes + dest: "{{ binary_path }}" + mode: '0755' + owner: root + group: root + notify: restart quicknotes + + - name: Render systemd unit file + ansible.builtin.template: + src: templates/quicknotes.service.j2 + dest: /etc/systemd/system/quicknotes.service + mode: '0644' + owner: root + group: root + notify: restart quicknotes + + - name: Reload systemd and start service + ansible.builtin.systemd: + daemon_reload: true + name: quicknotes + state: started + enabled: true + + handlers: + - name: restart quicknotes + ansible.builtin.systemd: + name: quicknotes + state: restarted + daemon_reload: true diff --git a/ansible/templates/quicknotes.service.j2 b/ansible/templates/quicknotes.service.j2 new file mode 100644 index 000000000..5ecc17aaa --- /dev/null +++ b/ansible/templates/quicknotes.service.j2 @@ -0,0 +1,18 @@ +[Unit] +Description=QuickNotes Service +After=network-online.target +Wants=network-online.target + +[Service] +User=quicknotes +Group=quicknotes +WorkingDirectory=/var/lib/quicknotes +ExecStart=/usr/local/bin/quicknotes +Restart=on-failure +RestartSec=5 +Environment=ADDR={{ listen_addr }} +Environment=DATA_PATH={{ data_path }} +Environment=SEED_PATH={{ seed_path }} + +[Install] +WantedBy=multi-user.target diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 000000000..eacad3b81 --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,36 @@ +# ===== СТЕЙДЖ 1: СБОРКА ===== +FROM golang:1.24-alpine AS builder + +WORKDIR /build + +# Кешируем зависимости +COPY go.mod go.sum ./ +RUN go mod download + +# Копируем исходники +COPY . . + +# Собираем статический бинарник +RUN CGO_ENABLED=0 GOOS=linux go build \ + -ldflags='-s -w' \ + -trimpath \ + -o quicknotes . + +# ===== СТЕЙДЖ 2: РАНТАЙМ ===== +FROM gcr.io/distroless/static:nonroot + +WORKDIR /app + +COPY --from=builder /build/quicknotes . + +COPY --from=busybox:stable-musl /bin/busybox /bin/busybox + +# Создаём каталог /data и даём права пользователю 65532 +USER root +RUN ["/bin/busybox", "mkdir", "-p", "/data"] +RUN ["/bin/busybox", "chown", "65532:65532", "/data"] +USER 65532 + +EXPOSE 8080 + +ENTRYPOINT ["/app/quicknotes"] \ No newline at end of file diff --git a/app/go.sum b/app/go.sum new file mode 100644 index 000000000..e69de29bb diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 000000000..87512cc97 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,35 @@ +services: + quicknotes: + build: + context: ./app + dockerfile: Dockerfile + image: quicknotes:lab6 + ports: + - "8080:8080" + volumes: + - quicknotes-data:/data + environment: + - ADDR=:8080 + - DATA_PATH=/data/notes.json + - SEED_PATH=/data/seed.json # если seed.json не нужен, оставь как есть + healthcheck: + test: ["CMD", "/bin/busybox", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + restart: unless-stopped + + # Бонусные параметры безопасности (все 6) + user: "65532:65532" + read_only: true + tmpfs: + - /tmp + - /run + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + +volumes: + quicknotes-data: \ No newline at end of file diff --git a/submissions/PASS.png b/submissions/PASS.png new file mode 100644 index 000000000..f51de156e Binary files /dev/null and b/submissions/PASS.png differ diff --git a/submissions/branch.png b/submissions/branch.png new file mode 100644 index 000000000..4820f11fa Binary files /dev/null and b/submissions/branch.png differ diff --git a/submissions/change6-1.png b/submissions/change6-1.png new file mode 100644 index 000000000..49fd4785c Binary files /dev/null and b/submissions/change6-1.png differ diff --git a/submissions/change6.png b/submissions/change6.png new file mode 100644 index 000000000..49fd4785c Binary files /dev/null and b/submissions/change6.png differ diff --git a/submissions/change_diff-1.png b/submissions/change_diff-1.png new file mode 100644 index 000000000..94ed55b97 Binary files /dev/null and b/submissions/change_diff-1.png differ diff --git a/submissions/change_diff.png b/submissions/change_diff.png new file mode 100644 index 000000000..94ed55b97 Binary files /dev/null and b/submissions/change_diff.png differ diff --git a/submissions/changed0-1.png b/submissions/changed0-1.png new file mode 100644 index 000000000..2c4dd0f6a Binary files /dev/null and b/submissions/changed0-1.png differ diff --git a/submissions/changed0.png b/submissions/changed0.png new file mode 100644 index 000000000..2c4dd0f6a Binary files /dev/null and b/submissions/changed0.png differ diff --git a/submissions/changed2-1.png b/submissions/changed2-1.png new file mode 100644 index 000000000..fc4c26f7b Binary files /dev/null and b/submissions/changed2-1.png differ diff --git a/submissions/changed2.png b/submissions/changed2.png new file mode 100644 index 000000000..fc4c26f7b Binary files /dev/null and b/submissions/changed2.png differ diff --git a/submissions/curlexe.png b/submissions/curlexe.png new file mode 100644 index 000000000..f3019c25e Binary files /dev/null and b/submissions/curlexe.png differ diff --git a/submissions/fail.png b/submissions/fail.png new file mode 100644 index 000000000..4367dad25 Binary files /dev/null and b/submissions/fail.png differ diff --git a/submissions/go_version.png b/submissions/go_version.png new file mode 100644 index 000000000..3399f0874 Binary files /dev/null and b/submissions/go_version.png differ diff --git a/submissions/image1.png b/submissions/image1.png new file mode 100644 index 000000000..ebab70ba4 Binary files /dev/null and b/submissions/image1.png differ diff --git a/submissions/image10.png b/submissions/image10.png new file mode 100644 index 000000000..de685b086 Binary files /dev/null and b/submissions/image10.png differ diff --git a/submissions/image11.png b/submissions/image11.png new file mode 100644 index 000000000..d0c4a812b Binary files /dev/null and b/submissions/image11.png differ diff --git a/submissions/image12.png b/submissions/image12.png new file mode 100644 index 000000000..eea3dd9c9 Binary files /dev/null and b/submissions/image12.png differ diff --git a/submissions/image2.png b/submissions/image2.png new file mode 100644 index 000000000..44b400579 Binary files /dev/null and b/submissions/image2.png differ diff --git a/submissions/image3.png b/submissions/image3.png new file mode 100644 index 000000000..b674b8aa7 Binary files /dev/null and b/submissions/image3.png differ diff --git a/submissions/image4.png b/submissions/image4.png new file mode 100644 index 000000000..f06e763f5 Binary files /dev/null and b/submissions/image4.png differ diff --git a/submissions/image5.png b/submissions/image5.png new file mode 100644 index 000000000..4a471b21d Binary files /dev/null and b/submissions/image5.png differ diff --git a/submissions/image6.png b/submissions/image6.png new file mode 100644 index 000000000..f64d1fe6f Binary files /dev/null and b/submissions/image6.png differ diff --git a/submissions/image7.png b/submissions/image7.png new file mode 100644 index 000000000..7f5558454 Binary files /dev/null and b/submissions/image7.png differ diff --git a/submissions/image8.png b/submissions/image8.png new file mode 100644 index 000000000..49ca7da81 Binary files /dev/null and b/submissions/image8.png differ diff --git a/submissions/image9.png b/submissions/image9.png new file mode 100644 index 000000000..ea4fa4f0c Binary files /dev/null and b/submissions/image9.png differ diff --git a/submissions/image_.png b/submissions/image_.png new file mode 100644 index 000000000..73e08acf8 Binary files /dev/null and b/submissions/image_.png differ diff --git a/submissions/lab3.md b/submissions/lab3.md new file mode 100644 index 000000000..bb2e355db --- /dev/null +++ b/submissions/lab3.md @@ -0,0 +1,63 @@ +# Lab 3 — CI/CD: A PR-Gated Pipeline for QuickNotes + +**Студент:** Руслан Кудинов +**Путь:** GitHub Actions +**Дата:** 17.06.2026 + +## Выбранный путь +Я выбрал GitHub Actions, так как это стандартный инструмент для курса. Из-за проблем с биллингом на основном аккаунте пришлось создать новый (RusKudinov). + +## Ссылка на зелёный CI run +https://github.com/RusKudinov/DevOps-Intro/actions/runs/27708444304 + +## Доказательство работы гейта +- **Сломанный тест:** ![альтернативный текст](fail.png) +- **Исправление:** ![альтернативный текст](PASS.png) +- **Коммит с поломкой:** `3b6b086 ` (можно указать) +- **Коммит с исправлением:** `014941a` + +## Скриншот branch protection +![альтернативный текст](branch.png) +--- + +## Ответы на дизайн-вопросы (Task 1.2) + +### a) Почему пиннить `ubuntu-24.04`, а не `ubuntu-latest`? +`ubuntu-latest` — плавающий тег, который может измениться (например, перейти на 26.04). Это приведёт к непредсказуемым изменениям окружения (версия ядра, библиотеки). Пиннинг фиксирует конкретную версию, делая сборку воспроизводимой. + +### b) Почему разделить vet, test, lint на отдельные джобы? +Если объединить их в одну, при падении lint мы не узнаем, прошли ли тесты. Разделение даёт параллельность, независимый статус каждой проверки и возможность использовать матрицу только для vet и test. + +### c) Какую атаку предотвращает SHA‑пиннинг? (GH path) +В марте 2025 года был скомпрометирован экшен `tj-actions/changed-files`. Злоумышленник внёс вредоносный код в тег `v4`. Все, кто использовал `@v4`, автоматически подхватили его. Пиннинг по SHA фиксирует конкретный коммит, который мы проверили, и защищает от таких supply‑chain‑атак. + +### d) Что такое `permissions:` и какой принцип? +`permissions:` определяет уровень доступа токена GITHUB_TOKEN в workflow. Мы устанавливаем `contents: read` — только чтение кода. Это принцип наименьших привилегий: даём минимум прав, необходимых для работы. Снижает ущерб при компрометации. + +--- + +## Ответы на вопросы Task 2 + +### f) Почему кэшировать по `go.sum`, а не по build‑output? +`go.sum` содержит хеши зависимостей — это надёжный идентификатор набора модулей. Build‑output зависит от архитектуры, версии Go, флагов сборки и может меняться без изменения кода. Кэширование по `go.sum` гарантирует, что при одинаковых входных данных кэш подходит. + +### g) Что делает `fail-fast: false` и когда нужен `true`? +`fail-fast: false` в матрице позволяет продолжать выполнение остальных комбинаций, даже если одна упала. Так мы видим все ошибки (например, тесты падают только на Go 1.24). `fail-fast: true` (по умолчанию) останавливает всё при первом падении — ускоряет фидбек, если ошибка, скорее всего, общая. + +### h) Какой риск кэша, созданного вредоносным PR? +Злоумышленник может попытаться записать кэш с вредоносными зависимостями. Однако GitHub не позволяет PR из форка читать кэш основной ветки и не даёт записывать кэш, который будет использован в `main`. Кэш привязан к ветке и SHA. Это задокументировано в официальной документации GitHub. + +--- + +## Таблица времени (Task 2.4) +![alt text](test1.png) +![alt text](test2.png) +![alt text](test3.png) +| Сценарий | Wall‑clock (сек) | +|----------|------------------| +| Без кэша, одна версия Go, без path‑фильтра | 81 | +| С кэшем (один Go) | 70 | +| С кэшем + матрица (две версии) | 80 | + +**Замечание:** В QuickNotes нет внешних зависимостей, поэтому кэш почти не даёт выигрыша. Основное время уходит на установку Go и старт раннера. В реальном проекте с сотнями модулей выгода была бы значительной. + diff --git a/submissions/lab5.md b/submissions/lab5.md new file mode 100644 index 000000000..44115ab31 --- /dev/null +++ b/submissions/lab5.md @@ -0,0 +1,186 @@ +# Lab 5 — Virtualization: QuickNotes in a Vagrant VM + +## Выполнил: Ruslan Kudinov +## Дата: 24.06.2026 + +--- + +## 1. Vagrantfile (файл в корне репозитория) + +```ruby +# -*- mode: ruby -*- +# vi: set ft=ruby : + +Vagrant.configure("2") do |config| + config.vm.box = "ubuntu/jammy64" + config.vm.hostname = "quicknotes-vm" + config.vm.network "forwarded_port", guest: 8080, host: 18080, host_ip: "127.0.0.1" + config.vm.synced_folder "./app", "/home/vagrant/app" + + config.vm.provider "virtualbox" do |vb| + vb.memory = "1024" + vb.cpus = 2 + end + + config.vm.provision "shell", inline: <<-SHELL + apt-get update + apt-get install -y wget git + wget -q https://go.dev/dl/go1.24.5.linux-amd64.tar.gz + tar -C /usr/local -xzf go1.24.5.linux-amd64.tar.gz + echo 'export PATH=$PATH:/usr/local/go/bin' >> /home/vagrant/.bashrc + echo 'export PATH=$PATH:/usr/local/go/bin' >> /root/.bashrc + export PATH=$PATH:/usr/local/go/bin + go version + SHELL +end +``` + +--- + +## 2. Выводы команд + +### 2.1 Первые строки `vagrant up` + + +![alt text](var_up.png) + +``` +==> default: Box 'ubuntu/jammy64' could not be found. Attempting to find and install... + default: Box Provider: virtualbox + default: Box Version: >= 0 +==> default: Loading metadata for box 'ubuntu/jammy64' + default: URL: https://vagrantcloud.com/api/v2/vagrant/ubuntu/jammy64 +==> default: Adding box 'ubuntu/jammy64' (v20241002.0.0) for provider: virtualbox + default: Downloading: https://vagrantcloud.com/ubuntu/boxes/jammy64/versions/20241002.0.0/providers/virtualbox/unknown/vagrant.box +==> default: Successfully added box 'ubuntu/jammy64' (v20241002.0.0) for 'virtualbox'! +==> default: Importing base box 'ubuntu/jammy64'... +==> default: Matching MAC address for NAT networking... +==> default: Checking if box 'ubuntu/jammy64' version '20241002.0.0' is up to date... +==> default: Setting the name of the VM: DevOps-Intro-new_default_1782329346451_41824 +``` + +### 2.2 Проверка установки Go внутри VM (после восстановления) + + +![alt text](go_version.png) +**Команда:** +```bash +vagrant ssh -c "/usr/local/go/bin/go version" +``` +**Вывод:** +``` +go version go1.24.5 linux/amd64 +``` + +### 2.3 Запуск QuickNotes и проверка с хоста + +![alt text](nohup.png) + + +![alt text](curlexe.png) + +**Запуск внутри VM:** +```bash +vagrant ssh -c "cd /home/vagrant/app && nohup ./quicknotes > /tmp/qn.log 2>&1 &" +``` + +**Проверка с хоста:** +```bash +curl.exe http://localhost:18080/health +``` +**Вывод:** +```json +{"notes":4,"status":"ok"} +``` + +--- + +## 3. Task 2 — Snapshots + +### 3.1 Команды и выводы + + + +![![alt text](image.png)](snap1.png) + +![alt text](snap2.png) + +![alt text](snap3.png) + +```powershell +# Сохраняем снэпшот +vagrant snapshot save working +# Вывод: Snapshot saved! + +# Ломаем VM (удаляем Go) +vagrant ssh -c "sudo rm -rf /usr/local/go" + +# Проверяем, что сломали +vagrant ssh -c "go version" +# Вывод: bash: line 1: go: command not found + +# Восстанавливаем и замеряем время +Measure-Command { vagrant snapshot restore working } +# Вывод: TotalSeconds : 26.7161966 + +# Проверяем восстановление (через полный путь) +vagrant ssh -c "/usr/local/go/bin/go version" +# Вывод: go version go1.24.5 linux/amd64 +``` + +### 3.2 Результаты + +- **Команда для поломки:** `vagrant ssh -c "sudo rm -rf /usr/local/go"` +- **Результат проверки поломки:** `bash: line 1: go: command not found` +- **Время восстановления:** `26.716 секунд` (из `Measure-Command`) +- **Результат проверки после восстановления:** `go version go1.24.5 linux/amd64` + +--- + +## 4. Ответы на вопросы + +### a) Synced folders: какой тип и почему? + +Я использовал тип `virtualbox` (по умолчанию). Он прост в настройке и не требует дополнительных служб (как NFS). Недостаток — производительность может быть ниже, но для учебного проекта это некритично. + +### b) NAT vs Bridged vs Host-only: какая сеть и почему `127.0.0.1` безопаснее? + +Используется NAT (сеть по умолчанию). Проброс порта на `127.0.0.1` делает сервис доступным только с локальной машины, а не из всей сети. Это безопаснее для курсовой работы, т.к. никто извне не сможет подключиться к VM. + +### c) Какой провайдер для provisioning и почему? + +Использован `shell`-провайдер, потому что он самый простой и не требует дополнительных инструментов (Ansible, Chef и т.д.). Достаточно одной команды для установки Go. + +### d) Почему pin Go к конкретной версии (1.24.5), а не `1.24`? + +Фиксация точной версии гарантирует, что все студенты получат одинаковое окружение, что исключает ошибки из-за различий в минорных версиях. Также это упрощает воспроизводимость. + +### e) Почему снэпшоты — не бэкапы? + +Снэпшот хранит состояние виртуальной машины в момент времени, но не защищает от потери данных, если диск VM повреждён. Также снэпшот не может быть восстановлен на другом хосте. Бэкап — это копия данных, которую можно перенести и восстановить независимо от VM. + +### f) Copy-on-write: что это значит для дискового пространства при 10 снэпшотах? + +При Copy-on-Write каждый снэпшот хранит только изменения с момента предыдущего. Поэтому 10 снэпшотов могут занимать не 10× размер VM, а только сумму изменений. Однако они всё равно потребляют место, и при длинной цепочке производительность может упасть. + +### g) Когда снэпшоты — антипаттерн? + +Снэпшоты становятся антипаттерном при длинных цепочках (например, >5), так как они замедляют работу VM, занимают много места и усложняют управление. Их стоит использовать только для кратковременных экспериментов, а не для постоянного резервирования. + +--- + +## 5. Бонус — VM vs Container Resource Baseline + +**Бонус не выполнялся** из-за нехватки времени и конфликта Hyper-V с VirtualBox. + +--- + +## Заключение + +Все требования Lab 5 выполнены: +- Vagrantfile создан, VM поднимается с Go 1.24.5. +- Порт 18080 проброшен, QuickNotes доступен с хоста. +- Снэпшоты созданы, поломка и восстановление подтверждены. + +--- + diff --git a/submissions/lab6.md b/submissions/lab6.md new file mode 100644 index 000000000..67d6297bc --- /dev/null +++ b/submissions/lab6.md @@ -0,0 +1,332 @@ +# Lab 6 — Containers: Dockerize QuickNotes + +## Выполнил: Кудинов Рулсан +## Дата: 24.06.2026 + +--- + +## 1. Dockerfile (файл `app/Dockerfile`) + +```dockerfile +# ===== СТЕЙДЖ 1: СБОРКА ===== +FROM golang:1.24-alpine AS builder + +WORKDIR /build + +# Кешируем зависимости +COPY go.mod go.sum ./ +RUN go mod download + +# Копируем исходники +COPY . . + +# Собираем статический бинарник +RUN CGO_ENABLED=0 GOOS=linux go build \ + -ldflags='-s -w' \ + -trimpath \ + -o quicknotes . + +# ===== СТЕЙДЖ 2: РАНТАЙМ ===== +FROM gcr.io/distroless/static:nonroot + +WORKDIR /app + +COPY --from=builder /build/quicknotes . + +COPY --from=busybox:stable-musl /bin/busybox /bin/busybox + +# Создаём каталог /data с правами nonroot +USER root +RUN ["/bin/busybox", "mkdir", "-p", "/data"] +RUN ["/bin/busybox", "chown", "65532:65532", "/data"] +USER 65532 + +EXPOSE 8080 + +ENTRYPOINT ["/app/quicknotes"] +``` + +--- + +## 2. Compose-файл (`compose.yaml`) + +```yaml +services: + quicknotes: + build: + context: ./app + dockerfile: Dockerfile + image: quicknotes:lab6 + ports: + - "8080:8080" + volumes: + - quicknotes-data:/data + environment: + - ADDR=:8080 + - DATA_PATH=/data/notes.json + - SEED_PATH=/data/seed.json + healthcheck: + test: ["CMD", "/bin/busybox", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + restart: unless-stopped + + # Security hardening (6 defaults) + user: "65532:65532" + read_only: true + tmpfs: + - /tmp + - /run + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + +volumes: + quicknotes-data: +``` + +--- + +## 3. Проверка размера образа + +**Команда:** +```powershell +docker images quicknotes:lab6 +``` + +**Скриншот 1** — ![](image1.png) + +**Вывод (текстовый):** +``` +IMAGE ID DISK USAGE CONTENT SIZE EXTRA +quicknotes:lab6 9dd77b1e8c59 16.8MB 4.09MB U +``` +Размер: **16.8 МБ** — условие выполнено. + +--- + +## 4. Проверка работы приложения (эндпоинт /health) + +**Команда:** +```powershell +curl.exe http://localhost:8080/health +``` + +**Скриншот 2** ![](image2.png) + +**Вывод:** +```json +{"notes":0,"status":"ok"} +``` + +--- + +## 5. Тест сохранения данных (persistence) + +### 5.1 Создание заметки и проверка наличия + +**Команда:** +```powershell +$body = '{"title":"durable","body":"survive a restart"}' +Invoke-WebRequest -Uri http://localhost:8080/notes -Method POST -Body $body -ContentType "application/json" +``` + +**Скриншот 3** — ![скриншот с ответом 201 Created и содержимым заметки.](image3.png) + +**Команда проверки:** +```powershell +curl.exe -s http://localhost:8080/notes +``` +**Скриншот 4** — ![скриншот с выводом JSON-массива, содержащего созданную заметку.](image4.png) + +**Вывод:** +```json +[{"id":1,"title":"durable","body":"survive a restart","created_at":"2026-06-24T17:34:50.077826506Z"}] +``` + +### 5.2 Перезапуск контейнера (том сохраняется) + +```powershell +docker compose down +docker compose up -d +curl.exe -s http://localhost:8080/notes +``` + +**Скриншот 5** — ![скриншот, где после `docker compose up -d` команда `curl` снова показывает ту же заметку.](image5.png) + +**Вывод:** +```json +[{"id":1,"title":"durable","body":"survive a restart","created_at":"2026-06-24T17:34:50.077826506Z"}] +``` + +### 5.3 Удаление тома и проверка + +```powershell +docker compose down -v +docker compose up -d +curl.exe -s http://localhost:8080/notes +``` + +**Скриншот 6** — ![скриншот, где после `docker compose down -v` и повторного подъёма команда `curl` возвращает `\[\]`.](image6.png) + +**Вывод:** +```json +[] +``` + +**Вывод:** данные сохраняются после перезапуска и исчезают только при удалении тома — persistence работает. + +--- + +## 6. Проверка security-настроек (бонус) + +Все команды выполнялись из корня проекта. + +### 6.1 Пользователь nonroot + +**Команда:** +```powershell +docker inspect quicknotes:lab6 --format '{{ .Config.User }}' +``` + +**Скриншот 7** — ![вывод `65532`](image7.png) + +**Вывод:** +``` +65532 +``` + +### 6.2 Отсутствие шелла + +**Команда:** +```powershell +docker compose exec quicknotes sh +``` + +**Скриншот 8** — ![ошибка `exec: "sh": executable file not found in $PATH`.](image8.png) + +**Вывод:** +``` +OCI runtime exec failed: exec failed: unable to start container process: exec: "sh": executable file not found in $PATH +``` + +### 6.3 Сброс capabilities + +**Команда:** +```powershell +docker inspect $(docker compose ps -q quicknotes) --format '{{ .HostConfig.CapDrop }}' +``` + +**Скриншот 9** — ![вывод `\[ALL\]`.](image9.png) + +**Вывод:** +``` +[ALL] +``` + +### 6.4 Read-only root + +**Команда:** +```powershell +docker compose exec quicknotes /bin/busybox touch /etc/test 2>&1 +``` + +**Скриншот 10** — ![ошибка `touch: /etc/test: Read-only file system`.](image10.png) + +**Вывод:** +``` +touch: /etc/test: Read-only file system +``` + +### 6.5 no-new-privileges + +**Команда:** +```powershell +docker inspect $(docker compose ps -q quicknotes) --format '{{ .HostConfig.SecurityOpt }}' +``` + +**Скриншот 11** — ![вывод `\[no-new-privileges:true\]`.](image11.png) + +**Вывод:** +``` +[no-new-privileges:true] +``` + +### 6.6 Trivy сканирование + +**Команда:** +```powershell +docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy:0.59.1 image --severity HIGH,CRITICAL --no-progress quicknotes:lab6 +``` + +**Скриншот 12** — ![полный вывод Trivy (или хотя бы итоговая таблица).](image12.png) +![alt text](image.png) +**Сокращённый вывод:** +``` +quicknotes:lab6 (debian 13.5) +============================= +Total: 0 (HIGH: 0, CRITICAL: 0) + +app/quicknotes (gobinary) +========================= +Total: 13 (HIGH: 13, CRITICAL: 0) +... +``` +На уровне ОС уязвимостей нет, в Go-библиотеке stdlib обнаружены 13 HIGH, но они исправлены в более новых версиях Go. + +--- + +## 7. Ответы на вопросы + +### a) Почему порядок слоёв важен? + +Порядок инструкций в Dockerfile влияет на использование кеша слоёв. Сначала копируются только `go.mod` и `go.sum`, затем выполняется `go mod download`. Этот слой пересобирается только при изменении зависимостей. Если сначала скопировать весь код, то любое изменение в исходниках приведёт к повторной загрузке всех зависимостей, что замедляет сборку. + +### b) Зачем `CGO_ENABLED=0`? + +Отключает использование C-кода и динамическую линковку. Бинарник становится полностью статическим и не требует внешних библиотек. В образе `distroless/static` нет динамического линковщика, поэтому без этого флага запуск невозможен (ошибка `no such file or directory`). + +### c) Что такое `gcr.io/distroless/static:nonroot`? + +Это минимальный образ от Google, содержащий только статически скомпилированный бинарник и минимальные системные файлы (например, `ca-certificates`, `timezone`). В нём нет оболочки, пакетного менеджера, утилит. Это снижает поверхность атак и количество уязвимостей. + +### d) `-ldflags='-s -w'` и `-trimpath` + +- `-s` — удаляет таблицу символов. +- `-w` — удаляет отладочную информацию (DWARF). +Эти флаги уменьшают размер бинарника. +- `-trimpath` — убирает абсолютные пути к исходникам, делая сборку воспроизводимой. +Цена — потеря отладочных символов, что затрудняет отладку в продакшене, но для финального образа это приемлемо. + +### e) Как сделать healthcheck без шелла? + +Мы скопировали статический `busybox` в образ и используем его команду `wget` для проверки `/health`. Поскольку `busybox` собран как статический бинарник, он работает без динамических библиотек и не требует оболочки. + +### f) Почему том сохраняется после `docker compose down`? + +Именованный том (`quicknotes-data`) управляется Docker отдельно от контейнера. Команда `docker compose down` без флага `-v` удаляет только контейнеры и сеть, но не тома. При следующем `up` том подключается с теми же данными. + +### g) `depends_on` без `condition: service_healthy` + +`depends_on` без условия `service_healthy` дожидается только запуска контейнера (статус `running`), но не проверяет, что сервис внутри готов принимать запросы. Это может привести к ошибкам, если зависимый сервис ещё не инициализировался. + +--- + +## 8. Бонус: какая из 6 мер даёт больше всего безопасности на строчку YAML? + +Самый большой эффект дают `cap_drop: ALL` и `read_only: true`. Они кардинально ограничивают возможности контейнера даже в случае взлома приложения, практически не увеличивая сложность конфигурации. + +--- + +## Заключение + +Все требования Lab 6 выполнены: +- Многостадийный Dockerfile собран, размер образа ≤ 25 МБ. +- Compose-файл с томом, healthcheck и security-параметрами работает. +- Persistence подтверждена. +- Все 6 security defaults применены и верифицированы. +- Trivy запущен, результаты задокументированы. + diff --git a/submissions/lab7.md b/submissions/lab7.md new file mode 100644 index 000000000..5f073c627 --- /dev/null +++ b/submissions/lab7.md @@ -0,0 +1,376 @@ +# Lab 7 — Configuration Management: Deploy QuickNotes via Ansible + +## Выполнил: [Твоё имя] +## Дата: 01.07.2026 + +--- + +## 1. Файлы Ansible + +### 1.1 `ansible/inventory.ini` + +```ini +[quicknotes-vm] +quicknotes-vm ansible_host=127.0.0.1 ansible_port=2222 ansible_user=vagrant ansible_private_key_file=/c/Users/kudin/Desktop/DevOpsCourse/Lab1/DevOps-Intro/DevOps-Intro-new/.vagrant/machines/default/virtualbox/private_key ansible_ssh_extra_args="-o StrictHostKeyChecking=no -o ControlMaster=no -o ControlPersist=no" +``` + +### 1.2 `ansible/playbook.yaml` + +```yaml +--- +- name: Deploy QuickNotes to Lab 5 VM + hosts: quicknotes-vm + become: true + gather_facts: false + + vars: + listen_addr: ":8080" + data_path: "/var/lib/quicknotes/notes.json" + seed_path: "/var/lib/quicknotes/seed.json" + binary_path: "/usr/local/bin/quicknotes" + data_dir: "/var/lib/quicknotes" + user: "quicknotes" + group: "quicknotes" + + tasks: + - name: Create system user quicknotes + ansible.builtin.user: + name: "{{ user }}" + system: true + shell: /usr/sbin/nologin + create_home: false + state: present + + - name: Ensure data directory exists + ansible.builtin.file: + path: "{{ data_dir }}" + state: directory + owner: "{{ user }}" + group: "{{ group }}" + mode: '0750' + + - name: Copy QuickNotes binary + ansible.builtin.copy: + src: files/quicknotes + dest: "{{ binary_path }}" + mode: '0755' + owner: root + group: root + notify: restart quicknotes + + - name: Render systemd unit file + ansible.builtin.template: + src: templates/quicknotes.service.j2 + dest: /etc/systemd/system/quicknotes.service + mode: '0644' + owner: root + group: root + notify: restart quicknotes + + - name: Reload systemd and start service + ansible.builtin.systemd: + daemon_reload: true + name: quicknotes + state: started + enabled: true + + handlers: + - name: restart quicknotes + ansible.builtin.systemd: + name: quicknotes + state: restarted + daemon_reload: true +``` + +### 1.3 `ansible/templates/quicknotes.service.j2` + +```jinja +[Unit] +Description=QuickNotes Service +After=network-online.target +Wants=network-online.target + +[Service] +User=quicknotes +Group=quicknotes +WorkingDirectory=/var/lib/quicknotes +ExecStart=/usr/local/bin/quicknotes +Restart=on-failure +RestartSec=5 +Environment=ADDR={{ listen_addr }} +Environment=DATA_PATH={{ data_path }} +Environment=SEED_PATH={{ seed_path }} + +[Install] +WantedBy=multi-user.target +``` + +--- + +## 2. Выводы команд + +### 2.1 Первый запуск плейбука (развёртывание) + +**Команда:** +```bash +ansible-playbook -i ansible/inventory.ini ansible/playbook.yaml +``` + +**Вывод:** +``` +PLAY [Deploy QuickNotes to Lab 5 VM] *************************************************************** + +TASK [Create system user quicknotes] *************************************************************** +changed: [quicknotes-vm] + +TASK [Ensure data directory exists] **************************************************************** +changed: [quicknotes-vm] + +TASK [Copy QuickNotes binary] ********************************************************************** +changed: [quicknotes-vm] + +TASK [Render systemd unit file] ******************************************************************** +changed: [quicknotes-vm] + +TASK [Reload systemd and start service] ************************************************************ +changed: [quicknotes-vm] + +RUNNING HANDLER [restart quicknotes] *************************************************************** +changed: [quicknotes-vm] + +PLAY RECAP ***************************************************************************************** +quicknotes-vm : ok=6 changed=6 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +![alt text](change6-1.png) +--- + +### 2.2 Второй запуск (без изменений) — доказательство идемпотентности + +**Команда:** +```bash +ansible-playbook -i ansible/inventory.ini ansible/playbook.yaml +``` + +**Вывод:** +``` +PLAY [Deploy QuickNotes to Lab 5 VM] *************************************************************** + +TASK [Create system user quicknotes] *************************************************************** +ok: [quicknotes-vm] + +TASK [Ensure data directory exists] **************************************************************** +ok: [quicknotes-vm] + +TASK [Copy QuickNotes binary] ********************************************************************** +ok: [quicknotes-vm] + +TASK [Render systemd unit file] ******************************************************************** +ok: [quicknotes-vm] + +TASK [Reload systemd and start service] ************************************************************ +ok: [quicknotes-vm] + +PLAY RECAP ***************************************************************************************** +quicknotes-vm : ok=5 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + + +![alt text](changed0-1.png) +--- + +### 2.3 Изменение переменной (порт с 8080 на 9090) — selective change + +**Изменение в `playbook.yaml`:** +```yaml +listen_addr: ":9090" +``` + +**Команда:** +```bash +ansible-playbook -i ansible/inventory.ini ansible/playbook.yaml +``` + +**Вывод:** +``` +PLAY [Deploy QuickNotes to Lab 5 VM] *************************************************************** + +TASK [Create system user quicknotes] *************************************************************** +ok: [quicknotes-vm] + +TASK [Ensure data directory exists] **************************************************************** +ok: [quicknotes-vm] + +TASK [Copy QuickNotes binary] ********************************************************************** +ok: [quicknotes-vm] + +TASK [Render systemd unit file] ******************************************************************** +changed: [quicknotes-vm] + +TASK [Reload systemd and start service] ************************************************************ +ok: [quicknotes-vm] + +RUNNING HANDLER [restart quicknotes] *************************************************************** +changed: [quicknotes-vm] + +PLAY RECAP ***************************************************************************************** +quicknotes-vm : ok=6 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + + +![alt text](changed2-1.png) +--- + +### 2.4 Проверка `--check --diff` + +**Изменение в `playbook.yaml`:** +```yaml +data_path: "/var/lib/quicknotes/data.json" +``` + +**Команда:** +```bash +ansible-playbook -i ansible/inventory.ini ansible/playbook.yaml --check --diff +``` + +**Вывод (diff):** +``` +TASK [Render systemd unit file] ******************************************************************** +--- before: /etc/systemd/system/quicknotes.service ++++ after: /home/kudin/.ansible/tmp/ansible-local-2086739ykota/tmpt5vxnobu/quicknotes.service.j2 +@@ -11,7 +11,7 @@ + Restart=on-failure + RestartSec=5 + Environment=ADDR=:9090 +-Environment=DATA_PATH=/var/lib/quicknotes/notes.json ++Environment=DATA_PATH=/var/lib/quicknotes/data.json + Environment=SEED_PATH=/var/lib/quicknotes/seed.json + + [Install] +``` + + +![alt text](change_diff-1.png) +--- + +### 2.5 Проверка работоспособности QuickNotes + +**Команда (с хоста):** +```powershell +curl.exe http://localhost:18080/health +``` + +**Вывод:** +```json +{"notes":0,"status":"ok"} +``` + + +![alt text](status_ok-1.png) +--- + +## 3. Ответы на вопросы + +### a) В чём разница между `command:` и модулями (`apt`, `file`, `copy`, `systemd`)? Какой из них идемпотентный и почему? + +`command:` и `shell:` всегда выполняют команду, независимо от состояния системы. Они не проверяют, нужно ли что-то менять — просто запускают указанную команду каждый раз. + +Модули (`file`, `copy`, `template`, `systemd`) являются идемпотентными — они проверяют текущее состояние системы и применяют изменения только если оно не соответствует желаемому. Это делает повторные запуски безопасными и предсказуемыми. + +Идемпотентность важна для автоматизации, потому что позволяет перезапускать плейбуки без риска сломать систему или выполнить лишние действия. + +--- + +### b) `notify:` и handlers: когда handler срабатывает, а когда нет? + +Handler срабатывает **в конце выполнения всех задач**, если хотя бы одна задача его «нотифицировала» и при этом **действительно изменила** состояние системы (т.е. задача завершилась со статусом `changed`). + +Handler **не срабатывает**, если: +- Задача не была изменена (`ok`). +- Задача не нотифицировала handler. +- Handler был нотифицирован, но задача завершилась с ошибкой. + +Это правильное поведение по умолчанию, потому что перезапуск сервиса нужен только когда что-то действительно изменилось (бинарник или конфиг), а не при каждом запуске плейбука. + +--- + +### c) Переменные в Ansible: какие места для их задания использованы в этой лабе? + +В этой лабе переменные заданы в трёх местах: + +1. **В плейбуке** (секция `vars:`) — это самое простое и наглядное место для задания переменных, относящихся к конкретному плейбуку. +2. **В инвентаре** (`inventory.ini`) — можно задавать переменные для конкретных хостов или групп, но в этой лабе инвентарь содержит только параметры подключения. +3. **В шаблоне** — переменные из плейбука подставляются в шаблон Jinja2 через `{{ переменная }}`. + +Такой подход обеспечивает гибкость: можно легко переопределить переменные для разных окружений без изменения самого плейбука. + +--- + +### d) Нужно ли `gather_facts: true` для этого плейбука? + +В этом плейбуке мы отключили `gather_facts: false`, потому что он не использует информацию о системе (ОС, архитектура, интерфейсы). Все задачи работают с конкретными путями и пользователями, которые мы задали явно. + +Отключение `gather_facts` ускоряет выполнение плейбука, так как Ansible не тратит время на сбор системной информации. Для простых плейбуков, работающих с известной ОС, это безопасно и эффективно. + +--- + +### e) Почему второй запуск показывает `changed=0`? + +Модули Ansible проверяют текущее состояние системы и сравнивают его с желаемым. Если всё уже соответствует описанию, задачи завершаются со статусом `ok`, а не `changed`. + +Например: +- `user` — проверяет, существует ли пользователь `quicknotes`. +- `file` — проверяет, существует ли папка `/var/lib/quicknotes` и совпадают ли права. +- `copy` — проверяет контрольную сумму файла и его права. +- `template` — сравнивает содержимое файла на VM с шаблоном. + +Если ни одно условие не требует изменений, весь плейбук завершается с `changed=0`. + +--- + +### f) Что было бы при использовании `shell` вместо `template`? + +Если бы вместо `template:` использовался `shell` для записи systemd-юнита, это сломало бы идемпотентность: + +1. **Команда выполнялась бы каждый раз** — даже если содержимое юнита не изменилось. +2. **Не было бы проверки изменений** — нельзя определить, нужно ли перезапускать сервис. +3. **Сложно отслеживать изменения** — нет diff, нет истории. +4. **Ошибки сложнее отлаживать** — shell-скрипты менее читаемы и труднее поддерживаются. + +Использование модулей — это правильный подход, потому что они абстрагируют детали и обеспечивают идемпотентность. + +--- + +### g) Что даёт `--check --diff` и какую ошибку можно поймать? + +`--check` — это сухой прогон (dry-run), который показывает, что бы изменилось, но ничего не применяет. + +`--diff` — показывает конкретные различия в файлах, которые будут изменены. + +Вместе они позволяют: +- Увидеть, какие файлы изменятся и как именно. +- Обнаружить неожиданные изменения (например, случайное изменение порта или пути). +- Проверить, что изменения соответствуют ожиданиям до их применения в продакшене. + +Это профессиональная практика, которая помогает избежать ошибок и неожиданных последствий при деплое. + +--- + +## 4. Бонус — `ansible-pull` GitOps Loop + +**Бонус не выполнялся** из-за ограничений по времени и сложностей с настройкой WSL на Windows. + +--- + +## 5. Заключение + +Все требования Lab 7 выполнены: +- Ansible плейбук разворачивает QuickNotes на VM из Lab 5. +- Сервис запущен и доступен через порт 18080. +- Идемпотентность доказана (второй запуск — `changed=0`). +- Selective change показан (изменение порта → `changed=2`, только template и handler). +- `--check --diff` показан для другого изменения. +- Ответы на все вопросы даны. + +--- + diff --git a/submissions/nohup.png b/submissions/nohup.png new file mode 100644 index 000000000..7fd2e503b Binary files /dev/null and b/submissions/nohup.png differ diff --git a/submissions/snap1.png b/submissions/snap1.png new file mode 100644 index 000000000..b5592b456 Binary files /dev/null and b/submissions/snap1.png differ diff --git a/submissions/snap2.png b/submissions/snap2.png new file mode 100644 index 000000000..ccc1284b8 Binary files /dev/null and b/submissions/snap2.png differ diff --git a/submissions/snap3.png b/submissions/snap3.png new file mode 100644 index 000000000..f8f1af67e Binary files /dev/null and b/submissions/snap3.png differ diff --git a/submissions/status_ok-1.png b/submissions/status_ok-1.png new file mode 100644 index 000000000..d23abb071 Binary files /dev/null and b/submissions/status_ok-1.png differ diff --git a/submissions/status_ok.png b/submissions/status_ok.png new file mode 100644 index 000000000..d23abb071 Binary files /dev/null and b/submissions/status_ok.png differ diff --git a/submissions/test1.png b/submissions/test1.png new file mode 100644 index 000000000..469c86d28 Binary files /dev/null and b/submissions/test1.png differ diff --git a/submissions/test2.png b/submissions/test2.png new file mode 100644 index 000000000..dd3986d60 Binary files /dev/null and b/submissions/test2.png differ diff --git a/submissions/test3.png b/submissions/test3.png new file mode 100644 index 000000000..f384f8687 Binary files /dev/null and b/submissions/test3.png differ diff --git a/submissions/var_up.png b/submissions/var_up.png new file mode 100644 index 000000000..156e91f51 Binary files /dev/null and b/submissions/var_up.png differ