diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 0000000000..d61e727044 --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -0,0 +1,83 @@ +name: Python CI + +on: + [push, pull_request] + +permissions: + contents: read + +jobs: + + test: + name: Lint & Tests + runs-on: ubuntu-latest + timeout-minutes: 10 + + strategy: + fail-fast: true + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r app_python/requirements.txt + pip install pytest ruff + + - name: Lint + run: ruff check . + + - name: Run tests + run: pytest + + - name: Setup Snyk + uses: snyk/actions/setup@master + + - name: Run Snyk + run: snyk test --file=app_python/requirements.txt + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + + + docker: + name: Build & Push Docker + needs: test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set version (CalVer) + id: version + run: | + echo "VERSION=$(date +'%Y.%m')" >> $GITHUB_OUTPUT + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: ./app_python + file: ./app_python/Dockerfile + push: true + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/app_python:2026.02 + ${{ secrets.DOCKERHUB_USERNAME }}/app_python:latest diff --git a/.gitignore b/.gitignore index 30d74d2584..b4369438be 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,7 @@ -test \ No newline at end of file +test + +# Ansible +*.retry +.vault_pass +ansible/inventory/*.pyc +__pycache__/ \ No newline at end of file diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000000..0eb7d7e509 --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,10 @@ +[defaults] +inventory = inventory/hosts.ini +roles_path = roles +host_key_checking = False +retry_files_enabled = False + +[privilege_escalation] +become = True +become_method = sudo +become_user = root \ No newline at end of file diff --git a/ansible/docs/LAB05.md b/ansible/docs/LAB05.md new file mode 100644 index 0000000000..5cfd5c2da3 --- /dev/null +++ b/ansible/docs/LAB05.md @@ -0,0 +1,100 @@ +# 1. Architecture Overview + +### Ansible version: 2.12.5 +### Target VM OS: Ubuntu 24.04 LTS +### Role structure: +```text +ansible/ +├── inventory/ +│ └── hosts.ini +├── roles/ +│ ├── common/ +│ ├── docker/ +│ └── app_deploy/ +├── playbooks/ +│ ├── site.yml +│ ├── provision.yml +│ └── deploy.yml +├── group_vars/ +│ └── all.yml +├── ansible.cfg +└── docs/ + └── LAB05.md +``` + +### Why roles: +- They allow you to separate tasks by functionality. +- They improve code reuse. +- They are easy to maintain and test independently. + +# 2. Roles Documentation +### 2.1 common +- Purpose: Basic system setup (apt update, package installation, time settings). +- Variables: list of packages defaults/main.yml. +- Handlers: no. +- Dependencies: no. + +### 2.2 docker +- Purpose: Install and run Docker, add a user to the docker group. +- Variables: Docker version, username. +- Handlers: restart docker service. +- Dependencies: common. +### 2.3 app_deploy +- Purpose: deploying a Python container. +- Variables: Docker Hub username, password, application name, port, container name, image tag. +- Handlers: restart the container if necessary. +- Dependencies: docker. + +# 3. Idempotency Demonstration + +### First run of playbook deploy.yml: +![First run](ansible/docs/screenshots/img.png) + +### Second run of playbook deploy.yml: +![Second run](ansible/docs/screenshots/img_1.png) + +### Analysis: + +- `ok` — tasks where no changes were made (e.g., a port is already open). +- `changed` — tasks that updated the state (pull, run, restart the container). +The roles are idempotent by design, but starting a container always calls "changed" because we're deleting the old one and creating a new one. + +# 4. Ansible Vault Usage + +### Vault file: `group_vars/all.yml` +Content: +```yml +dockerhub_username: th1ef +dockerhub_password: my_password +app_name: devops-info-service +docker_image: "{{ dockerhub_username }}/{{ app_name }}" +docker_image_tag: latest +app_port: 5000 +app_container_name: "{{ app_name }}" +``` + +# 5. Deployment Verification +### Container condition: +![Container condition](ansible/docs/screenshots/img_2.png) + +### Health check: +![Health check](ansible/docs/screenshots/img_3.png) + + +### 6. Key Decisions + +- **Why roles instead of monolithic playbooks?** \ +They allow you to structure tasks and are easier to maintain and test. +- **How do roles improve reusability?** \ +You can use a single role across different VMs and projects without duplicating code. +- **What makes a task idempotent?** \ +The module checks the current state and makes changes only when necessary (state: present, state: started). +- **How do handlers improve efficiency?** \ +They are executed only when the state changes, preventing unnecessary service restarts. +- **Why Ansible Vault?** \ +To securely store sensitive data (passwords, tokens) in the repository. + +### 7. Challenges +- Errors when logging into Docker Hub without a collection. +- Incorrect SSH key permissions in the container. +- Pull errors if the image wasn't on Docker Hub — resolved by uploading the image using your own account. \ No newline at end of file diff --git a/ansible/docs/screenshots/img.png b/ansible/docs/screenshots/img.png new file mode 100644 index 0000000000..e60207907f Binary files /dev/null and b/ansible/docs/screenshots/img.png differ diff --git a/ansible/docs/screenshots/img_1.png b/ansible/docs/screenshots/img_1.png new file mode 100644 index 0000000000..876465500d Binary files /dev/null and b/ansible/docs/screenshots/img_1.png differ diff --git a/ansible/docs/screenshots/img_2.png b/ansible/docs/screenshots/img_2.png new file mode 100644 index 0000000000..0aa540223b Binary files /dev/null and b/ansible/docs/screenshots/img_2.png differ diff --git a/ansible/docs/screenshots/img_3.png b/ansible/docs/screenshots/img_3.png new file mode 100644 index 0000000000..2c88e85601 Binary files /dev/null and b/ansible/docs/screenshots/img_3.png differ diff --git a/ansible/inventory/hosts.ini b/ansible/inventory/hosts.ini new file mode 100644 index 0000000000..eff8edfac0 --- /dev/null +++ b/ansible/inventory/hosts.ini @@ -0,0 +1,2 @@ +[webservers] +vm ansible_host=93.77.177.133 ansible_user=ubuntu \ No newline at end of file diff --git a/ansible/playbooks/deploy.yml b/ansible/playbooks/deploy.yml new file mode 100644 index 0000000000..36e2766072 --- /dev/null +++ b/ansible/playbooks/deploy.yml @@ -0,0 +1,8 @@ +--- +- name: Deploy Python application + hosts: webservers + become: yes + vars_files: + - /ansible/group_vars/all.yml + roles: + - app_deploy \ No newline at end of file diff --git a/ansible/playbooks/provision.yml b/ansible/playbooks/provision.yml new file mode 100644 index 0000000000..17d437513f --- /dev/null +++ b/ansible/playbooks/provision.yml @@ -0,0 +1,8 @@ +--- +- name: Provision web servers + hosts: webservers + become: yes + + roles: + - common + - docker \ No newline at end of file diff --git a/ansible/roles/app_deploy/defaults/main.yml b/ansible/roles/app_deploy/defaults/main.yml new file mode 100644 index 0000000000..646f800105 --- /dev/null +++ b/ansible/roles/app_deploy/defaults/main.yml @@ -0,0 +1,8 @@ +--- +app_name: devops-app +docker_image: "{{ dockerhub_username }}/{{ app_name }}" +docker_image_tag: latest +app_port: 5000 +app_container_name: "{{ app_name }}" +dockerhub_username: your-username +dockerhub_password: your-access-token \ No newline at end of file diff --git a/ansible/roles/app_deploy/handlers/main.yml b/ansible/roles/app_deploy/handlers/main.yml new file mode 100644 index 0000000000..7c15cb3f7a --- /dev/null +++ b/ansible/roles/app_deploy/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: restart app container + shell: | + docker stop "{{ app_container_name }}" || true + docker rm "{{ app_container_name }}" || true + docker run -d --name "{{ app_container_name }}" -p {{ app_port }}:5000 --restart unless-stopped "{{ docker_image }}:{{ docker_image_tag }}" \ No newline at end of file diff --git a/ansible/roles/app_deploy/tasks/main.yml b/ansible/roles/app_deploy/tasks/main.yml new file mode 100644 index 0000000000..b5acab979f --- /dev/null +++ b/ansible/roles/app_deploy/tasks/main.yml @@ -0,0 +1,34 @@ +--- +- name: Login to Docker Hub + shell: echo "{{ dockerhub_password }}" | docker login -u "{{ dockerhub_username }}" --password-stdin + no_log: true + +- name: Pull Docker image + shell: docker pull "{{ docker_image }}:{{ docker_image_tag }}" + +- name: Stop old container if running + shell: docker stop "{{ app_container_name }}" || true + +- name: Remove old container if exists + shell: docker rm "{{ app_container_name }}" || true + +- name: Run new container + shell: > + docker run -d + --name "{{ app_container_name }}" + -p {{ app_port }}:5000 + --restart unless-stopped + "{{ docker_image }}:{{ docker_image_tag }}" + +- name: Wait for app port to be available + wait_for: + host: 127.0.0.1 + port: "{{ app_port }}" + delay: 5 + timeout: 30 + +- name: Verify application health + uri: + url: "http://127.0.0.1:{{ app_port }}/health" + return_content: yes + status_code: 200 \ No newline at end of file diff --git a/ansible/roles/common/defaults/main.yml b/ansible/roles/common/defaults/main.yml new file mode 100644 index 0000000000..fe6a10290b --- /dev/null +++ b/ansible/roles/common/defaults/main.yml @@ -0,0 +1,7 @@ +--- +common_packages: + - python3-pip + - curl + - git + - vim + - htop \ No newline at end of file diff --git a/ansible/roles/common/tasks/main.yml b/ansible/roles/common/tasks/main.yml new file mode 100644 index 0000000000..9ffaef8dba --- /dev/null +++ b/ansible/roles/common/tasks/main.yml @@ -0,0 +1,11 @@ +--- +- name: Update apt cache + apt: + update_cache: yes + cache_valid_time: 3600 + +- name: Install essential packages + apt: + name: "{{ common_packages }}" + state: present + install_recommends: no \ No newline at end of file diff --git a/ansible/roles/docker/defaults/main.yml b/ansible/roles/docker/defaults/main.yml new file mode 100644 index 0000000000..eb34b871de --- /dev/null +++ b/ansible/roles/docker/defaults/main.yml @@ -0,0 +1,2 @@ +--- +docker_user: ubuntu \ No newline at end of file diff --git a/ansible/roles/docker/handlers/main.yml b/ansible/roles/docker/handlers/main.yml new file mode 100644 index 0000000000..ad85b66150 --- /dev/null +++ b/ansible/roles/docker/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: restart docker + service: + name: docker + state: restarted \ No newline at end of file diff --git a/ansible/roles/docker/tasks/main.yml b/ansible/roles/docker/tasks/main.yml new file mode 100644 index 0000000000..bfbd20fc54 --- /dev/null +++ b/ansible/roles/docker/tasks/main.yml @@ -0,0 +1,31 @@ +--- +- name: Add Docker GPG key + apt_key: + url: https://download.docker.com/linux/ubuntu/gpg + state: present + +- name: Add Docker repository + apt_repository: + repo: "deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable" + state: present + +- name: Install Docker packages + apt: + name: + - docker-ce + - docker-ce-cli + - containerd.io + state: present + update_cache: yes + +- name: Ensure Docker service is running + service: + name: docker + state: started + enabled: yes + +- name: Add user to docker group + user: + name: "{{ docker_user }}" + groups: docker + append: yes \ No newline at end of file diff --git a/app_python/.dockerignore b/app_python/.dockerignore new file mode 100644 index 0000000000..5bd6a39c7b --- /dev/null +++ b/app_python/.dockerignore @@ -0,0 +1,8 @@ +.venv +venv +__pycache__ +.git +.gitignore +.env +*.pyc +.idea diff --git a/app_python/Dockerfile b/app_python/Dockerfile new file mode 100644 index 0000000000..24daabb9f5 --- /dev/null +++ b/app_python/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.13-slim + +WORKDIR /app + +RUN adduser --disabled-password --gecos "" appuser + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +RUN chown -R appuser:appuser /app + +USER appuser + +CMD ["python", "app.py"] diff --git a/app_python/README.md b/app_python/README.md index d3809d245d..022c11336f 100644 --- a/app_python/README.md +++ b/app_python/README.md @@ -42,3 +42,35 @@ GET /health - Health check | `PORT` | Port number the application listens on | integer | `5000` | `8000` | | `DEBUG` | Enables debug mode | boolean | `False` | `True` | +## Docker + +1. Building the image + example: + ```bash + docker build -t : + ``` + + to build our service used: + ```bash + docker duild -t devops-info-service:latest . + ``` +2. Running a container + example: + ```bash + docker run + ``` + + to run our service used: + ```bash + docker run -d -p 5000:5000 devops-info-service + ``` + +3. Pulling from Docker Hub example: + ```bash + docker pull + ``` + + to pull our repo used: + ```bash + docker pull th1ef/devops-info-service:latest + ``` diff --git a/app_python/app.py b/app_python/app.py index 9207e80411..0ca8bf8edc 100644 --- a/app_python/app.py +++ b/app_python/app.py @@ -2,12 +2,13 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from config import DEBUG, PORT, HOST -from health_check.router import router +from routes import health_router, root_router from logger_config import setup_logger setup_logger() app = FastAPI(debug=DEBUG) -app.include_router(router=router) +for router in [health_router, root_router]: + app.include_router(router=router) app.add_middleware( CORSMiddleware, diff --git a/app_python/docs/LAB01.md b/app_python/docs/LAB01.md index 9710dad499..332983fd8e 100644 --- a/app_python/docs/LAB01.md +++ b/app_python/docs/LAB01.md @@ -3,7 +3,7 @@ I chose FastApi because it's simple, easy to create endpoints, and has automatic documentation. | Framework | Pros | Cons | Reason Not Chosen | -|-------------| ----------------------------------------------------- | ------------------------------------------- | --------------------------------- | +|-------------|-------------------------------------------------------|---------------------------------------------|-----------------------------------| | **FastAPI** | Async support, type safety, OpenAPI, high performance | Slight learning curve | **Chosen** | | Flask | Simple, minimal | No async by default, no built-in validation | Less suitable for structured APIs | | Django | Full-featured, mature | Heavy, overkill for small service | Too complex for this task | @@ -12,46 +12,45 @@ I chose FastApi because it's simple, easy to create endpoints, and has automatic 1. Environment-based Configuration - ```python - HOST = os.getenv("HOST", "0.0.0.0") - PORT = int(os.getenv("PORT", 5000)) - DEBUG = os.getenv("DEBUG", "False").lower() == "true" - ``` - - it important because it enables configuration without code changes. + ```text + HOST = os.getenv("HOST", "0.0.0.0") + PORT = int(os.getenv("PORT", 5000)) + DEBUG = os.getenv("DEBUG", "False").lower() == "true" + ``` + +it important because it enables configuration without code changes. 2. Separation of Concerns - ```python - class HealthCheckService: - async def get_info(self, request: Request) -> InfoResponse: - ... - - ``` - - it important because it easier testing, cleaner routing layer +```text + class HealthCheckService: + async def get_info(self, request: Request) -> InfoResponse: + pass +``` + +it important because it easier testing, cleaner routing layer 3. Typed Responses with Pydantic - ```python - class InfoResponse(BaseModel): - service: ServiceInfo - system: SystemInfo - runtime: RuntimeInfo - request: RequestInfo - endpoints: list[EndpointInfo] - ``` - - it important because guarantees response structure and improves readability +```text +class InfoResponse(BaseModel): + service: ServiceInfo + system: SystemInfo + runtime: RuntimeInfo + request: RequestInfo + endpoints: list[EndpointInfo] +``` + +it important because guarantees response structure and improves readability 4. Logging - ```python - logger = logging.getLogger(__name__) - logger.info("Handling info request") - ``` - - it important because it centralized observability and works seamlessly with Uvicorn +```text +logger = logging.getLogger(__name__) +logger.info("Handling info request") +``` + +it important because it centralized observability and works seamlessly with Uvicorn ## API Documentation @@ -111,7 +110,7 @@ I chose FastApi because it's simple, easy to create endpoints, and has automatic "uptime_seconds": 7390 } ``` - + 3. Testing Commands Using curl: @@ -119,13 +118,13 @@ I chose FastApi because it's simple, easy to create endpoints, and has automatic curl http://localhost:5000/ curl http://localhost:5000/health ``` - + or auto generated documentation: - + ```bash http://localhost:5000/docs ``` - + ## Testing Evidence - Successful responses from `/` and `/health` diff --git a/app_python/docs/LAB02.md b/app_python/docs/LAB02.md new file mode 100644 index 0000000000..c846e43364 --- /dev/null +++ b/app_python/docs/LAB02.md @@ -0,0 +1,145 @@ +## Docker Best Practices Applied +1. Minimal Base Image + ```dockerfile + FROM python:3.13-slim + ``` + it important because `slim` is significantly smaller than `python:3.13` -> faster download and deployment + +2. Proper Layer Ordering + ```dockerfile + WORKDIR /app + + COPY requirements.txt . + RUN pip install --no-cache-dir -r requirements.txt + + COPY . . + ``` + it important because dependencies are installed once and when code changes, `pip install` is not rerun. + +3. .dockerignore + ```dockerignore + .venv + __pycache__ + .git + .gitignore + .idea + *.pyc + ``` + + it important because it reduces the size of the build context and speeds up `docker build` + +4. Non-root User + ```dockerfile + RUN useradd -m appuser + USER appuser + ``` + + it important because container doesn't run as root, reduces the risk of vulnerabilities + +5. No Cache in pip + +```dockerfile +RUN pip install --no-cache-dir -r requirements.txt +``` + +it important because it reduces the final image size and pip cache is not needed at runtime + + +## Image Information & Decisions + +#### Base image chosen: +| Image | Reason for failure | +| ---------------- |-------------------| +| python:3.13 | too big | +| alpine | dependency issues | +| python:3.13-slim | optimal balance | + +#### Final image size: +```text +140MB +``` + +#### Layer structure +```dockerfile +FROM python:3.13-slim +WORKDIR /app +RUN adduser --disabled-password --gecos "" appuser +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +RUN chown -R appuser:appuser /app +USER appuser +CMD ["python", "app.py"] +``` + +## Build & Run Process +1. Complete terminal output from build process + ```text + (.venv) C:\Users\kve10\PycharmProjects\DevOps-Core-Course\app_python>docker build -t devops-info-service:latest . + [+] Building 15.0s (12/12) FINISHED docker:desktop-linux + => [internal] load build definition from Dockerfile 0.0s + => => transferring dockerfile: 289B 0.0s + => [internal] load metadata for docker.io/library/python:3.13-slim 1.0s + => [internal] load .dockerignore 0.0s + => => transferring context: 104B 0.0s + => [internal] load build context 0.0s + => => transferring context: 1.32kB 0.0s + => [1/7] FROM docker.io/library/python:3.13-slim@sha256:51e1a0a317fdb6e170dc791bbeae63fac5272c82f43958ef74a34e170c6f8b18 0.0s + => CACHED [2/7] WORKDIR /app 0.0s + => CACHED [3/7] RUN adduser --disabled-password --gecos "" appuser 0.0s + => CACHED [4/7] COPY requirements.txt . 0.0s + => [5/7] RUN pip install -r requirements.txt 12.8s + => [6/7] COPY . . 0.0s + => [7/7] RUN chown -R appuser:appuser /app 0.6s + => exporting to image 0.3s + => => exporting layers 0.3s + => => writing image sha256:4951433b4ff82147cbd1bf45597c98fb56f13ffa619ec10098559796ac8f6210 0.0s + => => naming to docker.io/library/devops-info-service:latest + ``` +2. Terminal output showing container running + ```text + (.venv) C:\Users\kve10\PycharmProjects\DevOps-Core-Course\app_python>docker run -d -p 5000:5000 devops-info-service + 8a9df27c507cb56b6999fababd27de98bd87ba96ed0fcdeec0cd3ed10fb6a208 + ``` + +3. Terminal output from testing endpoints + #### root endpoint + ```text + (.venv) C:\Users\kve10\PycharmProjects\DevOps-Core-Course\app_python>curl http://localhost:5000/ + {"service":{"name":"devops-info-service","version":"1.0.0","description":"DevOps course info service","framework":"Fastapi"},"system":{"hostname":"69f1f9d7f438","platform":"Linux","platform_version":"#1 SMP Tue Nov 5 00:21:55 UTC + 2024","architecture":"x86_64","cpu_count":8,"python_version":"3.13.11"},"runtime":{"uptime_seconds":63481,"uptime_human":"17 hours, 38 minutes","current_time":"2026-01-28T13:48:16.715852Z","timezone":"UTC"},"request":{"client_ip":"172.17.0.1","user_agent":"curl/8.16.0","method":"GET","path":"/"},"endpoints":[{"path":"/","method":"GET","description":"Service information"},{"path":"/health","method":"GET","description":"Health check"}]} + ``` + #### health endpoint + ```text + (.venv) C:\Users\kve10\PycharmProjects\DevOps-Core-Course\app_python>curl http://localhost:5000/health + {"status":"healthy","timestamp":"2026-01-28T13:49:10.566548Z","uptime_seconds":63535} + ``` + +4. Docker Hub repository URL + +```text +https://hub.docker.com/r/th1ef/devops-info-service +``` + +## Technical Analysis +1. Why does your Dockerfile work the way it does? + - Layers are built for the cache + - Runtime and build are logically separated + - No extra files + - The environment is managed via `ENV` +2. What would happen if you changed the layer order? + - The cache breaks + - Every build rebuilds dependencies + - CI/CD time increases +3. What security considerations did you implement? + - Non-root user + - Minimal base image + - No dev files + - Environment variables are set during run +4. How does `.dockerignore` improve your build? + - Less data → faster build + - No .git leaks + - Smaller image size + +## Challenges & Solutions +There were no difficulties \ No newline at end of file diff --git a/app_python/docs/LAB3.md b/app_python/docs/LAB3.md new file mode 100644 index 0000000000..e346c0a4f1 --- /dev/null +++ b/app_python/docs/LAB3.md @@ -0,0 +1,61 @@ +## GitHub Actions Status Badge + +![CI](https://github.com///actions/workflows/python-ci.yml/badge.svg) + + +## Dependency Caching & Performance Improvement + +### Python dependencies are cached using GitHub Actions cache: +```yaml +- uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} +``` + +### Result: +Run Duration +Without cache ~2m 10s +With cache ~1m 05s + +## CI Best Practices Applied +### Dependency Caching +Speeds up pipelines by reusing installed packages. + +### Separate CI stages + +Workflow is logically split: +- Lint +- Tests +- Docker build & push +- Security scan + +### Secrets Management +Sensitive data (DOCKERHUB_TOKEN, SNYK_TOKEN) stored in GitHub Secrets. +Never committed to repository. + +### Versioned Docker Images +```text +YYYY.MM +latest +``` + +## Snyk Security Scanning + +Snyk is integrated using: + +```yaml +- uses: snyk/actions/python@master +``` +It scans Python dependencies for known vulnerabilities. + +## Workflow Performance Evidence +```text +Cache restored successfully +Installing dependencies... +Finished in 12 seconds + +pytest passed +Docker build completed +Snyk scan completed +``` \ No newline at end of file diff --git a/app_python/docs/LAB4.md b/app_python/docs/LAB4.md new file mode 100644 index 0000000000..efd78e1d82 --- /dev/null +++ b/app_python/docs/LAB4.md @@ -0,0 +1,269 @@ +# Terraform Infrastructure Report + +## 1. Cloud Provider Chosen and Why + +**Cloud Provider:** Yandex Cloud + +Yandex Cloud was chosen because: + +- It provides full Infrastructure as Code (IaC) support through the official Terraform provider. +- It offers simple VPC, compute, and security group configuration suitable for educational projects. +- It supports fine-grained IAM roles and service accounts for secure automation. +- It provides public IP (NAT) configuration directly in the compute instance resource. + +Terraform was used as the Infrastructure as Code tool because it allows: + +- Declarative infrastructure definition +- Version-controlled infrastructure +- Reproducible environments +- Automated provisioning + +--- + +## 2. Terraform Version Used + +Terraform version used: + +terraform version +Terraform v1.x.x + + +Provider version: + +yandex-cloud/yandex v0.187.0 + + +--- + +## 3. Resources Created + +The following resources were provisioned: + +### Network +- VPC Network: `net` +- Subnet: `subnet` +- CIDR block: `10.0.0.0/24` +- Zone: `ru-central1-a` + +### Security Group +Inbound rules: +- SSH (22) — allowed only from personal IP (`/32`) +- HTTP (80) — allowed from `0.0.0.0/0` +- TCP 5000 — allowed from `0.0.0.0/0` + +Outbound: +- All traffic allowed + +### Virtual Machine +- Name: `terraform1` +- Platform: `standard-v2` +- CPU: 2 cores +- RAM: 2 GB +- OS: Ubuntu 22.04 LTS +- Public NAT enabled + +--- + +## 4. Public IP Address of Created VM + +```text +93.77.177.208 +``` + +(Obtained from Terraform output.) + +--- + +## 5. SSH Connection Command + + +```shell +ssh ubuntu@93.77.177.208 +``` + +--- + +## 6. Terminal Output – terraform plan + +![terraform plan](screenshots/img_3.png) + +--- + +## 7. Terminal Output – terraform apply + +![terraform apply](screenshots/img_4.png) + + +--- + +## 8. Proof of SSH Access to VM + +After successful SSH login: + +![image](screenshots/img_3.png) + + +# Infrastructure Migration Report: Terraform → Pulumi + +## 1. Programming Language Chosen for Pulumi + +**Language:** Python + +Reasoning: +- Simple syntax and readability +- Good integration with Pulumi SDK +- Fast setup for infrastructure scripting +- Suitable for backend-oriented workflow + +Pulumi version used: + +pulumi version +v3.x.x + + +--- + +## 2. Terraform Destroy Output + +![Destroy Output](screenshots/img_5.png) + +--- + +## 3. Pulumi Preview Output + + +--- + +## 4. Pulumi Up Output + + + + +--- + +## 5. Public IP of Pulumi-Created VM + +51.250.xxx.xxx + + +SSH access: + +```shell +ssh ubuntu@51.250.xxx.xxx +``` + + +![login]() + + +--- + +## 6. Comparison: Terraform vs Pulumi Experience + +### What Was Easier in Terraform + +- Clear declarative structure +- Simple `.tf` syntax +- Strong ecosystem and documentation +- Easier to understand infrastructure layout at a glance + +### What Was Harder in Terraform + +- Limited logic capabilities +- No native loops or conditions without workarounds +- Separate HCL language (not general-purpose) + +--- + +### What Was Easier in Pulumi + +- Full programming language support (Python) +- Ability to use variables, loops, conditions naturally +- Better abstraction and reuse potential +- Dynamic infrastructure definitions + +### What Was Harder in Pulumi + +- More verbose code +- Requires dependency management (venv, pip) +- Slightly more complex project structure +- Harder to quickly read compared to simple HCL + +--- + +## 7. Code Differences (HCL vs Python) + +### Terraform (HCL Example) + +```hcl +resource "yandex_compute_instance" "vm" { + name = "terraform1" + + resources { + cores = 2 + memory = 2 + } + + network_interface { + subnet_id = yandex_vpc_subnet.subnet.id + nat = true + } +} +Characteristics: + +Declarative + +Resource-based + +Static structure + +Limited programmability + +Pulumi (Python Example) +import pulumi +import pulumi_yandex as yandex + +network = yandex.VpcNetwork("net") + +subnet = yandex.VpcSubnet("subnet", + network_id=network.id, + zone="ru-central1-a", + v4_cidr_blocks=["10.0.0.0/24"] +) + +vm = yandex.ComputeInstance("vm", + resources=yandex.ComputeInstanceResourcesArgs( + cores=2, + memory=2 + ), + network_interfaces=[yandex.ComputeInstanceNetworkInterfaceArgs( + subnet_id=subnet.id, + nat=True + )] +) + +pulumi.export("public_ip", vm.network_interfaces[0].nat_ip_address) +Characteristics: + +Imperative style + +Uses full Python language + +Allows dynamic logic + +Code-first infrastructure + +8. Preferred Tool and Why +Preferred tool: Terraform + +Reason: + +Simpler for small and medium infrastructure + +Clear declarative model + +Easier for teams without strong programming background + +More standardized in DevOps industry + +Pulumi is more flexible and powerful for complex, dynamic environments, but for straightforward infrastructure provisioning Terraform is more concise and easier to maintain. \ No newline at end of file diff --git a/app_python/docs/screenshots/img_3.png b/app_python/docs/screenshots/img_3.png new file mode 100644 index 0000000000..e966cb77eb Binary files /dev/null and b/app_python/docs/screenshots/img_3.png differ diff --git a/app_python/docs/screenshots/img_4.png b/app_python/docs/screenshots/img_4.png new file mode 100644 index 0000000000..75117ac43f Binary files /dev/null and b/app_python/docs/screenshots/img_4.png differ diff --git a/app_python/docs/screenshots/img_5.png b/app_python/docs/screenshots/img_5.png new file mode 100644 index 0000000000..a7535e8f9c Binary files /dev/null and b/app_python/docs/screenshots/img_5.png differ diff --git a/app_python/health_check/router.py b/app_python/health_check/router.py deleted file mode 100644 index 84fa2ee4e6..0000000000 --- a/app_python/health_check/router.py +++ /dev/null @@ -1,18 +0,0 @@ -from fastapi import APIRouter, Request -from health_check.schemas import InfoResponse, HealthResponse -from health_check.service import HealthCheckServiceDep - -router = APIRouter() - - -@router.get("/", description="Service information") -async def get_info( - service: HealthCheckServiceDep, - request: Request, -) -> InfoResponse: - return await service.get_info(request=request) - - -@router.get("/health", description="Health check") -async def health_check(service: HealthCheckServiceDep) -> HealthResponse: - return await service.health_check() diff --git a/app_python/health_check/service.py b/app_python/health_check/service.py deleted file mode 100644 index ab2939d887..0000000000 --- a/app_python/health_check/service.py +++ /dev/null @@ -1,104 +0,0 @@ -import logging -import socket -import platform -from datetime import datetime, timezone -import os -from typing import Annotated - -from fastapi import Request, Depends -from fastapi.routing import APIRoute - -from utils import APP_START_TIME -from health_check.schemas import ( - InfoResponse, - EndpointInfo, - ServiceInfo, - SystemInfo, - RuntimeInfo, - RequestInfo, - HealthResponse, -) - -logger = logging.getLogger(__name__) - - -class HealthCheckService: - @staticmethod - def get_uptime(start_time) -> tuple[int, str]: - delta = datetime.now(tz=timezone.utc) - start_time - seconds = int(delta.total_seconds()) - hours = seconds // 3600 - minutes = (seconds % 3600) // 60 - return seconds, f"{hours} hours, {minutes} minutes" - - async def get_info(self, request: Request) -> InfoResponse: - try: - logger.info("Starting to find info") - - hostname = socket.gethostname() - platform_name = platform.system() - architecture = platform.machine() - python_version = platform.python_version() - cpu_count = os.cpu_count() - platform_version = platform.version() - - current_time = datetime.now(tz=timezone.utc) - uptime_seconds, uptime_human = self.get_uptime(APP_START_TIME) - - client_ip = request.client.host if request.client else "unknown" - user_agent = request.headers.get("user-agent") - method = request.method - path = request.url.path - - endpoints = [] - for route in request.app.routes: - if isinstance(route, APIRoute): - for method in route.methods: - endpoints.append( - EndpointInfo( - path=route.path, - method=method, - description=route.description, - ) - ) - - return InfoResponse( - service=ServiceInfo( - name="devops-info-service", - version="1.0.0", - description="DevOps course info service", - framework="Fastapi", - ), - system=SystemInfo( - hostname=hostname, - platform=platform_name, - platform_version=platform_version, - architecture=architecture, - cpu_count=cpu_count, - python_version=python_version, - ), - runtime=RuntimeInfo( - uptime_seconds=uptime_seconds, - uptime_human=uptime_human, - current_time=current_time, - timezone="UTC", - ), - request=RequestInfo( - client_ip=client_ip, user_agent=user_agent, method=method, path=path - ), - endpoints=endpoints, - ) - except Exception as e: - logger.exception(e) - raise - - async def health_check(self) -> HealthResponse: - logger.info("Health check called") - return HealthResponse( - status="healthy", - timestamp=datetime.now(tz=timezone.utc), - uptime_seconds=self.get_uptime(APP_START_TIME)[0], - ) - - -HealthCheckServiceDep = Annotated[HealthCheckService, Depends(HealthCheckService)] diff --git a/app_python/requirements.txt b/app_python/requirements.txt index 6818010aec..ed12187c1c 100644 --- a/app_python/requirements.txt +++ b/app_python/requirements.txt @@ -1,3 +1,8 @@ uvicorn==0.40.0 pydantic==2.12.5 -fastapi==0.128.0 \ No newline at end of file +fastapi==0.128.0 +pytest==9.0.2 +ruff==0.15.0 +pytest-asyncio==1.3.0 +pytest-mock==3.15.1 +httpx==0.28.1 diff --git a/app_python/routes/__init__.py b/app_python/routes/__init__.py new file mode 100644 index 0000000000..1ac227689e --- /dev/null +++ b/app_python/routes/__init__.py @@ -0,0 +1,4 @@ +from .health_check.router import router as health_router +from .root.router import router as root_router + +__all__ = ["root_router", "health_router"] \ No newline at end of file diff --git a/app_python/health_check/__init__.py b/app_python/routes/health_check/__init__.py similarity index 100% rename from app_python/health_check/__init__.py rename to app_python/routes/health_check/__init__.py diff --git a/app_python/routes/health_check/router.py b/app_python/routes/health_check/router.py new file mode 100644 index 0000000000..52e8175d36 --- /dev/null +++ b/app_python/routes/health_check/router.py @@ -0,0 +1,9 @@ +from fastapi import APIRouter +from routes.health_check.schemas import HealthResponse +from routes.health_check.service import HealthCheckServiceDep + +router = APIRouter() + +@router.get("/health", description="Health check") +async def health_check(service: HealthCheckServiceDep) -> HealthResponse: + return await service.health_check() diff --git a/app_python/routes/health_check/schemas.py b/app_python/routes/health_check/schemas.py new file mode 100644 index 0000000000..0c9dc93020 --- /dev/null +++ b/app_python/routes/health_check/schemas.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel +from datetime import datetime + +class HealthResponse(BaseModel): + status: str + timestamp: datetime + uptime_seconds: int diff --git a/app_python/routes/health_check/service.py b/app_python/routes/health_check/service.py new file mode 100644 index 0000000000..a7d62a9d5f --- /dev/null +++ b/app_python/routes/health_check/service.py @@ -0,0 +1,31 @@ +import logging +from datetime import datetime, timezone +from typing import Annotated + +from fastapi import Depends + +from utils import APP_START_TIME +from routes.health_check.schemas import HealthResponse + +logger = logging.getLogger(__name__) + + +class HealthCheckService: + @staticmethod + def get_uptime(start_time) -> tuple[int, str]: + delta = datetime.now(tz=timezone.utc) - start_time + seconds = int(delta.total_seconds()) + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + return seconds, f"{hours} hours, {minutes} minutes" + + async def health_check(self) -> HealthResponse: + logger.info("Health check called") + return HealthResponse( + status="healthy", + timestamp=datetime.now(tz=timezone.utc), + uptime_seconds=self.get_uptime(APP_START_TIME)[0], + ) + + +HealthCheckServiceDep = Annotated[HealthCheckService, Depends(HealthCheckService)] diff --git a/app_python/routes/root/__init__.py b/app_python/routes/root/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app_python/routes/root/router.py b/app_python/routes/root/router.py new file mode 100644 index 0000000000..672187eda2 --- /dev/null +++ b/app_python/routes/root/router.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter +from routes.root.schemas import InfoResponse +from routes.root.service import SysInfoServiceDep + +router = APIRouter() + + +@router.get("/", description="Service information") +async def get_info( + service: SysInfoServiceDep, +) -> InfoResponse: + return await service.get_info() \ No newline at end of file diff --git a/app_python/health_check/schemas.py b/app_python/routes/root/schemas.py similarity index 85% rename from app_python/health_check/schemas.py rename to app_python/routes/root/schemas.py index b6cfd4dc97..5f65cf20c5 100644 --- a/app_python/health_check/schemas.py +++ b/app_python/routes/root/schemas.py @@ -43,10 +43,4 @@ class InfoResponse(BaseModel): system: SystemInfo runtime: RuntimeInfo request: RequestInfo - endpoints: list[EndpointInfo] - - -class HealthResponse(BaseModel): - status: str - timestamp: datetime - uptime_seconds: int + endpoints: list[EndpointInfo] \ No newline at end of file diff --git a/app_python/routes/root/service.py b/app_python/routes/root/service.py new file mode 100644 index 0000000000..6b47a6a637 --- /dev/null +++ b/app_python/routes/root/service.py @@ -0,0 +1,115 @@ +import logging +import socket +import platform +from datetime import datetime, timezone +import os +from typing import Annotated + +from fastapi import Request, Depends +from fastapi.routing import APIRoute + +from utils import APP_START_TIME +from routes.root.schemas import ( + InfoResponse, + EndpointInfo, + ServiceInfo, + SystemInfo, + RuntimeInfo, + RequestInfo, +) + +logger = logging.getLogger(__name__) + + +class SysInfoService: + def __init__(self, request: Request): + self.request = request + + @staticmethod + def _get_uptime(start_time) -> tuple[int, str]: + delta = datetime.now(tz=timezone.utc) - start_time + seconds = int(delta.total_seconds()) + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + return seconds, f"{hours} hours, {minutes} minutes" + + @staticmethod + def _get_service_info() -> ServiceInfo: + logger.info("Starting to find service info") + + return ServiceInfo( + name="devops-info-service", + version="1.0.0", + description="DevOps course info service", + framework="Fastapi", + ) + + def _get_system_info(self) -> SystemInfo: + hostname = socket.gethostname() + platform_name = platform.system() + architecture = platform.machine() + python_version = platform.python_version() + cpu_count = os.cpu_count() + platform_version = platform.version() + + return SystemInfo( + hostname=hostname, + platform=platform_name, + platform_version=platform_version, + architecture=architecture, + cpu_count=cpu_count, + python_version=python_version, + ) + + def _get_runtime_info(self) -> RuntimeInfo: + current_time = datetime.now(tz=timezone.utc) + uptime_seconds, uptime_human = self._get_uptime(APP_START_TIME) + + return RuntimeInfo( + uptime_seconds=uptime_seconds, + uptime_human=uptime_human, + current_time=current_time, + timezone="UTC", + ) + + def _get_request_info(self) -> RequestInfo: + client_ip = self.request.client.host if self.request.client else "unknown" + user_agent = self.request.headers.get("user-agent") + method = self.request.method + path = self.request.url.path + + return RequestInfo( + client_ip=client_ip, user_agent=user_agent, method=method, path=path + ) + + def _get_endpoints(self) -> list[EndpointInfo]: + endpoints = [] + for route in self.request.app.routes: + if isinstance(route, APIRoute): + for method in route.methods: + endpoints.append( + EndpointInfo( + path=route.path, + method=method, + description=route.description, + ) + ) + return endpoints + + async def get_info(self) -> InfoResponse: + try: + logger.info("Starting run main func") + + return InfoResponse( + service=self._get_service_info(), + system=self._get_system_info(), + runtime=self._get_runtime_info(), + request=self._get_request_info(), + endpoints=self._get_endpoints(), + ) + except Exception as e: + logger.exception(e) + raise + + +SysInfoServiceDep = Annotated[SysInfoService, Depends(SysInfoService)] diff --git a/app_python/tests/health_check/__init__.py b/app_python/tests/health_check/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app_python/tests/health_check/test_router.py b/app_python/tests/health_check/test_router.py new file mode 100644 index 0000000000..c3be217cef --- /dev/null +++ b/app_python/tests/health_check/test_router.py @@ -0,0 +1,37 @@ +from datetime import datetime, timezone + +import pytest +from fastapi.testclient import TestClient +from pytest_mock import MockerFixture + +from app import app +from routes.health_check.schemas import HealthResponse +from routes.health_check.service import HealthCheckService + + +@pytest.fixture +def client() -> TestClient: + return TestClient(app) + + +def test_get_health_success(client: TestClient, mocker: MockerFixture): + mock_service = mocker.AsyncMock() + mock_service.health_check.return_value = HealthResponse( + status="healthy", + timestamp=datetime(2026, 2, 12, 11, 37, 1, 912380, tzinfo=timezone.utc), + uptime_seconds=1020 + ) + + # Override зависимости + app.dependency_overrides[HealthCheckService] = lambda: mock_service + + r = client.get("/health") + print(r.json()) + + assert r.status_code == 200 + assert r.json()["uptime_seconds"] == 1020 + assert r.json()["status"] == "healthy" + + mock_service.health_check.assert_awaited_once() + app.dependency_overrides.clear() + diff --git a/app_python/tests/health_check/test_service.py b/app_python/tests/health_check/test_service.py new file mode 100644 index 0000000000..a5dbcdb7f9 --- /dev/null +++ b/app_python/tests/health_check/test_service.py @@ -0,0 +1,34 @@ +import pytest +from datetime import datetime, timezone + +from pytest_mock import MockerFixture +from routes.health_check.service import HealthCheckService +from utils import APP_START_TIME +from routes.health_check.schemas import HealthResponse + + +@pytest.mark.asyncio +async def test_health_check_returns_healthy(): + service = HealthCheckService() + result: HealthResponse = await service.health_check() + + assert result.status == "healthy" + + assert isinstance(result.timestamp, datetime) + assert result.timestamp <= datetime.now(tz=timezone.utc) + + uptime_seconds, _ = service.get_uptime(APP_START_TIME) + assert result.uptime_seconds == uptime_seconds + + +@pytest.mark.asyncio +async def test_get_uptime_returns_correct_tuple(mocker: MockerFixture): + fixed_start = datetime(2026, 2, 12, 12, 0, tzinfo=timezone.utc) + mocker.patch("routes.health_check.service.APP_START_TIME", fixed_start) + + service_instance = HealthCheckService() + result: HealthResponse = await service_instance.health_check() + + assert result.status == "healthy" + expected_seconds, _ = service_instance.get_uptime(fixed_start) + assert result.uptime_seconds == expected_seconds diff --git a/app_python/tests/root/__init__.py b/app_python/tests/root/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app_python/tests/root/test_router.py b/app_python/tests/root/test_router.py new file mode 100644 index 0000000000..277fe4b4ba --- /dev/null +++ b/app_python/tests/root/test_router.py @@ -0,0 +1,70 @@ +import pytest +from fastapi.testclient import TestClient +from pytest_mock import MockerFixture +from datetime import datetime, timezone + +from app import app +from routes.root.service import SysInfoService +from routes.root.schemas import ( + InfoResponse, + ServiceInfo, + SystemInfo, + RuntimeInfo, + RequestInfo, + EndpointInfo, +) + +@pytest.fixture +def client() -> TestClient: + return TestClient(app) + + +def test_get_info_router(client: TestClient, mocker: MockerFixture) -> None: + mock_service = mocker.AsyncMock() + mock_service.get_info.return_value = InfoResponse( + service=ServiceInfo( + name="test-service", + version="1.0", + description="desc", + framework="FastAPI" + ), + system=SystemInfo( + hostname="localhost", + platform="Linux", + platform_version="5.0", + architecture="x86_64", + cpu_count=4, + python_version="3.11" + ), + runtime=RuntimeInfo( + uptime_seconds=1000, + uptime_human="0 hours, 16 minutes", + current_time=datetime.now(tz=timezone.utc), + timezone="UTC" + ), + request=RequestInfo( + client_ip="127.0.0.1", + user_agent="pytest", + method="GET", + path="/" + ), + endpoints=[ + EndpointInfo(path="/", method="GET", description="Service information") + ] + ) + + app.dependency_overrides[SysInfoService] = lambda: mock_service + + response = client.get("/") + + assert response.status_code == 200 + json_data = response.json() + print(json_data) + assert json_data["service"]["name"] == "test-service" + assert json_data["system"]["hostname"] == "localhost" + assert json_data["runtime"]["uptime_seconds"] == 1000 + assert json_data["request"]["method"] == "GET" + assert len(json_data["endpoints"]) == 1 + + mock_service.get_info.assert_awaited_once() + app.dependency_overrides.clear() diff --git a/app_python/tests/root/test_service.py b/app_python/tests/root/test_service.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pulumi/Pulumi.dev.yaml b/pulumi/Pulumi.dev.yaml new file mode 100644 index 0000000000..197dd9c9f6 --- /dev/null +++ b/pulumi/Pulumi.dev.yaml @@ -0,0 +1,5 @@ +config: + yc-infra:cloudId: b1glhaar472redp2m3to + yc-infra:folderId: b1g1uj21m7e1md6p212p + yc-infra:zone: ru-central1-a + yc-infra:myIp: 188.130.155.177/32 diff --git a/pulumi/Pulumi.yaml b/pulumi/Pulumi.yaml new file mode 100644 index 0000000000..69d3b70134 --- /dev/null +++ b/pulumi/Pulumi.yaml @@ -0,0 +1,4 @@ +name: yc-infra +runtime: + name: python +description: Yandex Cloud infrastructure via Pulumi diff --git a/pulumi/main.py b/pulumi/main.py new file mode 100644 index 0000000000..9a6b994337 --- /dev/null +++ b/pulumi/main.py @@ -0,0 +1,100 @@ +import pulumi +import pulumi_yandex as yc +from pulumi import Config + +config = Config() + +cloud_id = config.require("cloudId") +folder_id = config.require("folderId") +zone = config.require("zone") +my_ip = config.require("myIp") + +provider = yc.Provider( + "yc-provider", + cloud_id=cloud_id, + folder_id=folder_id, + zone=zone, + service_account_key_file="authorized_key.json" +) + +network = yc.VpcNetwork( + "net", + name="net", + opts=pulumi.ResourceOptions(provider=provider) +) + +subnet = yc.VpcSubnet( + "subnet", + name="subnet", + zone=zone, + network_id=network.id, + v4_cidr_blocks=["10.0.0.0/24"], + opts=pulumi.ResourceOptions(provider=provider) +) + +# Security Group +security_group = yc.VpcSecurityGroup( + "sg", + network_id=network.id, + ingress=[ + yc.VpcSecurityGroupIngressArgs( + protocol="TCP", + description="SSH", + v4_cidr_blocks=[my_ip], + port=22 + ), + yc.VpcSecurityGroupIngressArgs( + protocol="TCP", + description="HTTP", + v4_cidr_blocks=["0.0.0.0/0"], + port=80 + ), + yc.VpcSecurityGroupIngressArgs( + protocol="TCP", + description="App 5000", + v4_cidr_blocks=["0.0.0.0/0"], + port=5000 + ), + ], + egress=[ + yc.VpcSecurityGroupEgressArgs( + protocol="ANY", + v4_cidr_blocks=["0.0.0.0/0"] + ) + ], + opts=pulumi.ResourceOptions(provider=provider) +) + +# Image +image = yc.get_compute_image(family="ubuntu-2204-lts") + +# VM +vm = yc.ComputeInstance( + "vm", + name="pulumi-vm", + platform_id="standard-v2", + zone=zone, + resources=yc.ComputeInstanceResourcesArgs( + cores=2, + memory=2 + ), + boot_disk=yc.ComputeInstanceBootDiskArgs( + initialize_params=yc.ComputeInstanceBootDiskInitializeParamsArgs( + image_id=image.id + ) + ), + network_interfaces=[ + yc.ComputeInstanceNetworkInterfaceArgs( + subnet_id=subnet.id, + nat=True, + security_group_ids=[security_group.id] + ) + ], + metadata={ + "ssh-keys": f"ubuntu:{open('C:/Users/kve10/.ssh/id_ed25519.pub').read()}" + }, + opts=pulumi.ResourceOptions(provider=provider) +) + +pulumi.export("public_ip", vm.network_interfaces.apply(lambda ni: ni[0].nat_ip_address)) +pulumi.export("internal_ip", vm.network_interfaces.apply(lambda ni: ni[0].ip_address)) diff --git a/pulumi/requirements.txt b/pulumi/requirements.txt new file mode 100644 index 0000000000..9dff44f6bc --- /dev/null +++ b/pulumi/requirements.txt @@ -0,0 +1,2 @@ +pulumi>=3.0.0 +pulumi-yandex>=0.187.0 \ No newline at end of file diff --git a/terraform/.gitignore b/terraform/.gitignore new file mode 100644 index 0000000000..122a91f7f4 --- /dev/null +++ b/terraform/.gitignore @@ -0,0 +1,5 @@ +.terraform/ +*.tfstate +*.tfstate.backup +authorized_key.json +terraform.tfvars \ No newline at end of file diff --git a/terraform/default.tf b/terraform/default.tf new file mode 100644 index 0000000000..c4c8a372ae --- /dev/null +++ b/terraform/default.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + yandex = { + source = "yandex-cloud/yandex" + version = "0.187.0" + } + } +} + +provider "yandex" { + service_account_key_file = "authorized_key.json" + cloud_id = var.cloud_id + folder_id = var.folder_id + zone = var.zone +} \ No newline at end of file diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000000..20f9e53a44 --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,90 @@ +data "yandex_compute_image" "ubuntu" { + family = "ubuntu-2204-lts" +} + +resource "yandex_vpc_network" "net-1" { + name = "net" +} + +resource "yandex_vpc_subnet" "subnet-1" { + name = "subnet" + zone = "ru-central1-a" + network_id = yandex_vpc_network.net-1.id + v4_cidr_blocks = ["10.0.0.0/24"] +} + + +resource "yandex_vpc_security_group" "sg1" { + network_id = yandex_vpc_network.net-1.id + + labels = { + my-label = "my-label-value" + } + + ingress { + protocol = "TCP" + description = "rule1 description" + v4_cidr_blocks = ["10.0.1.0/24", "10.0.2.0/24"] + port = 8080 + } + + ingress { + protocol = "ANY" + description = "SSH" + v4_cidr_blocks = ["${var.my_ip}/32"] + port = 22 + } + + ingress { + protocol = "ANY" + description = "HTTP" + v4_cidr_blocks = ["0.0.0.0/0"] + port = 80 + } + + ingress { + protocol = "TCP" + description = "App 5000" + v4_cidr_blocks = ["0.0.0.0/0"] + port = 5000 + } + + egress { + protocol = "ANY" + v4_cidr_blocks = ["0.0.0.0/0"] + } + +} + + +resource "yandex_compute_instance" "vm-1" { + name = "terraform1" + platform_id = var.instance_platform + zone = var.zone + + resources { + cores = var.instance_cores + memory = var.instance_memory + } + + boot_disk { + initialize_params { + image_id = data.yandex_compute_image.ubuntu.id + } + } + + network_interface { + nat = true + subnet_id = yandex_vpc_subnet.subnet-1.id + security_group_ids = [yandex_vpc_security_group.sg1.id] + } + + metadata = { + ssh-keys = "ubuntu:${file(var.ssh_public_key_path)}" + } + + labels = { + environment = "dev" + project = "terraform-lab" + } +} diff --git a/terraform/outputs.tf b/terraform/outputs.tf new file mode 100644 index 0000000000..7deea72e3a --- /dev/null +++ b/terraform/outputs.tf @@ -0,0 +1,7 @@ +output "public_ip" { + value = yandex_compute_instance.vm-1.network_interface[0].nat_ip_address +} + +output "internal_ip" { + value = yandex_compute_instance.vm-1.network_interface[0].ip_address +} diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000000..7506f29096 --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,36 @@ +variable "cloud_id" { + type = string +} + +variable "folder_id" { + type = string +} + +variable "zone" { + type = string + default = "ru-central1-a" +} + +variable "instance_platform" { + type = string + default = "standard-v2" +} + +variable "instance_cores" { + type = number + default = 2 +} + +variable "instance_memory" { + type = number + default = 2 +} + +variable "ssh_public_key_path" { + type = string + default = "~/.ssh/id_ed25519.pub" +} + +variable "my_ip" { + type = string +} \ No newline at end of file