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/.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/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..4b04caad81 --- /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. + +--- + +### app_deploy 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/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/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..531a2a996e --- /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=~/.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..fcc372661d --- /dev/null +++ b/ansible/playbooks/deploy.yml @@ -0,0 +1,10 @@ +--- +- name: Deploy application container + hosts: webservers + become: yes + + collections: + - community.docker + + roles: + - app_deploy 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/app_deploy/defaults/main.yml b/ansible/roles/app_deploy/defaults/main.yml new file mode 100644 index 0000000000..cd3de83b87 --- /dev/null +++ b/ansible/roles/app_deploy/defaults/main.yml @@ -0,0 +1,21 @@ +--- +# Defaults for app_deploy role + +# Docker Hub username (can be overridden via Vault) +dockerhub_username: "fayzullin" # Поставь свой логин, если другой + +# App / image name +app_name: "devops-info-service" + +# Full image name and tag +docker_image: "{{ dockerhub_username }}/{{ app_name }}" +docker_image_tag: "latest" + +# Container and port +app_port: 5000 +app_container_name: "{{ app_name }}" +app_restart_policy: "unless-stopped" + +docker_restart_policy: "{{ app_restart_policy }}" +# Environment variables for the container (if needed) +app_env: {} diff --git a/ansible/roles/app_deploy/handlers/main.yml b/ansible/roles/app_deploy/handlers/main.yml new file mode 100644 index 0000000000..e7e8259b12 --- /dev/null +++ b/ansible/roles/app_deploy/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/app_deploy/tasks/main.yml b/ansible/roles/app_deploy/tasks/main.yml new file mode 100644 index 0000000000..5abb2d162e --- /dev/null +++ b/ansible/roles/app_deploy/tasks/main.yml @@ -0,0 +1,42 @@ +--- +###- name: Log in to Docker Hub +### community.docker.docker_login: + ### username: "{{ dockerhub_username }}" + ### password: "{{ dockerhub_password }}" + ###no_log: true + +- name: Pull application image + community.docker.docker_image: + name: "{{ docker_image }}:{{ docker_image_tag }}" + source: pull + +- name: Ensure old container is absent + community.docker.docker_container: + name: "{{ app_container_name }}" + state: absent + ignore_errors: true + +- name: Run application container + community.docker.docker_container: + name: "{{ app_container_name }}" + image: "{{ docker_image }}:{{ docker_image_tag }}" + published_ports: + - "{{ app_port }}:{{ app_port }}" + env: "{{ app_env }}" + restart_policy: "{{ docker_restart_policy }}" + state: started + +- name: Wait for application port to become open + ansible.builtin.wait_for: + host: "{{ ansible_host | default(inventory_hostname) }}" + port: "{{ app_port }}" + delay: 3 + timeout: 60 + +- name: Check health endpoint + ansible.builtin.uri: + url: "http://{{ ansible_host | default(inventory_hostname) }}:{{ app_port }}/health" + method: GET + status_code: 200 + register: health_result + failed_when: health_result.status != 200 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..f90bf70284 --- /dev/null +++ b/ansible/roles/common/tasks/main.yml @@ -0,0 +1,15 @@ +--- +- 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 + +- name: Set timezone + community.general.timezone: + name: "{{ common_timezone }}" + when: common_timezone is defined 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..b72e0b72e5 --- /dev/null +++ b/ansible/roles/docker/tasks/main.yml @@ -0,0 +1,27 @@ +--- +- 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_packages }}" + state: present + +- name: Install python3-docker for Ansible docker modules + ansible.builtin.apt: + name: python3-docker + state: present + +- 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: yes 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/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 +}