diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..c4641580bc --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,91 @@ +name: Python CI + +on: + push: + paths: + - 'app_python/**' + pull_request: + paths: + - 'app_python/**' + +jobs: + test: + runs-on: ubuntu-latest + permissions: + contents: read + actions: read + security-events: write + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Cache Python dependencies + uses: actions/cache@v4 + id: python-cache + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + cd app_python + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Ensure visits.txt exists + run: | + mkdir -p /app_python + touch /app_python/visits.txt + chmod 666 /app_python/visits.txt + + - name: Ensure visits file exists + run: echo "0" > app_python/visits.txt + + - name: Run tests + run: pytest ./app_python/unit_test.py -v + + - name: Run Snyk to check for vulnerabilities + uses: snyk/actions/python-3.10@master + with: + args: --skip-unresolved app_python/ + env: + SNYK_TOKEN: ${{ secrets.API_SNYK }} + + deploy: + needs: test + runs-on: ubuntu-latest + permissions: + contents: read + actions: read + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_LOGIN }} + password: ${{ secrets.DOCKERHUB_PASS }} + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + context: ./app_python + file: ./app_python/Dockerfile + push: true + tags: ${{ secrets.DOCKERHUB_LOGIN }}/flask_app:latest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..8b63d63bcf --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +env/ +venv/ +*.env +instance/ +*.log +*.pot +*.pyc +.vscode/ +.idea/ +.DS_Store +Thumbs.db +.coverage +nosetests.xml +*.cover +*.hypothesis/ +*.tox/ +*.nox/ +*.coverage +venv/ +.env/ +**/.terraform/ +**/.terraform.lock.hcl +**/terraform.tfstate +**/terraform.tfstate.backup +yandex/key.json +**/terraform.tfstate.*.backup + diff --git a/README.md b/README.md index d475641123..79e14d5ecf 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,4 @@ -# DevOps Engineering Labs - -## Introduction - -Welcome to the DevOps Engineering course labs! These hands-on labs are designed to guide you through various aspects of DevOps practices and principles. As you progress through the labs, you'll gain practical experience in application development, containerization, testing, infrastructure setup, CI/CD processes, and more. - -## Architecture - -This repository has a master branch containing an introduction. Each new lab assignment will be added as a markdown file with a lab number. - -## Rules - -To successfully complete the labs and pass the course, follow these rules: - -1. **Lab Dependency:** Complete the labs in order; each lab builds upon the previous one. -2. **Submission and Grading:** Submit your solutions as pull requests (PRs) to the master branch of this repository. You need at least 6/10 points for each lab to pass. -3. **Fork Repository:** Fork this repository to your workspace to create your own version for solving the labs. -4. **Recommended Workflow:** Build your solutions incrementally. Complete lab N based on lab N-1. -5. **PR Creation:** Create a PR from your fork to the master branch of this repository and from your fork's branch to your fork's master branch. -6. **Wait for Grade:** Once your PR is created, wait for your lab to be reviewed and graded. - -### Example for the first lab - -1. Fork this repository. -2. Checkout to the lab1 branch. -3. Complete the lab1 tasks. -4. Push the code to your repository. -5. Create a PR to the master branch of this repository from your fork's lab1 branch. -6. Create a PR to the master branch of your repository from your lab1 branch. -7. Wait for your grade. - -## Grading and Grades Distribution - -Your final grade will be determined based on labs and a final exam: - -- Labs: 70% of your final grade. -- Final Exam: 30% of your final grade. - -Grade ranges: - -- [90-100] - A -- [75-90) - B -- [60-75) - C -- [0-60) - D - -### Labs Grading - -Each lab is worth 10 points. Completing main tasks correctly earns you 10 points. Completing bonus tasks correctly adds 2.5 points. You can earn a maximum of 12.5 points per lab by completing all main and bonus tasks. - -Finishing all bonus tasks lets you skip the exam and grants you 5 extra points. Incomplete bonus tasks require you to take the exam, which could save you from failing it. - ->The labs account for 70% of your final grade. With 14 labs in total, each lab contributes 5% to your final grade. Completing all main tasks in a lab earns you the maximum 10 points, which corresponds to 5% of your final grade. ->If you successfully complete all bonus tasks, you'll earn an additional 2.5 points, totaling 12.5 points for that lab, or 6.25% of your final grade. Over the course of all 14 labs, the cumulative points from bonus tasks add up to 87.5% of your final grade. ->Additionally, a 5% bonus is granted for successfully finishing all bonus tasks, ensuring that if you successfully complete everything, your final grade will be 92.5%, which corresponds to an A grade. - -## Deadlines and Labs Distribution - -Each week, two new labs will be available. You'll have one week to submit your solutions. Refer to Moodle for presentation slides and deadlines. - -## Submission Policy - -Submitting your lab results on time is crucial for your grading. Late submissions receive a maximum score of 6 points for the corresponding lab. Remember, completing all labs is necessary to successfully pass the course. +## Unit Tests +Run tests: +```bash +pytest tests/ diff --git a/ansible/ANSIBLE.md b/ansible/ANSIBLE.md new file mode 100644 index 0000000000..857d81c4e5 --- /dev/null +++ b/ansible/ANSIBLE.md @@ -0,0 +1,185 @@ +# Ansible Docs + +## Check connection to server(s) +To check is connection to server for deploy successful, let's run following command +```sh +ansible -i ansible/inventory/default_aws_ec2.yml all -m ping +``` +Output: +``` +vm | SUCCESS => { + "changed": false, + "ping": "pong" +} +``` + +--- + +## Running the Playbook +To execute the playbook, run: +```sh +ansible-playbook -i ansible/inventory/default_aws_ec2.yml ansible/playbooks/dev/main.yaml -K +``` + +``` +PLAY [Deploy Docker] ************************************************************************** + +TASK [Gathering Facts] ************************************************************************ +ok: [vm] + +TASK [docker : Include installation tasks] **************************************************** +included: /home/usr/hw/DevOps/S25-core-course-labs/ansible/roles/docker/tasks/install_docker.yml for vm + +TASK [docker : Install required system packages] ********************************************** +ok: [vm] + +TASK [docker : Add Docker official GPG key] *************************************************** +ok: [vm] + +TASK [docker : Set up the Docker stable repository] ******************************************* +ok: [vm] + +TASK [docker : Install Docker CE, CLI, and containerd] **************************************** +ok: [vm] + +TASK [docker : Start and enable Docker service] *********************************************** +ok: [vm] + +TASK [docker : Add user to Docker group] ****************************************************** +ok: [vm] + +TASK [docker : Include Docker Compose tasks] ************************************************** +included: /home/usr/hw/DevOps/S25-core-course-labs/ansible/roles/docker/tasks/install_compose.yml for vm + +TASK [docker : Download Docker Compose] ******************************************************* +changed: [vm] + +TASK [docker : Verify Docker Compose installation] ******************************************** +ok: [vm] + +TASK [docker : debug] ************************************************************************* +ok: [vm] => { + "msg": "Docker Compose version: Docker Compose version v2.33.0" +} + +PLAY RECAP ************************************************************************************ +vm : ok=12 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 + +``` + +## Output `nsible-inventory --list` +``` +{ + "_meta": { + "hostvars": { + "vm": { + "ansible_host": "89.169.154.202", + "ansible_python_interpreter": "/usr/bin/python3", + "ansible_ssh_private_key_file": "~/.ssh/id_ed25519", + "ansible_user": "ubuntu" + } + } + }, + "all": { + "children": [ + "ungrouped" + ] + }, + "ungrouped": { + "hosts": [ + "vm" + ] + } +} +``` + +## Output of `ansible-inventory --graph` +``` +@all: + |--@ungrouped: + | |--vm +``` + +## Running the Playbook with web-app +```sh +ansible-playbook -i ansible/inventory/default_aws_ec2.yml ansible/playbooks/dev/main.yaml -K +``` + +``` +PLAY [Deploy Docker] ************************************************************************** + +TASK [Gathering Facts] ************************************************************************ +ok: [vm] + +TASK [docker : Include installation tasks] **************************************************** +included: /home/usr/hw/DevOps/S25-core-course-labs/ansible/roles/docker/tasks/install_docker.yml for vm + +TASK [docker : Install required system packages] ********************************************** +ok: [vm] + +TASK [docker : Add Docker official GPG key] *************************************************** +ok: [vm] + +TASK [docker : Set up the Docker stable repository] ******************************************* +ok: [vm] + +TASK [docker : Install Docker CE, CLI, and containerd] **************************************** +ok: [vm] + +TASK [docker : Start and enable Docker service] *********************************************** +ok: [vm] + +TASK [docker : Add user to Docker group] ****************************************************** +ok: [vm] + +TASK [docker : Include Docker Compose tasks] ************************************************** +included: /home/usr/hw/DevOps/S25-core-course-labs/ansible/roles/docker/tasks/install_compose.yml for vm + +TASK [docker : Download Docker Compose] ******************************************************* +ok: [vm] + +TASK [docker : Verify Docker Compose installation] ******************************************** +ok: [vm] + +TASK [docker : debug] ************************************************************************* +ok: [vm] => { + "msg": "Docker Compose version: Docker Compose version v2.33.0" +} + +TASK [web_app : Create Docker network] ******************************************************** +ok: [vm] + +TASK [web_app : Deploy application using Docker Compose] ************************************** +ok: [vm] + +TASK [web_app : Start application with Docker Compose] **************************************** +changed: [vm] + +PLAY RECAP ************************************************************************************ +vm : ok=15 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +## `ansible-playbook ansible/playbooks/dev/main.yaml --tags wipe` +``` +PLAY [Deploy Web Application] ***************************************************************** + +TASK [Gathering Facts] ************************************************************************ +ok: [vm] + +PLAY [Wipe Web Application] ******************************************************************* + +TASK [Gathering Facts] ************************************************************************ +ok: [vm] + +TASK [Remove Docker container] **************************************************************** +skipping: [vm] + +TASK [Remove Docker image] ******************************************************************** +skipping: [vm] + +TASK [Remove Docker volumes] ****************************************************************** +skipping: [vm] + +PLAY RECAP ************************************************************************************ +vm : ok=2 changed=0 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0 +``` diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000000..17ba3c88d3 --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,3 @@ +[defaults] +roles_path = ./roles +inventory = ./inventory/default_aws_ec2.yml \ No newline at end of file diff --git a/ansible/inventory/default_aws_ec2.yml b/ansible/inventory/default_aws_ec2.yml new file mode 100644 index 0000000000..6576d21545 --- /dev/null +++ b/ansible/inventory/default_aws_ec2.yml @@ -0,0 +1,7 @@ +all: + hosts: + vm: + ansible_host: 89.169.24.212 + ansible_user: ubuntu + ansible_ssh_private_key_file: ~/.ssh/id_ed25519 + ansible_python_interpreter: /usr/bin/python3 \ No newline at end of file diff --git a/ansible/playbooks/dev/main.yaml b/ansible/playbooks/dev/main.yaml new file mode 100644 index 0000000000..955a7f2aeb --- /dev/null +++ b/ansible/playbooks/dev/main.yaml @@ -0,0 +1,14 @@ +- name: Deploy Web Application + hosts: all + become: yes + roles: + - docker + - web_app + +- name: Wipe Web Application + hosts: all + become: yes + tasks: + - import_tasks: ../../roles/web_app/tasks/0-wipe.yml + tags: + - wipe \ No newline at end of file diff --git a/ansible/roles/docker/README.md b/ansible/roles/docker/README.md new file mode 100644 index 0000000000..8816b0e9f1 --- /dev/null +++ b/ansible/roles/docker/README.md @@ -0,0 +1,20 @@ +# Docker Role + +This role installs and configures Docker and Docker Compose. + +## Requirements + +- Ansible 2.9+ +- Ubuntu 22.04 + +## Role Variables + +- `docker_version`: The version of Docker to install (default: `latest`). +- `docker_compose_version`: The version of Docker Compose to install (default: `1.29.2`). + +## Example Playbook + +```yaml +- hosts: all + roles: + - role: docker \ 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..73b314ff7c --- /dev/null +++ b/ansible/roles/docker/defaults/main.yml @@ -0,0 +1 @@ +--- \ 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..def71c66cf --- /dev/null +++ b/ansible/roles/docker/handlers/main.yml @@ -0,0 +1,4 @@ +- name: Restart Docker + systemd: + name: docker + state: restarted \ No newline at end of file diff --git a/ansible/roles/docker/meta/main.yml b/ansible/roles/docker/meta/main.yml new file mode 100644 index 0000000000..ceed84023c --- /dev/null +++ b/ansible/roles/docker/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + author: Ivan Sannikov + description: Install Docker + license: MIT + company: Some Test compony + min_ansible_version: "2.9" + platforms: + - name: Ubuntu + versions: + - focal + - jammy + galaxy_tags: + - docker + - container + +dependencies: [] \ No newline at end of file diff --git a/ansible/roles/docker/tasks/install_compose.yml b/ansible/roles/docker/tasks/install_compose.yml new file mode 100644 index 0000000000..d9e1067bd9 --- /dev/null +++ b/ansible/roles/docker/tasks/install_compose.yml @@ -0,0 +1,13 @@ +- name: Download Docker Compose + get_url: + url: "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64" + dest: /usr/local/bin/docker-compose + mode: '0755' + +- name: Verify Docker Compose installation + command: docker-compose --version + register: docker_compose_version + changed_when: false + +- debug: + msg: "Docker Compose version: {{ docker_compose_version.stdout }}" \ No newline at end of file diff --git a/ansible/roles/docker/tasks/install_docker.yml b/ansible/roles/docker/tasks/install_docker.yml new file mode 100644 index 0000000000..e2a0443ef4 --- /dev/null +++ b/ansible/roles/docker/tasks/install_docker.yml @@ -0,0 +1,39 @@ +- name: Install required system packages + apt: + name: + - apt-transport-https + - ca-certificates + - curl + - software-properties-common + state: present + update_cache: yes + +- name: Add Docker official GPG key + apt_key: + url: https://download.docker.com/linux/ubuntu/gpg + state: present + +- name: Set up the Docker stable repository + apt_repository: + repo: "deb https://download.docker.com/linux/ubuntu focal stable" + state: present + +- name: Install Docker CE, CLI, and containerd + apt: + name: + - docker-ce + - docker-ce-cli + - containerd.io + state: latest + +- name: Start and enable Docker service + systemd: + name: docker + enabled: true + state: started + +- name: Add user to Docker group + user: + name: venom + groups: docker + append: yes \ 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..e948f34913 --- /dev/null +++ b/ansible/roles/docker/tasks/main.yml @@ -0,0 +1,5 @@ +- name: Include installation tasks + include_tasks: install_docker.yml + +- name: Include Docker Compose tasks + include_tasks: install_compose.yml \ No newline at end of file diff --git a/ansible/roles/web_app/README.md b/ansible/roles/web_app/README.md new file mode 100644 index 0000000000..770a685c50 --- /dev/null +++ b/ansible/roles/web_app/README.md @@ -0,0 +1,32 @@ +# web_app Ansible Role + +## Description + +This Ansible role deploys a web application container using Docker and Docker Compose. It pulls the Docker image from the specified registry, creates the required network, and deploys the application in a container. + +## Requirements + +- Docker +- Docker Compose +- Ansible >= 2.9 + +## Role Variables + +- `docker_image`: Docker image for the application (e.g., `ez4gotit/flask_app:latest`) +- `container_name`: Name of the Docker container (e.g., `flask_app`) +- `host_port`: Port on the host to map to the app port (default: `8080`) +- `app_port`: Port the app will listen on inside the container (default: `8080`) +- `network_name`: Name of the Docker network (default: `web_app_network`) +- `web_app_full_wipe`: Boolean to enable/disable full wipe (default: `false`) + +## Dependencies + +This role depends on the `docker` role to ensure Docker is installed and configured before deploying the web app. + +## Usage + +To deploy the web app: + +```sh +ansible-playbook ansible/playbooks/dev/main.yaml +``` \ No newline at end of file diff --git a/ansible/roles/web_app/defaults/main.yml b/ansible/roles/web_app/defaults/main.yml new file mode 100644 index 0000000000..6588f43285 --- /dev/null +++ b/ansible/roles/web_app/defaults/main.yml @@ -0,0 +1,11 @@ +#SPDX-License-Identifier: MIT-0 +--- +# defaults file for ansible/roles/web_app + +app_name: my_web_app +docker_image: "ez4gotit/flask_app:latest" +container_name: "flask_app" +app_port: 8080 +host_port: 8080 +network_name: "web_app_network" +web_app_full_wipe: false diff --git a/ansible/roles/web_app/handlers/main.yml b/ansible/roles/web_app/handlers/main.yml new file mode 100644 index 0000000000..0e1eccedcc --- /dev/null +++ b/ansible/roles/web_app/handlers/main.yml @@ -0,0 +1,8 @@ +#SPDX-License-Identifier: MIT-0 +--- +# handlers file for ansible/roles/web_app + +- name: Restart Application + command: + cmd: docker-compose restart + chdir: /home/ubuntu/ \ No newline at end of file diff --git a/ansible/roles/web_app/meta/main.yml b/ansible/roles/web_app/meta/main.yml new file mode 100644 index 0000000000..0caf3910e5 --- /dev/null +++ b/ansible/roles/web_app/meta/main.yml @@ -0,0 +1,36 @@ +#SPDX-License-Identifier: MIT-0 +galaxy_info: + author: Mikhail Dudinov + description: test + company: test + + # If the issue tracker for your role is not on github, uncomment the + # next line and provide a value + # issue_tracker_url: http://example.com/issue/tracker + + # Choose a valid license ID from https://spdx.org - some suggested licenses: + # - BSD-3-Clause (default) + # - MIT + # - GPL-2.0-or-later + # - GPL-3.0-only + # - Apache-2.0 + # - CC-BY-4.0 + license: license (GPL-2.0-or-later, MIT, etc) + + min_ansible_version: 2.0 + + # If this a Container Enabled role, provide the minimum Ansible Container version. + # min_ansible_container_version: + + galaxy_tags: [] + # List tags for your role here, one per line. A tag is a keyword that describes + # and categorizes the role. Users find roles by searching for tags. Be sure to + # remove the '[]' above, if you add tags to this list. + # + # NOTE: A tag is limited to a single word comprised of alphanumeric characters. + # Maximum 20 tags per role. + +dependencies: + - { role: docker } + # List your role dependencies here, one per line. Be sure to remove the '[]' above, + # if you add dependencies to this list. diff --git a/ansible/roles/web_app/tasks/0-wipe.yml b/ansible/roles/web_app/tasks/0-wipe.yml new file mode 100644 index 0000000000..49b59a6737 --- /dev/null +++ b/ansible/roles/web_app/tasks/0-wipe.yml @@ -0,0 +1,17 @@ +- name: Remove Docker container + docker_container: + name: "{{ container_name }}" + state: absent + when: web_app_full_wipe | default(false) + +- name: Remove Docker image + docker_image: + name: "{{ docker_image }}" + state: absent + when: web_app_full_wipe | default(false) + +- name: Remove Docker volumes + docker_volume: + name: "{{ container_name }}" + state: absent + when: web_app_full_wipe | default(false) \ No newline at end of file diff --git a/ansible/roles/web_app/tasks/main.yml b/ansible/roles/web_app/tasks/main.yml new file mode 100644 index 0000000000..4334c008e1 --- /dev/null +++ b/ansible/roles/web_app/tasks/main.yml @@ -0,0 +1,40 @@ +#SPDX-License-Identifier: MIT-0 +--- +# tasks file for ansible/roles/web_app + +# ansible/roles/web_app/tasks/main.yml +- name: Setup Docker Environment + block: + - name: Install Docker + apt: + name: docker.io + state: present + + - name: Start Docker Service + service: + name: docker + state: started + enabled: yes + tags: + - setup + +- name: Create Docker Compose file + template: + src: docker-compose.yml.j2 + dest: "/home/ubuntu/docker-compose.yml" + notify: Restart Application + +- name: Pull Docker image + docker_image: + name: "{{ docker_image }}" + source: pull + tags: + - docker + +- name: Start application with Docker Compose + command: + cmd: docker-compose up -d + chdir: /home/ubuntu/ + tags: + - docker + diff --git a/ansible/roles/web_app/templates/docker-compose.yml.j2 b/ansible/roles/web_app/templates/docker-compose.yml.j2 new file mode 100644 index 0000000000..0cd106a6a6 --- /dev/null +++ b/ansible/roles/web_app/templates/docker-compose.yml.j2 @@ -0,0 +1,14 @@ +version: '3' +services: + {{ app_name }}: + image: "{{ docker_image }}" + container_name: "{{ container_name }}" + ports: + - "{{ host_port }}:{{ app_port }}" + restart: always + networks: + - "{{ network_name }}" + +networks: + {{ network_name }}: + driver: bridge \ No newline at end of file diff --git a/ansible/roles/web_app/tests/inventory b/ansible/roles/web_app/tests/inventory new file mode 100644 index 0000000000..03ca42fd17 --- /dev/null +++ b/ansible/roles/web_app/tests/inventory @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +localhost + diff --git a/ansible/roles/web_app/tests/test.yml b/ansible/roles/web_app/tests/test.yml new file mode 100644 index 0000000000..fde722cf0d --- /dev/null +++ b/ansible/roles/web_app/tests/test.yml @@ -0,0 +1,6 @@ +#SPDX-License-Identifier: MIT-0 +--- +- hosts: localhost + remote_user: root + roles: + - ansible/roles/web_app diff --git a/ansible/roles/web_app/vars/main.yml b/ansible/roles/web_app/vars/main.yml new file mode 100644 index 0000000000..2c15087b5e --- /dev/null +++ b/ansible/roles/web_app/vars/main.yml @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +--- +# vars file for ansible/roles/web_app diff --git a/app_python/.dockerignore b/app_python/.dockerignore new file mode 100644 index 0000000000..5b59df5e9d --- /dev/null +++ b/app_python/.dockerignore @@ -0,0 +1,11 @@ +__pycache__ +*.pyc +*.pyo +*.pyd +venv/ +.git +.gitignore +Dockerfile +README.md +tests/ +*.log \ No newline at end of file diff --git a/app_python/.gitignore b/app_python/.gitignore new file mode 100644 index 0000000000..8b63d63bcf --- /dev/null +++ b/app_python/.gitignore @@ -0,0 +1,32 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +env/ +venv/ +*.env +instance/ +*.log +*.pot +*.pyc +.vscode/ +.idea/ +.DS_Store +Thumbs.db +.coverage +nosetests.xml +*.cover +*.hypothesis/ +*.tox/ +*.nox/ +*.coverage +venv/ +.env/ +**/.terraform/ +**/.terraform.lock.hcl +**/terraform.tfstate +**/terraform.tfstate.backup +yandex/key.json +**/terraform.tfstate.*.backup + diff --git a/app_python/DOCKER.md b/app_python/DOCKER.md new file mode 100644 index 0000000000..d3f0dfac45 --- /dev/null +++ b/app_python/DOCKER.md @@ -0,0 +1,27 @@ +# Docker Best Practices for Flask App + +## 1. Rootless Container +We ensure that the container runs as **non-root user**: `flask_app_user`\ +This is critical for security purposes, as running as the root user can expose the container to unnecessary risks + +## 2. Layer sanity +To minimize image size and optimize build time, we combine related commands, such as **installing dependencies** and **copying files**, into fewer layers\ +This allows Docker to cache layers **efficiently, reducing the time needed for future builds** + +## 3. Selective Copy +We selectively copy only the files that are necessary for the application to run: +* `requirements.txt` +* `web.py` +* `templates/` +* `static/` + +This reduces the final image size and prevents unnecessary files from being included in the Docker image + +## 4. Precise Version of Base Image +We use a specific version of the Python base image (`python:3.9-alpine3.15`) to ensure that the image is consistent and not affected by any future breaking changes in newer versions of the base image + +## 5. `.dockerignore`-file +We use a `.dockerignore` file to exclude unnecessary files from the Docker image, such as local python bytecode files, git-related files etc. + +## 6. Env Variables +We set the `FLASK_APP` and `FLASK_ENV` env variables to ensure the application runs in production mode, **improving performance and security** diff --git a/app_python/Dockerfile b/app_python/Dockerfile new file mode 100644 index 0000000000..a990558dca --- /dev/null +++ b/app_python/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.9-alpine3.15 +WORKDIR /app_python +RUN adduser -D flask_app_user +USER flask_app_user +COPY requirements.txt /app_python/ +RUN pip install --no-cache-dir -r requirements.txt +COPY web.py /app_python/ +COPY templates/ /app_python/templates/ +COPY static/ /app_python/static/ +EXPOSE 5000 +ENV FLASK_APP=web.py +ENV FLASK_ENV=production +CMD ["python", "web.py"] \ No newline at end of file diff --git a/app_python/PYTHON.md b/app_python/PYTHON.md new file mode 100644 index 0000000000..27761428b0 --- /dev/null +++ b/app_python/PYTHON.md @@ -0,0 +1,16 @@ +# Python Web Application. Current time in MSK + +## ✅ Code Structure & Best Practices +- **Separation of Concerns**: Flask follows MVC-like architecture: **HTML** in `templates/`, **styles** in `static/` +- **Modular Design**: The app is structured with separation between logic, templates, and static files +- **Environment Variables**: The `web.py` uses host `"0.0.0.0"` and port `5000` +- **PEP 8 Compliance**: Code follows PEP8 + +## ✅ Timezone Setup +- `pytz` to accurately fetch **Moscow Time** +- Ensures that the displayed time updates dinamicaly + +## ✅ Code Quality and Testing +- **Linting**: Used `flake8` and `pylint` for code cleanliness and finding potential errors +- **Logging**: Can be enhanced using Python’s `logging` module for debugging +- **Unit Testing**: Unit tests can be added with `pytest`. Unit test is implemented in `unit_test.py` file diff --git a/app_python/README.md b/app_python/README.md new file mode 100644 index 0000000000..121e053744 --- /dev/null +++ b/app_python/README.md @@ -0,0 +1,57 @@ +# Flask App: Moscow Time and Visits Counter + +This application displays the current time in Moscow and tracks the number of visits to the root endpoint. The visit count is persisted to a file and exposed via a dedicated endpoint. + +## Features + +- Displays the current time in Moscow using pytz +- Increments a persistent visit counter on / +- Returns current visit count on /visits +- Unit tests +- HTML and CSS for basic presentation + +## Run via Docker image from Docker Hub + +```sh +docker pull petrel312/flask_app:latest +docker run -p 5000:5000 petrel312/flask_app:latest +``` + +## Local Installation and Run + +```sh +git clone https://github.com/Petrel321/S25-core-course-labs.git +cd S25-core-course-labs +python3 -m venv venv +source venv/bin/activate # For Windows: venv\Scripts\activate +pip install -r requirements.txt +cd app_python +python web.py +``` + +## Build the Docker Image Locally + +```sh +git clone https://github.com/Petrel321/S25-core-course-labs.git +cd S25-core-course-labs/app_python +docker build -t any_docker_image_name . +``` + +## Run with Docker Compose and Persistence + +```sh +cd app_python +mkdir -p data +docker compose up --build +``` + +The visits counter is stored at /data/visits. The Docker Compose configuration mounts ./data from the host to /data inside the container so the counter is persisted between restarts. + +## Endpoints + +- / returns the application HTML page and increments the visits counter +- /visits returns the current counter value as JSON + +## Unit Test + +There is a unit test that checks the application returns a successful response. \ No newline at end of file diff --git a/app_python/docker-compose.yml b/app_python/docker-compose.yml new file mode 100644 index 0000000000..fecd61c9db --- /dev/null +++ b/app_python/docker-compose.yml @@ -0,0 +1,9 @@ +services: + app: + build: . + ports: + - "5000:5000" + environment: + - VISITS_FILE=/data/visits + volumes: + - ./data:/data \ No newline at end of file diff --git a/app_python/requirements.txt b/app_python/requirements.txt new file mode 100644 index 0000000000..d98a9624fb --- /dev/null +++ b/app_python/requirements.txt @@ -0,0 +1,22 @@ +astroid==3.3.8 +blinker==1.9.0 +click==8.1.8 +dill==0.3.9 +flake8==7.1.1 +Flask==3.1.0 +iniconfig==2.0.0 +isort==6.0.0 +itsdangerous==2.2.0 +Jinja2==3.1.5 +MarkupSafe==3.0.2 +mccabe==0.7.0 +packaging==24.2 +platformdirs==4.3.6 +pluggy==1.5.0 +pycodestyle==2.12.1 +pyflakes==3.2.0 +pylint==3.3.4 +pytest==8.3.4 +pytz==2024.2 +tomlkit==0.13.2 +Werkzeug==3.1.3 diff --git a/app_python/static/script.js b/app_python/static/script.js new file mode 100644 index 0000000000..ee73c32041 --- /dev/null +++ b/app_python/static/script.js @@ -0,0 +1,9 @@ +function updateTime() { + fetch('/ct') + .then(response => response.json()) + .then(data => { + document.getElementById('ct').textContent = data.ct; + }); +} + +setInterval(updateTime, 1000); diff --git a/app_python/static/style.css b/app_python/static/style.css new file mode 100644 index 0000000000..14f7b173cc --- /dev/null +++ b/app_python/static/style.css @@ -0,0 +1,21 @@ +body { + font-family: Arial, sans-serif; + text-align: center; + background-color: #f4f4f4; + margin: 0; + padding: 0; +} + +.container { + margin-top: 50px; +} + +h1 { + color: #333; +} + +p { + font-size: 24px; + font-weight: bold; + color: #007BFF; +} diff --git a/app_python/templates/index.html b/app_python/templates/index.html new file mode 100644 index 0000000000..35d3c2d3d4 --- /dev/null +++ b/app_python/templates/index.html @@ -0,0 +1,17 @@ + + + + + + Current time in Moscow + + + +
+

