diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..e097e0bd1 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,12 @@ +## Goal +Finish Lab 1 by setting up SSH commit signing and a pull request template. + +## Changes +- Enabled SSH-based commit signing +- Added lab report and screenshots +- Introduced a standard PR template + +## Testing +- [ ] Project builds and runs successfully +- [ ] Commits are signed and verified +- [ ] Documentation is complete and updated diff --git a/.github/workflows/lab3.yml b/.github/workflows/lab3.yml new file mode 100644 index 000000000..bb315133c --- /dev/null +++ b/.github/workflows/lab3.yml @@ -0,0 +1,49 @@ +name: Lab 3 CI + +on: + push: + branches: + - feature/lab3 + workflow_dispatch: + +jobs: + basic-info: + runs-on: ubuntu-latest + steps: + - name: Print basic info + run: | + echo "Hello from GitHub Actions!" + date + echo "Actor: $GITHUB_ACTOR" + echo "Ref: $GITHUB_REF" + echo "SHA: $GITHUB_SHA" + uname -a + + - name: System information + run: | + echo "=== SYSTEM INFO ===" + echo "--- OS / Kernel ---" + uname -a + cat /etc/os-release || true + + echo "--- CPU ---" + nproc + lscpu || true + cat /proc/cpuinfo | head -n 30 || true + + echo "--- Memory ---" + free -h || true + cat /proc/meminfo | head -n 20 || true + + echo "--- Disk ---" + df -h + lsblk || true + + echo "--- GitHub runner env ---" + echo "RUNNER_OS=$RUNNER_OS" + echo "RUNNER_ARCH=$RUNNER_ARCH" + echo "RUNNER_NAME=$RUNNER_NAME" + echo "RUNNER_TEMP=$RUNNER_TEMP" + echo "GITHUB_WORKFLOW=$GITHUB_WORKFLOW" + echo "GITHUB_RUN_ID=$GITHUB_RUN_ID" + echo "GITHUB_RUN_NUMBER=$GITHUB_RUN_NUMBER" diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..a38dbc03f --- /dev/null +++ b/.gitignore @@ -0,0 +1,164 @@ +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + diff --git a/demo.txt b/demo.txt new file mode 100644 index 000000000..df967b96a --- /dev/null +++ b/demo.txt @@ -0,0 +1 @@ +base diff --git a/labs/screenshots/img.png b/labs/screenshots/img.png new file mode 100644 index 000000000..b508633ac Binary files /dev/null and b/labs/screenshots/img.png differ diff --git a/labs/screenshots/img_1.png b/labs/screenshots/img_1.png new file mode 100644 index 000000000..2ad1069bd Binary files /dev/null and b/labs/screenshots/img_1.png differ diff --git a/labs/screenshots/img_10.png b/labs/screenshots/img_10.png new file mode 100644 index 000000000..97bc7f5d3 Binary files /dev/null and b/labs/screenshots/img_10.png differ diff --git a/labs/screenshots/img_11.png b/labs/screenshots/img_11.png new file mode 100644 index 000000000..803214196 Binary files /dev/null and b/labs/screenshots/img_11.png differ diff --git a/labs/screenshots/img_12.png b/labs/screenshots/img_12.png new file mode 100644 index 000000000..7d765e0d2 Binary files /dev/null and b/labs/screenshots/img_12.png differ diff --git a/labs/screenshots/img_13.png b/labs/screenshots/img_13.png new file mode 100644 index 000000000..843cd0154 Binary files /dev/null and b/labs/screenshots/img_13.png differ diff --git a/labs/screenshots/img_14.png b/labs/screenshots/img_14.png new file mode 100644 index 000000000..63d303a37 Binary files /dev/null and b/labs/screenshots/img_14.png differ diff --git a/labs/screenshots/img_15.png b/labs/screenshots/img_15.png new file mode 100644 index 000000000..16821f045 Binary files /dev/null and b/labs/screenshots/img_15.png differ diff --git a/labs/screenshots/img_16.png b/labs/screenshots/img_16.png new file mode 100644 index 000000000..d522b0339 Binary files /dev/null and b/labs/screenshots/img_16.png differ diff --git a/labs/screenshots/img_2.png b/labs/screenshots/img_2.png new file mode 100644 index 000000000..64379d14b Binary files /dev/null and b/labs/screenshots/img_2.png differ diff --git a/labs/screenshots/img_3.png b/labs/screenshots/img_3.png new file mode 100644 index 000000000..76f6d40e3 Binary files /dev/null and b/labs/screenshots/img_3.png differ diff --git a/labs/screenshots/img_4.png b/labs/screenshots/img_4.png new file mode 100644 index 000000000..c3040b7a1 Binary files /dev/null and b/labs/screenshots/img_4.png differ diff --git a/labs/screenshots/img_5.png b/labs/screenshots/img_5.png new file mode 100644 index 000000000..50c9b496b Binary files /dev/null and b/labs/screenshots/img_5.png differ diff --git a/labs/screenshots/img_6.png b/labs/screenshots/img_6.png new file mode 100644 index 000000000..b0f099dc0 Binary files /dev/null and b/labs/screenshots/img_6.png differ diff --git a/labs/screenshots/img_7.png b/labs/screenshots/img_7.png new file mode 100644 index 000000000..c1394480c Binary files /dev/null and b/labs/screenshots/img_7.png differ diff --git a/labs/screenshots/img_8.png b/labs/screenshots/img_8.png new file mode 100644 index 000000000..b254d366a Binary files /dev/null and b/labs/screenshots/img_8.png differ diff --git a/labs/screenshots/img_9.png b/labs/screenshots/img_9.png new file mode 100644 index 000000000..a25bf544c Binary files /dev/null and b/labs/screenshots/img_9.png differ diff --git a/labs/screenshots/lab_8_new/alert_settings.png b/labs/screenshots/lab_8_new/alert_settings.png new file mode 100644 index 000000000..9b5685e66 Binary files /dev/null and b/labs/screenshots/lab_8_new/alert_settings.png differ diff --git a/labs/screenshots/lab_8_new/api_check_config.png b/labs/screenshots/lab_8_new/api_check_config.png new file mode 100644 index 000000000..b5068881c Binary files /dev/null and b/labs/screenshots/lab_8_new/api_check_config.png differ diff --git a/labs/screenshots/lab_8_new/browser_check_config.png b/labs/screenshots/lab_8_new/browser_check_config.png new file mode 100644 index 000000000..d6fe695c4 Binary files /dev/null and b/labs/screenshots/lab_8_new/browser_check_config.png differ diff --git a/labs/screenshots/lab_8_new/browser_check_result.png b/labs/screenshots/lab_8_new/browser_check_result.png new file mode 100644 index 000000000..0ded5df3e Binary files /dev/null and b/labs/screenshots/lab_8_new/browser_check_result.png differ diff --git a/labs/screenshots/lab_8_new/dashboard_overview.png b/labs/screenshots/lab_8_new/dashboard_overview.png new file mode 100644 index 000000000..f62718c9b Binary files /dev/null and b/labs/screenshots/lab_8_new/dashboard_overview.png differ diff --git a/labs/screenshots/lab_9_new/trivy_critical_findings.png b/labs/screenshots/lab_9_new/trivy_critical_findings.png new file mode 100644 index 000000000..351ee1082 Binary files /dev/null and b/labs/screenshots/lab_9_new/trivy_critical_findings.png differ diff --git a/labs/screenshots/lab_9_new/zap_medium_alerts.png b/labs/screenshots/lab_9_new/zap_medium_alerts.png new file mode 100644 index 000000000..fb4717a65 Binary files /dev/null and b/labs/screenshots/lab_9_new/zap_medium_alerts.png differ diff --git a/labs/screenshots/lab_9_new/zap_report_overview.png b/labs/screenshots/lab_9_new/zap_report_overview.png new file mode 100644 index 000000000..776bc3540 Binary files /dev/null and b/labs/screenshots/lab_9_new/zap_report_overview.png differ diff --git a/labs/submission1.md b/labs/submission1.md new file mode 100644 index 000000000..01f3ddc7e --- /dev/null +++ b/labs/submission1.md @@ -0,0 +1,61 @@ +# Lab 1 + +## Task 1: SSH Commit Signature Verification + +**Summary of the benefits of signing commits** + +My answer: + +Signing Git commits shows that the commit was really created by you. A digital signature is added to the commit using your private key. + +First, it confirms the author. A signed commit proves that the code was made by the real owner of the account, not someone else using the same name or email. On platforms like GitHub, this is shown with a *Verified* badge, which helps others trust the commit. + +Second, it protects the commit from changes. The signature checks that the code, message, and author information were not modified after the commit was created. If anything is changed, the signature becomes invalid. + +Commit signing is important for project security, especially in team and open-source projects. It helps prevent fake commits and makes it clear who made each change. + +In short, signing commits is a simple way to make your project history more secure and trustworthy. + +**Evidence of successful SSH key setup and signed commit.** + + ![SSH Key Setup](screenshots/img.png) + +**Question: "Why is commit signing important in DevOps workflows?"** + + +My answer: + +In DevOps workflows, automation is widely used. CI/CD pipelines automatically build and deploy code. +If an attacker pretends to be a developer and pushes harmful code, the pipeline may deploy it to production. +Commit signing helps prevent this. Pipelines can be set to accept only commits signed with trusted keys. +This adds an extra layer of protection to the software supply chain. + + +**Screenshots or verification of the "Verified" badge on GitHub.** + + ![Verified Badge](screenshots/img_1.png) + + +## Task 2: PR Template and Checklist + +**Screenshot of PR template filling the desc** + + ![PR Template](screenshots/img_3.png) + +**Evidence that .github/pull_request_template.md exists on main branch.** + + The file exists at `.github/pull_request_template.md`. + ![File in .github folder](screenshots/img_2.png) + + +**Analysis of how PR templates improve collaboration** + +PR templates make the code review process more organized and easier to follow. + +1. **Clear information:** Each pull request includes a clear goal and list of changes, so reviewers immediately understand what was done. +2. **Same structure:** All contributors use the same format, which makes reviews faster and more consistent. +3. **Better quality:** Checklists remind authors to test their code and update documentation before opening a PR, which reduces extra comments and rework. + +**Challenges during setup** + +The main issue was the setup order. The PR template must be added and pushed to the `main` branch before creating a pull request. If this is not done, GitHub does not apply the template automatically. diff --git a/labs/submission10.md b/labs/submission10.md new file mode 100644 index 000000000..e6112331b --- /dev/null +++ b/labs/submission10.md @@ -0,0 +1,97 @@ +## Task 1 — Исследование artifact registries + +### 1.1 Краткое введение + +Artifact registry в облачной инфраструктуре нужен для хранения, публикации и распространения артефактов сборки: контейнерных образов, OCI-артефактов и пакетов для разных языков программирования. Если сравнивать AWS, GCP и Azure, то видно, что у Google Cloud эта зона выглядит более цельной: Artifact Registry задуман как единая точка хранения для разных типов артефактов. В AWS и Azure контейнерные артефакты и обычные пакеты чаще разведены по разным сервисам. ([Google Cloud Documentation][1]) + +### 1.2 Основные сервисы у разных облачных провайдеров + +#### AWS + +В AWS нельзя выделить один полностью универсальный artifact registry для всех сценариев. На практике здесь используются два основных сервиса. + +**Amazon ECR** — это managed registry для хранения контейнерных образов и связанных артефактов. По официальной документации AWS, ECR предназначен для хранения, публикации и развертывания container software, поддерживает secure sharing, интеграцию с AWS-средой и модель оплаты без предварительных обязательств, где основные расходы связаны с хранением данных и сетевой передачей. Для production-сценариев ECR удобен, когда команда разворачивает образы через ECS, EKS или другие AWS-сервисы. ([Amazon Web Services, Inc.][2]) + +**AWS CodeArtifact** — это отдельный сервис для пакетов приложений. Он описывается AWS как managed artifact repository для хранения и совместного использования software packages. CodeArtifact работает с популярными менеджерами пакетов и поддерживает форматы npm, PyPI, Maven, NuGet, Swift, Ruby, Cargo и generic packages. На мой взгляд, его главный плюс в том, что он закрывает задачу внутренних зависимостей и централизованного контроля библиотек внутри команды или компании. ([AWS Documentation][3]) + +#### GCP + +В Google Cloud основным сервисом является **Artifact Registry**. По официальной документации Google, это централизованное хранилище для artifacts и build dependencies внутри экосистемы Google Cloud. Сервис поддерживает Docker и OCI-артефакты, Helm charts в OCI-формате, а также Maven, npm, Python, Go и другие поддерживаемые форматы. Кроме обычных репозиториев, у него есть remote repositories и virtual repositories, что удобно для кэширования внешних источников и объединения нескольких источников в одну точку доступа. Также Google отдельно документирует vulnerability scanning для Artifact Registry и отдельную тарификацию на хранение, трафик и сканирование. ([Google Cloud Documentation][1]) + +#### Azure + +В Azure, как и в AWS, логика тоже разделяется на два сервиса. + +**Azure Container Registry (ACR)** используется для хранения container images и связанных artifacts. Microsoft описывает ACR как managed registry для хранения и управления container images и related artifacts. Для ACR важны такие возможности, как интеграция с Azure-сервисами, работа с приватными registry, а также разные уровни сервиса — Basic, Standard и Premium, где Premium дает расширенные возможности, например geo-replication. Поэтому ACR хорошо подходит для production-контейнеров в Azure-native окружении. ([Microsoft Learn][4]) + +**Azure Artifacts** — это сервис для пакетных зависимостей в Azure DevOps. Официальная документация указывает, что один feed может содержать несколько типов пакетов: npm, NuGet, Maven, Python, Cargo и Universal Packages. Кроме того, Azure Artifacts поддерживает upstream sources, то есть умеет сохранять и проксировать пакеты из публичных registry внутри собственного feed. Это удобно, когда нужно контролировать зависимости и уменьшить зависимость от внешних репозиториев. По цене Azure DevOps дает бесплатный объем хранения до 2 GiB, после чего включается помегабайтная тарификация. ([Microsoft Learn][5]) + +### 1.3 Сравнительная таблица + +Ниже я свела основные различия в одну таблицу. Она основана на официальной документации AWS, Google Cloud и Microsoft. ([Amazon Web Services, Inc.][2]) + +| Облако | Основной сервис / сервисы | Что поддерживается | Основные сильные стороны | Интеграции | Базовая модель цены | Когда особенно подходит | +| ------ | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------ | +| AWS | Amazon ECR + AWS CodeArtifact | ECR: Docker/OCI-образы и совместимые артефакты; CodeArtifact: npm, PyPI, Maven, NuGet, Swift, Ruby, Cargo, generic packages | Хорошая интеграция с AWS, IAM, хранение контейнеров отдельно от пакетов, managed package repository | ECS, EKS, EC2, IAM и другие AWS-сервисы | ECR: хранение + трафик; CodeArtifact: хранение, запросы и исходящий трафик | Когда инфраструктура уже глубоко завязана на AWS | +| GCP | Artifact Registry | Docker, OCI, Helm OCI, Maven, npm, Python, Go, remote и virtual repositories | Единая модель сервиса, централизованное хранение, vulnerability scanning, remote/virtual repos | Cloud Build, GKE, Cloud Run, IAM | Хранение + трафик; scanning оплачивается отдельно | Когда нужен единый слой хранения контейнеров и пакетов | +| Azure | Azure Container Registry + Azure Artifacts | ACR: Docker/OCI и related artifacts; Azure Artifacts: npm, NuGet, Maven, Python, Cargo, Universal Packages | Geo-replication, Azure DevOps feeds, upstream sources, strong Azure integration | AKS, App Service, Azure ML, Azure DevOps и др. | ACR: тарифные SKU; Azure Artifacts: 2 GiB бесплатно, затем оплата за хранение | Когда команда работает внутри Azure и Azure DevOps | + +### 1.4 Какой registry я бы выбрала для multi-cloud стратегии + +Если рассматривать именно **multi-cloud стратегию**, я бы выбрала **Google Artifact Registry**. + +Мой главный аргумент в том, что у GCP эта часть выглядит наиболее цельной. Один сервис закрывает и контейнерные артефакты, и несколько популярных пакетных экосистем, плюс дает remote и virtual repositories. В AWS и Azure решения более раздроблены: для контейнеров один сервис, для пакетов другой. Это не делает их плохими, но архитектурно такая схема сложнее, если хочется один понятный центр управления артефактами. Мой вывод здесь основан на том, как сами провайдеры описывают свои сервисы и поддерживаемые форматы. ([Google Cloud Documentation][1]) + +Кроме того, Artifact Registry хорошо встраивается в типичный cloud-native pipeline внутри Google Cloud: Cloud Build, GKE, Cloud Run, IAM и Artifact Analysis. Поэтому для команды, которой важны единые правила доступа, единый способ публикации артефактов и более простая структура CI/CD, этот вариант кажется мне самым удобным. При этом я считаю, что для контейнеров как таковых и Amazon ECR, и Azure Container Registry тоже являются очень сильными решениями, особенно если компания уже полностью сидит на AWS или Azure. ([Google Cloud Documentation][1]) + +--- + +## Task 2 — Исследование serverless computing platforms + +### 2.1 Основные serverless-платформы у разных провайдеров + +#### AWS + +Главный serverless compute-сервис в AWS — это **AWS Lambda**. Официальная документация AWS описывает Lambda как сервис, который запускает код по событиям и автоматически управляет вычислительными ресурсами. Lambda поддерживает managed runtimes, а также deployment в виде `.zip`-пакетов и container images. Сервис хорошо ложится на event-driven архитектуру: его удобно использовать с HTTP-вызовами, очередями, потоками событий и другими источниками событий AWS. Максимальный timeout стандартной функции составляет 900 секунд, то есть 15 минут. Оплата идет по числу запросов и времени выполнения. ([AWS Documentation][6]) + +Если говорить о производительности, то у Lambda остается проблема cold start, особенно для интерактивных сценариев. AWS предлагает несколько механизмов снижения задержек, в частности provisioned concurrency и SnapStart. В официальной документации прямо указано, что provisioned concurrency держит прединициализированные execution environments и ориентирован на уменьшение cold start latencies, а SnapStart позволяет заметно сократить время запуска. Поэтому Lambda особенно хорошо подходит для event-driven automation, интеграций, обработки сообщений и сравнительно небольших API. ([AWS Documentation][7]) + +#### GCP + +В Google Cloud основная serverless-платформа — это **Cloud Run**, а для function-oriented сценариев используются **Cloud Run functions**. По документации Google, Cloud Run — это fully managed application platform, на которой можно запускать код, функцию или контейнер. Главное преимущество Cloud Run в том, что он не привязывает разработчика к строго функции как сущности: если приложение можно упаковать в контейнер, его можно развернуть в Cloud Run. Для популярных языков Google также поддерживает source-based deployment. Кроме обычных HTTP-сервисов, Cloud Run умеет работать в режиме services, jobs и worker pools. ([Google Cloud Documentation][8]) + +С точки зрения оплаты и ограничений Cloud Run выглядит очень гибко. Google указывает, что для сервисов действует pay-per-use модель с гранулярностью 100 мс, а стандартный timeout запроса равен 5 минутам и может быть увеличен до 60 минут. Для Cloud Run functions Google отдельно пишет про лимиты до 60 минут для HTTP-triggered функций и до 9 минут для event-driven функций. Масштабирование до нуля тоже поддерживается, но при scale-to-zero возможна дополнительная задержка старта, поэтому Google рекомендует minimum instances и startup CPU boost для уменьшения latency. На мой взгляд, это делает Cloud Run особенно удобным для HTTP API, микросервисов и backend-приложений. ([Google Cloud Documentation][8]) + +#### Azure + +В Azure основными serverless-решениями я бы выделила **Azure Functions** и **Azure Container Apps**. + +**Azure Functions** — это serverless compute-платформа для event-driven кода. Microsoft пишет, что она позволяет строить приложения с меньшими инфраструктурными затратами и поддерживает типичные модели триггеров и bindings. Официально поддерживаются C#, Java, JavaScript, PowerShell и Python, а через custom handlers можно использовать и другие языки, например Go или Rust-подобные сценарии. Для HTTP API Azure Functions тоже подходит, потому что HTTP trigger прямо заявлен как стандартный сценарий. Но здесь есть важное ограничение: независимо от настроек timeout, HTTP-triggered функция должна ответить максимум за 230 секунд из-за idle timeout Azure Load Balancer. ([Microsoft Learn][9]) + +По модели оплаты и масштабированию Azure Functions зависит от выбранного hosting plan. В Consumption-плане оплата идет только за реально использованные compute resources во время работы функции. В Premium-плане execution charge отсутствует, но оплата идет за выделенные core seconds и memory, а также улучшается ситуация с холодным стартом. Microsoft отдельно отмечает, что Flex Consumption сейчас является рекомендуемым serverless hosting plan для Azure Functions. ([Microsoft Learn][10]) + +**Azure Container Apps** — это serverless-платформа для контейнерных приложений. Microsoft описывает ее как способ запускать containerized applications без лишнего управления инфраструктурой. Container Apps особенно хорошо подходит для микросервисов, background workers и job-сценариев. Масштабирование здесь строится на KEDA, причем официальная документация отдельно подчеркивает поддержку event-driven scaling и jobs. Поэтому Container Apps выглядит как более “сервисный” и контейнерный вариант по сравнению с классическими Azure Functions. ([Microsoft Learn][11]) + +### 2.2 Сравнительная таблица + +Ниже я свела основные различия serverless-платформ в одну таблицу. ([AWS Documentation][6]) + +| Провайдер | Основная serverless-платформа | Поддерживаемые языки / runtimes | Модель выполнения | Cold start и масштабирование | Оплата | Ограничения по времени | Типичные сценарии | +| --------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------- | +| AWS | AWS Lambda | Managed runtimes, deployment через zip и container images | Event-driven, HTTP, очереди, streams, другие event sources | Есть cold start; уменьшается через provisioned concurrency и SnapStart | По запросам и длительности выполнения | До 15 минут | Event-driven automation, интеграции, обработка сообщений, API | +| GCP | Cloud Run, Cloud Run functions | Любой язык через контейнер; для популярных языков есть source-based deployment | HTTP services, jobs, worker pools, HTTP и event-driven functions | Scale to zero, minimum instances, startup CPU boost | Pay-per-use, гранулярность 100 мс | Services: до 60 минут; functions: до 60 мин HTTP и до 9 мин event-driven | REST API, микросервисы, web backends, jobs | +| Azure | Azure Functions, Azure Container Apps | Functions: C#, Java, JavaScript, PowerShell, Python, custom handlers; Container Apps: любой язык в контейнере | Triggers and bindings, HTTP, event-driven containers, jobs | Зависит от плана; в Container Apps масштабирование через KEDA | Functions: зависит от hosting plan; Container Apps: pay-as-you-go | Для HTTP-triggered Functions ответ ограничен 230 сек; остальные лимиты зависят от плана | Serverless API, автоматизация, workflow, контейнерные сервисы | + +### Какую platform я бы выбрала для REST API backend + +Если речь идет именно о **REST API backend**, я бы выбрала **Google Cloud Run**. + +Мне этот вариант кажется самым удобным, потому что Cloud Run ближе к привычной модели обычного backend-сервиса, чем классический FaaS. Можно взять приложение на любом языке, упаковать его в контейнер и развернуть как нормальный HTTP-сервис с autoscaling, HTTPS endpoint и оплатой по факту использования. При этом timeout можно увеличить до 60 минут, что заметно гибче, чем 15 минут у Lambda и особенно чем HTTP-ограничение в 230 секунд у Azure Functions. Для API и микросервисов это очень серьезный плюс. ([Google Cloud Documentation][8]) + +AWS Lambda я бы поставила на второе место, если backend сильно зависит от событийной модели AWS и интеграций внутри экосистемы AWS. Azure Functions тоже является сильным решением, особенно если проект уже строится вокруг Azure. Но если выбирать универсальный и удобный именно для REST API вариант, то Cloud Run, на мой взгляд, выглядит наиболее естественно: он дает serverless-масштабирование, но не заставляет мыслить приложение только как набор коротких функций. ([AWS Documentation][12]) + +### Рефлексия: основные плюсы и минусы serverless computing + +Главные **плюсы** serverless-подхода — это снижение операционной нагрузки, автоматическое масштабирование и модель оплаты по фактическому использованию ресурсов. Все три провайдера подчеркивают, что разработчику не нужно напрямую управлять серверами, а платформа сама занимается запуском, масштабированием и инфраструктурной частью. Для API с непредсказуемой нагрузкой, событийных систем и автоматизации это действительно очень удобно. ([AWS Documentation][6]) + +Главные **минусы** — это cold starts, ограничения по времени выполнения, а также зависимость от конкретной платформы и ее модели. У Lambda есть ограничения по timeout и проблема холодного старта, у Cloud Run при scale-to-zero тоже может быть дополнительная задержка, а у Azure Functions есть достаточно жесткий лимит для HTTP-ответа. Поэтому serverless отлично подходит не для всего подряд, а прежде всего для stateless-приложений, событийных обработчиков, API и фоновых задач, где эти ограничения не становятся критичными. ([AWS Documentation][13]) \ No newline at end of file diff --git a/labs/submission2.md b/labs/submission2.md new file mode 100644 index 000000000..bf478089d --- /dev/null +++ b/labs/submission2.md @@ -0,0 +1,685 @@ +## Task 1 — Git Object Model Exploration + +### 1.1: Создание тестового коммита + +Команды: + +```bash +echo "Test content" > test.txt +git add test.txt +git commit -m "Add test file" +``` + +Проверка истории: + +```bash +git log --oneline -3 --decorate +``` + +Вывод: + +```text +1373a59 (HEAD -> feature/lab2) Add test file +ad54f82 (origin/main, main) Merge pull request #1 from KsAKarpeeva73/feature/lab1 +4a33850 (origin/feature/lab1) finish 2 task +``` + +--- + +### 1.2: Просмотр объектов Git (commit / tree / blob) + +#### 1) Commit hash + +Команда: + +```bash +git rev-parse HEAD +``` + +Вывод: + +```text +1373a5901995d2bd2885181902ac2f04f08087a2 +``` + +#### 2) Содержимое commit-объекта и tree hash + +Команда: + +```bash +git cat-file -p HEAD +``` + +Вывод + +```text +tree 97be9be53e014c66ed6b64a3c3cc194a64f1702a +parent ad54f82a98d81aee74303486830fca3054b5ab4c +author KsAKarpeeva73 1770922916 +0300 +committer KsAKarpeeva73 1770922916 +0300 +gpgsig -----BEGIN SSH SIGNATURE----- + U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgMN/NduiMfE+9e5+BjP0SiXiWGt + Nse+k+QK2UKL9tz44AAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 + AAAAQOPF8cHDdAYbSUr2an1mazUIUOqpTzdicSZbsPR+UCJ3VWEOWQGFMk79ME6W1EYSmk + bysJDJXq0IpEF9ur1jKgg= + -----END SSH SIGNATURE----- + +Add test file +``` + +#### 3) Содержимое tree-объекта (структура репозитория) и blob hash для test.txt + +Команда: + +```bash +git cat-file -p 97be9be53e014c66ed6b64a3c3cc194a64f1702a +``` + +Вывод: + +```text +040000 tree 6a760a8deafb7e27f0eef7130e46b23f9c8e63d5 .github +100644 blob a38dbc03f5f790c14c955f7cc235f89bef651095 .gitignore +100644 blob 6e60bebec0724892a7c82c52183d0a7b467cb6bb README.md +040000 tree a1061247fd38ef2a568735939f86af7b1000f83c app +040000 tree 09e15257766447b3ee5129c5de404ab0da3b66d4 labs +040000 tree d3fb3722b7a867a83efde73c57c49b5ab3e62c63 lectures +100644 blob 2eec599a1130d2ff231309bb776d1989b97c6ab2 test.txt +``` + +Здесь видно, что файл `test.txt` хранится как **blob** с хэшем: `2eec599a1130d2ff231309bb776d1989b97c6ab2` + +#### 4) Содержимое blob-объекта + +Команда: + +```bash +git cat-file -p 2eec599a1130d2ff231309bb776d1989b97c6ab2 +``` + +Вывод: + +```text +Test content +``` + +--- + +### Ответ + +* **Blob** — это содержимое файла. Git хранит данные файла отдельно, и blob не знает про имя файла или папку, только про байты. +* **Tree** — это снимок структуры: список файлов и подпапок с их именами и ссылками на blob/tree-объекты. +* **Commit** — это снимок состояния проекта: он ссылается на корневой tree, хранит автора/дату/сообщение и указывает на родительский коммит. + +### Как Git хранит данные в репо + +Цепочка такая: + +* Коммит `1373a590...` ссылается на дерево `97be9be5...` +* В дереве `97be9be5...` есть запись `test.txt`, которая ссылается на blob `2eec599a...` +* Blob `2eec599a...` содержит текст `Test content` + +То есть Git хранит не “файлы в папках” как обычная ОС, а набор объектов (blob/tree/commit), связанных между собой хэшами. + +--- + +## Task 2 — Reset and Reflog Recovery + +### 2.1: Создание ветки + +Команды: + +```bash +git status +git switch feature/lab2 +git status +git switch -c git-reset-practice +git status +``` + +Вывод: + +```text +On branch feature/lab2 +nothing to commit, working tree clean +``` + +Дальше я сделала три последовательных коммита в ветке `git-reset-practice`: + +```bash +echo "First commit" > file.txt +git add file.txt +git commit -m "First commit" + +echo "Second commit" >> file.txt +git add file.txt +git commit -m "Second commit" + +echo "Third commit" >> file.txt +git add file.txt +git commit -m "Third commit" +``` + +--- + +### 2.2: reset --soft, reset --hard и восстановление через reflog + +#### reset --soft HEAD~1 + +Команды: + +```bash +git switch git-reset-practice +git log --oneline -5 --decorate +git status +cat file.txt + +git reset --soft HEAD~1 + +git log --oneline -5 --decorate +git status +cat file.txt +``` + +Вывод: + +```text +M labs/submission2.md +Already on 'git-reset-practice' +291bd08 (HEAD -> git-reset-practice) First commit +6eac8d4 Second commit +7ec3d00 First commit +e57a2d9 (feature/lab2) finish 1 task +1373a59 Add test file +On branch git-reset-practice +Changes to be committed: + (use "git restore --staged ..." to unstage) + modified: labs/submission2.md + +First commit +6eac8d4 (HEAD -> git-reset-practice) Second commit +7ec3d00 First commit +e57a2d9 (feature/lab2) finish 1 task +1373a59 Add test file +ad54f82 (origin/main, main) Merge pull request #1 from KsAKarpeeva73/feature/lab1 +On branch git-reset-practice +Changes to be committed: + (use "git restore --staged ..." to unstage) + modified: file.txt + modified: labs/submission2.md + +First commit +``` +После `git reset --soft HEAD~1` HEAD сдвигается назад по истории, но изменения не пропадают. +По `git status` видно, что изменения остаются в индексе и считаются подготовленными к коммиту, в моём случае staged были `file.txt` и `labs/submission2.md`. + +--- + +#### reset --hard HEAD~1 + +Команды: + +```bash +git reset --hard HEAD@{2} +git log --oneline -5 --decorate +git status +cat file.txt + +git reset --hard HEAD~1 + +git log --oneline -5 --decorate +git status +cat file.txt +``` + +Вывод: + +```text +fatal: Cannot do hard reset with paths. +6eac8d4 (HEAD -> git-reset-practice) Second commit +7ec3d00 First commit +e57a2d9 (feature/lab2) finish 1 task +1373a59 Add test file +ad54f82 (origin/main, main) Merge pull request #1 from KsAKarpeeva73/feature/lab1 +On branch git-reset-practice +Changes to be committed: + (use "git restore --staged ..." to unstage) + modified: file.txt + modified: labs/submission2.md + +First commit +HEAD is now at 7ec3d00 First commit +7ec3d00 (HEAD -> git-reset-practice) First commit +e57a2d9 (feature/lab2) finish 1 task +1373a59 Add test file +ad54f82 (origin/main, main) Merge pull request #1 from KsAKarpeeva73/feature/lab1 +4a33850 (origin/feature/lab1) finish 2 task +On branch git-reset-practice +nothing to commit, working tree clean +First commit +``` + +После `git reset --hard HEAD~1` Git откатил HEAD назад и очистил рабочую папку: `git status` показал `nothing to commit, working tree clean`. +По `cat file.txt` видно, что содержимое реально откатилось и осталось только `First commit`. + +--- + +#### reflog и восстановление состояния + +Команды: + +```bash +git reflog -10 +git reset --hard HEAD@{2} +git log --oneline -5 --decorate +git status +cat file.txt +``` + +Вывод: + +```text +7ec3d00 (HEAD -> git-reset-practice) HEAD@{0}: reset: moving to HEAD~1 +6eac8d4 HEAD@{1}: reset: moving to HEAD~1 +291bd08 HEAD@{2}: checkout: moving from git-reset-practice to git-reset-practice +291bd08 HEAD@{3}: reset: moving to HEAD~1 +59f8fe7 HEAD@{4}: checkout: moving from git-reset-practice to git-reset-practice +59f8fe7 HEAD@{5}: commit (amend): finish 2 task +c85b86a HEAD@{6}: commit: finish 1 task +291bd08 HEAD@{7}: reset: moving to HEAD~1 +a7c55ba HEAD@{8}: reset: moving to HEAD~1 +bfcfb22 HEAD@{9}: commit: Third commit +HEAD is now at 291bd08 First commit +291bd08 (HEAD -> git-reset-practice) First commit +6eac8d4 Second commit +7ec3d00 First commit +e57a2d9 (feature/lab2) finish 1 task +1373a59 Add test file +On branch git-reset-practice +nothing to commit, working tree clean +First commit +``` + + +`git reflog` реально помогает, потому что он показывает, куда двигался HEAD, даже если коммиты уже не видны в обычном `git log`. +В моём reflog есть запись `bfcfb22 ... commit: Third commit` — это нужная точка, чтобы вернуть состояние с третьим коммитом. +Я попробовала восстановиться командой `git reset --hard HEAD@{2}`, но по reflog видно, что `HEAD@{2}` у меня сейчас относится к checkout, поэтому я в итоге вернулась на `291bd08 First commit`. + +Чтобы восстановиться именно на `Third commit`, нужно сброситься на `bfcfb22` (он у меня в reflog как `HEAD@{9}`): + + +```bash +git reset --hard bfcfb22 +``` + + +```bash +git log --oneline -5 --decorate +git status +cat file.txt +``` + +--- + +### Что меняется в working tree, индексе и истории + +* `git reset --soft HEAD~1` двигает HEAD назад, но изменения остаются подготовленными к коммиту, то есть остаются в индексе. +* `git reset --hard HEAD~1` двигает HEAD назад и полностью приводит рабочую директорию и индекс к состоянию того коммита, на который откатились. +* `git reflog` позволяет найти прошлые состояния HEAD и восстановиться, даже если я уже “ушла назад” reset-ом. + + + + +## Task 3 — Visualize Commit History + +### Команды + +```bash +git status +git switch feature/lab2 +git status +git add labs/submission2.md +git commit -m "docs: update submission2 tasks 1-2" +git switch -c side-branch +echo "Branch commit" >> history.txt +git add history.txt +git commit -m "Side branch commit" +git switch - +git log --oneline --graph --all --decorate -15 +``` + +### Вывод + +```text +On branch feature/lab2 +Your branch is up to date with 'origin/feature/lab2'. + +nothing to commit, working tree clean +Already on 'feature/lab2' +Your branch is up to date with 'origin/feature/lab2'. +On branch feature/lab2 +Your branch is up to date with 'origin/feature/lab2'. + +nothing to commit, working tree clean +``` + +```text +On branch feature/lab2 +Your branch is up to date with 'origin/feature/lab2'. + +nothing to commit, working tree clean +``` + +```text +Switched to a new branch 'side-branch' +[side-branch 5302521] Side branch commit + 1 file changed, 1 insertion(+) + create mode 100644 history.txt +``` + +```text +Switched to branch 'feature/lab2' +Your branch is up to date with 'origin/feature/lab2'. +``` + +```text +* 5302521 (side-branch) Side branch commit +* fef7d67 (HEAD -> feature/lab2, origin/feature/lab2) finish 2 task +| * 291bd08 (git-reset-practice) First commit +| * 6eac8d4 Second commit +| * 7ec3d00 First commit +|/ +* e57a2d9 finish 1 task +* 1373a59 Add test file +* ad54f82 (origin/main, main) Merge pull request #1 from KsAKarpeeva73/feature/lab1 +|\ +| * 4a33850 (origin/feature/lab1) finish 2 task +| * 99e184b finish 1 task +| * 990680f docs: add commit signing summary +| | * 08dabbc (tag: v1.0.0) Add test file +| |/ +|/| +* | c9679b3 chore: add PR template +|/ +* d6b6a03 Update lab2 +* 87810a0 feat: remove old Exam Exemption Policy +``` + +### Вывод + +Граф `git log --graph --all` удобно показывает, где именно разошлись ветки и какой коммит к какой ветке относится. По нему сразу видно параллельные ветки `side-branch` и `git-reset-practice` и то, что они не слиты обратно в `feature/lab2`. + + + +## Task 4 — Tagging Commits + +### Команды + +```bash +git switch feature/lab2 +git status +git tag --list +git show-ref --tags +git ls-remote --tags origin +git push origin v1.0.0 +git rev-parse --short HEAD +git tag v1.1.0 +git push origin v1.1.0 +git show v1.1.0 --oneline --no-patch +``` + +### Вывод + +```text +Already on 'feature/lab2' +Your branch is up to date with 'origin/feature/lab2'. +On branch feature/lab2 +Your branch is up to date with 'origin/feature/lab2'. + +nothing to commit, working tree clean +v1.0.0 +08dabbc0e01ca71487f6b14d542789e9f99be544 refs/tags/v1.0.0 +``` + +```text +08dabbc0e01ca71487f6b14d542789e9f99be544 refs/tags/v1.0.0 +``` + +```text +Everything up-to-date +``` + +```text +855b5d5 +Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 +To https://github.com/KsAKarpeeva73/DevOps-Intro.git + * [new tag] v1.1.0 -> v1.1.0 +``` + +```text +855b5d5 (HEAD -> feature/lab2, tag: v1.1.0, origin/feature/lab2) finish 3 task +``` + +### Теги и к каким коммитам привязаны + +* v1.0.0 → 08dabbc0e01ca71487f6b14d542789e9f99be544 +* v1.1.0 → 855b5d5 + +### Зачем вообще нужны теги + +Теги нужны, чтобы помечать конкретные стабильные версии кода и потом легко на них ссылаться. Это удобно для релизов, для заметок к версиям и для CI/CD, когда сборка или релиз запускаются по тегу. + + +## Task 5 — git switch vs git checkout vs git restore + +### Команды + +```bash +git switch feature/lab2 +git status +git branch + +git switch -c cmd-compare +git branch +git status +git switch - +git branch +git status + +git checkout -b cmd-compare-2 +git branch +git status +git switch - +git branch +git status + +echo "base" > demo.txt +git add demo.txt +git commit -m "chore: add demo file for restore" +git status + +echo "scratch" >> demo.txt +git status +cat demo.txt +git restore demo.txt +git status +cat demo.txt + +echo "staged-change" >> demo.txt +git add demo.txt +git status +cat demo.txt +git restore --staged demo.txt +git status +cat demo.txt + +git add demo.txt +git commit -m "chore: update demo file" +git log --oneline -3 --decorate +cat demo.txt + +git restore --source=HEAD~1 demo.txt +git status +cat demo.txt +``` + +### Вывод + +```text +Already on 'feature/lab2' +Your branch is up to date with 'origin/feature/lab2'. +On branch feature/lab2 +Your branch is up to date with 'origin/feature/lab2'. + +nothing to commit, working tree clean +* feature/lab2 + git-reset-practice + main + side-branch +``` + +```text +Switched to a new branch 'cmd-compare' +* cmd-compare + feature/lab2 + git-reset-practice + main + side-branch +On branch cmd-compare +nothing to commit, working tree clean +Switched to branch 'feature/lab2' +Your branch is up to date with 'origin/feature/lab2'. + cmd-compare +* feature/lab2 + git-reset-practice + main + side-branch +On branch feature/lab2 +Your branch is up to date with 'origin/feature/lab2'. + +nothing to commit, working tree clean +``` + +```text +Switched to a new branch 'cmd-compare-2' + cmd-compare +* cmd-compare-2 + feature/lab2 + git-reset-practice + main + side-branch +On branch cmd-compare-2 +nothing to commit, working tree clean +Switched to branch 'feature/lab2' +Your branch is up to date with 'origin/feature/lab2'. + cmd-compare + cmd-compare-2 +* feature/lab2 + git-reset-practice + main + side-branch +On branch feature/lab2 +Your branch is up to date with 'origin/feature/lab2'. + +nothing to commit, working tree clean +``` + +```text +[feature/lab2 f2a27d5] chore: add demo file for restore + 1 file changed, 1 insertion(+) + create mode 100644 demo.txt +On branch feature/lab2 +Your branch is ahead of 'origin/feature/lab2' by 1 commit. + (use "git push" to publish your local commits) + +nothing to commit, working tree clean +``` + +```text +On branch feature/lab2 +Your branch is ahead of 'origin/feature/lab2' by 1 commit. + (use "git push" to publish your local commits) + +Changes not staged for commit: + (use "git add ..." to update what will be committed) + (use "git restore ..." to discard changes in working directory) + modified: demo.txt + +no changes added to commit (use "git add" and/or "git commit -a") +base +scratch +On branch feature/lab2 +Your branch is ahead of 'origin/feature/lab2' by 1 commit. + (use "git push" to publish your local commits) + +nothing to commit, working tree clean +base +``` + +```text +On branch feature/lab2 +Your branch is ahead of 'origin/feature/lab2' by 1 commit. + (use "git push" to publish your local commits) + +Changes to be committed: + (use "git restore --staged ..." to unstage) + modified: demo.txt + +base +staged-change +On branch feature/lab2 +Your branch is ahead of 'origin/feature/lab2' by 1 commit. + (use "git push" to publish your local commits) + +Changes not staged for commit: + (use "git add ..." to update what will be committed) + (use "git restore ..." to discard changes in working directory) + modified: demo.txt + +no changes added to commit (use "git add" and/or "git commit -a") +base +staged-change +``` + +```text +[feature/lab2 8b2b2d4] chore: update demo file + 1 file changed, 1 insertion(+) +8b2b2d4 (HEAD -> feature/lab2) chore: update demo file +f2a27d5 chore: add demo file for restore +bd20ea6 (origin/feature/lab2, cmd-compare-2, cmd-compare) finish 4 task +base +staged-change +``` + +```text +On branch feature/lab2 +Your branch is ahead of 'origin/feature/lab2' by 2 commits. + (use "git push" to publish your local commits) + +Changes not staged for commit: + (use "git add ..." to update what will be committed) + (use "git restore ..." to discard changes in working directory) + modified: demo.txt + +no changes added to commit (use "git add" and/or "git commit -a") +base +``` + +### Когда что использовать + +`git switch` я использую, когда мне нужно только работать с ветками: создать ветку и перейти на неё или быстро вернуться на прошлую ветку через `git switch -`. +`git checkout` делает то же самое, но он старый и у него слишком много ролей, поэтому его легко перепутать с восстановлением файлов. +`git restore` я использую, когда нужно откатить изменения в файлах: убрать изменения из рабочей папки, снять файл из staged или вернуть файл из другого коммита. + +## Task 6 +![img_4](screenshots/img_4.png) +![img_5](screenshots/img_5.png) +![img_6](screenshots/img_6.png) +![img_7](screenshots/img_7.png) +![img_8](screenshots/img_8.png) +![img_9](screenshots/img_9.png) +![img_10](screenshots/img_10.png) +![img_11](screenshots/img_11.png) +## GitHub Community + +Я поставила звёзды нужным репозиториям и подписалась на преподавателя, ассистентов и одногруппников. Звёзды важны, потому что это простой способ поддержать проект и отметить его себе, а подписки помогают следить за активностью людей, быстрее находить полезные решения и проще взаимодействовать в командных задачах. diff --git a/labs/submission3.md b/labs/submission3.md new file mode 100644 index 000000000..f6b258bb9 --- /dev/null +++ b/labs/submission3.md @@ -0,0 +1,359 @@ +## Lab 3 (GitHub Actions) + +### Task 1 + +### Что было сделанo + +1. Создана отдельная ветка для лабы: `feature/lab3`. +2. Добавлен workflow GitHub Actions в правильную директорию репозитория: `.github/workflows/`. +3. Workflow настроен на автоматический запуск при `push` в ветку `feature/lab3`. +4. Внутри job добавлен шаг, который печатает базовую информацию о запуске (дата/время, актор, ref, SHA, ОС runner’а). + +### Доказательства + +Успешный запуск job `basic-info`: + +GitHub: + +Ссылка на лог job: +[https://github.com/KsAKarpeeva73/DevOps-Intro/actions/runs/22180571407/job/64140491668#step:1:2](https://github.com/KsAKarpeeva73/DevOps-Intro/actions/runs/22180571407/job/64140491668#step:1:2) + +![Запуск видно в actions](screenshots/img_12.png) + +### Почему workflow запустился + +Workflow был запущен автоматически из-за события **`push`** в ветку `feature/lab3` (это указано в секции `on: push` в YAML). После коммита и пуша GitHub создал новый run в разделе **Actions**, выполнил job и показал логи. + +### Ключевые понятия, которые проявились на практике + +* **Workflow** — YAML-файл, который описывает автоматизацию (что запускать и когда). +* **Trigger (event)** — событие, которое запускает workflow (здесь: `push`). +* **Job** — набор шагов, выполняемых на одном runner’е (здесь: `basic-info`). +* **Steps** — последовательные команды внутри job (здесь: `echo`, `date`, переменные окружения, `uname -a`). +* **Runner** — виртуальная машина, на которой выполняется job (здесь: `ubuntu-latest`). + +### Фрагмент лога +```log +basic-info +succeeded 4 minutes ago in 4s + +0s +Current runner version: '2.331.0' +Runner Image Provisioner +Operating System +Runner Image +GITHUB_TOKEN Permissions +Secret source: Actions +Prepare workflow directory +Prepare all required actions +Complete job name: basic-info +0s +Run echo "Hello from GitHub Actions!" +Hello from GitHub Actions! +Thu Feb 19 11:50:00 UTC 2026 +Actor: KsAKarpeeva73 +Ref: refs/heads/feature/lab3 +SHA: 2f63fe4487493743fb51b0f847f574b94cafe817 +Linux runnervmjduv7 6.14.0-1017-azure #17~24.04.1-Ubuntu SMP Mon Dec 1 20:10:50 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux +0s +Cleaning up orphan processes +``` + + + +Ок, под твой формат — вот **что именно вписать в `labs/submission3.md` для Task 2 (GitHub Actions)**. Я сделаю так, чтобы это закрывало пункты из чеклиста (изменения в YAML, системная инфа, сравнение manual/auto, короткий анализ, скриншоты). + +--- + +## Task 2 + +### Что поменялось + +Файл: `.github/workflows/lab3.yml` + +1. Добавил(а) ручной запуск: + +```yaml +workflow_dispatch: +``` +```log + - name: System information + run: | + echo "=== SYSTEM INFO ===" + echo "--- OS / Kernel ---" + uname -a + cat /etc/os-release || true + + echo "--- CPU ---" + nproc + lscpu || true + cat /proc/cpuinfo | head -n 30 || true + + echo "--- Memory ---" + free -h || true + cat /proc/meminfo | head -n 20 || true + + echo "--- Disk ---" + df -h + lsblk || true + + echo "--- GitHub runner env ---" + echo "RUNNER_OS=$RUNNER_OS" + echo "RUNNER_ARCH=$RUNNER_ARCH" + echo "RUNNER_NAME=$RUNNER_NAME" + echo "RUNNER_TEMP=$RUNNER_TEMP" + echo "GITHUB_WORKFLOW=$GITHUB_WORKFLOW" + echo "GITHUB_RUN_ID=$GITHUB_RUN_ID" + echo "GITHUB_RUN_NUMBER=$GITHUB_RUN_NUMBER" +``` + +--- + +### Доказательства + +Run: https://github.com/KsAKarpeeva73/DevOps-Intro/actions/runs/22181120803/job/64142367015 + +![screen 1](screenshots/img_13.png) +![screen 1](screenshots/img_14.png) +![screen 1](screenshots/img_15.png) +![screen 1](screenshots/img_16.png) + + +--- + +### Что вывелось в логах + +```log +basic-info +succeeded 5 minutes ago in 2s + +0s +Current runner version: '2.331.0' +Runner Image Provisioner +Operating System +Runner Image +GITHUB_TOKEN Permissions +Secret source: Actions +Prepare workflow directory +Prepare all required actions +Complete job name: basic-info +0s +Run echo "Hello from GitHub Actions!" +Hello from GitHub Actions! +Thu Feb 19 12:07:44 UTC 2026 +Actor: KsAKarpeeva73 +Ref: refs/heads/feature/lab3 +SHA: f0619cff5668be6a43df586f573bf90c951ba879 +Linux runnervmjduv7 6.14.0-1017-azure #17~24.04.1-Ubuntu SMP Mon Dec 1 20:10:50 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux +1s +Run echo "=== SYSTEM INFO ===" +=== SYSTEM INFO === +--- OS / Kernel --- +Linux runnervmjduv7 6.14.0-1017-azure #17~24.04.1-Ubuntu SMP Mon Dec 1 20:10:50 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux +PRETTY_NAME="Ubuntu 24.04.3 LTS" +NAME="Ubuntu" +VERSION_ID="24.04" +VERSION="24.04.3 LTS (Noble Numbat)" +VERSION_CODENAME=noble +ID=ubuntu +ID_LIKE=debian +HOME_URL="https://www.ubuntu.com/" +SUPPORT_URL="https://help.ubuntu.com/" +BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" +PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" +UBUNTU_CODENAME=noble +LOGO=ubuntu-logo +--- CPU --- +4 +Architecture: x86_64 +CPU op-mode(s): 32-bit, 64-bit +Address sizes: 48 bits physical, 48 bits virtual +Byte Order: Little Endian +CPU(s): 4 +On-line CPU(s) list: 0-3 +Vendor ID: AuthenticAMD +Model name: AMD EPYC 7763 64-Core Processor +CPU family: 25 +Model: 1 +Thread(s) per core: 2 +Core(s) per socket: 2 +Socket(s): 1 +Stepping: 1 +BogoMIPS: 4890.86 +Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm constant_tsc rep_good nopl tsc_reliable nonstop_tsc cpuid extd_apicid aperfmperf tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm cmp_legacy svm cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw topoext vmmcall fsgsbase bmi1 avx2 smep bmi2 erms invpcid rdseed adx smap clflushopt clwb sha_ni xsaveopt xsavec xgetbv1 xsaves user_shstk clzero xsaveerptr rdpru arat npt nrip_save tsc_scale vmcb_clean flushbyasid decodeassists pausefilter pfthreshold v_vmsave_vmload umip vaes vpclmulqdq rdpid fsrm +Virtualization: AMD-V +Hypervisor vendor: Microsoft +Virtualization type: full +L1d cache: 64 KiB (2 instances) +L1i cache: 64 KiB (2 instances) +L2 cache: 1 MiB (2 instances) +L3 cache: 32 MiB (1 instance) +NUMA node(s): 1 +NUMA node0 CPU(s): 0-3 +Vulnerability Gather data sampling: Not affected +Vulnerability Ghostwrite: Not affected +Vulnerability Indirect target selection: Not affected +Vulnerability Itlb multihit: Not affected +Vulnerability L1tf: Not affected +Vulnerability Mds: Not affected +Vulnerability Meltdown: Not affected +Vulnerability Mmio stale data: Not affected +Vulnerability Reg file data sampling: Not affected +Vulnerability Retbleed: Not affected +Vulnerability Spec rstack overflow: Vulnerable: Safe RET, no microcode +Vulnerability Spec store bypass: Vulnerable +Vulnerability Spectre v1: Mitigation; usercopy/swapgs barriers and __user pointer sanitization +Vulnerability Spectre v2: Mitigation; Retpolines; STIBP disabled; RSB filling; PBRSB-eIBRS Not affected; BHI Not affected +Vulnerability Srbds: Not affected +Vulnerability Tsa: Vulnerable: Clear CPU buffers attempted, no microcode +Vulnerability Tsx async abort: Not affected +Vulnerability Vmscape: Not affected +processor : 0 +vendor_id : AuthenticAMD +cpu family : 25 +model : 1 +model name : AMD EPYC 7763 64-Core Processor +stepping : 1 +microcode : 0xffffffff +cpu MHz : 3222.341 +cache size : 512 KB +physical id : 0 +siblings : 4 +core id : 0 +cpu cores : 2 +apicid : 0 +initial apicid : 0 +fpu : yes +fpu_exception : yes +cpuid level : 13 +wp : yes +flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm constant_tsc rep_good nopl tsc_reliable nonstop_tsc cpuid extd_apicid aperfmperf tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm cmp_legacy svm cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw topoext vmmcall fsgsbase bmi1 avx2 smep bmi2 erms invpcid rdseed adx smap clflushopt clwb sha_ni xsaveopt xsavec xgetbv1 xsaves user_shstk clzero xsaveerptr rdpru arat npt nrip_save tsc_scale vmcb_clean flushbyasid decodeassists pausefilter pfthreshold v_vmsave_vmload umip vaes vpclmulqdq rdpid fsrm +bugs : sysret_ss_attrs null_seg spectre_v1 spectre_v2 spec_store_bypass srso tsa +bogomips : 4890.86 +TLB size : 2560 4K pages +clflush size : 64 +cache_alignment : 64 +address sizes : 48 bits physical, 48 bits virtual +power management: + +processor : 1 +vendor_id : AuthenticAMD +--- Memory --- + total used free shared buff/cache available +Mem: 15Gi 781Mi 13Gi 35Mi 1.8Gi 14Gi +Swap: 3.0Gi 0B 3.0Gi +MemTotal: 16378532 kB +MemFree: 14030256 kB +MemAvailable: 15577804 kB +Buffers: 52880 kB +Cached: 1764808 kB +SwapCached: 0 kB +Active: 618396 kB +Inactive: 1419216 kB +Active(anon): 264908 kB +Inactive(anon): 0 kB +Active(file): 353488 kB +Inactive(file): 1419216 kB +Unevictable: 46752 kB +Mlocked: 43680 kB +SwapTotal: 3145724 kB +SwapFree: 3145724 kB +Zswap: 0 kB +Zswapped: 0 kB +Dirty: 368 kB +Writeback: 0 kB +--- Disk --- +Filesystem Size Used Avail Use% Mounted on +/dev/root 145G 53G 92G 37% / +tmpfs 7.9G 84K 7.9G 1% /dev/shm +tmpfs 3.2G 1000K 3.2G 1% /run +tmpfs 5.0M 0 5.0M 0% /run/lock +efivarfs 128M 29K 128M 1% /sys/firmware/efi/efivars +/dev/sda16 881M 63M 757M 8% /boot +/dev/sda15 105M 6.2M 99M 6% /boot/efi +tmpfs 1.6G 12K 1.6G 1% /run/user/1001 +NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS +sda 8:0 0 150G 0 disk +├─sda1 8:1 0 149G 0 part / +├─sda14 8:14 0 4M 0 part +├─sda15 8:15 0 106M 0 part /boot/efi +└─sda16 259:0 0 913M 0 part /boot +--- GitHub runner env --- +RUNNER_OS=Linux +RUNNER_ARCH=X64 +RUNNER_NAME=GitHub Actions 1000000003 +RUNNER_TEMP=/home/runner/work/_temp +GITHUB_WORKFLOW=Lab 3 CI +GITHUB_RUN_ID=22181120803 +GITHUB_RUN_NUMBER=4 +0s +Cleaning up orphan processes +``` + +--- + +### Сравнение ручного и автоматического запуска + +* **Автоматический запуск (`push`)** — workflow сам стартует при пуше в ветку `feature/lab3`. +* **Ручной запуск (`workflow_dispatch`)** — workflow запускается из интерфейса GitHub кнопкой **Run workflow**, без новых коммитов. Это удобно, когда нужно просто перепроверить окружение runner’а или прогнать workflow ещё раз. + +--- + +### Короткий анализ окружения runner + +Runner — стандартная виртуалка GitHub Actions на Ubuntu 24.04 LTS (Noble). +По логам видно, что окружение типичное для CI: Linux x86_64, доступ к базовым системным утилитам (`lscpu`, `free`, `df`, `lsblk`). Подходит для тестов/сборок, где нужно быстро получить повторяемое окружение. + +# *.github/workflows/lab3.yml* + +```yaml +name: Lab 3 CI + +on: + push: + branches: + - feature/lab3 + workflow_dispatch: + +jobs: + basic-info: + runs-on: ubuntu-latest + steps: + - name: Print basic info + run: | + echo "Hello from GitHub Actions!" + date + echo "Actor: $GITHUB_ACTOR" + echo "Ref: $GITHUB_REF" + echo "SHA: $GITHUB_SHA" + uname -a + + - name: System information + run: | + echo "=== SYSTEM INFO ===" + echo "--- OS / Kernel ---" + uname -a + cat /etc/os-release || true + + echo "--- CPU ---" + nproc + lscpu || true + cat /proc/cpuinfo | head -n 30 || true + + echo "--- Memory ---" + free -h || true + cat /proc/meminfo | head -n 20 || true + + echo "--- Disk ---" + df -h + lsblk || true + + echo "--- GitHub runner env ---" + echo "RUNNER_OS=$RUNNER_OS" + echo "RUNNER_ARCH=$RUNNER_ARCH" + echo "RUNNER_NAME=$RUNNER_NAME" + echo "RUNNER_TEMP=$RUNNER_TEMP" + echo "GITHUB_WORKFLOW=$GITHUB_WORKFLOW" + echo "GITHUB_RUN_ID=$GITHUB_RUN_ID" + echo "GITHUB_RUN_NUMBER=$GITHUB_RUN_NUMBER" +``` \ No newline at end of file diff --git a/labs/submission4.md b/labs/submission4.md new file mode 100644 index 000000000..276bed927 --- /dev/null +++ b/labs/submission4.md @@ -0,0 +1,696 @@ +## Task 1 — Operating System Analysis + +Environment: Ubuntu VPS. All outputs below are copied as-is. + +### 1.1 Boot Performance Analysis + +Command: + +```sh +systemd-analyze +``` + +Output: + +```text +Startup finished in 2.434s (kernel) + 23.970s (userspace) = 26.405s +graphical.target reached after 22.857s in userspace. +``` + +Observation: +Система грузится примерно за ~26.4 секунды суммарно. Большая часть времени — userspace (почти 24 секунды), то есть уже после загрузки ядра. До graphical.target дошло за ~22.9 секунды в userspace (видимо, поднимаются сервисы и сеть). + +Command: + +```sh +systemd-analyze blame --no-pager +``` + +Output: + +```text +16.106s cloud-init-local.service + 5.473s apt-daily.service + 2.035s cloud-init.service + 1.997s systemd-networkd-wait-online.service + 1.477s cloud-config.service + 1.399s dev-vda1.device + 1.091s cloud-final.service + 959ms apt-daily-upgrade.service + 872ms docker.service + 436ms ldconfig.service + 424ms tuned.service + 319ms user@0.service + 309ms systemd-udev-trigger.service + 281ms systemd-journal-catalog-update.service + 241ms fstrim.service + 213ms systemd-machine-id-commit.service + 145ms dev-hugepages.mount + 141ms dev-mqueue.mount + 137ms kmod-static-nodes.service + 136ms sys-kernel-debug.mount + 136ms systemd-fsck-root.service + 136ms modprobe@drm.service + 135ms systemd-logind.service + 134ms modprobe@configfs.service + 134ms modprobe@fuse.service + 132ms sys-kernel-tracing.mount + 116ms systemd-resolved.service + 111ms systemd-tmpfiles-setup-dev-early.service + 109ms systemd-modules-load.service + 107ms containerd.service + 107ms rsyslog.service + 93ms polkit.service + 88ms dbus.service + 84ms systemd-udevd.service + 79ms systemd-firstboot.service + 67ms systemd-networkd.service + 60ms systemd-journal-flush.service + 59ms systemd-remount-fs.service + 59ms systemd-update-utmp.service + 55ms ssh.service + 55ms systemd-binfmt.service + 55ms systemd-sysusers.service + 53ms systemd-tmpfiles-setup.service + 52ms systemd-timesyncd.service + 47ms packagekit.service + 44ms sys-fs-fuse-connections.mount + 41ms grub-common.service + 37ms sys-kernel-config.mount + 37ms systemd-random-seed.service + 36ms grub-initrd-fallback.service + 36ms systemd-update-utmp-runlevel.service + 31ms modprobe@dm_mod.service + 28ms systemd-tmpfiles-clean.service + 27ms systemd-journald.service + 26ms modprobe@loop.service + 25ms systemd-update-done.service + 23ms modprobe@efi_pstore.service + 20ms systemd-sysctl.service + 20ms e2scrub_reap.service + 18ms dpkg-db-backup.service + 16ms docker.socket + 15ms systemd-tmpfiles-setup-dev.service + 13ms user-runtime-dir@0.service + 10ms e2scrub_all.service + 10ms proc-sys-fs-binfmt_misc.mount + 8ms systemd-user-sessions.service + 2ms motd-news.service +``` + +Observation: +Топ по времени — cloud-init-local.service (16 сек), потом apt-daily.service (5.5 сек). Похоже это виртуалка/облако, и cloud-init реально заметно тормозит старт. Ещё видно systemd-networkd-wait-online.service ~2 секунды — ожидание сети. + +Command: + +```sh +uptime +``` + +Output: + +```text + 20:08:42 up 63 days, 3:31, 1 user, load average: 0.06, 0.02, 0.00 +``` + +Observation: +Аптайм большой (63 дня), средняя нагрузка очень маленькая (0.06/0.02/0.00), то есть сейчас система почти простаивает. + +Command: + +```sh +w +``` + +Output: + +```text + 20:08:42 up 63 days, 3:31, 1 user, load average: 0.06, 0.02, 0.00 +USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT +root 31.57.61.235 20:05 3days 0.00s 0.08s sshd: root@pts/0 +``` + +Observation: +Сейчас активен 1 пользователь (root), подключение по SSH с IP 31.57.61.235. По IDLE странно показывает 3 дня, но при этом логин в 20:05 — возможно особенность/глюк учёта idle или сессия держится долго. + +### 1.2 Process Forensics + +Command: + +```sh +ps -eo pid,ppid,cmd,%mem,%cpu --sort=-%mem | head -n 6 +``` + +Output: + +```text + PID PPID CMD %MEM %CPU + 387992 387960 python -u bot_pdf_cleaner.p 20.9 0.0 + 5367 1 /usr/lib/systemd/systemd-jo 1.7 0.0 + 13352 1 /usr/bin/python3 /usr/bin/f 1.0 0.1 + 14276 1 /usr/bin/dockerd -H fd:// - 0.9 0.1 + 388027 388002 /usr/local/bin/python3.12 / 0.6 0.3 +``` + +Observation: +Самый прожорливый по памяти процесс — python -u bot_pdf_cleaner.p (20.9% RAM). Остальные сильно меньше (journald ~1.7%, какие-то python сервисы ~1%, dockerd ~0.9%). + +Command: + +```sh +ps -eo pid,ppid,cmd,%mem,%cpu --sort=-%cpu | head -n 6 +``` + +Output: + +```text + PID PPID CMD %MEM %CPU + 388027 388002 /usr/local/bin/python3.12 / 0.6 0.3 + 748516 1 /usr/libexec/packagekitd 0.2 0.2 + 13352 1 /usr/bin/python3 /usr/bin/f 1.0 0.1 + 748296 1 /usr/lib/systemd/systemd -- 0.1 0.1 + 14276 1 /usr/bin/dockerd -H fd:// - 0.9 0.1 +``` + +Observation: +По CPU сейчас вообще тихо: максимум 0.3% у python3.12 процесса (PID 388027). В целом это совпадает с маленьким load average. + +### 1.3 Service Dependencies + +Command: + +```sh +systemctl list-dependencies --no-pager +``` + +Output: + +```text +default.target +* |-display-manager.service +* |-systemd-update-utmp-runlevel.service +* `-multi-user.target +* |-acpid.service +* |-containerd.service +* |-cron.service +* |-dbus.service +* |-dmesg.service +* |-docker.service +* |-e2scrub_reap.service +* |-fail2ban.service +* |-grub-common.service +* |-grub-initrd-fallback.service +* |-networkd-dispatcher.service +* |-qemu-guest-agent.service +* |-rsyslog.service +* |-systemd-ask-password-wall.path +* |-systemd-logind.service +* |-systemd-networkd.service +* |-systemd-update-utmp-runlevel.service +* |-systemd-user-sessions.service +* |-tuned.service +* |-ufw.service +* |-basic.target +* | |--.mount +* | |-tmp.mount +* | |-paths.target +* | | `-acpid.path +* | |-slices.target +* | | |--.slice +* | | `-system.slice +* | |-sockets.target +* | | |-acpid.socket +* | | |-dbus.socket +* | | |-docker.socket +* | | |-iscsid.socket +* | | |-ssh.socket +* | | |-systemd-initctl.socket +* | | |-systemd-journald-dev-log.socket +* | | |-systemd-journald.socket +* | | |-systemd-pcrextend.socket +* | | |-systemd-sysext.socket +* | | |-systemd-udevd-control.socket +* | | `-systemd-udevd-kernel.socket +* | |-sysinit.target +* | | |-dev-hugepages.mount +* | | |-dev-mqueue.mount +* | | |-kmod-static-nodes.service +* | | |-ldconfig.service +* | | |-open-iscsi.service +* | | |-proc-sys-fs-binfmt_misc.automount +* | | |-sys-fs-fuse-connections.mount +* | | |-sys-kernel-config.mount +* | | |-sys-kernel-debug.mount +* | | |-sys-kernel-tracing.mount +* | | |-systemd-ask-password-console.path +* | | |-systemd-binfmt.service +* | | |-systemd-firstboot.service +* | | |-systemd-hwdb-update.service +* | | |-systemd-journal-catalog-update.service +* | | |-systemd-journal-flush.service +* | | |-systemd-journald.service +* | | |-systemd-machine-id-commit.service +* | | |-systemd-modules-load.service +* | | |-systemd-pcrmachine.service +* | | |-systemd-pcrphase-sysinit.service +* | | |-systemd-pcrphase.service +* | | |-systemd-pstore.service +* | | |-systemd-random-seed.service +* | | |-systemd-repart.service +* | | |-systemd-resolved.service +* | | |-systemd-sysctl.service +* | | |-systemd-sysusers.service +* | | |-systemd-timesyncd.service +* | | |-systemd-tmpfiles-setup-dev-early.service +* | | |-systemd-tmpfiles-setup-dev.service +* | | |-systemd-tmpfiles-setup.service +* | | |-systemd-tpm2-setup-early.service +* | | |-systemd-tpm2-setup.service +* | | |-systemd-udev-trigger.service +* | | |-systemd-udevd.service +* | | |-systemd-update-done.service +* | | |-systemd-update-utmp.service +* | | |-cryptsetup.target +* | | |-integritysetup.target +* | | |-local-fs.target +* | | | |--.mount +* | | | |-systemd-fsck-root.service +* | | | `-systemd-remount-fs.service +* | | |-swap.target +* | | | `-swapfile.swap +* | | `-veritysetup.target +* | `-timers.target +* | |-apt-daily-upgrade.timer +* | |-apt-daily.timer +* | |-dpkg-db-backup.timer +* | |-e2scrub_all.timer +* | |-fstrim.timer +* | |-motd-news.timer +* | `-systemd-tmpfiles-clean.timer +* |-cloud-init.target +* | |-cloud-config.service +* | |-cloud-final.service +* | |-cloud-init-hotplugd.socket +* | |-cloud-init-local.service +* | `-cloud-init.service +* |-getty.target +* | |-getty-static.service +* | |-getty@tty1.service +* | `-serial-getty@ttyS0.service +* `-remote-fs.target +``` + +Observation: +default.target ведёт к multi-user.target (и ещё есть display-manager.service, но судя по серверу он может быть просто в конфиге/пакетах). В multi-user.target видно типичный серверный набор: docker, containerd, ufw, fail2ban, cron, rsyslog, плюс qemu-guest-agent (ещё один признак виртуалки). + +Command: + +```sh +systemctl list-dependencies multi-user.target --no-pager +``` + +Output: + +```text +multi-user.target +* |-acpid.service +* |-containerd.service +* |-cron.service +* |-dbus.service +* |-dmesg.service +* |-docker.service +* |-e2scrub_reap.service +* |-fail2ban.service +* |-grub-common.service +* |-grub-initrd-fallback.service +* |-networkd-dispatcher.service +* |-qemu-guest-agent.service +* |-rsyslog.service +* |-systemd-ask-password-wall.path +* |-systemd-logind.service +* |-systemd-networkd.service +* |-systemd-update-utmp-runlevel.service +* |-systemd-user-sessions.service +* |-tuned.service +* |-ufw.service +* |-basic.target +* | |--.mount +* | |-tmp.mount +* | |-paths.target +* | | `-acpid.path +* | |-slices.target +* | | |--.slice +* | | `-system.slice +* | |-sockets.target +* | | |-acpid.socket +* | | |-dbus.socket +* | | |-docker.socket +* | | |-iscsid.socket +* | | |-ssh.socket +* | | |-systemd-initctl.socket +* | | |-systemd-journald-dev-log.socket +* | | |-systemd-journald.socket +* | | |-systemd-pcrextend.socket +* | | |-systemd-sysext.socket +* | | |-systemd-udevd-control.socket +* | | `-systemd-udevd-kernel.socket +* | |-sysinit.target +* | | |-dev-hugepages.mount +* | | |-dev-mqueue.mount +* | | |-kmod-static-nodes.service +* | | |-ldconfig.service +* | | |-open-iscsi.service +* | | |-proc-sys-fs-binfmt_misc.automount +* | | |-sys-fs-fuse-connections.mount +* | | |-sys-kernel-config.mount +* | | |-sys-kernel-debug.mount +* | | |-sys-kernel-tracing.mount +* | | |-systemd-ask-password-console.path +* | | |-systemd-binfmt.service +* | | |-systemd-firstboot.service +* | | |-systemd-hwdb-update.service +* | | |-systemd-journal-catalog-update.service +* | | |-systemd-journal-flush.service +* | | |-systemd-journald.service +* | | |-systemd-machine-id-commit.service +* | | |-systemd-modules-load.service +* | | |-systemd-pcrmachine.service +* | | |-systemd-pcrphase-sysinit.service +* | | |-systemd-pcrphase.service +* | | |-systemd-pstore.service +* | | |-systemd-random-seed.service +* | | |-systemd-repart.service +* | | |-systemd-resolved.service +* | | |-systemd-sysctl.service +* | | |-systemd-sysusers.service +* | | |-systemd-timesyncd.service +* | | |-systemd-tmpfiles-setup-dev-early.service +* | | |-systemd-tmpfiles-setup-dev.service +* | | |-systemd-tmpfiles-setup.service +* | | |-systemd-tpm2-setup-early.service +* | | |-systemd-tpm2-setup.service +* | | |-systemd-udev-trigger.service +* | | |-systemd-udevd.service +* | | |-systemd-update-done.service +* | | |-systemd-update-utmp.service +* | | |-cryptsetup.target +* | | |-integritysetup.target +* | | |-local-fs.target +* | | | |--.mount +* | | | |-systemd-fsck-root.service +* | | | `-systemd-remount-fs.service +* | | |-swap.target +* | | | `-swapfile.swap +* | | `-veritysetup.target +* | `-timers.target +* | |-apt-daily-upgrade.timer +* | |-apt-daily.timer +* | |-dpkg-db-backup.timer +* | |-e2scrub_all.timer +* | |-fstrim.timer +* | |-motd-news.timer +* | `-systemd-tmpfiles-clean.timer +* |-cloud-init.target +* | |-cloud-config.service +* | |-cloud-final.service +* | |-cloud-init-hotplugd.socket +* | |-cloud-init-local.service +* | `-cloud-init.service +* |-getty.target +* | |-getty-static.service +* | |-getty@tty1.service +* | `-serial-getty@ttyS0.service +* `-remote-fs.target +``` + +Observation: +Тут проще видно, что multi-user.target реально тянет почти все основные сервисы (сеть, ssh socket, docker, firewall). То есть “нормальный” серверный профиль. + +### 1.4 User Sessions + +Command: + +```sh +who -a +``` + +Output: + +```text + system boot Dec 25 16:36 + run-level 5 Dec 25 16:37 +LOGIN ttyS0 Dec 25 16:37 577 id=tyS0 +LOGIN tty1 Dec 25 16:37 576 id=tty1 +root - pts/0 Feb 26 20:05 . 748319 (31.57.61.235) + pts/1 Feb 26 20:08 748525 id=ts/1 term=0 exit=0 + pts/2 Dec 25 20:14 14787 id=ts/2 term=0 exit=0 + pts/3 Dec 25 21:02 16140 id=ts/3 term=0 exit=0 + pts/4 Dec 25 21:00 23743 id=ts/4 term=0 exit=0 + pts/5 Dec 25 21:22 24005 id=ts/5 term=0 exit=0 +``` + +Observation: +Видно system boot 25 Dec 16:36 и run-level 5 (как “графический”, хотя это сервер — возможно просто уровень по умолчанию). Активная сессия root сейчас на pts/0. Есть старые pts/2..pts/5 с Dec 25 (уже завершены). + +Command: + +```sh +last -n 5 +``` + +Output: + +```text +root pts/0 31.57.61.235 Thu Feb 26 20:05 still logged in +root pts/0 31.57.61.235 Mon Feb 23 14:48 - 14:49 (00:01) +root pts/0 31.57.61.235 Mon Feb 23 14:28 - 14:28 (00:00) +root pts/0 95.182.115.130 Tue Jan 20 11:06 - 12:21 (01:14) +root pts/0 95.182.115.130 Mon Jan 19 11:39 - 12:15 (00:36) + +wtmp begins Thu Dec 25 16:36:59 2025 +``` + +Observation: +Последние входы — root по SSH. Сейчас залогинен с 31.57.61.235. До этого были короткие сессии 23 Feb. В январе заходили с 95.182.115.130. Лог начался с момента поднятия системы (25 Dec 2025). + +### 1.5 Memory Analysis + +Command: + +```sh +free -h +``` + +Output: + +```text + total used free shared buff/cache available +Mem: 7.8Gi 2.2Gi 294Mi 3.8Mi 5.6Gi 5.5Gi +Swap: 9Gi 32Mi 9Gi +``` + +Observation: +RAM всего 7.8 GiB, при этом “used” 2.2 GiB, но большая часть в buff/cache (5.6 GiB) — это норм для Linux (кэш под файловую систему). available 5.5 GiB, то есть реально памяти достаточно. Swap почти не используется (32 MiB из 9 GiB). + +Command: + +```sh +cat /proc/meminfo | grep -e MemTotal -e SwapTotal -e MemAvailable +``` + +Output: + +```text +MemTotal: 8132324 kB +MemAvailable: 5813852 kB +SwapTotal: 10485756 kB +``` + +Observation: +MemAvailable ~5.8 GB (в kB), что совпадает с free -h (5.5 GiB available, разница из-за округления). + +### Answers / Summary + +What is the top memory-consuming process? +python -u bot_pdf_cleaner.p (PID 387992) — 20.9% MEM. + +Resource utilization patterns: +Система почти не нагружена по CPU (load average ~0.06). По памяти всё спокойно: много ушло в кэш, но available большой. Самый заметный потребитель RAM — один python-процесс (похоже мой сервис/бот), остальные процессы на фоне. + + +## Task 2 — Networking Analysis + +### 2.1 Network Path Tracing + +Command: +```sh +traceroute github.com +``` + +Output: + +```text +traceroute to github.com (140.82.113.4), 30 hops max, 60 byte packets + 1 _gateway (31.57.63.1) 1.976 ms 1.889 ms 1.846 ms + 2 * * * + 3 * * * + 4 * * * + 5 * * * + 6 eqix-dc5.github-2.com (206.126.237.205) 61.269 ms 60.060 ms 60.001 ms + 7 * * * + 8 * * * + 9 * * * +10 * * * +11 * * * +12 * * * +13 * * * +14 * * * +15 * * * +16 * * * +17 * * * +18 * * * +19 * * * +20 * * * +21 * * * +22 * * * +23 * * * +24 * * * +25 * * * +26 * * * +27 * * * +28 * * * +29 * * * +30 * * * +``` + +Observation: +Первый хоп — это локальный шлюз внутри сети провайдера/виртуалки (31.57.63.1) с небольшой задержкой ~2 мс. Дальше почти все хопы скрыты (звёздочки), то есть промежуточные маршрутизаторы не отвечают на ICMP/UDP traceroute или режут TTL exceeded. При этом на 6-м хопе виден узел GitHub на Equinix (eqix-dc5.github-2.com, 206.126.237.205) с RTT около 60 мс, то есть путь реально доходит до сети GitHub, просто большая часть трассы не светится. + +Command: + +```sh +dig github.com +``` + +Output: + +```text +; <<>> DiG 9.18.39-0ubuntu0.24.04.2-Ubuntu <<>> github.com +;; global options: +cmd +;; Got answer: +;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 49515 +;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 + +;; OPT PSEUDOSECTION: +; EDNS: version: 0, flags:; udp: 65494 +;; QUESTION SECTION: +;github.com. IN A + +;; ANSWER SECTION: +github.com. 9 IN A 140.82.113.4 + +;; Query time: 1 msec +;; SERVER: 127.0.0.53#53(127.0.0.53) (UDP) +;; WHEN: Thu Feb 26 20:19:30 UTC 2026 +;; MSG SIZE rcvd: 55 +``` + +Observation: +Резолвится A-запись github.com -> 140.82.113.4, статус NOERROR. DNS-сервер указан 127.0.0.53 — это локальный stub от systemd-resolved, который дальше уже форвардит запрос наружу. Время запроса 1 мс, похоже ответ пришёл быстро (возможно из кэша). + +### 2.2 Packet Capture + +Command: + +```sh +sudo timeout 10 tcpdump -c 5 -i any 'port 53' -nn +``` + +Output: + +```text +tcpdump: data link type LINUX_SLL2 +tcpdump: verbose output suppressed, use -v[v]... for full protocol decode +listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes +20:20:09.147672 lo In IP 127.0.0.1.11386 > 127.0.0.53.53: 16119+ [1au] A? github.com. (51) +20:20:09.148136 eth0 Out IP 31.57.63.237.28411 > 1.1.1.1.53: 22587+ [1au] A? github.com. (39) +20:20:09.150723 eth0 In IP 1.1.1.1.53 > 31.57.63.237.28411: 22587 1/0/1 A 140.82.113.4 (55) +20:20:09.150993 lo In IP 127.0.0.53.53 > 127.0.0.1.11386: 16119 1/0/1 A 140.82.113.4 (55) +20:20:11.304994 lo In IP 127.0.0.1.41099 > 127.0.0.53.53: 27926+ [1au] A? github.com. (51) +5 packets captured +11 packets received by filter +0 packets dropped by kernel +``` + +Analysis of DNS query/response patterns: +Тут видно типичный паттерн systemd-resolved: приложение делает запрос на 127.0.0.53 (через lo), затем systemd-resolved отправляет реальный DNS-запрос наружу (eth0) на 1.1.1.1:53, получает ответ обратно от 1.1.1.1 и возвращает ответ приложению обратно на loopback. То есть локальный stub выступает прокси между приложением и внешним резолвером. + +One example DNS query from packet capture (sanitized): + +```text +20:20:09.148136 eth0 Out IP .28411 > 1.1.1.1.53: 22587+ [1au] A? github.com. (39) +``` + +### 2.3 Reverse DNS + +Command: + +```sh +dig -x 8.8.4.4 +``` + +Output: + +```text +; <<>> DiG 9.18.39-0ubuntu0.24.04.2-Ubuntu <<>> -x 8.8.4.4 +;; global options: +cmd +;; Got answer: +;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 61260 +;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 + +;; OPT PSEUDOSECTION: +; EDNS: version: 0, flags:; udp: 65494 +;; QUESTION SECTION: +;4.4.8.8.in-addr.arpa. IN PTR + +;; ANSWER SECTION: +4.4.8.8.in-addr.arpa. 78896 IN PTR dns.google. + +;; Query time: 5 msec +;; SERVER: 127.0.0.53#53(127.0.0.53) (UDP) +;; WHEN: Thu Feb 26 20:20:21 UTC 2026 +;; MSG SIZE rcvd: 73 +``` + +Command: + +```sh +dig -x 1.1.2.2 +``` + +Output: + +```text +; <<>> DiG 9.18.39-0ubuntu0.24.04.2-Ubuntu <<>> -x 1.1.2.2 +;; global options: +cmd +;; Got answer: +;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 40008 +;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1 + +;; OPT PSEUDOSECTION: +; EDNS: version: 0, flags:; udp: 65494 +;; QUESTION SECTION: +;2.2.1.1.in-addr.arpa. IN PTR + +;; AUTHORITY SECTION: +1.in-addr.arpa. 3600 IN SOA ns.apnic.net. read-txt-record-of-zone-first-dns-admin.apnic.net. 23597 7200 1800 604800 3600 + +;; Query time: 197 msec +;; SERVER: 127.0.0.53#53(127.0.0.53) (UDP) +;; WHEN: Thu Feb 26 20:20:21 UTC 2026 +;; MSG SIZE rcvd: 137 +``` + +Comparison of reverse lookup results: +Для 8.8.4.4 PTR-запись есть и нормально резолвится в dns.google (NOERROR). Для 1.1.2.2 PTR-записи нет (NXDOMAIN) — в ответе пришёл SOA для зоны 1.in-addr.arpa, что выглядит как “официально не существует записи” и поэтому обратное имя не выдаётся. Также заметно, что NXDOMAIN отвечал дольше (197 мс), чем успешный PTR для Google (5 мс). + diff --git a/labs/submission5.md b/labs/submission5.md new file mode 100644 index 000000000..aa1225a16 --- /dev/null +++ b/labs/submission5.md @@ -0,0 +1,360 @@ + +## Task 1 — Установка VirtualBox + +### Операционная система хоста + +Информация об операционной системе была получена с помощью команды PowerShell. + +Использованная команда: + +``` + +systeminfo + +``` + +Соответствующий вывод: + +``` + +Operating system: Windows 11 +Operating system version: 10.0.22631.5189 + +``` + +--- + +### Версия VirtualBox + +VirtualBox был установлен с официального сайта с использованием стандартного установщика и настроек по умолчанию. + +Использованная команда: + +``` + +"C:\Program Files\Oracle\VirtualBox\VBoxManage.exe" --version + +``` + +Вывод команды: + +``` + +7.2.4r170995 + +``` + +### Примечания по установке + +VirtualBox был успешно установлен с использованием стандартных параметров установщика. +Во время установки никаких ошибок не возникло. + + + +## Task 2 — Ubuntu VM и анализ системы + +### Конфигурация виртуальной машины + +При создании виртуальной машины были заданы следующие параметры: + +- RAM: 10 GB +- CPU: 6 cores +- Storage: 25+ GB +- OS: Ubuntu 24.04 LTS + + + +## CPU information + +Использованная команда: + +``` + +lscpu + +``` + +Вывод команды: + +``` + +Architecture: x86_64 +CPU op-mode(s): 32-bit, 64-bit +Address sizes: 39 bits physical, 48 bits virtual +Byte Order: Little Endian +CPU(s): 6 +On-line CPU(s) list: 0-5 +Vendor ID: GenuineIntel +Model name: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz +CPU family: 6 +Model: 141 +Thread(s) per core: 1 +Core(s) per socket: 6 +Socket(s): 1 +Stepping: 1 +BogoMIPS: 4608.00 +Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pg +e mca cmov pat pse36 clflush mmx fxsr sse sse2 ht s +yscall nx rdtscp lm constant_tsc rep_good nopl xtop +ology nonstop_tsc cpuid tsc_known_freq pni pclmulqd +q ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe po +pcnt aes xsave avx f16c rdrand hypervisor lahf_lm a +bm 3dnowprefetch fsgsbase bmi1 avx2 bmi2 invpcid rd +seed adx clflushopt sha_ni arat md_clear flush_l1d +arch_capabilities +Virtualization features: +Hypervisor vendor: KVM +Virtualization type: full +Caches (sum of all): +L1d: 288 KiB (6 instances) +L1i: 192 KiB (6 instances) +L2: 7.5 MiB (6 instances) +L3: 144 MiB (6 instances) +NUMA: +NUMA node(s): 1 +NUMA node0 CPU(s): 0-5 + +``` + +## Memory information + +Использованная команда: + +``` + +free -h + +``` + +Вывод: + + +``` + total used free shared buff/cache available + + +Mem: 9.9Gi 1.9Gi 351Mi 216Mi 7.9Gi 8.0Gi +Swap: 0B 0B 0B + +``` + +Дополнительно использована команда: + +``` + +cat /proc/meminfo + +``` + +Вывод: + +``` + +MemTotal: 10386876 kB +MemFree: 359508 kB +MemAvailable: 8364116 kB +Buffers: 11920 kB +Cached: 8222964 kB +SwapCached: 0 kB +Active: 2617848 kB +Inactive: 6976056 kB +Active(anon): 1340080 kB +Inactive(anon): 61988 kB +Active(file): 1277768 kB +Inactive(file): 6914068 kB +Unevictable: 0 kB +Mlocked: 0 kB +SwapTotal: 0 kB +SwapFree: 0 kB + +``` + +--- + +## Network configuration + +Использованная команда: + +``` + +ip a + +``` + +Вывод: + +``` + +1: lo: mtu 65536 +inet 127.0.0.1/8 scope host lo + +2: enp0s3: mtu 1500 +inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic enp0s3 + +``` + +Дополнительно использована команда: + +``` + +ip route + +``` + +Вывод: + +``` + +default via 10.0.2.2 dev enp0s3 proto dhcp src 10.0.2.15 metric 100 +10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.15 metric 100 + +``` + +--- + +## Storage information + +Использованная команда: + +``` + +df -h + +``` + +Вывод: + +``` + +Filesystem Size Used Avail Use% Mounted on +tmpfs 1015M 1.8M 1013M 1% /run +/dev/sr0 6.2G 6.2G 0 100% /cdrom +/cow 5.0G 173M 4.8G 4% / +tmpfs 5.0G 8.0K 5.0G 1% /dev/shm + +``` + +Дополнительно использована команда: + +``` + +lsblk + +``` + +Вывод: + +``` + +NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS +sda 8:0 0 62.9G 0 disk +sr0 11:0 1 6.2G 0 rom /cdrom + +``` + +--- + +## Operating system information + +Использованная команда: + +``` + +lsb_release -a + +``` + +Вывод: + +``` + +Distributor ID: Ubuntu +Description: Ubuntu 24.04.4 LTS +Release: 24.04 +Codename: noble + +``` + +Дополнительно: + +``` + +uname -a + +``` + +Вывод: + +``` + +Linux ubuntu 6.17.0-14-generic #14~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Thu Jan 15 15:52:10 UTC 2 x86_64 x86_64 x86_64 GNU/Linux + +``` + +--- + +## Virtualization detection + +Использованная команда: + +``` + +systemd-detect-virt + +``` + +Вывод: + +``` + +oracle + +``` + +Это подтверждает, что система работает внутри VirtualBox. + +--- + +## System summary + +Использованная команда: + +``` + +hostnamectl + +``` + +Вывод: + +``` + +Static hostname: ubuntu +Virtualization: oracle +Operating System: Ubuntu 24.04.4 LTS +Kernel: Linux 6.17.0-14-generic +Architecture: x86-64 +Hardware Model: VirtualBox + +``` + +--- + +## Additional system information + +Использованные команды: + +``` + +uptime +whoami + +``` + +Вывод: + +``` + +19:01:46 up 34 min, 1 user, load average: 0.00, 0.00, 0.02 +ubuntu + diff --git a/labs/submission6.md b/labs/submission6.md new file mode 100644 index 000000000..d6f1cb3b0 --- /dev/null +++ b/labs/submission6.md @@ -0,0 +1,854 @@ +## Task 1 — Container Lifecycle & Image Management + +### 1.1 Basic Container Operations + +Сначала я посмотрела, какие контейнеры уже есть в системе. + +#### Command +```sh +docker ps -a +``` + +#### Output +```text +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +f5831db4fae7 amnezia-openvpn-cloak "dumb-init /opt/amne…" 2 weeks ago Up 7 days 0.0.0.0:43->443/tcp, [::]:43->443/tcp amnezia-openvpn-cloak +4ab953316938 amnezia-xray "dumb-init /opt/amne…" 2 weeks ago Up 7 days 0.0.0.0:321->321/tcp, [::]:321->321/tcp amnezia-xray +9c88e1508a41 amnezia-awg2 "dumb-init /opt/amne…" 2 weeks ago Up 7 days 0.0.0.0:433->433/udp, [::]:433->433/udp amnezia-awg2 +9542101897b0 pdf-web "bash -c 'uvicorn ap…" 7 weeks ago Up 7 days 0.0.0.0:8081->8000/tcp, [::]:8081->8000/tcp pdf-cleaner-web +4b91238d14c4 pdf-bot "bash -c 'python -u …" 7 weeks ago Up 7 days pdf-cleaner-bot +``` + +Дальше я скачала образ Ubuntu и посмотрела его в списке образов. + +#### Command +```sh +docker pull ubuntu:latest +``` + +#### Output +```text +latest: Pulling from library/ubuntu +01d7766a2e4a: Pull complete +fd8cda969ed2: Download complete +Digest: sha256:d1e2e92c075e5ca139d51a140fff46f84315c0fdce203eab2807c7e495eff4f9 +Status: Downloaded newer image for ubuntu:latest +docker.io/library/ubuntu:latest +``` + +#### Command +```sh +docker images ubuntu +``` + +#### Output +```text + i Info → U In Use +IMAGE ID DISK USAGE CONTENT SIZE EXTRA +ubuntu:latest d1e2e92c075e 119MB 31.7MB +``` + +После этого я посмотрела ID образа, его размер и число слоев. + +#### Command +```sh +docker image inspect ubuntu:latest --format='Image ID: {{.Id}} +Image size (bytes): {{.Size}} +Layer count: {{len .RootFS.Layers}}' +``` + +#### Output +```text +Image ID: sha256:d1e2e92c075e5ca139d51a140fff46f84315c0fdce203eab2807c7e495eff4f9 +Image size (bytes): 29737017 +Layer count: 1 +``` + +Дальше я запустила контейнер и посмотрела внутри версию ОС и процессы. + +#### Command +```sh +docker run --name ubuntu_container ubuntu:latest bash -lc 'cat /etc/os-release; echo; ps aux || (apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y procps && ps aux)' +``` + +#### Output +```text +PRETTY_NAME="Ubuntu 24.04.4 LTS" +NAME="Ubuntu" +VERSION_ID="24.04" +VERSION="24.04.4 LTS (Noble Numbat)" +VERSION_CODENAME=noble +ID=ubuntu +ID_LIKE=debian +HOME_URL="https://www.ubuntu.com/" +SUPPORT_URL="https://help.ubuntu.com/" +BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" +PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" +UBUNTU_CODENAME=noble +LOGO=ubuntu-logo + +USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND +root 1 35.7 0.0 4324 3456 ? Ss 17:07 0:00 bash -lc cat /etc/os-release; echo; ps aux || (apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y procps && ps aux) +root 10 0.0 0.0 7888 3968 ? R 17:07 0:00 ps aux +``` + +### 1.2 Image Export and Dependency Analysis + +После этого я сохранила образ в tar-файл и посмотрела его размер. + +#### Command +```sh +docker save -o ubuntu_image.tar ubuntu:latest +``` + +#### Output +```text +``` + +#### Command +```sh +ls -lh ubuntu_image.tar +``` + +#### Output +```text +-rw------- 1 root root 31M Mar 12 17:07 ubuntu_image.tar +``` + +Дальше я попробовала удалить образ, пока контейнер `ubuntu_container` еще существует. + +#### Command +```sh +docker rmi ubuntu:latest +``` + +#### Output +```text +Error response from daemon: conflict: unable to delete ubuntu:latest (must be forced) - container 98b5aa610810 is using its referenced image d1e2e92c075e +``` + +После этого я удалила контейнер и повторила удаление образа. + +#### Command +```sh +docker rm ubuntu_container +``` + +#### Output +```text +ubuntu_container +``` + +#### Command +```sh +docker rmi ubuntu:latest +``` + +#### Output +```text +Untagged: ubuntu:latest +Deleted: sha256:d1e2e92c075e5ca139d51a140fff46f84315c0fdce203eab2807c7e495eff4f9 +``` + +В конце я еще раз посмотрела список контейнеров. + +#### Command +```sh +docker ps -a +``` + +#### Output +```text +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +f5831db4fae7 amnezia-openvpn-cloak "dumb-init /opt/amne…" 2 weeks ago Up 7 days 0.0.0.0:43->443/tcp, [::]:43->443/tcp amnezia-openvpn-cloak +4ab953316938 amnezia-xray "dumb-init /opt/amne…" 2 weeks ago Up 7 days 0.0.0.0:321->321/tcp, [::]:321->321/tcp amnezia-xray +9c88e1508a41 amnezia-awg2 "dumb-init /opt/amne…" 2 weeks ago Up 7 days 0.0.0.0:433->433/udp, [::]:433->433/udp amnezia-awg2 +9542101897b0 pdf-web "bash -c 'uvicorn ap…" 7 weeks ago Up 7 days 0.0.0.0:8081->8000/tcp, [::]:8081->8000/tcp pdf-cleaner-web +4b91238d14c4 pdf-bot "bash -c 'python -u …" 7 weeks ago Up 7 days pdf-cleaner-bot +``` + +### Analysis + +Размер образа по `docker image inspect` — `29737017` байт. Размер файла `ubuntu_image.tar` — `31M`. Они близкие, но могут не совпадать точно. + +Количество слоев у образа — `1`. + +Первая попытка удалить образ завершилась ошибкой, потому что контейнер `ubuntu_container` все еще был создан на основе этого образа. Пока контейнер существует, Docker считает, что образ используется. + +То есть сначала нужно удалить контейнер, и только потом можно удалить образ. + +В `ubuntu_image.tar` входят слои образа, его конфигурация и метаданные, которые нужны для последующей загрузки через `docker load`. + +--- + +## Task 2 — Custom Image Creation & Analysis + +### 2.1 Deploy and Customize Nginx + +Сначала я убрала старые контейнеры и старый образ, чтобы не было конфликтов. + +#### Command +```sh +docker rm -f nginx_container my_website_container web web_new 2>/dev/null || true +``` + +#### Output +```text +``` + +#### Command +```sh +docker rmi -f my_website:latest 2>/dev/null || true +``` + +#### Output +```text +``` + +Дальше я скачала образ `nginx`. + +#### Command +```sh +docker pull nginx +``` + +#### Output +```text +Using default tag: latest +latest: Pulling from library/nginx +df9da45c1db2: Pull complete +18a071c04bd1: Pull complete +a9d395129dce: Pull complete +75a1d70aee50: Pull complete +206356c42440: Pull complete +79697674b897: Pull complete +9eef040df109: Pull complete +d99947bc9177: Download complete +23abb0f9ce55: Download complete +Digest: sha256:bc45d248c4e1d1709321de61566eb2b64d4f0e32765239d66573666be7f13349 +Status: Downloaded newer image for nginx:latest +docker.io/library/nginx:latest +``` + +После этого я запустила контейнер `nginx_container`. + +#### Command +```sh +docker run -d -p 80:80 --name nginx_container nginx +``` + +#### Output +```text +0c748e25536040e71cf97d4543d028e5d0d7ff3b093f81ce1867673f2cb5bae6 +``` + +Сразу после запуска я проверила страницу через `curl`. + +#### Command +```sh +curl http://localhost +``` + +#### Output +```text +curl: (56) Recv failure: Connection reset by peer +``` + +С первого раза ответ не пришел, потому что Nginx еще не успел нормально подняться. + +Теперь я создала свой `index.html`. + +#### Command +```sh +cat > index.html <<'EOF' + + +The best + + +

