+
Targets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Type |
+ Ready |
+ Labels |
+ Details |
+
+
+
+
+
+
+ Docker
+ |
+
+
+ true
+
+ |
+
+
+
+ __address__="172.18.0.6:8000"
+
+ __meta_docker_container_id="ffe5cbb34d59e89d7fdb0dca0be0189893e8de610881c95c2be84d1b1c195f28"
+
+ __meta_docker_container_label_app="devops-python"
+
+ __meta_docker_container_label_com_docker_compose_config_hash="d7517c5a68fb6bf1bf05c028fbfee6a8753bea644b611c0ab61dbb51e8109abf"
+
+ __meta_docker_container_label_com_docker_compose_container_number="1"
+
+ __meta_docker_container_label_com_docker_compose_depends_on=""
+
+ __meta_docker_container_label_com_docker_compose_image="sha256:b96ea47427f533a9a5d1f84ed7b35673b94fc546497982b8a92a7256ef397521"
+
+ __meta_docker_container_label_com_docker_compose_image_builder="classic"
+
+ __meta_docker_container_label_com_docker_compose_oneoff="False"
+
+ __meta_docker_container_label_com_docker_compose_project="monitoring"
+
+ __meta_docker_container_label_com_docker_compose_project_config_files="/Users/newspec/Desktop/DevOps/DevOps-Core-Course/monitoring/docker-compose.yml"
+
+ __meta_docker_container_label_com_docker_compose_project_working_dir="/Users/newspec/Desktop/DevOps/DevOps-Core-Course/monitoring"
+
+ __meta_docker_container_label_com_docker_compose_replace="app-python"
+
+ __meta_docker_container_label_com_docker_compose_service="app-python"
+
+ __meta_docker_container_label_com_docker_compose_version="5.1.0"
+
+ __meta_docker_container_label_logging="promtail"
+
+ __meta_docker_container_name="/app-python"
+
+ __meta_docker_container_network_mode="monitoring_logging"
+
+ __meta_docker_network_id="dbbae221773ea21a02a0ec784e0d0e4cc26fb8aaaeb96c20f922fd85ec49629c"
+
+ __meta_docker_network_ingress="false"
+
+ __meta_docker_network_internal="false"
+
+ __meta_docker_network_ip="172.18.0.6"
+
+ __meta_docker_network_label_com_docker_compose_config_hash="ddec219b739fc99508f3c08de6c29964e557ed6549f4f58bb6df60e82e20dbb5"
+
+ __meta_docker_network_label_com_docker_compose_network="logging"
+
+ __meta_docker_network_label_com_docker_compose_project="monitoring"
+
+ __meta_docker_network_label_com_docker_compose_version="5.1.0"
+
+ __meta_docker_network_name="monitoring_logging"
+
+ __meta_docker_network_scope="local"
+
+ __meta_docker_port_private="8000"
+
+ __meta_docker_port_public="8000"
+
+ __meta_docker_port_public_ip="0.0.0.0"
+
+
+ |
+
+
+ |
+
+
+
+
+ Docker
+ |
+
+
+ true
+
+ |
+
+
+
+ __address__="172.18.0.5:8080"
+
+ __meta_docker_container_id="c75882b0895b26287815c4e9e8916e0b17e476db2871b6f3c4411e2b15937ef7"
+
+ __meta_docker_container_label_app="devops-go"
+
+ __meta_docker_container_label_com_docker_compose_config_hash="ed019c72ac77a3d405b4a4f5b01db8d1b8a965f8f2866ac5c73d16993f7a9918"
+
+ __meta_docker_container_label_com_docker_compose_container_number="1"
+
+ __meta_docker_container_label_com_docker_compose_depends_on=""
+
+ __meta_docker_container_label_com_docker_compose_image="sha256:fa3df4a039dcccba11cdd2b72d01db76094b517186e171e2c8dfea2a1bd469c4"
+
+ __meta_docker_container_label_com_docker_compose_image_builder="classic"
+
+ __meta_docker_container_label_com_docker_compose_oneoff="False"
+
+ __meta_docker_container_label_com_docker_compose_project="monitoring"
+
+ __meta_docker_container_label_com_docker_compose_project_config_files="/Users/newspec/Desktop/DevOps/DevOps-Core-Course/monitoring/docker-compose.yml"
+
+ __meta_docker_container_label_com_docker_compose_project_working_dir="/Users/newspec/Desktop/DevOps/DevOps-Core-Course/monitoring"
+
+ __meta_docker_container_label_com_docker_compose_service="app-go"
+
+ __meta_docker_container_label_com_docker_compose_version="5.1.0"
+
+ __meta_docker_container_label_logging="promtail"
+
+ __meta_docker_container_name="/app-go"
+
+ __meta_docker_container_network_mode="monitoring_logging"
+
+ __meta_docker_network_id="dbbae221773ea21a02a0ec784e0d0e4cc26fb8aaaeb96c20f922fd85ec49629c"
+
+ __meta_docker_network_ingress="false"
+
+ __meta_docker_network_internal="false"
+
+ __meta_docker_network_ip="172.18.0.5"
+
+ __meta_docker_network_label_com_docker_compose_config_hash="ddec219b739fc99508f3c08de6c29964e557ed6549f4f58bb6df60e82e20dbb5"
+
+ __meta_docker_network_label_com_docker_compose_network="logging"
+
+ __meta_docker_network_label_com_docker_compose_project="monitoring"
+
+ __meta_docker_network_label_com_docker_compose_version="5.1.0"
+
+ __meta_docker_network_name="monitoring_logging"
+
+ __meta_docker_network_scope="local"
+
+ __meta_docker_port_private="8080"
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+```
+
+### Generate Test Traffic
+
+```bash
+# Generate successful requests
+for i in {1..20}; do
+ curl http://localhost:8000/
+ curl http://localhost:8000/health
+done
+
+# Generate error requests
+for i in {1..10}; do
+ curl http://localhost:8000/nonexistent-$i
+done
+```
+
+### Verify Logs in Grafana
+
+1. Open Grafana: http://localhost:3000
+2. Login: `*****`
+3. Navigate to **Explore**
+4. Select **Loki** data source
+5. Try these queries:
+
+```logql
+# All logs
+{app="devops-python"}
+
+# Only errors
+{app="devops-python", level="ERROR"}
+
+# Only INFO logs
+{app="devops-python", level="INFO"}
+
+# Count by level
+sum by (level) (count_over_time({app="devops-python"}[5m]))
+```
+
+### Test LogQL Queries
+
+**Basic filtering:**
+```bash
+# All logs from Python app
+{app="devops-python"}
+
+# Logs from both apps
+{app=~"devops-.*"}
+
+# Only ERROR level
+{app="devops-python", level="ERROR"}
+
+# Specific HTTP method
+{app="devops-python", method="GET"}
+```
+
+**Metrics from logs:**
+```bash
+# Request rate
+rate({app="devops-python"}[1m])
+
+# Count by level
+sum by (level) (count_over_time({app="devops-python"}[5m]))
+
+# Error rate
+sum(rate({app="devops-python", level="ERROR"}[5m]))
+```
+
+---
+
+## 8. Challenges
+
+### Challenge 1: Loki Configuration Errors
+
+**Problem**: Initial Loki startup failed with deprecated configuration fields.
+
+**Error message**:
+```
+error parsing config: yaml: unmarshal errors:
+ line X: field max_look_back_period not found
+```
+
+**Solution**:
+- Removed deprecated `max_look_back_period` from `chunk_store_config`
+- Added `delete_request_store: filesystem` to compactor configuration
+- Updated to TSDB-specific configuration for Loki 3.0
+
+### Challenge 2: Promtail Not Collecting Logs
+
+**Problem**: Promtail wasn't discovering containers or collecting logs.
+
+**Root cause**: Missing Docker socket and container log directory mounts.
+
+**Solution**:
+```yaml
+volumes:
+ - /var/run/docker.sock:/var/run/docker.sock:ro
+ - /var/lib/docker/containers:/var/lib/docker/containers:ro
+```
+
+**Additional fix**: Added label-based filtering to only collect from containers with `logging=promtail` label.
+
+### Challenge 3: Mixed Log Formats
+
+**Problem**: Application logs contained both JSON and plain text (from uvicorn), preventing consistent parsing.
+
+**Example**:
+```
+INFO: 127.0.0.1:52134 - "GET / HTTP/1.1" 200 OK
+{"timestamp": "2026-03-09T22:30:25.607856+00:00", "level": "INFO", ...}
+```
+
+**Solution**:
+- Disabled uvicorn access logs: `uvicorn.run(app, access_log=False, log_config=None)`
+- Ensured all application logs use JSON formatter
+- Result: Pure JSON output for consistent parsing
+
+### Challenge 4: JSON Parsing in LogQL
+
+**Problem**: Query `{app="devops-python"} | json | level="ERROR"` returned "No data" despite ERROR logs existing.
+
+**Root cause**: Docker logs were already JSON-encoded, causing Loki to store them as escaped strings:
+```json
+"{\"timestamp\": \"...\", \"level\": \"ERROR\", ...}"
+```
+
+The `| json` parser couldn't extract fields from escaped JSON.
+
+**Solution**: Added **pipeline stages** to Promtail configuration to parse JSON at collection time:
+
+```yaml
+pipeline_stages:
+ - json:
+ expressions:
+ level: level
+ method: method
+ status_code: status_code
+ - labels:
+ level:
+ method:
+ status_code:
+```
+
+**Result**: Fields are now extracted as labels during ingestion, enabling direct filtering:
+- `{level="ERROR"}` instead of `| json | level="ERROR"`
+- Faster queries (no runtime parsing)
+- Fields indexed at ingestion time
+---
+
+## Evidence of Completion
+
+### Task 1: Deploy Loki Stack
+
+**Screenshot showing logs from at least 3 containers in Grafana Explore:**
+
+
+
+
+
+
+### Task 2: Integrate Your Applications
+
+**Screenshot of JSON log output from your app:**
+
+**Screenshot of Grafana showing logs from both applications:**
+
+
+**At least 3 different LogQL queries that work:**
+- `{app="devops-python"}`
+
+- `{app="devops-python"} |= "ERROR"`
+
+- `{app="devops-python"} | json | method="GET"`
+
+### Task 3: Build Log Dashboard
+
+**Screenshot of your dashboard showing all 4 panels with real data.:**
+
+
+### Task 4: Production Readiness
+
+**docker-compose ps showing all services healthy***
+```
+docker ps
+CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
+9a3014c256e9 monitoring-app-go "./myapp" 11 minutes ago Up 11 minutes 0.0.0.0:8001->8080/tcp, [::]:8001->8080/tcp app-go
+41601c3d6499 grafana/promtail:3.0.0 "/usr/bin/promtail -β¦" 12 minutes ago Up 12 minutes 0.0.0.0:9080->9080/tcp, [::]:9080->9080/tcp promtail
+b68a6c76cc9a grafana/grafana:12.3.1 "/run.sh" 12 minutes ago Up 12 minutes (healthy) 0.0.0.0:3000->3000/tcp, [::]:3000->3000/tcp grafana
+2b62dd0622f8 grafana/loki:3.0.0 "/usr/bin/loki -confβ¦" 12 minutes ago Up 12 minutes (healthy) 0.0.0.0:3100->3100/tcp, [::]:3100->3100/tcp loki
+ffe5cbb34d59 monitoring-app-python "python app.py" 57 minutes ago Up 57 minutes 0.0.0.0:8000->8000/tcp, [::]:8000->8000/tcp app-python
+```
+**Screenshot of Grafana login page (no anonymous access):**
+
+
+### Bonus β Ansible Automation
+**Ansible playbook execution output:**
+```bash
+Using /Users/newspec/Desktop/DevOps/DevOps-Core-Course/ansible/ansible.cfg as config file
+
+PLAY [Deploy Monitoring Stack] **********************************************************************************************************************************
+
+TASK [Gathering Facts] ******************************************************************************************************************************************
+ok: [localhost]
+
+TASK [monitoring : Include setup tasks] *************************************************************************************************************************
+included: /Users/newspec/Desktop/DevOps/DevOps-Core-Course/ansible/roles/monitoring/tasks/setup.yml for localhost
+
+TASK [monitoring : Create monitoring directory structure] *******************************************************************************************************
+ok: [localhost] => (item=/Users/newspec/Desktop/DevOps/DevOps-Core-Course/ansible/../monitoring) => {"ansible_loop_var": "item", "changed": false, "gid": 20, "group": "staff", "item": "/Users/newspec/Desktop/DevOps/DevOps-Core-Course/ansible/../monitoring", "mode": "0755", "owner": "newspec", "path": "/Users/newspec/Desktop/DevOps/DevOps-Core-Course/ansible/../monitoring", "size": 224, "state": "directory", "uid": 501}
+ok: [localhost] => (item=/Users/newspec/Desktop/DevOps/DevOps-Core-Course/ansible/../monitoring/loki) => {"ansible_loop_var": "item", "changed": false, "gid": 20, "group": "staff", "item": "/Users/newspec/Desktop/DevOps/DevOps-Core-Course/ansible/../monitoring/loki", "mode": "0755", "owner": "newspec", "path": "/Users/newspec/Desktop/DevOps/DevOps-Core-Course/ansible/../monitoring/loki", "size": 96, "state": "directory", "uid": 501}
+ok: [localhost] => (item=/Users/newspec/Desktop/DevOps/DevOps-Core-Course/ansible/../monitoring/promtail) => {"ansible_loop_var": "item", "changed": false, "gid": 20, "group": "staff", "item": "/Users/newspec/Desktop/DevOps/DevOps-Core-Course/ansible/../monitoring/promtail", "mode": "0755", "owner": "newspec", "path": "/Users/newspec/Desktop/DevOps/DevOps-Core-Course/ansible/../monitoring/promtail", "size": 96, "state": "directory", "uid": 501}
+ok: [localhost] => (item=/Users/newspec/Desktop/DevOps/DevOps-Core-Course/ansible/../monitoring/docs) => {"ansible_loop_var": "item", "changed": false, "gid": 20, "group": "staff", "item": "/Users/newspec/Desktop/DevOps/DevOps-Core-Course/ansible/../monitoring/docs", "mode": "0755", "owner": "newspec", "path": "/Users/newspec/Desktop/DevOps/DevOps-Core-Course/ansible/../monitoring/docs", "size": 544, "state": "directory", "uid": 501}
+
+TASK [monitoring : Template Loki configuration] *****************************************************************************************************************
+ok: [localhost] => {"changed": false, "checksum": "d42de2d0cd64379828e0bf9003a88aeceff2f0b1", "dest": "/Users/newspec/Desktop/DevOps/DevOps-Core-Course/ansible/../monitoring/loki/config.yml", "gid": 20, "group": "staff", "mode": "0644", "owner": "newspec", "path": "/Users/newspec/Desktop/DevOps/DevOps-Core-Course/ansible/../monitoring/loki/config.yml", "size": 1457, "state": "file", "uid": 501}
+
+TASK [monitoring : Template Promtail configuration] *************************************************************************************************************
+ok: [localhost] => {"changed": false, "checksum": "af82481cc89df3f966895d245c9433a2e0c2e411", "dest": "/Users/newspec/Desktop/DevOps/DevOps-Core-Course/ansible/../monitoring/promtail/config.yml", "gid": 20, "group": "staff", "mode": "0644", "owner": "newspec", "path": "/Users/newspec/Desktop/DevOps/DevOps-Core-Course/ansible/../monitoring/promtail/config.yml", "size": 1731, "state": "file", "uid": 501}
+
+TASK [monitoring : Template Docker Compose file] ****************************************************************************************************************
+changed: [localhost] => {"changed": true, "checksum": "511a7e9611d7defd5ab4b7d1a5549b1c4d6956de", "dest": "/Users/newspec/Desktop/DevOps/DevOps-Core-Course/ansible/../monitoring/docker-compose.yml", "gid": 20, "group": "staff", "md5sum": "bb3a3b54449ce0caed008059ffc5f4ed", "mode": "0644", "owner": "newspec", "size": 2185, "src": "/Users/newspec/.ansible/tmp/ansible-tmp-1773101608.078568-37435-183065845996472/.source.yml", "state": "file", "uid": 501}
+
+TASK [monitoring : Create .env file for secrets] ****************************************************************************************************************
+changed: [localhost] => {"censored": "the output has been hidden due to the fact that 'no_log: true' was specified for this result", "changed": true}
+
+TASK [monitoring : Include deployment tasks] ********************************************************************************************************************
+included: /Users/newspec/Desktop/DevOps/DevOps-Core-Course/ansible/roles/monitoring/tasks/deploy.yml for localhost
+
+TASK [monitoring : Deploy monitoring stack with Docker Compose] *************************************************************************************************
+[ERROR]: Task failed: Module failed: failed to connect to the docker API at unix:///var/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /var/run/docker.sock: connect: no such file or directory
+Origin: /Users/newspec/Desktop/DevOps/DevOps-Core-Course/ansible/roles/monitoring/tasks/deploy.yml:4:3
+
+2 # Deployment tasks for monitoring stack
+3
+4 - name: Deploy monitoring stack with Docker Compose
+ ^ column 3
+
+fatal: [localhost]: FAILED! => {"changed": false, "cmd": "/opt/homebrew/bin/docker --host unix:///var/run/docker.sock version --format '{{ json . }}'", "msg": "failed to connect to the docker API at unix:///var/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /var/run/docker.sock: connect: no such file or directory", "rc": 1, "stderr": "failed to connect to the docker API at unix:///var/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /var/run/docker.sock: connect: no such file or directory\n", "stderr_lines": ["failed to connect to the docker API at unix:///var/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /var/run/docker.sock: connect: no such file or directory"], "stdout": "{\"Client\":{\"Platform\":{\"Name\":\"Docker Engine - Community\"},\"Version\":\"29.3.0\",\"ApiVersion\":\"1.54\",\"DefaultAPIVersion\":\"1.54\",\"GitCommit\":\"5927d80c76\",\"GoVersion\":\"go1.26.1\",\"Os\":\"darwin\",\"Arch\":\"arm64\",\"BuildTime\":\"Thu Mar 5 14:22:32 2026\",\"Context\":\"default\"},\"Server\":null}\n", "stdout_lines": ["{\"Client\":{\"Platform\":{\"Name\":\"Docker Engine - Community\"},\"Version\":\"29.3.0\",\"ApiVersion\":\"1.54\",\"DefaultAPIVersion\":\"1.54\",\"GitCommit\":\"5927d80c76\",\"GoVersion\":\"go1.26.1\",\"Os\":\"darwin\",\"Arch\":\"arm64\",\"BuildTime\":\"Thu Mar 5 14:22:32 2026\",\"Context\":\"default\"},\"Server\":null}"]}
+
+PLAY RECAP ******************************************************************************************************************************************************
+localhost : ok=8 changed=2 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
+
+newspec@172 ansible %
+```
+**Idempotency test (run twice, second shows no changes):**
+```bash
+LAY [Deploy Monitoring Stack] *************************************************
+
+TASK [Gathering Facts] *********************************************************
+ok: [localhost]
+
+TASK [monitoring : Include setup tasks] ****************************************
+included: /Users/newspec/Desktop/DevOps/DevOps-Core-Course/ansible/roles/monitoring/tasks/setup.yml for localhost
+
+TASK [monitoring : Create monitoring directory structure] **********************
+ok: [localhost] => (item=/Users/newspec/Desktop/DevOps/DevOps-Core-Course/ansible/../monitoring)
+ok: [localhost] => (item=/Users/newspec/Desktop/DevOps/DevOps-Core-Course/ansible/../monitoring/loki)
+ok: [localhost] => (item=/Users/newspec/Desktop/DevOps/DevOps-Core-Course/ansible/../monitoring/promtail)
+ok: [localhost] => (item=/Users/newspec/Desktop/DevOps/DevOps-Core-Course/ansible/../monitoring/docs)
+
+TASK [monitoring : Template Loki configuration] ********************************
+ok: [localhost]
+
+TASK [monitoring : Template Promtail configuration] ****************************
+ok: [localhost]
+
+TASK [monitoring : Template Docker Compose file] *******************************
+ok: [localhost]
+
+TASK [monitoring : Create .env file for secrets] *******************************
+ok: [localhost]
+
+TASK [Display access information] **********************************************
+ok: [localhost] => {
+ "msg": "========================================\nMonitoring Stack Deployed Successfully!\n========================================\n\nGrafana UI: http://localhost:3000\nUsername: admin\nPassword: admin123\n\nLoki API: http://localhost:3100\n\nNext Steps:\n1. Open Grafana in your browser\n2. Add Loki data source (http://loki:3100)\n3. Explore logs in the Explore tab\n4. Create dashboards for your applications\n\n========================================"
+}
+
+PLAY RECAP *********************************************************************
+localhost : ok=8 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
+
+
+=== Idempotency Check ===
+localhost : ok=8 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
+```
\ No newline at end of file
diff --git a/monitoring/docs/image-1.png b/monitoring/docs/image-1.png
new file mode 100644
index 0000000000..0cbcdaa411
Binary files /dev/null and b/monitoring/docs/image-1.png differ
diff --git a/monitoring/docs/image-10.png b/monitoring/docs/image-10.png
new file mode 100644
index 0000000000..f2347ba966
Binary files /dev/null and b/monitoring/docs/image-10.png differ
diff --git a/monitoring/docs/image-11.png b/monitoring/docs/image-11.png
new file mode 100644
index 0000000000..3657509025
Binary files /dev/null and b/monitoring/docs/image-11.png differ
diff --git a/monitoring/docs/image-12.png b/monitoring/docs/image-12.png
new file mode 100644
index 0000000000..f9fa917fc6
Binary files /dev/null and b/monitoring/docs/image-12.png differ
diff --git a/monitoring/docs/image-13.png b/monitoring/docs/image-13.png
new file mode 100644
index 0000000000..8d4bd0ea5a
Binary files /dev/null and b/monitoring/docs/image-13.png differ
diff --git a/monitoring/docs/image-2.png b/monitoring/docs/image-2.png
new file mode 100644
index 0000000000..e4868d2099
Binary files /dev/null and b/monitoring/docs/image-2.png differ
diff --git a/monitoring/docs/image-3.png b/monitoring/docs/image-3.png
new file mode 100644
index 0000000000..e539c1f7f0
Binary files /dev/null and b/monitoring/docs/image-3.png differ
diff --git a/monitoring/docs/image-4.png b/monitoring/docs/image-4.png
new file mode 100644
index 0000000000..d11e230e0a
Binary files /dev/null and b/monitoring/docs/image-4.png differ
diff --git a/monitoring/docs/image-5.png b/monitoring/docs/image-5.png
new file mode 100644
index 0000000000..9eab0bbe58
Binary files /dev/null and b/monitoring/docs/image-5.png differ
diff --git a/monitoring/docs/image-6.png b/monitoring/docs/image-6.png
new file mode 100644
index 0000000000..911ebcd52f
Binary files /dev/null and b/monitoring/docs/image-6.png differ
diff --git a/monitoring/docs/image-7.png b/monitoring/docs/image-7.png
new file mode 100644
index 0000000000..c31195b8ac
Binary files /dev/null and b/monitoring/docs/image-7.png differ
diff --git a/monitoring/docs/image-8.png b/monitoring/docs/image-8.png
new file mode 100644
index 0000000000..3e8a73b178
Binary files /dev/null and b/monitoring/docs/image-8.png differ
diff --git a/monitoring/docs/image-9.png b/monitoring/docs/image-9.png
new file mode 100644
index 0000000000..0fec9e58bf
Binary files /dev/null and b/monitoring/docs/image-9.png differ
diff --git a/monitoring/docs/image.png b/monitoring/docs/image.png
new file mode 100644
index 0000000000..0cfcd7dddd
Binary files /dev/null and b/monitoring/docs/image.png differ
diff --git a/monitoring/loki/config.yml b/monitoring/loki/config.yml
new file mode 100644
index 0000000000..2ff6d38004
--- /dev/null
+++ b/monitoring/loki/config.yml
@@ -0,0 +1,75 @@
+auth_enabled: false
+
+server:
+ http_listen_port: 3100
+ grpc_listen_port: 9096
+
+common:
+ instance_addr: 127.0.0.1
+ path_prefix: /loki
+ storage:
+ filesystem:
+ chunks_directory: /loki/chunks
+ rules_directory: /loki/rules
+ replication_factor: 1
+ ring:
+ kvstore:
+ store: inmemory
+
+query_range:
+ results_cache:
+ cache:
+ embedded_cache:
+ enabled: true
+ max_size_mb: 100
+
+schema_config:
+ configs:
+ - from: 2024-01-01
+ store: tsdb
+ object_store: filesystem
+ schema: v13
+ index:
+ prefix: index_
+ period: 24h
+
+storage_config:
+ tsdb_shipper:
+ active_index_directory: /loki/tsdb-index
+ cache_location: /loki/tsdb-cache
+ cache_ttl: 24h
+ filesystem:
+ directory: /loki/chunks
+
+limits_config:
+ retention_period: 168h
+ reject_old_samples: true
+ reject_old_samples_max_age: 168h
+ ingestion_rate_mb: 10
+ ingestion_burst_size_mb: 20
+ max_query_series: 500
+ max_query_parallelism: 32
+
+compactor:
+ working_directory: /loki/compactor
+ compaction_interval: 10m
+ retention_enabled: true
+ retention_delete_delay: 2h
+ retention_delete_worker_count: 150
+ delete_request_store: filesystem
+
+table_manager:
+ retention_deletes_enabled: true
+ retention_period: 168h
+
+ruler:
+ storage:
+ type: local
+ local:
+ directory: /loki/rules
+ rule_path: /loki/rules-temp
+ alertmanager_url: http://localhost:9093
+ ring:
+ kvstore:
+ store: inmemory
+ enable_api: true
\ No newline at end of file
diff --git a/monitoring/promtail/config.yml b/monitoring/promtail/config.yml
new file mode 100644
index 0000000000..170aca60f3
--- /dev/null
+++ b/monitoring/promtail/config.yml
@@ -0,0 +1,63 @@
+server:
+ http_listen_port: 9080
+ grpc_listen_port: 0
+
+positions:
+ filename: /tmp/positions.yaml
+
+clients:
+ - url: http://loki:3100/loki/api/v1/push
+
+scrape_configs:
+ - job_name: docker
+ docker_sd_configs:
+ - host: unix:///var/run/docker.sock
+ refresh_interval: 5s
+ filters:
+ - name: label
+ values: ["logging=promtail"]
+ relabel_configs:
+ # Extract container name
+ - source_labels: ['__meta_docker_container_name']
+ regex: '/(.*)'
+ target_label: 'container'
+ # Extract container ID
+ - source_labels: ['__meta_docker_container_id']
+ target_label: 'container_id'
+ # Extract image name
+ - source_labels: ['__meta_docker_container_label_com_docker_compose_service']
+ target_label: 'service'
+ # Extract app label if present
+ - source_labels: ['__meta_docker_container_label_app']
+ target_label: 'app'
+ # Add job label
+ - source_labels: ['__meta_docker_container_label_logging']
+ target_label: 'job'
+ replacement: 'docker'
+
+ # Pipeline stages to parse JSON logs
+ pipeline_stages:
+ # Parse JSON from log line
+ - json:
+ expressions:
+ level: level
+ message: message
+ timestamp: timestamp
+ logger: logger
+ service: service
+ method: method
+ path: path
+ status_code: status_code
+ client_ip: client_ip
+
+ # Add extracted fields as labels
+ - labels:
+ level:
+ service:
+ method:
+ status_code:
+
+ # Use timestamp from log if available
+ - timestamp:
+ source: timestamp
+ format: RFC3339Nano
\ No newline at end of file
diff --git a/pulumi/Pulumi.yaml b/pulumi/Pulumi.yaml
new file mode 100644
index 0000000000..0e9b5069df
--- /dev/null
+++ b/pulumi/Pulumi.yaml
@@ -0,0 +1,3 @@
+name: lab04-pulumi
+runtime: python
+description: Lab 04 - Infrastructure as Code with Pulumi (Yandex Cloud)
\ No newline at end of file
diff --git a/pulumi/__main__.py b/pulumi/__main__.py
new file mode 100644
index 0000000000..e719228a87
--- /dev/null
+++ b/pulumi/__main__.py
@@ -0,0 +1,150 @@
+"""
+Lab 04 - Pulumi Infrastructure as Code
+Provisions a VM on Yandex Cloud with network and security configuration
+"""
+
+import pulumi
+import pulumi_yandex as yandex
+
+# Get configuration
+config = pulumi.Config()
+folder_id = config.require("folder_id")
+zone = config.get("zone") or "ru-central1-a"
+vm_name = config.get("vm_name") or "lab04-pulumi-vm"
+ssh_user = config.get("ssh_user") or "ubuntu"
+ssh_public_key_path = config.get("ssh_public_key_path") or "~/.ssh/id_rsa.pub"
+# CIDR allowed to SSH, e.g. 203.0.113.10/32
+ssh_allowed_cidr = config.require("ssh_allowed_cidr")
+
+# Read SSH public key
+ssh_key_expanded = ssh_public_key_path.replace(
+ "~", pulumi.runtime.get_config("HOME") or "~"
+)
+with open(ssh_key_expanded, "r") as f:
+ ssh_public_key = f.read().strip()
+
+# Get latest Ubuntu 24.04 image
+ubuntu_image = yandex.get_compute_image(
+ family="ubuntu-2404-lts",
+ folder_id="standard-images"
+)
+
+# Create VPC network
+network = yandex.VpcNetwork(
+ "lab04-network",
+ name="lab04-pulumi-network",
+ description="Network for Lab 04 Pulumi VM",
+ folder_id=folder_id
+)
+
+# Create subnet
+subnet = yandex.VpcSubnet(
+ "lab04-subnet",
+ name="lab04-pulumi-subnet",
+ description="Subnet for Lab 04 Pulumi VM",
+ v4_cidr_blocks=["10.128.0.0/24"],
+ zone=zone,
+ network_id=network.id,
+ folder_id=folder_id
+)
+
+# Create security group
+security_group = yandex.VpcSecurityGroup(
+ "lab04-sg",
+ name="lab04-pulumi-security-group",
+ description="Security group for Lab 04 Pulumi VM",
+ network_id=network.id,
+ folder_id=folder_id,
+ ingresses=[
+ # Allow SSH from specific IP
+ yandex.VpcSecurityGroupIngressArgs(
+ protocol="TCP",
+ description="Allow SSH from my IP",
+ v4_cidr_blocks=[ssh_allowed_cidr],
+ port=22
+ ),
+ # Allow HTTP
+ yandex.VpcSecurityGroupIngressArgs(
+ protocol="TCP",
+ description="Allow HTTP",
+ v4_cidr_blocks=["0.0.0.0/0"],
+ port=80
+ ),
+ # Allow custom port 5000
+ yandex.VpcSecurityGroupIngressArgs(
+ protocol="TCP",
+ description="Allow app port 5000",
+ v4_cidr_blocks=["0.0.0.0/0"],
+ port=5000
+ ),
+ ],
+ egresses=[
+ # Allow all outbound traffic
+ yandex.VpcSecurityGroupEgressArgs(
+ protocol="ANY",
+ description="Allow all outbound traffic",
+ v4_cidr_blocks=["0.0.0.0/0"],
+ from_port=0,
+ to_port=65535
+ ),
+ ]
+)
+
+# Create VM instance
+vm = yandex.ComputeInstance(
+ "lab04-vm",
+ name=vm_name,
+ hostname=vm_name,
+ platform_id="standard-v2",
+ zone=zone,
+ folder_id=folder_id,
+ resources=yandex.ComputeInstanceResourcesArgs(
+ cores=2,
+ memory=1,
+ core_fraction=20 # Free tier: 20% CPU
+ ),
+ boot_disk=yandex.ComputeInstanceBootDiskArgs(
+ initialize_params=yandex.ComputeInstanceBootDiskInitializeParamsArgs(
+ image_id=ubuntu_image.id,
+ size=10, # 10 GB
+ type="network-hdd"
+ )
+ ),
+ network_interfaces=[
+ yandex.ComputeInstanceNetworkInterfaceArgs(
+ subnet_id=subnet.id,
+ nat=True, # Assign public IP
+ security_group_ids=[security_group.id]
+ )
+ ],
+ metadata={
+ "ssh-keys": f"{ssh_user}:{ssh_public_key}"
+ },
+ labels={
+ "environment": "lab04",
+ "managed_by": "pulumi",
+ "purpose": "devops-course"
+ },
+ scheduling_policy=yandex.ComputeInstanceSchedulingPolicyArgs(
+ preemptible=False
+ )
+)
+
+# Export outputs
+pulumi.export("vm_id", vm.id)
+pulumi.export("vm_name", vm.name)
+pulumi.export("vm_public_ip", vm.network_interfaces[0].nat_ip_address)
+pulumi.export("vm_private_ip", vm.network_interfaces[0].ip_address)
+pulumi.export("network_id", network.id)
+pulumi.export("subnet_id", subnet.id)
+pulumi.export("ssh_command", vm.network_interfaces[0].nat_ip_address.apply(
+ lambda ip: f"ssh {ssh_user}@{ip}"
+))
+pulumi.export("connection_info", {
+ "public_ip": vm.network_interfaces[0].nat_ip_address,
+ "private_ip": vm.network_interfaces[0].ip_address,
+ "ssh_user": ssh_user,
+ "ssh_command": vm.network_interfaces[0].nat_ip_address.apply(
+ lambda ip: f"ssh {ssh_user}@{ip}"
+ )
+})
diff --git a/pulumi/requirements.txt b/pulumi/requirements.txt
new file mode 100644
index 0000000000..2356228903
--- /dev/null
+++ b/pulumi/requirements.txt
@@ -0,0 +1,2 @@
+pulumi>=3.0.0,<4.0.0
+pulumi-yandex>=0.13.0
\ No newline at end of file
diff --git a/terraform/.terraformrc b/terraform/.terraformrc
new file mode 100644
index 0000000000..9bc7728211
--- /dev/null
+++ b/terraform/.terraformrc
@@ -0,0 +1,9 @@
+provider_installation {
+ network_mirror {
+ url = "https://terraform-mirror.yandexcloud.net/"
+ include = ["registry.terraform.io/*/*"]
+ }
+ direct {
+ exclude = ["registry.terraform.io/*/*"]
+ }
+}
\ No newline at end of file
diff --git a/terraform/.tflint.hcl b/terraform/.tflint.hcl
new file mode 100644
index 0000000000..96e00d361b
--- /dev/null
+++ b/terraform/.tflint.hcl
@@ -0,0 +1,24 @@
+plugin "terraform" {
+ enabled = true
+ preset = "recommended"
+}
+
+rule "terraform_naming_convention" {
+ enabled = true
+}
+
+rule "terraform_documented_variables" {
+ enabled = true
+}
+
+rule "terraform_documented_outputs" {
+ enabled = true
+}
+
+rule "terraform_unused_declarations" {
+ enabled = true
+}
+
+rule "terraform_deprecated_index" {
+ enabled = true
+}
\ No newline at end of file
diff --git a/terraform/github.tf b/terraform/github.tf
new file mode 100644
index 0000000000..39b5a40c02
--- /dev/null
+++ b/terraform/github.tf
@@ -0,0 +1,52 @@
+# GitHub Provider Configuration for Repository Management
+# This file demonstrates importing existing infrastructure into Terraform
+# Note: required_providers for github is defined in main.tf
+
+provider "github" {
+ token = var.github_token
+ owner = var.github_owner
+}
+
+# Import existing DevOps-Core-Course repository
+resource "github_repository" "devops_course" {
+ name = "DevOps-Core-Course"
+ description = "DevOps Engineering: Core Practices - Lab assignments and projects"
+ visibility = "public"
+
+ has_issues = true
+ has_wiki = false
+ has_projects = false
+ has_downloads = true
+
+ allow_merge_commit = true
+ allow_squash_merge = true
+ allow_rebase_merge = true
+ allow_auto_merge = false
+
+ delete_branch_on_merge = true
+
+ topics = [
+ "devops",
+ "terraform",
+ "pulumi",
+ "docker",
+ "kubernetes",
+ "ansible",
+ "ci-cd",
+ "infrastructure-as-code"
+ ]
+}
+
+# Branch protection for master branch (optional)
+resource "github_branch_protection" "master_protection" {
+ repository_id = github_repository.devops_course.node_id
+ pattern = "master"
+
+ required_pull_request_reviews {
+ dismiss_stale_reviews = true
+ require_code_owner_reviews = false
+ required_approving_review_count = 0
+ }
+
+ enforce_admins = false
+}
\ No newline at end of file
diff --git a/terraform/main.tf b/terraform/main.tf
new file mode 100644
index 0000000000..1b24dfba9f
--- /dev/null
+++ b/terraform/main.tf
@@ -0,0 +1,118 @@
+terraform {
+ required_providers {
+ yandex = {
+ source = "yandex-cloud/yandex"
+ version = "~> 0.187"
+ }
+ github = {
+ source = "integrations/github"
+ version = "~> 5.0"
+ }
+ }
+ required_version = ">= 1.9.0"
+}
+
+provider "yandex" {
+ service_account_key_file = pathexpand(var.service_account_key_file)
+ cloud_id = var.cloud_id
+ folder_id = var.folder_id
+ zone = var.zone
+}
+
+# Get latest Ubuntu 24.04 image
+data "yandex_compute_image" "ubuntu" {
+ family = "ubuntu-2404-lts"
+}
+
+# Create VPC network
+resource "yandex_vpc_network" "lab04_network" {
+ name = "lab04-network"
+ description = "Network for Lab 04 VM"
+}
+
+# Create subnet
+resource "yandex_vpc_subnet" "lab04_subnet" {
+ name = "lab04-subnet"
+ description = "Subnet for Lab 04 VM"
+ v4_cidr_blocks = ["10.128.0.0/24"]
+ zone = var.zone
+ network_id = yandex_vpc_network.lab04_network.id
+}
+
+# Create security group with required rules
+resource "yandex_vpc_security_group" "lab04_sg" {
+ name = "lab04-sg"
+ description = "Lab04 security group"
+ network_id = yandex_vpc_network.lab04_network.id
+
+ ingress {
+ protocol = "TCP"
+ description = "SSH from my IP"
+ v4_cidr_blocks = [var.ssh_allowed_cidr]
+ port = 22
+ }
+
+ ingress {
+ protocol = "TCP"
+ 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"
+ description = "Allow all egress"
+ v4_cidr_blocks = ["0.0.0.0/0"]
+ from_port = 0
+ to_port = 65535
+ }
+}
+
+# Create VM instance
+resource "yandex_compute_instance" "lab04_vm" {
+ name = var.vm_name
+ hostname = var.vm_name
+ platform_id = "standard-v2"
+ zone = var.zone
+
+ resources {
+ cores = 2
+ memory = 1
+ core_fraction = 20 # Free tier: 20% CPU
+ }
+
+ boot_disk {
+ initialize_params {
+ image_id = data.yandex_compute_image.ubuntu.id
+ size = 10 # 10 GB HDD
+ type = "network-hdd"
+ }
+ }
+
+ network_interface {
+ subnet_id = yandex_vpc_subnet.lab04_subnet.id
+ nat = true # Assign public IP
+ security_group_ids = [yandex_vpc_security_group.lab04_sg.id]
+ }
+
+ metadata = {
+ ssh-keys = "${var.ssh_user}:${file(var.ssh_public_key_path)}"
+ }
+
+ labels = {
+ environment = "lab04"
+ managed_by = "terraform"
+ purpose = "devops-course"
+ }
+
+ scheduling_policy {
+ preemptible = false
+ }
+}
\ No newline at end of file
diff --git a/terraform/outputs.tf b/terraform/outputs.tf
new file mode 100644
index 0000000000..0699bacaae
--- /dev/null
+++ b/terraform/outputs.tf
@@ -0,0 +1,53 @@
+# VM outputs
+output "vm_id" {
+ description = "ID of the created VM"
+ value = yandex_compute_instance.lab04_vm.id
+}
+
+output "vm_name" {
+ description = "Name of the created VM"
+ value = yandex_compute_instance.lab04_vm.name
+}
+
+output "vm_public_ip" {
+ description = "Public IP address of the VM"
+ value = yandex_compute_instance.lab04_vm.network_interface[0].nat_ip_address
+}
+
+output "vm_private_ip" {
+ description = "Private IP address of the VM"
+ value = yandex_compute_instance.lab04_vm.network_interface[0].ip_address
+}
+
+# Network outputs
+output "network_id" {
+ description = "ID of the VPC network"
+ value = yandex_vpc_network.lab04_network.id
+}
+
+output "subnet_id" {
+ description = "ID of the subnet"
+ value = yandex_vpc_subnet.lab04_subnet.id
+}
+
+output "security_group_id" {
+ description = "ID of the security group"
+ value = yandex_vpc_security_group.lab04_sg.id
+}
+
+# SSH connection command
+output "ssh_command" {
+ description = "SSH command to connect to the VM"
+ value = "ssh ${var.ssh_user}@${yandex_compute_instance.lab04_vm.network_interface[0].nat_ip_address}"
+}
+
+# Connection info
+output "connection_info" {
+ description = "Complete connection information"
+ value = {
+ public_ip = yandex_compute_instance.lab04_vm.network_interface[0].nat_ip_address
+ private_ip = yandex_compute_instance.lab04_vm.network_interface[0].ip_address
+ ssh_user = var.ssh_user
+ ssh_command = "ssh ${var.ssh_user}@${yandex_compute_instance.lab04_vm.network_interface[0].nat_ip_address}"
+ }
+}
\ No newline at end of file
diff --git a/terraform/variables.tf b/terraform/variables.tf
new file mode 100644
index 0000000000..5efc57b2e9
--- /dev/null
+++ b/terraform/variables.tf
@@ -0,0 +1,61 @@
+# Yandex Cloud configuration
+variable "cloud_id" {
+ description = "Yandex Cloud ID"
+ type = string
+}
+
+variable "folder_id" {
+ description = "Yandex Cloud folder ID"
+ type = string
+}
+
+variable "service_account_key_file" {
+ description = "Path to service account key file (JSON)"
+ type = string
+ default = "~/.config/yandex-cloud/key.json"
+}
+
+variable "zone" {
+ description = "Yandex Cloud availability zone"
+ type = string
+ default = "ru-central1-a"
+}
+
+# VM configuration
+variable "vm_name" {
+ description = "Name of the virtual machine"
+ type = string
+ default = "lab04-vm"
+}
+
+# SSH configuration
+variable "ssh_user" {
+ description = "SSH username for VM access"
+ type = string
+ default = "ubuntu"
+}
+
+variable "ssh_public_key_path" {
+ description = "Path to SSH public key file"
+ type = string
+ default = "~/.ssh/id_rsa.pub"
+}
+
+variable "ssh_allowed_cidr" {
+ description = "CIDR allowed to SSH, e.g. 203.0.113.10/32"
+ type = string
+}
+
+# GitHub configuration (for bonus task)
+variable "github_token" {
+ description = "GitHub personal access token"
+ type = string
+ sensitive = true
+ default = ""
+}
+
+variable "github_owner" {
+ description = "GitHub repository owner (username or organization)"
+ type = string
+ default = ""
+}
\ No newline at end of file