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/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/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/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/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