website

+ + +EOF +``` + +#### Output +```text +``` + +#### Command +```sh +cat index.html +``` + +#### Output +```text + + +The best + + +

website

+ + +``` + +Дальше я скопировала файл в контейнер. + +#### Command +```sh +docker cp index.html nginx_container:/usr/share/nginx/html/ +``` + +#### Output +```text +Successfully copied 2.05kB to nginx_container:/usr/share/nginx/html/ +``` + +После этого страница уже открывалась с моим содержимым. + +#### Command +```sh +curl http://localhost +``` + +#### Output +```text + + +The best + + +

website

+ + +``` + +### 2.2 Create and Test Custom Image + +После этого я сохранила текущее состояние контейнера в новый образ `my_website:latest`. + +#### Command +```sh +docker commit nginx_container my_website:latest +``` + +#### Output +```text +sha256:ece26b94835d6c1341b8d170572c4882a8c9caf5bd4f9d1543cd826b1a135565 +``` + +#### Command +```sh +docker images my_website +``` + +#### Output +```text + i Info → U In Use +IMAGE ID DISK USAGE CONTENT SIZE EXTRA +my_website:latest ece26b94835d 237MB 63MB +``` + +Дальше я удалила исходный контейнер и запустила новый уже из своего образа. + +#### Command +```sh +docker rm -f nginx_container +``` + +#### Output +```text +nginx_container +``` + +#### Command +```sh +docker run -d -p 80:80 --name my_website_container my_website:latest +``` + +#### Output +```text +7695b2a0bb0962a1111892e80f460600d505f0d3710a66b4050352ca31b54b72 +``` + +Сразу после запуска я еще раз проверила страницу. + +#### Command +```sh +curl http://localhost +``` + +#### Output +```text +curl: (56) Recv failure: Connection reset by peer +``` + +Дальше я посмотрела изменения в файловой системе контейнера. + +#### Command +```sh +docker diff my_website_container +``` + +#### Output +```text +C /run +C /run/nginx.pid +C /etc +C /etc/nginx +C /etc/nginx/conf.d +C /etc/nginx/conf.d/default.conf +``` + +### Analysis + +Я взяла стандартный образ `nginx`, заменила `index.html` внутри контейнера и проверила через `curl`, что теперь отдается мой HTML: + +```html + + +The best + + +