Current time in Moscow

+

{{ time }}

+
+ + + + \ No newline at end of file diff --git a/app_python/unit_test.py b/app_python/unit_test.py new file mode 100644 index 0000000000..518f45f18d --- /dev/null +++ b/app_python/unit_test.py @@ -0,0 +1,24 @@ +""" +Unit tests +""" +import pytest +from web import app + + +@pytest.fixture +def client(): + """ + testing + """ + app.config["TESTING"] = True + with app.test_client() as client: + yield client + + +def test_index_page(client): + """ + Testing that main page loads and contains time + """ + response = client.get("/") + assert response.status_code == 200 + assert b"Current time" in response.data diff --git a/app_python/web.py b/app_python/web.py new file mode 100644 index 0000000000..1bd9321b85 --- /dev/null +++ b/app_python/web.py @@ -0,0 +1,84 @@ +""" +Flask application for displaying the current time in Moscow +""" + +import logging +import os +import threading +from datetime import datetime +from flask import Flask, render_template, jsonify +import pytz + + +app = Flask(__name__) +logging.basicConfig(level=logging.INFO) +VISITS_FILE = os.getenv("VISITS_FILE", "/data/visits") +_visits_lock = threading.Lock() + + +def _read_visits(): + try: + with open(VISITS_FILE, "r", encoding="utf-8") as handle: + return int(handle.read().strip() or "0") + except FileNotFoundError: + return 0 + except ValueError: + return 0 + + +def _write_visits(value): + os.makedirs(os.path.dirname(VISITS_FILE), exist_ok=True) + tmp_path = f"{VISITS_FILE}.tmp" + with open(tmp_path, "w", encoding="utf-8") as handle: + handle.write(str(value)) + os.replace(tmp_path, VISITS_FILE) + + +def _increment_visits(): + with _visits_lock: + current = _read_visits() + current += 1 + _write_visits(current) + return current + + +def get_moscow_time(): + """ + Current MSK Time + """ + moscow_tz = pytz.timezone("Europe/Moscow") + return datetime.now(moscow_tz).strftime("%H:%M:%S") + + +@app.route("/ct") +def get_time(): + """ + Used to get MSK time and use it in script.js file + """ + return jsonify({"ct": get_moscow_time()}) + + +@app.route("/") +def index(): + """ + Displays the current time in Moscow + """ + logging.info("Main page with timezone was loaded") + moscow_tz = pytz.timezone("Europe/Moscow") + current_time = datetime.now(moscow_tz).strftime("%H:%M:%S") + _increment_visits() + + return render_template("index.html", time=current_time) + + +@app.route("/visits") +def visits(): + """ + Returns visits count. + """ + with _visits_lock: + return jsonify({"visits": _read_visits()}) + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000, debug=True) diff --git a/k8s/12.md b/k8s/12.md new file mode 100644 index 0000000000..e7a0d590d3 --- /dev/null +++ b/k8s/12.md @@ -0,0 +1,61 @@ +# Task 12: Application Persistence and ConfigMap + +Author: Mikhail Dudinov @ez4gotit + +## Task 1: Application Persistence + +Commands: + +```sh +cd app_python +mkdir -p data +docker compose up --build +``` +![alt text](imag1.png) +Proof of visits file on host: + +```sh +cat app_python/data/visits +``` + + +Proof of visit count endpoint: + +```sh +curl http://localhost:5000/visits +``` + +Output: + +![alt text](imag2.png) + +## Task 2: ConfigMap Implementation + +Create config.json and ConfigMap via Helm chart. + +Relevant files: +- k8s/helm-python/files/config.json +- k8s/helm-python/templates/configmap.yaml +- k8s/helm-python/templates/deployment.yaml + +Install or upgrade chart: + +```sh +helm upgrade --install helm-python k8s/helm-python +``` + +Verify pods: + +```sh +kubectl get po +``` + + +Verify ConfigMap file in pod: + +```sh +kubectl exec -- cat /config.json +``` + + +![alt text](imag3.png) \ No newline at end of file diff --git a/k8s/13.md b/k8s/13.md new file mode 100644 index 0000000000..333e25c0bc --- /dev/null +++ b/k8s/13.md @@ -0,0 +1,482 @@ +# Lab13 + +## Install ArgoCD via Helm + +```sh +helm repo add argo https://argoproj.github.io/argo-helm +``` +```text +"argo" has been added to your repositories +``` + +```sh +helm install argo argo/argo-cd --namespace argocd --create-namespace +``` +```text +NAME: argo +LAST DEPLOYED: Thu Feb 12 23:57:16 2026 +NAMESPACE: argocd +STATUS: deployed +REVISION: 1 +TEST SUITE: None +NOTES: +To access the server UI, use one of the following options: + +1. kubectl port-forward service/argo-argocd-server -n argocd 8080:443 + + and then open the browser on http://localhost:8080 and accept the certificate + +2. enable ingress in the values file `server.ingress.enabled` and either + - Add the annotation for ssl passthrough: https://argo-cd.readthedocs.io/en/stable/operator-manual/ingress/#option-1-ssl-passthrough + - Set the `configs.params."server.insecure"` in the values file and terminate SSL at your ingress: https://argo-cd.readthedocs.io/en/stable/operator-manual/ingress/#option-2-multiple-ingress-objects-and-hosts + + +After the first access, log in with username admin and the password generated during installation. Retrieve the password with: + +kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d + +(Delete the initial secret afterwards as recommended in the Getting Started Guide: https://argo-cd.readthedocs.io/en/stable/getting_started/#4-login-using-the-cli) + +``` + +```sh +kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=argocd-server -n argocd --timeout=90s +``` +```text +pod/argo-argocd-server-7b688c6d85-gzbj8 condition met +``` + +## Install ArgoCD CLI + +```sh +curl -sSL -o argocd https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64 +chmod +x argocd +sudo mv argocd /usr/local/bin/ +``` + +```sh +argocd version +``` +```text +argocd: v2.14.5+f463a94 + BuildDate: 2026-02-11T03:40:10Z + GitCommit: f463a945d57267e9691cede37021d9ddc5994f36 + GitTreeState: clean + GoVersion: go1.23.3 + Compiler: gc + Platform: linux/amd64 +FATA[0000] Argo CD server address unspecified +``` + +## Access the ArgoCD UI + +```sh +kubectl port-forward svc/argo-argocd-server -n argocd 8080:443 & +``` +```text +Forwarding from [::1]:8080 -> 8080 +``` + +```sh +kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 --decode +``` +```text +jk-QHx0Td3rWS5kg +``` + +```sh +argocd login localhost:8080 --insecure +``` +```text +Username: admin +Password: +'admin:login' logged in successfully +Context 'localhost:8080' updated +``` + +## Configure Python App Sync + +```sh +kubectl apply -f ArgoCD/argocd-python-app.yaml +``` +```text +application.argoproj.io/arhocd-app created +``` + +```sh +argocd app sync arhocd-app --server localhost:8080 --insecure +``` +```text +TIMESTAMP GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE +2026-02-12T23:52:48+00:00 apps Deployment default arhocd-app-helm-python OutOfSync Missing +2026-02-12T23:52:48+00:00 Secret default some-secret OutOfSync Missing +2026-02-12T23:52:48+00:00 Service default arhocd-app-helm-python OutOfSync Missing +2026-02-12T23:52:57+00:00 ServiceAccount default internal-app OutOfSync Missing +2026-02-12T23:53:09+00:00 Pod default preinstall-hook +2026-02-12T23:53:11+00:00 Pod default preinstall-hook Running Synced PreSync pod/preinstall-hook created +2026-02-12T23:53:28+00:00 ServiceAccount default internal-app Synced Missing +2026-02-12T23:53:42+00:00 Secret default some-secret Synced Missing +2026-02-12T23:53:46+00:00 Service default arhocd-app-helm-python Synced Healthy +2026-02-12T23:53:49+00:00 apps Deployment default arhocd-app-helm-python Synced Progressing +2026-02-12T23:54:14+00:00 Service default arhocd-app-helm-python Synced Healthy service/arhocd-app-helm-python created +2026-02-12T23:54:15+00:00 apps Deployment default arhocd-app-helm-python Synced Progressing deployment.apps/arhocd-app-helm-python created +2026-02-12T23:54:16+00:00 Pod default preinstall-hook Succeeded Synced PreSync pod/preinstall-hook created +2026-02-12T23:54:16+00:00 ServiceAccount default internal-app Synced Missing serviceaccount/internal-app created +2026-02-12T23:54:16+00:00 Secret default some-secret Synced Missing secret/some-secret created +2026-02-12T23:54:18+00:00 apps Deployment default arhocd-app-helm-python Synced Healthy deployment.apps/arhocd-app-helm-python created +2026-02-12T23:54:21+00:00 Pod default postinstall-hook Running Synced PostSync pod/postinstall-hook created +2026-02-12T23:54:22+00:00 Pod default postinstall-hook Succeeded Synced PostSync pod/postinstall-hook created + +Name: argocd/arhocd-app +Project: default +Server: https://kubernetes.default.svc +Namespace: default +URL: https://argocd.example.com/applications/arhocd-app +Source: +- Repo: https://github.com/ez4gotit/S25-core-course-labs.git + Target: lab13-solution + Path: k8s/helm-python + Helm Values: values.yaml +SyncWindow: Sync Allowed +Sync Policy: Automated +Sync Status: Synced to lab13-solution (e84e82d) +Health Status: Healthy + +Operation: Sync +Sync Revision: e84e82d86fde0673786ca7af313d54f3b95aa937 +Phase: Succeeded +Start: 2026-02-12 23:52:48 +0000 UTC +Finished: 2026-02-12 23:54:22 +0000 UTC +Duration: 94s +Message: successfully synced (no more tasks) + +GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE + Pod default preinstall-hook Succeeded PreSync pod/preinstall-hook created + ServiceAccount default internal-app Synced serviceaccount/internal-app created + Secret default some-secret Synced secret/some-secret created + Service default arhocd-app-helm-python Synced Healthy service/arhocd-app-helm-python created +apps Deployment default arhocd-app-helm-python Synced Healthy deployment.apps/arhocd-app-helm-python created + Pod default postinstall-hook Succeeded PostSync pod/postinstall-hook created +``` + +```sh +argocd app get arhocd-app +``` +```text +Name: argocd/arhocd-app +Project: default +Server: https://kubernetes.default.svc +Namespace: default +URL: https://argocd.example.com/applications/arhocd-app +Source: +- Repo: https://github.com/ez4gotit/S25-core-course-labs.git + Target: lab13-solution + Path: k8s/helm-python + Helm Values: values.yaml +SyncWindow: Sync Allowed +Sync Policy: Automated +Sync Status: Synced to lab13-solution (e84e82d) +Health Status: Healthy + +GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE + Pod default preinstall-hook Succeeded PreSync pod/preinstall-hook created + ServiceAccount default internal-app Synced serviceaccount/internal-app created + Secret default some-secret Synced secret/some-secret created + Service default arhocd-app-helm-python Synced Healthy service/arhocd-app-helm-python created +apps Deployment default arhocd-app-helm-python Synced Healthy deployment.apps/arhocd-app-helm-python created + Pod default postinstall-hook Succeeded PostSync pod/postinstall-hook created +``` + +![alt text](image-16.png) + +## Test Sync Workflow. Changing `replicaCount` to `2` +```sh +argocd app get arhocd-app +``` +```text +Name: argocd/arhocd-app +Project: default +Server: https://kubernetes.default.svc +Namespace: default +URL: https://argocd.example.com/applications/arhocd-app +Source: +- Repo: https://github.com/ez4gotit/S25-core-course-labs.git + Target: lab13-solution + Path: k8s/helm-python + Helm Values: values.yaml +SyncWindow: Sync Allowed +Sync Policy: Automated +Sync Status: Synced to lab13-solution (2f029bb) +Health Status: Healthy + +GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE + Pod default preinstall-hook Succeeded PreSync pod/preinstall-hook created + ServiceAccount default internal-app Synced serviceaccount/internal-app unchanged + Secret default some-secret Synced secret/some-secret unchanged + Service default arhocd-app-helm-python Synced Healthy service/arhocd-app-helm-python unchanged +apps Deployment default arhocd-app-helm-python Synced Healthy deployment.apps/arhocd-app-helm-python configured + Pod default postinstall-hook Running PostSync pod/postinstall-hook created +``` + +![alt text](image-17.png) + +### Verify number of replicas: +```sh +kubectl get deployments.apps +``` + +```text +arhocd-app-helm-python 2/2 2 2 76m +``` + +```sh +kubectl describe deployment arhocd-app-helm-python | grep "Replicas" +``` +```sh +Replicas: 2 desired | 2 updated | 2 total | 2 available | 0 unavailable + Available True MinimumReplicasAvailable +``` + +![alt text](image-15.png) + +# Task 2 + +## Deploy Multi-Environment via ArgoCD + +## Create Namespaces + +```sh +kubectl create namespace dev +kubectl create namespace prod +``` +```text +namespace/dev created +namespace/prod created +``` + +## Test Auto-Sync + +```sh +kubectl apply -f k8s/ArgoCD/argocd-python-dev.yaml +kubectl apply -f k8s/ArgoCD/argocd-python-prod.yaml +``` + +```text +application.argoproj.io/arhocd-app-dev configured +application.argoproj.io/arhocd-app-prod created +``` + +```sh +argocd app list +``` + +![alt text](image-18.png) + +```sh +argocd app get arhocd-app-prod +``` + +```text +Name: argocd/arhocd-app-prod +Project: default +Server: https://kubernetes.default.svc +Namespace: prod +URL: https://argocd.example.com/applications/arhocd-app-prod +Source: +- Repo: https://github.com/ez4gotit/S25-core-course-labs.git + Target: lab13-solution + Path: k8s/helm-python + Helm Values: values-prod.yaml +SyncWindow: Sync Allowed +Sync Policy: Automated (Prune) +Sync Status: Synced to lab13-solution (b6080a7) +Health Status: Healthy + +GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE + Pod prod preinstall-hook Succeeded PreSync pod/preinstall-hook created + ServiceAccount prod internal-app Synced serviceaccount/internal-app unchanged + Secret prod some-secret Synced secret/some-secret unchanged + Service prod arhocd-app-prod-helm-python Synced Healthy service/arhocd-app-prod-helm-python unchanged +apps Deployment prod arhocd-app-prod-helm-python Synced Healthy deployment.apps/arhocd-app-prod-helm-python unchanged + Pod prod postinstall-hook Succeeded PostSync pod/postinstall-hook created +``` + +```sh +argocd app get arhocd-app-dev +``` + +```text +Name: argocd/arhocd-app-dev +Project: default +Server: https://kubernetes.default.svc +Namespace: dev +URL: https://argocd.example.com/applications/arhocd-app-dev +Source: +- Repo: https://github.com/ez4gotit/S25-core-course-labs.git + Target: lab13-solution + Path: k8s/helm-python + Helm Values: values-dev.yaml +SyncWindow: Sync Allowed +Sync Policy: Automated (Prune) +Sync Status: Synced to lab13-solution (b6080a7) +Health Status: Healthy + +GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE + Pod dev preinstall-hook Succeeded PreSync pod/preinstall-hook created + ServiceAccount dev internal-app Synced serviceaccount/internal-app created + Secret dev some-secret Synced secret/some-secret created + Service dev arhocd-app-dev-helm-python Synced Healthy service/arhocd-app-dev-helm-python created +apps Deployment dev arhocd-app-dev-helm-python Synced Healthy deployment.apps/arhocd-app-dev-helm-python created + Pod dev postinstall-hook Succeeded PostSync pod/postinstall-hook created +``` + +### After update replicaCount to 4 in `values-prod.yaml` + +```sh +argocd app get python-app-prod +``` +```text +Name: argocd/arhocd-app-prod +Project: default +Server: https://kubernetes.default.svc +Namespace: prod +URL: https://argocd.example.com/applications/arhocd-app-prod +Source: +- Repo: https://github.com/ez4gotit/S25-core-course-labs.git + Target: lab13-solution + Path: k8s/helm-python + Helm Values: values-prod.yaml +SyncWindow: Sync Allowed +Sync Policy: Automated (Prune) +Sync Status: OutOfSync from lab13-solution (deaa6d2) +Health Status: Healthy + +GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE + Pod prod preinstall-hook Running PreSync pod/preinstall-hook created + Secret prod some-secret Synced + Service prod arhocd-app-prod-helm-python Synced Healthy + ServiceAccount prod internal-app Synced +apps Deployment prod arhocd-app-prod-helm-python OutOfSync Healthy +``` + +## Self-Heal Testing + +### Test 1: Manual Override of Replica Count + +```sh +> kubectl patch deployment python-app-prod-app-python -n prod --patch '{"spec": {"replicas": 3}}' +``` +```sh +deployment.apps/python-app-prod-app-python patched +``` + +```sh +> argocd app sync python-app-prod +``` +```sh +TIMESTAMP GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE +2026-02-11T22:19:05+03:00 ConfigMap prod app-python-config Synced +2026-02-11T22:19:05+03:00 Service prod python-app-prod-app-python Synced Healthy +2026-02-11T22:19:05+03:00 ServiceAccount prod python-app-prod-app-python Synced +2026-02-11T22:19:05+03:00 apps Deployment prod python-app-prod-app-python Synced Healthy +2026-02-11T22:19:05+03:00 Pod prod preinstall-hook +2026-02-11T22:19:07+03:00 Pod prod preinstall-hook Running Synced PreSync pod/preinstall-hook created +2026-02-11T22:19:30+03:00 Pod prod preinstall-hook Succeeded Synced PreSync pod/preinstall-hook created +2026-02-11T22:19:30+03:00 ServiceAccount prod python-app-prod-app-python Synced serviceaccount/python-app-prod-app-python unchanged +2026-02-11T22:19:30+03:00 ConfigMap prod app-python-config Synced configmap/app-python-config unchanged +2026-02-11T22:19:30+03:00 Service prod python-app-prod-app-python Synced Healthy service/python-app-prod-app-python unchanged +2026-02-11T22:19:30+03:00 apps Deployment prod python-app-prod-app-python Synced Healthy deployment.apps/python-app-prod-app-python configured +2026-02-11T22:19:30+03:00 Pod prod postinstall-hook Running Synced PostSync pod/postinstall-hook created +2026-02-11T22:19:51+03:00 Pod prod postinstall-hook Succeeded Synced PostSync pod/postinstall-hook created + +Name: argocd/python-app-prod +Project: default +Server: https://kubernetes.default.svc +Namespace: prod +URL: https://argocd.example.com/applications/python-app-prod +Source: +- Repo: https://github.com/WellNotWell/DevOps-labs.git + Target: lab-13 + Path: k8s/app-python + Helm Values: values-prod.yaml +SyncWindow: Sync Allowed +Sync Policy: Automated (Prune) +Sync Status: Synced to lab-13 (f2aa19d) +Health Status: Healthy + +Operation: Sync +Sync Revision: f2aa19d3f88d7645f462e3a758aecd676c4b7d18 +Phase: Succeeded +Start: 2026-02-11 22:19:05 +0300 MSK +Finished: 2026-02-11 22:19:51 +0300 MSK +Duration: 46s +Message: successfully synced (no more tasks) + +GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE + Pod prod preinstall-hook Succeeded PreSync pod/preinstall-hook created + ServiceAccount prod python-app-prod-app-python Synced serviceaccount/python-app-prod-app-python unchanged + ConfigMap prod app-python-config Synced configmap/app-python-config unchanged + Service prod python-app-prod-app-python Synced Healthy service/python-app-prod-app-python unchanged +apps Deployment prod python-app-prod-app-python Synced Healthy deployment.apps/python-app-prod-app-python configured + Pod prod postinstall-hook Succeeded PostSync pod/postinstall-hook created +``` + +```sh +> argocd app get python-app-prod +``` +```sh +Name: argocd/python-app-prod +Project: default +Server: https://kubernetes.default.svc +Namespace: prod +URL: https://argocd.example.com/applications/python-app-prod +Source: +- Repo: https://github.com/WellNotWell/DevOps-labs.git + Target: lab-13 + Path: k8s/app-python + Helm Values: values-prod.yaml +SyncWindow: Sync Allowed +Sync Policy: Automated (Prune) +Sync Status: Synced to lab-13 (f2aa19d) +Health Status: Healthy + +GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE + Pod prod preinstall-hook Succeeded PreSync pod/preinstall-hook created + ServiceAccount prod python-app-prod-app-python Synced serviceaccount/python-app-prod-app-python unchanged + ConfigMap prod app-python-config Synced configmap/app-python-config unchanged + Service prod python-app-prod-app-python Synced Healthy service/python-app-prod-app-python unchanged +apps Deployment prod python-app-prod-app-python Synced Healthy deployment.apps/python-app-prod-app-python configured + Pod prod postinstall-hook Succeeded PostSync pod/postinstall-hook created +``` + +Additional verification run with updated screenshots: +![test_1_1](images/test_1_1.png) +![test_1_2](images/test_1_2.png) + +### Documentation + +#### Before deletion + +![alt text](image-19.png) +![alt text](image-20.png) + +#### After deletion + +![alt text](image-21.png) +![alt text](image-22.png) + +#### Explanation of how ArgoCD handles configuration drift vs. runtime events + +##### Configuration Drift +- ArgoCD continuously monitors the desired state (Git) vs. the actual state (Kubernetes) +- If a drift is detected, it marks the application as **OutOfSync** +- It can **automatically correct** drift based on sync policies + +##### Runtime Events +- External runtime events (e.g., pod restarts, scaling) do **not** trigger a sync +- ArgoCD focuses on declarative state enforcement, not transient runtime changes +- It does not react to **ephemeral** Kubernetes events unless they cause drift + + + + diff --git a/k8s/ArgoCD/argocd-python-app.yaml b/k8s/ArgoCD/argocd-python-app.yaml new file mode 100644 index 0000000000..886ff2d66a --- /dev/null +++ b/k8s/ArgoCD/argocd-python-app.yaml @@ -0,0 +1,20 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: arhocd-app + namespace: argocd +spec: + project: default + source: + repoURL: https://github.com/ez4gotit/S25-core-course-labs.git + targetRevision: lab13-solution + path: k8s/helm-python + helm: + valueFiles: + - values.yaml + destination: + server: https://kubernetes.default.svc + namespace: default + syncPolicy: + automated: {} + diff --git a/k8s/ArgoCD/argocd-python-dev.yaml b/k8s/ArgoCD/argocd-python-dev.yaml new file mode 100644 index 0000000000..c48ad16343 --- /dev/null +++ b/k8s/ArgoCD/argocd-python-dev.yaml @@ -0,0 +1,22 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: arhocd-app-dev + namespace: argocd +spec: + project: default + source: + repoURL: https://github.com/ez4gotit/S25-core-course-labs.git + targetRevision: lab13-solution + path: k8s/helm-python + helm: + valueFiles: + - values-dev.yaml + destination: + server: https://kubernetes.default.svc + namespace: dev + syncPolicy: + automated: + prune: true + selfHeal: true + diff --git a/k8s/ArgoCD/argocd-python-prod.yaml b/k8s/ArgoCD/argocd-python-prod.yaml new file mode 100644 index 0000000000..5040122d79 --- /dev/null +++ b/k8s/ArgoCD/argocd-python-prod.yaml @@ -0,0 +1,22 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: arhocd-app-prod + namespace: argocd +spec: + project: default + source: + repoURL: https://github.com/ez4gotit/S25-core-course-labs.git + targetRevision: lab13-solution + path: k8s/helm-python + helm: + valueFiles: + - values-prod.yaml + destination: + server: https://kubernetes.default.svc + namespace: prod + syncPolicy: + automated: + prune: true + selfHeal: true + diff --git a/k8s/HELM.md b/k8s/HELM.md new file mode 100644 index 0000000000..e1cc491767 --- /dev/null +++ b/k8s/HELM.md @@ -0,0 +1,487 @@ +# Helm + +This document records the Helm fundamentals and the chart implementation for the Kubernetes application, including installation, chart creation, hooks, and validation outputs. + +Dudinov Mikhail @ez4gotit + +## Installing Helm + +```sh +wget https://get.helm.sh/helm-v3.17.1-linux-amd64.tar.gz +tar zxvf helm-v3.17.1-linux-amd64.tar.gz +sudo mv linux-amd64/helm /usr/local/bin/helm +``` +![alt text](image-2.png) + +## Create Helm Chart +```sh +cd k8s/ +helm create helm-python +``` + +### Change `values.yaml` +```yaml +image: + repository: petrel312/flask_app + tag: "latest" +service: + type: ClusterIP + port: 5000 +``` + +```sh +helm install helm-python ./helm-python +``` +```text +NAME: helm-python +LAST DEPLOYED: 11 Februrary Thursday 2026 11:55 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +NOTES: +1. Get the application URL by running these commands: + export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=helm-python,app.kubernetes.io/instance=helm-python" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace default $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace default port-forward $POD_NAME 8080:$CONTAINER_PORT +``` + +Check: +```sh +helm list +``` +![alt text](image-3.png) + +## Access the Application + +```sh +minikube service helm-python +``` +```text +|-----------|-------------|-------------|--------------| +| NAMESPACE | NAME | TARGET PORT | URL | +|-----------|-------------|-------------|--------------| +| default | helm-python | | No node port | +|-----------|-------------|-------------|--------------| +service default/helm-python has no node port +Services [default/helm-python] have type "ClusterIP" not meant to be exposed, however for local development minikube allows you to access this. +Starting tunnel for service helm-python. +|-----------|-------------|-------------|------------------------| +| NAMESPACE | NAME | TARGET PORT | URL | +|-----------|-------------|-------------|------------------------| +| default | helm-python | | http://127.0.0.1:37613 | +|-----------|-------------|-------------|------------------------| +Opening service default/helm-python in default browser... +http://127.0.0.1:37613 +Because you are using a Docker driver on linux, the terminal needs to be open to run it. +``` +![alt text](image-4.png) + +## Output for `get pods,svc` +```text +NAME READY STATUS RESTARTS AGE +pod/helm-python-9966b5d8b-nx9n4 1/1 Running 0 11m + +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +service/helm-python ClusterIP 10.110.23.151 5000/TCP 11m +service/kubernetes ClusterIP 10.96.0.1 443/TCP 9d +``` + +## Chart Hooks + +### Create pre and post install .yaml files +```yaml +piVersion: v1 +kind: Pod +metadata: + name: preinstall-hook + annotations: + "helm.sh/hook": "pre-install" + "helm.sh/hook-delete-policy": "before-hook-creation" +spec: + containers: + - name: pre-install-container + image: busybox + imagePullPolicy: IfNotPresent + command: ['sh', '-c', 'echo The pre-install hook is running && sleep 20' ] + restartPolicy: Never + terminationGracePeriodSeconds: 0 +``` +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: postinstall-hook + annotations: + "helm.sh/hook": "post-install" + "helm.sh/hook-delete-policy": "before-hook-creation" +spec: + containers: + - name: post-install-container + image: busybox + imagePullPolicy: Always + command: ['sh', '-c', 'echo The post-install hook is running && sleep 15' ] + restartPolicy: Never + terminationGracePeriodSeconds: 0 + +``` + +### Troubleshoot Hooks +```sh +helm lint helm-python +``` +```text +==> Linting helm-python +[INFO] Chart.yaml: icon is recommended + +1 chart(s) linted, 0 chart(s) failed +``` + +```sh +helm install --dry-run helm-hooks helm-python +``` +```text +NAME: helm-hooks +LAST DEPLOYED: 11 Februrary Thursday 2026 11:55 +NAMESPACE: default +STATUS: pending-install +REVISION: 1 +HOOKS: +--- +# Source: helm-python/templates/post-install.yaml +apiVersion: v1 +kind: Pod +metadata: + name: postinstall-hook + annotations: + "helm.sh/hook": "post-install" + "helm.sh/hook-delete-policy": "before-hook-creation" +spec: + containers: + - name: post-install-container + image: busybox + imagePullPolicy: Always + command: ['sh', '-c', 'echo The post-install hook is running && sleep 15' ] + restartPolicy: Never + terminationGracePeriodSeconds: 0 +--- +# Source: helm-python/templates/pre-install.yaml +apiVersion: v1 +kind: Pod +metadata: + name: preinstall-hook + annotations: + "helm.sh/hook": "pre-install" + "helm.sh/hook-delete-policy": "before-hook-creation" +spec: + containers: + - name: pre-install-container + image: busybox + imagePullPolicy: IfNotPresent + command: ['sh', '-c', 'echo The pre-install hook is running && sleep 20' ] + restartPolicy: Never + terminationGracePeriodSeconds: 0 +--- +# Source: helm-python/templates/tests/test-connection.yaml +apiVersion: v1 +kind: Pod +metadata: + name: "helm-hooks-helm-python-test-connection" + labels: + helm.sh/chart: helm-python-0.1.0 + app.kubernetes.io/name: helm-python + app.kubernetes.io/instance: helm-hooks + app.kubernetes.io/version: "1.16.0" + app.kubernetes.io/managed-by: Helm + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['helm-hooks-helm-python:5000'] + restartPolicy: Never +MANIFEST: +--- +# Source: helm-python/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: helm-hooks-helm-python + labels: + helm.sh/chart: helm-python-0.1.0 + app.kubernetes.io/name: helm-python + app.kubernetes.io/instance: helm-hooks + app.kubernetes.io/version: "1.16.0" + app.kubernetes.io/managed-by: Helm +automountServiceAccountToken: true +--- +# Source: helm-python/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: helm-hooks-helm-python + labels: + helm.sh/chart: helm-python-0.1.0 + app.kubernetes.io/name: helm-python + app.kubernetes.io/instance: helm-hooks + app.kubernetes.io/version: "1.16.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + ports: + - port: 5000 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: helm-python + app.kubernetes.io/instance: helm-hooks +--- +# Source: helm-python/templates/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: helm-hooks-helm-python + labels: + helm.sh/chart: helm-python-0.1.0 + app.kubernetes.io/name: helm-python + app.kubernetes.io/instance: helm-hooks + app.kubernetes.io/version: "1.16.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: helm-python + app.kubernetes.io/instance: helm-hooks + template: + metadata: + labels: + helm.sh/chart: helm-python-0.1.0 + app.kubernetes.io/name: helm-python + app.kubernetes.io/instance: helm-hooks + app.kubernetes.io/version: "1.16.0" + app.kubernetes.io/managed-by: Helm + spec: + serviceAccountName: helm-hooks-helm-python + containers: + - name: helm-python + image: "petrel312/flask_app:latest" + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 5000 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + +NOTES: +1. Get the application URL by running these commands: + export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=helm-python,app.kubernetes.io/instance=helm-hooks" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace default $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace default port-forward $POD_NAME 8080:$CONTAINER_PORT +``` + +```sh +helm install helm-hooks helm-python +kubectl get po +``` +```text +NAME READY STATUS RESTARTS AGE +helm-hooks-helm-python-65499654d6-ds5fr 1/1 Running 0 36s +helm-python-9966b5d8b-nx9n4 1/1 Running 1 (13m ago) 51m +postinstall-hook 0/1 Completed 0 36s +preinstall-hook 0/1 Completed 0 59s +``` + +### Output +#### `kubectl get po` +```sh +kubectl get po +``` +```text +NAME READY STATUS RESTARTS AGE +helm-hooks-helm-python-65499654d6-ds5fr 1/1 Running 0 36s +helm-python-9966b5d8b-nx9n4 1/1 Running 1 (13m ago) 51m +postinstall-hook 0/1 Completed 0 36s +preinstall-hook 0/1 Completed 0 59s +``` + +#### `kubectl describe po ` +```sh +kubectl describe po preinstall-hook +``` +```text +Name: preinstall-hook +Namespace: default +Priority: 0 +Service Account: default +Node: minikube/192.168.49.2 +Start Time: 11 Februrary Thursday 2026 11:55 +Labels: +Annotations: helm.sh/hook: pre-install + helm.sh/hook-delete-policy: before-hook-creation +Status: Succeeded +IP: 10.244.0.47 +IPs: + IP: 10.244.0.47 +Containers: + pre-install-container: + Container ID: docker://4fef06f34191cf14da12fe8eaa8ec6fbb3c265eca3ed1ca695bf896c9d8ebb7c + Image: busybox + Image ID: docker-pullable://busybox@sha256:498a000f370d8c37927118ed80afe8adc38d1edcbfc071627d17b25c88efcab0 + Port: + Host Port: + Command: + sh + -c + echo The pre-install hook is running && sleep 20 + State: Terminated + Reason: Completed + Exit Code: 0 + Started: 11 Februrary Thursday 2026 11:55 + Finished: 11 Februrary Thursday 2026 11:55 + Ready: False + Restart Count: 0 + Environment: + Mounts: + /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-hcmq8 (ro) +Conditions: + Type Status + PodReadyToStartContainers False + Initialized True + Ready False + ContainersReady False + PodScheduled True +Volumes: + kube-api-access-hcmq8: + Type: Projected (a volume that contains injected data from multiple sources) + TokenExpirationSeconds: 3607 + ConfigMapName: kube-root-ca.crt + ConfigMapOptional: + DownwardAPI: true +QoS Class: BestEffort +Node-Selectors: +Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s + node.kubernetes.io/unreachable:NoExecute op=Exists for 300s +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal Scheduled 86s default-scheduler Successfully assigned default/preinstall-hook to minikube + Normal Pulled 85s kubelet Container image "busybox" already present on machine + Normal Created 85s kubelet Created container: pre-install-container + Normal Started 85s kubelet Started container pre-install-container +``` + +#### `kubectl describe po ` +```sh +kubectl describe po postinstall-hook +``` +```text +Name: postinstall-hook +Namespace: default +Priority: 0 +Service Account: default +Node: minikube/192.168.49.2 +Start Time: 11 Februrary Thursday 2026 11:55 +Labels: +Annotations: helm.sh/hook: post-install + helm.sh/hook-delete-policy: before-hook-creation +Status: Succeeded +IP: 10.244.0.49 +IPs: + IP: 10.244.0.49 +Containers: + post-install-container: + Container ID: docker://8507cb844c8a353b10fd0cd2bb0dcf876b1ffe927d9ed694ade0d0d50465dab5 + Image: busybox + Image ID: docker-pullable://busybox@sha256:498a000f370d8c37927118ed80afe8adc38d1edcbfc071627d17b25c88efcab0 + Port: + Host Port: + Command: + sh + -c + echo The post-install hook is running && sleep 15 + State: Terminated + Reason: Completed + Exit Code: 0 + Started: 11 Februrary Thursday 2026 11:55 + Finished: 11 Februrary Thursday 2026 11:55 + Ready: False + Restart Count: 0 + Environment: + Mounts: + /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-jwsnk (ro) +Conditions: + Type Status + PodReadyToStartContainers False + Initialized True + Ready False + ContainersReady False + PodScheduled True +Volumes: + kube-api-access-jwsnk: + Type: Projected (a volume that contains injected data from multiple sources) + TokenExpirationSeconds: 3607 + ConfigMapName: kube-root-ca.crt + ConfigMapOptional: + DownwardAPI: true +QoS Class: BestEffort +Node-Selectors: +Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s + node.kubernetes.io/unreachable:NoExecute op=Exists for 300s +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal Scheduled 2m11s default-scheduler Successfully assigned default/postinstall-hook to minikube + Normal Pulling 2m10s kubelet Pulling image "busybox" + Normal Pulled 2m8s kubelet Successfully pulled image "busybox" in 2.201s (2.201s including waiting). Image size: 4269694 bytes. + Normal Created 2m8s kubelet Created container: post-install-container + Normal Started 2m8s kubelet Started container post-install-container +``` + +### Hook Delete Policy. Cleanup +Change `"helm.sh/hook-delete-policy"` to `hook-succeeded` in post and pre install files +```sh +vim helm-python/templates/post-install.yaml +vim helm-python/templates/pre-install.yaml +``` +```yaml +"helm.sh/hook-delete-policy": "hook-succeeded" +``` + +```sh +kubectl get po +``` +```text +NAME READY STATUS RESTARTS AGE +helm-hooks-helm-python-65499654d6-tvbsg 1/1 Running 0 39s +helm-python-9966b5d8b-nx9n4 1/1 Running 1 (22m ago) 60m +python-app-6d8dbb66cd-gnwwx 1/1 Running 0 15m +python-app-6d8dbb66cd-kksvb 1/1 Running 0 15m +python-app-6d8dbb66cd-rgg8s 1/1 Running 0 15m +python-app-6d8dbb66cd-t5p64 1/1 Running 0 15m +``` + +```sh +kubectl get pods,svc +``` +![alt text](image-5.png) + +## Checklist + +- Helm installed and verified +- Helm chart created and configured +- Application deployed and accessible +- Hooks implemented and validated +- Hook deletion policy applied +- Outputs captured and documented \ No newline at end of file diff --git a/k8s/README.md b/k8s/README.md new file mode 100644 index 0000000000..6205649749 --- /dev/null +++ b/k8s/README.md @@ -0,0 +1,111 @@ + +# Kubernetes Deployment + + +## Kubernetes Setup and Basic Deployment + +```sh +# Create a deployment named 'python-app' using specified Docker image +kubectl create deployment python-app --image=petrel312/flask_app:latest +``` +```sh +# Check deployment status +kubectl get deployments +NAME READY UP-TO-DATE AVAILABLE AGE +python-app 1/1 1 1 64m +``` +```sh +# Expose the deployment to make it accessible via NodePort on port 5000 +kubectl expose deployment python-app --type=NodePort --port=5000 +``` + +## Check that the app is running: +```sh +# Get service URL (open it in browser) +minikube service python-app --url +http://192.168.49.2:31166 +``` + +```sh +|-----------|------------|-------------|---------------------------| +| NAMESPACE | NAME | TARGET PORT | URL | +|-----------|------------|-------------|---------------------------| +| default | python-app | 5000 | http://192.168.49.2:31166 | +|-----------|------------|-------------|---------------------------| +🎉 Opening service default/python-app in default browser... +👉 http://192.168.49.2:31166 +``` + +## Output for `kubectl get pods,svc`: +```sh +# Check current pods and services +kubectl get pods,svc + +NAME READY STATUS RESTARTS AGE +pod/python-app-5d9667f55b-mnssh 1/1 Running 1 (23m ago) 66m + +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +service/kubernetes ClusterIP 10.96.0.1 443/TCP 70m +service/python-app NodePort 10.97.49.146 5000:31166/TCP 65m +``` + +## Delete deployment: +```sh +# Remove the deployment +kubectl delete deployment python-app +deployment.apps "python-app" deleted +``` + +## Declarative Kubernetes Manifests + +```bash +# Apply all manifests in the current directory (deployment.yml, service.yml) +kubectl apply -f . +deployment.apps/python-app created +service/python-app configured +``` + +```bash +# Check that new pods and services were created +kubectl get pods,svc +NAME READY STATUS RESTARTS AGE +pod/python-app-6d8dbb66cd-9w6m8 1/1 Running 0 28s +pod/python-app-6d8dbb66cd-ch26l 1/1 Running 0 28s +pod/python-app-6d8dbb66cd-wnhbw 1/1 Running 0 28s +pod/python-app-6d8dbb66cd-x7qtk 1/1 Running 0 28s + +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +service/kubernetes ClusterIP 10.96.0.1 443/TCP 84m +service/python-app NodePort 10.97.49.146 5000:31166/TCP 80m +``` + +```bash +# List all services and their URLs in Minikube +minikube service --all +|-----------|------------|-------------|--------------| +| NAMESPACE | NAME | TARGET PORT | URL | +|-----------|------------|-------------|--------------| +| default | kubernetes | | No node port | +|-----------|------------|-------------|--------------| +😿 service default/kubernetes has no node port +|-----------|------------|-------------|---------------------------| +| NAMESPACE | NAME | TARGET PORT | URL | +|-----------|------------|-------------|---------------------------| +| default | python-app | 5000 | http://192.168.49.2:31166 | +|-----------|------------|-------------|---------------------------| +❗ Services [default/kubernetes] have type "ClusterIP" not meant to be exposed, however for local development minikube allows you to access this ! +🎉 Opening service default/python-app in default browser... +👉 http://192.168.49.2:31166 +🏃 Starting tunnel for service kubernetes. +|-----------|------------|-------------|------------------------| +| NAMESPACE | NAME | TARGET PORT | URL | +|-----------|------------|-------------|------------------------| +| default | kubernetes | | http://127.0.0.1:36947 | +|-----------|------------|-------------|------------------------| +🎉 Opening service default/kubernetes in default browser... +👉 http://127.0.0.1:36947 +❗ Because you are using a Docker driver on linux, the terminal needs to be open to run it. +``` + +![alt text](image.png) +![alt text](image-1.png) diff --git a/k8s/deployment.yml b/k8s/deployment.yml new file mode 100644 index 0000000000..9c0ba5148c --- /dev/null +++ b/k8s/deployment.yml @@ -0,0 +1,19 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: python-app +spec: + replicas: 4 + selector: + matchLabels: + app: python-app + template: + metadata: + labels: + app: python-app + spec: + containers: + - name: python-app + image: ez4gotit/flask_app:latest + ports: + - containerPort: 5000 \ No newline at end of file diff --git a/k8s/helm-python/.helmignore b/k8s/helm-python/.helmignore new file mode 100644 index 0000000000..0e8a0eb36f --- /dev/null +++ b/k8s/helm-python/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/k8s/helm-python/Chart.yaml b/k8s/helm-python/Chart.yaml new file mode 100644 index 0000000000..f39c58620a --- /dev/null +++ b/k8s/helm-python/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: helm-python +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/k8s/helm-python/files/config.json b/k8s/helm-python/files/config.json new file mode 100644 index 0000000000..2130581a71 --- /dev/null +++ b/k8s/helm-python/files/config.json @@ -0,0 +1,8 @@ +{ + "appName": "helm-python", + "environment": "dev", + "features": { + "visits": true, + "metrics": false + } +} \ No newline at end of file diff --git a/k8s/helm-python/templates/NOTES.txt b/k8s/helm-python/templates/NOTES.txt new file mode 100644 index 0000000000..95a9061e45 --- /dev/null +++ b/k8s/helm-python/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "helm-python.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "helm-python.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "helm-python.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "helm-python.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/k8s/helm-python/templates/_helpers.tpl b/k8s/helm-python/templates/_helpers.tpl new file mode 100644 index 0000000000..655bb8d718 --- /dev/null +++ b/k8s/helm-python/templates/_helpers.tpl @@ -0,0 +1,84 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "helm-python.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "helm-python.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "helm-python.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "helm-python.labels" -}} +helm.sh/chart: {{ include "helm-python.chart" . }} +{{ include "helm-python.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "helm-python.selectorLabels" -}} +app.kubernetes.io/name: {{ include "helm-python.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "helm-python.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "helm-python.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Secret-based environment variables. +*/}} +{{- define "helm-python.secretEnv" -}} +{{- $secretName := .Values.secrets.name | default "some-secret" -}} +- name: SOME_SECRET + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: SOME_SECRET +- name: APP_USERNAME + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: username +- name: APP_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: password +{{- end }} diff --git a/k8s/helm-python/templates/configmap.yaml b/k8s/helm-python/templates/configmap.yaml new file mode 100644 index 0000000000..b8804cfc39 --- /dev/null +++ b/k8s/helm-python/templates/configmap.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "helm-python.fullname" . }}-config + labels: + {{- include "helm-python.labels" . | nindent 4 }} +data: + config.json: | + {{- .Files.Get "files/config.json" | nindent 4 }} \ No newline at end of file diff --git a/k8s/helm-python/templates/deployment.yaml b/k8s/helm-python/templates/deployment.yaml new file mode 100644 index 0000000000..0039111b1e --- /dev/null +++ b/k8s/helm-python/templates/deployment.yaml @@ -0,0 +1,86 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "helm-python.fullname" . }} + labels: + {{- include "helm-python.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "helm-python.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "helm-python.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "helm-python.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + {{- include "helm-python.secretEnv" . | nindent 12 }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - name: app-config + mountPath: /config.json + subPath: config.json + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - name: app-config + configMap: + name: {{ include "helm-python.fullname" . }}-config + {{- with .Values.volumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/k8s/helm-python/templates/hpa.yaml b/k8s/helm-python/templates/hpa.yaml new file mode 100644 index 0000000000..b7b1db27cb --- /dev/null +++ b/k8s/helm-python/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "helm-python.fullname" . }} + labels: + {{- include "helm-python.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "helm-python.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/k8s/helm-python/templates/ingress.yaml b/k8s/helm-python/templates/ingress.yaml new file mode 100644 index 0000000000..5d2af89c7b --- /dev/null +++ b/k8s/helm-python/templates/ingress.yaml @@ -0,0 +1,43 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "helm-python.fullname" . }} + labels: + {{- include "helm-python.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- with .pathType }} + pathType: {{ . }} + {{- end }} + backend: + service: + name: {{ include "helm-python.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/k8s/helm-python/templates/post-install.yaml b/k8s/helm-python/templates/post-install.yaml new file mode 100644 index 0000000000..272e5c0b80 --- /dev/null +++ b/k8s/helm-python/templates/post-install.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: postinstall-hook + annotations: + "helm.sh/hook": "post-install" + "helm.sh/hook-delete-policy": "hook-succeeded" +spec: + containers: + - name: post-install-container + image: busybox + imagePullPolicy: Always + command: ['sh', '-c', 'echo The post-install hook is running && sleep 15' ] + restartPolicy: Never + terminationGracePeriodSeconds: 0 diff --git a/k8s/helm-python/templates/pre-install.yaml b/k8s/helm-python/templates/pre-install.yaml new file mode 100644 index 0000000000..a53914cd56 --- /dev/null +++ b/k8s/helm-python/templates/pre-install.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: preinstall-hook + annotations: + "helm.sh/hook": "pre-install" + "helm.sh/hook-delete-policy": "hook-succeeded" +spec: + containers: + - name: pre-install-container + image: busybox + imagePullPolicy: IfNotPresent + command: ['sh', '-c', 'echo The pre-install hook is running && sleep 20' ] + restartPolicy: Never + terminationGracePeriodSeconds: 0 diff --git a/k8s/helm-python/templates/secrets.yaml b/k8s/helm-python/templates/secrets.yaml new file mode 100644 index 0000000000..bac292f307 --- /dev/null +++ b/k8s/helm-python/templates/secrets.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Values.secrets.name | default "some-secret" }} + labels: + {{- include "helm-python.labels" . | nindent 4 }} +data: + {{- range $key, $val := .Values.secrets.data }} + {{ $key }}: {{ $val | quote | b64enc }} + {{- end }} diff --git a/k8s/helm-python/templates/service.yaml b/k8s/helm-python/templates/service.yaml new file mode 100644 index 0000000000..c47d7d8515 --- /dev/null +++ b/k8s/helm-python/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "helm-python.fullname" . }} + labels: + {{- include "helm-python.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "helm-python.selectorLabels" . | nindent 4 }} diff --git a/k8s/helm-python/templates/serviceaccount.yaml b/k8s/helm-python/templates/serviceaccount.yaml new file mode 100644 index 0000000000..c0c799cc82 --- /dev/null +++ b/k8s/helm-python/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "helm-python.serviceAccountName" . }} + labels: + {{- include "helm-python.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/k8s/helm-python/templates/tests/test-connection.yaml b/k8s/helm-python/templates/tests/test-connection.yaml new file mode 100644 index 0000000000..9254394e7a --- /dev/null +++ b/k8s/helm-python/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "helm-python.fullname" . }}-test-connection" + labels: + {{- include "helm-python.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "helm-python.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/k8s/helm-python/values-dev.yaml b/k8s/helm-python/values-dev.yaml new file mode 100644 index 0000000000..1a4ee9e5c5 --- /dev/null +++ b/k8s/helm-python/values-dev.yaml @@ -0,0 +1,9 @@ +replicaCount: 1 + +resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 50m + memory: 64Mi \ No newline at end of file diff --git a/k8s/helm-python/values-prod.yaml b/k8s/helm-python/values-prod.yaml new file mode 100644 index 0000000000..1d2a32b74f --- /dev/null +++ b/k8s/helm-python/values-prod.yaml @@ -0,0 +1,9 @@ +replicaCount: 4 + +resources: + limits: + cpu: 300m + memory: 512Mi + requests: + cpu: 150m + memory: 256Mi \ No newline at end of file diff --git a/k8s/helm-python/values.yaml b/k8s/helm-python/values.yaml new file mode 100644 index 0000000000..b33a00b61e --- /dev/null +++ b/k8s/helm-python/values.yaml @@ -0,0 +1,147 @@ +# Default values for helm-python. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ +replicaCount: 1 + +# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ +image: + repository: petrel312/flask_app + # This sets the pull policy for images. + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "latest" + +# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +imagePullSecrets: [] +# This is to override the chart name. +nameOverride: "" +fullnameOverride: "" + +# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "internal-app" + +# This is for setting Kubernetes Annotations to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +podAnnotations: { + "vault.hashicorp.com/agent-inject": "true", + "vault.hashicorp.com/role": "internal-app", + "vault.hashicorp.com/agent-inject-secret-database-config.txt": "internal/data/database/config", + "vault.hashicorp.com/agent-inject-template-database-config.txt": "username={{`{{- with secret \\\"internal/data/database/config\\\" -}}`}}{{`{{ .Data.data.username }}`}}{{`{{- end -}}`}}\npassword={{`{{- with secret \\\"internal/data/database/config\\\" -}}`}}{{`{{ .Data.data.password }}`}}{{`{{- end -}}`}}", + "vault.hashicorp.com/agent-inject-command": "sh -c 'test -s /vault/secrets/database-config.txt'" +} + +#podAnnotations: {} + +# This is for setting Kubernetes Labels to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ +service: + # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: ClusterIP + # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports + port: 5000 + +# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +#resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +resources: + limits: + cpu: 200m + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi + +# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ +livenessProbe: + httpGet: + path: / + port: http +readinessProbe: + httpGet: + path: / + port: http + +# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +secrets: + name: "some-secret" + data: + SOME_SECRET: "P@ssw0rd" + username: "appuser" + password: "P@ssw0rd" diff --git a/k8s/image-1.png b/k8s/image-1.png new file mode 100644 index 0000000000..de559a650b Binary files /dev/null and b/k8s/image-1.png differ diff --git a/k8s/image-10.png b/k8s/image-10.png new file mode 100644 index 0000000000..fc3efa886d Binary files /dev/null and b/k8s/image-10.png differ diff --git a/k8s/image-11.png b/k8s/image-11.png new file mode 100644 index 0000000000..5f4bfc6216 Binary files /dev/null and b/k8s/image-11.png differ diff --git a/k8s/image-12.png b/k8s/image-12.png new file mode 100644 index 0000000000..4280bf0c29 Binary files /dev/null and b/k8s/image-12.png differ diff --git a/k8s/image-13.png b/k8s/image-13.png new file mode 100644 index 0000000000..d46d40affe Binary files /dev/null and b/k8s/image-13.png differ diff --git a/k8s/image-14.png b/k8s/image-14.png new file mode 100644 index 0000000000..9af7b33746 Binary files /dev/null and b/k8s/image-14.png differ diff --git a/k8s/image-15.png b/k8s/image-15.png new file mode 100644 index 0000000000..c2204efb0f Binary files /dev/null and b/k8s/image-15.png differ diff --git a/k8s/image-16.png b/k8s/image-16.png new file mode 100644 index 0000000000..00e08e529b Binary files /dev/null and b/k8s/image-16.png differ diff --git a/k8s/image-17.png b/k8s/image-17.png new file mode 100644 index 0000000000..70cd8697ff Binary files /dev/null and b/k8s/image-17.png differ diff --git a/k8s/image-18.png b/k8s/image-18.png new file mode 100644 index 0000000000..5a8afca4e4 Binary files /dev/null and b/k8s/image-18.png differ diff --git a/k8s/image-19.png b/k8s/image-19.png new file mode 100644 index 0000000000..1b2f136cc5 Binary files /dev/null and b/k8s/image-19.png differ diff --git a/k8s/image-2.png b/k8s/image-2.png new file mode 100644 index 0000000000..9cdf8bad77 Binary files /dev/null and b/k8s/image-2.png differ diff --git a/k8s/image-20.png b/k8s/image-20.png new file mode 100644 index 0000000000..eed01672ea Binary files /dev/null and b/k8s/image-20.png differ diff --git a/k8s/image-21.png b/k8s/image-21.png new file mode 100644 index 0000000000..3e0d3421cb Binary files /dev/null and b/k8s/image-21.png differ diff --git a/k8s/image-22.png b/k8s/image-22.png new file mode 100644 index 0000000000..d148f3364d Binary files /dev/null and b/k8s/image-22.png differ diff --git a/k8s/image-3.png b/k8s/image-3.png new file mode 100644 index 0000000000..9d91b93fd1 Binary files /dev/null and b/k8s/image-3.png differ diff --git a/k8s/image-4.png b/k8s/image-4.png new file mode 100644 index 0000000000..89ec90bb38 Binary files /dev/null and b/k8s/image-4.png differ diff --git a/k8s/image-5.png b/k8s/image-5.png new file mode 100644 index 0000000000..c2d0c2cd8e Binary files /dev/null and b/k8s/image-5.png differ diff --git a/k8s/image-6.png b/k8s/image-6.png new file mode 100644 index 0000000000..49daeb0a77 Binary files /dev/null and b/k8s/image-6.png differ diff --git a/k8s/image-7.png b/k8s/image-7.png new file mode 100644 index 0000000000..6465c9c7cc Binary files /dev/null and b/k8s/image-7.png differ diff --git a/k8s/image-8.png b/k8s/image-8.png new file mode 100644 index 0000000000..9b8a89d011 Binary files /dev/null and b/k8s/image-8.png differ diff --git a/k8s/image-9.png b/k8s/image-9.png new file mode 100644 index 0000000000..9dc29e31c0 Binary files /dev/null and b/k8s/image-9.png differ diff --git a/k8s/image.png b/k8s/image.png new file mode 100644 index 0000000000..870a16ec4e Binary files /dev/null and b/k8s/image.png differ diff --git a/k8s/img.png b/k8s/img.png new file mode 100644 index 0000000000..39666d7b7e Binary files /dev/null and b/k8s/img.png differ diff --git a/k8s/img2.png b/k8s/img2.png new file mode 100644 index 0000000000..83842bbba1 Binary files /dev/null and b/k8s/img2.png differ diff --git a/k8s/img3.png b/k8s/img3.png new file mode 100644 index 0000000000..a48d4be1c0 Binary files /dev/null and b/k8s/img3.png differ diff --git a/k8s/lab11_HELM_EXTENDED.md b/k8s/lab11_HELM_EXTENDED.md new file mode 100644 index 0000000000..d60c190746 --- /dev/null +++ b/k8s/lab11_HELM_EXTENDED.md @@ -0,0 +1,194 @@ +# Kubernetes Secrets and Hashicorp Vault + +This document summarizes the implementation steps and evidence for Kubernetes Secrets, Helm-managed secrets, and HashiCorp Vault integration for Lab 11. + +Author: Mikhail Dudinov @ez4gotit + +## 1. Creating Secret via kubectl + +Required secret format for the task: +- name: app-credentials +- keys: username, password + +Example command set for the required secret: +```sh +kubectl create secret generic app-credentials \ + --from-literal=username="appuser" \ + --from-literal=password="P@ssw0rd" +``` + +Notes on encoding and encryption: +- Kubernetes Secrets store values as base64-encoded strings. Base64 is encoding, not encryption, and provides no confidentiality. +- By default, Kubernetes does not encrypt Secret data at rest in etcd. Encryption at rest must be explicitly enabled with an encryption provider configuration. +- etcd encryption is recommended in production to reduce exposure of Secret values in persistent storage. Access control, RBAC, and network policy should also be enforced. + +```sh +kubectl create secret generic test --from-literal=SOME_SECRET=P@ssw0rd +kubectl get secrets +kubectl describe secret test +kubectl get secret test -o jsonpath='{.data}' +echo "UEBzc3cwcmQ=" | base64 --decode +``` + +![alt text](image-6.png) + +## Secrets with Helm + +This section applies Secret templating in the Helm chart and injects secret values into the Deployment as environment variables. The Secret name is configurable and defaults to `some-secret`. Labels use the chart helper templates for consistent metadata. + +### Create `secrets.yaml` +```sh +vim helm-python/templates/secrets.yaml +``` +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Values.secrets.name | default "some-secret" }} + labels: + {{- include "helm-python.labels" . | nindent 4 }} +data: + {{- range $key, $val := .Values.secrets.data }} + {{ $key }}: {{ $val | quote | b64enc }} + {{- end }} +``` + +### Add env into Deployment +```sh +vim helm-python/templates/deployment.yaml +``` +```yaml +env: + - name: SOME_SECRET + valueFrom: + secretKeyRef: + name: {{ .Values.secrets.name | default "some-secret" }} + key: SOME_SECRET +``` + +### Edit `values.yaml` +```sh +vim helm-python/values.yaml +``` +```yaml +secrets: + name: "some-secret" + data: + SOME_SECRET: "P@ssw0rd" + username: "appuser" + password: "P@ssw0rd" +``` + +### Helm Upgrade +```bash +helm upgrade --install helm-secrets ./helm-python/ +kubectl get pods,svc +``` + +![alt text](image-7.png) + +### Pod Verification +```bash +kubectl exec pod/helm-secrets-helm-python-84f9c74747-scc9f -- printenv | grep SOME_SECRET +``` + +![alt text](image-8.png) + +## Vault Secret Management System + +Vault is used for centralized secret management with stronger access control, auditing, and secret injection capabilities. The deployment uses Vault in development mode for learning purposes. + +### Install Vault +```sh +helm repo add hashicorp https://helm.releases.hashicorp.com +helm repo update +helm install vault hashicorp/vault --set "server.dev.enabled=true" +kubectl get pods +``` + +![alt text](image-9.png) + +### Vault Configuration +#### Enable KV-v2 Secrets +```sh +kubectl exec -it vault-0 -- /bin/sh +vault secrets enable -path=internal kv-v2 +vault kv put secret/database/config username="dbuser" password="somepassword" +vault kv get secret/database/config +exit +``` + +![alt text](image-10.png) + +#### Enable Kubernetes Auth +```sh +kubectl exec -it vault-0 -- /bin/sh +vault auth enable kubernetes +vault write auth/kubernetes/config kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443" +``` + +#### Create a Policy for read access to secret +```sh +vault policy write internal-app - << EOF +path "internal/data/database/config" { + capabilities = ["read"] +} +EOF +``` + +#### Create vault policy +```sh +vault write auth/kubernetes/role/internal-app \ + bound_service_account_names=internal-app \ + bound_service_account_namespaces=default \ + policies=internal-app \ + ttl=24h +exit +``` + +![alt text](image-11.png) + +#### Check result +```sh +kubectl create sa internal-app +kubectl get serviceaccounts +``` + +![alt text](image-12.png) + +```sh +kubectl annotate serviceaccount internal-app \ + meta.helm.sh/release-name=helm-secrets \ + meta.helm.sh/release-namespace=default -n default +``` + +```sh +kubectl label serviceaccount internal-app \ + app.kubernetes.io/managed-by=Helm -n default +helm install helm-secrets ./helm-python +``` + +```sh +vim helm-python/values.yaml +helm upgrade --install helm-secrets ./helm-python +kubectl get po +``` + +![alt text](image-13.png) + +```sh +kubectl exec -it helm-secrets-helm-python-6dbb75d64f-r4ww9 --container helm-python -- sh +``` + +![alt text](image-14.png) + +### Resource Management +```yaml +resources: + limits: + cpu: 200m + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi +``` diff --git a/k8s/service.yml b/k8s/service.yml new file mode 100644 index 0000000000..3efdc337f6 --- /dev/null +++ b/k8s/service.yml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: python-app +spec: + type: NodePort + selector: + app: python-app + ports: + - protocol: TCP + port: 5000 + targetPort: 5000 \ No newline at end of file diff --git a/monitoring/LOGGING.md b/monitoring/LOGGING.md new file mode 100644 index 0000000000..139884e985 --- /dev/null +++ b/monitoring/LOGGING.md @@ -0,0 +1,40 @@ +# Logging using Grafana Loki + +## Overview + +Our logging system includes Grafana, Loki, Promtail + +## Components + +### Grafana + +* Grafana provides an intuitive web UI that lets you create queries and visualize data in custom dashboards. +* It fetches logs directly from Loki, allowing you to display and analyze log data alongside other metrics. +* Runs on port 3000 and, in this setup, is configured to work without authentication for easy access. + +### Loki + +* Loki is designed to efficiently store and index logs, making it quick and effective to search through time-series log data. +* It runs using the configuration file located at `/etc/loki/local-config.yaml`, which defines its behavior, storage backend, and indexing schema. +* Listens on port 3100 for incoming log streams and uses a time-series database approach to store logs in an organized manner. + +### Promtail + +* Promtail gathers logs from your containerized environments and forwards them to Loki, ensuring that all relevant log data is captured. +* Operates with settings defined in the `promtail-config.yaml` +* Key settings: + - Listens for HTTP requests on port 9080. + - Monitors log files from the directory `/var/lib/docker/docker/containers/*/*log`, which is where Docker stores container logs. + - Saves its progress in the file `/tmp/positions.yaml` to avoid reprocessing logs. + - Sends collected logs to Loki at `http://loki:3100/api/prom/push` + +## Screenshots + +### Docker Containers +![Docker Containers](image-1.png) + +### All Logs +![All Logs](image-2.png) + +### App Logs +![App Logs logs](image.png) \ No newline at end of file diff --git a/monitoring/METRICS.md b/monitoring/METRICS.md new file mode 100644 index 0000000000..0f54b8bfce --- /dev/null +++ b/monitoring/METRICS.md @@ -0,0 +1,19 @@ +# Metrics + +Installation includes a Prometheus container that collects metrics from Prometheus, a Loki container and web-app + +## Key Functionalities: +1. **Networking**: +All services are connected to the monitoring network for inter-service communication +2. **Resource Limits**: +Memory limits are defined for each service (128M to 512M) to optimize resource usage. + +## Logs & Metrics +![alt text](image-3.png) +![alt text](image-4.png) +![alt text](image-5.png) +![alt text](image-6.png) +![alt text](image-7.png) +![alt text](image-8.png) +![alt text](image-9.png) +![alt text](image-10.png) diff --git a/monitoring/docker-compose.yml b/monitoring/docker-compose.yml new file mode 100644 index 0000000000..b3f1387e5f --- /dev/null +++ b/monitoring/docker-compose.yml @@ -0,0 +1,103 @@ +services: + loki: + image: grafana/loki:latest + ports: + - "3100:3100" + command: -config.file=/etc/loki/local-config.yaml + restart: always + deploy: + resources: + limits: + memory: 512M + networks: + - lab7 + + promtail: + image: grafana/promtail:latest + volumes: + - /var/log:/var/log + - ./promtail-config.yaml:/etc/promtail/config.yml + - /var/lib/docker/containers:/var/lib/docker/containers:ro + command: -config.file=/etc/promtail/config.yml + restart: always + deploy: + resources: + limits: + memory: 128M + networks: + - lab7 + + grafana: + environment: + - GF_PATHS_PROVISIONING=/etc/grafana/provisioning + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + - GF_FEATURE_TOGGLES_ENABLE=alertingSimplifiedRouting,alertingQueryAndExpressionsStepMode + entrypoint: + - sh + - -euc + - | + mkdir -p /etc/grafana/provisioning/datasources + cat < /etc/grafana/provisioning/datasources/ds.yaml + apiVersion: 1 + datasources: + - name: Loki + type: loki + access: proxy + orgId: 1 + url: http://loki:3100 + basicAuth: false + isDefault: true + version: 1 + editable: false + - name: Prometheus + type: prometheus + access: proxy + orgId: 1 + url: http://prometheus:9090 + basicAuth: false + isDefault: false + version: 1 + editable: true + EOF + /run.sh + image: grafana/grafana:latest + ports: + - "3000:3000" + restart: always + deploy: + resources: + limits: + memory: 256M + networks: + - lab7 + + web_app: + image: petrel312/flask_app:latest + ports: + - "5000:5000" + restart: always + deploy: + resources: + limits: + memory: 128M + networks: + - lab7 + + prometheus: + image: prom/prometheus:latest + ports: + - "9090:9090" + volumes: + - ./prometheus-config.yaml:/etc/prometheus/prometheus.yml + restart: always + deploy: + resources: + limits: + memory: 512M + networks: + - lab7 + +networks: + lab7: + driver: bridge \ No newline at end of file diff --git a/monitoring/image-1.png b/monitoring/image-1.png new file mode 100644 index 0000000000..3716ae58c6 Binary files /dev/null and b/monitoring/image-1.png differ diff --git a/monitoring/image-10.png b/monitoring/image-10.png new file mode 100644 index 0000000000..a666f5345e Binary files /dev/null and b/monitoring/image-10.png differ diff --git a/monitoring/image-2.png b/monitoring/image-2.png new file mode 100644 index 0000000000..e3749e6839 Binary files /dev/null and b/monitoring/image-2.png differ diff --git a/monitoring/image-3.png b/monitoring/image-3.png new file mode 100644 index 0000000000..415a869c88 Binary files /dev/null and b/monitoring/image-3.png differ diff --git a/monitoring/image-4.png b/monitoring/image-4.png new file mode 100644 index 0000000000..19a536b359 Binary files /dev/null and b/monitoring/image-4.png differ diff --git a/monitoring/image-5.png b/monitoring/image-5.png new file mode 100644 index 0000000000..b8a3049e3f Binary files /dev/null and b/monitoring/image-5.png differ diff --git a/monitoring/image-6.png b/monitoring/image-6.png new file mode 100644 index 0000000000..b8a3049e3f Binary files /dev/null and b/monitoring/image-6.png differ diff --git a/monitoring/image-7.png b/monitoring/image-7.png new file mode 100644 index 0000000000..e1de5b2bf7 Binary files /dev/null and b/monitoring/image-7.png differ diff --git a/monitoring/image-8.png b/monitoring/image-8.png new file mode 100644 index 0000000000..9e672bda75 Binary files /dev/null and b/monitoring/image-8.png differ diff --git a/monitoring/image-9.png b/monitoring/image-9.png new file mode 100644 index 0000000000..e09e5594e5 Binary files /dev/null and b/monitoring/image-9.png differ diff --git a/monitoring/image.png b/monitoring/image.png new file mode 100644 index 0000000000..4ef3e5643c Binary files /dev/null and b/monitoring/image.png differ diff --git a/monitoring/prometheus-config.yaml b/monitoring/prometheus-config.yaml new file mode 100644 index 0000000000..e0095e14e3 --- /dev/null +++ b/monitoring/prometheus-config.yaml @@ -0,0 +1,19 @@ +global: + scrape_interval: 15s + +scrape_configs: + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] + + - job_name: 'web_app' + static_configs: + - targets: ['python_app:5000'] + + - job_name: 'loki' + static_configs: + - targets: ['loki:3100'] + + - job_name: 'grafana' + static_configs: + - targets: ['localhost:3000'] \ No newline at end of file diff --git a/monitoring/promtail-config.yaml b/monitoring/promtail-config.yaml new file mode 100644 index 0000000000..064e2e1b9c --- /dev/null +++ b/monitoring/promtail-config.yaml @@ -0,0 +1,18 @@ +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: web_app_logs + static_configs: + - targets: + - localhost + labels: + job: varlogs + __path__: /var/lib/docker/containers/*/*log diff --git a/terraform/.gitignore b/terraform/.gitignore new file mode 100644 index 0000000000..8b63d63bcf --- /dev/null +++ b/terraform/.gitignore @@ -0,0 +1,32 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +env/ +venv/ +*.env +instance/ +*.log +*.pot +*.pyc +.vscode/ +.idea/ +.DS_Store +Thumbs.db +.coverage +nosetests.xml +*.cover +*.hypothesis/ +*.tox/ +*.nox/ +*.coverage +venv/ +.env/ +**/.terraform/ +**/.terraform.lock.hcl +**/terraform.tfstate +**/terraform.tfstate.backup +yandex/key.json +**/terraform.tfstate.*.backup + diff --git a/terraform/TF.md b/terraform/TF.md new file mode 100644 index 0000000000..edc554eaaf --- /dev/null +++ b/terraform/TF.md @@ -0,0 +1,648 @@ +# Terraform Infrastructure + +## Preparation & Installing Terraform +### Install on Arch +```sh +sudo pacman -S terraform +``` +### Add to path +```sh +export PATH=$PATH:/path/to/terraform +``` +### Check +```sh +terraform --version +``` +### Start docker-service +```sh +sudo systemctl start docker +``` + +## Test usage with python web-app +### terraform state list +``` +docker_container.python_container +docker_image.python_app +``` + +### terraform state show +#### docker_container.python_container: +``` +resource "docker_container" "python_container" { + attach = false + bridge = null + command = [ + "python", + "web.py", + ] + container_read_refresh_timeout_milliseconds = 15000 + cpu_set = null + cpu_shares = 0 + domainname = null + entrypoint = [] + env = [] + hostname = "589f151df519" + id = "589f151df5190a33436fc6e84753f6f0be1d2a83ef4295c9cd37b616256f8699" + image = "sha256:a5c2a8018de16fc6f2546861f9e710a4a169ef14d8304a68fb223fc208c37c6f" + init = false + ipc_mode = "private" + log_driver = "json-file" + logs = false + max_retry_count = 0 + memory = 0 + memory_swap = 0 + must_run = true + name = "app_python" + network_data = [ + { + gateway = "172.17.0.1" + global_ipv6_address = null + global_ipv6_prefix_length = 0 + ip_address = "172.17.0.2" + ip_prefix_length = 16 + ipv6_gateway = null + mac_address = "02:42:ac:11:00:02" + network_name = "bridge" + }, + ] + network_mode = "bridge" + pid_mode = null + privileged = false + publish_all_ports = false + read_only = false + remove_volumes = true + restart = "no" + rm = false + runtime = "runc" + security_opts = [] + shm_size = 64 + start = true + stdin_open = false + stop_signal = null + stop_timeout = 0 + tty = false + user = "flask_app_user" + userns_mode = null + wait = false + wait_timeout = 60 + working_dir = "/app_python" + + ports { + external = 5000 + internal = 5000 + ip = "0.0.0.0" + protocol = "tcp" + } +} +``` + +#### docker_image.python_app: +``` +resource "docker_image" "python_app" { + id = "sha256:78a74fb73bfb12a8641cc50cbc82f57c610aaafa73b628896cb71a475497922cpython:3.11" + image_id = "sha256:78a74fb73bfb12a8641cc50cbc82f57c610aaafa73b628896cb71a475497922c" + name = "python:3.11" + repo_digest = "python@sha256:14b4620f59a90f163dfa6bd252b68743f9a41d494a9fde935f9d7669d98094bb" +} +``` + +### terraform output +``` +python_container_id = "589f151df5190a33436fc6e84753f6f0be1d2a83ef4295c9cd37b616256f8699" +python_container_image = "petrel312/flask_app:latest" +python_container_name = "app_python" +``` + +## Integration with YandexCloud + +I used this guide [https://yandex.cloud/en-ru/docs/tutorials/infrastructure-management/terraform-quickstart#linux_1](https://yandex.cloud/en-ru/docs/tutorials/infrastructure-management/terraform-quickstart#linux_1) + +### Prepare YandexCloud +1. Go to [https://console.yandex.cloud/](https://console.yandex.cloud/) +2. Create account +3. Create billing account. It should have status either `ACTIVE` or `TRIAL_ACTIVE` +4. Create Authorized Key and download `.json` with private and public key +5. Configure user +```sh +yc config profile create +yc config set service-account-key key.json +yc config set cloud-id +yc config set folder-id +``` +6. Export env variables +```sh +export YC_TOKEN=$(yc iam create-token) +export YC_CLOUD_ID=$(yc config get cloud-id) +export YC_FOLDER_ID=$(yc config get folder-id) +``` + +### Install YC on Arch +```sh +curl -sSL https://storage.yandexcloud.net/yandexcloud-yc/install.sh | bash +``` +### Check +```sh +yc --version +``` + +### Configure ssh key for user: +```sh +ssh-keygen +yc organization-manager organization list +yc organization-manager user list --organization-id +yc organization-manager oslogin user-ssh-key create \ +--organization-id \ +--name "terraform-key" \ +--subject-id \ +--data "" +--expires-at 2026-01-02T15:04:05Z +``` + +### Prepare workspace for terraform for YandexCloud +```sh +mkdir -p terraform/yandex +cd terraform/yandex +touch main.tf +touch variables.tf +``` + +### Select image for VM +```sh +yc compute image list --folder-id standard-images +``` + +### Fill `main.tf` and `variables.tf` (if needed) with your data +```sh +vim main.tf +vim variables.tf +``` + +### terraform init +``` +Initializing the backend... +Initializing provider plugins... +- Reusing previous version of yandex-cloud/yandex from the dependency lock file +- Using previously-installed yandex-cloud/yandex v0.136.0 +Terraform has made some changes to the provider dependency selections recorded +in the .terraform.lock.hcl file. Review those changes and commit them to your +version control system if they represent changes you intended to make. + +Terraform has been successfully initialized! + +You may now begin working with Terraform. Try running "terraform plan" to see +any changes that are required for your infrastructure. All Terraform commands +should now work. + +If you ever set or change modules or backend configuration for Terraform, +rerun this command to reinitialize your working directory. If you forget, other +commands will detect it and remind you to do so if necessary. +``` + +### terraform validate +``` +Success! The configuration is valid. +``` + +### terraform fmt +``` +main.tf +variables.tf +``` + +### terraform plan +``` +Terraform used the selected providers to generate the following execution plan. Resource actions are +indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # yandex_compute_instance.vm-1 will be created + + resource "yandex_compute_instance" "vm-1" { + + created_at = (known after apply) + + folder_id = (known after apply) + + fqdn = (known after apply) + + gpu_cluster_id = (known after apply) + + hardware_generation = (known after apply) + + hostname = (known after apply) + + id = (known after apply) + + maintenance_grace_period = (known after apply) + + maintenance_policy = (known after apply) + + metadata = { + + "ssh-keys" = <<-EOT + ubuntu:ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJt96sjTJIW3FqlQIHgDaMcyPRMV1IbbjUGP/mQ4p45G ivans@ivan-systemproductname + EOT + } + + name = "terraform-vm-1" + + network_acceleration_type = "standard" + + platform_id = "standard-v1" + + service_account_id = (known after apply) + + status = (known after apply) + + zone = (known after apply) + + + boot_disk { + + auto_delete = true + + device_name = (known after apply) + + disk_id = (known after apply) + + mode = (known after apply) + + + initialize_params { + + block_size = (known after apply) + + description = (known after apply) + + image_id = "fd80bm0rh4rkepi5ksdi" + + name = (known after apply) + + size = 20 + + snapshot_id = (known after apply) + + type = "network-hdd" + } + } + + + metadata_options (known after apply) + + + network_interface { + + index = (known after apply) + + ip_address = (known after apply) + + ipv4 = true + + ipv6 = (known after apply) + + ipv6_address = (known after apply) + + mac_address = (known after apply) + + nat = true + + nat_ip_address = (known after apply) + + nat_ip_version = (known after apply) + + security_group_ids = (known after apply) + + subnet_id = (known after apply) + } + + + placement_policy (known after apply) + + + resources { + + core_fraction = 100 + + cores = 2 + + memory = 2 + } + + + scheduling_policy (known after apply) + } + + # yandex_vpc_network.network-1 will be created + + resource "yandex_vpc_network" "network-1" { + + created_at = (known after apply) + + default_security_group_id = (known after apply) + + folder_id = (known after apply) + + id = (known after apply) + + labels = (known after apply) + + name = "default-1" + + subnet_ids = (known after apply) + } + + # yandex_vpc_subnet.subnet-1 will be created + + resource "yandex_vpc_subnet" "subnet-1" { + + created_at = (known after apply) + + folder_id = (known after apply) + + id = (known after apply) + + labels = (known after apply) + + name = (known after apply) + + network_id = (known after apply) + + v4_cidr_blocks = [ + + "192.168.20.0/24", + ] + + v6_cidr_blocks = (known after apply) + + zone = "ru-central1-a" + } + +Plan: 3 to add, 0 to change, 0 to destroy. +``` + +### terraform apply +``` +Terraform used the selected providers to generate the following execution plan. Resource actions are +indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # yandex_compute_instance.vm-1 will be created + + resource "yandex_compute_instance" "vm-1" { + + created_at = (known after apply) + + folder_id = (known after apply) + + fqdn = (known after apply) + + gpu_cluster_id = (known after apply) + + hardware_generation = (known after apply) + + hostname = (known after apply) + + id = (known after apply) + + maintenance_grace_period = (known after apply) + + maintenance_policy = (known after apply) + + metadata = { + + "ssh-keys" = <<-EOT + ubuntu:ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJt96sjTJIW3FqlQIHgDaMcyPRMV1IbbjUGP/mQ4p45G ivans@ivan-systemproductname + EOT + } + + name = "terraform-vm-1" + + network_acceleration_type = "standard" + + platform_id = "standard-v1" + + service_account_id = (known after apply) + + status = (known after apply) + + zone = (known after apply) + + + boot_disk { + + auto_delete = true + + device_name = (known after apply) + + disk_id = (known after apply) + + mode = (known after apply) + + + initialize_params { + + block_size = (known after apply) + + description = (known after apply) + + image_id = "fd80bm0rh4rkepi5ksdi" + + name = (known after apply) + + size = 20 + + snapshot_id = (known after apply) + + type = "network-hdd" + } + } + + + metadata_options (known after apply) + + + network_interface { + + index = (known after apply) + + ip_address = (known after apply) + + ipv4 = true + + ipv6 = (known after apply) + + ipv6_address = (known after apply) + + mac_address = (known after apply) + + nat = true + + nat_ip_address = (known after apply) + + nat_ip_version = (known after apply) + + security_group_ids = (known after apply) + + subnet_id = (known after apply) + } + + + placement_policy (known after apply) + + + resources { + + core_fraction = 100 + + cores = 2 + + memory = 2 + } + + + scheduling_policy (known after apply) + } + + # yandex_vpc_network.network-1 will be created + + resource "yandex_vpc_network" "network-1" { + + created_at = (known after apply) + + default_security_group_id = (known after apply) + + folder_id = (known after apply) + + id = (known after apply) + + labels = (known after apply) + + name = "default-1" + + subnet_ids = (known after apply) + } + + # yandex_vpc_subnet.subnet-1 will be created + + resource "yandex_vpc_subnet" "subnet-1" { + + created_at = (known after apply) + + folder_id = (known after apply) + + id = (known after apply) + + labels = (known after apply) + + name = (known after apply) + + network_id = (known after apply) + + v4_cidr_blocks = [ + + "192.168.20.0/24", + ] + + v6_cidr_blocks = (known after apply) + + zone = "ru-central1-a" + } + +Plan: 3 to add, 0 to change, 0 to destroy. + +Do you want to perform these actions? + Terraform will perform the actions described above. + Only 'yes' will be accepted to approve. + + Enter a value: yes + +yandex_vpc_network.network-1: Creating... +yandex_vpc_network.network-1: Creation complete after 2s [id=enprlcfrmf00nd0ceirr] +yandex_vpc_subnet.subnet-1: Creating... +yandex_vpc_subnet.subnet-1: Creation complete after 0s [id=e9bd1sa39e34qmfqfh3h] +yandex_compute_instance.vm-1: Creating... +yandex_compute_instance.vm-1: Still creating... [10s elapsed] +yandex_compute_instance.vm-1: Still creating... [20s elapsed] +yandex_compute_instance.vm-1: Still creating... [30s elapsed] +yandex_compute_instance.vm-1: Still creating... [40s elapsed] +yandex_compute_instance.vm-1: Creation complete after 41s [id=fhmrjisl997h4frvggi1] + +Apply complete! Resources: 3 added, 0 changed, 0 destroyed. +``` + +### terraform state list +``` +yandex_compute_instance.vm-1 +yandex_vpc_network.network-1 +yandex_vpc_subnet.subnet-1 +``` + +### terraform state show +#### yandex_compute_instance.vm-1: +``` +resource "yandex_compute_instance" "vm-1" { + created_at = "2025-02-06T00:40:32Z" + description = null + folder_id = "b1gc5ps7gbb88cdmp0u2" + fqdn = "fhmrjisl997h4frvggi1.auto.internal" + gpu_cluster_id = null + hardware_generation = [ + { + generation2_features = [] + legacy_features = [ + { + pci_topology = "PCI_TOPOLOGY_V1" + }, + ] + }, + ] + hostname = null + id = "fhmrjisl997h4frvggi1" + maintenance_grace_period = null + metadata = { + "ssh-keys" = <<-EOT + ubuntu:ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJt96sjTJIW3FqlQIHgDaMcyPRMV1IbbjUGP/mQ4p45G ivans@ivan-systemproductname + EOT + } + name = "terraform-vm-1" + network_acceleration_type = "standard" + platform_id = "standard-v1" + service_account_id = null + status = "running" + zone = "ru-central1-a" + + boot_disk { + auto_delete = true + device_name = "fhmhpl9iut0p69fi6i43" + disk_id = "fhmhpl9iut0p69fi6i43" + mode = "READ_WRITE" + + initialize_params { + block_size = 4096 + description = null + image_id = "fd80bm0rh4rkepi5ksdi" + kms_key_id = null + name = null + size = 20 + snapshot_id = null + type = "network-hdd" + } + } + + metadata_options { + aws_v1_http_endpoint = 1 + aws_v1_http_token = 2 + gce_http_endpoint = 1 + gce_http_token = 1 + } + + network_interface { + index = 0 + ip_address = "192.168.20.14" + ipv4 = true + ipv6 = false + ipv6_address = null + mac_address = "d0:0d:1b:9c:b9:54" + nat = true + nat_ip_address = "89.169.154.202" + nat_ip_version = "IPV4" + security_group_ids = [] + subnet_id = "e9bd1sa39e34qmfqfh3h" + } + + placement_policy { + host_affinity_rules = [] + placement_group_id = null + placement_group_partition = 0 + } + + resources { + core_fraction = 100 + cores = 2 + gpus = 0 + memory = 2 + } + + scheduling_policy { + preemptible = false + } +} +``` + +#### yandex_vpc_network.network-1: +``` +resource "yandex_vpc_network" "network-1" { + created_at = "2025-02-06T00:40:30Z" + default_security_group_id = "enpe0e80or2msilrnhiv" + description = null + folder_id = "b1gc5ps7gbb88cdmp0u2" + id = "enprlcfrmf00nd0ceirr" + labels = {} + name = "default-1" + subnet_ids = [] +} +``` + +#### yandex_vpc_subnet.subnet-1: +``` +resource "yandex_vpc_subnet" "subnet-1" { + created_at = "2025-02-06T00:40:31Z" + description = null + folder_id = "b1gc5ps7gbb88cdmp0u2" + id = "e9bd1sa39e34qmfqfh3h" + labels = {} + name = null + network_id = "enprlcfrmf00nd0ceirr" + route_table_id = null + v4_cidr_blocks = [ + "192.168.20.0/24", + ] + v6_cidr_blocks = [] + zone = "ru-central1-a" +} +``` + +## Github + +### Prepare workspace +```sh +mkdir -p terraform/github +cd terraform/github +touch main.tf # fill it +touch variables.tf #fill it, if needed +``` + +### Create Access Token on GitHub +1. Settings -> Developer settings -> personal access tokens -> Tokens (classic) +2. Create new Token (classic) +3. Save it + +### terraform import "github_repository.repo" "S25-core-course-labs" +``` +github_repository.repo: Importing from ID "S25-core-course-labs"... +github_repository.repo: Import prepared! + Prepared github_repository for import +github_repository.repo: Refreshing state... [id=S25-core-course-labs] + +Import successful! + +The resources that were imported are shown above. These resources are now in +your Terraform state and will henceforth be managed by Terraform. +``` + +### terraform apply +``` +github_repository.repo: Refreshing state... [id=S25-core-course-labs] + +Terraform used the selected providers to generate the following execution plan. Resource actions are +indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # github_branch_default.default will be created + + resource "github_branch_default" "default" { + + branch = "lab4-solution" + + id = (known after apply) + + repository = "S25-core-course-labs" + } + + # github_branch_protection.default will be created + + resource "github_branch_protection" "default" { + + allows_deletions = false + + allows_force_pushes = false + + blocks_creations = false + + enforce_admins = true + + id = (known after apply) + + pattern = "lab4-solution" + + repository_id = "S25-core-course-labs" + + require_conversation_resolution = true + + require_signed_commits = false + + required_linear_history = false + + + required_pull_request_reviews { + + required_approving_review_count = 1 + } + } + +Plan: 2 to add, 0 to change, 0 to destroy. + +Do you want to perform these actions? + Terraform will perform the actions described above. + Only 'yes' will be accepted to approve. + + Enter a value: yes + +github_branch_default.default: Creating... +github_branch_default.default: Creation complete after 2s [id=S25-core-course-labs] +github_branch_protection.default: Creating... +github_branch_protection.default: Creation complete after 4s [id=BPR_kwDONxdSs84DigsZ] + +Apply complete! Resources: 2 added, 0 changed, 0 destroyed. +``` diff --git a/terraform/docker/.gitignore b/terraform/docker/.gitignore new file mode 100644 index 0000000000..8b63d63bcf --- /dev/null +++ b/terraform/docker/.gitignore @@ -0,0 +1,32 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +env/ +venv/ +*.env +instance/ +*.log +*.pot +*.pyc +.vscode/ +.idea/ +.DS_Store +Thumbs.db +.coverage +nosetests.xml +*.cover +*.hypothesis/ +*.tox/ +*.nox/ +*.coverage +venv/ +.env/ +**/.terraform/ +**/.terraform.lock.hcl +**/terraform.tfstate +**/terraform.tfstate.backup +yandex/key.json +**/terraform.tfstate.*.backup + diff --git a/terraform/docker/main.tf b/terraform/docker/main.tf new file mode 100644 index 0000000000..02f706f799 --- /dev/null +++ b/terraform/docker/main.tf @@ -0,0 +1,23 @@ +terraform { + required_providers { + docker = { + source = "kreuzwerker/docker" + version = "~> 3.0.1" + } + } +} + +provider "docker" {} + +resource "docker_image" "python_app" { + name = "python:3.11" +} + +resource "docker_container" "python_container" { + name = var.python_container_name + image = var.python_image_name + ports { + internal = 5000 + external = 5000 + } +} diff --git a/terraform/docker/outputs.tf b/terraform/docker/outputs.tf new file mode 100644 index 0000000000..a48cdbd53a --- /dev/null +++ b/terraform/docker/outputs.tf @@ -0,0 +1,14 @@ +output "python_container_id" { + description = "The ID of the Python container" + value = docker_container.python_container.id +} + +output "python_container_name" { + description = "The name of the Python container" + value = docker_container.python_container.name +} + +output "python_container_image" { + description = "The image used for the Python container" + value = docker_container.python_container.image +} diff --git a/terraform/docker/variables.tf b/terraform/docker/variables.tf new file mode 100644 index 0000000000..2343610cae --- /dev/null +++ b/terraform/docker/variables.tf @@ -0,0 +1,9 @@ +variable "python_container_name" { + type = string + default = "app_python" +} + +variable "python_image_name" { + type = string + default = "petrel312/flask_app:latest" +} diff --git a/terraform/github/.gitignore b/terraform/github/.gitignore new file mode 100644 index 0000000000..8b63d63bcf --- /dev/null +++ b/terraform/github/.gitignore @@ -0,0 +1,32 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +env/ +venv/ +*.env +instance/ +*.log +*.pot +*.pyc +.vscode/ +.idea/ +.DS_Store +Thumbs.db +.coverage +nosetests.xml +*.cover +*.hypothesis/ +*.tox/ +*.nox/ +*.coverage +venv/ +.env/ +**/.terraform/ +**/.terraform.lock.hcl +**/terraform.tfstate +**/terraform.tfstate.backup +yandex/key.json +**/terraform.tfstate.*.backup + diff --git a/terraform/github/main.tf b/terraform/github/main.tf new file mode 100644 index 0000000000..d96a3a6746 --- /dev/null +++ b/terraform/github/main.tf @@ -0,0 +1,39 @@ +terraform { + required_providers { + github = { + source = "integrations/github" + version = "~> 4.0" + } + } +} + +provider "github" { + token = var.token +} + +resource "github_repository" "repo" { + name = var.repo_name + description = var.repo_description + visibility = var.repo_visibility + auto_init = true + has_issues = true + has_wiki = true + license_template = "mit" + gitignore_template = "VisualStudio" +} + +resource "github_branch_default" "default" { + repository = github_repository.repo.name + branch = var.default_branch +} + +resource "github_branch_protection" "default" { + repository_id = github_repository.repo.id + pattern = github_branch_default.default.branch + require_conversation_resolution = true + enforce_admins = true + + required_pull_request_reviews { + required_approving_review_count = 1 + } +} diff --git a/terraform/github/variables.tf b/terraform/github/variables.tf new file mode 100644 index 0000000000..d6fd88aed1 --- /dev/null +++ b/terraform/github/variables.tf @@ -0,0 +1,28 @@ +variable "token" { + description = "Access token" + type = string + sensitive = true +} + +variable "repo_name" { + description = "GitHub repository name" + type = string + default = "S25-core-course-labs" +} + +variable "repo_description" { + description = "Repository description" + type = string + default = "Smth with terraform" +} + +variable "repo_visibility" { + description = "Repository visibility (public or private)" + type = string + default = "public" +} + +variable "default_branch" { + description = "The default branch for the repository" + default = "lab4-solution" +} diff --git a/terraform/yandex/.gitignore b/terraform/yandex/.gitignore new file mode 100644 index 0000000000..8b63d63bcf --- /dev/null +++ b/terraform/yandex/.gitignore @@ -0,0 +1,32 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +env/ +venv/ +*.env +instance/ +*.log +*.pot +*.pyc +.vscode/ +.idea/ +.DS_Store +Thumbs.db +.coverage +nosetests.xml +*.cover +*.hypothesis/ +*.tox/ +*.nox/ +*.coverage +venv/ +.env/ +**/.terraform/ +**/.terraform.lock.hcl +**/terraform.tfstate +**/terraform.tfstate.backup +yandex/key.json +**/terraform.tfstate.*.backup + diff --git a/terraform/yandex/main.tf b/terraform/yandex/main.tf new file mode 100644 index 0000000000..e20335419f --- /dev/null +++ b/terraform/yandex/main.tf @@ -0,0 +1,52 @@ +terraform { + required_providers { + yandex = { + source = "yandex-cloud/yandex" + } + } + required_version = ">=0.13" +} + +provider "yandex" { + token = var.yc_token + cloud_id = var.cloud_id + folder_id = var.folder_id + zone = var.zone +} + +resource "yandex_vpc_network" "network-1" { + name = "default-1" +} + + +resource "yandex_vpc_subnet" "subnet-1" { + zone = var.zone + network_id = yandex_vpc_network.network-1.id + v4_cidr_blocks = ["192.168.20.0/24"] +} + +resource "yandex_compute_instance" "vm-1" { + name = "terraform-vm-1" + + resources { + cores = var.cores + memory = var.memory + } + + boot_disk { + initialize_params { + image_id = var.image_id + size = var.boot_size + } + } + + network_interface { + subnet_id = yandex_vpc_subnet.subnet-1.id + nat = true + } + + metadata = { + ssh-keys = "ubuntu:${file(var.ssh_key)}" + } +} + diff --git a/terraform/yandex/variables.tf b/terraform/yandex/variables.tf new file mode 100644 index 0000000000..d52d54ff52 --- /dev/null +++ b/terraform/yandex/variables.tf @@ -0,0 +1,44 @@ +variable "yc_token" { + type = string + default = "t1.9euelZqelMyenc6emozJzp7Jl5yemu3rnpWampDOnMiTypDIkoyRkYmJlZPl8_dTDnBC-e8oNzk__N3z9xM9bUL57yg3OT_8zef1656VmonIionGx8-Py8mKjJiRm4zK7_zF656VmonIionGx8-Py8mKjJiRm4zK.1FgxC-xsjZBJgGQPnIPZnYH7byPQ3T1pRw-59CZeWO8T_k4D7kKcoi-wkYKQAZh3QQtTmM_GKmOVwySrDRvlDQ" +} + +variable "folder_id" { + type = string + default = "b1gc5ps7gbb88cdmp0u2" +} + +variable "cloud_id" { + type = string + default = "b1g719qcjt3un5vps33i" +} + +variable "zone" { + type = string + default = "ru-central1-a" +} + +variable "cores" { + type = number + default = 2 +} + +variable "memory" { + type = number + default = 2 +} + +variable "image_id" { + type = string + default = "fd80bm0rh4rkepi5ksdi" +} + +variable "boot_size" { + type = number + default = 20 +} + +variable "ssh_key" { + type = string + default = "~/.ssh/id_ed25519.pub" +} \ No newline at end of file