diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 0000000000..8f9b7095ff --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -0,0 +1,87 @@ +name: Python CI (app_python) + +on: + push: + branches: ["master"] + paths: + - "app_python/**" + - ".github/workflows/python-ci.yml" + pull_request: + paths: + - "app_python/**" + - ".github/workflows/python-ci.yml" + +concurrency: + group: python-ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + test-and-lint: + runs-on: ubuntu-latest + defaults: + run: + working-directory: app_python + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: "pip" + cache-dependency-path: | + app_python/requirements.txt + app_python/requirements-dev.txt + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt -r requirements-dev.txt + + - name: Lint (ruff) + run: | + ruff check . + + - name: Run tests (pytest) + run: | + pytest -q + + - name: Install Snyk CLI + run: npm install -g snyk + + - name: Snyk scan (dependencies) + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + run: snyk test --file=requirements.txt --severity-threshold=high || true + + + docker-build-and-push: + runs-on: ubuntu-latest + needs: test-and-lint + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Generate CalVer version + run: | + echo "VERSION=$(date -u +%Y.%m.%d)-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: ./app_python + file: ./app_python/Dockerfile + push: true + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/devops-info-service:${{ env.VERSION }} + ${{ secrets.DOCKERHUB_USERNAME }}/devops-info-service:latest diff --git a/.gitignore b/.gitignore index 30d74d2584..e8384ee8c7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,18 @@ -test \ No newline at end of file +test +# Terraform +terraform/.terraform/ +terraform/.terraform.lock.hcl +terraform/terraform.tfstate +terraform/terraform.tfstate.backup +terraform/*.tfvars + +# Yandex Cloud SA key +.yc/ + +venv/ +*/venv/ +__pycache__/ +.terraform/ +.pulumi/ +*.tfstate +*.tfstate.backup diff --git a/ansible/.github/workflows/ansible-deploy.yml b/ansible/.github/workflows/ansible-deploy.yml new file mode 100644 index 0000000000..c9dd3010cf --- /dev/null +++ b/ansible/.github/workflows/ansible-deploy.yml @@ -0,0 +1,62 @@ +name: Ansible Deployment + +on: + push: + branches: [ "main", "master", "lab06" ] + paths: + - "ansible/**" + - ".github/workflows/ansible-deploy.yml" + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install ansible + lint + run: pip install ansible ansible-lint + - name: ansible-lint + run: | + cd ansible + ansible-lint playbooks/*.yml + + deploy: + needs: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install Ansible + community.docker + run: | + pip install ansible + ansible-galaxy collection install community.docker + + - name: Setup SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh-keyscan -H "${{ secrets.VM_HOST }}" >> ~/.ssh/known_hosts + + - name: Create vault pass file + run: | + echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > /tmp/vault_pass + chmod 600 /tmp/vault_pass + + - name: Deploy + env: + ANSIBLE_HOST_KEY_CHECKING: "False" + run: | + cd ansible + ansible-playbook playbooks/deploy.yml \ + -i inventory/hosts.ini \ + --vault-password-file /tmp/vault_pass + + - name: Verify + run: | + sleep 5 + curl -f "http://${{ secrets.VM_HOST }}:5000/health" diff --git a/ansible/.gitignore b/ansible/.gitignore new file mode 100644 index 0000000000..ff6e09c2c4 --- /dev/null +++ b/ansible/.gitignore @@ -0,0 +1,6 @@ + +# Ansible +*.retry +.vault_pass +__pycache__/ +ansible/.venv/ diff --git a/ansible/0 b/ansible/0 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000000..1dae3b014c --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,13 @@ +[defaults] +inventory = inventory/hosts.ini +roles_path = roles +host_key_checking = False +remote_user = vboxuser +retry_files_enabled = False +forks = 10 +timeout = 30 + +[privilege_escalation] +become = True +become_method = sudo +become_user = root diff --git a/ansible/docs/LAB05.md b/ansible/docs/LAB05.md new file mode 100644 index 0000000000..7400bf3c02 --- /dev/null +++ b/ansible/docs/LAB05.md @@ -0,0 +1,157 @@ +# LAB05 — Ansible Fundamentals + +## 1. Architecture Overview + +- **Ansible Version:** 2.16+ +- **Target OS:** Ubuntu 24.04 LTS +- **Cloud Provider:** Yandex Cloud +- **Application:** DevOps Info Service (FastAPI) +- **Container Runtime:** Docker + +This lab implements a fully automated, role-based infrastructure provisioning and container deployment system using Ansible. + +### Why Roles? + +Roles were used instead of monolithic playbooks to achieve: + +- Modularity +- Reusability +- Separation of concerns +- Clean project structure +- Easier scalability and maintenance + +--- + +## 2. Role Structure + +### common role +Purpose: Basic system preparation + +Tasks: +- Update APT cache +- Install essential packages (curl, git, vim, htop, python3-pip) +- Configure timezone + +Idempotency ensured using: +- `apt` module with `state: present` +- `timezone` module + +--- + +### docker role +Purpose: Install and configure Docker + +Tasks: +- Install Docker from Ubuntu repository +- Enable and start docker service +- Add user to docker group +- Install python3-docker for Ansible Docker modules + +Handlers: +- Restart Docker service (if needed) + +All tasks are state-based and idempotent. + +--- + +### web_app role +Purpose: Deploy containerized application securely + +Tasks: +- Pull Docker image +- Remove old container if exists +- Run container with restart policy +- Wait for application port +- Perform health check via HTTP + +Security: +- Docker Hub credentials stored in encrypted Vault file +- `no_log: true` used for sensitive tasks + +--- + +## 3. Idempotency Demonstration + +### First Run + +Initial execution resulted in multiple `changed` tasks because packages and services were installed. + +### Second Run + +Second execution showed: + + +changed=0 + + +This confirms idempotency. + +Idempotency is achieved by: +- Using declarative modules +- Avoiding raw shell commands +- Defining desired system state explicitly + +--- + +## 4. Application Deployment Verification + +After deployment: + +- Container is running (`docker ps`) +- Port 5000 is exposed publicly +- Health endpoint returns HTTP 200 +- Root endpoint returns system metadata + +Public URL: + +http://93.77.190.119:5000 + +Health endpoint: + +http://93.77.190.119:5000/health + +--- + +## 5. Ansible Vault + +Sensitive variables are stored in: + + +group_vars/all.yml + + +File is encrypted using: + + +$ANSIBLE_VAULT;1.1;AES256 + + +Vault ensures: +- Secrets are not stored in plaintext +- Safe version control +- Secure automation + +--- + +## 6. Key DevOps Principles Applied + +- Infrastructure as Code +- Idempotent configuration management +- Secure secret management +- Containerized deployment +- Automated verification +- Role-based modular architecture + +--- + +## 7. Conclusion + +The system successfully provisions infrastructure, installs Docker, and deploys a containerized application using Ansible roles. + +The solution is: + +- Idempotent +- Secure +- Modular +- Reproducible +- Production-ready diff --git a/ansible/docs/LAB06.md b/ansible/docs/LAB06.md new file mode 100644 index 0000000000..044edeac9c --- /dev/null +++ b/ansible/docs/LAB06.md @@ -0,0 +1,351 @@ +## 1. Overview + +In this lab, the infrastructure automation was extended using more advanced Ansible concepts including: + +Task tagging and selective execution + +Blocks for logical grouping of tasks + +Docker Compose deployment + +Controlled wipe logic for application reset + +CI/CD pipeline integration using GitHub Actions + +The goal of this lab was to improve maintainability, flexibility, and automation capabilities of the infrastructure management system. + +The application deployed in this lab is the DevOps Info Service, a containerized FastAPI application running on a remote Ubuntu server. + +## 2. Infrastructure Architecture +Control Node + +Local machine running: + +Ansible 2.16+ + +SSH access to target VM + +Target Node + +Remote VM running: + +Ubuntu 24.04 LTS + +Docker Engine + +Docker Compose (v2 plugin) + +Application + +Containerized FastAPI service: + +fayzullin/devops-info-service:latest + +The service exposes the following endpoints: + +/ - service information +/health - health check endpoint + +Public access: + +http://93.77.190.119:5000 +## 3. Ansible Role Architecture + +The project uses a modular role-based structure. + +ansible/ +├── inventory/ +│ └── hosts.ini +├── playbooks/ +│ ├── provision.yml +│ └── deploy.yml +├── roles/ +│ ├── common/ +│ ├── docker/ +│ └── web_app/ +│ ├── tasks/ +│ │ ├── main.yml +│ │ └── wipe.yml +│ ├── defaults/ +│ │ └── main.yml +│ ├── templates/ +│ │ └── docker-compose.yml.j2 +│ └── meta/ +│ └── main.yml +└── docs/ + └── LAB06.md +Roles + +common + +installs base system packages + +configures system settings + +docker + +installs Docker engine + +enables docker service + +installs python docker bindings + +web_app + +deploys the application using Docker Compose + +supports application wipe/reset + +performs health verification + +## 4. Task Tags + +Tags were implemented to allow selective execution of tasks. + +Example tags used in the project: + +common +packages +docker_install +docker_config +app_deploy +compose +web_app_wipe +Listing tags +ansible-playbook playbooks/provision.yml --list-tags +Example selective execution + +Install only Docker: + +ansible-playbook playbooks/provision.yml --tags docker_install + +Install only system packages: + +ansible-playbook playbooks/provision.yml --tags packages + +Tags allow faster execution during debugging or partial updates. + +## 5. Blocks Usage + +Ansible blocks were used to group logically related tasks inside the web_app role. + +Example structure: + +block: + - create compose directory + - generate docker-compose.yml + - run docker compose + - wait for service + - run health check + +Benefits of blocks: + +improved readability + +structured execution flow + +easier error handling + +ability to apply conditions or tags to multiple tasks + +## 6. Docker Compose Deployment + +The application is deployed using Docker Compose v2. + +Compose file is generated dynamically using an Ansible template. + +Template + +roles/web_app/templates/docker-compose.yml.j2 + +Example structure: + +version: "3.8" + +services: + devops-info-service: + image: fayzullin/devops-info-service:latest + container_name: devops-info-service + ports: + - "5000:5000" + restart: unless-stopped +Deployment Flow + +The web_app role performs the following steps: + +Create project directory + +/opt/devops-info-service + +Render docker-compose file from template + +Run Docker Compose + +docker compose up -d + +Wait until port becomes available + +Verify service health endpoint + +## 7. Idempotency + +Ansible ensures idempotent infrastructure management. + +This means repeated execution does not modify the system if the desired state is already achieved. + +Example: + +First run: + +changed=3 + +Second run: + +changed=0 + +This proves the system converges to the desired state without unnecessary modifications. + +## 8. Wipe Logic (Controlled Reset) + +A controlled wipe mechanism was implemented to allow safe application reset. + +The wipe mechanism requires two conditions: + +1️⃣ variable web_app_wipe=true + +2️⃣ tag web_app_wipe + +This prevents accidental destruction of running services. + +Scenario 1 — Normal deploy +ansible-playbook deploy.yml + +Result: + +wipe skipped +application running +Scenario 2 — Wipe only +ansible-playbook deploy.yml \ +-e "web_app_wipe=true" \ +--tags web_app_wipe + +Result: + +containers removed +compose directory deleted +Scenario 3 — Clean reinstall +ansible-playbook deploy.yml \ +-e "web_app_wipe=true" + +Result: + +wipe executed +application redeployed +Scenario 4 — Tag only (blocked) +ansible-playbook deploy.yml \ +--tags web_app_wipe + +Result: + +wipe skipped (safety condition) +## 9. Health Verification + +After deployment the service health endpoint is verified: + +curl http://93.77.190.119:5000/health + +Example response: + +{ + "status": "healthy", + "timestamp": "...", + "uptime_seconds": 7128 +} + +This confirms that the containerized application is running correctly. + +## 10. CI/CD Pipeline + +A CI/CD pipeline was implemented using GitHub Actions. + +Pipeline stages: + +1. Lint Stage + +Runs: + +ansible-lint + +Purpose: + +validate Ansible syntax + +detect best practice violations + +2. Deploy Stage + +Steps: + +Checkout repository + +Install Ansible + +Install community.docker collection + +Configure SSH access + +Run Ansible deployment playbook + +Deployment command: + +ansible-playbook playbooks/deploy.yml +3. Verification Stage + +After deployment, the pipeline checks application availability: + +curl http://:5000/health + +If the endpoint responds successfully, the deployment is considered successful. + +## 11. Security Considerations + +Sensitive data is stored using Ansible Vault. + +Encrypted file: + +group_vars/all.yml + +Secrets stored: + +Docker Hub credentials + +environment variables + +Vault ensures: + +secrets are encrypted in Git + +secure infrastructure configuration + +## 12. Conclusion + +This lab extended the automation infrastructure with advanced Ansible features. + +Key improvements implemented: + +modular role architecture + +selective task execution using tags + +logical task grouping using blocks + +Docker Compose deployment + +controlled wipe/reset mechanism + +CI/CD automation using GitHub Actions + +full idempotent infrastructure management + +The final system provides a reliable, maintainable, and production-ready automated deployment pipeline. + diff --git a/ansible/docs/screenshots/curl-health.png b/ansible/docs/screenshots/curl-health.png new file mode 100644 index 0000000000..6b59e3ef28 Binary files /dev/null and b/ansible/docs/screenshots/curl-health.png differ diff --git a/ansible/docs/screenshots/lab06-compose-deploy-1.txt b/ansible/docs/screenshots/lab06-compose-deploy-1.txt new file mode 100644 index 0000000000..e25a3f9dc9 --- /dev/null +++ b/ansible/docs/screenshots/lab06-compose-deploy-1.txt @@ -0,0 +1,48 @@ + +PLAY [Deploy application container] ******************************************** + +TASK [Gathering Facts] ********************************************************* +ok: [lab04-vm] + +TASK [docker : Ensure apt cache is up to date] ********************************* +ok: [lab04-vm] + +TASK [docker : Install Docker package from Ubuntu repo] ************************ +ok: [lab04-vm] + +TASK [docker : Install python3-docker for Ansible docker modules] ************** +ok: [lab04-vm] + +TASK [docker : Ensure docker service is enabled and running] ******************* +ok: [lab04-vm] + +TASK [docker : Ensure user is added to docker group] *************************** +ok: [lab04-vm] + +TASK [docker : Ensure docker service is enabled (always)] ********************** +ok: [lab04-vm] + +TASK [web_app : Include wipe tasks] ******************************************** +included: /home/vboxuser/DevOps-Core-Course/ansible/roles/web_app/tasks/wipe.yml for lab04-vm + +TASK [web_app : Wipe web application (compose down)] *************************** +skipping: [lab04-vm] + +TASK [web_app : Remove compose project directory] ****************************** +skipping: [lab04-vm] + +TASK [web_app : Log wipe completion] ******************************************* +skipping: [lab04-vm] + +TASK [web_app : Create compose project directory] ****************************** +changed: [lab04-vm] + +TASK [web_app : Template docker-compose.yml] *********************************** +changed: [lab04-vm] + +TASK [web_app : Run Docker Compose (v2)] *************************************** +fatal: [lab04-vm]: FAILED! => {"changed": false, "msg": "Docker CLI /usr/bin/docker does not have the compose plugin installed"} + +PLAY RECAP ********************************************************************* +lab04-vm : ok=10 changed=2 unreachable=0 failed=1 skipped=3 rescued=0 ignored=0 + diff --git a/ansible/docs/screenshots/lab06-compose-deploy-2.txt b/ansible/docs/screenshots/lab06-compose-deploy-2.txt new file mode 100644 index 0000000000..4f51ba1394 --- /dev/null +++ b/ansible/docs/screenshots/lab06-compose-deploy-2.txt @@ -0,0 +1,48 @@ + +PLAY [Deploy application container] ******************************************** + +TASK [Gathering Facts] ********************************************************* +ok: [lab04-vm] + +TASK [docker : Ensure apt cache is up to date] ********************************* +ok: [lab04-vm] + +TASK [docker : Install Docker package from Ubuntu repo] ************************ +ok: [lab04-vm] + +TASK [docker : Install python3-docker for Ansible docker modules] ************** +ok: [lab04-vm] + +TASK [docker : Ensure docker service is enabled and running] ******************* +ok: [lab04-vm] + +TASK [docker : Ensure user is added to docker group] *************************** +ok: [lab04-vm] + +TASK [docker : Ensure docker service is enabled (always)] ********************** +ok: [lab04-vm] + +TASK [web_app : Include wipe tasks] ******************************************** +included: /home/vboxuser/DevOps-Core-Course/ansible/roles/web_app/tasks/wipe.yml for lab04-vm + +TASK [web_app : Wipe web application (compose down)] *************************** +skipping: [lab04-vm] + +TASK [web_app : Remove compose project directory] ****************************** +skipping: [lab04-vm] + +TASK [web_app : Log wipe completion] ******************************************* +skipping: [lab04-vm] + +TASK [web_app : Create compose project directory] ****************************** +ok: [lab04-vm] + +TASK [web_app : Template docker-compose.yml] *********************************** +ok: [lab04-vm] + +TASK [web_app : Run Docker Compose (v2)] *************************************** +fatal: [lab04-vm]: FAILED! => {"changed": false, "msg": "Docker CLI /usr/bin/docker does not have the compose plugin installed"} + +PLAY RECAP ********************************************************************* +lab04-vm : ok=10 changed=0 unreachable=0 failed=1 skipped=3 rescued=0 ignored=0 + diff --git a/ansible/docs/screenshots/lab06-compose-ps.txt b/ansible/docs/screenshots/lab06-compose-ps.txt new file mode 100644 index 0000000000..3533fee5e0 --- /dev/null +++ b/ansible/docs/screenshots/lab06-compose-ps.txt @@ -0,0 +1,83 @@ +lab04-vm | FAILED | rc=125 >> +unknown shorthand flag: 'f' in -f +See 'docker --help'. + +Usage: docker [OPTIONS] COMMAND + +A self-sufficient runtime for containers + +Common Commands: + run Create and run a new container from an image + exec Execute a command in a running container + ps List containers + build Build an image from a Dockerfile + pull Download an image from a registry + push Upload an image to a registry + images List images + login Log in to a registry + logout Log out from a registry + search Search Docker Hub for images + version Show the Docker version information + info Display system-wide information + +Management Commands: + builder Manage builds + container Manage containers + context Manage contexts + image Manage images + manifest Manage Docker image manifests and manifest lists + network Manage networks + plugin Manage plugins + system Manage Docker + trust Manage trust on Docker images + volume Manage volumes + +Swarm Commands: + swarm Manage Swarm + +Commands: + attach Attach local standard input, output, and error streams to a running container + commit Create a new image from a container's changes + cp Copy files/folders between a container and the local filesystem + create Create a new container + diff Inspect changes to files or directories on a container's filesystem + events Get real time events from the server + export Export a container's filesystem as a tar archive + history Show the history of an image + import Import the contents from a tarball to create a filesystem image + inspect Return low-level information on Docker objects + kill Kill one or more running containers + load Load an image from a tar archive or STDIN + logs Fetch the logs of a container + pause Pause all processes within one or more containers + port List port mappings or a specific mapping for the container + rename Rename a container + restart Restart one or more containers + rm Remove one or more containers + rmi Remove one or more images + save Save one or more images to a tar archive (streamed to STDOUT by default) + start Start one or more stopped containers + stats Display a live stream of container(s) resource usage statistics + stop Stop one or more running containers + tag Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE + top Display the running processes of a container + unpause Unpause all processes within one or more containers + update Update configuration of one or more containers + wait Block until one or more containers stop, then print their exit codes + +Global Options: + --config string Location of client config files (default "/root/.docker") + -c, --context string Name of the context to use to connect to the daemon (overrides DOCKER_HOST env var and default context set with "docker context use") + -D, --debug Enable debug mode + -H, --host list Daemon socket to connect to + -l, --log-level string Set the logging level ("debug", "info", "warn", "error", "fatal") (default "info") + --tls Use TLS; implied by --tlsverify + --tlscacert string Trust certs signed only by this CA (default "/root/.docker/ca.pem") + --tlscert string Path to TLS certificate file (default "/root/.docker/cert.pem") + --tlskey string Path to TLS key file (default "/root/.docker/key.pem") + --tlsverify Use TLS and verify the remote + -v, --version Print version information and quit + +Run 'docker COMMAND --help' for more information on a command. + +For more help on how to use Docker, head to https://docs.docker.com/go/guides/non-zero return code diff --git a/ansible/docs/screenshots/lab06-deploy-second-run.txt b/ansible/docs/screenshots/lab06-deploy-second-run.txt new file mode 100644 index 0000000000..aa8620f109 --- /dev/null +++ b/ansible/docs/screenshots/lab06-deploy-second-run.txt @@ -0,0 +1,42 @@ + +PLAY [Deploy application container] ******************************************** + +TASK [Gathering Facts] ********************************************************* +ok: [lab04-vm] + +TASK [docker : Ensure apt cache is up to date] ********************************* +ok: [lab04-vm] + +TASK [docker : Install Docker package from Ubuntu repo] ************************ +ok: [lab04-vm] + +TASK [docker : Install python3-docker for Ansible docker modules] ************** +ok: [lab04-vm] + +TASK [docker : Ensure docker service is enabled and running] ******************* +ok: [lab04-vm] + +TASK [docker : Ensure user is added to docker group] *************************** +ok: [lab04-vm] + +TASK [docker : Ensure docker service is enabled (always)] ********************** +ok: [lab04-vm] + +TASK [web_app : Pull application image] **************************************** +ok: [lab04-vm] + +TASK [web_app : Ensure old container is absent] ******************************** +changed: [lab04-vm] + +TASK [web_app : Run application container] ************************************* +changed: [lab04-vm] + +TASK [web_app : Wait for application port to become open] ********************** +ok: [lab04-vm] + +TASK [web_app : Check health endpoint] ***************************************** +ok: [lab04-vm] + +PLAY RECAP ********************************************************************* +lab04-vm : ok=12 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 + diff --git a/ansible/docs/screenshots/lab06-docker-ps.txt b/ansible/docs/screenshots/lab06-docker-ps.txt new file mode 100644 index 0000000000..e7f36b697d --- /dev/null +++ b/ansible/docs/screenshots/lab06-docker-ps.txt @@ -0,0 +1,3 @@ +lab04-vm | CHANGED | rc=0 >> +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +ff00f6efae77 fayzullin/devops-info-service:latest "python app.py" 2 hours ago Up 2 hours 0.0.0.0:5000->5000/tcp devops-info-service diff --git a/ansible/docs/screenshots/lab06-health.txt b/ansible/docs/screenshots/lab06-health.txt new file mode 100644 index 0000000000..fd25dbfc26 --- /dev/null +++ b/ansible/docs/screenshots/lab06-health.txt @@ -0,0 +1 @@ +{"status":"healthy","timestamp":"2026-03-05T21:08:46.452541+00:00","uptime_seconds":7950} \ No newline at end of file diff --git a/ansible/docs/screenshots/lab06-list-tags.txt b/ansible/docs/screenshots/lab06-list-tags.txt new file mode 100644 index 0000000000..2781daa24c --- /dev/null +++ b/ansible/docs/screenshots/lab06-list-tags.txt @@ -0,0 +1,5 @@ + +playbook: playbooks/provision.yml + + play #1 (webservers): Provision web servers TAGS: [] + TASK TAGS: [common, docker, docker_config, docker_install, packages, users] diff --git a/ansible/docs/screenshots/lab06-root.txt b/ansible/docs/screenshots/lab06-root.txt new file mode 100644 index 0000000000..73a5d61ae4 --- /dev/null +++ b/ansible/docs/screenshots/lab06-root.txt @@ -0,0 +1 @@ +{"service":{"name":"devops-info-service","version":"1.0.0","description":"DevOps course info service","framework":"FastAPI"},"system":{"hostname":"ff00f6efae77","platform":"Linux","platform_version":"5.4.0-216-generic","architecture":"x86_64","cpu_count":2,"python_version":"3.12.12"},"runtime":{"uptime_seconds":7950,"uptime_human":"2 hours, 12 minutes","current_time":"2026-03-05T21:08:46.808476+00:00","timezone":"UTC"},"request":{"client_ip":"109.248.33.37","user_agent":"curl/8.5.0","method":"GET","path":"/"},"endpoints":[{"path":"/","method":"GET","description":"Service information"},{"path":"/health","method":"GET","description":"Health check"}]} \ No newline at end of file diff --git a/ansible/docs/screenshots/lab06-tags-docker_install.txt b/ansible/docs/screenshots/lab06-tags-docker_install.txt new file mode 100644 index 0000000000..e430f38617 --- /dev/null +++ b/ansible/docs/screenshots/lab06-tags-docker_install.txt @@ -0,0 +1,18 @@ + +PLAY [Provision web servers] *************************************************** + +TASK [Gathering Facts] ********************************************************* +ok: [lab04-vm] + +TASK [docker : Ensure apt cache is up to date] ********************************* +changed: [lab04-vm] + +TASK [docker : Install Docker package from Ubuntu repo] ************************ +ok: [lab04-vm] + +TASK [docker : Install python3-docker for Ansible docker modules] ************** +ok: [lab04-vm] + +PLAY RECAP ********************************************************************* +lab04-vm : ok=4 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 + diff --git a/ansible/docs/screenshots/lab06-tags-packages.txt b/ansible/docs/screenshots/lab06-tags-packages.txt new file mode 100644 index 0000000000..d0af66c6af --- /dev/null +++ b/ansible/docs/screenshots/lab06-tags-packages.txt @@ -0,0 +1,18 @@ + +PLAY [Provision web servers] *************************************************** + +TASK [Gathering Facts] ********************************************************* +ok: [lab04-vm] + +TASK [common : Ensure apt cache is up to date] ********************************* +ok: [lab04-vm] + +TASK [common : Install common packages] **************************************** +ok: [lab04-vm] + +TASK [common : Log common completion] ****************************************** +ok: [lab04-vm] + +PLAY RECAP ********************************************************************* +lab04-vm : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 + diff --git a/ansible/docs/screenshots/lab06-wipe-s1-normal.txt b/ansible/docs/screenshots/lab06-wipe-s1-normal.txt new file mode 100644 index 0000000000..4f51ba1394 --- /dev/null +++ b/ansible/docs/screenshots/lab06-wipe-s1-normal.txt @@ -0,0 +1,48 @@ + +PLAY [Deploy application container] ******************************************** + +TASK [Gathering Facts] ********************************************************* +ok: [lab04-vm] + +TASK [docker : Ensure apt cache is up to date] ********************************* +ok: [lab04-vm] + +TASK [docker : Install Docker package from Ubuntu repo] ************************ +ok: [lab04-vm] + +TASK [docker : Install python3-docker for Ansible docker modules] ************** +ok: [lab04-vm] + +TASK [docker : Ensure docker service is enabled and running] ******************* +ok: [lab04-vm] + +TASK [docker : Ensure user is added to docker group] *************************** +ok: [lab04-vm] + +TASK [docker : Ensure docker service is enabled (always)] ********************** +ok: [lab04-vm] + +TASK [web_app : Include wipe tasks] ******************************************** +included: /home/vboxuser/DevOps-Core-Course/ansible/roles/web_app/tasks/wipe.yml for lab04-vm + +TASK [web_app : Wipe web application (compose down)] *************************** +skipping: [lab04-vm] + +TASK [web_app : Remove compose project directory] ****************************** +skipping: [lab04-vm] + +TASK [web_app : Log wipe completion] ******************************************* +skipping: [lab04-vm] + +TASK [web_app : Create compose project directory] ****************************** +ok: [lab04-vm] + +TASK [web_app : Template docker-compose.yml] *********************************** +ok: [lab04-vm] + +TASK [web_app : Run Docker Compose (v2)] *************************************** +fatal: [lab04-vm]: FAILED! => {"changed": false, "msg": "Docker CLI /usr/bin/docker does not have the compose plugin installed"} + +PLAY RECAP ********************************************************************* +lab04-vm : ok=10 changed=0 unreachable=0 failed=1 skipped=3 rescued=0 ignored=0 + diff --git a/ansible/docs/screenshots/lab06-wipe-s2-wipe-only.txt b/ansible/docs/screenshots/lab06-wipe-s2-wipe-only.txt new file mode 100644 index 0000000000..ad2cf0d4aa --- /dev/null +++ b/ansible/docs/screenshots/lab06-wipe-s2-wipe-only.txt @@ -0,0 +1,24 @@ + +PLAY [Deploy application container] ******************************************** + +TASK [Gathering Facts] ********************************************************* +ok: [lab04-vm] + +TASK [web_app : Include wipe tasks] ******************************************** +included: /home/vboxuser/DevOps-Core-Course/ansible/roles/web_app/tasks/wipe.yml for lab04-vm + +TASK [web_app : Wipe web application (compose down)] *************************** +fatal: [lab04-vm]: FAILED! => {"changed": false, "msg": "Docker CLI /usr/bin/docker does not have the compose plugin installed"} +...ignoring + +TASK [web_app : Remove compose project directory] ****************************** +changed: [lab04-vm] + +TASK [web_app : Log wipe completion] ******************************************* +ok: [lab04-vm] => { + "msg": "Application devops-info-service wiped successfully" +} + +PLAY RECAP ********************************************************************* +lab04-vm : ok=5 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=1 + diff --git a/ansible/docs/screenshots/lab06-wipe-s3-clean-reinstall.txt b/ansible/docs/screenshots/lab06-wipe-s3-clean-reinstall.txt new file mode 100644 index 0000000000..5ef7098e80 --- /dev/null +++ b/ansible/docs/screenshots/lab06-wipe-s3-clean-reinstall.txt @@ -0,0 +1,51 @@ + +PLAY [Deploy application container] ******************************************** + +TASK [Gathering Facts] ********************************************************* +ok: [lab04-vm] + +TASK [docker : Ensure apt cache is up to date] ********************************* +ok: [lab04-vm] + +TASK [docker : Install Docker package from Ubuntu repo] ************************ +ok: [lab04-vm] + +TASK [docker : Install python3-docker for Ansible docker modules] ************** +ok: [lab04-vm] + +TASK [docker : Ensure docker service is enabled and running] ******************* +ok: [lab04-vm] + +TASK [docker : Ensure user is added to docker group] *************************** +ok: [lab04-vm] + +TASK [docker : Ensure docker service is enabled (always)] ********************** +ok: [lab04-vm] + +TASK [web_app : Include wipe tasks] ******************************************** +included: /home/vboxuser/DevOps-Core-Course/ansible/roles/web_app/tasks/wipe.yml for lab04-vm + +TASK [web_app : Wipe web application (compose down)] *************************** +fatal: [lab04-vm]: FAILED! => {"changed": false, "msg": "Docker CLI /usr/bin/docker does not have the compose plugin installed"} +...ignoring + +TASK [web_app : Remove compose project directory] ****************************** +ok: [lab04-vm] + +TASK [web_app : Log wipe completion] ******************************************* +ok: [lab04-vm] => { + "msg": "Application devops-info-service wiped successfully" +} + +TASK [web_app : Create compose project directory] ****************************** +changed: [lab04-vm] + +TASK [web_app : Template docker-compose.yml] *********************************** +changed: [lab04-vm] + +TASK [web_app : Run Docker Compose (v2)] *************************************** +fatal: [lab04-vm]: FAILED! => {"changed": false, "msg": "Docker CLI /usr/bin/docker does not have the compose plugin installed"} + +PLAY RECAP ********************************************************************* +lab04-vm : ok=13 changed=2 unreachable=0 failed=1 skipped=0 rescued=0 ignored=1 + diff --git a/ansible/docs/screenshots/lab06-wipe-s4a-tag-only-blocked.txt b/ansible/docs/screenshots/lab06-wipe-s4a-tag-only-blocked.txt new file mode 100644 index 0000000000..b202aa972a --- /dev/null +++ b/ansible/docs/screenshots/lab06-wipe-s4a-tag-only-blocked.txt @@ -0,0 +1,21 @@ + +PLAY [Deploy application container] ******************************************** + +TASK [Gathering Facts] ********************************************************* +ok: [lab04-vm] + +TASK [web_app : Include wipe tasks] ******************************************** +included: /home/vboxuser/DevOps-Core-Course/ansible/roles/web_app/tasks/wipe.yml for lab04-vm + +TASK [web_app : Wipe web application (compose down)] *************************** +skipping: [lab04-vm] + +TASK [web_app : Remove compose project directory] ****************************** +skipping: [lab04-vm] + +TASK [web_app : Log wipe completion] ******************************************* +skipping: [lab04-vm] + +PLAY RECAP ********************************************************************* +lab04-vm : ok=2 changed=0 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0 + diff --git a/ansible/docs/screenshots/provision-first.png b/ansible/docs/screenshots/provision-first.png new file mode 100644 index 0000000000..e0365e7b13 Binary files /dev/null and b/ansible/docs/screenshots/provision-first.png differ diff --git a/ansible/docs/screenshots/provision-second.png b/ansible/docs/screenshots/provision-second.png new file mode 100644 index 0000000000..a21735c389 Binary files /dev/null and b/ansible/docs/screenshots/provision-second.png differ diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml new file mode 100644 index 0000000000..649ba87324 --- /dev/null +++ b/ansible/group_vars/all.yml @@ -0,0 +1,19 @@ +$ANSIBLE_VAULT;1.1;AES256 +32356462313737333333373263633564396636643732653235326563636663386333656433356634 +3231343764306434303939316564373334623765323934610a396632613631303061363339643061 +30613132343937343461373936633532343937643635393130333730643633353235336562613733 +3539373561303261650a653739656565623434316664346162623833393566306538663837316263 +33393134643665393033313339653562363938653064313739393735393131613361366161396236 +37383065353531336334646135383530636463303135316436646637646330353365363665366436 +36666538646530396161636166373130313334383332613866386535333734323462613337323265 +62303634626336356134316461656666373165666631376231326439393862333337666662616131 +62356136633630373965356463366362373365393832626362373637356533336635383337656561 +38306466663430626431623735653463373337396364666236366433313332376466356234663535 +32656261383164613437303332646532336537343833343932323337636239383534326664356665 +30616166616534396235656437343465346163376234366232643232663765386531623238653735 +39646430333830343664373939333766326431376638336161613630373332646138306639653439 +66366333363332356532343065646237653562643937633163346165643966623638633235393030 +38353735653636366264303164326230666339623039643933373036306233333637656339643733 +38333435303966646536663865653666303166346339346330316338343935633361663634303839 +61396264316132353762323931346130353239613366656531343735653464396536306664623130 +3030333933376564363832626232623763653961313135386465 diff --git a/ansible/inventory/hosts.ini b/ansible/inventory/hosts.ini new file mode 100644 index 0000000000..2cfc94f730 --- /dev/null +++ b/ansible/inventory/hosts.ini @@ -0,0 +1,5 @@ +[webservers] +lab04-vm ansible_host=93.77.190.119 ansible_user=ubuntu ansible_ssh_private_key_file=/home/vboxuser/.ssh/id_ed25519 + +[webservers:vars] +ansible_python_interpreter=/usr/bin/python3 diff --git a/ansible/playbooks/deploy.yml b/ansible/playbooks/deploy.yml new file mode 100644 index 0000000000..5371e0570b --- /dev/null +++ b/ansible/playbooks/deploy.yml @@ -0,0 +1,10 @@ +--- +- name: Deploy application container + hosts: webservers + become: yes + + collections: + - community.docker + + roles: + - web_app diff --git a/ansible/playbooks/provision.yml b/ansible/playbooks/provision.yml new file mode 100644 index 0000000000..f53efb0248 --- /dev/null +++ b/ansible/playbooks/provision.yml @@ -0,0 +1,8 @@ +--- +- name: Provision web servers + hosts: webservers + become: yes + + roles: + - common + - docker diff --git a/ansible/roles/common/defaults/main.yml b/ansible/roles/common/defaults/main.yml new file mode 100644 index 0000000000..49824977e8 --- /dev/null +++ b/ansible/roles/common/defaults/main.yml @@ -0,0 +1,13 @@ +--- +common_timezone: "Etc/UTC" + +common_packages: + - python3-pip + - curl + - git + - vim + - htop + - ca-certificates + - apt-transport-https + - software-properties-common + - gnupg diff --git a/ansible/roles/common/tasks/main.yml b/ansible/roles/common/tasks/main.yml new file mode 100644 index 0000000000..2fbc715722 --- /dev/null +++ b/ansible/roles/common/tasks/main.yml @@ -0,0 +1,49 @@ +--- +- name: Common | Packages block + block: + - name: Ensure apt cache is up to date + ansible.builtin.apt: + update_cache: yes + cache_valid_time: 3600 + + - name: Install common packages + ansible.builtin.apt: + name: "{{ common_packages }}" + state: present + rescue: + - name: Fix apt cache on failure + ansible.builtin.command: apt-get update --fix-missing + changed_when: false + always: + - name: Log common completion + ansible.builtin.lineinfile: + path: /tmp/ansible-common-done.txt + create: true + line: "common role finished" + state: present + become: true + tags: + - common + - packages + + +- name: Common | Users block + block: + - name: Ensure admin user exists (optional) + ansible.builtin.user: + name: "{{ common_admin_user }}" + groups: sudo + append: true + state: present + when: common_admin_user is defined and common_admin_user | length > 0 + always: + - name: Log users completion + ansible.builtin.lineinfile: + path: /tmp/ansible-users-done.txt + create: true + line: "users block finished" + state: present + become: true + tags: + - common + - users diff --git a/ansible/roles/docker/defaults/main.yml b/ansible/roles/docker/defaults/main.yml new file mode 100644 index 0000000000..d1761730cb --- /dev/null +++ b/ansible/roles/docker/defaults/main.yml @@ -0,0 +1,5 @@ +--- +docker_packages: + - docker.io + +docker_user: "ubuntu" diff --git a/ansible/roles/docker/handlers/main.yml b/ansible/roles/docker/handlers/main.yml new file mode 100644 index 0000000000..1a5058da5e --- /dev/null +++ b/ansible/roles/docker/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: restart docker + ansible.builtin.service: + name: docker + state: restarted diff --git a/ansible/roles/docker/tasks/main.yml b/ansible/roles/docker/tasks/main.yml new file mode 100644 index 0000000000..257939f4ec --- /dev/null +++ b/ansible/roles/docker/tasks/main.yml @@ -0,0 +1,56 @@ +--- +- name: Docker | Install block + block: + - name: Ensure apt cache is up to date + ansible.builtin.apt: + update_cache: yes + cache_valid_time: 3600 + + - name: Install Docker package from Ubuntu repo + ansible.builtin.apt: + name: + - docker.io + state: present + + - name: Install python3-docker for Ansible docker modules + ansible.builtin.apt: + name: python3-docker + state: present + + rescue: + - name: Wait a bit and retry apt update (network hiccup) + ansible.builtin.pause: + seconds: 10 + + - name: Retry apt update + ansible.builtin.apt: + update_cache: yes + + tags: + - docker + - docker_install + become: true + + +- name: Docker | Config block + block: + - name: Ensure docker service is enabled and running + ansible.builtin.service: + name: docker + state: started + enabled: yes + + - name: Ensure user is added to docker group + ansible.builtin.user: + name: "{{ docker_user }}" + groups: docker + append: true + always: + - name: Ensure docker service is enabled (always) + ansible.builtin.service: + name: docker + enabled: yes + tags: + - docker + - docker_config + become: true diff --git a/ansible/roles/web_app/defaults/main.yml b/ansible/roles/web_app/defaults/main.yml new file mode 100644 index 0000000000..11c518d733 --- /dev/null +++ b/ansible/roles/web_app/defaults/main.yml @@ -0,0 +1,18 @@ +--- +# Web app defaults + +app_name: devops-info-service + +docker_image: fayzullin/devops-info-service +docker_tag: latest + +app_port: 5000 +app_internal_port: 5000 + +docker_restart_policy: unless-stopped +app_env: {} + +# wipe logic (double gate) +web_app_wipe: false +compose_project_dir: "/opt/{{ app_name }}" +docker_compose_version: "3.8" diff --git a/ansible/roles/web_app/handlers/main.yml b/ansible/roles/web_app/handlers/main.yml new file mode 100644 index 0000000000..e7e8259b12 --- /dev/null +++ b/ansible/roles/web_app/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: restart app container + community.docker.docker_container: + name: "{{ app_container_name }}" + state: restarted diff --git a/ansible/roles/web_app/meta/main.yml b/ansible/roles/web_app/meta/main.yml new file mode 100644 index 0000000000..cb7d8e0460 --- /dev/null +++ b/ansible/roles/web_app/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - role: docker diff --git a/ansible/roles/web_app/tasks/main.yml b/ansible/roles/web_app/tasks/main.yml new file mode 100644 index 0000000000..83ad8223e9 --- /dev/null +++ b/ansible/roles/web_app/tasks/main.yml @@ -0,0 +1,41 @@ +--- +- name: Include wipe tasks + ansible.builtin.include_tasks: wipe.yml + tags: + - web_app_wipe + +- name: Deploy application with Docker Compose + block: + - name: Create compose project directory + ansible.builtin.file: + path: "{{ compose_project_dir }}" + state: directory + mode: "0755" + + - name: Template docker-compose.yml + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ compose_project_dir }}/docker-compose.yml" + mode: "0644" + + - name: Run Docker Compose (v2) + community.docker.docker_compose_v2: + project_src: "{{ compose_project_dir }}" + pull: always + state: present + + - name: Wait for application port to become open + ansible.builtin.wait_for: + host: "{{ ansible_host | default(inventory_hostname) }}" + port: "{{ app_port }}" + delay: 2 + timeout: 60 + + - name: Check health endpoint + ansible.builtin.uri: + url: "http://{{ ansible_host | default(inventory_hostname) }}:{{ app_port }}/health" + method: GET + status_code: 200 + tags: + - app_deploy + - compose diff --git a/ansible/roles/web_app/tasks/wipe.yml b/ansible/roles/web_app/tasks/wipe.yml new file mode 100644 index 0000000000..7c2829347f --- /dev/null +++ b/ansible/roles/web_app/tasks/wipe.yml @@ -0,0 +1,24 @@ +--- +- name: Wipe web application (compose down) + community.docker.docker_compose_v2: + project_src: "{{ compose_project_dir }}" + state: absent + ignore_errors: true + when: web_app_wipe | bool + tags: + - web_app_wipe + +- name: Remove compose project directory + ansible.builtin.file: + path: "{{ compose_project_dir }}" + state: absent + when: web_app_wipe | bool + tags: + - web_app_wipe + +- name: Log wipe completion + ansible.builtin.debug: + msg: "Application {{ app_name }} wiped successfully" + when: web_app_wipe | bool + tags: + - web_app_wipe diff --git a/ansible/roles/web_app/templates/docker-compose.yml.j2 b/ansible/roles/web_app/templates/docker-compose.yml.j2 new file mode 100644 index 0000000000..e47842566b --- /dev/null +++ b/ansible/roles/web_app/templates/docker-compose.yml.j2 @@ -0,0 +1,13 @@ +version: "3.8" + +services: + {{ app_name }}: + image: {{ docker_image }}:{{ docker_tag }} + container_name: {{ app_name }} + ports: + - "{{ app_port }}:{{ app_internal_port }}" + environment: +{% for k, v in (app_env | default({})).items() %} + {{ k }}: "{{ v }}" +{% endfor %} + restart: {{ docker_restart_policy }} diff --git a/ansible/{censored: b/ansible/{censored: new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ansible/{changed: b/ansible/{changed: new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ansible/{reason: b/ansible/{reason: new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app_python/.dockerignore b/app_python/.dockerignore new file mode 100644 index 0000000000..5db5bf582e --- /dev/null +++ b/app_python/.dockerignore @@ -0,0 +1,13 @@ +pycache/ +*.py[cod] +venv/ +.env +*.log + +.vscode/ +.idea/ +.git +.gitignore + +docs/ +tests/ diff --git a/app_python/.gitignore b/app_python/.gitignore new file mode 100644 index 0000000000..3ca35248a3 --- /dev/null +++ b/app_python/.gitignore @@ -0,0 +1,14 @@ +# Python +__pycache__/ +*.py[cod] +venv/ +.env +*.log + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store + diff --git a/app_python/Dockerfile b/app_python/Dockerfile new file mode 100644 index 0000000000..c4f33cf3af --- /dev/null +++ b/app_python/Dockerfile @@ -0,0 +1,20 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +WORKDIR /app + +RUN adduser --disabled-password --gecos "" appuser + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY app.py . + +USER appuser + +EXPOSE 5000 + +CMD ["python", "app.py"] diff --git a/app_python/README.md b/app_python/README.md new file mode 100644 index 0000000000..18ae98e060 --- /dev/null +++ b/app_python/README.md @@ -0,0 +1,80 @@ +# DevOps Info Service (FastAPI) + +[![Python CI (app_python)](https://github.com/fayz131/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg)](https://github.com/fayzullin/DevOps-Core-Course/actions/workflows/python-ci.yml) + + +## Overview +DevOps Info Service is a web application that provides information about the running service and the system it is running on. The application is designed as a foundation for future DevOps labs, including containerization, CI/CD, and monitoring. + +## Prerequisites +- Python 3.11 or newer +- pip +- Python virtual environment (venv) + +## Installation +Navigate to the application directory: + +cd app_python + +Create and activate a virtual environment: + +python3 -m venv venv +source venv/bin/activate + +Install dependencies: + +pip install -r requirements.txt + +## Running the Application +Start the application: + +python app.py + +Run with custom configuration: + +HOST=127.0.0.1 PORT=8080 python app.py + +## API Endpoints + +GET / +Returns service, system, runtime, and request information. + +GET /health +Returns application health status and uptime. + +## Configuration + +Environment variables: + +HOST — server host (default: 0.0.0.0) +PORT — server port (default: 5000) + +## Docker + +### Build image + +```bash +docker build -t devops-info-service:lab2 . +``` + +Run container +```bash +docker run --rm -p 5000:5000 devops-info-service:lab2 +``` + +From Docker Hub +```bash +docker pull fayzullin/devops-info-service:lab2 +docker run --rm -p 5000:5000 fayzullin/devops-info-service:lab2 +``` + +## Testing + +Install dev dependencies and run tests: + +```bash +pip install -r requirements.txt -r requirements-dev.txt +pytest +``` + + diff --git a/app_python/app.py b/app_python/app.py new file mode 100644 index 0000000000..29cb4e95d9 --- /dev/null +++ b/app_python/app.py @@ -0,0 +1,122 @@ +""" +DevOps Info Service +FastAPI web application providing system and runtime information. +""" + +import os +import socket +import platform +import logging +from datetime import datetime, timezone + +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse +import uvicorn + + +HOST = os.getenv("HOST", "0.0.0.0") +PORT = int(os.getenv("PORT", 5000)) + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger(__name__) + + +START_TIME = datetime.now(timezone.utc) +app = FastAPI(title="DevOps Info Service") + +logger.info("Application initialized") + + +def get_uptime(): + """Calculate application uptime.""" + delta = datetime.now(timezone.utc) - START_TIME + seconds = int(delta.total_seconds()) + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + return seconds, f"{hours} hours, {minutes} minutes" + + +def get_system_info(): + """Collect system information.""" + return { + "hostname": socket.gethostname(), + "platform": platform.system(), + "platform_version": platform.release(), + "architecture": platform.machine(), + "cpu_count": os.cpu_count(), + "python_version": platform.python_version(), + } + + +@app.get("/") +async def index(request: Request): + """Main endpoint returning service and system information.""" + logger.info("Handling request to '/'") + + uptime_seconds, uptime_human = get_uptime() + + return { + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "FastAPI", + }, + "system": get_system_info(), + "runtime": { + "uptime_seconds": uptime_seconds, + "uptime_human": uptime_human, + "current_time": datetime.now(timezone.utc).isoformat(), + "timezone": "UTC", + }, + "request": { + "client_ip": request.client.host, + "user_agent": request.headers.get("user-agent"), + "method": request.method, + "path": request.url.path, + }, + "endpoints": [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"}, + ], + } + + +@app.get("/health") +async def health(): + """Health check endpoint for monitoring.""" + logger.info("Health check requested") + + uptime_seconds, _ = get_uptime() + return { + "status": "healthy", + "timestamp": datetime.now(timezone.utc).isoformat(), + "uptime_seconds": uptime_seconds, + } + + +@app.exception_handler(404) +async def not_found(request: Request, exc): + """Handle 404 errors.""" + return JSONResponse( + status_code=404, + content={"error": "Not Found", "message": "Endpoint does not exist"}, + ) + + +@app.exception_handler(500) +async def internal_error(request: Request, exc): + """Handle unexpected server errors.""" + logger.error(f"Internal server error: {exc}") + return JSONResponse( + status_code=500, + content={"error": "Internal Server Error", "message": "An unexpected error occurred"}, + ) + +if __name__ == "__main__": + logger.info(f"Starting server on {HOST}:{PORT}") + uvicorn.run("app:app", host=HOST, port=PORT) + diff --git a/app_python/docs/LAB01.md b/app_python/docs/LAB01.md new file mode 100644 index 0000000000..4157c81f1f --- /dev/null +++ b/app_python/docs/LAB01.md @@ -0,0 +1,80 @@ +# LAB01 — DevOps Info Service + +## Framework Selection +For this lab, FastAPI was chosen as the web framework due to its modern design, +high performance, and built-in support for OpenAPI documentation. This makes it a suitable choice for building production-ready services and for future DevOps labs. + +| Framework | Advantages | Disadvantages | +|----------|------------|---------------| +| Flask | Simple and lightweight | No built-in API docs | +| FastAPI | Async, automatic docs, fast | Slight learning curve | +| Django | Full-featured framework | Overkill for small services | + +--- + +## Best Practices Applied +The following best practices were applied during development: + +- Clear and simple project structure +- Environment-based configuration using `HOST` and `PORT` +- Separation of logic into helper functions +- Use of UTC timezone for all runtime timestamps +- Dependency management using `requirements.txt` +- Virtual environment usage +- Handling of invalid endpoints using a custom 404 handler + +These practices improve readability, portability, and reliability of the application. + +--- + +## API Documentation + +### Main Endpoint — `GET /` +Returns detailed information about the service, system, runtime state, request metadata, and available endpoints. + +Example request: +```bash +curl http://localhost:5000/ +``` +The response includes: +- Service metadata (name, version, framework) +- System information (hostname, OS, CPU, Python version) +- Runtime information (uptime, current UTC time) +- Request details (client IP, user agent, HTTP method) +- List of available endpoints + +--- + +### Health Check — `GET /health` + +Returns the current health status of the application and uptime in seconds. + +Example request: +```bash +curl http://localhost:5000/health +``` +--- + +## Testing Evidence + +To confirm correct application behavior, the following screenshots were taken: + +- `01-main-endpoint.png` — response from the main endpoint (`GET /`) +- `02-health-check.png` — response from the health check endpoint (`GET /health`) +- `03-formatted-output.png` — formatted JSON output in the terminal + +All screenshots are located in the `docs/screenshots` directory. + +--- + +## Challenges & Solutions + +One of the challenges encountered was handling requests to non-existent endpoints. +This was solved by implementing a custom 404 error handler that returns a clear JSON response instead of a default HTML error page. + +--- + +## GitHub Community + +Starring repositories on GitHub helps support open-source maintainers and makes it easier to keep track of useful projects. +Following developers allows learning from their work, staying updated on new technologies, and building professional connections within the developer community. diff --git a/app_python/docs/LAB02.md b/app_python/docs/LAB02.md new file mode 100644 index 0000000000..37021d5f61 --- /dev/null +++ b/app_python/docs/LAB02.md @@ -0,0 +1,86 @@ +# Lab 2 — Docker Containerization + +## Docker Best Practices Applied + +- **Specific base image version** + Used `python:3.12-slim` as a lightweight official Python image. Using a specific version makes builds reproducible and avoids unexpected changes when the latest tag is updated. + +- **Layer caching with requirements.txt** + `requirements.txt` is copied and dependencies are installed before copying the application code. This allows Docker to reuse the dependency layer when only the code changes, speeding up rebuilds. + +- **Non-root user** + A dedicated non-root user `appuser` is created and the application is started under this user. Running containers as non-root reduces the impact of potential security vulnerabilities. + +- **Minimal file copy** + Only the files required at runtime are copied into the image (`requirements.txt` and `app.py`). Test files, documentation, and development artifacts are excluded via `.dockerignore`. This reduces image size and attack surface. + +- **Environment variables for Python** + `PYTHONDONTWRITEBYTECODE` and `PYTHONUNBUFFERED` are set to prevent `.pyc` creation and to ensure unbuffered output, which is useful for logging in containers. + +## Image Information & Decisions + +- **Base image:** `python:3.12-slim` + Chosen as a good balance between size and compatibility. The slim image is smaller than the full Python image but still based on Debian. + +- **Layer structure:** + 1. Pull base image + 2. Set environment variables + 3. Set working directory + 4. Create non-root user + 5. Copy `requirements.txt` and install dependencies + 6. Copy application code + 7. Switch to non-root user + 8. Set default command + +- **Optimization choices:** + - `--no-cache-dir` for pip + - `.dockerignore` excludes `venv`, `.git`, `docs`, `tests`, etc. + - Running as non-root user + +## Build & Run Process + +### Build + +```bash +docker build -t devops-info-service:lab2 . +``` + +### Run locally + +```bash +docker run --rm -p 5000:5000 devops-info-service:lab2 +``` + +### Test endpoints + +```bash +curl http://localhost:5000/ +curl http://localhost:5000/health +``` + +### Docker Hub repository + +Image is available at: +https://hub.docker.com/r/fayzullin/devops-info-service + + +Tag used: +```bash +fayzullin/devops-info-service:lab2 +``` + +### Technical Analysis + +The Dockerfile installs dependencies before copying the application code. If the order was reversed, any code change would force dependencies to be reinstalled on every build. Running as a non-root user improves security, and .dockerignore reduces the build context size, making builds faster and images smaller. Additionally, running the container as a non-root user reduces the potential impact of container escape vulnerabilities and follows Docker security best practices. + + +### Challenges & Solutions + +**Challenge:** Understanding how layer caching influences build speed. +**Solution:** Reordered layers so that dependency installation is separated from application code. + +**Challenge:** Running the app as a non-root user. +**Solution:** Created a dedicated appuser user and switched to it using the USER directive. + +**Challenge:** Reducing image size. +**Solution:** Used python:3.12-slim, disabled pip cache, and excluded unnecessary files via .dockerignore. diff --git a/app_python/docs/LAB03.md b/app_python/docs/LAB03.md new file mode 100644 index 0000000000..9aebd42ef7 --- /dev/null +++ b/app_python/docs/LAB03.md @@ -0,0 +1,83 @@ +# Lab 3 — Continuous Integration (CI/CD) + +## Overview + +This lab introduces automated testing and CI/CD using GitHub Actions for the FastAPI DevOps Info Service. + +The pipeline performs: +- Linting (ruff) +- Unit testing (pytest) +- Security scanning (Snyk) +- Docker image build and push to Docker Hub + +## Testing Framework + +**Framework used:** pytest + +Pytest was chosen because: +- Simple and readable assertions +- Great integration with FastAPI +- Industry standard in modern Python projects + +### Tests Implemented + +- `GET /` — validates response structure and required fields +- `GET /health` — validates health check structure +- `404 handler` — validates JSON error response + +### Run tests locally + +```bash +pip install -r requirements.txt -r requirements-dev.txt +pytest +``` + +## CI Workflow + +Workflow file: +.github/workflows/python-ci.yml + +### Trigger Strategy + +Workflow runs on: + +* Pull requests affecting app_python/** +* Push to master affecting app_python/** + +Path filters prevent unnecessary runs in monorepo. + +### Versioning Strategy + +Strategy: Calendar Versioning (CalVer) + +Format: +YYYY.MM.DD- + +Docker tags created: + +* fayzullin/devops-info-service: + +* fayzullin/devops-info-service:latest + +This is suitable for continuously deployed services. + +## CI Best Practices Applied + +Fail fast — Docker build runs only if tests pass. + +Dependency caching — pip cache speeds up builds. + +Path filters — workflow runs only when app_python changes. + +Concurrency control — cancels outdated runs. + +## Security Scanning + +Snyk is integrated to scan dependencies. +Build fails only on high severity vulnerabilities + +## Evidence + +GitHub Actions run: (add link after successful run) + +Docker Hub: https://hub.docker.com/r/fayzullin/devops-info-service diff --git a/app_python/docs/screenshots/01-main-endpoint.png b/app_python/docs/screenshots/01-main-endpoint.png new file mode 100644 index 0000000000..690563c723 Binary files /dev/null and b/app_python/docs/screenshots/01-main-endpoint.png differ diff --git a/app_python/docs/screenshots/02-health-check.png b/app_python/docs/screenshots/02-health-check.png new file mode 100644 index 0000000000..1719f04a17 Binary files /dev/null and b/app_python/docs/screenshots/02-health-check.png differ diff --git a/app_python/docs/screenshots/03-formatted-output.png b/app_python/docs/screenshots/03-formatted-output.png new file mode 100644 index 0000000000..de12345346 Binary files /dev/null and b/app_python/docs/screenshots/03-formatted-output.png differ diff --git a/app_python/requirements-dev.txt b/app_python/requirements-dev.txt new file mode 100644 index 0000000000..34d28434a1 --- /dev/null +++ b/app_python/requirements-dev.txt @@ -0,0 +1,4 @@ +pytest==8.3.3 +httpx==0.27.2 +ruff==0.7.2 + diff --git a/app_python/requirements.txt b/app_python/requirements.txt new file mode 100644 index 0000000000..ebc98913e8 --- /dev/null +++ b/app_python/requirements.txt @@ -0,0 +1,2 @@ +fastapi==0.115.8 +uvicorn[standard]==0.32.0 diff --git a/app_python/tests/__init__.py b/app_python/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app_python/tests/test_app.py b/app_python/tests/test_app.py new file mode 100644 index 0000000000..066aa56152 --- /dev/null +++ b/app_python/tests/test_app.py @@ -0,0 +1,60 @@ +from fastapi.testclient import TestClient + +from app import app + +client = TestClient(app) + + +def test_root_returns_required_structure(): + response = client.get("/", headers={"User-Agent": "pytest"}) + assert response.status_code == 200 + + data = response.json() + + assert "service" in data + assert "system" in data + assert "runtime" in data + assert "request" in data + assert "endpoints" in data + + service = data["service"] + assert service["name"] == "devops-info-service" + assert service["version"] == "1.0.0" + assert service["framework"] == "FastAPI" + + system = data["system"] + for key in ["hostname", "platform", "platform_version", "architecture", "cpu_count", "python_version"]: + assert key in system + + runtime = data["runtime"] + assert isinstance(runtime["uptime_seconds"], int) + assert runtime["uptime_seconds"] >= 0 + assert isinstance(runtime["uptime_human"], str) + assert isinstance(runtime["current_time"], str) + assert runtime["timezone"] == "UTC" + + req = data["request"] + assert req["method"] == "GET" + assert req["path"] == "/" + assert isinstance(req["user_agent"], (str, type(None))) + + +def test_health_endpoint(): + response = client.get("/health") + assert response.status_code == 200 + + data = response.json() + assert data["status"] == "healthy" + assert isinstance(data["timestamp"], str) + assert isinstance(data["uptime_seconds"], int) + assert data["uptime_seconds"] >= 0 + + +def test_404_returns_json(): + response = client.get("/does-not-exist") + assert response.status_code == 404 + + data = response.json() + assert data["error"] == "Not Found" + assert "message" in data + diff --git a/pulumi/lab04-yc/.gitignore b/pulumi/lab04-yc/.gitignore new file mode 100644 index 0000000000..a3807e5bdb --- /dev/null +++ b/pulumi/lab04-yc/.gitignore @@ -0,0 +1,2 @@ +*.pyc +venv/ diff --git a/pulumi/lab04-yc/Pulumi.yaml b/pulumi/lab04-yc/Pulumi.yaml new file mode 100644 index 0000000000..82f52af683 --- /dev/null +++ b/pulumi/lab04-yc/Pulumi.yaml @@ -0,0 +1,7 @@ +name: lab04-yc +description: A minimal Python Pulumi program +runtime: python +config: + pulumi:tags: + value: + pulumi:template: python diff --git a/pulumi/lab04-yc/__main__.py b/pulumi/lab04-yc/__main__.py new file mode 100644 index 0000000000..5703481991 --- /dev/null +++ b/pulumi/lab04-yc/__main__.py @@ -0,0 +1,128 @@ +import os +import pathlib + +import pulumi +import pulumi_yandex as yandex + + + +ZONE = "ru-central1-a" + + +FOLDER_ID = "b1g1cmmbss046n25oln3" + +SSH_PUBLIC_KEY_PATH = os.path.expanduser("~/.ssh/id_ed25519.pub") + +SSH_USERNAME = "ubuntu" + + + +def read_ssh_public_key(path: str) -> str: + p = pathlib.Path(path) + if not p.exists(): + raise FileNotFoundError(f"SSH public key not found: {p}") + return p.read_text().strip() + + +ssh_pub = read_ssh_public_key(SSH_PUBLIC_KEY_PATH) + + + +net = yandex.VpcNetwork( + "lab-network", + folder_id=FOLDER_ID, +) + +subnet = yandex.VpcSubnet( + "lab-subnet", + folder_id=FOLDER_ID, + network_id=net.id, + zone=ZONE, + v4_cidr_blocks=["10.0.0.0/24"], +) + + + +sg = yandex.VpcSecurityGroup( + "lab-sg", + folder_id=FOLDER_ID, + network_id=net.id, + description="Security group for lab04 VM (SSH, HTTP, app port)", + egresses=[ + yandex.VpcSecurityGroupEgressArgs( + protocol="ANY", + description="Allow all outbound", + v4_cidr_blocks=["0.0.0.0/0"], + from_port=0, + to_port=65535, + ) + ], + ingresses=[ + yandex.VpcSecurityGroupIngressArgs( + protocol="TCP", + description="SSH", + v4_cidr_blocks=["0.0.0.0/0"], + port=22, + ), + yandex.VpcSecurityGroupIngressArgs( + protocol="TCP", + description="HTTP", + v4_cidr_blocks=["0.0.0.0/0"], + port=80, + ), + yandex.VpcSecurityGroupIngressArgs( + protocol="TCP", + description="App port 5000", + v4_cidr_blocks=["0.0.0.0/0"], + port=5000, + ), + ], +) + + + +image = yandex.get_compute_image( + family="ubuntu-2004-lts", + folder_id="standard-images", +) + + + +vm = yandex.ComputeInstance( + "lab-vm", + folder_id=FOLDER_ID, + zone=ZONE, + platform_id="standard-v2", + resources=yandex.ComputeInstanceResourcesArgs( + cores=2, + memory=1, + core_fraction=20, + ), + boot_disk=yandex.ComputeInstanceBootDiskArgs( + initialize_params=yandex.ComputeInstanceBootDiskInitializeParamsArgs( + image_id=image.id, + size=10, + type="network-hdd", + ) + ), + network_interfaces=[ + yandex.ComputeInstanceNetworkInterfaceArgs( + subnet_id=subnet.id, + nat=True, + security_group_ids=[sg.id], + ) + ], + metadata={ + "ssh-keys": f"{SSH_USERNAME}:{ssh_pub}", + }, + labels={ + "lab": "lab04", + "tool": "pulumi", + }, +) + + +pulumi.export("external_ip", vm.network_interfaces[0].nat_ip_address) +pulumi.export("zone", vm.zone) +pulumi.export("subnet_id", subnet.id) +pulumi.export("security_group_id", sg.id) diff --git a/pulumi/lab04-yc/requirements.txt b/pulumi/lab04-yc/requirements.txt new file mode 100644 index 0000000000..bc4e43087b --- /dev/null +++ b/pulumi/lab04-yc/requirements.txt @@ -0,0 +1 @@ +pulumi>=3.0.0,<4.0.0 diff --git a/terraform/docs/LAB04.md b/terraform/docs/LAB04.md new file mode 100644 index 0000000000..72ff4cee9d --- /dev/null +++ b/terraform/docs/LAB04.md @@ -0,0 +1,212 @@ +# LAB04 — Infrastructure as Code (Terraform & Pulumi) + +## 1. Cloud Provider & Infrastructure + +**Cloud Provider:** Yandex Cloud +**Folder ID:** b1g1cmmbss046n25oln3 +**Region / Zone:** ru-central1-a +**Instance Type:** standard-v2 (2 vCPU with 20% core fraction, 1 GB RAM) +**Disk:** 10 GB HDD +**Operating System:** Ubuntu 24.04 LTS + +The smallest available instance type compatible with Yandex Cloud free tier was selected to minimize cost. + +### Security Configuration + +The following ports are opened in the security group: + +- TCP 22 — SSH (restricted access for remote management) +- TCP 80 — HTTP (future deployment) +- TCP 5000 — Application port (DevOps Info Service from previous labs) + +### Created Resources + +- VPC Network (`lab-network`) +- Subnet (`lab-subnet`) +- Security Group (`lab-sg`) +- Virtual Machine (`lab-vm`) +- Public IP Address + +Estimated cost: **0 RUB** (free tier usage). +Terraform Version: 1.9.8 +Pulumi Version: 3.222.0 + +--- + +## 2. Terraform Implementation + +### Terraform Version +Terraform CLI 1.9.x (Ubuntu Linux) + +### Project Structure + +terraform/ +├── main.tf +├── variables.tf +├── outputs.tf +└── docs/LAB04.md + + +### Authentication + +Authentication was configured using a Yandex Cloud service account JSON key: +~/.yc/terraform-key.json + + +Provider configuration: + +```hcl +provider "yandex" { + service_account_key_file = pathexpand("~/.yc/terraform-key.json") + folder_id = var.folder_id + zone = var.zone +} +``` + +### Workflow + +``` +terraform init +terraform fmt +terraform validate +terraform plan +terraform apply +``` + + +Example output from terraform plan: +Plan: 3 to add, 0 to change, 0 to destroy. + +Example output from terraform apply: +Apply complete! Resources: 3 added, 0 changed, 0 destroyed. + +Outputs: + +external_ip = "X.X.X.X" + + +SSH Verification +ssh ubuntu@ + + +SSH connection was successful. + +Cleanup + +After verifying functionality, Terraform resources were destroyed: + +terraform destroy + + +All resources created by Terraform were removed successfully to avoid duplication and unnecessary usage. + +## 3. Pulumi Implementation +Pulumi Version + +Pulumi CLI v3.222.0 + +Language + +Python + +Project Structure +pulumi/lab04-yc/ + ├── Pulumi.yaml + ├── Pulumi.dev.yaml + ├── requirements.txt + ├── __main__.py + └── venv/ + +Authentication + +Pulumi uses the same Yandex Cloud service account key: + +export YC_SERVICE_ACCOUNT_KEY_FILE=/home/vboxuser/.yc/terraform-key.json + +Resources Created + +The same infrastructure was recreated using Pulumi: + +VpcNetwork + +VpcSubnet + +VpcSecurityGroup + +ComputeInstance + +Public IP + +Pulumi Commands +pulumi preview +pulumi up + + +Preview example: + ++ yandex:index:VpcNetwork ++ yandex:index:VpcSubnet ++ yandex:index:VpcSecurityGroup ++ yandex:index:ComputeInstance + + +Apply output: + +Outputs: + external_ip : "93.77.190.119" + zone : "ru-central1-a" + +SSH Verification +ssh ubuntu@93.77.190.119 + + +SSH access was successful. + +## 4. Terraform vs Pulumi Comparison +Ease of Learning + +Terraform was easier to start with due to extensive documentation and straightforward declarative syntax. Pulumi required more setup (virtual environments, Python dependencies). + +Code Readability + +Terraform configurations are compact and declarative, making them easy to read for simple infrastructure. Pulumi provides more flexibility but adds programming complexity. + +Debugging + +Terraform errors are generally clear during plan and apply. Pulumi provides Python stack traces, which can be more detailed but sometimes harder to interpret. + +Documentation + +Terraform has broader documentation and community examples. Pulumi documentation is solid but less extensive for Yandex Cloud specifically. + +Use Case Preference + +Terraform is preferable for straightforward infrastructure definitions. +Pulumi is more powerful when complex logic, loops, or programming constructs are required. + +## 5. Lab 5 Preparation & Cleanup + +For Lab 5 (Ansible), the VM created using Pulumi will be kept active. + +Active VM: + +IP Address: 93.77.190.119 +Zone: ru-central1-a +User: ubuntu + +Terraform resources were destroyed. +Pulumi-managed VM remains running for future configuration management tasks. + +No secrets or state files were committed to Git. + +Infrastructure can be recreated at any time using: + +terraform apply + + +or + +pulumi up + +Terraform state was stored locally. The file terraform.tfstate was added to .gitignore and not committed to the repository. + diff --git a/terraform/docs/screenshots/pulumi-up.png b/terraform/docs/screenshots/pulumi-up.png new file mode 100644 index 0000000000..a6e37bd63d Binary files /dev/null and b/terraform/docs/screenshots/pulumi-up.png differ diff --git a/terraform/docs/screenshots/terraform-apply.png b/terraform/docs/screenshots/terraform-apply.png new file mode 100644 index 0000000000..e9c3bc3afd Binary files /dev/null and b/terraform/docs/screenshots/terraform-apply.png differ diff --git a/terraform/docs/screenshots/yc-vm-running.png b/terraform/docs/screenshots/yc-vm-running.png new file mode 100644 index 0000000000..7a87ed9991 Binary files /dev/null and b/terraform/docs/screenshots/yc-vm-running.png differ diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000000..62ed0354df --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,56 @@ +terraform { + required_providers { + yandex = { + source = "yandex-cloud/yandex" + } + } +} + +provider "yandex" { + service_account_key_file = "/home/vboxuser/.yc/terraform-key.json" + cloud_id = "b1g0qsmtu1cheeq79i0d" + folder_id = "b1g1cmmbss046n25oln3" + zone = "ru-central1-a" +} +resource "yandex_vpc_network" "lab_network" { + name = "lab-network" +} + +resource "yandex_vpc_subnet" "lab_subnet" { + name = "lab-subnet" + zone = "ru-central1-a" + network_id = yandex_vpc_network.lab_network.id + v4_cidr_blocks = ["10.10.0.0/24"] +} + +resource "yandex_compute_instance" "lab_vm" { + name = "lab-vm" + zone = "ru-central1-a" + platform_id = "standard-v2" + + resources { + cores = 2 + memory = 1 + core_fraction = 20 + } + + boot_disk { + initialize_params { + image_id = "fd80bm0rh4rkepi5ksdi" # Ubuntu 24.04 LTS + size = 10 + } + } + + network_interface { + subnet_id = yandex_vpc_subnet.lab_subnet.id + nat = true + } + + metadata = { + ssh-keys = "ubuntu:${file("/home/vboxuser/.ssh/id_ed25519.pub")}" + } +} + +output "external_ip" { + value = yandex_compute_instance.lab_vm.network_interface.0.nat_ip_address +}