website

+ + +``` + +В выводе `docker diff` у всех строк стоит `C`, то есть `Changed`. Это значит, что эти файлы или директории были изменены по сравнению с исходным состоянием контейнера. + +Обозначения такие: +- `A` — added; +- `C` — changed; +- `D` — deleted. + +Плюсы `docker commit`: +- быстро; +- удобно для разового эксперимента; +- можно сразу сохранить текущее состояние контейнера. + +Минусы `docker commit`: +- плохо воспроизводится; +- не видно шагов сборки; +- неудобно поддерживать. + +Плюсы Dockerfile: +- все шаги сборки описаны явно; +- сборку легко повторить; +- удобно хранить в репозитории; +- лучше подходит для нормальной разработки. + +Минусы Dockerfile: +- нужно заранее описывать шаги; +- для быстрых экспериментов он менее удобен. + +Для нормальной работы лучше Dockerfile, а `docker commit` удобен для быстрого эксперимента. + +--- + +## Task 3 — Container Networking & Service Discovery + +### 3.1 Create Custom Network + +Сначала я удалила старые контейнеры и сеть. + +#### Command +```sh +docker rm -f container1 container2 2>/dev/null || true +docker network rm lab_network 2>/dev/null || true +``` + +#### Output +```text +``` + +Дальше я скачала образ `alpine`. + +#### Command +```sh +docker pull alpine +``` + +#### Output +```text +Using default tag: latest +latest: Pulling from library/alpine +589002ba0eae: Pull complete +9e595aac14e0: Download complete +caa817ad3aea: Download complete +Digest: sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659 +Status: Downloaded newer image for alpine:latest +docker.io/library/alpine:latest +``` + +После этого я создала сеть `lab_network`. + +#### Command +```sh +docker network create lab_network +``` + +#### Output +```text +303794bd28bd1cfadd649fb3b93f504577d0f6821b71bf401e05deebd4419a35 +``` + +Потом я посмотрела список сетей. + +#### Command +```sh +docker network ls +``` + +#### Output +```text +NETWORK ID NAME DRIVER SCOPE +9da3e9efa4fa amnezia-dns-net bridge local +c4865cfd80ed bridge bridge local +b1db0e87d9dd host host local +303794bd28bd lab_network bridge local +4bbb14d9c1d5 none null local +e05dd8261414 pdf_default bridge local +``` + +Дальше я запустила два контейнера в этой сети. + +#### Command +```sh +docker run -dit --network lab_network --name container1 alpine ash +``` + +#### Output +```text +6323ed23d60c908ba2af2d9ae67c2a26f066e025505133cbd4d97d4fe3935cb9 +``` + +#### Command +```sh +docker run -dit --network lab_network --name container2 alpine ash +``` + +#### Output +```text +e8d7b61eb9f2c6b91ca102022bfd2c30fefeaea6c229c60587470febec19e82e +``` + +### 3.2 Test Connectivity and DNS + +После этого я проверила связь между контейнерами. + +#### Command +```sh +docker exec container1 ping -c 3 container2 +``` + +#### Output +```text +PING container2 (172.19.0.3): 56 data bytes +64 bytes from 172.19.0.3: seq=0 ttl=64 time=0.127 ms +64 bytes from 172.19.0.3: seq=1 ttl=64 time=0.099 ms +64 bytes from 172.19.0.3: seq=2 ttl=64 time=0.132 ms + +--- container2 ping statistics --- +3 packets transmitted, 3 packets received, 0% packet loss +round-trip min/avg/max = 0.099/0.119/0.132 ms +``` + +Потом я посмотрела параметры сети и IP-адреса контейнеров. + +#### Command +```sh +docker network inspect lab_network +``` + +#### Output +```text +[ + { + "Name": "lab_network", + "Id": "303794bd28bd1cfadd649fb3b93f504577d0f6821b71bf401e05deebd4419a35", + "Created": "2026-03-12T17:13:35.825908407Z", + "Scope": "local", + "Driver": "bridge", + "EnableIPv4": true, + "EnableIPv6": false, + "IPAM": { + "Driver": "default", + "Options": {}, + "Config": [ + { + "Subnet": "172.19.0.0/16", + "IPRange": "", + "Gateway": "172.19.0.1" + } + ] + }, + "Internal": false, + "Attachable": false, + "Ingress": false, + "ConfigFrom": { + "Network": "" + }, + "ConfigOnly": false, + "Options": {}, + "Labels": {}, + "Containers": { + "6323ed23d60c908ba2af2d9ae67c2a26f066e025505133cbd4d97d4fe3935cb9": { + "Name": "container1", + "EndpointID": "c40eb543f448e8c0f956ee361675a907c8d7b396824c007ab58cd0d546c4fff8", + "MacAddress": "f6:50:5b:43:34:4b", + "IPv4Address": "172.19.0.2/16", + "IPv6Address": "" + }, + "e8d7b61eb9f2c6b91ca102022bfd2c30fefeaea6c229c60587470febec19e82e": { + "Name": "container2", + "EndpointID": "ca3ac2ccc400e058cb9ea4da7f0de784af9bfdccd5be982f9ac3bdff9ad785da", + "MacAddress": "d6:3b:37:e3:d3:07", + "IPv4Address": "172.19.0.3/16", + "IPv6Address": "" + } + }, + "Status": { + "IPAM": { + "Subnets": { + "172.19.0.0/16": { + "IPsInUse": 5, + "DynamicIPsAvailable": 65531 + } + } + } + } + } +] +``` + +После этого я проверила DNS-разрешение имени `container2`. + +#### Command +```sh +docker exec container1 nslookup container2 +``` + +#### Output +```text +Server: 127.0.0.11 +Address: 127.0.0.11:53 + +Non-authoritative answer: +Name: container2 +Address: 172.19.0.3 + +Non-authoritative answer: +``` + +### Analysis + +Пинг прошел успешно, значит контейнеры в одной сети видят друг друга. + +По `docker network inspect` видно, что: +- `container1` получил `172.19.0.2/16`; +- `container2` получил `172.19.0.3/16`. + +По `nslookup` видно, что имя `container2` разрешается во внутренний IP `172.19.0.3`. + +Это работает за счет внутреннего DNS Docker. В пользовательской сети Docker сам связывает имена контейнеров с их IP-адресами, поэтому можно обращаться по имени, а не по IP. + +Плюсы user-defined bridge network по сравнению с обычной `bridge`: +- контейнеры могут обращаться друг к другу по имени; +- сеть лучше изолирована; +- удобнее управлять контейнерами; +- не нужно вручную искать IP. + +--- + +## Task 4 — Data Persistence with Volumes + +### 4.1 Create and Use Volume + +Сначала я удалила старые контейнеры, которые могли занимать порт `80`. + +#### Command +```sh +docker rm -f my_website_container web web_new 2>/dev/null || true +``` + +#### Output +```text +my_website_container +``` + +Дальше я пересоздала volume `app_data`. + +#### Command +```sh +docker volume rm app_data 2>/dev/null || true +docker volume create app_data +``` + +#### Output +```text +app_data +``` + +После этого я посмотрела список volume. + +#### Command +```sh +docker volume ls +``` + +#### Output +```text +DRIVER VOLUME NAME +local 8e6851f21fdb7bd9a3d02d66359a0086b9d34a2217abb2f638970854c805927d +local 34b72800effbac4db81f08e2cee5e842fd46ecf21234b2bafc9571b3e350eef4 +local 685e3588d2909acf742627298325ce90dc7932c37a7302282170f89e2f1c3632 +local 1810c51bd27622450c5f5ba61be8a6435b2991fc30230b2bc942fd6d20db9478 +local 9465cecc5c6a85bfc766c6b63b978738b3eb2b3f1d328134dc94868cba496143 +local app_data +local b57fab918fe65891a1fa47bf6163b57d784a12ce24dff885a351c67b8cffd173 +local d28e526c4b534b7370955145ec0a3c27ab9a62f4e60efde667ed1cafd3db3c2e +local fa7264ed72506c0edd3d1100596f0169cb03d631603537c902c2280315bf4855 +``` + +Дальше я запустила контейнер `web` с volume `app_data`. + +#### Command +```sh +docker run -d -p 80:80 -v app_data:/usr/share/nginx/html --name web nginx +``` + +#### Output +```text +237bb57ee87581bfc2ccacb1835d933bf0184f14f48a148635aa713cb8a2c158 +``` + +После этого я создала свой `index.html`. + +#### Command +```sh +cat > index.html <<'EOF' +

