{"id":14986114,"url":"https://github.com/pabloqpacin/devops_101","last_synced_at":"2026-02-21T16:40:56.005Z","repository":{"id":246885070,"uuid":"824117532","full_name":"pabloqpacin/devops_101","owner":"pabloqpacin","description":"Labs for Vagrant, Ansible, Kubernetes, CI/CD pipelines, Terraform...","archived":false,"fork":false,"pushed_at":"2025-10-12T01:57:29.000Z","size":109,"stargazers_count":0,"open_issues_count":3,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-24T06:38:09.828Z","etag":null,"topics":["ansible","minikube","vagrant"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pabloqpacin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-07-04T11:54:37.000Z","updated_at":"2024-07-22T10:50:05.000Z","dependencies_parsed_at":"2024-09-25T00:31:05.011Z","dependency_job_id":null,"html_url":"https://github.com/pabloqpacin/devops_101","commit_stats":{"total_commits":5,"total_committers":1,"mean_commits":5.0,"dds":0.0,"last_synced_commit":"9a074e19d42545e4c65e441914621bda849d8a1d"},"previous_names":["pabloqpacin/devops_101"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/pabloqpacin/devops_101","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pabloqpacin%2Fdevops_101","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pabloqpacin%2Fdevops_101/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pabloqpacin%2Fdevops_101/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pabloqpacin%2Fdevops_101/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pabloqpacin","download_url":"https://codeload.github.com/pabloqpacin/devops_101/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pabloqpacin%2Fdevops_101/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29686799,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-21T15:51:39.154Z","status":"ssl_error","status_checked_at":"2026-02-21T15:49:03.425Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ansible","minikube","vagrant"],"created_at":"2024-09-24T14:12:20.792Z","updated_at":"2026-02-21T16:40:55.934Z","avatar_url":"https://github.com/pabloqpacin.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# devops_101\n\n\u003e ***DevOps for the Desperate**. A Hands-On Survival Guide*. [Libro](https://nostarch.com/devops-desperate), [repo](https://github.com/bradleyd/devops_for_the_desperate).\n\n- [devops\\_101](#devops_101)\n  - [Entornos de desarrollo y operaciones](#entornos-de-desarrollo-y-operaciones)\n  - [Objetivos](#objetivos)\n  - [Proyectos](#proyectos)\n    - [Proyecto 1. Vagrant + Ansible](#proyecto-1-vagrant--ansible)\n      - [1.1 (Ch. 1) Instalación de Vagrant y Ansible](#11-ch-1-instalación-de-vagrant-y-ansible)\n      - [1.2 Configuraciones: hardware, VirtualBox, Vagrant](#12-configuraciones-hardware-virtualbox-vagrant)\n      - [1.3 Implementación del `Vagrantfile`](#13-implementación-del-vagrantfile)\n      - [1.4 Ansible: `site.yml`](#14-ansible-siteyml)\n      - [1.5 (Ch. 2) Ansible: usuarios, grupos y contraseñas](#15-ch-2-ansible-usuarios-grupos-y-contraseñas)\n        - [`pam_pwquality.yml`](#pam_pwqualityyml)\n        - [`user_and_group.yml`](#user_and_groupyml)\n        - [Demo: usuarios y permisos](#demo-usuarios-y-permisos)\n      - [1.6 (Ch. 3) Ansible: ssh and 2FA](#16-ch-3-ansible-ssh-and-2fa)\n        - [Generar claves ssh y `authorized_keys.yml`](#generar-claves-ssh-y-authorized_keysyml)\n        - [`two_factor.yml` y `google_authenticator`](#two_factoryml-y-google_authenticator)\n      - [1.7 (Ch. 4) Webapp \\\u0026 sudoers (con Jinja)](#17-ch-4-webapp--sudoers-con-jinja)\n        - [`web_application.yml`, `greeting.service`, `greeting.py` y `wsgi.py`](#web_applicationyml-greetingservice-greetingpy-y-wsgipy)\n        - [`sudoers.yml` y `templates/developers.j2` (Jinja2)](#sudoersyml-y-templatesdevelopersj2-jinja2)\n        - [*Provisioning the VM*](#provisioning-the-vm)\n      - [1.8 (Ch. 5)  `ufw` firewall](#18-ch-5--ufw-firewall)\n        - [`firewall.yml`](#firewallyml)\n    - [Proyecto 2. Docker (en *Minikube*), Kubernetes y CI/CD pipelines](#proyecto-2-docker-en-minikube-kubernetes-y-cicd-pipelines)\n        - [2.1 (Ch. 6) Instalación de minikube y docker-client](#21-ch-6-instalación-de-minikube-y-docker-client)\n        - [2.2 Imagen Docker de aplicación `telnet-server`](#22-imagen-docker-de-aplicación-telnet-server)\n        - [2.3 Demo de aplicación `telnet-server` (en Docker), revisión de logs](#23-demo-de-aplicación-telnet-server-en-docker-revisión-de-logs)\n        - [2.4 (Ch. 7) Kubernetes: `deployment.yaml` y `service.yaml`](#24-ch-7-kubernetes-deploymentyaml-y-serviceyaml)\n        - [2.5 (Ch. 8) Desplegando y testeando código (Skaffold CI/CD)](#25-ch-8-desplegando-y-testeando-código-skaffold-cicd)\n\n\n## Entornos de desarrollo y operaciones\n\nNuestro hardware:\n\n| Máquina       | Procesador                    | RAM   | Almacenamiento                            | OS            | ¿Multiboot?\n| ---           | ---                           | ---   | ---                                       | ---           | ---\n| Acer EX2511   | i5-4210U (4)\u003cbr\u003e @ 2.70 GHz   | 16GB | 1x240GB SSD\u003cbr\u003e 1x480 SSD                  | Pop!_OS 22.04 | Arch Linux\n| **MSI GL76**  | i7-11800H (16)\u003cbr\u003e @ 4.60 GHz | 32GB | 1x2TB NVMe\u003cbr\u003e 1x1TB NVMe\u003cbr\u003e 1x1TB HDD    | Pop!_OS 22.04 | No\n| **Pi 5**      | ...                           | ...   | ...                                       | ...           | No\n \n\n\u003c!--\nCloud IaaS:\n\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n  \u003cth\u003eProvider\n  \u003cth\u003eCuenta\n  \u003cth\u003eServicios\n  \u003cth\u003eIntegración\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n    \u003ctd rowspan=3\u003eAWS\n    \u003ctd\u003epq2\n    \u003ctd\u003eVM + IP fija\n    \u003ctd\u003eDonDominio: pabloqpacin.com\n\u003c/tr\u003e\n\u003ctr\u003e\n    \u003ctd\u003epqp\n    \u003ctd colspan=2\u003e...\n\u003c/tr\u003e\n\u003ctr\u003e\n    \u003ctd\u003ep.q\n    \u003ctd colspan=2\u003e...\n\u003c/tr\u003e\n\u003ctr\u003e\n    \u003ctd rowspan=2\u003eTrevenque\n    \u003ctd colspan=3\u003e... vSphere, Plesk...\n\u003c/tr\u003e\n\u003ctr\u003e\n    \u003ctd colspan=3\u003e...\n\u003c/tr\u003e\n\u003ctr\u003e\n    \u003ctd\u003eGCP\n    \u003ctd colspan=3\u003e...\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n --\u003e\n\n\n## Objetivos\n\nTecnologías que queremos aprender:\n\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n    \u003cth\u003eProyecto\n    \u003cth colspan=2\u003eTecnologías\n    \u003cth\u003eEntorno/\n    \u003cth\u003ePlataforma\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n    \u003ctd\u003e1\n    \u003ctd\u003e\u003cb\u003eVagrant\n    \u003ctd\u003e\u003cb\u003eAnsible\n    \u003ctd\u003eLocal (Acer EX2511)\n    \u003ctd\u003eVirtualBox\n\u003ctr\u003e\n    \u003ctd\u003e2\n    \u003ctd colspan=2\u003eTerraform\n    \u003ctd\u003eRemoto\n    \u003ctd\u003eAWS\n\u003ctr\u003e\n    \u003ctd\u003e3\n    \u003ctd\u003eKubernetes\n    \u003ctd\u003eCI/CD\n    \u003ctd\u003e...\n    \u003ctd\u003e...\n\u003c/tbody\u003e\n\u003c/table\u003e\n\n\n## Proyectos\n\n**IMPORTANTE**: \u003cu\u003eclonar el repo\u003c/u\u003e para manejar los archivos de los proyectos.\n\n```bash\ngit clone https://github.com/pabloqpacin/devops_101.git $HOME/devops_101\n```\n\n### Proyecto 1. Vagrant + Ansible\n\n\u003c!-- - [ ] [/vagrant](/vagrant/)\n- [ ] [/ansible](/ansible/) --\u003e\n\nNos conectamos con `ssh` desde nuestra máquina de desarrollo *MSI GL76*  a la de operaciones *Acer EX2511*. Ambas están en nuestra red local y pilotan el sistema operativo *Pop!_OS* (derivado de Ubuntu).\n\nLa máquina *EX2511* tiene el OS instalado en `/dev/sdb1` (esta sería la partición *root* o `/`). Previamente hemos creado la partición `/dev/sdb2` con idea de almacenar VMs. Aunque no es necesario, decidimos dar persistencia al montaje de particiones con los siguientes comandos.\n\n```bash\nsudo mkdir -p /media/$USER/LAB\nUUID=$(blkid /dev/sdb2 | awk '{print $3}' | awk -F '=' '{print $2}' | tr -d '\"')\necho \"UUID=$UUID /media/$USER/LAB ext4 defaults 0  2\" | \\\n    sudo tee -a /etc/fstab\nsudo mount -a\n# df -h | grep /media/$USER/LAB\n```\n\n\n#### 1.1 (Ch. 1) Instalación de Vagrant y Ansible\n\nInstalamos **Vagrant** (Ubuntu/Debian).\n\n\u003c!--\n```bash\nif command -v vagrant \u0026\u003e/dev/null; then\n    echo \"Vagrant is already installed.\"\nelse\n    DISTRO=$(grep 'ID_LIKE' /etc/os-release | awk -F '=' '{print $2}' | tr -d '\"')\n    case $DISTRO in\n        'ubuntu debian' | 'ubuntu' | 'debian')\n            wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg\n            echo \"deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main\" | sudo tee /etc/apt/sources.list.d/hashicorp.list\n            sudo apt update \u0026\u0026 sudo apt install vagrant\n            ;;\n        *)\n            echo \"Distro not supported. Terminating script.\"\n            exit 1\n            ;;\n    esac\nfi\n```\n--\u003e\n\n```bash\nwget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg\necho \"deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main\" | \\\n    sudo tee /etc/apt/sources.list.d/hashicorp.list\nsudo apt update \u0026\u0026 sudo apt install vagrant\n```\n\nInstalar **Ansible** no es necesario para operar con Vagrant, pero dado que nuestro `Vagrantfile` hace uso de Ansible, mejor instalarlo ya (Ubuntu/Debian). Si no lo hacemos, habría que comentar las líneas relevantes del `Vagrantfile`.\n\n```bash\nsudo apt update\nsudo apt install software-properties-common\nsudo add-apt-repository --yes --update ppa:ansible/ansible\nsudo apt install -y ansible\n```\n\n\n#### 1.2 Configuraciones: hardware, VirtualBox, Vagrant\n\nHemos preparado [el script `vagrant_vbox_env.sh`](/scripts/vagrant_vbox_env.sh) para realizar varias tareas importantes.\n\n1. Asignar a la variable de entorno `$VAGRANT_HOME` el valor `/var/vagrant.d` (por defecto sería `~/.vagrant.d`). Aquí se almacenarán varios archivos de configuración de **Vagrant**. Cada imagen o *box* que descarguemos pesará medio GB así que puede llegar a pesar mucho y preferimos dejar este tipo de *bloat* fuera de `/home`.\n\n\u003c!-- Primero descargamos el script y lo guardamos como `~/vagrant_vbox_env.sh`. Si hemos clonado el repositorio también podríamos copiarlo o hacer un symlink en local.\n\n```bash\ncurl -so ~/vagrant_vbox_env.sh \\\n    https://raw.githubusercontent.com/pabloqpacin/devops-101/main/vagrant/vagrant_vbox_env_sh\n``` --\u003e\n\n2. Queremos almacenar las VMs en la **partición** `/dev/sdb2` montada como `/media/$USER/LAB` de forma persistente. Igualmente verificamos que la partición está montada y si no es así se intenta mediante con el comando `gio mount -d /dev/sdb2`. Desafortunadamente este comando requiere iniciar la sesión gráfica de escritorio tipo Gnome, Cosmic..., por eso la persistencia.\n\n3. Finalmente, revisamos y definimos el directorio donde **VirtualBox** almacenará por defecto las VMs. \n\n```bash\n# VBoxManage list systemproperties | grep \"Default machine folder\" \nVBoxManage setproperty machinefolder /media/$USER/LAB/VBox\n```\n\nHacemos que la shell (*zsh* o *bash*) ejecute nuestro script verificador al iniciarse.\n\n```bash\necho -e \"\\nsource ~/devops_101/scripts/vagrant_vbox_env.sh\" \u003e\u003e ~/.zshrc || \\\necho -e \"\\nsource ~/devops_101/scripts/vagrant_vbox_env.sh\" \u003e\u003e ~/.bashrc\n```\n\nCon todo preparado, podemos iniciar una nueva shell e instalar los plugins necesarios para este proyecto.\n\n```bash\n# watch tree $VAGRANT_HOME\n\nvagrant plugin update\nvagrant plugin install vagrant-vbguest\n# vagrant plugin install vagrant-share vagrant-disksize\nvagrant plugin list\n```\n\n\n#### 1.3 Implementación del `Vagrantfile`\n\nNos vamos al directorio `vagrant` de nuestro repositorio.\n\n```bash\ncd ~/devops_101/vagrant\n```\n\nRepasamos [nuestro `Vagrantfile`](/vagrant/Vagrantfile).\n\n```Vagrantfile\nVagrant.configure(\"2\") do |config|\n    config.vm.box = \"ubuntu/jammy64\"\n    config.vm.hostname = \"vagrant-ubuntu-2204\"\n\n    config.vm.network \"public_network\", bridge: \"enp2s0\"\n\n    config.vbguest.auto_update = false\n\n    config.vm.provider \"virtualbox\" do |vb|\n        vb.name = \"vagrant-ubuntu-2204\"\n        vb.memory = \"2048\"\n        vb.cpus = 2\n    end\n\n    config.vm.synced_folder \".\", \"vagrant\", disabled: true\n\n    config.vm.provision \"ansible\" do |ansible|\n        ansible.playbook = \"../ansible/site.yml\"\n        ansible.compatibility_mode = \"2.0\"\n    end\n\nend\n```\n\n\u003c!-- - [ ] ¿Guardar VM en grupo de VBox? --\u003e\n\u003c!-- - [ ] `config.disksize.size = '50GB'` --\u003e\n\u003c!-- - [ ] ¿Desactivar primera interfaz NAT? --\u003e\n\u003c!-- - [ ] Asignar interfaz a red NAT --\u003e\n\nVerificamos que el `Vagrantfile` está correcto, iniciamos y verificamos la implantación.\n\n\u003e **NOTA**: el comando `vagrant` solo tiene en cuenta las VMs asociadas al `Vagrantfile` del directorio actual en la shell (`pwd`).\n\n\n```bash\n# vagrant list-commands\nvagrant validate\nvagrant up\n# vagrant up --debug\nvagrant status\n```\n\nPodemos ejecutar comandos en la VM y conectarnos a la nueva VM. Podemos detener/apagar la VM, y eliminarla. También podemos verificar las imágenes/*boxes*.\n\n```bash\nvagrant ssh -c \"sudo apt update \u0026\u0026 sudo apt install neofetch --no-install-recommends \u0026\u0026 neofetch\"\n# vagrant ssh\n\nvagrant halt\n# vagrant destroy\n\nvagrant box list\n```\n\n\u003e **NOTA**: si abrimos la GUI de VirtualBox podremos ver las nuevas VMs y es posible conectarse a ellas. Para el login, el usuario y la contraseña son `vagrant`.\n\n\n#### 1.4 Ansible: `site.yml`\n\nCon **Ansible** ya instalado, nos aseguramos de que nuestro `Vagrantfile` contiene estas líneas:\n\n```Vagrantfile\nconfig.vm.provision \"ansible\" do |ansible|\n    ansible.playbook = \"../ansible/site.yml\"\n    ansible.compatibility_mode = \"2.0\"\nend\n```\n\nEste código cargará nuestro *playbook* (archivo de configuración) de Ansible principal para este proyecto. Será necesario ir modificando este archivo `site.yml` para implementar cosas. El resto de esta documentación/proyecto tratará en detalle el resto de archivos `.yml` y las operaciones con Ansible.\n\nEste sería nuestro `site.yml` actualmente.\n\n```yaml\n---\n- name: Provision VM\n  hosts: all\n  become: true\n  become_method: sudo\n  remote_user: ubuntu\n  tasks:\n    #  - import_tasks: chapter2/pam_pwquality.yml\n    #  - import_tasks: chapter2/user_and_group.yml\n    #  - import_tasks: chapter3/authorized_keys.yml\n    #  - import_tasks: chapter3/two_factor.yml\n    #  - import_tasks: chapter4/web_application.yml\n    #  - import_tasks: chapter4/sudoers.yml\n    #  - import_tasks: chapter5/firewall.yml\n  handlers:\n    #  - import_tasks: handlers/restart_ssh.yml\n```\n\nAl levantar la VM con **Vagrant**, se ejecutará este *playbook* con éxito, si bien al no tener tareas específicas (los *playbooks* que las llevarán a cabo están comentados) no se hará ningún *provisioning* en la VM.\n\n\u003e Decidimos que los archivos sean `.yml` y no `.yaml` por seguir el estilo de la documentación oficial (eg. [Ansible YAML file syntax and structure](https://developers.redhat.com/learning/learn:ansible:yaml-essentials-ansible/resource/resources:ansible-yaml-file-syntax-and-structure)), además de que es el estilo propuesto en el libro. Igualmente cambiamos la línea `become: yes` por `become: true`.\n\n\n#### 1.5 (Ch. 2) Ansible: usuarios, grupos y contraseñas\n\nIremos creando los archivos `.yml` con las tareas en el directorio `ansible/chapter2/`. Para operar con ellos habrá que descomentar las líneas relevantes en `ansible/site.yml`.\n\nSi la VM ya existe (ya hicimos `vagrant up`) usaremos este comando para aplicar **Ansible** \u003c!--según se define en nuestro `Vagrantfile`--\u003e.\n\n```bash\n# vagrant up\nvagrant provision\n# vagrant provision --debug\n```\n\n\n##### `pam_pwquality.yml`\n\nEn esta primera tarea se instala el paquete `libpam-pwquality` y se edita el archivo de configuración `/etc/pam.d/common-password` para imponer las siguientes restricciones en la creación de contraseñas:\n\n- Un mínimo de 12 caracteres\n- Una letra minúscula\n- Una letra mayúscula\n- Un caracter numérico\n- Un caracter no alfanumérico\n- Tres intentos\n- Desactivar invalidación de root\n\n```yaml\n---\n- name: Install libpam-pwquality\n  package:\n    name: \"libpam-pwquality\"\n    state: present\n\n- name: Configure pam_pwquality\n  lineinfile:\n    path: \"/etc/pam.d/common-password\"\n    regexp: \"pam_pwquality.so\"\n    line: \"password required pam_pwquality.so minlen=12 lcredit=-1 ucredit=-1 dcredit=-1 ocredit=-1 retry=3 enforce_for_root\"\n    state: present\n\n    #- name: Limit Password Reuse\n    #  lineinfile:\n    #    dest: \"/etc/pam.d/common-password\"\n    #    regexp: \"remember=5\"\n    #    line: \"password sufficient pam_unix.so use_authtok remember=5\"\n    #    state: present\n```\n\n\u003c!-- - [ ] ¿Cómo se instala? Supongo que `apt install foo` pero bueno, en otros casos podría ser `snap install bar`... --\u003e\n\n##### `user_and_group.yml`\n\n\u003e **IMPORTANTE**: es insecuro tener contraseñas o llaves en un repo público. Implementar [**Ansible Vault**](https://docs.ansible.com/ansible/latest/vault_guide/index.html)... \u003c!--https://docs.ansible.com/ansible/2.9/user_guide/vault.html--\u003e\n\n\nEn nuestra máquina (no la VM) vamos a necesitar los programas `pwgen` para generar contraseñas seguras y `mkpasswd` para generar los *hashes* de estas contraseñas. Escribiremos el *hash* en el siguiente archivo `.yml`. \u003c!--Aunque la contraseña no nos hace falta, podemos guardarla en nuestro **gestor de contraseñas** favorito, KeePassXC.--\u003e\n\n```bash\nsudo apt update\nsudo apt install pwgen whois\n\npass=$(pwgen --secure --capitalize --numerals --symbols 12 1)\n\necho $pass | mkpasswd --stdin --method=sha-512; echo $pass\n    # $6$QJmzvbMhlt7C.qOO$uSkIZs/nINf2HFR/.nerO3qfRzIOR53BwZVwJspkkKdrO1KLOzIcW7hG7UWAhGTh/VJVvxhbZO7qloGqGs30E/\n    # ]aR8WG{/yqG}\n```\n\n\nEste es el archivo, y con estas tareas conseguimos lo siguiente:\n\n- crear el grupo *developers*\n- crear el usuario *bender*\n- añadir a *bender* al grupo *developers*\n- crear el directorio `/opt/engineering`\n- crear un archivo en el nuevo directorio\n\n```yaml\n- name: Ensure group 'developers' exists\n  group:\n    name: developers\n    state: present\n\n- name: Create the user 'bender'\n  user:\n    name: bender\n    shell: /bin/bash\n    password: $6$QJmzvbMhlt7C.qOO$uSkIZs/nINf2HFR/.nerO3qfRzIOR53BwZVwJspkkKdrO1KLOzIcW7hG7UWAhGTh/VJVvxhbZO7qloGqGs30E/\n\n- name: Assign 'bender' to the 'developers' group\n  user:\n    name: bender\n    groups: developers\n    append: yes\n\n- name: Create a directory named 'engineering'\n  file:\n    path: /opt/engineering\n    state: directory\n    mode: 0750\n    group: developers\n\n- name: Create a file in the engineering directory\n  file:\n    path: \"/opt/engineering/private.txt\"\n    state: touch\n    mode: 0770\n    group: developers\n```\n\n\u003c!-- - [ ] en principio solo local, ¿y en carpeta compartida (tema Vagrant)? --\u003e\n\nEste es un buen momento para editar el `site.yml` y ejecutar `vagrant provision`.\n\n##### Demo: usuarios y permisos\n\nNos logueamos en la VM. Nuestro usuario debería ser `vagrant`.\n\n```bash\ncd ~/devops_101/vagrant\n# vagrant ssh -c \"whoami\"\nvagrant ssh\n```\n\nVerificamos que existen el usuario *bender* y el grupo *developers*.\n\n```bash\ngetent passwd bender\n    # bender:x:1002:1003::/home/bender:/bin/bash\n\ngetent group developers bender\n    # developers:x:1002:bender\n    # bender:x:1003:\n```\n\nPara el archivo, primero comprobamos que *vagrant* no tiene acceso, luego nos logueamos como *bender* y comprobamos que tenemos acceso.\n\n```bash\nls -la /opt/engineering/\n    # ls: cannot open directory '/opt/engineering/': Permission denied\n\n# su bender\n    # ]aR8WG{/yqG}\n\nsudo su - bender\n\n# groups\n    # bender developers\n\nls -la /opt/engineering/\n    # drwxr-x--- 2 root developers 4096 Jul  6 14:54 .\n    # drwxr-xr-x 3 root root       4096 Jul  6 14:54 ..\n    # -rwxrwx--- 1 root developers    4 Jul  6 15:07 private.txt\n```\n\n#### 1.6 (Ch. 3) Ansible: ssh and 2FA\n\nPara el usuario *bender* de nuestra VM. Desactivaremos el acceso por ssh con contraseña y habilitaremos 2FA con **llaves ssh** y *google authenticator*.\n\n##### Generar claves ssh y `authorized_keys.yml`\n\nEn nuestra máquina de operaciones generamos nuevas claves **ssh** (privada y pública). El siguiente comando nos pedirá una *passphrase*, que debemos guardar en un gestor de contraseñas o similar.\n\n```bash\nssh-keygen -t rsa -f ~/.ssh/dftd -C dftd\n\n# read -p \"Passphrase para las llaves 'dftd': \" passphrase\n# ssh-keygen -t rsa -f ~/.ssh/dftd -C dftd -N \"$passphrase\"\n```\n\nAhora podemos editar nuestro `site.yml` para incluir el archivo `ansible/chapter3/authorized_keys.yml`.\n\n```yaml\n---\n- name: Set authorized key file from local user\n  authorized_key:\n    user: bender \n    state: present\n    key: \"{{ lookup('file', lookup('env','HOME') + '/.ssh/dftd.pub') }}\"\n```\n\n\n##### `two_factor.yml` y `google_authenticator`\n\n\u003c!--\nFor this example, you’ll use a *time-based one-time password (TOTP)* to\nsatisfy the “something you have” portion, along with your public key for\naccess. You’ll use the `Google Authenticator` package to configure your VM to\nuse TOTP tokens for logging in. These TOTP tokens are usually generated\nfrom an application like `oathtool` (*https://www.nongnu.org/oath-toolkit/*) and\nare valid for only a short period of time. I have taken the liberty of creating\n10 TOTP tokens that Ansible will use for you, but I will also show you how\nto use oathtool (more on this later).\n--\u003e\n\nEl segundo factor de autenticación va a ser **TOTP** (*Time-based one-time password*) mediante `GoogleAuthenticator`. Lo ideal sería, teniendo preparada la app de Android **Google Authenticator**, acceder como *bender* a la VM tras instalar el paquete `libpam-google-authenticate` (1ª tarea a continuación), ejecutarlo de la siguiente forma, escanear el QR con el móvil e introducir en la terminal el código que salga en la app.\n\n```bash\ngoogle-authenticator -f -t -d -r 3 -R 30 -w 17 -e 10\n    # Warning: pasting the following URL into your browser exposes the OTP secret to Google:\n    #   https://www.google.com/chart?chs=200x200\u0026chld=M|0\u0026cht=qr\u0026chl=otpauth://totp/vagrant@vagrant-ubuntu-2204%3Fsecret%3DX5AH\u003c...\u003eJT7Q%26issuer%3Dvagrant-ubuntu-2204\n    # \u003cQR\u003e\n    # Your new secret key is: X5AH\u003c...\u003eJT7Q\n    # Enter code from app (-1 to skip): 972935\n\n    # Code confirmed\n    # Your emergency scratch codes are:\n    #   11880927\n    #   35111193\n    #   32810950\n    #   87502136\n    #   81456931\n    #   79721071\n    #   31977925\n    #   28440037\n    #   12366122\n    #   65260038\n```\n\nAl completarse el proceso, se crea el archivo `~/.google_authenticator`. En este caso en lugar de generar todo esto, vamos a copiar un archivo proporcionado por el autor del **libro** (2ª tarea). Habrá que revisar esto, tema `oathtool` etc.\n\n\u003e **IMPORTANTE**: de nuevo, no hay seguridad si publicamos en internet los tokens y las llaves secretas; lo suyo sería usar ***Ansible Vault***, [*HashiCorp's Vault*](https://www.vaultproject.io/) o algo similar.\n\n\nEn definitiva, el archivo `.yml` de este apartado cumplirá los siguientes objetivos:\n1. Instalar `libpam-google-authenticate`\n2. Copiar un archivo de configuración de `GoogleAuthenticator` \u003c!--OJO--\u003e\n3. Desactivar el login por contraseña para **ssh** (mediante *PAM*)\n4. Configurar *PAM* para usar `GoogleAuthenticator` para el login de *bender* por **ssh**\n5. Activar `ChallengeResponseAuthentication` en el `sshd_config`\n6. Configurar Método de Autenticación para *bender*, *vagrant* y *ubuntu*\n7. Incluir handler \"Restart SSH Server\"\n\n\u003c!-- \u003e **NOTA**: diferencias frente al repo del autor: en el `Vagrantfile` hemos cambiado la *box* `focal64` por `jammy64`, Ubuntu 20.04 y 22.04 respectivamente. Esto ha supuesto que el paquete `ssh` tenga una versión más reciente y un **conclicto**, así que para la tarea *Set ChallengeResponseAuthentication to Yes* fue necesario cambiar la línea. --\u003e\n\n```yaml\n- name: Install the libpam-google-authenticator package\n  apt:\n    name: \"libpam-google-authenticator\"\n    update_cache: yes\n    state: present\n\n- name: Copy over Preconfigured GoogleAuthenticator config\n  copy:\n    src: ../ansible/chapter3/google_authenticator\n    dest: /home/bender/.google_authenticator\n    owner: bender\n    group: bender\n    mode: '0600'\n  no_log: true\n\n- name: Disable password authentication for SSH\n  lineinfile:\n    dest: \"/etc/pam.d/sshd\"\n    regex: \"@include common-auth\"\n    line: \"#@include common-auth\"\n\n- name: Configure PAM to use GoogleAuthenticator for SSH logins\n  ansible.builtin.blockinfile:\n    path: \"/etc/pam.d/sshd\"\n    prepend_newline: true\n    insertafter: EOF\n    block: |\n        auth required pam_google_authenticator.so nullok\"\n\n- name: Set ChallengeResponseAuthentication to Yes\n  lineinfile:\n    dest: \"/etc/ssh/sshd_config\"\n    regexp: \"^KbdInteractiveAuthentication (yes|no)\"\n    line: \"KbdInteractiveAuthentication yes\"\n    state: present\n\n- name: Set Authentication Methods for bender, vagrant, and ubuntu\n  blockinfile:\n    path: \"/etc/ssh/sshd_config\"\n    block: |\n      Match User \"ubuntu,vagrant\"\n          AuthenticationMethods publickey\n      Match User \"bender,!vagrant,!ubuntu\"\n          AuthenticationMethods publickey,keyboard-interactive\n    state: present\n  notify: \"Restart SSH Server\"\n```\n\n\u003c!--\n4ª (Configure PAM to use GoogleAuthenticator for SSH logins)\n\nThis task tells PAM about the Google Authenticator module. It uses the\nAnsible lineinfile module again to edit the PAM sshd file. This time, you\njust want to add the auth line to the bottom of the PAM file, which lets PAM\nknow it should use Google Authenticator as an authentication mechanism.\nThe nullok option at the end of the line tells PAM that this authentication\nmethod is optional, which allows you to avoid locking out users until they\nhave successfully configured 2FA. In a production environment, you should\nremove the nullok option once all users have enabled 2FA.\n--\u003e\n\n\u003c!-- 5ª --\u003e\n\u003c!-- 6ª --\u003e\n\u003c!-- 7ª --\u003e\n\n\u003c!-- oathtool --\u003e\n\n\u003e **OJO**: Ansible *handlers*...\n\nAhora deberíamos conectarnos a la VM como *bender* y se nos pedirá tanto la *passphrase* de nuestra llave **ssh** y  los tokens de *Google Authenticator* (al usar uno, se elimina automáticamente de `~/.google_authenticator`).\n\n\n```bash\n# # Tener en cuenta el puerto si hay varias VMs funcionando\n# vagrant ssh-config\n# PORT=$(vagrant ssh-config | grep 'Port' | awk '{print $2}')\n\n# Conexión a la VM\nssh -i ~/.ssh/dftd -p 2222 bender@localhost\n\n# # Si hay problemas\n# vagrant ssh\n# less /var/log/auth.log\n# less /var/log/syslog\n```\n\n- [ ] Revisar el tema para hacerlo TOTP, incluso compatible con Android apps\n- [ ] Curiosamente todavía podemos acceder con `vagrant ssh`... ¿No deberíamos caparlo?\n\n\n#### 1.7 (Ch. 4) Webapp \u0026 sudoers (con Jinja)\n\n\n##### `web_application.yml`, `greeting.service`, `greeting.py` y `wsgi.py`\n\n- [ ] Why Nginx tho? Is it so that UFW can block connection attempts?\n\n```yaml\n---\n- name: Install python3-flask, gunicorn3, and nginx\n  apt:\n    name:\n      - python3-flask\n      - gunicorn\n      - nginx\n    update_cache: yes\n\n- name: Copy Flask Sample Application\n  copy:\n    src: \"../ansible/chapter4/{{ item }}\"\n    dest: \"/opt/engineering/{{ item }}\"\n    group: developers\n    mode: '0750'\n  loop:\n    - greeting.py\n    - wsgi.py\n\n- name: Copy systemd Unit file for Greeting\n  copy:\n    src: \"../ansible/chapter4/greeting.service\"\n    dest: \"/etc/systemd/system/greeting.service\"\n\n- name: Start and enable Greeting Application\n  systemd:\n    name: greeting.service\n    daemon_reload: yes\n    state: started\n    enabled: yes\n```\n\n##### `sudoers.yml` y `templates/developers.j2` (Jinja2)\n\n```yaml\n---\n- set_fact:\n    greeting_application_file: \"/opt/engineering/greeting.py\"\n\n- name: Create sudoers file for developers group\n  template:\n    src: \"../ansible/templates/developers.j2\"\n    dest: \"/etc/sudoers.d/developers\"\n    validate: 'visudo -cf %s'\n    owner: root\n    group: root\n    mode: 0440\n```\n\n```j2\n# Command alias\nCmnd_Alias\tSTART_GREETING    = /bin/systemctl start greeting , \\\n\t\t\t\t    /bin/systemctl start greeting.service\nCmnd_Alias\tSTOP_GREETING     = /bin/systemctl stop greeting , \\\n\t\t\t\t    /bin/systemctl stop greeting.service\nCmnd_Alias\tRESTART_GREETING  = /bin/systemctl restart greeting , \\\n\t\t\t\t    /bin/systemctl restart greeting.service\n\n# Host Alias\nHost_Alias      LOCAL_VM = {{ hostvars[inventory_hostname]['ansible_default_ipv4']['address'] }}\n# User specification\n%developers LOCAL_VM = (root) NOPASSWD: START_GREETING, STOP_GREETING, \\\n\t    \t       RESTART_GREETING, \\\n\t\t       sudoedit {{ greeting_application_file }}\n\n```\n\n##### *Provisioning the VM*\n\n```bash\nvagrant provision\n\nssh -i ~/.ssh/dftd -p 2222 bender@localhost\n```\n```bash\ncurl http://localhost:5000\n\n# # NO usar sudo, pide contraseña...\n# sed -i 's/Greetings/Greetings and Salutations/' /opt/engineering/greeting.py\n\n# Usamos sudoedit, según /etc/sudoers.d/developers\nsudoedit /opt/engineering/greeting.py\n\nsudo systemctl restart greeting\n# sudo systemctl stop greeting\n# curl -w \"\\n\" http://localhost:5000\n# sudo systemctl start greeting\ncurl -w \"\\n\" http://localhost:5000\n\n# No podremos, y ahora lo veremos revisando el propio log\nsudo tail -f /var/log/auth.log\n```\n\nPara revisar los logs nos logueamos como *vagrant*. \n\n```bash\nvagrant ssh\n\n# less /var/log/auth.log\ngrep 'sudo' /var/log/auth.log | grep 'bender' | grep 'COMMAND'\n```\n\n\n\n#### 1.8 (Ch. 5)  `ufw` firewall\n\n##### `firewall.yml`\n\n- Whitelisting.\n\n```yaml\n---\n\n- name: Turn Logging level to low\n  ufw:\n    logging: 'low'\n\n- name: Allow SSH over port 22\n  ufw:\n    rule: allow\n    port: '22'\n    proto: tcp\n\n- name: Allow all access to port 5000\n  ufw:\n    rule: allow\n    port: '5000'\n    proto: tcp\n\n- name: Rate limit excessive abuse on port 5000\n  ufw:\n    rule: limit\n    port: '5000'\n    proto: tcp\n\n\n- name: Drop all other traffic\n  ufw:\n    state: enabled\n    policy: deny\n    direction: incoming\n```\n\n```bash\nvagrant provision \u0026\u0026 vagrant ssh\nsudo sed -i 's/hitcount 6/hitcount 10/' /etc/ufw/user.rules\n\nVM_IP=$(ip -4 -br a | tail -n 1 | awk '{print $3}')\n  # 192.168.1.43/24\n```\n\nDesde un pc real de la red real escaneamos la VM:\n\n\n```bash\nnmap -F 192.168.1.43\n  # Host seems down\n\nnmap -Pn 192.168.1.43\n  # Ports 22 \u0026 5000\n\nnmap -Pn -sV 192.168.1.43\n  # OpenSSH 8.9\n  # upnp?\n\nfor i in {1..6} ; do curl -w \"\\n\" http://192.168.1.43:5000 ; done\n  # A la sexta, Connection refused\n```\n\nVolvemos a loguearnos como admin para revisar logs:\n\n```bash\nvagrant ssh\n\nsudo less /var/log/ufw.log\n```\n\n\n### Proyecto 2. Docker (en *Minikube*), Kubernetes y CI/CD pipelines\n\n##### 2.1 (Ch. 6) Instalación de minikube y docker-client\n\nEn la misma máquina EX2511.\n\nValores por defecto de `minikube start`: `--cpus=2 --memory='3900m' --disk-size='20g'`\n\n\n```bash\ncd /tmp\ncurl -LO https://storage.googleapis.com/minikube/releases/latest/minikube_latest_amd64.deb\n# sudo dpkg -i minikube_latest_amd64.deb\ncd -\n```\n\nNecesario iniciar minikube cada vez que queramos usarlo...\n\n```bash\n# minikube start --driver=virtualbox\nminikube start\n```\n\u003c!--\n```log\npabloqpacin@pop-os ~$ minikube start\n* minikube v1.33.1 on Debian bookworm/sid\n* Automatically selected the virtualbox driver. Other choices: none, ssh\n* Downloading VM boot image ...\n    \u003e minikube-v1.33.1-amd64.iso....:  65 B / 65 B [---------] 100.00% ? p/s 0s\n    \u003e minikube-v1.33.1-amd64.iso:  314.16 MiB / 314.16 MiB  100.00% 41.96 MiB p\n* Starting \"minikube\" primary control-plane node in \"minikube\" cluster\n* Downloading Kubernetes v1.30.0 preload ...\n    \u003e preloaded-images-k8s-v18-v1...:  342.90 MiB / 342.90 MiB  100.00% 38.13 M\n* Creating virtualbox VM (CPUs=2, Memory=3900MB, Disk=20000MB) ...\n* Preparing Kubernetes v1.30.0 on Docker 26.0.2 ...\n  - Generating certificates and keys ...\n  - Booting up control plane ...\n  - Configuring RBAC rules ...\n* Configuring bridge CNI (Container Networking Interface) ...\n* Verifying Kubernetes components...\n  - Using image gcr.io/k8s-minikube/storage-provisioner:v5\n* Enabled addons: default-storageclass, storage-provisioner\n* kubectl not found. If you need it, try: 'minikube kubectl -- get pods -A'\n* Done! kubectl is now configured to use \"minikube\" cluster and \"default\" namespace by default\n```\n --\u003e\n\n```bash\nsudo apt install docker-ce-cli\n\necho \"eval $(minikube -p minikube docker-env)\" \u003e\u003e ~/.zshrc || \\\necho \"eval $(minikube -p minikube docker-env)\" \u003e\u003e ~/.bashrc\n\ndocker version\n```\n\n##### 2.2 Imagen Docker de aplicación `telnet-server`\n\nContenido del directorio `telnet-server/`:\n\n```log\n*[main][~/devops_101]$ tree telnet-server\n devops_101/telnet-server\n├──  container-tests\n│  └──  command-and-metadata-test.yaml\n├──  kubernetes\n│  ├──  deployment.yaml\n│  └──  service.yaml\n├──  metrics\n│  └──  server.go\n├──  telnet\n│  ├──  banner.go\n│  ├──  server.go\n│  └──  server_test.go\n├──  build.sh\n├──  Dockerfile\n├──  go.mod\n├──  go.sum\n├──  main.go\n└──  skaffold.yaml\n```\n\nDockerfile para ***multistage** build*:\n\n```dockerfile\n# Build stage\nFROM golang:alpine AS build-env\nADD . /\nRUN cd / \u0026\u0026 go build -o telnet-server\n\n# final stage\nFROM alpine:latest as final\nWORKDIR /app\nENV TELNET_PORT 2323\nENV METRIC_PORT 9000\nCOPY --from=build-env /telnet-server /app/\n\nENTRYPOINT [\"./telnet-server\"]\n```\n\nComandos para crear la imagen y ejecutar un contenedor:\n\n```bash\n# Crear la imagen\ndocker build -t dftd/telnet-server:v1 .\n\ndocker image ls dftd/telnet-server:v1\ndocker history dftd/telnet-server:v1\n\n# Ejecutar contenedor (instancia de la imagen)\ndocker run -d --name telnet-server -p 2323:2323 dftd/telnet-server:v1\n# docker container ls -f name=telnet-server\ndocker ps -f name=telnet-server\n```\n\nOtros comandos `docker` importantes:\n\n```bash\ndocker exec telnet-server env\ndocker exec -it telnet-server /bin/sh\n\ndocker inspect telnet-server\n  # State\n  # NetworkSettings\n\ndocker stats --no-stream dftd/telnet-server\n```\n\n##### 2.3 Demo de aplicación `telnet-server` (en Docker), revisión de logs\n\nInstalamos el cliente `telnet` si es necesario e iniciamos `minikube` y el contenedor si hemos apagado la máquina.\n\n```bash\nsudo apt install telnet\n\nminikube start\n\ndocker ps -f name=telnet-server\ndocker start telnet-server\ndocker ps -f name=telnet-server\n```\n\nNos conectamos al servidor telnet del contenedor.\n\n```log\n[pabloqpacin:~]$ telnet $(minikube ip) 2323\nTrying 192.168.59.100...\nConnected to 192.168.59.100.\nEscape character is '^]'.\n\n____________ ___________\n|  _  \\  ___|_   _|  _  \\\n| | | | |_    | | | | | |\n| | | |  _|   | | | | | |\n| |/ /| |     | | | |/ /\n|___/ \\_|     \\_/ |___/\n\n\u003ed\nFri Jul 12 16:13:34 +0000 UTC 2024\n\u003eq\nGood Bye!\nConnection closed by foreign host.\n[pabloqpacin:~]$\n```\n\nRevisamos los logs.\n\n```logs\n~ ᐅ docker logs telnet-server\ntelnet-server: 2024/07/11 18:44:41 telnet-server listening on [::]:2323\ntelnet-server: 2024/07/11 18:44:41 Metrics endpoint listening on :9000\ntelnet-server: 2024/07/12 16:10:42 Metrics endpoint listening on :9000\ntelnet-server: 2024/07/12 16:10:42 telnet-server listening on [::]:2323\ntelnet-server: 2024/07/12 16:11:45 [IP=192.168.59.1] New session\ntelnet-server: 2024/07/12 16:13:34 [IP=192.168.59.1] Requested command: d\ntelnet-server: 2024/07/12 16:13:37 [IP=192.168.59.1] User quit session\n\n~ ᐅ docker logs --tail=2 telnet-server\n~ ᐅ docker logs -f telnet-server\n```\n\n\n##### 2.4 (Ch. 7) Kubernetes: `deployment.yaml` y `service.yaml`\n\n\n\u003cdetails\u003e\n\u003csummary\u003eArchivos .yaml\u003c/summary\u003e\n\n`deployment.yaml`:\n\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: telnet-server\n  labels:\n    app: telnet-server\nspec:\n  replicas: 2\n  selector:\n    matchLabels:\n      app: telnet-server\n  strategy:\n    type: RollingUpdate\n    rollingUpdate:\n      maxSurge: 1        # how many pods we can add at a time\n      maxUnavailable: 0  # maxUnavailable define how many pods can be unavailable\n  template:\n    metadata:\n      labels:\n        app: telnet-server\n      annotations:\n        prometheus.io/scrape: 'true'\n        prometheus.io/port:   '9000'\n    spec:\n      containers:\n      - image: dftd/telnet-server:v1\n        imagePullPolicy: IfNotPresent\n        name: telnet-server\n        resources:\n          requests:\n            cpu: 0.1\n            memory: 1M\n          limits:\n            cpu: 0.5\n            memory: 100M\n        ports:\n        - containerPort: 2323\n          name: telnet\n        - containerPort: 9000\n          name: metrics\n```\n\n`service.yaml`:\n\n```yaml\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: telnet-server\n  labels:\n    app: telnet-server\nspec:\n  ports:\n  - port: 2323\n    name: telnet\n    protocol: TCP\n    targetPort: 2323\n  selector:\n    app: telnet-server\n  type: LoadBalancer\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: telnet-server-metrics\n  labels:\n    app: telnet-server\n  annotations:\n      prometheus.io/scrape: 'true'\n      prometheus.io/port:   '9000'\n\nspec:\n  ports:\n  - name: metrics\n    port: 9000\n    protocol: TCP\n    targetPort: 9000\n  selector:\n    app: telnet-server\n  type: ClusterIP\n```\n\n\u003c/details\u003e\n\n\nPuesta en marcha:\n\n```bash\n# minikube start\n\nminikube kubectl cluster-info\n  # Kubernetes control plane is running at https://192.168.59.100:8443\n  # CoreDNS is running at https://192.168.59.100:8443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy\n  # To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.\n\nminikube kubectl -- explain deployment.metadata.labels\n```\n```bash\nminikube kubectl -- apply -f telnet-server/kubernetes/\n\nminikube kubectl -- get deployments.apps telnet-server\nminikube kubectl -- get pods -l app=telnet-server\n\nminikube kubectl -- get services -l app=telnet-server\n```\n\n```bash\nminikube tunnel\n  # ...\n\nminikube kubectl -- get services telnet-server\n  # OJO con EXTERNAL-IP (10.105.23.82)\n\ntelnet 10.105.23.82 2323\n  # d\n  # q\n\n# minikube kubectl -- get endpoints -l app=telnet-server\n```\n\n```bash\nminikube kubectl -- get pods -l app=telnet-server\nminikube kubectl -- delete pod \u003ctelnet-server-775769766-2bmd5\u003e\nminikube kubectl -- get pods -l app=telnet-server\n```\n\n```bash\n# Para **escalar**: modificar los archivos bajo control de versiones y comando `apply`. Igualmente así se hace por comandos, pero mejor evitar esta práctica:\nminikube kubectl -- scale deployment telnet-server --replicas=3\n```\n\n```bash\nminikube kubectl -- logs\nminikube kubectl -- logs -l app=telnet-server --all-containers=true --prefix=true\n```\n\n\n##### 2.5 (Ch. 8) Desplegando y testeando código (Skaffold CI/CD)\n\nPrimero instalamos **Skaffold**, **container-structure-test** y **Go**:\n\n```bash\ncurl -Lo skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-linux-amd64 \u0026\u0026 \\\nsudo install skaffold /usr/local/bin/\n\ncurl -LO https://github.com/GoogleContainerTools/container-structure-test/releases/latest/download/container-structure-test-linux-amd64 \u0026\u0026 \\\nchmod +x container-structure-test-linux-amd64 \u0026\u0026 \\\nsudo mv container-structure-test-linux-amd64 /usr/local/bin/container-structure-test\n\nsudo rm -rf /usr/local/go \u0026\u0026 \\\nsudo tar -C /usr/local -xzf go1.22.5.linux-amd64.tar.gz\nif echo $PATH | grep -qv /usr/local/go/bin; then\n    echo -e \"\\nexport PATH=$PATH:/usr/local/go/bin\" ~/.zshrc || \\\n    echo -e \"\\nexport PATH=$PATH:/usr/local/go/bin\" ~/.bashrc\nfi\n```\n\nRevisamos el archivo `telnet-server/skaffold.yaml`:\n\n```yaml\napiVersion: skaffold/v2beta19\nkind: Config\nbuild:\n  local: {}\n  artifacts:\n  - image: dftd/telnet-server\ntest:\n- image: dftd/telnet-server\n  custom:\n  - command: go test ./... -v\n  structureTests:\n  - ./container-tests/command-and-metadata-test.yaml\ndeploy:\n  kubectl:\n    manifests:\n    - kubernetes/*\n```\n\nRevisamos `telnet-server/container-tests/command-and-metadata-test.yaml`:\n\n```yaml\nschemaVersion: 2.0.0\ncommandTests:\n  - name: \"telnet-server\"\n    command: \"./telnet-server\"\n    args: [\"-i\"]\n    expectedOutput: [\"telnet port :2323\\nMetrics Port: :9000\"]\nmetadataTest:\n  envVars:\n    - key: TELNET_PORT\n      value: 2323\n    - key: METRIC_PORT\n      value: 9000\n  entrypoint: [\"./telnet-server\"]\n  workdir: \"/app\"\n```\n\n\u003c!-- build.sh --\u003e\n\nPonemos las herramientas a prueba:\n\n```bash\ncd telnet-server\nskaffold dev --cleanup=false\n  # Dejar la terminal abierta\n```\n\n\u003c!--\n```log\n[telnet-server] skaffold dev --cleanup=false                                                              devel  ✱\nGenerating tags...\n - dftd/telnet-server -\u003e dftd/telnet-server:064aa01-dirty\nChecking cache...\n - dftd/telnet-server: Not found. Building\nStarting build...\nFound [minikube] context, using local docker daemon.\nBuilding [dftd/telnet-server]...\nTarget platforms: [linux/amd64]\nSending build context to Docker daemon   29.7kB\nStep 1/9 : FROM golang:alpine AS build-env\nalpine: Pulling from library/golang\nec99f8b99825: Already exists\n8bfb7f89ddd5: Already exists\n32a2f51ff3dd: Already exists\n935834aa092a: Already exists\n4f4fb700ef54: Already exists\nDigest: sha256:8c9183f715b0b4eca05b8b3dbf59766aaedb41ec07477b132ee2891ac0110a07\nStatus: Downloaded newer image for golang:alpine\n ... a60a31a97fdb\nStep 2/9 : ADD . /\n ... 38b276ee5b5e\nStep 3/9 : RUN cd / \u0026\u0026 go build -o telnet-server\n ... Running in cbcf9236a84d\ngo: downloading github.com/prometheus/client_golang v1.6.0\ngo: downloading github.com/beorn7/perks v1.0.1\ngo: downloading github.com/cespare/xxhash/v2 v2.1.1\ngo: downloading github.com/golang/protobuf v1.4.0\ngo: downloading github.com/prometheus/client_model v0.2.0\ngo: downloading github.com/prometheus/common v0.9.1\ngo: downloading github.com/prometheus/procfs v0.0.11\ngo: downloading google.golang.org/protobuf v1.21.0\ngo: downloading github.com/matttproud/golang_protobuf_extensions v1.0.1\ngo: downloading golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f\n ... 3eab8907fb04\nStep 4/9 : FROM alpine:latest as final\nlatest: Pulling from library/alpine\nec99f8b99825: Already exists\nDigest: sha256:b89d9c93e9ed3597455c90a0b88a8bbb5cb7188438f70953fede212a0c4394e0\nStatus: Downloaded newer image for alpine:latest\n ... a606584aa9aa\nStep 5/9 : WORKDIR /app\n ... Running in a78041e13836\n ... a5800a1d2290\nStep 6/9 : ENV TELNET_PORT 2323\n ... Running in 8f25e7e7e30d\n ... 3480f217d941\nStep 7/9 : ENV METRIC_PORT 9000\n ... Running in 74bf178e922a\n ... 1545c2369c8b\nStep 8/9 : COPY --from=build-env /telnet-server /app/\n ... 5ff7451e75f3\nStep 9/9 : ENTRYPOINT [\"./telnet-server\"]\n ... Running in 2d72c86cd620\n ... f64eee4f97d7\nSuccessfully built f64eee4f97d7\nSuccessfully tagged dftd/telnet-server:064aa01-dirty\nBuild [dftd/telnet-server] succeeded\nStarting test...\nTesting images...\n\n=======================================================\n====== Test file: command-and-metadata-test.yaml ======\n=======================================================\n=== RUN: Command Test: telnet-server\n--- PASS\nduration: 383.16803ms\nstdout: telnet port :2323\nMetrics Port: :9000\n\n=== RUN: Metadata Test\n--- PASS\nduration: 0s\n\n=======================================================\n======================= RESULTS =======================\n=======================================================\nPasses:      2\nFailures:    0\nDuration:    383.16803ms\nTotal tests: 2\n\nPASS\nRunning custom test command: \"go test ./... -v\"\ngo: downloading github.com/stretchr/testify v1.4.0\ngo: downloading github.com/davecgh/go-spew v1.1.1\ngo: downloading github.com/stretchr/objx v0.1.1\ngo: downloading gopkg.in/yaml.v2 v2.2.8\ngo: downloading github.com/pmezard/go-difflib v1.0.0\n?       telnet-server   [no test files]\n?       telnet-server/metrics   [no test files]\n=== RUN   TestServerRun\nMocked charge notification function\n    server_test.go:23: PASS:    Run()\n--- PASS: TestServerRun (0.00s)\nPASS\nok      telnet-server/telnet    0.006s\nCommand finished successfully.\nTags used in deployment:\n - dftd/telnet-server -\u003e dftd/telnet-server:f64eee4f97d7a6e0c3dcd6daf4d6b103ea1d50e34eebfdaf5c192fd34e3d4f88\nStarting deploy...\n - deployment.apps/telnet-server configured\n - service/telnet-server configured\n - service/telnet-server-metrics configured\nWaiting for deployments to stabilize...\n - deployment/telnet-server is ready.\nDeployments stabilized in 3.122 seconds\nListing files to watch...\n - dftd/telnet-server\nPress Ctrl+C to exit\nWatching for changes...\n[telnet-server] telnet-server: 2024/07/18 15:15:04 Metrics endpoint listening on :9000\n[telnet-server] telnet-server: 2024/07/18 15:15:04 telnet-server listening on [::]:2323\n[telnet-server] telnet-server: 2024/07/18 15:15:06 telnet-server listening on [::]:2323\n[telnet-server] telnet-server: 2024/07/18 15:15:06 Metrics endpoint listening on :9000\n```\n--\u003e\n\nHacemos cambios en el código:\n\n```bash\nsed -i 's/colorGreen, b/colorYellow, b/' telnet-server/telnet/banner.go\n\nkubectl get services telnet-server\n  # 10.105.23.82 (EXTERNAL-IP)\n\ntelnet 10.105.23.82 2323\n\n# AHORA SALE AMARILLO, se han actualizado los pods\n```\n\nKubernetes rollout...\n\n```bash\nkubectl rollout history deployment\n# kubectl rollout undo deployment telnet-server --to-revision=1\n```\n\n\n\n\n\n\n\u003c!-- ##### 2.4 (Ch. 7) Kubernetes... --\u003e\n\n\n\u003c!-- ### Proyecto 2. Terraform --\u003e\n\u003c!-- ### Proyecto 3. Kubernetes + CI/CD --\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpabloqpacin%2Fdevops_101","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpabloqpacin%2Fdevops_101","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpabloqpacin%2Fdevops_101/lists"}