diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000..fb67e23ef0 Binary files /dev/null and b/.DS_Store differ diff --git a/.github/workflows/ansible-deploy-bonus.yml b/.github/workflows/ansible-deploy-bonus.yml new file mode 100644 index 0000000000..f4b36aa038 --- /dev/null +++ b/.github/workflows/ansible-deploy-bonus.yml @@ -0,0 +1,99 @@ +name: Ansible Deployment Go App + +on: + push: + branches: [ main, master, lab06 ] + paths: + - 'ansible/vars/app_bonus.yml' + - 'ansible/playbooks/deploy-bonus.yml' + - 'ansible/roles/web_app/tasks/**' + - 'ansible/roles/web_app/templates/docker-compose.yml.j2' + - '.github/workflows/ansible-deploy-bonus.yml' + pull_request: + branches: [ main, master, lab06 ] + paths: + - 'ansible/vars/app_bonus.yml' + - 'ansible/playbooks/deploy-bonus.yml' + +env: + ANSIBLE_CONFIG: ansible/ansible.cfg + ANSIBLE_ROLES_PATH: ${{ github.workspace }}/ansible/roles + ANSIBLE_HOST_KEY_CHECKING: False + ANSIBLE_SSH_ARGS: '-o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' + VM_HOST: ${{ secrets.VM_HOST }} + VM_USER: ${{ secrets.VM_USER }} + APP_NAME: 'devops-go' + +jobs: + lint: + name: Lint Go App Ansible + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: 'pip' + + - name: Install dependencies + run: | + pip install ansible ansible-lint yamllint + ansible-galaxy collection install community.docker + + - name: Syntax check Go playbook + run: | + cd ansible + export ANSIBLE_ROLES_PATH="$GITHUB_WORKSPACE/ansible/roles" + ansible-playbook playbooks/deploy-bonus.yml --syntax-check + + - name: Run ansible-lint + run: | + cd ansible + ansible-lint playbooks/deploy-bonus.yml + + deploy: + name: Deploy Go App + needs: lint + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install Ansible + run: | + pip install ansible + ansible-galaxy collection install community.docker + + - name: Setup Ansible Vault + run: | + echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > /tmp/vault_pass + chmod 600 /tmp/vault_pass + + - name: Setup roles + run: | + mkdir -p ansible/playbooks/roles + cp -r ansible/roles/* ansible/playbooks/roles/ 2>/dev/null || true + cp -r roles/* ansible/playbooks/roles/ 2>/dev/null || true + + - name: Deploy Go + run: | + cd ansible + export ANSIBLE_ROLES_PATH=$(pwd)/roles + export ANSIBLE_HOST_KEY_CHECKING=False + ansible-playbook playbooks/deploy-bonus.yml --syntax-check\ + --vault-password-file /tmp/vault_pass + + - name: Cleanup + run: rm -f /tmp/vault_pass + if: always() diff --git a/.github/workflows/ansible-deploy-matrix.yml b/.github/workflows/ansible-deploy-matrix.yml new file mode 100644 index 0000000000..ab6455d6e5 --- /dev/null +++ b/.github/workflows/ansible-deploy-matrix.yml @@ -0,0 +1,74 @@ +name: Ansible Deployment Matrix + +on: + push: + branches: [ main, master, lab06 ] + paths: + - 'ansible/roles/web_app/**' + - '.github/workflows/ansible-deploy-matrix.yml' + +env: + ANSIBLE_CONFIG: ansible/ansible.cfg + ANSIBLE_ROLES_PATH: ${{ github.workspace }}/ansible/roles + +jobs: + matrix-deploy: + name: Deploy ${{ matrix.app.name }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + app: + - name: Python + var_file: app_python.yml + playbook: deploy-python.yml + port: 8000 + image: info-service-python + container_port: 8000 + + - name: Go + var_file: app_bonus.yml + playbook: deploy_bonus.yml + port: 8001 + image: info-service-go + container_port: 8080 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install Ansible + run: | + pip install ansible + ansible-galaxy collection install community.docker + + - name: Setup Ansible Vault + run: | + echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > /tmp/vault_pass + chmod 600 /tmp/vault_pass + + - name: Setup roles + run: | + mkdir -p ansible/playbooks/roles + cp -r ansible/roles/* ansible/playbooks/roles/ 2>/dev/null || true + cp -r roles/* ansible/playbooks/roles/ 2>/dev/null || true + + - name: Deploy ${{ matrix.app.name }} + run: | + cd ansible + export ANSIBLE_ROLES_PATH="$GITHUB_WORKSPACE/ansible/roles" + + ansible-playbook playbooks/deploy-python.yml --syntax-check\ + --vault-password-file /tmp/vault_pass + + ansible-playbook playbooks/deploy-bonus.yml --syntax-check\ + --vault-password-file /tmp/vault_pass + + - name: Cleanup + run: rm -f /tmp/vault_pass + if: always() diff --git a/.github/workflows/ansible-deploy-python.yml b/.github/workflows/ansible-deploy-python.yml new file mode 100644 index 0000000000..45caa92f48 --- /dev/null +++ b/.github/workflows/ansible-deploy-python.yml @@ -0,0 +1,100 @@ +name: Ansible Deployment Python App + +on: + push: + branches: [ main, master, lab06 ] + paths: + - 'ansible/vars/app_python.yml' + - 'ansible/playbooks/deploy-python.yml' + - 'ansible/roles/web_app/tasks/**' + - 'ansible/roles/web_app/templates/**' + - 'ansible/roles/web_app/defaults/**' + - '.github/workflows/ansible-deploy-python.yml' + pull_request: + branches: [ main, master, lab06 ] + paths: + - 'ansible/vars/app_python.yml' + - 'ansible/playbooks/deploy_python.yml' + +env: + ANSIBLE_CONFIG: ansible/ansible.cfg + ANSIBLE_ROLES_PATH: ${{ github.workspace }}/ansible/roles + ANSIBLE_HOST_KEY_CHECKING: False + ANSIBLE_SSH_ARGS: '-o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' + VM_HOST: ${{ secrets.VM_HOST }} + VM_USER: ${{ secrets.VM_USER }} + APP_NAME: 'devops-python' + +jobs: + lint: + name: Lint Python App Ansible + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: 'pip' + + - name: Install dependencies + run: | + pip install ansible ansible-lint + ansible-galaxy collection install community.docker + + - name: Syntax check Python playbook + run: | + cd ansible + export ANSIBLE_ROLES_PATH="$GITHUB_WORKSPACE/ansible/roles" + ansible-playbook playbooks/deploy-python.yml --syntax-check + + - name: Run ansible-lint + run: | + cd ansible + ansible-lint playbooks/deploy-python.yml + + deploy: + name: Deploy Python App + needs: lint + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install Ansible + run: | + pip install ansible + ansible-galaxy collection install community.docker + + - name: Setup Ansible Vault + run: | + echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > /tmp/vault_pass + chmod 600 /tmp/vault_pass + + - name: Setup roles + run: | + mkdir -p ansible/playbooks/roles + cp -r ansible/roles/* ansible/playbooks/roles/ 2>/dev/null || true + cp -r roles/* ansible/playbooks/roles/ 2>/dev/null || true + + - name: Deploy Python + run: | + cd ansible + export ANSIBLE_ROLES_PATH="$GITHUB_WORKSPACE/ansible/roles" + export ANSIBLE_HOST_KEY_CHECKING=False + ansible-playbook playbooks/deploy-python.yml --syntax-check\ + --vault-password-file /tmp/vault_pass + + - name: Cleanup + run: rm -f /tmp/vault_pass + if: always() diff --git a/.github/workflows/ansible-deploy.yml b/.github/workflows/ansible-deploy.yml new file mode 100644 index 0000000000..eb3dfd1b64 --- /dev/null +++ b/.github/workflows/ansible-deploy.yml @@ -0,0 +1,78 @@ +name: Ansible Deployment + +on: + push: + branches: [ main, master, lab06 ] + paths: + - 'ansible/**' + - '.github/workflows/ansible-deploy.yml' + pull_request: + branches: [ main, master, lab06 ] + paths: + - 'ansible/**' + +env: + ANSIBLE_CONFIG: ansible/ansible.cfg + ANSIBLE_ROLES_PATH: ansible/roles + ANSIBLE_HOST_KEY_CHECKING: False + ANSIBLE_SSH_ARGS: '-o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' + VM_HOST: ${{ secrets.VM_HOST }} + VM_USER: ${{ secrets.VM_USER }} + +jobs: + lint: + name: Ansible Lint + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: 'pip' + + - name: Install dependencies + run: | + pip install ansible ansible-lint + + - name: Run ansible-lint + run: | + cd ansible + ansible-lint playbooks/*.yml + + + + deploy: + name: Deploy Application + needs: lint + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup SSH + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Setup Ansible Vault + run: | + echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > /tmp/vault_pass + chmod 600 /tmp/vault_pass + + - name: Run Ansible playbook + run: | + export ANSIBLE_ROLES_PATH="$GITHUB_WORKSPACE/ansible/roles" + export ANSIBLE_HOST_KEY_CHECKING=False + export ANSIBLE_SSH_ARGS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" + ansible-playbook ansible/playbooks/deploy.yml --syntax-check\ + --vault-password-file /tmp/vault_pass \ + + - name: Cleanup + run: rm -f /tmp/vault_pass + if: always() diff --git a/.github/workflows/go-ci.yml b/.github/workflows/go-ci.yml new file mode 100644 index 0000000000..0e758c5814 --- /dev/null +++ b/.github/workflows/go-ci.yml @@ -0,0 +1,110 @@ +name: Go CI + +on: + push: + branches: [ "master", "lab01", "lab02", "lab03" ] + paths: + - "app_go/**" + pull_request: + paths: + - "app_go/**" + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.22" + + - name: Cache Go modules + uses: actions/cache@v4 + with: + path: | + ~/go/pkg/mod + ~/.cache/go-build + key: ${{ runner.os }}-go-${{ hashFiles('app_go/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Run Go tests with coverage + run: | + cd app_go + go test ./... -coverprofile=coverage.out + go tool cover -func=coverage.out | tee coverage.txt + TOTAL=$(go tool cover -func=coverage.out | grep total: | awk '{print substr($3, 1, length($3)-1)}') + echo "Total coverage: $TOTAL%" + if (( $(echo "$TOTAL < 70" | bc -l) )); then + echo "Coverage below 70%" + exit 1 + fi + + - name: Convert Go coverage to Cobertura + run: | + go install github.com/boumenot/gocover-cobertura@latest + cd app_go + gocover-cobertura < coverage.out > coverage.xml + + - name: Upload Go coverage to Coveralls + uses: coverallsapp/github-action@v2 + with: + format: cobertura + file: app_go/coverage.xml + flag-name: go + parallel: true + + - name: Finalize Coveralls + uses: coverallsapp/github-action@v2 + with: + parallel-finished: true + + + security: + runs-on: ubuntu-latest + needs: test + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.22" + + - name: Run Snyk security scan + uses: snyk/actions/setup@master + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + command: test + args: --file=app_go/go.mod + + + + docker: + needs: test + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: ./app_go + push: true + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/info-service-go:latest diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 0000000000..eed271920e --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -0,0 +1,116 @@ +name: Python CI + +on: + push: + branches: [ "master", "lab01", "lab02", "lab03" ] + paths: + - "app_python/**" + pull_request: + paths: + - "app_python/**" + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "^3.13" + + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('app_python/requirements*.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install dependencies + run: | + pip install -r app_python/requirements.txt + pip install -r app_python/requirements-dev.txt + + - name: Run tests with coverage + run: | + cd app_python + coverage run -m pytest + coverage report --fail-under=70 + coverage xml + + - name: Upload coverage to Coveralls + uses: coverallsapp/github-action@v2 + with: + format: cobertura + file: app_python/coverage.xml + flag-name: python + parallel: true + + + security: + runs-on: ubuntu-latest + needs: test + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "^3.13" + + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-security-${{ hashFiles('app_python/requirements*.txt') }} + restore-keys: | + ${{ runner.os }}-pip-security- + + - name: Install dependencies + run: | + pip install -r app_python/requirements.txt + pip install -r app_python/requirements-dev.txt + + - name: Run Snyk scan for main dependencies + uses: snyk/actions/setup@master + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + command: test + args: --file=app_python/requirements.txt + + - name: Run Snyk scan for dev dependencies + uses: snyk/actions/setup@master + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + command: test + args: --file=app_python/requirements-dev.txt + + docker: + needs: test + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: ./app_python + push: true + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/info-service-python:latest diff --git a/.github/workflows/terraform-ci.yml b/.github/workflows/terraform-ci.yml new file mode 100644 index 0000000000..6d1f6619ca --- /dev/null +++ b/.github/workflows/terraform-ci.yml @@ -0,0 +1,43 @@ +name: Terraform CI + +on: + push: + branches: [ "master", "lab04" ] + paths: + - "terraform/**" + pull_request: + paths: + - "terraform/**" + +jobs: + terraform: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.14.4 + + - name: Terraform fmt check + working-directory: terraform + run: terraform fmt -check -recursive -diff + + - name: Terraform init + working-directory: terraform + run: terraform init -backend=false + + - name: Terraform validate + working-directory: terraform + run: terraform validate + + - name: Install tflint + run: | + curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash + + - name: Run tflint + working-directory: terraform + run: tflint --init && tflint diff --git a/.gitignore b/.gitignore index 30d74d2584..9daeafb986 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -test \ No newline at end of file +test diff --git a/ansible/.ansible-lint b/ansible/.ansible-lint new file mode 100644 index 0000000000..861e48189e --- /dev/null +++ b/ansible/.ansible-lint @@ -0,0 +1,21 @@ +--- +skip_list: + - key-order + - var-naming + - yaml[truthy] + - yaml[octal-values] + - yaml[trailing-spaces] + - risky-file-permissions + - ignore-errors + - fqcn[action] + - playbooks/site.yml + - internal-error + - var-naming[no-role-prefix] + - fqcn[action-core] + - key-order[task] + +exclude_paths: + - roles/web_app/tasks/main.yml + +warn_list: + - experimental diff --git a/ansible/.gitignore b/ansible/.gitignore new file mode 100644 index 0000000000..e1decaf298 --- /dev/null +++ b/ansible/.gitignore @@ -0,0 +1,4 @@ +*.retry +.vault_pass +ansible/inventory/*.pyc +__pycache__/ diff --git a/ansible/README.md b/ansible/README.md new file mode 100644 index 0000000000..bb56261eb3 --- /dev/null +++ b/ansible/README.md @@ -0,0 +1,10 @@ +[![Ansible Deployment](https://github.com/scruffyscarf/DevOps-Core-Course/actions/workflows/ansible-deploy.yml/badge.svg)](https://github.com/your-username/your-repo/actions/workflows/ansible-deploy.yml) + +[![Python Deployment](https://github.com/scruffyscarf/DevOps-Core-Course/actions/workflows/ansible-deploy-python.yml/badge.svg)](https://github.com/USER/REPO/actions/workflows/ansible-deploy-python.yml) + +[![Go Deployment](https://github.com/scruffyscarf/DevOps-Core-Course/actions/workflows/ansible-deploy-bonus.yml/badge.svg)](https://github.com/USER/REPO/actions/workflows/ansible-deploy-bonus.yml) + +# Ansible Configuration + +## Overview +Ansible configuration for Yandex Cloud instances diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000000..4f10386245 --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,16 @@ +[defaults] +inventory = ./inventory/hosts.ini +roles_path = ./roles +host_key_checking = False +retry_files_enabled = False +interpreter_python = auto_silent +stdout_callback = ansible.builtin.default +result_format = yaml +callback_whitelist = timer,profile_tasks +remote_user = {{ lookup('env', 'VM_USER', 'ubuntu') }} + +[privilege_escalation] +become = True +become_method = sudo +become_user = root +become_ask_pass = False diff --git a/ansible/docs/LAB05.md b/ansible/docs/LAB05.md new file mode 100644 index 0000000000..770fb55505 --- /dev/null +++ b/ansible/docs/LAB05.md @@ -0,0 +1,372 @@ +# Lab 5 — Ansible Fundamentals + + + +## Architecture Overview + +### Ansible version + +`ansible [core 2.20.2]` + +### VM OS and version + +`Ubuntu 22.04` + +### Role structure diagram + +```bash +ansible/ +├── inventory/ +│ └── hosts.ini # Static inventory +├── roles/ +│ ├── common/ # Common system tasks +│ │ ├── tasks/ +│ │ │ └── main.yml +│ │ └── defaults/ +│ │ └── main.yml +│ ├── docker/ # Docker installation +│ │ ├── tasks/ +│ │ │ └── main.yml +│ │ ├── handlers/ +│ │ │ └── main.yml +│ │ └── defaults/ +│ │ └── main.yml +│ └── app_deploy/ # Application deployment +│ ├── tasks/ +│ │ └── main.yml +│ ├── handlers/ +│ │ └── main.yml +│ └── defaults/ +│ └── main.yml +├── playbooks/ +│ ├── site.yml # Main playbook +│ ├── provision.yml # System provisioning +│ └── deploy.yml # App deployment +├── group_vars/ +│ └── all.yml # Encrypted variables (Vault) +├── ansible.cfg # Ansible configuration +└── docs/ + └── LAB05.md # Your documentation +``` + +### Roles instead of monolithic playbooks + +- Reusability +- Clean Architecture +- Parallel Development +- Selective Execution +- Parameterization +- Debugging & Maintenance +- Scalability + + + +## Roles Documentation + +### Common + +- **Purpose**: Installs common system packages and configures the base system +- **Variables**: `common_packages`, `timezone` +- **Handlers**: There are no handlers in this role +- **Dependencies**: It does not depend on other roles, it is a basic role + +### Docker + +- **Purpose**: Installs Docker and related components on target hosts +- **Variables**: `docker_users`, `docker_version`, `docker_repository`, `docker_repository_key_url` +- **Handlers**: Restart docker +- **Dependencies**: Depends on the role of the `Common` + +### App Deploy + +- **Purpose**: Deploys the containerized application on target hosts +- **Variables**: `dockerhub_username`, `dockerhub_password`, `app_name`, `app_port`, `app_host_port`, `docker_image`, `app_environment` +- **Handlers**: Restart app container +- **Dependencies**: Depends on the role of `Docker` + + + +## Idempotency Demonstration + +### FIRST `provision.yml` run + +```bash +ansible-playbook playbooks/provision.yml +``` + +```bash +PLAY [Provision web servers] ************************************************************************************************************************************* + +TASK [Gathering Facts] ******************************************************************************************************************************************* +ok: [info-service] + +TASK [common : Update apt cache] ********************************************************************************************************************************* +ok: [info-service] + +TASK [common : Install common packages] ************************************************************************************************************************** +changed: [info-service] + +TASK [common : Set timezone] ************************************************************************************************************************************* +changed: [info-service] + +TASK [common : Ensure pip is installed] ************************************************************************************************************************** +changed: [info-service] + +TASK [common : Create .ssh directory for root] ******************************************************************************************************************* +ok: [info-service] + +TASK [docker : Install prerequisites for Docker] ***************************************************************************************************************** +changed: [info-service] + +TASK [docker : Add Docker GPG key] ******************************************************************************************************************************* +changed: [info-service] + +TASK [docker : Add Docker repository] **************************************************************************************************************************** +changed: [info-service] + +TASK [docker : Update apt cache after adding Docker repo] ******************************************************************************************************** +changed: [info-service] + +TASK [docker : Install Docker packages] ************************************************************************************************************************** +changed: [info-service] + +TASK [docker : Ensure Docker service is running] ***************************************************************************************************************** +ok: [info-service] + +TASK [docker : Add users to docker group] ************************************************************************************************************************ +changed: [info-service] => (item=ubuntu) +ok: [info-service] => (item=ubuntu) + +TASK [docker : Install Docker SDK for Python] ******************************************************************************************************************** +changed: [info-service] + +RUNNING HANDLER [docker : Restart docker] ************************************************************************************************************************ +changed: [info-service] + +PLAY RECAP ******************************************************************************************************************************************************* +info-service : ok=15 changed=10 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +### SECOND provision.yml run + +```bash +ansible-playbook playbooks/provision.yml +``` + +```bash +PLAY [Provision web servers] ************************************************************************************************************************************* + +TASK [Gathering Facts] ******************************************************************************************************************************************* +ok: [info-service] + +TASK [common : Update apt cache] ********************************************************************************************************************************* +ok: [info-service] + +TASK [common : Install common packages] ************************************************************************************************************************** +ok: [info-service] + +TASK [common : Set timezone] ************************************************************************************************************************************* +ok: [info-service] + +TASK [common : Ensure pip is installed] ************************************************************************************************************************** +ok: [info-service] + +TASK [common : Create .ssh directory for root] ******************************************************************************************************************* +ok: [info-service] + +TASK [docker : Install prerequisites for Docker] ***************************************************************************************************************** +ok: [info-service] + +TASK [docker : Add Docker GPG key] ******************************************************************************************************************************* +ok: [info-service] + +TASK [docker : Add Docker repository] **************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Update apt cache after adding Docker repo] ******************************************************************************************************** +changed: [info-service] + +TASK [docker : Install Docker packages] ************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Ensure Docker service is running] ***************************************************************************************************************** +ok: [info-service] + +TASK [docker : Add users to docker group] ************************************************************************************************************************ +changed: [info-service] => (item=ubuntu) +ok: [info-service] => (item=ubuntu) + +TASK [docker : Install Docker SDK for Python] ******************************************************************************************************************** +ok: [info-service] + +RUNNING HANDLER [docker : Restart docker] ************************************************************************************************************************ +ok: [info-service] + +PLAY RECAP ******************************************************************************************************************************************************* +info-service : ok=15 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +### Changes + +- Install common packages +- Set timezone +- Ensure pip is installed +- Install prerequisites for Docker +- Add Docker GPG key +- Add Docker repository +- Update apt cache after adding Docker repo +- Install Docker packages +- Install Docker SDK for Python +- Restart docker + +### Changes repeatable + +- Update apt cache after adding Docker repo + +### Idempotency + +- Checking the status +- The `state` parameter +- A declarative approach +- Modular architecture + + + +## Ansible Vault Usage + +### Store credentials securely + +- Sensitive data are stored in encrypted YAML files +- Symmetric AES256 encryption is used + +### Vault password management strategy + +- Keep files in `/group_vars` +- The password file +- Environment variable + +### `ansible/group_vars/all.yml` + +```bash +$ANSIBLE_VAULT;1.1;AES256 +66653565383132356534656566383838386364666139613762633333396565623931343239363439 +... +37373362643837623333386131353464643161623435646134366564383135613233326631333037 +34333963373935636335 +``` + +### Ansible importance + +- **Security** — passwords are not stored in clear text in Git +- **Compliance with standards** — PCI DSS, HIPAA, GDPR require encryption of secrets +- **Collaboration** — safely share the code with the team +- **CI/CD integration** — automation without compromising secrets +- **Idempotence** — commit the entire code, including the configuration with secrets + + + +## Deployment Verification + +### `deploy.yml` run + +```bash +ansible-playbook playbooks/deploy.yml --ask-vault-pass +``` + +```bash +Vault password: + +PLAY [Deploy application] **************************************************************************************************************************************** + +TASK [Gathering Facts] ******************************************************************************************************************************************* +ok: [info-service] + +TASK [app_deploy : Login to Docker Hub] ************************************************************************************************************************** +ok: [info-service] + +TASK [app_deploy : Pull Docker image] **************************************************************************************************************************** +ok: [info-service] + +TASK [app_deploy : Check if container exists] ******************************************************************************************************************** +ok: [info-service] + +TASK [app_deploy : Stop existing container if running] *********************************************************************************************************** +changed: [info-service] + +TASK [app_deploy : Remove old container if exists] *************************************************************************************************************** +changed: [info-service] + +TASK [app_deploy : Create application directory] ***************************************************************************************************************** +ok: [info-service] + +TASK [app_deploy : Deploy application container] ***************************************************************************************************************** +changed: [info-service] + +TASK [app_deploy : Wait for application to start] **************************************************************************************************************** +ok: [info-service] + +TASK [app_deploy : Check application health endpoint] ************************************************************************************************************ +ok: [info-service] + +TASK [app_deploy : Display health check result] ****************************************************************************************************************** +ok: [info-service] => { + "msg": "Application is healthy! Response: {'status': 'healthy', 'timestamp': '2026-02-09T15:11:15.557890+00:00', 'uptime_seconds': 12}" +} + +TASK [Show running containers] *********************************************************************************************************************************** +changed: [info-service] + +TASK [Display container status] ********************************************************************************************************************************** +ok: [info-service] => { + "msg": [ + "NAMES IMAGE STATUS PORTS", + "info-service scruffyscarf/info-service:latest Up 17 seconds 0.0.0.0:5000->5000/tcp" + ] +} + +PLAY RECAP ******************************************************************************************************************************************************* +info-service : ok=13 changed=4 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +### `docker ps` + +```bash +"NAMES IMAGE STATUS PORTS", +"info-service scruffyscarf/info-service:latest Up 17 seconds 0.0.0.0:5000->5000/tcp" +``` + +### `http://ip:5000/health` + +```bash +"msg": "Application is healthy! Response: {'status': 'healthy', 'timestamp': '2026-02-09T15:11:15.557890+00:00', 'uptime_seconds': 12}" +``` + + + +## Key Decisions + +### Roles instead of plain playbooks + +Roles provide modular organization, separating configuration into reusable components. They promote code sharing and maintain clean, readable playbooks that orchestrate roles rather than containing all logic. + +### Roles improve reusability + +Roles encapsulate related tasks, variables, and handlers into self-contained units that can be easily imported across multiple playbooks and projects. This allows teams to share standardized configurations and reduces code duplication. + +### Task idempotency + +A task is idempotent when it checks the system's current state before making changes, ensuring it only modifies the system if the desired state isn't already achieved. This allows safe repeated execution without causing unintended side effects. + +### Handlers improve efficiency + +Handlers trigger actions only when notified by tasks that actually change system state, avoiding unnecessary restarts or reloads. They execute once at playbook end even if triggered multiple times, reducing service disruption. + +### Ansible Vault necessity + +Ansible Vault encrypts sensitive data like passwords and API keys, preventing credential exposure in version control. It's essential for security compliance and safe collaboration when managing infrastructure as code. + + + +## Dynamic Inventory with Cloud Plugins + +Yandex Cloud doesn't have any plugin for Ansible. diff --git a/ansible/docs/LAB06.md b/ansible/docs/LAB06.md new file mode 100644 index 0000000000..83a5c6bec1 --- /dev/null +++ b/ansible/docs/LAB06.md @@ -0,0 +1,1345 @@ +# Lab 6 — Advanced Ansible & CI/CD + + + +## Refactor with Blocks & Tags + +### Selective execution with `--tags` + +```bash +ansible-playbook playbooks/provision.yml --tags "docker" +``` + +```bash +PLAY [Provision web servers] *********************************************************************************************************************************** + +TASK [Gathering Facts] ***************************************************************************************************************************************** +ok: [info-service] + +TASK [common : Log package installation completion] ************************************************************************************************************ +ok: [info-service] => { + "msg": "Package installation block completed" +} + +TASK [common : Create completion timestamp] ******************************************************************************************************************** +changed: [info-service] + +TASK [common : User management completed] ********************************************************************************************************************** +ok: [info-service] => { + "msg": "User management block finished" +} + +TASK [docker : Docker role execution started] ****************************************************************************************************************** +ok: [info-service] => { + "msg": "Starting Docker role execution" +} + +TASK [docker : Install Docker prerequisites] ******************************************************************************************************************* +ok: [info-service] + +TASK [docker : Add Docker GPG key] ***************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Add Docker repository] ************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Update apt cache after repository setup] ******************************************************************************************************** +changed: [info-service] + +TASK [docker : Install Docker packages] ************************************************************************************************************************ +ok: [info-service] + +TASK [docker : Install Docker Python SDK] ********************************************************************************************************************** +ok: [info-service] + +TASK [docker : Ensure Docker service is running] *************************************************************************************************************** +ok: [info-service] + +TASK [docker : Add users to docker group] ********************************************************************************************************************** +ok: [info-service] => (item=ubuntu) +ok: [info-service] => (item=ubuntu) + +TASK [docker : Create docker-compose directory] **************************************************************************************************************** +ok: [info-service] + +TASK [docker : Verify Docker installation] ********************************************************************************************************************* +ok: [info-service] + +TASK [docker : Display Docker version] ************************************************************************************************************************* +ok: [info-service] => { + "msg": "Docker version: Docker version 29.2.1, build a5c7197" +} + +PLAY RECAP ***************************************************************************************************************************************************** +info-service : ok=16 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + + + + + +```bash +ansible-playbook playbooks/provision.yml --skip-tags "common" +``` + +```bash +PLAY [Provision web servers] *********************************************************************************************************************************** + +TASK [Gathering Facts] ***************************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Docker role execution started] ****************************************************************************************************************** +ok: [info-service] => { + "msg": "Starting Docker role execution" +} + +TASK [docker : Install Docker prerequisites] ******************************************************************************************************************* +ok: [info-service] + +TASK [docker : Add Docker GPG key] ***************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Add Docker repository] ************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Update apt cache after repository setup] ******************************************************************************************************** +changed: [info-service] + +TASK [docker : Install Docker packages] ************************************************************************************************************************ +ok: [info-service] + +TASK [docker : Install Docker Python SDK] ********************************************************************************************************************** +ok: [info-service] + +TASK [docker : Ensure Docker service is running] *************************************************************************************************************** +ok: [info-service] + +TASK [docker : Add users to docker group] ********************************************************************************************************************** +ok: [info-service] => (item=ubuntu) +ok: [info-service] => (item=ubuntu) + +TASK [docker : Create docker-compose directory] **************************************************************************************************************** +ok: [info-service] + +TASK [docker : Verify Docker installation] ********************************************************************************************************************* +ok: [info-service] + +TASK [docker : Display Docker version] ************************************************************************************************************************* +ok: [info-service] => { + "msg": "Docker version: Docker version 29.2.1, build a5c7197" +} + +PLAY RECAP ***************************************************************************************************************************************************** +info-service : ok=13 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + + + + + +```bash +ansible-playbook playbooks/provision.yml --tags "packages" +``` + +```bash +PLAY [Provision web servers] *********************************************************************************************************************************** + +TASK [Gathering Facts] ***************************************************************************************************************************************** +ok: [info-service] + +TASK [common : Update apt cache] ******************************************************************************************************************************* +ok: [info-service] + +TASK [common : Install common packages] ************************************************************************************************************************ +ok: [info-service] + +TASK [common : Upgrade system packages] ************************************************************************************************************************ +skipping: [info-service] + +TASK [common : Log package installation completion] ************************************************************************************************************ +ok: [info-service] => { + "msg": "Package installation block completed" +} + +TASK [common : Create completion timestamp] ******************************************************************************************************************** +changed: [info-service] + +TASK [common : User management completed] ********************************************************************************************************************** +ok: [info-service] => { + "msg": "User management block finished" +} + +TASK [docker : Install Docker packages] ************************************************************************************************************************ +ok: [info-service] + +PLAY RECAP ***************************************************************************************************************************************************** +info-service : ok=7 changed=1 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0 +``` + + + + + +```bash +ansible-playbook playbooks/provision.yml --tags "docker" --check +``` + +```bash +PLAY [Provision web servers] *********************************************************************************************************************************** + +TASK [Gathering Facts] ***************************************************************************************************************************************** +ok: [info-service] + +TASK [common : Log package installation completion] ************************************************************************************************************ +ok: [info-service] => { + "msg": "Package installation block completed" +} + +TASK [common : Create completion timestamp] ******************************************************************************************************************** +changed: [info-service] + +TASK [common : User management completed] ********************************************************************************************************************** +ok: [info-service] => { + "msg": "User management block finished" +} + +TASK [docker : Docker role execution started] ****************************************************************************************************************** +ok: [info-service] => { + "msg": "Starting Docker role execution" +} + +TASK [docker : Install Docker prerequisites] ******************************************************************************************************************* +ok: [info-service] + +TASK [docker : Add Docker GPG key] ***************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Add Docker repository] ************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Update apt cache after repository setup] ******************************************************************************************************** +changed: [info-service] + +TASK [docker : Install Docker packages] ************************************************************************************************************************ +ok: [info-service] + +TASK [docker : Install Docker Python SDK] ********************************************************************************************************************** +ok: [info-service] + +TASK [docker : Ensure Docker service is running] *************************************************************************************************************** +ok: [info-service] + +TASK [docker : Add users to docker group] ********************************************************************************************************************** +ok: [info-service] => (item=ubuntu) +ok: [info-service] => (item=ubuntu) + +TASK [docker : Create docker-compose directory] **************************************************************************************************************** +ok: [info-service] + +TASK [docker : Verify Docker installation] ********************************************************************************************************************* +skipping: [info-service] + +TASK [docker : Display Docker version] ************************************************************************************************************************* +ok: [info-service] => { + "msg": "Docker version: " +} + +PLAY RECAP ***************************************************************************************************************************************************** +info-service : ok=15 changed=2 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0 +``` + + + + + +```bash +ansible-playbook playbooks/provision.yml --tags "docker_install" +``` + +```bash +PLAY [Provision web servers] *********************************************************************************************************************************** + +TASK [Gathering Facts] ***************************************************************************************************************************************** +ok: [info-service] + +TASK [common : Log package installation completion] ************************************************************************************************************ +ok: [info-service] => { + "msg": "Package installation block completed" +} + +TASK [common : Create completion timestamp] ******************************************************************************************************************** +changed: [info-service] + +TASK [common : User management completed] ********************************************************************************************************************** +ok: [info-service] => { + "msg": "User management block finished" +} + +TASK [docker : Install Docker prerequisites] ******************************************************************************************************************* +ok: [info-service] + +TASK [docker : Add Docker GPG key] ***************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Add Docker repository] ************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Update apt cache after repository setup] ******************************************************************************************************** +changed: [info-service] + +TASK [docker : Install Docker packages] ************************************************************************************************************************ +ok: [info-service] + +TASK [docker : Install Docker Python SDK] ********************************************************************************************************************** +ok: [info-service] + +PLAY RECAP ***************************************************************************************************************************************************** +info-service : ok=10 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +### List of all available tags + +- app_user +- apt +- block_config +- block_install +- block_packages +- block_repository +- block_users +- common +- config +- containers +- debug +- directories +- docker +- docker_config +- docker_install +- gpg, hostname +- packages +- prerequisites +- python +- repository +- service +- ssh +- sudo +- system +- timezone +- upgrade +- users + + + +## Upgrade to Docker Compose + +### Docker Compose deployment success + +```bash +--ask-vault-pass +``` + +```bash +PLAY [Deploy application] ******************************************************************************************************************************************** + +TASK [Gathering Facts] *********************************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Install Docker prerequisites] ************************************************************************************************************************* +ok: [info-service] + +TASK [docker : Add Docker GPG key] *********************************************************************************************************************************** +changed: [info-service] + +TASK [docker : Add Docker repository] ******************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Update apt cache after repository setup] ************************************************************************************************************** +changed: [info-service] + +TASK [docker : Install Docker packages] ****************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Install Docker Python SDK] **************************************************************************************************************************** +changed: [info-service] + +TASK [docker : Ensure Docker service is running] ********************************************************************************************************************* +changed: [info-service] + +TASK [docker : Add users to docker group] **************************************************************************************************************************** +changed: [info-service] => (item=ubuntu) +changed: [info-service] => (item=appuser) + +TASK [docker : Create docker-compose directory] ********************************************************************************************************************** +changed: [info-service] + +TASK [docker : Verify Docker installation] *************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Display Docker version] ******************************************************************************************************************************* +ok: [info-service] => { + "msg": "Docker version: Docker version 29.2.1, build a5c7197" +} + +TASK [common : Update apt cache] ************************************************************************************************************************************* +changed: [info-service] + +TASK [common : Install common packages] ****************************************************************************************************************************** +changed: [info-service] + +TASK [common : Upgrade system packages] ****************************************************************************************************************************** +skipping: [info-service] + +TASK [common : Log package installation completion] ****************************************************************************************************************** +ok: [info-service] => { + "msg": "Package installation block completed" +} + +TASK [common : Create completion timestamp] ************************************************************************************************************************** +changed: [info-service] + +TASK [common : Create application user] ****************************************************************************************************************************** +changed: [info-service] + +TASK [common : Ensure SSH directory exists for app user] ************************************************************************************************************* +changed: [info-service] + +TASK [common : Add users to sudo group] ****************************************************************************************************************************** +skipping: [info-service] + +TASK [common : User management completed] **************************************************************************************************************************** +ok: [info-service] => { + "msg": "User management block finished" +} + +TASK [common : Set timezone] ***************************************************************************************************************************************** +changed: [info-service] + +TASK [common : Configure hostname] *********************************************************************************************************************************** +changed: [info-service] + +TASK [common : Configure SSH hardening] ****************************************************************************************************************************** +ok: [info-service] => (item={'key': 'PasswordAuthentication', 'value': 'no'}) +ok: [info-service] => (item={'key': 'PermitRootLogin', 'value': 'no'}) +ok: [info-service] => (item={'key': 'ClientAliveInterval', 'value': '300'}) + +TASK [web_app : Login to Docker Hub] ********************************************************************************************************************************* +ok: [info-service] + +TASK [web_app : Pull Docker image] *********************************************************************************************************************************** +ok: [info-service] + +TASK [web_app : Check if container exists] *************************************************************************************************************************** +ok: [info-service] + +TASK [web_app : Stop existing container if running] ****************************************************************************************************************** +changed: [info-service] + +TASK [web_app : Remove old container if exists] ********************************************************************************************************************** +changed: [info-service] + +TASK [web_app : Create application directory] ************************************************************************************************************************ +changed: [info-service] + +TASK [web_app : Deploy application container] ************************************************************************************************************************ +changed: [info-service] + +TASK [web_app : Wait for application to start] *********************************************************************************************************************** +ok: [info-service] + +TASK [web_app : Check application health endpoint] ******************************************************************************************************************* +ok: [info-service] + +TASK [web_app : Display health check result] ************************************************************************************************************************* +ok: [info-service] => { + "msg": "Application is healthy! Response: {'status': 'healthy', 'timestamp': '2026-02-10T13:06:04.889120+00:00', 'uptime_seconds': 13}" +} + +TASK [Show running containers] *************************************************************************************************************************************** +changed: [info-service] + +TASK [Display container status] ************************************************************************************************************************************** +ok: [info-service] => { + "msg": [ + "NAMES IMAGE STATUS PORTS", + "info-service scruffyscarf/info-service:latest Up 17 seconds 0.0.0.0:8000->5000/tcp" + ] +} + +PLAY RECAP *********************************************************************************************************************************************************** +info-service : ok=34 changed=19 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 +``` + +### Idempotency + +```bash +ansible-playbook playbooks/deploy.yml --ask-vault-pass +``` + +```bash +PLAY [Deploy application] ******************************************************************************************************************************************** + +TASK [Gathering Facts] *********************************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Install Docker prerequisites] ************************************************************************************************************************* +ok: [info-service] + +TASK [docker : Add Docker GPG key] *********************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Add Docker repository] ******************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Update apt cache after repository setup] ************************************************************************************************************** +changed: [info-service] + +TASK [docker : Install Docker packages] ****************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Install Docker Python SDK] **************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Ensure Docker service is running] ********************************************************************************************************************* +ok: [info-service] + +TASK [docker : Add users to docker group] **************************************************************************************************************************** +ok: [info-service] => (item=ubuntu) +ok: [info-service] => (item=appuser) + +TASK [docker : Create docker-compose directory] ********************************************************************************************************************** +ok: [info-service] + +TASK [docker : Verify Docker installation] *************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Display Docker version] ******************************************************************************************************************************* +ok: [info-service] => { + "msg": "Docker version: Docker version 29.2.1, build a5c7197" +} + +TASK [common : Update apt cache] ************************************************************************************************************************************* +ok: [info-service] + +TASK [common : Install common packages] ****************************************************************************************************************************** +ok: [info-service] + +TASK [common : Upgrade system packages] ****************************************************************************************************************************** +skipping: [info-service] + +TASK [common : Log package installation completion] ****************************************************************************************************************** +ok: [info-service] => { + "msg": "Package installation block completed" +} + +TASK [common : Create completion timestamp] ************************************************************************************************************************** +changed: [info-service] + +TASK [common : Create application user] ****************************************************************************************************************************** +ok: [info-service] + +TASK [common : Ensure SSH directory exists for app user] ************************************************************************************************************* +ok: [info-service] + +TASK [common : Add users to sudo group] ****************************************************************************************************************************** +skipping: [info-service] + +TASK [common : User management completed] **************************************************************************************************************************** +ok: [info-service] => { + "msg": "User management block finished" +} + +TASK [common : Set timezone] ***************************************************************************************************************************************** +ok: [info-service] + +TASK [common : Configure hostname] *********************************************************************************************************************************** +ok: [info-service] + +TASK [common : Configure SSH hardening] ****************************************************************************************************************************** +ok: [info-service] => (item={'key': 'PasswordAuthentication', 'value': 'no'}) +ok: [info-service] => (item={'key': 'PermitRootLogin', 'value': 'no'}) +ok: [info-service] => (item={'key': 'ClientAliveInterval', 'value': '300'}) + +TASK [web_app : Login to Docker Hub] ********************************************************************************************************************************* +ok: [info-service] + +TASK [web_app : Pull Docker image] *********************************************************************************************************************************** +ok: [info-service] + +TASK [web_app : Check if container exists] *************************************************************************************************************************** +ok: [info-service] + +TASK [web_app : Stop existing container if running] ****************************************************************************************************************** +changed: [info-service] + +TASK [web_app : Remove old container if exists] ********************************************************************************************************************** +changed: [info-service] + +TASK [web_app : Create application directory] ************************************************************************************************************************ +ok: [info-service] + +TASK [web_app : Deploy application container] ************************************************************************************************************************ +changed: [info-service] + +TASK [web_app : Wait for application to start] *********************************************************************************************************************** +ok: [info-service] + +TASK [web_app : Check application health endpoint] ******************************************************************************************************************* +ok: [info-service] + +TASK [web_app : Display health check result] ************************************************************************************************************************* +ok: [info-service] => { + "msg": "Application is healthy! Response: {'status': 'healthy', 'timestamp': '2026-02-10T13:06:04.889120+00:00', 'uptime_seconds': 13}" +} + +TASK [Show running containers] *************************************************************************************************************************************** +changed: [info-service] + +TASK [Display container status] ************************************************************************************************************************************** +ok: [info-service] => { + "msg": [ + "NAMES IMAGE STATUS PORTS", + "info-service scruffyscarf/info-service:latest Up 17 seconds 0.0.0.0:8000->5000/tcp" + ] +} + +PLAY RECAP *********************************************************************************************************************************************************** +info-service : ok=34 changed=6 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 +``` + +### Application running and accessible + +```bash +curl http://localhost:8000/health +``` + +```bash +{"status":"healthy","timestamp":"2026-02-10T13:35:52.669019+00:00","uptime_seconds":1801} +``` + +### Contents of templated docker compose + +```bash +docker-compose.yml.j2 +``` + +```bash +version: '{{ compose_version }}' + +services: + {{ app_name }}: + image: {{ docker_image }}:{{ docker_tag }} + container_name: {{ app_name }}_{{ app_name }} + hostname: {{ app_name }} + + ports: + - "{{ app_port }}:{{ app_internal_port }}" + + environment: + {% for key, value in app_environment.items() %} + - {{ key }}={{ value }} + {% endfor %} + + - APP_SECRET_KEY={{ app_secret_key | default('change_me_in_production') }} + + env_file: + - .env + + volumes: + - {{ data_volume }}:/app/data + - {{ log_volume }}:/app/logs + - ./config:/app/config:ro + + networks: + - {{ network_name }} + + restart: {{ service_restart_policy }} + + healthcheck: + test: {{ service_healthcheck.test | to_json }} + interval: {{ service_healthcheck.interval }} + timeout: {{ service_healthcheck.timeout }} + retries: {{ service_healthcheck.retries }} + start_period: {{ service_healthcheck.start_period }} + + deploy: + resources: + limits: + cpus: '0.50' + memory: 512M + reservations: + cpus: '0.25' + memory: 256M + + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + labels: + - "maintainer=DevOps Team" + - "version={{ app_version }}" + - "description={{ app_description }}" + +networks: + {{ network_name }}: + driver: {{ network_driver }} + name: {{ network_name }} + +volumes: + {{ data_volume }}: + name: {{ data_volume }} + {{ log_volume }}: + name: {{ log_volume }} + +``` + + + +## Wipe Logic Implementation + +### Output of Scenario 1 + +```bash +ansible-playbook playbooks/deploy.yml --ask-vault-pass +``` + +```bash +PLAY [Deploy application] ******************************************************************************************************************************************** + +TASK [Gathering Facts] *********************************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Install Docker prerequisites] ************************************************************************************************************************* +ok: [info-service] + +TASK [docker : Add Docker GPG key] *********************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Add Docker repository] ******************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Update apt cache after repository setup] ************************************************************************************************************** +changed: [info-service] + +TASK [docker : Install Docker packages] ****************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Install Docker Python SDK] **************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Ensure Docker service is running] ********************************************************************************************************************* +ok: [info-service] + +TASK [docker : Add users to docker group] **************************************************************************************************************************** +ok: [info-service] => (item=ubuntu) +ok: [info-service] => (item=appuser) + +TASK [docker : Create docker-compose directory] ********************************************************************************************************************** +ok: [info-service] + +TASK [docker : Verify Docker installation] *************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Display Docker version] ******************************************************************************************************************************* +ok: [info-service] => { + "msg": "Docker version: Docker version 29.2.1, build a5c7197" +} + +TASK [common : Update apt cache] ************************************************************************************************************************************* +ok: [info-service] + +TASK [common : Install common packages] ****************************************************************************************************************************** +ok: [info-service] + +TASK [common : Upgrade system packages] ****************************************************************************************************************************** +skipping: [info-service] + +TASK [common : Log package installation completion] ****************************************************************************************************************** +ok: [info-service] => { + "msg": "Package installation block completed" +} + +TASK [common : Create completion timestamp] ************************************************************************************************************************** +changed: [info-service] + +TASK [common : Create application user] ****************************************************************************************************************************** +ok: [info-service] + +TASK [common : Ensure SSH directory exists for app user] ************************************************************************************************************* +ok: [info-service] + +TASK [common : Add users to sudo group] ****************************************************************************************************************************** +skipping: [info-service] + +TASK [common : User management completed] **************************************************************************************************************************** +ok: [info-service] => { + "msg": "User management block finished" +} + +TASK [common : Set timezone] ***************************************************************************************************************************************** +ok: [info-service] + +TASK [common : Configure hostname] *********************************************************************************************************************************** +ok: [info-service] + +TASK [common : Configure SSH hardening] ****************************************************************************************************************************** +ok: [info-service] => (item={'key': 'PasswordAuthentication', 'value': 'no'}) +ok: [info-service] => (item={'key': 'PermitRootLogin', 'value': 'no'}) +ok: [info-service] => (item={'key': 'ClientAliveInterval', 'value': '300'}) + +TASK [web_app : Include wipe tasks] ********************************************************************************************************************************** +included: /Users/scruffyscarf/DevOps-Core-Course/ansible/roles/web_app/tasks/wipe.yml for info-service + +TASK [web_app : Wipe web application - confirmation check] *********************************************************************************************************** +skipping: [info-service] + +TASK [web_app : Check if Docker Compose project exists] ************************************************************************************************************** +ok: [info-service] + +TASK [web_app : Stop and remove Docker Compose project] ************************************************************************************************************** +skipping: [info-service] + +TASK [web_app : Remove application directory] ************************************************************************************************************************ +ok: [info-service] + +TASK [web_app : Remove Docker images] ******************************************************************************************************************************** +skipping: [info-service] + +TASK [web_app : Verify wipe completion] ****************************************************************************************************************************** +[ERROR]: Task failed: Action failed. +Origin: /Users/scruffyscarf/DevOps-Core-Course/ansible/roles/web_app/tasks/wipe.yml:65:7 + +63 - images +64 +65 - name: Verify wipe completion + ^ column 7 + +fatal: [info-service]: FAILED! => {"changed": false, "cmd": "docker ps -a --filter \"name=info-service\" --format \"{{.Names}}\"\n", "delta": "0:00:00.033141", "end": "2026-02-10 18:46:55.273423", "failed_when_result": true, "msg": "", "rc": 0, "start": "2026-02-10 18:46:55.240282", "stderr": "", "stderr_lines": [], "stdout": "info-service", "stdout_lines": ["info-service"]} + +PLAY RECAP *********************************************************************************************************************************************************** +info-service : ok=25 changed=2 unreachable=0 failed=1 skipped=5 rescued=0 ignored=0 +``` + +### Output of Scenario 2 + +```bash +ansible-playbook playbooks/deploy.yml --ask-vault-pass \ + -e "web_app_wipe=true" \ + --tags web_app_wipe +``` + +```bash +PLAY [Deploy application] ******************************************************************************************************************************************** + +TASK [Gathering Facts] *********************************************************************************************************************************************** +ok: [info-service] + +TASK [common : Log package installation completion] ****************************************************************************************************************** +ok: [info-service] => { + "msg": "Package installation block completed" +} + +TASK [common : Create completion timestamp] ************************************************************************************************************************** +changed: [info-service] + +TASK [common : User management completed] **************************************************************************************************************************** +ok: [info-service] => { + "msg": "User management block finished" +} + +TASK [web_app : Include wipe tasks] ********************************************************************************************************************************** +included: /Users/scruffyscarf/DevOps-Core-Course/ansible/roles/web_app/tasks/wipe.yml for info-service + +TASK [web_app : Wipe web application - confirmation check] *********************************************************************************************************** +ok: [info-service] => { + "msg": "===========================================\nWIPE OPERATION INITIATED\n\nThis will remove:\n1. Docker containers for info-service\n2. Docker volumes for info-service\n3. Application directory: /opt/info-service\n4. Docker images (optional)\n\nWipe variable: true\nTag: web_app_wipe\n===========================================\n" +} + +TASK [web_app : Check if Docker Compose project exists] ************************************************************************************************************** +ok: [info-service] + +TASK [web_app : Stop and remove Docker Compose project] ************************************************************************************************************** +skipping: [info-service] + +TASK [web_app : Remove application directory] ************************************************************************************************************************ +ok: [info-service] + +TASK [web_app : Remove Docker images] ********************************************************************************************************************* +skipping: [info-service] + +TASK [web_app : Verify wipe completion] ****************************************************************************************************************************** +ok: [info-service] + +TASK [web_app : Display wipe results] ******************************************************************************************************************************** +ok: [info-service] => { + "msg": "===========================================\nWIPE OPERATION COMPLETED\n\nCompose file existed: False\nDirectory removed: False\n\nRemaining containers:\nNone - all containers removed successfully\n\nApplication directory exists: False\n===========================================\n" +} + +PLAY RECAP *********************************************************************************************************************************************************** +info-service : ok=10 changed=1 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 +``` + +### Output of Scenario 3 + +```bash +ansible-playbook playbooks/deploy.yml --ask-vault-pass \ + -e "web_app_wipe=true" +``` + +```bash +PLAY [Deploy application] ******************************************************************************************************************************************** + +TASK [Gathering Facts] *********************************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Install Docker prerequisites] ************************************************************************************************************************* +ok: [info-service] + +TASK [docker : Add Docker GPG key] *********************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Add Docker repository] ******************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Update apt cache after repository setup] ************************************************************************************************************** +changed: [info-service] + +TASK [docker : Install Docker packages] ****************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Install Docker Python SDK] **************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Ensure Docker service is running] ********************************************************************************************************************* +ok: [info-service] + +TASK [docker : Add users to docker group] **************************************************************************************************************************** +ok: [info-service] => (item=ubuntu) +ok: [info-service] => (item=appuser) + +TASK [docker : Create docker-compose directory] ********************************************************************************************************************** +ok: [info-service] + +TASK [docker : Verify Docker installation] *************************************************************************************************************************** +ok: [info-service] + +TASK [docker : Display Docker version] ******************************************************************************************************************************* +ok: [info-service] => { + "msg": "Docker version: Docker version 29.2.1, build a5c7197" +} + +TASK [common : Update apt cache] ************************************************************************************************************************************* +ok: [info-service] + +TASK [common : Install common packages] ****************************************************************************************************************************** +ok: [info-service] + +TASK [common : Upgrade system packages] ****************************************************************************************************************************** +skipping: [info-service] + +TASK [common : Log package installation completion] ****************************************************************************************************************** +ok: [info-service] => { + "msg": "Package installation block completed" +} + +TASK [common : Create completion timestamp] ************************************************************************************************************************** +changed: [info-service] + +TASK [common : Create application user] ****************************************************************************************************************************** +ok: [info-service] + +TASK [common : Ensure SSH directory exists for app user] ************************************************************************************************************* +ok: [info-service] + +TASK [common : Add users to sudo group] ****************************************************************************************************************************** +skipping: [info-service] + +TASK [common : User management completed] **************************************************************************************************************************** +ok: [info-service] => { + "msg": "User management block finished" +} + +TASK [common : Set timezone] ***************************************************************************************************************************************** +ok: [info-service] + +TASK [common : Configure hostname] *********************************************************************************************************************************** +ok: [info-service] + +TASK [common : Configure SSH hardening] ****************************************************************************************************************************** +ok: [info-service] => (item={'key': 'PasswordAuthentication', 'value': 'no'}) +ok: [info-service] => (item={'key': 'PermitRootLogin', 'value': 'no'}) +ok: [info-service] => (item={'key': 'ClientAliveInterval', 'value': '300'}) + +TASK [web_app : Include wipe tasks] ********************************************************************************************************************************** +included: /Users/scruffyscarf/DevOps-Core-Course/ansible/roles/web_app/tasks/wipe.yml for info-service + +TASK [web_app : Wipe web application - confirmation check] *********************************************************************************************************** +ok: [info-service] => { + "msg": "===========================================\nWIPE OPERATION INITIATED\n\nThis will remove:\n1. Docker containers for info-service\n2. Docker volumes for info-service\n3. Application directory: /opt/info-service\n4. Docker images (optional)\n\nWipe variable: true\nTag: web_app_wipe\n===========================================\n" +} + +TASK [web_app : Check if Docker Compose project exists] ************************************************************************************************************** +ok: [info-service] + +TASK [web_app : Stop and remove Docker Compose project] ************************************************************************************************************** +skipping: [info-service] + +TASK [web_app : Remove application directory] ************************************************************************************************************************ +ok: [info-service] + +TASK [web_app : Remove Docker images] ******************************************************************************************************************************** +skipping: [info-service] + +TASK [web_app : Verify wipe completion] ****************************************************************************************************************************** +ok: [info-service] + +TASK [web_app : Display wipe results] ******************************************************************************************************************************** +ok: [info-service] => { + "msg": "===========================================\nWIPE OPERATION COMPLETED\n\nCompose file existed: False\nDirectory removed: False\n\nRemaining containers:\nNone - all containers removed successfully\n\nApplication directory exists: False\n===========================================\n" +} + +TASK [web_app : Login to Docker Hub] ********************************************************************************************************************************* +ok: [info-service] + +TASK [web_app : Pull Docker image] *********************************************************************************************************************************** +ok: [info-service] + +TASK [web_app : Check if container exists] *************************************************************************************************************************** +ok: [info-service] + +TASK [web_app : Stop existing container if running] ****************************************************************************************************************** +skipping: [info-service] + +TASK [web_app : Remove old container if exists] ********************************************************************************************************************** +ok: [info-service] + +TASK [web_app : Create application directory] ************************************************************************************************************************ +changed: [info-service] + +TASK [web_app : Deploy application container] ************************************************************************************************************************ +changed: [info-service] + +TASK [web_app : Wait for application to start] *********************************************************************************************************************** +ok: [info-service] + +TASK [web_app : Check application health endpoint] ******************************************************************************************************************* +ok: [info-service] + +TASK [web_app : Display health check result] ************************************************************************************************************************* +ok: [info-service] => { + "msg": "Application is healthy! Response: {'status': 'healthy', 'timestamp': '2026-02-10T16:23:37.762844+00:00', 'uptime_seconds': 12}" +} + +TASK [Show running containers] *************************************************************************************************************************************** +changed: [info-service] + +TASK [Display container status] ************************************************************************************************************************************** +ok: [info-service] => { + "msg": [ + "NAMES IMAGE STATUS PORTS", + "info-service scruffyscarf/info-service:latest Up 16 seconds 0.0.0.0:8000->5000/tcp" + ] +} + +PLAY RECAP *********************************************************************************************************************************************************** +info-service : ok=39 changed=5 unreachable=0 failed=0 skipped=5 rescued=0 ignored=0 +``` + +### Output of Scenario 4 + +```bash +ansible-playbook playbooks/deploy.yml --tags web_app_wipe +``` + +```bash +PLAY [Deploy application] ******************************************************************************************************************************************** + +TASK [Gathering Facts] *********************************************************************************************************************************************** +ok: [info-service] + +TASK [common : Log package installation completion] ****************************************************************************************************************** +ok: [info-service] => { + "msg": "Package installation block completed" +} + +TASK [common : Create completion timestamp] ************************************************************************************************************************** +changed: [info-service] + +TASK [common : User management completed] **************************************************************************************************************************** +ok: [info-service] => { + "msg": "User management block finished" +} + +TASK [web_app : Include wipe tasks] ********************************************************************************************************************************** +included: /Users/scruffyscarf/DevOps-Core-Course/ansible/roles/web_app/tasks/wipe.yml for info-service + +TASK [web_app : Wipe web application - confirmation check] *********************************************************************************************************** +ok: [info-service] => { + "msg": "===========================================\nWIPE OPERATION INITIATED\n\nThis will remove:\n1. Docker containers for info-service\n2. Docker volumes for info-service\n3. Application directory: /opt/info-service\n4. Docker images (optional)\n\nWipe variable: False\nTag: web_app_wipe\n===========================================\n" +} + +TASK [web_app : Check if Docker Compose project exists] ************************************************************************************************************** +ok: [info-service] + +TASK [web_app : Stop and remove Docker Compose project] ************************************************************************************************************** +skipping: [info-service] + +TASK [web_app : Remove application directory] ************************************************************************************************************************ +changed: [info-service] + +TASK [web_app : Remove Docker images] ******************************************************************************************************************************** +skipping: [info-service] + +TASK [web_app : Verify wipe completion] ****************************************************************************************************************************** +ok: [info-service] + +TASK [web_app : Display wipe results] ******************************************************************************************************************************** +ok: [info-service] => { + "msg": "===========================================\nWIPE OPERATION COMPLETED\n\nCompose file existed: False\nDirectory removed: True\n\nRemaining containers:\ne765a2ecee53 scruffyscarf/info-service:latest \"python app.py\" 4 minutes ago Up 4 minutes 0.0.0.0:8000->5000/tcp info-service\n\nApplication directory exists: False\n===========================================\n" +} + +PLAY RECAP *********************************************************************************************************************************************************** +info-service : ok=10 changed=2 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 +``` + +### Application running after clean reinstall + +```bash +curl http://localhost:8000/health +``` + +```bash +{"status":"healthy","timestamp":"2026-02-10T16:28:44.844433+00:00","uptime_seconds":319} +``` + +### Research + +- Use both variable AND tag (Double safety mechanism) + +**Safety and flexibility:** The variable (`web_app_wipe`) controls the logic, the tag (`web_app_wipe`) controls the execution. Double protection against accidental deletion: must explicitly specify both the =true variable and the tag. This prevents accidental wipe during normal deployment. + +- Difference between never tag and this approach + +**`never` tag:** Tasks with the `never` tag are never performed automatically, only if `--tags never` is explicitly specified. +**Approach:** Tasks are executed when `web_app_wipe=true` And the tag `web_app_wipe` is present. More controllable: can run wipe without changing the playbook, just via `-e "web_app_wipe=true"'. + +- Wipe logic come BEFORE deployment in main.yml (Clean reinstall scenario) + +**Sequence of operations:** Wipe -> Deploy = clean reinstall. If wipe occurs after deployment, it will delete the newly deployed application. The "delete old -> install new" order is critical for the clean reinstallation scenario. + +- Clean reinstallation vs. rolling update + +**Clean reinstallation:** +- Migration of versions with breaking changes +- Correction of the corrupted state +- Changing the application architecture +- Testing from a clean state + +**Rolling update:** +- Minor updates without downtime +- Hotfixes in production +- Saving state/data +- Blue-green deployments + +- Extend wipe Docker images and volumes + +**Add tasks:** +```bash +- name: Clean unused Docker resources + shell: docker system prune -af +``` + + + +## CI/CD with GitHub Actions + +### Ansible lint passing + +```bash +ansible-lint playbooks/*.yml +``` + +```bash +Passed: 0 failure(s), 0 warning(s) in 3 files processed of 3 encountered. Last profile that met the validation criteria was 'production'. +``` + +### App responding + +```bash +ansible-playbook ansible/playbooks/deploy.yml\ + --vault-password-file /tmp/vault_pass \ +``` + +```bash +ok: [info-service] => { + "msg": [ + "NAMES IMAGE STATUS PORTS", + "info-service scruffyscarf/info-service:latest Up 16 seconds 0.0.0.0:8000->5000/tcp" + ] +} +``` + +### What are the security implications of storing SSH keys in GitHub Secrets? + +- Keys are encrypted at rest but exposed during workflow execution +- No automatic rotation mechanism +- Compromised GitHub token = exposed SSH keys +- Limited audit trails for key usage +- Keys persist in workflow logs if accidentally printed + +### How would you implement a staging → production deployment pipeline? + +1. Deploy to staging automatically on main merge +2. Run integration/smoke tests against staging +3. Manual approval gate +4. Deploy to production using same artifacts +5. Health checks post-deployment +6. Automated rollback on failure thresholds + +### What would you add to make rollbacks possible? + +- Versioned artifacts +- Blue/green deployments with traffic switch +- Database migrations backward compatible +- Feature flags for gradual rollout +- Ansible playbook idempotency with previous state +- Git tags for deployment states + +### How does self-hosted runner improve security compared to GitHub-hosted? + +- No GitHub IP ranges exposure +- Keys/certificates never leave your network +- Compliance with internal security policies +- Isolated execution environment +- No risk of cross-tenant attacks +- Full audit control + + + +## Multi-App Deployment + +### Multi-app architecture explanation + +Multi-app deployment uses a single Ansible role deployed multiple times with different variables. This follows the DRY principle and provides: + +- **Single source of truth** for deployment logic +- **Consistent configuration** across applications +- **Reduced maintenance** overhead +- **Tested and proven** deployment patterns + +### Variable file strategy + +```bash +vars/ +├── app_python.yml # Python-specific configuration +└── app_bonus.yml # Go-specific configuration +``` + +### Role reusability benefits + +```yaml +# Single role, multiple invocations +- include_role: + name: web_app + vars: + app_name: devops-python # Different each time + docker_image: python-app # Different each time + app_port: 8000 # Different each time +``` + +### Port conflict resolution + +| Application | Host Port | Container Port | Purpose | +|------------|-----------|----------------|---------| +| Python App | 8000 | 8000 | Main Python service | +| Go App | 8001 | 8080 | High-performance Go service | + +### Independent vs. combined deployment trade-offs + +| Approach | Pros | Cons | Best For | +|------------|-----------|----------------|---------| +| Independent | Granular control, Faster single app updates, clear responsibility | More commands, potential drift| Development, testing | +| Combined | One command, consistent state, simple orchestration | Slower, all or nothing | Production, staging | + + + +## Multi-App CI/CD + +### Multi-app CI/CD architecture + +```bash +┌─────────────────┐ ┌──────────────────┐ +│ Python App │ │ Go App │ +│ Workflow │ │ Workflow │ +├─────────────────┤ ├──────────────────┤ +│ • deploy-python │ │ • deploy-bonus │ +│ • vars_python │ │ • vars_bonus │ +│ • port: 8000 │ │ • port: 8001 │ +└────────┬────────┘ └────────┬─────────┘ + │ │ + └───────────┬───────────┘ + │ + ┌────────▼────────┐ + │ Target VM │ + │ • 2 containers │ + │ • 2 ports │ + └─────────────────┘ +``` + +### Workflow triggering logic + +```bash +on: + push: + paths: + - 'ansible/vars/app_python.yml' + - 'ansible/playbooks/deploy_python.yml' + - 'ansible/roles/web_app/**' +``` + +```bash +on: + push: + paths: + - 'ansible/vars/app_bonus.yml' + - 'ansible/playbooks/deploy_bonus.yml' + - 'ansible/roles/web_app/**' +``` + +### Path filter strategy + +```yaml +# Prevent unnecessary runs +paths-ignore: + - '**.md' # Documentation + - 'docs/**' # Docs directory + - '.gitignore' # Git files + - 'LICENSE' # License file +``` + +### Matrix vs separate workflows comparison + +| Aspect | Separate Workflows | Matrix Strategy | +|--------------------|--------------------------|------------------------| +| Files | 2 workflow files | 1 workflow file | +| Trigger Control | Granular per app | All or nothing | +| Parallel Execution | Native | Matrix strategy | +| Readability | Clear purpose | More complex | +| Maintenance | Two files to update | Single source | +| Failure Isolation | One app fails, other works | Matrix may fail partially | +| Debugging | Simple logs | Nested logs | + + +### Evidence of independent deployments + +1. Python-Only Change + - Python workflow: RUNNING + - Go workflow: SKIPPED (path filter) + +2. Go-Only Change + - Python workflow: SKIPPED + - Go workflow: RUNNING + +3. Shared Role Change + - Python workflow: RUNNING + - Go workflow: RUNNING diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml new file mode 100644 index 0000000000..ed9d2ecc21 --- /dev/null +++ b/ansible/group_vars/all.yml @@ -0,0 +1,19 @@ +$ANSIBLE_VAULT;1.1;AES256 +32343936303566623239346637363461343661616538333434656338626135383665323235653432 +3163653466343363383263336133393561643761653130310a383038663031656131636362633032 +64633430326337656663386138386330333061623061636561366365303163633661326661326362 +6538646264363138380a636632333366623962313133363630363066356130316436323339343561 +61396165353039663236656462623435656638626231323738353131326633656264333862333934 +61303036333131303232663533383733366338363438636235633965633931643338646661643938 +35363932326539396438363735336361363566333830346363363662313535343431326437623761 +34626463353332396237333632366662353638336338343733333561303062366165616531393631 +39383335356263353763353537373062326564663339356635366232363961336566653332303261 +65313565656331396331376132313531306536656262313364386339346336346232613736313039 +30316662633032643931306136633364306531343266313236646134356637373933396637663531 +35333666326264343837383362663534306139623833366235306634336136643566643965313465 +32383436316534336530363464623365373132623631653764396534383037373933613431376164 +64396664353332643338366264353762343662633830653832316665346164616365333363613437 +30346366616439343536653733313730646437626633326463323761613832363833303662653032 +63613561613932373935623932646634313531666439383563356634363766623334366662393830 +61313561326266613939613063343266336363393965393066653462346239636661323264663562 +3534306134386431336135313564666333393663656130306637 diff --git a/ansible/inventory/hosts.ci.yml b/ansible/inventory/hosts.ci.yml new file mode 100644 index 0000000000..31a8ef03f1 --- /dev/null +++ b/ansible/inventory/hosts.ci.yml @@ -0,0 +1,13 @@ +--- +all: + vars: + ansible_python_interpreter: /usr/bin/python3 + +webservers: + hosts: + info-service: + ansible_host: "{{ lookup('env', 'VM_HOST') }}" + ansible_user: "{{ lookup('env', 'VM_USER') }}" + ansible_port: 22 + ansible_ssh_private_key_file: /home/runner/.ssh/id_ed25519 + ansible_ssh_common_args: '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' \ No newline at end of file diff --git a/ansible/inventory/hosts.ini b/ansible/inventory/hosts.ini new file mode 100644 index 0000000000..4a71821346 --- /dev/null +++ b/ansible/inventory/hosts.ini @@ -0,0 +1,6 @@ +[webservers] +info-service ansible_host='93.77.183.255' ansible_user='ubuntu' + +[webservers:vars] +ansible_python_interpreter=/usr/bin/python3 +ansible_ssh_private_key_file={{ lookup('env', 'ANSIBLE_SSH_KEY_FILE', '~/.ssh/id_ed25519') }} diff --git a/ansible/playbooks/deploy-all.yml b/ansible/playbooks/deploy-all.yml new file mode 100644 index 0000000000..cebdc860a0 --- /dev/null +++ b/ansible/playbooks/deploy-all.yml @@ -0,0 +1,65 @@ +--- +- name: Deploy All Applications + hosts: webservers + become: true + vars: + skip_failed: false + deploy_python: true + deploy_go: true + + tasks: + - name: Include Python app variables + ansible.builtin.include_vars: ../python_app.yml + when: deploy_python | bool + tags: [python, vars] + + - name: Deploy Python Application + ansible.builtin.include_role: + name: web_app + apply: + tags: [python, deploy] + vars: + app_name: devops-python + docker_image: "{{ dockerhub_username }}/devops-info-service" + app_port: 8000 + app_internal_port: 8000 + compose_project_dir: "/opt/devops-python" + when: deploy_python | bool + tags: [python, deploy] + + - name: Include Go app variables + include_vars: ../vars/app_bonus.yml + when: deploy_go | bool + tags: [go, vars] + + - name: Deploy Go Application + include_role: + name: web_app + apply: + tags: [go, deploy] + vars: + app_name: devops-go + docker_image: "{{ dockerhub_username }}/devops-info-service-go" + app_port: 8001 + app_internal_port: 8080 + compose_project_dir: "/opt/devops-go" + when: deploy_go | bool + tags: [go, deploy] + + post_tasks: + - name: Verify all applications + block: + - name: Check Python app + ansible.builtin.uri: + url: "http://localhost:8000/health" + method: GET + register: python_check + ignore_errors: true + + - name: Check Go app + ansible.builtin.uri: + url: "http://localhost:8001/health" + method: GET + register: go_check + ignore_errors: true + diff --git a/ansible/playbooks/deploy-bonus.yml b/ansible/playbooks/deploy-bonus.yml new file mode 100644 index 0000000000..6df0e02c2d --- /dev/null +++ b/ansible/playbooks/deploy-bonus.yml @@ -0,0 +1,43 @@ +--- +- name: Deploy Go Web Application + hosts: webservers + become: true + vars_files: + - ../vars/app_bonus.yml + vars: + web_app_wipe: false + + pre_tasks: + - name: Display deployment target + debug: + msg: | + ======================================== + Deploying Go Application + App: {{ app_name }} + Image: {{ docker_image }}:{{ docker_tag }} + Port: {{ app_port }}:{{ app_internal_port }} + Environment: {{ app_environment.APP_ENV }} + ======================================== + tags: always + + roles: + - role: web_app + role_path: ../roles + tags: [bonus, golang, web_app] + + post_tasks: + - name: Verify Go application + uri: + url: "http://localhost:{{ app_port }}{{ health_check_path }}" + method: GET + status_code: 200 + register: health_check + retries: 5 + delay: 5 + until: health_check.status == 200 + tags: [bonus, verify] + + - name: Display Go app health + debug: + msg: "Go App Health: {{ health_check.json | default('OK') }}" + tags: [bonus, verify] diff --git a/ansible/playbooks/deploy-python.yml b/ansible/playbooks/deploy-python.yml new file mode 100644 index 0000000000..4d7e44e3e9 --- /dev/null +++ b/ansible/playbooks/deploy-python.yml @@ -0,0 +1,43 @@ +--- +- name: Deploy Python Web Application + hosts: webservers + become: true + vars_files: + - ../vars/app_python.yml + vars: + web_app_wipe: false + + pre_tasks: + - name: Display deployment target + debug: + msg: | + ======================================== + Deploying Python Application + App: {{ app_name }} + Image: {{ docker_image }}:{{ docker_tag }} + Port: {{ app_port }}:{{ app_internal_port }} + Environment: {{ app_environment.APP_ENV }} + ======================================== + tags: always + + roles: + - role: web_app + role_path: ../roles + tags: [python, web_app] + + post_tasks: + - name: Verify Python application + uri: + url: "http://localhost:{{ app_port }}{{ health_check_path }}" + method: GET + status_code: 200 + register: health_check + retries: 5 + delay: 5 + until: health_check.status == 200 + tags: [python, verify] + + - name: Display Python app health + debug: + msg: "Python App Health: {{ health_check.json | default('OK') }}" + tags: [python, verify] diff --git a/ansible/playbooks/deploy.yml b/ansible/playbooks/deploy.yml new file mode 100644 index 0000000000..39ffb282c0 --- /dev/null +++ b/ansible/playbooks/deploy.yml @@ -0,0 +1,22 @@ +--- +- name: Deploy application + hosts: webservers + become: true + vars_files: + - "../group_vars/all.yml" + + roles: + - role: web_app + tags: + - app + - deploy + + post_tasks: + - name: Show running containers + command: docker ps --format "table {{ "{{" }}.Names{{ "}}" }}\t{{ "{{" }}.Image{{ "}}" }}\t{{ "{{" }}.Status{{ "}}" }}\t{{ "{{" }}.Ports{{ "}}" }}" + register: containers + changed_when: false + + - name: Display container status + debug: + msg: "{{ containers.stdout_lines }}" diff --git a/ansible/playbooks/provision.yml b/ansible/playbooks/provision.yml new file mode 100644 index 0000000000..644aada37b --- /dev/null +++ b/ansible/playbooks/provision.yml @@ -0,0 +1,14 @@ +--- +- name: Provision web servers + hosts: webservers + become: true + + roles: + - role: common + tags: + - common + - system + - role: docker + tags: + - docker + - containers diff --git a/ansible/playbooks/site.yml b/ansible/playbooks/site.yml new file mode 100644 index 0000000000..d693404e7e --- /dev/null +++ b/ansible/playbooks/site.yml @@ -0,0 +1,8 @@ +--- +- name: Placeholder playbook + hosts: localhost + gather_facts: false + tasks: + - name: Placeholder task + debug: + msg: "This is a placeholder playbook" diff --git a/ansible/roles/common/defaults/main.yml b/ansible/roles/common/defaults/main.yml new file mode 100644 index 0000000000..ed2589ea8c --- /dev/null +++ b/ansible/roles/common/defaults/main.yml @@ -0,0 +1,17 @@ +--- +common_packages: + - python3-pip + - curl + - wget + - git + - vim + - htop + - net-tools + - ufw + - software-properties-common + - apt-transport-https + - ca-certificates + - gnupg + - lsb-release + +common_timezone: "Europe/Moscow" diff --git a/ansible/roles/common/handlers/main.yml b/ansible/roles/common/handlers/main.yml new file mode 100644 index 0000000000..a8b41ae8d3 --- /dev/null +++ b/ansible/roles/common/handlers/main.yml @@ -0,0 +1,7 @@ +--- +- name: Restart sshd + systemd: + name: sshd + state: restarted + daemon_reload: yes + become: true diff --git a/ansible/roles/common/tasks/main.yml b/ansible/roles/common/tasks/main.yml new file mode 100644 index 0000000000..60e08d1bc4 --- /dev/null +++ b/ansible/roles/common/tasks/main.yml @@ -0,0 +1,170 @@ +--- +- name: System packages installation block + block: + - name: Update apt cache + apt: + update_cache: true + cache_valid_time: 3600 + register: apt_update + tags: + - packages + - apt + + - name: Install common packages + apt: + name: "{{ common_packages }}" + state: present + update_cache: no + register: package_install + tags: + - packages + - apt + + - name: Upgrade system packages + apt: + upgrade: dist + update_cache: no + when: auto_upgrade | default(false) + tags: + - packages + - upgrade + + rescue: + - name: Handle package installation failure + debug: + msg: "Package installation failed, attempting to fix..." + tags: + - rescue + - debug + + - name: Fix broken packages + apt: + name: "{{ common_packages }}" + state: fixed + when: apt_update is failed or package_install is failed + tags: + - rescue + - apt + + - name: Retry apt update with fix + apt: + update_cache: true + force_apt_get: yes + when: apt_update is failed + tags: + - rescue + - apt + + always: + - name: Log package installation completion + debug: + msg: "Package installation block completed" + tags: + - always + - debug + + - name: Create completion timestamp + copy: + mode: "0644" + content: "Common role completed at {{ ansible_facts['date_time']['iso8601'] }}" + dest: /tmp/ansible_common_completion.log + tags: + - always + - logging + + tags: + - packages + - block_packages + +- name: User management block + block: + - name: Create application user + user: + name: "{{ app_user }}" + state: present + system: no + create_home: true + shell: /bin/bash + when: app_user is defined + tags: + - users + - app_user + + - name: Ensure SSH directory exists for app user + file: + path: "/home/{{ app_user }}/.ssh" + state: directory + mode: '0700' + owner: "{{ app_user }}" + group: "{{ app_user }}" + when: app_user is defined + tags: + - users + - ssh + + - name: Add users to sudo group + user: + name: "{{ item }}" + groups: sudo + append: yes + loop: "{{ admin_users | default([]) }}" + tags: + - users + - sudo + + rescue: + - name: Handle user management failure + debug: + msg: "User management failed, check permissions" + tags: + - rescue + - debug + + always: + - name: User management completed + debug: + msg: "User management block finished" + tags: + - always + - debug + + tags: + - users + - block_users + +- name: System configuration block + block: + - name: Set timezone + timezone: + name: "{{ timezone | default('UTC') }}" + tags: + - config + - timezone + + - name: Configure hostname + hostname: + name: "{{ inventory_hostname }}" + tags: + - config + - hostname + + - name: Configure SSH hardening + lineinfile: + path: /etc/ssh/sshd_config + regexp: "^{{ item.key }}(\\s+|$)" + line: "{{ item.key }} {{ item.value }}" + state: present + backup: yes + loop: + - { key: 'PasswordAuthentication', value: 'no' } + - { key: 'PermitRootLogin', value: 'no' } + - { key: 'ClientAliveInterval', value: '300' } + notify: restart sshd + tags: + - config + - ssh + - security + + tags: + - config + - block_config diff --git a/ansible/roles/docker/defaults/main.yml b/ansible/roles/docker/defaults/main.yml new file mode 100644 index 0000000000..28c929318a --- /dev/null +++ b/ansible/roles/docker/defaults/main.yml @@ -0,0 +1,12 @@ +--- +docker_users: + - ubuntu + - "{{ ansible_user }}" + +docker_version: "" + +docker_repository: "deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_facts['distribution_release'] }} stable" +docker_repository_key_url: "https://download.docker.com/linux/ubuntu/gpg" +docker_repo_url: "https://download.docker.com/linux/ubuntu" +docker_distribution: "jammy" +docker_arch: "amd64" diff --git a/ansible/roles/docker/handlers/main.yml b/ansible/roles/docker/handlers/main.yml new file mode 100644 index 0000000000..250eba75cc --- /dev/null +++ b/ansible/roles/docker/handlers/main.yml @@ -0,0 +1,14 @@ +--- +- name: Restart docker + service: + name: docker + state: restarted + enabled: true + listen: "restart docker" + +- name: Restart sshd + systemd: + name: sshd + state: restarted + daemon_reload: yes + become: true diff --git a/ansible/roles/docker/tasks/cleanup.yml b/ansible/roles/docker/tasks/cleanup.yml new file mode 100644 index 0000000000..35bd3668cd --- /dev/null +++ b/ansible/roles/docker/tasks/cleanup.yml @@ -0,0 +1,56 @@ +--- +- name: Cleanup existing Docker repository configuration + block: + - name: Remove all Docker repository files + file: + path: "{{ item }}" + state: absent + loop: + - /etc/apt/sources.list.d/docker.list + - /etc/apt/sources.list.d/additional-repositories.list + - /etc/apt/keyrings/docker.gpg + - /etc/apt/keyrings/docker.asc + - /usr/share/keyrings/docker.gpg + - /etc/apt/trusted.gpg.d/docker.gpg + - /etc/apt/trusted.gpg.d/docker-archive-keyring.gpg + - /etc/apt/trusted.gpg.d/docker-ce.gpg + become: true + ignore_errors: true + + - name: Remove any Docker repository from sources.list + lineinfile: + path: /etc/apt/sources.list + regexp: '.*docker.*|.*download.docker.*' + state: absent + become: true + + - name: Remove any Docker repository from sources.list.d + shell: | + grep -l "docker" /etc/apt/sources.list.d/* 2>/dev/null | xargs rm -f || true + grep -l "download.docker" /etc/apt/sources.list.d/* 2>/dev/null | xargs rm -f || true + become: true + changed_when: false + failed_when: false + ignore_errors: true + + - name: Clean apt cache + shell: | + rm -rf /var/lib/apt/lists/* + apt-get clean + become: true + ignore_errors: true + changed_when: false + + - name: Update apt cache + apt: + update_cache: yes + cache_valid_time: 0 + force_apt_get: yes + become: true + register: apt_update + retries: 3 + delay: 5 + until: apt_update is success + + tags: + - docker_cleanup diff --git a/ansible/roles/docker/tasks/main.yml b/ansible/roles/docker/tasks/main.yml new file mode 100644 index 0000000000..3f7dc0c643 --- /dev/null +++ b/ansible/roles/docker/tasks/main.yml @@ -0,0 +1,169 @@ +--- +- name: Include cleanup tasks + include_tasks: cleanup.yml + tags: + - docker_cleanup + +- name: Docker prerequisites and repository setup block + block: + - name: Create keyrings directory + file: + path: /etc/apt/keyrings + state: directory + mode: '0755' + tags: + - docker_install + - gpg + + - name: Install Docker prerequisites + apt: + name: + - ca-certificates + - curl + - gnupg + - lsb-release + state: present + update_cache: yes + tags: + - docker_install + - prerequisites + + - name: Add Docker GPG key + get_url: + url: "https://download.docker.com/linux/ubuntu/gpg" + dest: /etc/apt/keyrings/docker.asc + mode: '0644' + register: gpg_key_added + tags: + - docker_install + - gpg + + - name: Add Docker repository + apt_repository: + repo: "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu {{ ansible_facts['distribution_release'] }} stable" + state: present + update_cache: no + tags: + - docker_install + - repository + + always: + - name: Update apt cache after repository setup + apt: + update_cache: yes + tags: + - docker_install + - apt + + rescue: + - name: Handle GPG key addition failure + debug: + msg: "GPG key addition failed, attempting alternative method..." + tags: + - rescue + - debug + + tags: + - docker_install + - block_repository + +- name: Docker installation block + block: + - name: Install Docker packages + apt: + name: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-buildx-plugin + - docker-compose-plugin + state: present + notify: restart docker + tags: + - docker_install + - packages + + - name: Install Docker Python SDK + pip: + name: + - docker + - docker-compose + state: present + tags: + - docker_install + - python + + rescue: + - name: Handle Docker installation failure + debug: + msg: "Docker installation failed, cleaning up..." + tags: + - rescue + - debug + + - name: Clean up failed Docker installation + apt: + name: + - docker-ce + - docker-ce-cli + - containerd.io + state: absent + purge: yes + ignore_errors: true + tags: + - rescue + - cleanup + + tags: + - docker_install + - block_install + +- name: Docker configuration block + block: + - name: Ensure Docker service is running + service: + name: docker + state: started + enabled: true + tags: + - docker_config + - service + + - name: Add users to docker group + user: + name: "{{ item }}" + groups: docker + append: yes + loop: "{{ docker_users | default([]) }}" + tags: + - docker_config + - users + + - name: Create docker-compose directory + file: + path: /usr/local/lib/docker/cli-plugins + state: directory + mode: '0755' + tags: + - docker_config + - directories + + always: + - name: Verify Docker installation + command: docker --version + register: docker_version + changed_when: false + tags: + - docker_config + - verification + + - name: Display Docker version + debug: + msg: "Docker version: {{ docker_version.stdout }}" + tags: + - docker_config + - debug + + tags: + - docker_config + - block_config diff --git a/ansible/roles/web_app/defaults/main.yml b/ansible/roles/web_app/defaults/main.yml new file mode 100644 index 0000000000..d2956c5874 --- /dev/null +++ b/ansible/roles/web_app/defaults/main.yml @@ -0,0 +1,44 @@ +--- +app_container_name: "devops-app" +app_description: "DevOps Python Application" +app_name: "devops-app" +app_version: "1.0.0" + +app_health_check_path: "/health" +app_health_check_timeout: 60 +app_health_check_interval: 5 + +docker_image: "{{ dockerhub_username }}/{{ app_name }}" +docker_tag: "latest" +app_port: "5000" +docker_restart_policy: "unless-stopped" + +compose_version: "3.8" +compose_project_name: "{{ app_name }}" +compose_project_dir: "/opt/{{ app_name }}" + +service_restart_policy: "unless-stopped" +service_healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:{{ app_port }}/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +network_name: "{{ app_name }}_network" +network_driver: "bridge" + +data_volume: "{{ app_name }}_data" +log_volume: "{{ app_name }}_logs" + +app_environment: + APP_ENV: "production" + APP_DEBUG: "false" + APP_PORT: "{{ app_host_port }}" + APP_NAME: "{{ app_name }}" + +web_app_wipe: false +wipe_remove_volumes: true +wipe_remove_images: "none" +wipe_force_remove_images: false +wipe_timeout: 300 diff --git a/ansible/roles/web_app/handlers/main.yml b/ansible/roles/web_app/handlers/main.yml new file mode 100644 index 0000000000..fab5915e44 --- /dev/null +++ b/ansible/roles/web_app/handlers/main.yml @@ -0,0 +1,16 @@ +--- +- name: Restart app container + docker_container: + name: "{{ app_container_name }}" + state: started + restart: yes + listen: "restart app" + +- name: Show running containers + command: docker ps --format "table {{ "{{" }}.Names{{ "}}" }}\t{{ "{{" }}.Image{{ "}}" }}\t{{ "{{" }}.Status{{ "}}" }}\t{{ "{{" }}.Ports{{ "}}" }}" + register: containers + changed_when: false + +- name: Display container status + debug: + msg: "{{ containers.stdout_lines }}" diff --git a/ansible/roles/web_app/meta/main.yml b/ansible/roles/web_app/meta/main.yml new file mode 100644 index 0000000000..249fa38ecf --- /dev/null +++ b/ansible/roles/web_app/meta/main.yml @@ -0,0 +1,34 @@ +--- +galaxy_info: + author: "scruffyscarf" + description: "Deploy web application using Docker Compose" + license: "MIT" + min_ansible_version: "2.12" + platforms: + - name: Ubuntu + versions: + - focal + - jammy + - noble + galaxy_tags: + - docker + - compose + - web + - deployment + +dependencies: + - role: docker + vars: + docker_users: + - "{{ ansible_user }}" + - "{{ app_user | default('appuser') }}" + docker_compose_install: true + + - role: common + vars: + app_user: "appuser" + common_packages: + - python3-pip + - curl + - git + - htop diff --git a/ansible/roles/web_app/tasks/main.yml b/ansible/roles/web_app/tasks/main.yml new file mode 100644 index 0000000000..4bdcd7f3e3 --- /dev/null +++ b/ansible/roles/web_app/tasks/main.yml @@ -0,0 +1,119 @@ +--- +- name: Include wipe tasks + include_tasks: wipe.yml + when: web_app_wipe | bool + tags: + - web_app_wipe + +- name: Login to Docker Hub + docker_login: + username: "{{ dockerhub_username }}" + password: "{{ dockerhub_password }}" + registry: "https://index.docker.io/v1/" + no_log: true + tags: + - app + - auth + - docker + +- name: Pull Docker image + docker_image: + name: "{{ docker_image }}:{{ docker_image_tag }}" + source: pull + force_source: true + tags: + - app + - pull + - docker + +- name: Check if container exists + docker_container_info: + name: "{{ app_container_name }}" + register: container_info + ignore_errors: true + changed_when: false + tags: + - app + - check + - docker + +- name: Stop existing container if running + docker_container: + name: "{{ app_container_name }}" + state: stopped + when: + - container_info is not failed + - container_info.container.State.Running | default(false) + tags: + - app + - stop + - docker + +- name: Remove old container if exists + docker_container: + name: "{{ app_container_name }}" + state: absent + force_kill: true + when: container_info is not failed + tags: + - app + - remove + - docker + +- name: Create application directory + file: + path: /opt/{{ app_name }} + state: directory + mode: '0755' + tags: + - app + - directories + +- name: Deploy application container + docker_container: + name: "{{ app_container_name }}" + image: "{{ docker_image }}:{{ docker_image_tag }}" + state: started + restart_policy: "{{ docker_restart_policy }}" + ports: + - "{{ app_host_port }}:{{ app_port }}" + env: "{{ app_environment | default({}) }}" + pull: false + tags: + - app + - deploy + - docker + +- name: Wait for application to start + wait_for: + port: "{{ app_host_port }}" + host: "127.0.0.1" + delay: 5 + timeout: "{{ app_health_check_timeout }}" + tags: + - app + - wait + - health + +- name: Check application health endpoint + uri: + url: "http://localhost:{{ app_host_port }}{{ app_health_check_path }}" + method: GET + status_code: 200 + timeout: 10 + register: health_check + until: health_check.status == 200 + retries: 12 + delay: 5 + tags: + - app + - health + - verification + +- name: Display health check result + debug: + msg: "Application is healthy! Response: {{ health_check.json | default('No JSON response') }}" + when: health_check.status == 200 + tags: + - app + - debug diff --git a/ansible/roles/web_app/tasks/wipe.yml b/ansible/roles/web_app/tasks/wipe.yml new file mode 100644 index 0000000000..28211b04a6 --- /dev/null +++ b/ansible/roles/web_app/tasks/wipe.yml @@ -0,0 +1,134 @@ +--- +- name: Check if application exists + stat: + path: "{{ compose_project_dir }}/docker-compose.yml" + register: app_exists + tags: + - web_app_wipe + - check + +- name: Wipe application - Main cleanup block + block: + - name: Stop and remove Docker Compose project + community.docker.docker_compose_v2: + project_src: "{{ compose_project_dir }}" + project_name: "{{ app_name }}" + state: absent + remove_orphans: yes + remove_volumes: "{{ wipe_remove_volumes | default(true) }}" + remove_images: "{{ wipe_remove_images | default('none') }}" + timeout: "{{ wipe_timeout | default(300) }}" + register: compose_removed + when: app_exists.stat.exists + tags: + - web_app_wipe + - docker_compose + - cleanup + + - name: Remove Docker containers by name + docker_container: + name: "{{ item }}" + state: absent + force_kill: yes + loop: + - "{{ app_name }}_{{ app_name }}" + - "{{ app_name }}_web_1" + - "{{ app_name }}_app_1" + ignore_errors: true + when: not app_exists.stat.exists or compose_removed.failed | default(false) + tags: + - web_app_wipe + - docker + - fallback + + - name: Remove Docker volumes by name + docker_volume: + name: "{{ item }}" + state: absent + loop: + - "{{ data_volume }}" + - "{{ log_volume }}" + ignore_errors: true + when: wipe_remove_volumes | default(true) + tags: + - web_app_wipe + - volumes + + - name: Remove Docker network + docker_network: + name: "{{ network_name }}" + state: absent + ignore_errors: true + tags: + - web_app_wipe + - network + + - name: Remove application directory + file: + path: "{{ compose_project_dir }}" + state: absent + force: yes + register: dir_removed + tags: + - web_app_wipe + - filesystem + + - name: Remove Docker images + docker_image: + name: "{{ docker_image }}" + tag: "{{ docker_tag }}" + state: absent + force_absent: "{{ wipe_force_remove_images | default(false) }}" + when: wipe_remove_images | default('none') in ['local', 'all'] + tags: + - web_app_wipe + - images + + - name: Clean up orphaned containers for this app + shell: | + docker ps -a --filter "name={{ app_name }}" --format "{{ '{{.Names}}' }}" | xargs -r docker rm -f + register: orphan_cleanup + changed_when: orphan_cleanup.stdout_lines | length > 0 + ignore_errors: true + tags: + - web_app_wipe + - cleanup + + rescue: + - name: Handle wipe failure + debug: + msg: | + Wipe operation partially failed for {{ app_name }} + + Manual cleanup may be required: + docker stop $(docker ps -a -q --filter "name={{ app_name }}") + docker rm $(docker ps -a -q --filter "name={{ app_name }}") + docker volume rm $(docker volume ls -q --filter "name={{ app_name }}") + rm -rf {{ compose_project_dir }} + tags: + - web_app_wipe + - rescue + + always: + - name: Verify wipe completion + block: + - name: Check for remaining containers + shell: | + docker ps -a --filter "name={{ app_name }}" --format "table {{ '{{.Names}}' }}\t{{ '{{.Status}}' }}" + register: remaining_containers + changed_when: false + failed_when: false + + - name: Check for remaining directories + stat: + path: "{{ compose_project_dir }}" + register: remaining_dir + + tags: + - web_app_wipe + - verification + + tags: + - web_app_wipe + - block_wipe + - "app_name_{{ app_name }}" 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..17845d41f9 --- /dev/null +++ b/ansible/roles/web_app/templates/docker-compose.yml.j2 @@ -0,0 +1,68 @@ +version: '{{ compose_version }}' + +services: + {{ app_name }}: + image: {{ docker_image }}:{{ docker_tag }} + container_name: {{ app_name }}_{{ app_name }} + hostname: {{ app_name }} + + ports: + - "{{ app_port }}:{{ app_internal_port }}" + + environment: + {% for key, value in app_environment.items() %} + - {{ key }}={{ value }} + {% endfor %} + + - APP_SECRET_KEY={{ app_secret_key | default('change_me_in_production') }} + + env_file: + - .env + + volumes: + - {{ data_volume }}:/app/data + - {{ log_volume }}:/app/logs + - ./config:/app/config:ro + + networks: + - {{ network_name }} + + restart: {{ service_restart_policy }} + + healthcheck: + test: {{ service_healthcheck.test | to_json }} + interval: {{ service_healthcheck.interval }} + timeout: {{ service_healthcheck.timeout }} + retries: {{ service_healthcheck.retries }} + start_period: {{ service_healthcheck.start_period }} + + deploy: + resources: + limits: + cpus: '0.50' + memory: 512M + reservations: + cpus: '0.25' + memory: 256M + + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + labels: + - "maintainer=DevOps Team" + - "version={{ app_version }}" + - "description={{ app_description }}" + +networks: + {{ network_name }}: + driver: {{ network_driver }} + name: {{ network_name }} + +volumes: + {{ data_volume }}: + name: {{ data_volume }} + {{ log_volume }}: + name: {{ log_volume }} diff --git a/ansible/vars/app_bonus.yml b/ansible/vars/app_bonus.yml new file mode 100644 index 0000000000..edc5c2c518 --- /dev/null +++ b/ansible/vars/app_bonus.yml @@ -0,0 +1,47 @@ +--- +app_name: devops-go +app_display_name: "DevOps Go Info Service" +app_description: "Go web application with system information" + +docker_image: "{{ dockerhub_username }}/devops-info-service-go" +docker_tag: latest + +app_port: 8001 +app_internal_port: 8080 + +compose_project_dir: "/opt/{{ app_name }}" +compose_project_name: "{{ app_name }}" +network_name: "{{ app_name }}_network" +data_volume: "{{ app_name }}_data" +log_volume: "{{ app_name }}_logs" + +health_check_path: "/health" +health_check_interval: "30s" +health_check_timeout: "10s" +health_check_retries: 3 +health_check_start_period: "40s" + +cpu_limit: "0.25" +memory_limit: "256M" +cpu_reservation: "0.1" +memory_reservation: "128M" + +app_environment: + APP_ENV: "production" + APP_DEBUG: "false" + APP_PORT: "{{ app_internal_port }}" + APP_NAME: "{{ app_name }}" + APP_VERSION: "1.0.0" + GOMAXPROCS: "2" + LOG_LEVEL: "INFO" + +log_driver: "json-file" +log_max_size: "10m" +log_max_file: "3" + +container_labels: + maintainer: "DevOps Team" + version: "1.0.0" + app_type: "golang" + environment: "production" + language: "go" diff --git a/ansible/vars/app_python.yml b/ansible/vars/app_python.yml new file mode 100644 index 0000000000..82320c2cd4 --- /dev/null +++ b/ansible/vars/app_python.yml @@ -0,0 +1,47 @@ +--- +app_name: devops-python +app_display_name: "DevOps Python Info Service" +app_description: "Python Flask web application with system information" + +docker_image: "{{ dockerhub_username }}/devops-info-service" +docker_tag: latest + +app_port: 8000 +app_internal_port: 8000 + +compose_project_dir: "/opt/{{ app_name }}" +compose_project_name: "{{ app_name }}" +network_name: "{{ app_name }}_network" +data_volume: "{{ app_name }}_data" +log_volume: "{{ app_name }}_logs" + +# Health Check Configuration +health_check_path: "/health" +health_check_interval: "30s" +health_check_timeout: "10s" +health_check_retries: 3 +health_check_start_period: "40s" + +cpu_limit: "0.5" +memory_limit: "512M" +cpu_reservation: "0.25" +memory_reservation: "256M" + +app_environment: + APP_ENV: "production" + APP_DEBUG: "false" + APP_PORT: "{{ app_internal_port }}" + APP_NAME: "{{ app_name }}" + APP_VERSION: "1.0.0" + PYTHONUNBUFFERED: "1" + LOG_LEVEL: "INFO" + +log_driver: "json-file" +log_max_size: "10m" +log_max_file: "3" + +container_labels: + maintainer: "DevOps Team" + version: "1.0.0" + app_type: "python" + environment: "production" diff --git a/app_go/.dockerignore b/app_go/.dockerignore new file mode 100644 index 0000000000..0ac7c7ac86 --- /dev/null +++ b/app_go/.dockerignore @@ -0,0 +1,16 @@ +*.exe +*.exe~ +*.test +*.out +go/pkg/mod/cache/ +.env +.env.local +*.log +.DS_Store +Thumbs.db +.vscode/ +.idea/ +*.swp +*.swo +dist/ +build/ diff --git a/app_go/Dockerfile b/app_go/Dockerfile new file mode 100644 index 0000000000..38d3085735 --- /dev/null +++ b/app_go/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:1.22 AS builder +WORKDIR /app +COPY go.mod ./ +RUN go mod download +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ + go build -o info-service +FROM alpine:latest +RUN adduser -D appuser +WORKDIR /app +COPY --from=builder /app/info-service . +USER appuser +EXPOSE 5000 +CMD ["./info-service"] diff --git a/app_go/README.md b/app_go/README.md new file mode 100644 index 0000000000..137459a806 --- /dev/null +++ b/app_go/README.md @@ -0,0 +1,55 @@ +![CI](https://github.com/scruffyscarf/DevOps-Core-Course/actions/workflows/go-ci.yml/badge.svg) + +[![Coverage Status](https://coveralls.io/repos/github/scruffyscarf/DevOps-Core-Course/badge.svg?branch=lab03)](https://coveralls.io/github/scruffyscarf/DevOps-Core-Course?branch=lab03) + +# DevOps Info Service + +## Overview +A simple Go web service that provides system and runtime information. + +## Requirements +- Go 1.22+ + +## Run +```bash +go run main.go +``` + +## API Endpoints +- **GET /** - Service and system information +- **GET /health** - Health check + + +## Docker + +This application can be run inside a Docker container. + +### Build Docker Image + +Use Docker to build the image locally from the Dockerfile: + +```bash +docker build -t info-service . +``` + +### Run Docker Container + +Run the container and expose the application port to the host: + +```bash +docker run -p : info-service +``` + +You can also configure the service using environment variables: + +```bash +docker run -e PORT= -p : info-service +``` + +### Pull Image from Docker Hub + +If the image is published to Docker Hub, it can be pulled directly: + +```bash +docker pull /info-service +``` diff --git a/app_go/docs/GO.md b/app_go/docs/GO.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app_go/docs/LAB01.md b/app_go/docs/LAB01.md new file mode 100644 index 0000000000..6afc596a33 --- /dev/null +++ b/app_go/docs/LAB01.md @@ -0,0 +1,191 @@ +# Lab01 - DevOps Info Service: Web Application Development + + + +## 1. Language Selection - **Go** + +### Why Flask: +- Small binaries +- Fast compilation +- Everywhere in use + +### Comparison Table with Alternatives: +| Language | Description | +|----------|-------------| +| Go | Small binaries, fast compilation, everywhere in use | +| Rust | Memory safety, modern features | +| Java/Spring Boot | Enterprise standard | +| C#/ASP.NET Core | Cross-platform .NET | + + + +## 2. Best Practices Applied + +### Clean Code Organization: +```bash +package main + +import ( + "encoding/json" + "net/http" + "os" + "runtime" + "time" + "fmt" +) + +// Structs +type Service struct { + Name string `json:"name"` + Version string `json:"version"` + Description string `json:"description"` + Framework string `json:"framework"` +} +``` +Need for understanding the code more fast and meet the standards. + +### Git Ignore (.gitignore): +```bash +# Python +__pycache__/ +*.py[cod] +venv/ +*.log + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +``` +Need for keep the large or important files and prevent them from being sent. + + + +## 3. API Documentation + +### Request (Main Endpoint): +```bash +curl http://localhost:5000 | jq +``` + +### Response : +```bash +{ + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "net/http" + }, + "system": { + "hostname": "Scarff.local", + "platform": "darwin", + "platform_version": "unknown", + "architecture": "arm64", + "cpu_count": 8, + "go_version": "go1.24.3" + }, + "runtime": { + "uptime_seconds": 46, + "uptime_human": "0 hour(s), 0 minute(s)", + "current_time": "2026-01-24T12:19:53Z", + "timezone": "UTC" + }, + "request": { + "client_ip": "[::1]:51382", + "user_agent": "curl/8.7.1", + "method": "GET", + "path": "/" + }, + "endpoints": [ + { + "path": "/", + "method": "GET", + "description": "Service information" + }, + { + "path": "/health", + "method": "GET", + "description": "Health check" + } + ] +} +``` + +### Request (Health Check): +```bash +curl http://localhost:5000/health | jq +``` + +### Response : +```bash +{ + "status": "healthy", + "timestamp": "2026-01-24T12:20:39Z", + "uptime_seconds": 91 +} +``` + + + +## 4. Testing Evidence + +### Main Endpoint: +![01-main-endpoint](screenshots/01-main-endpoint.png) + +### Health Check: +![02-health-check](screenshots/02-health-check.png) + +### Formatted Output: +![03-formatted-output](screenshots/03-formatted-output.png) + + + +## 5. GitHub Community + +### Why Stars Matter + +**Discovery & Bookmarking:** +- Stars help you bookmark interesting projects for later reference +- Star count indicates project popularity and community trust +- Starred repos appear in your GitHub profile, showing your interests + +**Open Source Signal:** +- Stars encourage maintainers (shows appreciation) +- High star count attracts more contributors +- Helps projects gain visibility in GitHub search and recommendations + +**Professional Context:** +- Shows you follow best practices and quality projects +- Indicates awareness of industry tools and trends + + +### Why Following Matters + +**Networking:** +- See what other developers are working on +- Discover new projects through their activity +- Build professional connections beyond the classroom + +**Learning:** +- Learn from others' code and commits +- See how experienced developers solve problems +- Get inspiration for your own projects + +**Collaboration:** +- Stay updated on classmates' work +- Easier to find team members for future projects +- Build a supportive learning community + +**Career Growth:** +- Follow thought leaders in your technology stack +- See trending projects in real-time +- Build visibility in the developer community + +**GitHub Best Practices:** +- Star repos you find useful (not spam) +- Follow developers whose work interests you +- Engage meaningfully with the community +- Your GitHub activity shows employers your interests and involvement \ No newline at end of file diff --git a/app_go/docs/LAB02.md b/app_go/docs/LAB02.md new file mode 100644 index 0000000000..fe383b9451 --- /dev/null +++ b/app_go/docs/LAB02.md @@ -0,0 +1,90 @@ +# Lab 2 — Docker Containerization + + + +## Docker Best Practices Applied + +### Non-root User +The application runs inside the container using a non-root user instead of root. Running containers as root increases security risks. If the application is compromised, the attacker would gain elevated privileges inside the container. Using a non-root user limits the potential impact. + +```bash +RUN adduser -D appuser +USER appuser +``` + +### .dockerignore +A `.dockerignore` file is used to exclude unnecessary files from the build context. Excluding files such as virtual environments, caches, and git metadata reduces build context size, speeds up builds, and prevents accidental inclusion of development artifacts. + + + +## Image Information & Decisions + +### Base Image Choice +Base image: `golang:1.22`. + +### Layer Structure Explanation +1. Base Go image +2. Dependency installation layer +3. Application source code layer +4. Runtime configuration (non-root user, startup command) + +### Optimization Choices +- Proper layer ordering +- .dockerignore usage +- Non-root user execution + + + +## Build & Run Process + +### Image Build Output +```bash +docker build -t info-service . +``` +```bash +View build details: docker-desktop://dashboard/build/desktop-linux/desktop-linux/ve0ypxkglj3os1i0cu5naitlc +``` + +### Running Container +```bash +docker run -p 5000:5000 info-service +``` + +### Testing Endpoints + +```bash +curl http://localhost:5000 +``` +```bash +{"service":{"name":"devops-info-service","version":"1.0.0","description":"DevOps course info service","framework":"net/http"},"system":{"hostname":"bd7afeeb7b00","platform":"linux","platform_version":"unknown","architecture":"amd64","cpu_count":8,"go_version":"go1.22.12"},"runtime":{"uptime_seconds":128,"uptime_human":"0 hour(s), 2 minute(s)","current_time":"2026-01-31T19:22:51Z","timezone":"UTC"},"request":{"client_ip":"172.17.0.1:59016","user_agent":"curl/8.7.1","method":"GET","path":"/"},"endpoints":[{"path":"/","method":"GET","description":"Service information"},{"path":"/health","method":"GET","description":"Health check"}]} +``` +```bash +curl http://localhost:5000/health +``` +```bash +{"status":"healthy","timestamp":"2026-01-31T19:22:42Z","uptime_seconds":119} +``` + +### Docker Hub Repository +```bash +https://hub.docker.com/repository/docker/scruffyscarf/info-service/tags/go/sha256:c52e55316458d88f37d8ead47f2dc625ba7b0094a1f728d777f100e352b2fa99 +``` + + + +## Technical Analysis + +### Why the Dockerfile Works This Way + +`Dockerfile` is structured to follow Docker best practices for security, performance, and maintainability by minimizing image size and maximizing cache reuse. + +### Effect of Changing Layer Order +If application files were copied before installing dependencies, Docker would reinstall dependencies on every code change, resulting in slower builds. + +### Security Considerations +- Application runs as a non-root user +- Minimal base image reduces attack surface +- No sensitive or development files included in the image + +### Role of .dockerignore +The `.dockerignore` file prevents unnecessary files from being sent to the Docker daemon, improving build speed and reducing image size. diff --git a/app_go/docs/LAB03.md b/app_go/docs/LAB03.md new file mode 100644 index 0000000000..c751286cdf --- /dev/null +++ b/app_go/docs/LAB03.md @@ -0,0 +1,57 @@ +# Lab 3 — Continuous Integration (CI/CD) + +![CI](https://github.com/scruffyscarf/DevOps-Core-Course/actions/workflows/go-ci.yml/badge.svg) + +[![Coverage Status](https://coveralls.io/repos/github/scruffyscarf/DevOps-Core-Course/badge.svg?branch=lab03)](https://coveralls.io/github/scruffyscarf/DevOps-Core-Course?branch=lab03) + +## Run tests + +```bash +cd app_go +go test ./... +``` + +## Result + +```bash +ok info-service 0.867s +``` + +## Versioning Strategy +**Semantic Versioning (SemVer)** was chosen: +- It is more informative about the compatibility of changes +- It's easier to track dependencies between versions +- Standard practice in the Docker ecosystem + +[Docker Image](https://hub.docker.com/r/scruffyscarf/info-service-go/tags) + +## CI Workflow Triggers +The Go CI workflow runs on: +- `push` to `master`, `lab01`, `lab02`, `lab03` +- only when files in `app_go/**` change +- `pull_request` affecting `app_go/**` + +## Versioning Strategy +**Calendar Versioning (CalVer)** was chosen: +- Aligns well with continuous delivery +- Simple and predictable +- Avoids manual semantic version bumps + +## CI Best Practices + +- **Dependency caching** - speeds up CI by reusing installed Python packages. + +- **Path-based triggers** - prevents unnecessary workflow runs in monorepo setup. + +- **Fail-fast testing** - coverage threshold prevents merging poorly tested code. + +- **Separate jobs** - improves clarity and parallel execution. + +## Caching Results +- First run: ~1 minute +- Cached run: 23 seconds + +### Snyk Security Scan +- Scans both runtime and dev dependencies +- No critical vulnerabilities found +- Workflow fails automatically on detected issues diff --git a/app_go/docs/screenshots/01-main-endpoint.png b/app_go/docs/screenshots/01-main-endpoint.png new file mode 100644 index 0000000000..302afbd8a2 Binary files /dev/null and b/app_go/docs/screenshots/01-main-endpoint.png differ diff --git a/app_go/docs/screenshots/02-health-check.png b/app_go/docs/screenshots/02-health-check.png new file mode 100644 index 0000000000..255d0fe0c1 Binary files /dev/null and b/app_go/docs/screenshots/02-health-check.png differ diff --git a/app_go/docs/screenshots/03-formatted-output.png b/app_go/docs/screenshots/03-formatted-output.png new file mode 100644 index 0000000000..c6d8defb59 Binary files /dev/null and b/app_go/docs/screenshots/03-formatted-output.png differ diff --git a/app_go/go.mod b/app_go/go.mod new file mode 100644 index 0000000000..687ec0ec1c --- /dev/null +++ b/app_go/go.mod @@ -0,0 +1,3 @@ +module info-service + +go 1.22 \ No newline at end of file diff --git a/app_go/health_endpoint_test.go b/app_go/health_endpoint_test.go new file mode 100644 index 0000000000..b8ae7d1cc7 --- /dev/null +++ b/app_go/health_endpoint_test.go @@ -0,0 +1,39 @@ +package main + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestHealthEndpoint(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/health", nil) + w := httptest.NewRecorder() + + healthHandler(w, req) + + res := w.Result() + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + t.Fatalf("expected status 200, got %d", res.StatusCode) + } + + var data map[string]interface{} + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + t.Fatalf("failed to decode json: %v", err) + } + + if data["status"] != "healthy" { + t.Fatalf("expected status 'healthy', got %v", data["status"]) + } + + if _, ok := data["timestamp"]; !ok { + t.Fatal("missing 'timestamp'") + } + + if _, ok := data["uptime_seconds"]; !ok { + t.Fatal("missing 'uptime_seconds'") + } +} diff --git a/app_go/info-service b/app_go/info-service new file mode 100755 index 0000000000..6b0f53e101 Binary files /dev/null and b/app_go/info-service differ diff --git a/app_go/main.go b/app_go/main.go new file mode 100644 index 0000000000..4fd89964b3 --- /dev/null +++ b/app_go/main.go @@ -0,0 +1,142 @@ +package main + +import ( + "encoding/json" + "net/http" + "os" + "runtime" + "time" + "fmt" +) + +// Structs +type Service struct { + Name string `json:"name"` + Version string `json:"version"` + Description string `json:"description"` + Framework string `json:"framework"` +} + +type System struct { + Hostname string `json:"hostname"` + Platform string `json:"platform"` + PlatformVersion string `json:"platform_version"` + Architecture string `json:"architecture"` + CPUCount int `json:"cpu_count"` + GoVersion string `json:"go_version"` +} + +type RuntimeInfo struct { + UptimeSeconds int64 `json:"uptime_seconds"` + UptimeHuman string `json:"uptime_human"` + CurrentTime string `json:"current_time"` + Timezone string `json:"timezone"` +} + +type RequestInfo struct { + ClientIP string `json:"client_ip"` + UserAgent string `json:"user_agent"` + Method string `json:"method"` + Path string `json:"path"` +} + +type ServiceInfo struct { + Service Service `json:"service"` + System System `json:"system"` + Runtime RuntimeInfo `json:"runtime"` + Request RequestInfo `json:"request"` + Endpoints []Endpoint `json:"endpoints"` +} + +type Endpoint struct { + Path string `json:"path"` + Method string `json:"method"` + Description string `json:"description"` +} + +type HealthResponse struct { + Status string `json:"status"` + Timestamp string `json:"timestamp"` + UptimeSeconds int64 `json:"uptime_seconds"` +} + +// Global start time +var startTime = time.Now() + +// Helpers +func uptime() (int64, string) { + + seconds := int64(time.Since(startTime).Seconds()) + hours := seconds / 3600 + minutes := (seconds % 3600) / 60 + uptimeHuman := fmt.Sprintf("%d hour(s), %d minute(s)", hours, minutes) + + return seconds, uptimeHuman +} + +// Handlers +func mainHandler(w http.ResponseWriter, r *http.Request) { + uptimeSeconds, uptimeHuman := uptime() + hostname, _ := os.Hostname() + + response := ServiceInfo{ + Service: Service{ + Name: "info-service", + Version: "1.0.0", + Description: "DevOps course info service", + Framework: "net/http", + }, + System: System{ + Hostname: hostname, + Platform: runtime.GOOS, + PlatformVersion: "unknown", + Architecture: runtime.GOARCH, + CPUCount: runtime.NumCPU(), + GoVersion: runtime.Version(), + }, + Runtime: RuntimeInfo{ + UptimeSeconds: uptimeSeconds, + UptimeHuman: uptimeHuman, + CurrentTime: time.Now().UTC().Format(time.RFC3339), + Timezone: "UTC", + }, + Request: RequestInfo{ + ClientIP: r.RemoteAddr, + UserAgent: r.UserAgent(), + Method: r.Method, + Path: r.URL.Path, + }, + Endpoints: []Endpoint{ + {Path: "/", Method: "GET", Description: "Service information"}, + {Path: "/health", Method: "GET", Description: "Health check"}, + }, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func healthHandler(w http.ResponseWriter, r *http.Request) { + uptimeSeconds, _ := uptime() + + response := HealthResponse{ + Status: "healthy", + Timestamp: time.Now().UTC().Format(time.RFC3339), + UptimeSeconds: uptimeSeconds, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +// Main +func main() { + port := os.Getenv("PORT") + if port == "" { + port = "5000" + } + + http.HandleFunc("/", mainHandler) + http.HandleFunc("/health", healthHandler) + http.ListenAndServe(":"+port, nil) +} diff --git a/app_go/main_endpoint_test.go b/app_go/main_endpoint_test.go new file mode 100644 index 0000000000..95bb8d1cff --- /dev/null +++ b/app_go/main_endpoint_test.go @@ -0,0 +1,48 @@ +package main + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestMainEndpoint(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + w := httptest.NewRecorder() + + mainHandler(w, req) + + res := w.Result() + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + t.Fatalf("expected status 200, got %d", res.StatusCode) + } + + var data map[string]interface{} + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + t.Fatalf("failed to decode json: %v", err) + } + + if _, ok := data["service"]; !ok { + t.Fatal("missing 'service' field") + } + if _, ok := data["system"]; !ok { + t.Fatal("missing 'system' field") + } + if _, ok := data["runtime"]; !ok { + t.Fatal("missing 'runtime' field") + } + if _, ok := data["request"]; !ok { + t.Fatal("missing 'request' field") + } + if _, ok := data["endpoints"]; !ok { + t.Fatal("missing 'endpoints' field") + } + + service := data["service"].(map[string]interface{}) + if service["name"] != "info-service" { + t.Fatalf("expected service name 'info-service', got %v", service["name"]) + } +} diff --git a/app_python/.dockerignore b/app_python/.dockerignore new file mode 100644 index 0000000000..133453ae05 --- /dev/null +++ b/app_python/.dockerignore @@ -0,0 +1,11 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +venv/ +.env +.git +.gitignore +docs/ +tests/ +*.log diff --git a/app_python/.gitignore b/app_python/.gitignore new file mode 100644 index 0000000000..ba1db0ed69 --- /dev/null +++ b/app_python/.gitignore @@ -0,0 +1,12 @@ +# Python +__pycache__/ +*.py[cod] +venv/ +*.log + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store diff --git a/app_python/Dockerfile b/app_python/Dockerfile new file mode 100644 index 0000000000..f895070918 --- /dev/null +++ b/app_python/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.13-slim +RUN useradd -m devopsuser +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +USER devopsuser +EXPOSE 5000 +CMD ["python", "app.py"] diff --git a/app_python/README.md b/app_python/README.md new file mode 100644 index 0000000000..8abff54eb0 --- /dev/null +++ b/app_python/README.md @@ -0,0 +1,65 @@ +![CI](https://github.com/scruffyscarf/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg) + +[![Coverage Status](https://coveralls.io/repos/github/scruffyscarf/DevOps-Core-Course/badge.svg?branch=lab03)](https://coveralls.io/github/scruffyscarf/DevOps-Core-Course?branch=lab03) + +# DevOps Info Service + +## Overview +A simple Python web service that provides system and runtime information. + +## Prerequisites +- Python 3.11+ +- Flask==3.1.0 + +## Installation +```bash +python -m venv venv +source venv/bin/activate +pip install -r requirements.txt +pip install -r requirements-dev.txt +``` + +## Running the Application +```bash +python app.py +# Or with custom config +PORT=8080 python app.py +``` + +## API Endpoints +- **GET /** - Service and system information +- **GET /health** - Health check + +## Docker + +This application can be run inside a Docker container. + +### Build Docker Image + +Use Docker to build the image locally from the Dockerfile: + +```bash +docker build -t info-service . +``` + +### Run Docker Container + +Run the container and expose the application port to the host: + +```bash +docker run -p : info-service +``` + +You can also configure the service using environment variables: + +```bash +docker run -e PORT= -p : info-service +``` + +### Pull Image from Docker Hub + +If the image is published to Docker Hub, it can be pulled directly: + +```bash +docker pull /info-service +``` diff --git a/app_python/app.py b/app_python/app.py new file mode 100644 index 0000000000..57e86267d0 --- /dev/null +++ b/app_python/app.py @@ -0,0 +1,96 @@ +""" +DevOps Info Service +Main application module +""" +import os +import platform +import socket +import time +import logging +from datetime import datetime, timezone +from flask import Flask, jsonify, request + +# Configuration +HOST = os.getenv("HOST", "0.0.0.0") +PORT = int(os.getenv("PORT", 5000)) +DEBUG = os.getenv("DEBUG", "False").lower() == "true" + +# Application start time +app = Flask(__name__) +start_time = time.time() + +# Logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s" +) + +# Helpers +def get_uptime(): + uptime_seconds = int(time.time() - start_time) + hours = uptime_seconds // 3600 + minutes = (uptime_seconds % 3600) // 60 + return uptime_seconds, f"{hours} hour(s), {minutes} minute(s)" + +# Routes +@app.route("/", methods=["GET"]) +def main_info(): + """Main endpoint - service and system information.""" + # Implementation + uptime_seconds, uptime_human = get_uptime() + + response = { + "service": { + "name": "info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Flask" + }, + "system": { + "hostname": socket.gethostname(), + "platform": platform.system(), + "platform_version": platform.version(), + "architecture": platform.machine(), + "cpu_count": os.cpu_count(), + "python_version": platform.python_version() + }, + "runtime": { + "uptime_seconds": uptime_seconds, + "uptime_human": uptime_human, + "current_time": datetime.now(timezone.utc).isoformat(), + "timezone": "UTC" + }, + "request": { + "client_ip": request.remote_addr, + "user_agent": request.headers.get("User-Agent"), + "method": request.method, + "path": request.path + }, + "endpoints": [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"} + ] + } + + logging.info("Main endpoint accessed") + return jsonify(response) + + +@app.route("/health", methods=["GET"]) +def health_check(): + """Health endpoint - available server information.""" + # Implementation + uptime_seconds, _ = get_uptime() + + response = { + "status": "healthy", + "timestamp": datetime.now(timezone.utc).isoformat(), + "uptime_seconds": uptime_seconds + } + + logging.info("Health check accessed") + return jsonify(response), 200 + +# Run +if __name__ == "__main__": + app.run(host=HOST, port=PORT, debug=DEBUG) diff --git a/app_python/docs/LAB01.md b/app_python/docs/LAB01.md new file mode 100644 index 0000000000..306954cfec --- /dev/null +++ b/app_python/docs/LAB01.md @@ -0,0 +1,191 @@ +# Lab01 - DevOps Info Service: Web Application Development + + + +## 1. Framework Selection - **Flask** + +### Why Flask: +- Lightweight +- Easy to learn +- Everywhere in use + +### Comparison Table with Alternatives: +| Framework | Description | +|---------|-------------| +| Flask | Lightweight, easy to learn | +| FastAPI | Modern, async, auto-documentation | +| Django | Full-featured, includes ORM | + + + +## 2. Best Practices Applied + +### Clean Code Organization: +```bash +""" +DevOps Info Service +Main application module +""" +import os +import platform +import socket +import time +import logging +from datetime import datetime, timezone +from flask import Flask, jsonify, request + +# Configuration +HOST = os.getenv("HOST", "0.0.0.0") +PORT = int(os.getenv("PORT", 5000)) +DEBUG = os.getenv("DEBUG", "False").lower() == "true" + +# Application start time +app = Flask(__name__) +start_time = time.time() +``` +Need for understanding the code more fast and meet the standards. + +### Logging: +```bash +# Logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s" +) +``` +Need for understand how the code will behave. + +### Dependencies (requirements.txt): +```bash +Flask==3.1.0 +``` +Need for correct execution of the code. + + + +## 3. API Documentation + +### Request (Main Endpoint): +```bash +curl http://localhost:5000 | jq +``` + +### Response : +```bash +{ + "endpoints": [ + { + "description": "Service information", + "method": "GET", + "path": "/" + }, + { + "description": "Health check", + "method": "GET", + "path": "/health" + } + ], + "request": { + "client_ip": "127.0.0.1", + "method": "GET", + "path": "/", + "user_agent": "curl/8.7.1" + }, + "runtime": { + "current_time": "2026-01-24T12:53:03.550450+00:00", + "timezone": "UTC", + "uptime_human": "0 hour(s), 0 minute(s)", + "uptime_seconds": 11 + }, + "service": { + "description": "DevOps course info service", + "framework": "Flask", + "name": "devops-info-service", + "version": "1.0.0" + }, + "system": { + "architecture": "arm64", + "cpu_count": 8, + "hostname": "Scarff.local", + "platform": "Darwin", + "platform_version": "Darwin Kernel Version 25.2.0: Tue Nov 18 21:09:55 PST 2025; root:xnu-12377.61.12~1/RELEASE_ARM64_T8103", + "python_version": "3.14.0" + } +} +``` + +### Request (Health Check): +```bash +curl http://localhost:5000/health | jq +``` + +### Response : +```bash +{ + "status": "healthy", + "timestamp": "2026-01-24T12:53:13.304962+00:00", + "uptime_seconds": 20 +} +``` + + + +## 4. Testing Evidence + +### Main Endpoint: +![01-main-endpoint](screenshots/01-main-endpoint.png) + +### Health Check: +![02-health-check](screenshots/02-health-check.png) + +### Formatted Output: +![03-formatted-output](screenshots/03-formatted-output.png) + + + +## 5. GitHub Community + +### Why Stars Matter + +**Discovery & Bookmarking:** +- Stars help you bookmark interesting projects for later reference +- Star count indicates project popularity and community trust +- Starred repos appear in your GitHub profile, showing your interests + +**Open Source Signal:** +- Stars encourage maintainers (shows appreciation) +- High star count attracts more contributors +- Helps projects gain visibility in GitHub search and recommendations + +**Professional Context:** +- Shows you follow best practices and quality projects +- Indicates awareness of industry tools and trends + + +### Why Following Matters + +**Networking:** +- See what other developers are working on +- Discover new projects through their activity +- Build professional connections beyond the classroom + +**Learning:** +- Learn from others' code and commits +- See how experienced developers solve problems +- Get inspiration for your own projects + +**Collaboration:** +- Stay updated on classmates' work +- Easier to find team members for future projects +- Build a supportive learning community + +**Career Growth:** +- Follow thought leaders in your technology stack +- See trending projects in real-time +- Build visibility in the developer community + +**GitHub Best Practices:** +- Star repos you find useful (not spam) +- Follow developers whose work interests you +- Engage meaningfully with the community +- Your GitHub activity shows employers your interests and involvement \ No newline at end of file diff --git a/app_python/docs/LAB02.md b/app_python/docs/LAB02.md new file mode 100644 index 0000000000..6334708633 --- /dev/null +++ b/app_python/docs/LAB02.md @@ -0,0 +1,120 @@ +# Lab 2 — Docker Containerization + + + +## Docker Best Practices Applied + +### Non-root User +The application runs inside the container using a non-root user instead of root. Running containers as root increases security risks. If the application is compromised, the attacker would gain elevated privileges inside the container. Using a non-root user limits the potential impact. + +```bash +RUN adduser devopsuser +USER devopsuser +``` + +### Layer Caching + +Dependencies are installed before copying the application source code. Docker caches image layers. When only application code changes, dependency layers are reused, which significantly speeds up rebuilds. + +```bash +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +``` + +### Minimal Base Image + +A slim Python base image is used. Slim images reduce image size, build time, and attack surface while still providing the required runtime environment. + +```bash +FROM python:3.13-slim +``` + +### .dockerignore +A `.dockerignore` file is used to exclude unnecessary files from the build context. Excluding files such as virtual environments, caches, and git metadata reduces build context size, speeds up builds, and prevents accidental inclusion of development artifacts. + + + +## Image Information & Decisions + +### Base Image Choice +Base image: `python:3.13-slim`. The image matches the Python version used locally, is smaller than full Python images, and is officially maintained. + +### Final Image Size +The final image size is significantly smaller than a full Python image due to the use of a slim base image and exclusion of unnecessary files. This size is appropriate for production use. + +### Layer Structure Explanation +1. Base Python image +2. Dependency installation layer +3. Application source code layer +4. Runtime configuration (non-root user, startup command) + +### Optimization Choices +- Slim base image +- Proper layer ordering +- .dockerignore usage +- Non-root user execution + + + +## Build & Run Process + +### Image Build Output +```bash +docker build -t info-service . +``` +```bash +View build details: docker-desktop://dashboard/build/desktop-linux/desktop-linux/ve0ypxkglj3os1i0cu5naitlc +``` + +### Running Container +```bash +docker run -p 5000:5000 info-service +``` +```bash +* Serving Flask app 'app' + * Debug mode: off +2026-01-31 18:28:21,077 [INFO] WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:5000 + * Running on http://172.17.0.2:5000 +2026-01-31 18:28:21,077 [INFO] Press CTRL+C to quit +``` + +### Testing Endpoints + +```bash +curl http://localhost:5000 +``` +```bash +{"service":{"name":"devops-info-service","version":"1.0.0","description":"DevOps course info service","framework":"net/http"},"system":{"hostname":"bd7afeeb7b00","platform":"linux","platform_version":"unknown","architecture":"amd64","cpu_count":8,"python_version":"python3.14"},"runtime":{"uptime_seconds":128,"uptime_human":"0 hour(s), 2 minute(s)","current_time":"2026-01-31T19:22:51Z","timezone":"UTC"},"request":{"client_ip":"172.17.0.1:59016","user_agent":"curl/8.7.1","method":"GET","path":"/"},"endpoints":[{"path":"/","method":"GET","description":"Service information"},{"path":"/health","method":"GET","description":"Health check"}]} +``` +```bash +curl http://localhost:5000/health +``` +```bash +{"status":"healthy","timestamp":"2026-01-31T19:22:42Z","uptime_seconds":119} +``` + +### Docker Hub Repository +```bash +https://hub.docker.com/repository/docker/scruffyscarf/info-service/tags/first/sha256-98ddc0a8908218f935dd65fd91618e41983c861c7ffa2b5fb96c2309c5f206b2 +``` + + + +## Technical Analysis + +### Why the Dockerfile Works This Way + +`Dockerfile` is structured to follow Docker best practices for security, performance, and maintainability by minimizing image size and maximizing cache reuse. + +### Effect of Changing Layer Order +If application files were copied before installing dependencies, Docker would reinstall dependencies on every code change, resulting in slower builds. + +### Security Considerations +- Application runs as a non-root user +- Minimal base image reduces attack surface +- No sensitive or development files included in the image + +### Role of .dockerignore +The `.dockerignore` file prevents unnecessary files from being sent to the Docker daemon, improving build speed and reducing image size. \ No newline at end of file diff --git a/app_python/docs/LAB03.md b/app_python/docs/LAB03.md new file mode 100644 index 0000000000..75bb8847be --- /dev/null +++ b/app_python/docs/LAB03.md @@ -0,0 +1,81 @@ +# Lab 3 — Continuous Integration (CI/CD) + +![CI](https://github.com/scruffyscarf/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg) + +[![Coverage Status](https://coveralls.io/repos/github/scruffyscarf/DevOps-Core-Course/badge.svg?branch=lab03)](https://coveralls.io/github/scruffyscarf/DevOps-Core-Course?branch=lab03) + +## Testing Framework +**pytest** was chosen because: +- Simple and readable syntax +- Powerful fixtures +- Industry standard for Python projects +- Excellent CI integration + +### Test Structure +Covered endpoints: +- `GET /` — response structure and required fields +- `GET /health` — service health status + +## Run tests + +```bash +cd app_python +pip install -r requirements.txt +pip install -r requirements-dev.txt +pytest +coverage run -m pytest +coverage report +``` + +## Result + +```bash +======================================================================= test session starts ======================================================================= +platform darwin -- Python 3.14.2, pytest-9.0.2, pluggy-1.6.0 +rootdir: /Users/scruffyscarf/DevOps-Core-Course/app_python +collected 2 items + +tests/test_health_endpoint.py . [ 50%] +tests/test_main_endpoint.py . [100%] + +======================================================================== 2 passed in 0.15s ======================================================================== +``` + +## Versioning Strategy +**Semantic Versioning (SemVer)** was chosen: +- It is more informative about the compatibility of changes +- It's easier to track dependencies between versions +- Standard practice in the Docker ecosystem + +[Docker Image](https://hub.docker.com/r/scruffyscarf/info-service-python/tags) + +## CI Workflow Triggers +The Python CI workflow runs on: +- `push` to `master`, `lab01`, `lab02`, `lab03` +- only when files in `app_python/**` change +- `pull_request` affecting `app_python/**` + +## Versioning Strategy +**Calendar Versioning (CalVer)** was chosen: +- Aligns well with continuous delivery +- Simple and predictable +- Avoids manual semantic version bumps + +## CI Best Practices + +- **Dependency caching** - speeds up CI by reusing installed Python packages. + +- **Path-based triggers** - prevents unnecessary workflow runs in monorepo setup. + +- **Fail-fast testing** - coverage threshold prevents merging poorly tested code. + +- **Separate jobs** - improves clarity and parallel execution. + +## Caching Results +- First run: ~1 minute +- Cached run: 23 seconds + +### Snyk Security Scan +- Scans both runtime and dev dependencies +- No critical vulnerabilities found +- Workflow fails automatically on detected issues diff --git a/app_python/docs/screenshots/01-main-endpoint.png b/app_python/docs/screenshots/01-main-endpoint.png new file mode 100644 index 0000000000..72277dec88 Binary files /dev/null and b/app_python/docs/screenshots/01-main-endpoint.png differ diff --git a/app_python/docs/screenshots/02-health-check.png b/app_python/docs/screenshots/02-health-check.png new file mode 100644 index 0000000000..671cf007ce Binary files /dev/null and b/app_python/docs/screenshots/02-health-check.png differ diff --git a/app_python/docs/screenshots/03-formatted-output.png b/app_python/docs/screenshots/03-formatted-output.png new file mode 100644 index 0000000000..85c7913de8 Binary files /dev/null and b/app_python/docs/screenshots/03-formatted-output.png differ diff --git a/app_python/requirements-dev.txt b/app_python/requirements-dev.txt new file mode 100644 index 0000000000..45dac38474 --- /dev/null +++ b/app_python/requirements-dev.txt @@ -0,0 +1,3 @@ +pytest==8.0.0 +pytest-cov +coveralls diff --git a/app_python/requirements.txt b/app_python/requirements.txt new file mode 100644 index 0000000000..22ac75b399 --- /dev/null +++ b/app_python/requirements.txt @@ -0,0 +1 @@ +Flask==3.1.0 diff --git a/app_python/tests/__init__.py b/app_python/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app_python/tests/test_health_endpoint.py b/app_python/tests/test_health_endpoint.py new file mode 100644 index 0000000000..d297a8ec9e --- /dev/null +++ b/app_python/tests/test_health_endpoint.py @@ -0,0 +1,13 @@ +from app import app + +def test_health_endpoint(): + client = app.test_client() + response = client.get("/health") + + assert response.status_code == 200 + + data = response.get_json() + + assert data["status"] == "healthy" + assert "timestamp" in data + assert "uptime_seconds" in data diff --git a/app_python/tests/test_main_endpoint.py b/app_python/tests/test_main_endpoint.py new file mode 100644 index 0000000000..35949665ec --- /dev/null +++ b/app_python/tests/test_main_endpoint.py @@ -0,0 +1,18 @@ +import json +from app import app + +def test_main_endpoint(): + client = app.test_client() + response = client.get("/") + + assert response.status_code == 200 + + data = response.get_json() + + assert "service" in data + assert "system" in data + assert "runtime" in data + assert "request" in data + assert "endpoints" in data + + assert data["service"]["name"] == "info-service" diff --git a/pulumi/.gitignore b/pulumi/.gitignore new file mode 100644 index 0000000000..3fe9664377 --- /dev/null +++ b/pulumi/.gitignore @@ -0,0 +1,6 @@ +*.pyc +__pycache__/ +venv/ +pulumi/venv/ +Pulumi.*.yaml +key.json diff --git a/pulumi/Pulumi.yaml b/pulumi/Pulumi.yaml new file mode 100644 index 0000000000..42162f6ff0 --- /dev/null +++ b/pulumi/Pulumi.yaml @@ -0,0 +1,11 @@ +name: info-service +description: A simple Python web service that provides system and runtime information. +runtime: + name: python + options: + toolchain: pip + virtualenv: venv +config: + pulumi:tags: + value: + pulumi:template: python diff --git a/pulumi/__main__.py b/pulumi/__main__.py new file mode 100644 index 0000000000..a83a656468 --- /dev/null +++ b/pulumi/__main__.py @@ -0,0 +1,123 @@ +"""A Python Pulumi program""" + +import pulumi +import pulumi_yandex as yc + +# ====================== +# CONFIG +# ====================== + +config = pulumi.Config() + +cloud_id = config.require("cloud_id") +folder_id = config.require("folder_id") +ssh_public_key = config.require("ssh_public_key") +zone = "ru-central1-a" +ssh_cidr = "192.145.30.13/32" +vm_name = "info-service-vm" + +# ====================== +# PROVIDER +# ====================== + +provider = yc.Provider( + "yc", + cloud_id=cloud_id, + folder_id=folder_id, + zone=zone, + service_account_key_file=config.get("serviceAccountKeyFile"), +) + +# ====================== +# NETWORK +# ====================== + +network = yc.VpcNetwork( + "info-service-network", + name="info-service-network", + opts=pulumi.ResourceOptions(provider=provider), +) + +subnet = yc.VpcSubnet( + "info-service-subnet", + name="info-service-subnet", + zone=zone, + network_id=network.id, + v4_cidr_blocks=["10.0.0.0/24"], + opts=pulumi.ResourceOptions(provider=provider), +) + +# ====================== +# SECURITY GROUP +# ====================== + +security_group = yc.VpcSecurityGroup( + "info-service-security-group", + name="info-service-security-group", + network_id=network.id, + ingresses=[ + yc.VpcSecurityGroupIngressArgs( + protocol="TCP", + port=22, + v4_cidr_blocks=[ssh_cidr], + ), + yc.VpcSecurityGroupIngressArgs( + protocol="TCP", + port=80, + v4_cidr_blocks=["0.0.0.0/0"], + ), + yc.VpcSecurityGroupIngressArgs( + protocol="TCP", + port=5000, + v4_cidr_blocks=["0.0.0.0/0"], + ), + ], + egresses=[ + yc.VpcSecurityGroupEgressArgs( + protocol="ANY", + v4_cidr_blocks=["0.0.0.0/0"], + ) + ], + opts=pulumi.ResourceOptions(provider=provider), +) + +# ====================== +# COMPUTE INSTANCE +# ====================== + +instance = yc.ComputeInstance( + "info-service-vm", + name=vm_name, + zone=zone, + resources=yc.ComputeInstanceResourcesArgs( + cores=2, + memory=1, + core_fraction=20, + ), + boot_disk=yc.ComputeInstanceBootDiskArgs( + initialize_params=yc.ComputeInstanceBootDiskInitializeParamsArgs( + image_id="fd8i5gvlr8t2tcesgf2g", # Ubuntu 22.04 + size=10, + ) + ), + network_interfaces=[ + yc.ComputeInstanceNetworkInterfaceArgs( + subnet_id=subnet.id, + nat=True, + security_group_ids=[security_group.id], + ) + ], + metadata={ + "ssh-keys": f"ubuntu:{ssh_public_key}" + }, + opts=pulumi.ResourceOptions(provider=provider), +) + +# ====================== +# OUTPUTS +# ====================== + +pulumi.export( + "public_ip", + instance.network_interfaces[0].nat_ip_address, +) diff --git a/pulumi/docs/LAB04.md b/pulumi/docs/LAB04.md new file mode 100644 index 0000000000..c33a88fc37 --- /dev/null +++ b/pulumi/docs/LAB04.md @@ -0,0 +1,102 @@ +# Lab 4 — Infrastructure as Code (Terraform & Pulumi) + +## Programming language - Python + +- **Clarity** — easy-managed +- **Simplicity** — straightforward syntax +- **Speed** — fast start + +## pulumi version + +v3.219.0 + +## pulumi preview + +```bash + Type Name Plan Info + + pulumi:pulumi:Stack info-service-dev create 2 messages + + ├─ pulumi:providers:yandex yc create + + ├─ yandex:index:VpcNetwork info-service-network create + + ├─ yandex:index:VpcSubnet info-service-subnet create + + ├─ yandex:index:VpcSecurityGroup info-service-security-group create + + └─ yandex:index:ComputeInstance info-service-vm create +``` + +## pulumi up + +```bash + Type Name Status Info + + pulumi:pulumi:Stack info-service-dev created (49s) 2 messages + + ├─ pulumi:providers:yandex yc created (0.24s) + + ├─ yandex:index:VpcNetwork info-service-network created (3s) + + ├─ yandex:index:VpcSubnet info-service-subnet created (0.76s) + + ├─ yandex:index:VpcSecurityGroup info-service-security-group created (1s) + + └─ yandex:index:ComputeInstance info-service-vm created (40s) + +Outputs: + public_ip: "89.169.*.*" +``` + +## VM Public IP address + +`89.169.*.*` + +## SSH connection + +`ssh ubuntu@89.169.*.*` +```bash +The authenticity of host '89.169.*.* (89.169.*.*)' can't be established. +ED25519 key fingerprint is SHA256:0M8.... + +ubuntu@fhm...:~$ +``` + +## Resources + +- info-service-dev +- info-service-vm +- info-service-subnet +- info-service-security-group +- info-service-network +- yc + +## pulumi destroy + +```bash + Type Name Status + - pulumi:pulumi:Stack info-service-dev deleted (0.31s) + - ├─ yandex:index:ComputeInstance info-service-vm deleted (31s) + - ├─ yandex:index:VpcSubnet info-service-subnet deleted (6s) + - ├─ yandex:index:VpcSecurityGroup info-service-security-group deleted (1s) + - ├─ yandex:index:VpcNetwork info-service-network deleted (1s) + - └─ pulumi:providers:yandex yc deleted (0.53s) + +Outputs: + - public_ip: "89.169.*.*" + +The resources in the stack have been deleted, but the history and configuration associated with the stack are still maintained. +If you want to remove the stack completely, run `pulumi stack rm dev`. +``` + +## Terraform vs Pulumi + +Advantages of HCL: + +- Declarative syntax +- A single standard for all providers +- Good readability for infrastructure +- Extensive community and documentation + +Advantages of Pulumi: + +- A full-fledged programming language +- Typing and auto-completion in the IDE +- The ability to use functions, loops, classes +- It's easier to reuse the code + +## Preferable tool - Terraform + +- Super-stable infrastructures +- Collaboration with external teams +- Enterprise features +- Stability diff --git a/pulumi/requirements.txt b/pulumi/requirements.txt new file mode 100644 index 0000000000..9663af4ec3 --- /dev/null +++ b/pulumi/requirements.txt @@ -0,0 +1,2 @@ +pulumi>=3.0.0,<4.0.0 +pulumi-yandex>=0.9.0 diff --git a/terraform/.gitignore b/terraform/.gitignore new file mode 100644 index 0000000000..910c74b12f --- /dev/null +++ b/terraform/.gitignore @@ -0,0 +1,6 @@ +*.tfstate +*.tfstate.* +.terraform* +.terraform/ +terraform.tfvars +key.json diff --git a/terraform/docs/LAB04.md b/terraform/docs/LAB04.md new file mode 100644 index 0000000000..dab7bbde88 --- /dev/null +++ b/terraform/docs/LAB04.md @@ -0,0 +1,188 @@ +# Lab 4 — Infrastructure as Code (Terraform & Pulumi) + + + +## Cloud Provider - Yandex Cloud + +- **Accessibility** — no problems with access and payment +- **Tariff** — 4000 rubles from start +- **Good documentation** — simplifies learning + +## terraform version + +Terraform v1.14.4 + +## terraform init + +```bash +Initializing the backend... +Initializing provider plugins... +- Finding latest version of yandex-cloud/yandex... +- Installing yandex-cloud/yandex v0.184.0... +- Installed yandex-cloud/yandex v0.184.0 (self-signed, key ID E40...) +Partner and community providers are signed by their developers. +If you'd like to know more about provider signing, you can read about it here: +https://developer.hashicorp.com/terraform/cli/plugins/signing +Terraform has created a lock file .terraform.lock.hcl to record the provider +selections it made above. Include this file in your version control repository +so that Terraform can guarantee to make the same selections by default when +you run "terraform init" in the future. + +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 plan + +```bash +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 will be created + + resource "yandex_compute_instance" "vm" { + + 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 +... +``` + +## terraform apply + +```bash +yandex_vpc_network.this: Creating... +yandex_vpc_network.this: Creation complete after 2s [id=enpkn4h3jfsactmlsjbe] +yandex_vpc_subnet.this: Creating... +yandex_vpc_security_group.this: Creating... +yandex_vpc_subnet.this: Creation complete after 1s [id=e9bn6kss7d6fond1rlpp] +yandex_vpc_security_group.this: Creation complete after 2s [id=enp80v7buefldcri6bhg] +yandex_compute_instance.vm: Creating... +yandex_compute_instance.vm: Still creating... [10s elapsed] +yandex_compute_instance.vm: Still creating... [20s elapsed] +yandex_compute_instance.vm: Still creating... [30s elapsed] +yandex_compute_instance.vm: Still creating... [40s elapsed] +yandex_compute_instance.vm: Creation complete after 45s [id=fhm...] + +Apply complete! Resources: 4 added, 0 changed, 0 destroyed. + +Outputs: + +public_ip = "93.77.*.*" +``` + +## VM Public IP address + +`93.77.*.*` + +## SSH connection + +`ssh ubuntu@93.77.*.*` +```bash +The authenticity of host '93.77.*.* (93.77.*.*)' can't be established. +ED25519 key fingerprint is SHA256:6hk.... + +ubuntu@fhm...:~$ +``` + +## Resources + +- info-service +- info-service-network +- info-service-security-groups +- info-service-subnet + +## terraform destroy + +```bash +yandex_compute_instance.vm: Destroying... [id=fhm6ue6tlbqaps0ajb9k] +yandex_compute_instance.vm: Still destroying... [id=fhm6ue6tlbqaps0ajb9k, 00m10s elapsed] +yandex_compute_instance.vm: Still destroying... [id=fhm6ue6tlbqaps0ajb9k, 00m20s elapsed] +yandex_compute_instance.vm: Still destroying... [id=fhm6ue6tlbqaps0ajb9k, 00m30s elapsed] +yandex_compute_instance.vm: Destruction complete after 31s +yandex_vpc_subnet.this: Destroying... [id=e9b923runku4pks1jhao] +yandex_vpc_security_group.this: Destroying... [id=enp8sg70v6kdketes7fg] +yandex_vpc_security_group.this: Destruction complete after 0s +yandex_vpc_subnet.this: Destruction complete after 5s +yandex_vpc_network.this: Destroying... [id=enpqmv18bdjd65ja03o7] +yandex_vpc_network.this: Destruction complete after 0s + +Destroy complete! Resources: 4 destroyed. +``` + +## Terraform vs Pulumi + +Advantages of HCL: + +- Declarative syntax +- A single standard for all providers +- Good readability for infrastructure +- Extensive community and documentation + +Advantages of Pulumi: + +- A full-fledged programming language +- Typing and auto-completion in the IDE +- The ability to use functions, loops, classes +- It's easier to reuse the code + +## Preferable tool - Terraform + +- Super-stable infrastructures +- Collaboration with external teams +- Enterprise features +- Stability + +## tflint + +`No output` + +## GitHub repository import process + +```bash +export GITHUB_TOKEN="..." +terraform init +terraform import github_repository.course_repo "DevOps-Core-Course" +terraform plan +terraform apply + +Terraform has been successfully initialized! + +github_repository.course_repo: Importing from ID "DevOps-Core-Course" +github_repository.course_repo: Import prepared! + Prepared github_repository for import +github_repository.course_repo: Refreshing state... [id=DevOps-Core-Course] + +Import successful! +``` + +## Why importing matters + +- **A single source of truth**: All resources are managed through a single tool +- **Consistency**: Eliminates configuration drift +- **Change Security**: Terraform shows the difference before applying +- **Documentation**: The code becomes the documentation of the infrastructure +- **Recovery**: Easy infrastructure recovery in case of failures + +## Benefits for managing repos with IaC + +- **Reproducibility**: Identical repositories can be created for different environments. +- **Versioning**: The history of settings changes in Git +- **Code Review**: Repository settings are reviewed as code +- **Automating**: Massive changes across multiple repositories +- **Security**: Standardized security settings diff --git a/terraform/github/github.tf b/terraform/github/github.tf new file mode 100644 index 0000000000..f7465dc02f --- /dev/null +++ b/terraform/github/github.tf @@ -0,0 +1,19 @@ +terraform { + required_providers { + github = { + source = "integrations/github" + version = "~> 6.0" + } + } +} + +provider "github" { + # token = "ghp_***" + owner = "scruffyscarf" +} + +resource "github_repository" "repo" { + name = "DevOps-Core-Course" + description = "DevOps practice" + visibility = "public" +} diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000000..ccb5b8066a --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,65 @@ +resource "yandex_vpc_network" "this" { + name = "info-service-network" +} + +resource "yandex_vpc_subnet" "this" { + name = "info-service-subnet" + zone = "ru-central1-a" + network_id = yandex_vpc_network.this.id + v4_cidr_blocks = ["10.0.0.0/24"] +} + +resource "yandex_vpc_security_group" "this" { + name = "info-service-security-groups" + network_id = yandex_vpc_network.this.id + + ingress { + protocol = "TCP" + port = 22 + v4_cidr_blocks = [var.ssh_cidr] + } + + ingress { + protocol = "TCP" + port = 80 + v4_cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + protocol = "TCP" + port = 5000 + v4_cidr_blocks = ["0.0.0.0/0"] + } + + egress { + protocol = "ANY" + v4_cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "yandex_compute_instance" "vm" { + name = var.vm_name + + resources { + cores = 2 + memory = 1 + core_fraction = 20 + } + + boot_disk { + initialize_params { + image_id = "fd8i5gvlr8t2tcesgf2g" # Ubuntu 22.04 + size = 10 + } + } + + network_interface { + subnet_id = yandex_vpc_subnet.this.id + nat = true + security_group_ids = [yandex_vpc_security_group.this.id] + } + + metadata = { + ssh-keys = "ubuntu:${var.ssh_public_key}" + } +} diff --git a/terraform/outputs.tf b/terraform/outputs.tf new file mode 100644 index 0000000000..66decbfb9e --- /dev/null +++ b/terraform/outputs.tf @@ -0,0 +1,3 @@ +output "public_ip" { + value = yandex_compute_instance.vm.network_interface[0].nat_ip_address +} diff --git a/terraform/providers.tf b/terraform/providers.tf new file mode 100644 index 0000000000..2f88fd4f8b --- /dev/null +++ b/terraform/providers.tf @@ -0,0 +1,17 @@ +terraform { + required_version = ">= 1.5.0" + + required_providers { + yandex = { + source = "yandex-cloud/yandex" + version = ">= 0.100.0" + } + } +} + +provider "yandex" { + service_account_key_file = var.sa_key_file + cloud_id = var.cloud_id + folder_id = var.folder_id + zone = "ru-central1-a" +} diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000000..42f5377b8a --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,29 @@ +variable "ssh_cidr" { + description = "CIDR block for SSH access" + type = string + default = "192.145.30.13/32" +} + +variable "vm_name" { + description = "Name of the virtual machine" + type = string + default = "info-service" +} + +variable "sa_key_file" { + description = "Path to Yandex Cloud service account key" + type = string +} + +variable "cloud_id" { + type = string +} + +variable "folder_id" { + type = string +} + +variable "ssh_public_key" { + description = "SSH public key for VM access" + type = string +}