Persistent Data

+EOF +``` + +#### Output +```text +``` + +#### Command +```sh +cat index.html +``` + +#### Output +```text +

Persistent Data

+``` + +Потом я скопировала этот файл в контейнер. + +#### Command +```sh +docker cp index.html web:/usr/share/nginx/html/ +``` + +#### Output +```text +Successfully copied 2.05kB to web:/usr/share/nginx/html/ +``` + +После этого я проверила содержимое через `curl`. + +#### Command +```sh +curl http://localhost +``` + +#### Output +```text +

Persistent Data

+``` + +### 4.2 Verify Persistence + +Дальше я остановила и удалила контейнер `web`. + +#### Command +```sh +docker stop web && docker rm web +``` + +#### Output +```text +web +web +``` + +После этого я создала новый контейнер `web_new` с тем же volume `app_data`. + +#### Command +```sh +docker run -d -p 80:80 -v app_data:/usr/share/nginx/html --name web_new nginx +``` + +#### Output +```text +da08d824cd7b222447ade7692984677ab5e221b7d11176e34baf640ecee3b667 +``` + +Потом я снова проверила страницу. + +#### Command +```sh +curl http://localhost +``` + +#### Output +```text +

Persistent Data

+``` + +В конце я посмотрела информацию о volume. + +#### Command +```sh +docker volume inspect app_data +``` + +#### Output +```text +[ + { + "CreatedAt": "2026-03-12T17:15:27Z", + "Driver": "local", + "Labels": null, + "Mountpoint": "/var/lib/docker/volumes/app_data/_data", + "Name": "app_data", + "Options": null, + "Scope": "local" + } +] +``` + +### Analysis + +Для проверки я использовала такой HTML: + +```html +

Persistent Data

+``` + +После удаления контейнера `web` и запуска `web_new` данные не исчезли. Значит, файл сохранился не внутри контейнера, а во внешнем volume `app_data`. + +Смысл volume в том, что данные живут отдельно от контейнера. Контейнер можно удалить и создать заново, а данные останутся. + +Это важно для приложений, которым нужно сохранять файлы, конфиги, базу данных и другие данные между перезапусками. + +Разница такая: +- `volumes` — отдельное хранилище Docker, подходит для постоянных данных; +- `bind mounts` — папка с хоста, удобно в разработке; +- `container storage` — обычный слой контейнера, исчезает вместе с ним. + +Для постоянных данных правильнее использовать volume. diff --git a/labs/submission7.md b/labs/submission7.md new file mode 100644 index 000000000..437bea393 --- /dev/null +++ b/labs/submission7.md @@ -0,0 +1,340 @@ +# Lab 7 — GitOps Fundamentals + +## Task 1 — Git State Reconciliation + +### 1.1 Initial desired state and current state + +`desired-state.txt` + +```text +version: 1.0 +app: myapp +replicas: 3 +```` + +`current-state.txt` + +```text +version: 1.0 +app: myapp +replicas: 3 +``` + +Изначально текущее состояние было синхронизировано с желаемым, то есть `current-state.txt` был создан как копия `desired-state.txt`. + +--- + +### 1.2 Reconciliation script + +`reconcile.sh` + +```bash +#!/bin/bash +# reconcile.sh - GitOps reconciliation loop + +DESIRED=$(cat desired-state.txt) +CURRENT=$(cat current-state.txt) + +if [ "$DESIRED" != "$CURRENT" ]; then + echo "$(date) - ⚠️ DRIFT DETECTED!" + echo "Reconciling current state with desired state..." + cp desired-state.txt current-state.txt + echo "$(date) - ✅ Reconciliation complete" +else + echo "$(date) - ✅ States synchronized" +fi +``` + +Скрипт сравнивает желаемое состояние с текущим. Если есть расхождение, он считает это drift и возвращает текущее состояние к содержимому `desired-state.txt`. + +--- + +### 1.3 Manual drift detection and reconciliation + +Сначала drift был создан вручную изменением `current-state.txt`. + +`current-state.txt` before reconciliation: + +```text +version: 2.0 +app: myapp +replicas: 5 +``` + +Output of manual reconciliation: + +```text +Thu Mar 19 16:52:29 UTC 2026 - ⚠️ DRIFT DETECTED! +Reconciling current state with desired state... +Thu Mar 19 16:52:29 UTC 2026 - ✅ Reconciliation complete +``` + +Output of diff after reconciliation: + +```text +diff exit code: 0 +``` + +Код возврата `0` означает, что после выполнения `reconcile.sh` файлы `desired-state.txt` и `current-state.txt` снова стали одинаковыми. + +`current-state.txt` after reconciliation: + +```text +version: 1.0 +app: myapp +replicas: 3 +``` + +--- + +### 1.4 Continuous reconciliation and auto-healing + +Для имитации непрерывной работы GitOps-оператора был запущен цикл: + +```bash +watch -n 5 ./reconcile.sh +``` + +После этого в другом терминале в `current-state.txt` была внесена несанкционированная правка. Цикл автоматически обнаружил drift и восстановил состояние. + +Output from continuous reconciliation loop: + +```text +Every 5.0s: ./reconcile.sh server-3-pdf: Thu Mar 19 16:53:35 2026 + +Thu Mar 19 16:53:35 UTC 2026 - ⚠️ DRIFT DETECTED! +Reconciling current state with desired state... +Thu Mar 19 16:53:35 UTC 2026 - ✅ Reconciliation complete +``` + +`current-state.txt` after auto-healing: + +```text +version: 1.0 +app: myapp +replicas: 3 +``` + +--- + +### Analysis + +Логика reconciliation loop здесь простая: есть файл с желаемым состоянием, который играет роль source of truth, и есть файл с текущим состоянием. Скрипт регулярно сравнивает их содержимое. Если обнаруживается расхождение, текущее состояние принудительно приводится к желаемому. По сути это и есть базовая идея GitOps: система не ждет ручного вмешательства, а сама постоянно сверяет реальное состояние с тем, что описано декларативно. + +Такой подход хорошо предотвращает configuration drift. Даже если кто-то вручную поменял состояние, эти изменения не считаются легитимными, пока они не отражены в source of truth. Поэтому при следующей проверке система откатывает несанкционированные изменения и возвращается к корректной конфигурации. + +--- + +### Reflection + +Декларативная конфигурация в продакшене удобнее императивных команд по нескольким причинам. Во-первых, всегда есть одно понятное описание того, как система должна выглядеть. Во-вторых, это легче версионировать, ревьюить и откатывать через Git. В-третьих, декларативный подход лучше масштабируется: не нужно помнить последовательность ручных действий, достаточно хранить итоговое желаемое состояние. Плюс это уменьшает число человеческих ошибок, потому что система сама доводит инфраструктуру до нужного состояния, а не полагается на ручные шаги администратора. + +--- + +## Task 2 — GitOps Health Monitoring + +### 2.1 Health check script + +`healthcheck.sh` + +```bash +#!/bin/bash +# healthcheck.sh - Monitor GitOps sync health + +DESIRED_MD5=$(md5sum desired-state.txt | awk '{print $1}') +CURRENT_MD5=$(md5sum current-state.txt | awk '{print $1}') + +if [ "$DESIRED_MD5" != "$CURRENT_MD5" ]; then + echo "$(date) - ❌ CRITICAL: State mismatch detected!" | tee -a health.log + echo " Desired MD5: $DESIRED_MD5" | tee -a health.log + echo " Current MD5: $CURRENT_MD5" | tee -a health.log +else + echo "$(date) - ✅ OK: States synchronized" | tee -a health.log +fi +``` + +Скрипт считает MD5-хэши для желаемого и текущего состояний. Если хэши отличаются, значит содержимое файлов различается, и это фиксируется как критическое отклонение в `health.log`. + +--- + +### 2.2 Testing health monitoring + +#### Healthy state + +Output of healthy state check: + +```text +Thu Mar 19 16:54:35 UTC 2026 - ✅ OK: States synchronized +``` + +`health.log` after healthy check: + +```text +Thu Mar 19 16:54:35 UTC 2026 - ✅ OK: States synchronized +``` + +#### Drifted state + +Для проверки drift в `current-state.txt` была добавлена строка: + +```text +unapproved-change: true +``` + +`current-state.txt` with drift: + +```text +version: 1.0 +app: myapp +replicas: 3 +unapproved-change: true +``` + +Output of health check on drifted state: + +```text +Thu Mar 19 16:54:35 UTC 2026 - ❌ CRITICAL: State mismatch detected! + Desired MD5: a15a1a4f965ecd8f9e23a33a6b543155 + Current MD5: 48168ff3ab5ffc0214e81c7e2ee356f5 +``` + +`health.log` after drift detection: + +```text +Thu Mar 19 16:54:35 UTC 2026 - ✅ OK: States synchronized +Thu Mar 19 16:54:35 UTC 2026 - ❌ CRITICAL: State mismatch detected! + Desired MD5: a15a1a4f965ecd8f9e23a33a6b543155 + Current MD5: 48168ff3ab5ffc0214e81c7e2ee356f5 +``` + +#### Fixing drift and verifying health again + +Output after reconciliation and repeated health check: + +```text +Thu Mar 19 16:54:35 UTC 2026 - ⚠️ DRIFT DETECTED! +Reconciling current state with desired state... +Thu Mar 19 16:54:35 UTC 2026 - ✅ Reconciliation complete +Thu Mar 19 16:54:35 UTC 2026 - ✅ OK: States synchronized +``` + +`health.log` after fix: + +```text +Thu Mar 19 16:54:35 UTC 2026 - ✅ OK: States synchronized +Thu Mar 19 16:54:35 UTC 2026 - ❌ CRITICAL: State mismatch detected! + Desired MD5: a15a1a4f965ecd8f9e23a33a6b543155 + Current MD5: 48168ff3ab5ffc0214e81c7e2ee356f5 +Thu Mar 19 16:54:35 UTC 2026 - ✅ OK: States synchronized +``` + +--- + +### 2.3 Continuous health monitoring + +`monitor.sh` + +```bash +#!/bin/bash +# monitor.sh - Combined reconciliation and health monitoring + +echo "Starting GitOps monitoring..." +for i in {1..10}; do + echo + echo "--- Check #$i ---" + ./healthcheck.sh + ./reconcile.sh + sleep 3 +done +``` + +Output of `monitor.sh`: + +```text +Starting GitOps monitoring... + +--- Check #1 --- +Thu Mar 19 16:54:35 UTC 2026 - ✅ OK: States synchronized +Thu Mar 19 16:54:35 UTC 2026 - ✅ States synchronized + +--- Check #2 --- +Thu Mar 19 16:54:38 UTC 2026 - ✅ OK: States synchronized +Thu Mar 19 16:54:38 UTC 2026 - ✅ States synchronized + +--- Check #3 --- +Thu Mar 19 16:54:41 UTC 2026 - ✅ OK: States synchronized +Thu Mar 19 16:54:41 UTC 2026 - ✅ States synchronized + +--- Check #4 --- +Thu Mar 19 16:54:44 UTC 2026 - ✅ OK: States synchronized +Thu Mar 19 16:54:44 UTC 2026 - ✅ States synchronized + +--- Check #5 --- +Thu Mar 19 16:54:47 UTC 2026 - ✅ OK: States synchronized +Thu Mar 19 16:54:47 UTC 2026 - ✅ States synchronized + +--- Check #6 --- +Thu Mar 19 16:54:50 UTC 2026 - ✅ OK: States synchronized +Thu Mar 19 16:54:50 UTC 2026 - ✅ States synchronized + +--- Check #7 --- +Thu Mar 19 16:54:53 UTC 2026 - ✅ OK: States synchronized +Thu Mar 19 16:54:53 UTC 2026 - ✅ States synchronized + +--- Check #8 --- +Thu Mar 19 16:54:56 UTC 2026 - ✅ OK: States synchronized +Thu Mar 19 16:54:56 UTC 2026 - ✅ States synchronized + +--- Check #9 --- +Thu Mar 19 16:54:59 UTC 2026 - ✅ OK: States synchronized +Thu Mar 19 16:54:59 UTC 2026 - ✅ States synchronized + +--- Check #10 --- +Thu Mar 19 16:55:02 UTC 2026 - ✅ OK: States synchronized +Thu Mar 19 16:55:02 UTC 2026 - ✅ States synchronized +``` + +Complete `health.log`: + +```text +Thu Mar 19 16:54:35 UTC 2026 - ✅ OK: States synchronized +Thu Mar 19 16:54:35 UTC 2026 - ❌ CRITICAL: State mismatch detected! + Desired MD5: a15a1a4f965ecd8f9e23a33a6b543155 + Current MD5: 48168ff3ab5ffc0214e81c7e2ee356f5 +Thu Mar 19 16:54:35 UTC 2026 - ✅ OK: States synchronized +Thu Mar 19 16:54:35 UTC 2026 - ✅ OK: States synchronized +Thu Mar 19 16:54:38 UTC 2026 - ✅ OK: States synchronized +Thu Mar 19 16:54:41 UTC 2026 - ✅ OK: States synchronized +Thu Mar 19 16:54:44 UTC 2026 - ✅ OK: States synchronized +Thu Mar 19 16:54:47 UTC 2026 - ✅ OK: States synchronized +Thu Mar 19 16:54:50 UTC 2026 - ✅ OK: States synchronized +Thu Mar 19 16:54:53 UTC 2026 - ✅ OK: States synchronized +Thu Mar 19 16:54:56 UTC 2026 - ✅ OK: States synchronized +Thu Mar 19 16:54:59 UTC 2026 - ✅ OK: States synchronized +Thu Mar 19 16:55:02 UTC 2026 - ✅ OK: States synchronized +``` + +--- + +### Analysis + +Checksums нужны для быстрого контроля целостности и синхронности состояния. Если содержимое двух файлов отличается хотя бы на один символ, их MD5-хэши будут разными. За счет этого можно очень быстро понять, совпадает текущее состояние с желаемым или нет, не разбирая файл построчно вручную. Для такой учебной задачи этого более чем достаточно. + +При этом важно понимать, что MD5 здесь используется не как криптографическая защита, а именно как простой механизм сравнения состояний. В GitOps-сценарии это похоже на быстрый индикатор того, что live state ушел от desired state. + +--- + +### Comparison with real GitOps tools + +Это прямо похоже на то, как работают инструменты вроде ArgoCD. У ArgoCD есть понятие `Sync Status`: он сравнивает состояние, описанное в Git, с тем, что реально существует в кластере. Если они совпадают, система находится в состоянии `Synced`. Если нет — состояние становится `OutOfSync`, и оператор может либо показать проблему, либо автоматически привести систему к нужному виду. + +В этой лабораторной мы сделали ту же самую идею, только в очень упрощенном виде и без Kubernetes. `desired-state.txt` играет роль Git-репозитория с эталонной конфигурацией, `current-state.txt` — роль live state, `reconcile.sh` — роль reconciliation loop, а `healthcheck.sh` — роль мониторинга статуса синхронизации. + +--- + +## Conclusion + +В ходе лабораторной были реализованы два ключевых принципа GitOps: автоматическая реконсиляция состояния и мониторинг синхронизации. В первой части удалось показать, как система обнаруживает drift и автоматически восстанавливает корректную конфигурацию. Во второй части был добавлен health monitoring через MD5-хэши и логирование, что позволило явно фиксировать как нормальное состояние, так и отклонения. + + + diff --git a/labs/submission8.md b/labs/submission8.md new file mode 100644 index 000000000..7f329539d --- /dev/null +++ b/labs/submission8.md @@ -0,0 +1,271 @@ +# Lab 8 - Site Reliability Engineering (SRE) + +## Task 1 - Key Metrics for SRE and System Analysis + +Для первой части я использовала VPS на Ubuntu. Сначала поставила нужные утилиты: + +```bash +sudo apt install htop sysstat iotop -y +``` + +После этого сняла состояние системы через `top`, `ps`, `iostat`, `pidstat`, `df`, `du` и `find`. + +### CPU, memory и общая нагрузка + +```bash +Sat May 9 10:55:57 UTC 2026 +10:55:57 up 3 days, 16:44, 2 users, load average: 0.10, 0.05, 0.05 + +Tasks: 145 total, 1 running, 144 sleeping, 0 stopped, 0 zombie +%Cpu(s): 4.3 us, 4.3 sy, 0.0 ni, 91.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st +MiB Mem : 3915.9 total, 1659.6 free, 530.2 used, 1949.2 buff/cache +MiB Swap: 0.0 total, 0.0 free, 0.0 used. 3385.7 avail Mem +``` + +По этим цифрам видно, что сервер почти не загружен: `load average` низкий, CPU idle около `91%`, swap не используется. + +### Top CPU consumers + +```bash +PID USER COMMAND %CPU %MEM +1361 root amneziawg-go 2.8 1.2 +264745 root sshd 2.1 0.2 +264755 root bash 0.7 0.0 +1332 root xray 0.3 0.8 +583 root containerd 0.2 1.3 +556 root fail2ban-server 0.2 2.5 +609 root dockerd 0.1 1.9 +``` + +Мой top-3 по CPU: + +1. `amneziawg-go` - `2.8%` +2. `sshd` - `2.1%` +3. `bash` - `0.7%` + +`sshd` и `bash` попали в список из-за моей SSH-сессии, поэтому из постоянных сервисов заметнее всего `amneziawg-go`, `xray`, Docker и `fail2ban`. + +### Top memory consumers + +```bash +PID USER COMMAND %CPU %MEM +226 root systemd-journal 0.0 3.8 +556 root fail2ban-server 0.2 2.5 +609 root dockerd 0.1 1.9 +583 root containerd 0.2 1.3 +1361 root amneziawg-go 2.8 1.2 +1332 root xray 0.3 0.8 +568 root tuned 0.0 0.7 +``` + +Мой top-3 по памяти: + +1. `systemd-journal` - `3.8%` +2. `fail2ban-server` - `2.5%` +3. `dockerd` - `1.9%` + +Памяти хватает с запасом: занято около `530 MiB` из `3915.9 MiB`. + +### I/O usage + +Для диска я посмотрела `iostat -x 1 5`: + +```bash +avg-cpu: %user %nice %system %iowait %steal %idle + 0.87 0.00 0.92 0.01 1.08 97.11 + +Device r/s rkB/s w/s wkB/s w_await aqu-sz %util +vda 0.03 4.57 3.50 21.65 0.57 0.00 0.04 + +avg-cpu: %user %nice %system %iowait %steal %idle + 0.50 0.00 1.01 0.00 1.51 96.98 + +Device r/s rkB/s w/s wkB/s w_await aqu-sz %util +vda 0.00 0.00 1.00 8.00 0.00 0.00 0.00 +``` + +Потом проверила процессы, которые писали на диск: + +```bash +Average: UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command +Average: 0 164 0.00 15.17 0.00 0 jbd2/vda1-8 +Average: 0 226 0.00 7.98 0.00 0 systemd-journal +Average: 0 556 0.00 2.40 0.00 0 fail2ban-server +Average: 102 595 0.00 0.80 0.00 0 rsyslogd +``` + +Мой top-3 по I/O: + +1. `jbd2/vda1-8` - `15.17 kB_wr/s` +2. `systemd-journal` - `7.98 kB_wr/s` +3. `fail2ban-server` - `2.40 kB_wr/s` + +Сильной дисковой нагрузки не было. В основном это обычные записи логов и журналирование файловой системы. + +### Disk space + +```bash +Filesystem Size Used Avail Use% Mounted on +tmpfs 392M 1.3M 391M 1% /run +/dev/vda1 38G 5.7G 31G 16% / +tmpfs 2.0G 0 2.0G 0% /dev/shm +tmpfs 5.0M 0 5.0M 0% /run/lock +tmpfs 392M 8.0K 392M 1% /run/user/0 +``` + +Место на диске сейчас не проблема: корневой раздел занят только на `16%`. + +Самые большие директории в `/var`: + +```bash +4.4G /var +3.2G /var/log +2.0G /var/log/journal/0f005944d3d34ff48780f21b3a552e1a +2.0G /var/log/journal +1.1G /var/lib +897M /var/lib/docker +894M /var/lib/docker/overlay2 +190M /var/cache +``` + +Самые большие файлы в `/var`: + +```bash +373M /var/log/btmp +339M /var/log/auth.log +210M /var/log/fail2ban.log +84M /var/log/syslog +82M /var/log/ufw.log +82M /var/log/kern.log +70M /var/lib/apt/lists/nova.clouds.archive.ubuntu.com_ubuntu_dists_noble_universe_binary-amd64_Packages +57M /var/cache/apt/srcpkgcache.bin +57M /var/cache/apt/pkgcache.bin +49M /var/log/journal/0f005944d3d34ff48780f21b3a552e1a/system.journal +``` + +Top-3 largest files: + +1. `/var/log/btmp` - `373M` +2. `/var/log/auth.log` - `339M` +3. `/var/log/fail2ban.log` - `210M` + +### Analysis + +В момент проверки сервер работал спокойно. CPU, RAM и disk I/O не были близко к лимитам. Больше всего внимания у меня вызвали не процессы, а логи: `/var/log` занимает `3.2G`, а самые большие файлы связаны с авторизацией, `fail2ban`, `ufw` и системными логами. + +### Reflection + +Я бы сделала такие улучшения: + +1. Проверила `logrotate` для `auth.log`, `btmp`, `fail2ban.log`, `ufw.log` и `kern.log`. +2. Поставила лимит для `journald`, например через `SystemMaxUse`. +3. Добавила alert на заполнение `/`, например на `80%` и `90%`. +4. Раз в какое-то время чистила старые Docker layers, если они точно не нужны. +5. Следила бы за количеством failed SSH logins, потому что большие `auth.log` и `btmp` часто связаны с попытками входа. + +## Task 2 - Practical Website Monitoring Setup + +### Website + +Для мониторинга я выбрала сайт: + +```text +https://www.iana.org/domains/reserved +``` + +Я взяла IANA, потому что это публичная страница без логина. На ней удобно проверить и простой HTTP-ответ, и реальный browser flow: открыть страницу, найти заголовок, кликнуть по ссылке и проверить переход. + +### API Check + +Я создала API Check для URL: + +```text +https://www.iana.org/domains/reserved +``` + +Настройки: + +- Method: `GET` +- URL: `https://www.iana.org/domains/reserved` +- Follow redirects: enabled +- Expected result: HTTP `200` +- Interval: `5 minutes` + +Скриншот API check: + +![API check configuration](screenshots/lab_8_new/api_check_config.png) + +### Browser Check + +Browser Check я назвала `IANA browser flow`. Он проверяет не только доступность, но и поведение страницы. + +Сценарий: + +```js +const { expect, test } = require('@playwright/test') + +test('iana reserved domains browser flow', async ({ page }) => { + await page.goto('https://www.iana.org/domains/reserved') + + await expect(page.getByRole('heading', { name: 'IANA-managed Reserved Domains' })).toBeVisible() + await expect(page.getByText('Certain domains are set aside')).toBeVisible() + + await page.getByRole('link', { name: 'Root Zone Management' }).click() + await expect(page).toHaveURL('https://www.iana.org/domains/root') + await expect(page.getByRole('heading', { name: 'Root Zone Management' })).toBeVisible() +}) +``` + +Скриншот конфигурации Browser Check: + +![Browser check configuration](screenshots/lab_8_new/browser_check_config.png) + +Успешный ручной запуск Browser Check: + +![Successful browser check result](screenshots/lab_8_new/browser_check_result.png) + +### Alerting + +Для alerting я оставила email-канал на эту же почту и включила retry policy. + +Настройки retry: + +- Strategy: `Linear` +- Max retries: `2` +- Base backoff: `60 seconds` +- Max total retry duration: `600 seconds` +- Location: retry from the same location + +Я выбрала retry, потому что один случайный network timeout не должен сразу создавать шумный alert. Если проблема повторяется после retry, тогда это уже больше похоже на настоящий incident. + +Скриншот alert settings: + +![Alert settings](screenshots/lab_8_new/alert_settings.png) + +### Dashboard + +На dashboard обе проверки находятся в состоянии `PASSING`: + +- `https://www.iana.org/domains/reserved` - API check +- `IANA browser flow` - Browser check + +Dashboard также показывает availability и latency: + +- API check: `100%`, average около `370 ms` +- Browser check: `100%`, average около `5.02 s` + +![Dashboard overview](screenshots/lab_8_new/dashboard_overview.png) + +### Analysis + +API Check нужен как быстрый сигнал: сайт отвечает или нет. Он дешёвый и простой, но не видит, сломалась ли сама страница. + +Browser Check полезнее для пользовательского сценария. В моём случае он проверяет, что страница IANA открылась, нужный текст есть, ссылка `Root Zone Management` работает и после клика открывается правильная страница. Это ближе к реальному поведению пользователя, чем просто HTTP `200`. + +Интервал `5 minutes` для API Check и `10 minutes` для Browser Check мне кажется нормальным. API можно проверять чаще, потому что он легче. Browser Check тяжелее, поэтому я оставила его реже, чтобы не создавать лишнюю нагрузку и шум. + +### Reflection + +Такой мониторинг помогает не смотреть dashboard вручную. Если сайт перестанет отвечать, это поймает API Check. Если страница будет открываться, но сломается важный текст или переход, это поймает Browser Check. + +В реальной системе я бы добавила ещё проверку latency threshold и запуск из нескольких регионов. Тогда было бы проще отличить глобальную проблему сайта от локальной сетевой проблемы в одном регионе. diff --git a/labs/submission9.md b/labs/submission9.md new file mode 100644 index 000000000..c34ca2b59 --- /dev/null +++ b/labs/submission9.md @@ -0,0 +1,195 @@ +# Lab 9 — Introduction to DevSecOps Tools + +## Task 1 — Web Application Scanning with OWASP ZAP + +### Запуск Juice Shop + +Сначала я подняла Juice Shop в Docker: + +```bash +docker run -d --name juice-shop -p 3000:3000 bkimminich/juice-shop +``` + +Потом проверила, что приложение отвечает: + +```bash +curl -I http://127.0.0.1:3000 +``` + +Основные заголовки в ответе: + +```text +HTTP/1.1 200 OK +Access-Control-Allow-Origin: * +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +Feature-Policy: payment 'self' +Cache-Control: public, max-age=0 +Content-Type: text/html; charset=UTF-8 +Content-Length: 75002 +``` + +### Запуск OWASP ZAP + +Так как сканирование выполнялось на Linux VPS, я использовала `--network host`, чтобы ZAP-контейнер видел локальный Juice Shop на `127.0.0.1:3000`. + +```bash +docker run --rm --network host -v "$(pwd)":/zap/wrk:rw \ + -t ghcr.io/zaproxy/zaproxy:stable zap-baseline.py \ + -t http://127.0.0.1:3000 \ + -j \ + -a \ + -m 5 \ + -r zap-report.html +``` + +Короткий итог из вывода ZAP: + +```text +Using the Automation Framework +Total of 166 URLs +FAIL-NEW: 0 FAIL-INPROG: 0 WARN-NEW: 15 WARN-INPROG: 0 INFO: 0 IGNORE: 0 PASS: 55 +``` + +В HTML-отчете ZAP получилась такая сводка: + +```text +High: 0 +Medium: 4 +Low: 8 +Informational: 8 +False Positives: 0 +``` + +![ZAP report overview](screenshots/lab_9_new/zap_report_overview.png) + +### Medium risk vulnerabilities + +ZAP нашел 4 Medium risk проблемы: + +| Vulnerability | Risk | Instances | +| --- | --- | --- | +| Content Security Policy (CSP) Header Not Set | Medium | Systemic | +| Cross-Domain Misconfiguration | Medium | Systemic | +| Missing Anti-clickjacking Header | Medium | 3 | +| Session ID in URL Rewrite | Medium | Systemic | + +![ZAP medium alerts](screenshots/lab_9_new/zap_medium_alerts.png) + +### Две наиболее интересные проблемы + +**Content Security Policy (CSP) Header Not Set.** +На части страниц нет `Content-Security-Policy`. Из-за этого браузеру не задается строгий список разрешенных источников для скриптов, стилей, картинок и фреймов. Если в приложении появится XSS или возможность подмешать внешний скрипт, отсутствие CSP упростит эксплуатацию. Для исправления я бы добавила CSP с базовым ограничением вроде `default-src 'self'` и отдельно настроила источники для API, статики и `frame-ancestors`. + +**Session ID in URL Rewrite.** +ZAP увидел session id в URL у Socket.IO-запросов, например в параметре `sid`. Это плохая практика, потому что такие значения могут попасть в историю браузера, reverse proxy logs, server logs или `Referer`. Сессионные идентификаторы лучше передавать через cookie с `HttpOnly`, `Secure` и `SameSite`, а не через query string. + +Самой интересной для меня была `Session ID in URL Rewrite`, потому что это не просто "не хватает заголовка", а реальный пример утечки чувствительного идентификатора через URL. + +### Security headers status + +На главной странице часть защитных заголовков есть: + +| Header | Status | Комментарий | +| --- | --- | --- | +| `X-Content-Type-Options: nosniff` | present | Помогает браузеру не угадывать MIME-type. | +| `X-Frame-Options: SAMEORIGIN` | present | Снижает риск clickjacking для основной страницы. | +| `Feature-Policy: payment 'self'` | present | Ограничивает payment API, но сам `Feature-Policy` уже устаревший, лучше использовать `Permissions-Policy`. | +| `Access-Control-Allow-Origin: *` | present, but risky | Любой origin может читать разрешенные CORS-ответы, поэтому ZAP отметил Cross-Domain Misconfiguration. | + +Проблемные или отсутствующие заголовки: + +| Header / check | Status | Почему важно | +| --- | --- | --- | +| `Content-Security-Policy` | missing | Без CSP слабее защита от XSS и content injection. | +| Anti-clickjacking header | missing on some Socket.IO responses | На части ответов нет защиты от встраивания во frame. | +| `X-Content-Type-Options` | missing on some Socket.IO responses | Не везде одинаково настроена защита от MIME sniffing. | +| `Cross-Origin-Embedder-Policy` | missing or invalid | Слабее изоляция cross-origin ресурсов. | +| `Sec-Fetch-Dest` | missing | Сервер не может использовать этот сигнал для фильтрации подозрительных запросов. | + +### Web application analysis + +В этом скане чаще всего встречались не SQL injection или XSS, а security misconfiguration: отсутствующие или непоследовательно выставленные security headers, слишком широкий CORS и передача session id через URL. В реальных веб-приложениях это тоже частая проблема: приложение может работать корректно функционально, но быть плохо защищено на уровне HTTP policy и конфигурации. + +После сканирования я остановила и удалила контейнер: + +```bash +docker stop juice-shop && docker rm juice-shop +``` + +## Task 2 — Container Vulnerability Scanning with Trivy + +### Запуск Trivy + +Для проверки Docker-образа я использовала Trivy и оставила только `HIGH` и `CRITICAL` уязвимости: + +```bash +docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ + aquasec/trivy:latest image \ + --severity HIGH,CRITICAL \ + bkimminich/juice-shop +``` + +Также сохранила вывод в table/json, чтобы было проще разобрать результаты: + +```bash +docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ + aquasec/trivy:latest image \ + --severity HIGH,CRITICAL \ + --format table \ + bkimminich/juice-shop > trivy-table.txt + +docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ + aquasec/trivy:latest image \ + --severity HIGH,CRITICAL \ + --format json \ + -o trivy.json \ + bkimminich/juice-shop +``` + +Итог по Trivy: + +```text +Total: 65 (HIGH: 46, CRITICAL: 19) +CRITICAL=19 +HIGH=46 +``` + +![Trivy critical findings](screenshots/lab_9_new/trivy_critical_findings.png) + +### Key findings + +| Package | Vulnerability | Severity | Installed version | Fixed version | +| --- | --- | --- | --- | --- | +| `crypto-js` | `CVE-2023-46233` | CRITICAL | `3.3.0` | `4.2.0` | +| `jsonwebtoken` | `CVE-2015-9235` | CRITICAL | `0.1.0`, `0.4.0` | `4.2.2` | +| `lodash` | `CVE-2019-10744` | CRITICAL | `2.4.2` | `4.17.12` | +| `braces` | `CVE-2024-4068` | HIGH | `2.3.2` | `3.0.3` | + +Самые частые пакеты в результатах: `vm2`, `tar`, `multer`, `jsonwebtoken`, `minimatch`, `lodash`. Самый частый тип уязвимостей — CVE: в результатах было `CVE: 61`, `NSWG: 3`, `GHSA: 1`. + +### Container scanning analysis + +Container image scanning важен до production, потому что образ может содержать старые библиотеки даже тогда, когда само приложение собирается и запускается без ошибок. В моем скане видно много уязвимостей уровня зависимостей Node.js: `crypto-js`, `jsonwebtoken`, `lodash`, `vm2` и другие. Если такой образ отправить в production без проверки, команда может случайно выкатить уже известные уязвимости. + +Trivy помогает найти такие проблемы до деплоя. По результатам можно обновить зависимости, пересобрать образ, заменить base image или временно задокументировать исключение, если уязвимость не эксплуатируется в конкретном контексте. + +### CI/CD reflection + +Я бы встроила эти проверки так: + +1. После сборки Docker image запускать Trivy: + +```bash +trivy image --severity HIGH,CRITICAL --exit-code 1 my-app:${CI_COMMIT_SHA} +``` + +Если есть `CRITICAL`, pipeline должен падать. Для `HIGH` можно либо тоже блокировать merge, либо сначала отправлять отчет в security review, если проект только внедряет DevSecOps. + +2. После деплоя в test/staging окружение запускать ZAP baseline scan: + +```bash +zap-baseline.py -t https://staging.example.com -r zap-report.html +``` + +ZAP HTML report и Trivy JSON/SARIF я бы сохраняла как CI artifacts. Так у команды будет история сканов, а не только красный или зеленый статус job. diff --git a/test.txt b/test.txt new file mode 100644 index 000000000..2eec599a1 --- /dev/null +++ b/test.txt @@ -0,0 +1 @@ +Test content