Documentación

Toda la documentación de este sitio esta abierta a mejoras y correcciones. En el sidebar derecho tienes todas las opciones para contribuir.

Puede ser que parte del contenido lo haya publicado como entrada del Blog y no lo encuentres aquí reflejado.

1 - Contenedores

Sección que agrupa los conocimientos sobre tecnologías de contenedores como docker, kubernetes… etc.

1.1 - Docker

Una de las técnologías de contenedores más populares del momento

1.1.1 - Comandos docker

“Guía de comandos generales de docker”

Introducir usuario en el grupo docker

sudo usermod -a -G docker [nombre_usuario]

 

Refrescar grupo sin tener que reiniciar

newgrp docker

 

Buscar un contenedor para descargar

docker search [nombre_contenedor]

 

Instalar una imagen

docker pull [nombre_imagen]

 

Listar imágenes instaladas

docker images

Ver imágenes ejecutándose

docker ps

 

Iniciar una imagen

docker run [nombre_imagen]

  Para acceder al contenedor, además de crearlo, se puede hacer de dos maneras. Una es haciendo referencia al IMAGE ID y otra al repositorio (REPOSITORY) y la etiqueta (TAG).

docker run -i -t b72889fa879c /bin/bash
docker run -i -t ubuntu:14.04 /bin/bash

 

El usuario también puede ponerle una etiqueta personalizada que haga referencia a una imagen instalada en su sistema.

docker tag b72889fa879c oldlts:latest

 

Para crear el contenedor y ponerlo en marcha hay que seguir el mismo paso de antes, pero cambiando la referencia por la etiqueta creada por el usuario.

docker run -i -t oldlts:latest /bin/bash

 

Para iniciar un contenedor en modo demonio

docker run -d [identificador_imagen]

   

Como ya hemos comentado, cada vez que ejecutamos el comando run estamos creando un contenedor nuevo, por lo que lo recomendable es ejecutarlo tan solo una vez. Luego podemos listar los contenedores disponibles a través del siguiente comando.

docker ps -a

 

Hay dos maneras de poner en marcha el contenedor a través del mismo comando, pudiéndose utilizar su identificador (CONTAINER ID) o su nombre (NAMES).

docker start ef7e107e0aae
docker start lonely_wing

 

Si se quiere acceder (attach, que se podría traducir por adjuntar o unir) al contenedor se puede recurrir a una de estas dos opciones.

docker attach ef7e107e0aae
docker attach lonely_wing

Salir del terminal de docker sin apagarlo

Control + P & Control + Q 

Para detener un contenedor

docker stop ef7e107e0aae
docker stop lonely_wing

 

Para borrar un contenedor

docker rm ef7e107e0aae
docker rm lonely_wing

Parar todos los contenedores

docker stop $(docker ps -a -q)

Terminal de un contenedor arrancado

docker exec -ti f38197856de0 /bin/bash

 

Eliminar todos los contenedores

docker rm $(docker ps -a -q)

 

Eliminar todas las imágenes

docker rmi $(docker images -q)

 

Realizar commit de una imagen

docker commit -a "[información creador]" -m "[versión del programa]" [identificador_container] [nombre_repositorio:nombre_TAG]

 

Obtener la ruta del registro de un contenedor

docker inspect --format='{{.LogPath}}' $ID_CONTENEDOR

1.1.2 - Fundamentos

Primera entrada del curso de docker hablando sobre sus fundamentos.

Docker es un proyecto de código abierto que automatiza el despliegue de aplicaciones dentro de contenedores de software. Comenzó como un proyecto interno dentro de dotCloud, empresa enfocada a una plataforma como un servicio PaaS (Platform as a Service). Fué iniciado por Solomon Hykes con contribuciones de otros ingenieros de la compañia.

Docker fué liberado como código abierto en 2013. El 13 de marzo de 2014, con el lanzamiento de la versión 0.9 se dejó de utilizar LXC como entorno de ejecución por defecto y lo reemplazó con su propia biblioteca, libcontainer, escrito en Go. Para 2015 el proyecto ya tenía más 20.000 estrellas en GitHub y más de 900 colaboradores.

Pero… ¿Cómo funciona?

Docker se basa en la ejecución de procesos aislados entre sí y empaquetados en “contenedores” con todas las dependencias necesarias para funcionar.

Esto es posible gracias a dos funcionalidades del kernel de linux que se llaman “namespaces” y “cgroups”.

El soporte de los namescaces o espacios de nombres aísla la vista que tiene una aplicación de su entorno operativo,​ incluyendo árboles de proceso, red, ID de usuario y sistemas de archivos montados. Por otra parte, los cgroups del kernel proporcionan aislamiento de recursos, incluyendo la CPU, la memoria, el bloque de E/S y de la red.

Como resumen, se lanza un proceso aislado con todas las dependencias necesarias para que funcione.

Arquitectura de Docker

Hemos hablado de procesos y contenedores, pero esto es solo una pequeña pieza de todos los objetos que conforman Docker a día de hoy.

1.1.3 - Escáneres de vulnerabilidades

Herramientas que nos permiten analizar la seguridad en nuestros contenedores.

Introducción

Los contenedores nos han permitido la facilidad y comodidad de empaquetar nuestras aplicaciones y servicios, y también nos permiten asegurar que se ejecuten de forma segura. Sin embargo, las imágenes se contruyen con muchos componentes de terceros sobre los que no tenemos visibilidad. Para esta labor tenemos diferentes herramientas que nos ayudan a analizar la seguridad de nuestros contenedores.

Herramientas

Snyk - Docker Desktop

Es sin duda una de las más desconocidas debido a su reciente implementación en la plataforma de Docker pero, dada su integración nativa y que no es necesario realizar instalaciones adicionales, es una herramienta más que adecuada.

Tiene unas limitaciones de uso mensual pero podemos iniciar sesión con una cuenta gratuita para ampliarlo. Snyk.

Podemos utilizar esta herramienta simplemente escribiendo:

docker scan <imagen>

Otra forma de utilizarla, es con el parámetro “–dependency-tree”, el cuál muestra todo el árbol de dependencias de la images.

docker scan --dependency-tree <imagen>
     ├─ ca-certificates @ 20200601~deb10u1
     │  └─ openssl @ 1.1.1d-0+deb10u3
     │     └─ openssl/libssl1.1 @ 1.1.1d-0+deb10u3
     ├─ curl @ 7.64.0-4+deb10u1
     │  └─ curl/libcurl4 @ 7.64.0-4+deb10u1
     │     ├─ e2fsprogs/libcom-err2 @ 1.44.5-1+deb10u3
     │     ├─ krb5/libgssapi-krb5-2 @ 1.17-3
     │     │  ├─ e2fsprogs/libcom-err2 @ 1.44.5-1+deb10u3
     │     │  ├─ krb5/libk5crypto3 @ 1.17-3
     │     │  │  └─ krb5/libkrb5support0 @ 1.17-3
     │     │  ├─ krb5/libkrb5-3 @ 1.17-3
     │     │  │  ├─ e2fsprogs/libcom-err2 @ 1.44.5-1+deb10u3
     │     │  │  ├─ krb5/libk5crypto3 @ 1.17-3
     │     │  │  ├─ krb5/libkrb5support0 @ 1.17-3
     │     │  │  └─ openssl/libssl1.1 @ 1.1.1d-0+deb10u3
     │     │  └─ krb5/libkrb5support0 @ 1.17-3
     │     ├─ libidn2/libidn2-0 @ 2.0.5-1+deb10u1
     │     │  └─ libunistring/libunistring2 @ 0.9.10-1
     │     ├─ krb5/libk5crypto3 @ 1.17-3
     │     ├─ krb5/libkrb5-3 @ 1.17-3
     │     ├─ openldap/libldap-2.4-2 @ 2.4.47+dfsg-3+deb10u2
     │     │  ├─ gnutls28/libgnutls30 @ 3.6.7-4+deb10u4
     │     │  │  ├─ nettle/libhogweed4 @ 3.4.1-1
     │     │  │  │  └─ nettle/libnettle6 @ 3.4.1-1
     │     │  │  ├─ libidn2/libidn2-0 @ 2.0.5-1+deb10u1
     │     │  │  ├─ nettle/libnettle6 @ 3.4.1-1
     │     │  │  ├─ p11-kit/libp11-kit0 @ 0.23.15-2
     │     │  │  │  └─ libffi/libffi6 @ 3.2.1-9
     │     │  │  ├─ libtasn1-6 @ 4.13-3
     │     │  │  └─ libunistring/libunistring2 @ 0.9.10-1
     │     │  ├─ cyrus-sasl2/libsasl2-2 @ 2.1.27+dfsg-1+deb10u1
     │     │  │  └─ cyrus-sasl2/libsasl2-modules-db @ 2.1.27+dfsg-1+deb10u1
     │     │  │     └─ db5.3/libdb5.3 @ 5.3.28+dfsg1-0.5
     │     │  └─ openldap/libldap-common @ 2.4.47+dfsg-3+deb10u2
     │     ├─ nghttp2/libnghttp2-14 @ 1.36.0-2+deb10u1
     │     ├─ libpsl/libpsl5 @ 0.20.2-2
     │     │  ├─ libidn2/libidn2-0 @ 2.0.5-1+deb10u1
     │     │  └─ libunistring/libunistring2 @ 0.9.10-1
     │     ├─ rtmpdump/librtmp1 @ 2.4+20151223.gitfa8646d.1-2
     │     │  ├─ gnutls28/libgnutls30 @ 3.6.7-4+deb10u4
     │     │  ├─ nettle/libhogweed4 @ 3.4.1-1
     │     │  └─ nettle/libnettle6 @ 3.4.1-1
     │     ├─ libssh2/libssh2-1 @ 1.8.0-2.1
     │     │  └─ libgcrypt20 @ 1.8.4-5
     │     └─ openssl/libssl1.1 @ 1.1.1d-0+deb10u3
     ├─ gnupg2/dirmngr @ 2.2.12-1+deb10u1

1.2 - Kubernetes

Kubernetes

1.2.1 - Fundamentos

TODO

1.2.2 - Pods

Los pods se son una unidad de ejecución de contenedores, concretamente la unidad más pequeña con la que se puede trabajar en kubernetes. Estos son los comandos básicos para usar un contenedor en Kubernetes.

Crear un pod

Especificaremos el nombre que le queremos asignar a ese pod y la imagen que utilizaremos.

kubectl run <nom_pod> --image=<imagen>

Ver un pod

kubectl get pods # Listar todos los pods en el cluster
kubectl get pods -o wide  # Listar los pods en una tabla más amplia
kubectl get pods <nom_pod> # Listar el pod especificado
kubectl describe pods <nom_pod> # Describe el pod nginx
kubectl describe pods <nom_pod> --yaml  # Nos devuelve todo el manifiesto del pod

Al hacer un describe del pod veríamos la siguiente salida:

Name:         nginx
Namespace:    default
Priority:     0
Node:         minikube/192.168.49.2
Start Time:   Mon, 20 Dec 2021 20:12:08 +0100
Labels:       run=nginx
Annotations:  <none>
Status:       Running
IP:           172.17.0.3
IPs:
  IP:  172.17.0.3
Containers:
  nginx:
    Container ID:   docker://f19cee240b99b737dc71db300dcfe2ad51a1596b35b2861aea274820aa841530
    Image:          nginx
    Image ID:       docker-pullable://nginx@sha256:9522864dd661dcadfd9958f9e0de192a1fdda2c162a35668ab6ac42b465f0603
    Port:           <none>
    Host Port:      <none>
    State:          Running
      Started:      Mon, 20 Dec 2021 20:12:14 +0100
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-58m5c (ro)
Conditions:
  Type              Status
  Initialized       True 
  Ready             True 
  ContainersReady   True 
  PodScheduled      True 
Volumes:
  kube-api-access-58m5c:
    Type:                    Projected (a volume that contains injected data from multiple sources)
    TokenExpirationSeconds:  3607
    ConfigMapName:           kube-root-ca.crt
    ConfigMapOptional:       <nil>
    DownwardAPI:             true
QoS Class:                   BestEffort
Node-Selectors:              <none>
Tolerations:                 node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
                             node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  43s   default-scheduler  Successfully assigned default/nginx to minikube
  Normal  Pulling    42s   kubelet            Pulling image "nginx"
  Normal  Pulled     37s   kubelet            Successfully pulled image "nginx" in 4.842081346s
  Normal  Created    37s   kubelet            Created container nginx
  Normal  Started    37s   kubelet            Started container nginx

Destruir un pod

kubectl delete pod nginx

Problemas de los pods

No saben restaurarse ni replicarse a si mismos. Necesitan de alguien que gestione estos procesos. Para esto se utilizan otro tipo de elementos: Replicasets

1.2.3 - Manifiestos y etiquetas

Por debajo de todas las acciones de kubernetes, lo que el motor entiende, son archivos manifiesto que definen el tipo de cada elemento.

Cuando se coge cierta experiencia se dejan de usar comandos para usar manifiestos y poder aplicar varios a la vez, haciendo el proceso menos tedioso.

Obtener el manifiesto de un pod

kubectl get pods <nombre pod> -o <formato>

Existen varios formatos de salida pero los más comunes son yaml (el usado nativamente por kubernetes), json, name, go-template (para customizaciones)… https://kubernetes.io/docs/reference/kubectl/overview/#custom-columns

Definir un pod en un manifiesto

Creamos la definición de un pod de prueba que escribirá “Hello world” cada hora:

apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
  labels:
    app: myapp
spec:
  containers:
  - name: myapp-container
    image: busybox
    command: ['sh', '-c', 'echo Hello World!; sleep 3600']

Luego podríamos crear el elemento con el comando:

kubectl apply -f pods.yaml

También podríamos eliminarlo usando el manifiesto con el comando:

kubectl deletec-f pods.yaml

Multiples contenedores en un pod

Podemos definir varios contenedores en un pod. En este ejemplo podemos ver el balanceo que hace kubernetes a nivel de red entre los distintos contenedores de un pod.

apiVersion: v1
kind: Pod
metadata:
  name: doscont
spec:
  containers:
  - name: cont1
    image: python:3.6-alpine
    command: ['sh', '-c', 'echo "cont1 > index.html" && python -m http.server 8082']
  - name: cont2
    image: python:3.6-alpine
    command: ['sh', '-c', 'echo "cont2 > index.html" && python -m http.server 8083']

Labels

Podemos usar etiquetas para filtrar recursos en proyectos de cierto tamaño. Por ejemplo, separando frontend de backend:

apiVersion: v1
kind: Pod
metadata:
  name: podtest2
  labels:
    app: front
    env: dev
spec:
  containers:
  - name: cont1
    image: nginx:alpine
---
apiVersion: v1
kind: Pod
metadata:
  name: podtest3
  labels:
    app: back
    env: dev
spec:
  containers:
  - name: cont1
    image: nginx:alpine

Ahora para filtrar desde el cli podríamos usar el parámetro “-l” para ello de la siguiente manera:

kubectl get pod -l app=front

Podríamos filtrar por cualquier variable que hayamos definido, también “env”:

kubectl get pod -l env=dev

Incluso multiples labels a la vez:

kubectl get pod -l app=front,env=dev

1.2.4 - Replicasets

Por debajo de todas las acciones de kubernetes, lo que el motor entiende, son archivos manifiesto que definen el tipo de cada elemento.

Cuando se coge cierta experiencia se dejan de usar comandos para usar manifiestos y poder aplicar varios a la vez, haciendo el proceso menos tedioso.

Obtener el manifiesto de un pod

kubectl get pods <nombre pod> -o <formato>

Existen varios formatos de salida pero los más comunes son yaml (el usado nativamente por kubernetes), json, name, go-template (para customizaciones)… https://kubernetes.io/docs/reference/kubectl/overview/#custom-columns

Definir un pod en un manifiesto

Creamos la definición de un pod de prueba que escribirá “Hello world” cada hora:

apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
  labels:
    app: myapp
spec:
  containers:
  - name: myapp-container
    image: busybox
    command: ['sh', '-c', 'echo Hello World!; sleep 3600']

Luego podríamos crear el elemento con el comando:

kubectl apply -f pods.yaml

También podríamos eliminarlo usando el manifiesto con el comando:

kubectl deletec-f pods.yaml

Multiples contenedores en un pod

Podemos definir varios contenedores en un pod. En este ejemplo podemos ver el balanceo que hace kubernetes a nivel de red entre los distintos contenedores de un pod.

apiVersion: v1
kind: Pod
metadata:
  name: doscont
spec:
  containers:
  - name: cont1
    image: python:3.6-alpine
    command: ['sh', '-c', 'echo "cont1 > index.html" && python -m http.server 8082']
  - name: cont2
    image: python:3.6-alpine
    command: ['sh', '-c', 'echo "cont2 > index.html" && python -m http.server 8083']

Labels

Podemos usar etiquetas para filtrar recursos en proyectos de cierto tamaño. Por ejemplo, separando frontend de backend:

apiVersion: v1
kind: Pod
metadata:
  name: podtest2
  labels:
    app: front
    env: dev
spec:
  containers:
  - name: cont1
    image: nginx:alpine
---
apiVersion: v1
kind: Pod
metadata:
  name: podtest3
  labels:
    app: back
    env: dev
spec:
  containers:
  - name: cont1
    image: nginx:alpine

Ahora para filtrar desde el cli podríamos usar el parámetro “-l” para ello de la siguiente manera:

kubectl get pod -l app=front

Podríamos filtrar por cualquier variable que hayamos definido, también “env”:

kubectl get pod -l env=dev

Incluso multiples labels a la vez:

kubectl get pod -l app=front,env=dev

1.2.5 - Deployments

Los deployments son elementos de configuración que permiten la creación de una aplicación de una sola instancia.

El deployment gestiona uno o varios objetos replicaset y estos a su vez gestionan uno o más pods.

Definición de un deployment

Este es un ejemplo de su estructura básica:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80

Aplicaría la configuración anterior con el comando:

kubectl apply -f deployment.yaml

Este deployment gestionaría los servicios de replicaset y los contenedores de nginx definidos.

Podríamos consultar el estado del deployment con el comando:

kubectl get deployment nginx-deployment

El cual nos devolvería una salida similar a la siguiente:

NAME                            READY   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment                3       3            3           2m

Crear un deployment con comandos

También podemos crear un deployment con una sola instancia con el comando:

kubectl create deployment nginx --image=nginx

Actualizar un deployment

Supongamos que queremos actualizar el deployment para que gestione una nueva imagen, concretamente, las de nginx basadas en alpine. El yaml de configuración quedaría así:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80

Aplicamos los cambios de nuevo con el comando ‘kubectl apply -f deployment.yaml’ y esta vez nos devuelve que se ha configurado, en vez de crearse:

kubectl apply -f deployment.yaml
deployment.apps/deployment-test configured #Salida del comando

Además, kubernetes gestiona las actualizaciones de los deployment para que sean progresivo entre un cambio de versión y el servicio de que dan los pods no se interrumpa.

Se puede consultar la actualización del deployment en tiempo real con el comando:

kubectl rollout status deployment nginx-deployment

Este comando nos devolvería paso a paso la actualización del deployment:

Waiting for deployment "nginx-deployment" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "nginx-deployment" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "nginx-deployment" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "nginx-deployment" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "nginx-deployment" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "nginx-deployment" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "nginx-deployment" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "nginx-deployment" rollout to finish: 1 old replicas are pending termination...
deployment "nginx-deployment" successfully rolled out

Si el despliegue ya ha terminado no se mostrará este proceso de actualización. Aun así, podremos consultarlo en el registro de eventos usando el comando:

kubectl describe deployment nginx-deployment
Events:
  Type    Reason             Age    From                   Message
  ----    ------             ----   ----                   -------
  Normal  ScalingReplicaSet  4m27s  deployment-controller  Scaled up replica set nginx-deployment-59c46f7dff to 3
  Normal  ScalingReplicaSet  4m8s   deployment-controller  Scaled up replica set nginx-deployment-5c4d5dcbf5 to 1
  Normal  ScalingReplicaSet  4m4s   deployment-controller  Scaled down replica set nginx-deployment-59c46f7dff to 2
  Normal  ScalingReplicaSet  4m4s   deployment-controller  Scaled up replica set nginx-deployment-5c4d5dcbf5 to 2
  Normal  ScalingReplicaSet  4m2s   deployment-controller  Scaled down replica set nginx-deployment-59c46f7dff to 1
  Normal  ScalingReplicaSet  4m2s   deployment-controller  Scaled up replica set nginx-deployment-5c4d5dcbf5 to 3
  Normal  ScalingReplicaSet  4m     deployment-controller  Scaled down replica set nginx-deployment-59c46f7dff to 0

Escalar un deployment

Podemos escalar el deployment con el comando:

kubectl scale deployment nginx-deployment --replicas=5

Historial de un deployment

Podemos consultar el historial de un deployment con el comando:

kubectl rollout history deployment nginx-deployment

Modificar el límite del historial de un deployment

Por defecto, el historial de un deployment muestra las últimas 10 actualizaciones a menos que modifiquemos el valor ‘revisionHistoryLimit’ en los spec del deployment. Por ejemplo:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
 revisionHistoryLimit: 3
  replicas: 3
  selector:
    matchLabels:
      app: nginx
...

Hacer un rollback a una versión anterior

Es posible hacer un rollback a una versión anterior de un deployment, ya sea porque el último despliegue no funcione o la aplicación tenga errores inesperados. Se podría hacer con el comando:

kubectl rollout undo deployment nginx-deployment

Este comando hace un rollback a la versión anterior del deployment. También podríamos especificarle una versión específica:

kubectl rollout undo deployment nginx-deployment --to-revision=1

Pausar y reanudar un deployment

Podemos pausar un deployment con el comando:

kubectl rollout pause deployment nginx-deployment

Para reanudar un deployment usaremos el comando:

kubectl rollout resume deployment nginx-deployment

1.2.6 - Services

Los servicios en kubernetes son una forma de agrupar pods mediande sus etiquetas o labels y disponer a los usuarios el acceso a los recursos que están asociados a ellos.

Los pods en kubernetes son efímeros y cambiaran frecuentemente, con ellos, tambien sus IPs por lo que los servicios entregan una IP única (también tiene DNS), además de balancear las peticiones entre los pods que están asociados a un servicio.

Partiendo del deployment anterior podemos crear un servicio de la misma forma:


apiVersion: v1
kind: Service
metadata:
  name: nginx-service
  labels:
    app: nginx
spec:
  selector:
	app: nginx
  ports:
  - port: 8080
	targetPort: 80

Es importante destacar que el selector del servicio tiene que igual al label del deployment para que este funcione.

Haciendo foco en la declaración del servicio tambien hay que destacar que, la instrucción port indica el puerto al que va a escuchar el servicio, y targetPort indica el puerto del pod al que el servicio va a enviar las peticiones.

Podemos consultar el estado del servicio con el comando:

kubectl get service nginx-service
kubectl get svc nginx-service # Podemos abreviar el comando anterior

Podemos describir el servicio con el comando:

kubectl describe svc nginx-service

Esto nos devolvería una salida similar a la siguiente:

Name:              nginx-service
Namespace:         default
Labels:            app=front
Annotations:       <none>
Selector:          app=front
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                10.98.234.17
IPs:               10.98.234.17
Port:              <unset>  8080/TCP
TargetPort:        80/TCP
Endpoints:         172.17.0.3:80,172.17.0.4:80,172.17.0.5:80
Session Affinity:  None
Events:            <none>

Endpoints

Uno de los datos más interesantes de la salida anterior en el campo Endpoints. Este recoge las IPs de los pods con los que esta conectando el servicio para automatizar que el usuario pueda acceder a ellos.

Además, si algún pod nuevo aparece o desaparece el servicio sabría y actualizaría los endpoints.

También podríamos listar todos los endpoints del namespace con el commando:

kubectl get endpoints

Podríamos abreviar el comando anterior (con ep) y a la vez consultar específicamente el endpoint de un servicio:

kubectl get ep nginx-service

Tipos de servicios

La jerarquía de los servicios es la siguiente:

Jerarquía de servicios

ClusterIP

Es el servicio por defecto en kubernetes, en caso de que no especifiquemos ningún otro. Su función es crear una conexión a los pods sin exponerlos a la red externa.

Si listamos los servicios podemos ver que, el servicio nginx-service que lanzamos antes, tiene la IP del cluster asignada pero la IP externa se queda con el valor none.

kubectl get service                                                       
NAME            TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)     AGE
kubernetes      ClusterIP   10.96.0.1      <none>        443/TCP     19d
nginx-service   ClusterIP   10.98.234.17   <none>        38080/TCP   10h

NodePort

Es un servicio que conecta un puerto de nodo (red externa) a un puerto de uno o más pods.

Si no le especificamos un puerto, el servicio utiliza uno en el rango del 30000 al 32767.

Para crear un servicio de tipo NodePort solo tenemos que indicarlo en el type dentro del spec del servicio:

apiVersion: 1                                                                      
kind: Service
metadata:
  name: nginx-service
  labels:
    app: front
spec:
  type: NodePort # Definición del tipo de servicio
  selector:
    app: front
  ports:
  - port: 8080
    targetPort: 80

Este tipo también crea una IP del cluster, pero en este caso también abre un puerto a la red externa.

LoadBalancer

Sirve para exponer servicios a través de la red externa. Podría ser utilizado para exponer servicios web, servicios de bases de datos, etc.

Se podría definir un servicio de tipo LoadBalancer con el siguiente comando para un deployment específico:

kubectl expose deployment nginx-deployment --type=LoadBalancer

Accediendo a una aplicación con un servicio

Podríamos crear un servicio vía kubectl:

kubectl expose deployment/nginx --port=80 --type=NodePort

Podemos consultar los servicios con los siguientes comandos:

kubectl get svc # Listar todos los servicios

kubectl get svc nginx -o yaml # Listar un servicio concreto

Borrar endpoints

Podemos borrar un endpoint con el comando:

kubectl delete endpoint nginx
kubectl delete ep nginx # Podemos abreviar el comando anterior

1.2.7 - Ingress Controller

El ingress controller es un servicio que se ejecuta en un pod y que permite observar los objetos endpoint. Cuando un nuevo objeto es creado, ingress controller lo detecta y aplica las reglas que tenga definidas para enrutar el tráfico (normalmente HTTP).

En resumen, permite enrutar tráfico desde fuera de un cluster a los servicios del mismo.

Cualquier tecnología que sirviera como proxy inverso se puede utilizar como ingress controller. Uno de los más comunes es nginx.

Ejemplos de configuración de nginx para diferentes plataformas ( docker desktop, minikube, AWS, GCP, Azure…)

Instalación de un ingress controller

Podemos instalar el ingress controller basado en nginx con helm. En esta página tengo la documentación sobre Helm.

Primero añadimos el repositorio de ingress-nginx y actualizamos:

 helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
 helm repo update

Descargamos el chart:

helm fetch ingress-nginx/ingress-nginx --untar

Modificamos el fichero values.yaml y en la línea que pone kind: Deployment actualizamos el valor por DaemonSet quedando así:

## DaemonSet or Deployment
kind: DaemonSet

Instalamos el chart que acabamos de modificar:

helm install myingress .

Ahora ya podemos añadir objetos de tipo ingress en kubernetes.

Manifiesto de kubernetes

Podemos declarar el objeto del manifiesto de kubernetes como en el siguiente ejemplo:

apiVersion: networking.k8s.io/v1beta1 
kind: Ingress 
metadata:
rules:
  - host: <hostname> 
    http:
      paths:
      - backend:
          service:
            name: <nombre> 
            port:
              number: <puerto> 
        path: /
        pathType: ImplementationSpecific

Gestión de objetos ingress

Los principales comando de gestión son:

kubectl get ingress
kubectl delete ingress <nombre>
kubectl edit ingress <nombre>

1.2.8 - Jobs

Los Jobs en kubernetes son una forma de automatizar tareas en kubernetes. A diferencia de los Pods, los Jobs tienen número de ejecuciones definido y un tiempo de ejecución limitado.

Estos recursos se suelen utilizar para tareas de mantenimiento que se ejecutan de forma puntual y recurrente.

Sintaxis básica

Este sería un ejemplo de sintáxis básica de un Job:

apiVersion: batch/v1
kind: Job
metadata:
  name: test-job 
spec:
  completions: 5 # Número de ejecuciones
  template:
    spec:
      containers:
      - name: test 
        image: busybox
        command: ["/bin/sleep"]
        args: ["3"]
      restartPolicy: Never

El parámetro diferenciador del Jobs frente a los Pods es el completions. Este define el número de ejecuciones que se realizarán y una vez que se alcanza el número de ejecuciones, el Job se detendrá.

Si vemos el estado de un Job en kubernetes, podemos ver que está en estado Pending si no se ha iniciado, Running si se está ejecutando y Succeeded si se ha terminado con éxito.

1.2.9 - Namespaces y context

Namespaces

Los namespaces son una forma de agrupar y aislar los recursos de kubernetes. Esto permite que podamos segregar los diferentes recursos de una aplicación ( pod, deployment, service, etc) para establecer unas cuotas recursos, políticas de seguridad y configuraciones específicas.

Por omisión, kubernetes crea un namespace llamado default que es el namespace por defecto.

Listar namespaces

Podemos listar los namespaces con el comando:

kubectl get ns

Esto nos muestra nuestro namespace por defecto y los namespaces del sistema de kubernetes (no tocar estos namespaces):

NAME              STATUS   AGE
default           Active   26d
kube-node-lease   Active   26d
kube-public       Active   26d
kube-system       Active   26d

Puede ser interesante listar los namespaces junto con sus `labels``:

kubectl get ns --show-labels

Este comando nos muestra nuestros namespaces de la siguiente forma:

NAME              STATUS   AGE   LABELS
default           Active   26d   kubernetes.io/metadata.name=default
kube-node-lease   Active   26d   kubernetes.io/metadata.name=kube-node-lease
kube-public       Active   26d   kubernetes.io/metadata.name=kube-public
kube-system       Active   26d   kubernetes.io/metadata.name=kube-system

Crear un namespace

Podemos crear un namespace simplemente con el comando:

kubectl create ns <nombre-namespace>

Aún así, también también lo podemos crear con un fichero de configuración (este en formato json) para dejarlo definido como código:

{
  "apiVersion": "v1",
  "kind": "Namespace",
  "metadata": {
    "name": "development",
    "labels": {
      "name": "development"
    }
  }
}

Para utilizar este json de configuración podemos utilizar el comando:

kubectl create -f <fichero-json>

Borrar un namespace

Podemos borrar un namespace con el comando:

kubectl delete ns <nombre-namespace>

Cambiar de namespace y contextos

Podríamos ejecutar comandos en cualquier namespace añadiendo el parámentro --namespace o -n a cualquier comando, por ejemplo:

kubectl get pods --namespace development

El proceso anterior sería más farragoso, excepto que queramos lanzar un comando puntual en un namespace concreto, es más recomendable utilizar la configuración de contexto de kubectl:

kubectl config set-context <nombre-context> --namespace=<nombre-namespace>

Así estaríamos asociando un namespace a un contexto.

Context

Los contextos en kubernetes permiten definir a nuestro cliente diferentes entornos a los que conectarse. Estos entornos puedes ser namespaces o clusters diferentes.

Podemos ver nuestra configuración con el comando:

kubectl config view

Esto nos mostrará nuestra configuración de organizada de la siguiente manera:

apiVersion: v1
kind: Config
preferences: {}

clusters:
- cluster:
  name: development
- cluster:
  name: scratch

users:
- name: developer
- name: experimenter

contexts:
- context:
  name: dev-frontend
  user: developer
  cluster: development
- context:
    cluster: scratch
    namespace: develop
    user: experimenter
  name: scratch-frontend

Podemos distinguir tres elementros de la configuración:

  • Clusters: definen los clusters a los que podemos conectarnos.
  • Users: definen los usuarios que podemos utilizar para conectarnos a los clusters.
  • Contexts: definen los contextos a los que podemos conectarnos. Estos contextos guardan la relación de usuario, cluster y namespace.

Esta organización nos permite definir clusters y usuarios individualmente y luego ir asociándolos en contexto concretos.

Este fichero de configuración se suele alojar en el directorio ~/.kube/config. Podemos añadir una nueva configuración editando el fichero o usando el comando:

kubectl config set-context <nombre del contexto> --namespace=<nombre namespace OPCIONAL> \
  --cluster=<nombre del cluster> \
  --user=<usuario>

Definir usuario

Se puede definir un usuario con el comando en nuestro fichero de configuración con el siguiente comando:

kubectl config set-credentials <nombre-usuario> --client-certificate=<certificado> --client-key=<clave>

Definir un cluster

También podemos definir por comandos clusters:

kubectl config set-cluster <nombre-cluster> --server=<url del cluster> --certificate-authority=<certificado-autoridad>

1.2.10 - Secrets y configmaps

Kubernetes nos permite compartir información y configuraciones entre el cluster y los distintos recursos de kubernetes.

Secrets

Los secretos en kubernetes son una forma de almacenar información sensible. Estos se almacenan en una base de datos de forma privada y pueden consumir por otros recursos de kubernetes.

Podemos obtener, crear o eliminar secretos en kubernetes con los siguientes comandos.

Listar secretos:

kubectl get secrets

Crear secretos:

# Create a new secret named my-secret with keys for each file in folder bar
kubectl create secret generic my-secret --from-file=path/to/bar

# Create a new secret named my-secret with specified keys instead of names on disk
kubectl create secret generic my-secret --from-file=ssh-privatekey=path/to/id_rsa --from-file=ssh-publickey=path/to/id_rsa.pub

# Create a new secret named my-secret with key1=supersecret and key2=topsecret
kubectl create secret generic my-secret --from-literal=key1=supersecret --from-literal=key2=topsecret

# Create a new secret named my-secret using a combination of a file and a literal
kubectl create secret generic my-secret --from-file=ssh-privatekey=path/to/id_rsa --from-literal=passphrase=topsecret

# Create a new secret named my-secret from an env file
kubectl create secret generic my-secret --from-env-file=path/to/bar.env

Borrar un secreto:

kubectl delete secret <nombre>

Usando secretos en un pod

Un secreto se puede usar en un pod. Podríamos pasarlo como una variable de entorno como en el siguiente ejemplo:

...
spec:
    containers:
    - image: mysql:5.5
      name: dbpod
      env:
      - name: MYSQL_ROOT_PASSWORD
        valueFrom:
            secretKeyRef:
              name: mysql
              key: password 

También podríamos montarlo como un volumen en su manifest. Este Requeriría el path donde estamos montando un fichero con el contenido del secreto. Mounting Secrets as Volumes You can also mount secrets as files using a volume definition in a pod manifest. The mount path will contain a file whose name will be the key of the secret created with the kubectl create secret step earlier.

...
spec:
    containers:
    - image: busybox
      command:
        - sleep
        - "3600"
      volumeMounts:
      - mountPath: /mysqlsecret
        name: mysqlsecret
      name: busy
    volumes:
    - name: mysqlsecret
        secret:
            secretName: mmysql

Crea un configmap

Dado el caracter efímero de un pod en kubernetes necesitamos algún método para compartir ficheros o información entre los contenedores dentro de un pod.

Crear un configmap con comandos

Para crear un config map usamos el siguiente comando:

 kubectl create configmap <nombre> \
 --from-literal=text=<texto> \
 --from-file=<fichero> \
 --from-file=<directorio>

Este nos permite importar información ya sea text en plano que introduzcamos en el comando (--from-literal=text=), el contenido de un fichero (--from-file=) o el contenido de un directorio completo (--from-file=).

Podemos consultar el contenido de un configmap con el siguiente comando:

kubectl get configmap <nombre>

Aunque este solo nos mostrará el total de datos y su edad. Podemos obtener el contenido completo especificando la salida en formato yaml:

kubectl get configmap <nombre> -o yaml

Crear un configmap con yaml

Podríamos declararlo como un yaml para facilitar el almacenamiento de la configuración como código.

apiVersion: v1
kind: ConfigMap
metadata:
  name: cars
  namespace: default
data:
  car.make: Opel 
  car.model: Astra 
  car.trim: OPC

Para guardar el configmap en el cluster podemos usar el comando:

kubectl create -f <configmap>.yaml

Usar configmap en un pod

Utilizar en el entorno de un pod

Podemos configurar el contenido de un configmap en un pod como variable de entorno así:

apiVersion: v1
kind: Pod
metadata:
  name: demo 
spec:
  containers:
  - name: nginx
    image: nginx
    env:
    - name: <nombre de la variable de entorno> 
      valueFrom:
        configMapKeyRef:
          name: <nombre_configmap> 
          key: <clave a usar> 

Esto nos permitiría importar una única clave del configmap.

También podríamos importar todo el contenido del configmap así:

apiVersion: v1
kind: Pod
metadata:
  name: demo 
spec:
  containers:
  - name: nginx
    image: nginx
    envFrom:
    - configMapRef:
      name: <nombre_configmap> 

Cambiaríamos el configMapKeyRef por configMapRef y env por envFrom. Por último, borraríamos key y valueFrom datos que ya no tendríamos que especificar.

Montar como volumen en un pod

Podemos montar un configmap como un volumen en un pod. Esta sería una configuración de ejemplO:

apiVersion: v1
kind: Pod
metadata:
  name: shell-demo
spec:
  containers:
  - name: nginx
    image: nginx
    volumeMounts:
    - name: car-vol
      mountPath: /etc/cars
  volumes:
    - name: car-vol
      configMap:
        name: cars

Borrar un configmap

Podemos elimitar este objeto de kubernetes con el comando:

kubectl delete

1.2.11 - Volúmenes y cuotas

En kubernetes existe la posibilidad de crear volumenes para persistir los datos de los pods. Estos se agrupan en dos objetos:

  • PV: “Persistent Volume”, es la declaración de un espacio del host que el cluster va a reservar para su uso.
  • PVC: “Persistent Volume Claim” es la petición de reserva de espacio de un PV para un uso más especifico, por ejemplo, para un único proyecto.

Esta asiganación de espacio se realiza a dos niveles para reservar espacio para el cluster por un lado (PV) y luego se utilizan los objetos (PVC) para repartir ese espacio entre diferentes proyectos (namespaces) u objetos.

Persistent Volume (PV)

Estos permiten múltiples configuraciones en función del tipo de cluster. En entornos de nube lo normal suele ser usar almacenamiento nativo del proveedor. En este ejemplo lo haremos utilizando un volumen NFS totalmente válido para infraestructura no gestionada por un proveedor cloud.

En esta entrada explico como trabajar con NFS (solo es necesaria la parte de servidor). NFS en Linux

Una vez que tenemos configurado el volumen NFS podemos configurarlo como volumen persistente en kubernetes con una configuración como la siguiente:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pvvol
spec:
  capacity:
    storage: 40Gi
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  nfs:
    path: /ruta/carpeta/nfs
    server: <host>   #Puede ser un disco local o remoto
    readOnly: false

Persistent Volume Claim

Los claim sirve para hacer peticiones de espacio al cluster para que pueda ser consumido por un pod.

Podemos consular los PVC existentes con el comando:

kubectl get pvc

Para crear un objeto PVC podemos usar un fichero de configuración como el siguiente:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-test
spec:
  accessModes:
  - ReadWriteMany
  resources:
     requests:
       storage: 200Mi

Aplicaríamos la configuración anterior con el comando create:

kubectl create -f config_anterior.yaml

Ya tendríamos nuestro PVC. Este ahora podría ser consumido por un pod. Lo veremos en el siguiente punto.

Usar un PVC Persistent Volume Claim en un pod

Podríamos hacer que cualquier pod tuviera acceso a este PVC o volumen con una configuración como la siguiente:

...
spec:
	containers:
	- image: nginx
	imagePullPolicy: Always
	name: nginx
	volumeMounts:
	- name: nfs-vol
		mountPath: /opt
	ports:
	- containerPort: 80
		protocol: TCP
	resources: {}
	volumes:  # Concretamente todo lo del grupo volumes 
	- name: nfs-vol
	  persistentVolumeClaim:
		claimName: pvc-test # Importante usar el mismo nombre que la declaración del PVC
...

Una vez terminada la configuración del pod, aplicamos el yaml con el comando create:

kubectl create -f nfs-pod.yaml

Si hacemos un describe del pod podemos ver el montaje de este volumen:

...
Volumes:
  nfs-vol:
    Type:       PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
    ClaimName:  pvc-test
    ReadOnly:   false
...

ResourceQuota para limitar el uso de un PVC

El objeto encargado de crear cuotas de recursos es ResourceQuota. Este nos permite limitar el tamaño y número de PVC

TODO

1.2.12 - Helm

Helm es una herramienta que nos permite gestionar, versionar y desplegar múltiples recursos de kubernetes.

Los componentes en helm se estructuran de la siguiente manera:

├── Chart.yaml
├── README.md
├── templates
│   ├── NOTES.txt
│   ├── _helpers.tpl
│   ├── configmap.yaml
│   ├── deployment.yaml
│   ├── pvc.yaml
│   ├── secrets.yaml
│   └── svc.yaml
└── values.yaml

El fichero chart.yaml contiene los metadatos del chart, el values contiene las claves y atributos a modificar y el templates contienen los manifiestos de kubernetes.

Las templates se generan como un recurso nombre de kubernetes solo que plantillando las variables para que el chart sirva a diferentes propósitos y organizaciones. Por ejemplo:

apiVersion: v1
kind: Secret
metadata:
    name: {{ template "fullname" . }}
    labels:
        app: {{ template "fullname" . }}
        chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
        release: "{{ .Release.Name }}"
        heritage: "{{ .Release.Service }}"
type: Opaque
data:
    mariadb-root-password: {{ default "" .Values.mariadbRootPassword | b64enc | quote }}
    mariadb-password: {{ default "" .Values.mariadbPassword | b64enc | quote }}

Este elemento de tipo secreto esta plantillado para que todos sus campos se recojan del fichero values. Esto nos permite, centralizar todos los valores en un único fichero (values.yaml) y por otra parte, permitir que nuestros elementos se kubernetes sean reutilizables.

Este conjunto de elementos se llama chart y se pueden interactuar con ellos como repositorios de git.

Repositorios

Por defecto, helm busca charts dentro de la web de Artifactory Hub.

Podríamos buscar charts con el comando:

helm search hub <nombre del chart> #Buscar repositorios

helm search repo <nomrbre del repositorio> #Buscar dentro del repositorio

También podríamos añadir nuevos repositorios, por ejemplo, el de bitnami:

helm repo add bitnami ht‌tps://charts.bitnami.com/bitnami

Los repositorios que trae por defecto y los que añadimos nosotros manualmente, pueden actualizarse con el comando:

helm repo update

Si ahora quisieramos buscar charts solo dentro de este repositorio podríamos hacerlo así:

helm search repo bitnami

Desplegando un chart

Podríamos desplegar un chart con el comando helm install pero la mayoría de ellos necesitan una personalización para que funcionen correctamente por lo que primero debemos descargarlos en local para leer su README y modificar los valores pertinentes.

Esto lo podríamos hacer con el comando:

helm fetch <nombre repositorio> --untar

Tras modificar todo lo que nos resultara necesario, podemos lanzar el siguiente comando en la ruta del repositorio:

helm install <nombre del despliegue>

Podríamos desinstalarlo con el comando:

helm uninstall <nombre del despliegue>

También podríamos listar todos los charts desplegados y sus respectivas versiones con el comando:

helm list

1.2.13 - Mantenimiento Y actualización

El mantenimiento de kubernetes es una tarea que se realiza con frecuencia.

Backup base de datos etcd

Durante los procesos de actualización, por muy estables que sean, siempre es buena idea crear una copia de seguridad de la base de datos del cluster.

  1. Lo primero que tenemos que hacer es buscar el directorio de los datos de etcd:
sudo grep data-dir /etc/kubernetes/manifests/etcd.yaml

Toda esta parte se realiza ejecutando comandos dentro del contenedor de etcd. Se llama etcd-<nombre nodo> , aunque podrías listar los pods del sistema para encontrarlo con kubectl -n kube-system get pods.

  1. Comprobamos el estado de la base de datos de etcd:
kubectl -n kube-system exec -it etcd-<nombre_pod> -- sh -c "ETCDCTL_API=3 \
ETCDCTL_CACERT=/etc/kubernetes/pki/etcd/ca.crt \
ETCDCTL_CERT=/etc/kubernetes/pki/etcd/server.crt \
ETCDCTL_KEY=/etc/kubernetes/pki/etcd/server.key \
etcdctl endpoint health"
  1. Comprobamos el estado del cluster:
kubectl -n kube-system exec -it etcd-kube-master -- sh -c "ETCDCTL_API=3 \
ETCDCTL_CACERT=/etc/kubernetes/pki/etcd/ca.crt \
ETCDCTL_CERT=/etc/kubernetes/pki/etcd/server.crt \
ETCDCTL_KEY=/etc/kubernetes/pki/etcd/server.key \
etcdctl --endpoints=https://127.0.0.1:2379 member list -w table"
  1. Por último, hacemos la copia de seguridad con el comando snapshot:
kubectl -n kube-system exec -it etcd-kube-master -- sh -c "ETCDCTL_API=3 \
ETCDCTL_CACERT=/etc/kubernetes/pki/etcd/ca.crt \
ETCDCTL_CERT=/etc/kubernetes/pki/etcd/server.crt \
ETCDCTL_KEY=/etc/kubernetes/pki/etcd/server.key \
etcdctl --endpoints=https://127.0.0.1:2379 snapshot save /var/lib/etcd/snapshot.db"

Si hacemos un ls en directorio del paso 1 (normalmente /var/lib/etcd) podremos ver el la base de datos que acabamos de extraer:

sudo ls -l /var/lib/etcd

Actualizar el cluster

Lo primero es actualizar la herramienta kubeadm, la cuál, nos ayudará a actualizar el cluster.

  1. Actualizamos los metadatos de los paquetes del sistema con:
sudo apt update
  1. Podemos consultar las versiones disponibles con la herramienta madison las versiones disponibles con la herramienta madison:
sudo apt-cache madison kubeadm
  1. Si teníamos bloqueado el paquete kubeadm para que no se actualizara automáticamente, lo desbloqueamos:
sudo apt-mark unhold kubeadm
  1. Instalamos la versión deseada:
sudo apt install -y kubeadm=1.23.1-00
  1. Volvemos a bloquear la actualización del paquete:
sudo apt-mark hold kubeadm
  1. Podemos comprobar la versión que accabamos de instalar:
sudo kubeadm version
  1. Para preparar el nodo para la actualización, tenemos que desalojar a todos los pods como sea posible (si estuvieramos actualizando un nodo trabajador el drain tendríamos que hacerlo desde el maestro). Se puede realizar así:
kubectl drain <nombre_nodo> --ignore-daemonsets
  1. El comando kubeadm nos permite previsualizar los cambios que va a generar la actualización con el comando plan:
sudo kubeadm upgrade plan
  1. Podemos realizar la actualización del nodo con el comando apply:
sudo kubeadm upgrade plan
  1. Actualizamos el resto de paquetes a la misma version:
sudo apt-mark unhold kubelet kubectl
sudo apt-get install -y kubelet=1.23.1-00 kubectl=1.23.1-00
sudo apt-mark hold kubelet kubectl
  1. Aunque hemos actualizado correctamente, si ejecutamos kubectl get nodes nos seguirá mostrando la versión anterior. La actualización se hará efectiva hasta que reiniciemos los servicios:
sudo systemctl daemon-reload
sudo systemctl restart kubelet
  1. Por último, en el proceso de actualización de un nodo, este desactiva el planificador de tareas. Podemos desbloquearlo así:
kubectl uncordon <nombre_nodo>

Se puede verificar el estado con el comando:

kubectl get nodes

2 - Pentesting

Sección dedicada a la parte de pentesting

2.1 - Escalada privilegios

Seccion dedicada al reconocimiento activo de información contra un sistema.

2.1.1 - Contenedores

Introducción

Los contenedores son procesos aislados que, por defecto, ¿se podrían considerar como seguros?. Su enfoque nos dice que sí pero existen muchos casos en los que, principalmente por malas configuraciones, podrían ser vulnerables.

Aislados pero no herméticos - Posibles malas configuraciones

Tecnologías de contenedores como Docker, LXC, LXD, etc.. permiten a los usuarios lanzar un proceso aislado pero, existen multiples funcionalidades, que podrían comprometer la aplicación en mayor o menor medida:

Montaje de volúmenes

Esta funcionalidad permite montar un volumen en un contenedor. Un volumen puede ser una carpeta o archivo en el sistema de archivos del host o un filesystem aislado que cree docker junto con el contenedor. Los volúmenes se suelen utilizar para dar persistencia a los datos de un contenedor y así evitar cuando se para o se vuelve a desplegar un contenedor los datos ser pierdan.

Cuando montamos como volumen parte del host en un contenedor tenemos que tener en cuenta el usuario que ejecuta el engine de docker y el grupo de permisos y, por otra parte, el usuario que ejecuta el contenedor. Si montamos ficheros del hosts sensibles y los montamos en cualquier contenedor que ejecute el usuario root, este usuario sería capaz de acceder a los ficheros. Es importante que los contenedores no utilicen volúmenes sensibles, montar ficheros o directorios muy específicos y que los contenedores no utilicen el usuario root.

También se podría cambiar el usuario que ejecuta el engine de docker pero acarreo muchos problemas de funcionamiento a día de hoy y no lo recomiendo. Lo ideal es crear un usuario en el host y asignarle la propiedad de los archivos que queremos montar en el contenedor y, a su vez, ejecutar el contenedor con dicho usuario.

Si por ejemplo ejecutamos un contenedor montado un directorio del host (en este caso /etc) y ejecutamos el contenedor como root, este podrá leerlo y modificarlo sin problemas.

docker run -it -v /etc:/host busybox sh  

cat /host/shadow # Comando dentro del contenedor
root:*:18970:0:99999:7:::
...
systemd-timesync:*:18970:0:99999:7:::
systemd-network:*:18970:0:99999:7:::
systemd-resolve:*:18970:0:99999:7:::
strike:<CENSORED>:18986:0:99999:7:::
...

En mi linux tengo un usuario strike que no tiene permiso de root. Vamos a ejecutar este contenedor con este usuario para entender que docker arrastra los permisos de archivos del host a los contenedores.

Primero hago un cat /etc/passwd para obtener el uid de mi usuario strike:

cat /etc/passwd                                                                                           
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...
strike:x:1000:1000:,,,:/home/strike:/usr/bin/zsh
...

Sabiendo que el uid de mi usuario strike es 1000, vamos a ejecutar el contenedor con este usuario (especificamos el usuario con el parámetro -u <uid>):

docker run -it -u 1000 -v /etc/:/host busybox sh

cat /host/shadow # Comando dentro del contenedor> $ docker run -it -u 1000 -v /etc/:/host busybox 
cat: cant open host/shadow: Permission denied
/ $

Es importante entender esto para no comprometer la seguridad de los archivos del host. Por eso hay que evitar utilizar el usuario root en los contenedores y, por otra parte, evitar montar ficheros sensibles.

Ejecución de contenedores en modo privilegiado

Este modo de ejecución permite a un contenedor acceder a ciertos recursos que, por defecto, estan restringidos. Este modo se activa con la opción --privileged en el comando run de un contenedor.

Esto permitiría acceder al hardware del host y los recursos de red. Podría montar dispositivos como USB, interfaces de red.. etc.

Dicho esto, la forma más sencilla de escalar privilegios es montando el disco del host y buscando secretos u otros accesos:

# Dentro del contenedor privilegiado suponiendo que el disco del hosts se llama /dev/sda
mkdir -p /mnt/hola
mount /dev/sda1 /mnt/hola

Hay más formas pero he documentado la más sencilla e interesante. En esta referencia podéis encontrar más formas: Documentación de Hacktricks

Escalado a través del grupo de docker

Imaginaros ahora que hemos accedido a un hosts del que no somos root pero tiene docker instalado y nuestro usuario tiene permisos para ejecutar docker. Podríamos ejecutar un contenedor en modo privilegiado para acceder a los recursos del host y conseguir escalar.

docker run -it -v /:/host/ debian chroot /host/ bash

Host vulnerable

Aunque no es muy frecuente, aparecen vulnerabilidades en las tecnologías, ya sea en docker, el kernel de linux, etc. que puede permitir que un host sea vulnerado.

Como siempre, la recomendación es tener actualizado el kernel de linux a la última versión estable, así como también el engine de docker o la tecnología de contenedores que estés utilizando.

Secretos o variables de entorno

El objetivo de crear una imagen de contenedor es paquetizar tu software para que esté listo para arrancar al instante, eso sí, siempre requiere una configuración en la mayoría de los casos. Si es una base de datos, necesitará definir usuarios y contraseñas, si es una página web, necesitará definir una configuración de servidor, etc.

Meter esos secretos en la imagen sería un fallo de seguridad y además rompería la versatilidad de coger una imagen que pueda funcionar en diferentes casos. Para configurar un contenedor, lo más común, es añadir variables de entono a la imagen en tiempo de ejecución.

Por ejemplo, para configurar un servidor de base de datos de mariadb y que funcione en un contenedor tenemos que definir al menos la contraseña del usuario root:

docker run --name some-mariadb -e MARIADB_ROOT_PASSWORD=contraseña -d mariadb:latest

Muchas aplicaciones no gestionan esto correctamente, es decir, no limpian las variables de entorno que datos sensibles una vez que cargan los secretos en memoria.

Por eso, uno de los primeros pasos de un pentester es consultar el environment del contenedor:

# Simplemente entrando al terminal del contenedor y ejecutando el comando env dentro del contenedor
> $ docker exec -it some-mariadb /bin/bash
root@5f3f1ce5b7e1: env
HOSTNAME=5f3f1ce5b7e1
PWD=/
HOME=/root
MARIADB_VERSION=1:10.7.3+maria~focal
GOSU_VERSION=1.14
TERM=xterm
MARIADB_MAJOR=10.7
SHLVL=1
MARIADB_ROOT_PASSWORD=contraseña
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
_=/usr/bin/env

Podríamos ver que el credencial sigue ahí una vez arrancado el contenedor.

También podríamos ver las variables de entorno desde fuera con el siguiente comando:

docker container inspect --format '{{.Config.Env}}' <nombre contenedor>

Montaje del socket

Cuando utilizamos diferentes comandos de docker, como por ejemplo docker run, lo que hace el cliente de docker es comunicarse con el engine mediante un socket.

En algunos escenarios en los que se necesita ejecutar comandos de docker dentro de un contenedor, por ejemplo, un orquestador de servicios montado sobre docker que necesite levantar otros contenedores a su vez.

Jerarquía de servicios Ejemplo de una herramienta Jenkins que orquesta el despliegue de contenedores. A su vez, esta herramienta también está dentro de un contenedor y tiene el socket de docker montado. Por último, tanto el contenedor del front como el del jenkins están expuestos a internet.

Si este orquestador es vulnerado por un atacante, teniendo acceso al socket del docker engine (que recordemos que se ejecuta con el usuario root), podría montar el sistema de archivos del host con permisos de root fácilmente.

Por ejemplo, para docker:

docker run -it -v /:/host/ debian:11 chroot /host/ bash

Pivotar a otros contenedores de la red.

Docker por defecto crea una red donde ejecuta todos estos contenedores. Si no especificamos nada, todos los contenedores se ejecutan en la misma red. Esto puede permitir que se comprometa las seguridad de otros contenedores de la red.

Supongamos el escenario anterior del jenkins con el socket de docker montado. Imaginaros que este caso pudiésemos vulnerar el back de la aplicación. Este no tendría acceso directamente al socket de docker pero podríamos intentar pivotar a otros contenedores de la red.

Para solventar esto, en el momento de la creación de un contenedor, podemos especificar una red diferente. Por ejemplo, para aislar la aplicación web completamente del jenkins:

# Primero creamos la red
docker network create <nombre de la red>

# Creamos el front y el back de la aplicación y los añadimos a la nueva red
docker run -d --name front --network <nombre de la red> <imagen del front>
docker run -d --name back --network <nombre de la red> <imagen del back>

Así evitaríamos que aunque una aplicación sea vulnerada no afecte al resto de contenedor y servicios que estén desplegados en el mismo host.

Herramientas

Deepce

Esta herramienta permite enumerar y escalar privilegios en contenedores. Está escrita puramente en sh sin ninguna dependencia pero, para aprovechar todas las funcionalidades, usa herramientas como curl, nmap, nslookup y dig si estan disponibles.

Este es su repositorio de github: https://github.com/stealthcopter/deepce

La descarga de la aplicación se puede hacer:

# Con wget
wget https://github.com/stealthcopter/deepce/raw/main/deepce.sh

# Con curl
curl -sL https://github.com/stealthcopter/deepce/raw/main/deepce.sh -o deepce.sh

Una vez descargado, le asignamos permisos de ejecución y lo ejecutamos:

chmod +x deepce.sh
./deepce.sh

2.2 - Reconocimiento

Seccion dedicada al reconocimiento activo de información contra un sistema.

2.2.1 - SNMP

Explicación y enumeración de información sobre el protocolo SNMP

Introducción

SNMP o Simple Network Mangement Protocol es un protocolo usado para monitorizar los dispositivos de una red ( por ejemplo, routers, switches, impresoras, IoTs…). Las versiones 1,2 y 2c son bastante inseguras y transmiten la información en texto plano. Estos problemas se solucionaron con la versión 3. En cualquiera de los casos teniendo credenciales se puede lsitar información muy valiosa de un sistema.

Enumeración

Nmap tiene varios scripts para enumerar información sobre este protocolo.

Lo primero que tendremos que identificar es el protocolo y puerto. Por defecto, opera en el 161 UDP. Con NMAP podríamos lanzar un barrido a todos los puertos UDP con el siguiente comando:

nmap -v -p- -sU <Objetivo>

Una vez identificado el protocolo y puerto, especificamos a nmap el puesto concreto sobre el que operar:

nmap -p161 -sU <Objetivo>

Ahora toca utilizar los scripts de nmap. Podríamos dejarlo en modo automático con el parámetro -sC pero no es el método más eficaz dado que muchos de estos script requieren parámetros.

3 - Programación

Sección dedicada a la parte de programación.

3.1 - Git

Sección dedicada al control de versiones GIT

3.1.1 - Solucionar errores

En git es muy común equivocarse y, dada su funcionamiento, puede ser tedioso corregir ciertos descuidos. En este artículo trataremos los errores más comunes

Git es un sistema muy estricto y metódico diseñado para garantizar la integridad de nuestro código a lo largo de infinidad de versiones y cambios generados por múltiples programadores.

Es normal y frecuente en estos procesos equivocarnos, como por ejemplo, escribir el mensaje que no era en un commit, olvidarse de añadir algún archivo en un commit o añadir el que no querías… etc.

Por suerte, para todos ellos hay solución y veremos diferentes comandos que git ofrece para arreglar errores.

Vamos a ir viendo las diferentes opciones agrupadas por comandos:

Ammend - Enmendar el commit más reciente

Esta opción trabaja en conjunto con el comando commit y es una manera práctica de modificar el commit más reciente. Te permite combinar los cambios prepados con el commit anterior en en lugar de crear uno nuevo.

Sin embargo, este comando no se limita a alterar el cambio más reciente, sino que lo reemplaza por completo. Importante tenerlo en cuenta sobre todo en repositorios públicos cuyos commits puedan ser dependencias de otras ramas o herramientas.

Uso básico

Supón que acabas de terminar un commit y quieres modificar su mensaje porque has puesto lo que no debías. Podrías ejecutar esto:

git commit --amend

Tras ejecutarlo se nos mostrará el editor de texto seleccionado en git para editar el menaje del último commit. En esta entrada puedes ver como cambiar el editor de texto que git usará para estas labores.

Añadir archivo al último commit

Podría pasar también, que te hubieras dejado de añadir un archivo al último commit. Es cierto que podrías crear un nuevo commit pero queda más limpio si corriges el anterior. Para ello añadiríamos el o los archivos que nos hubieramos dejado en el anterior commit usando el comando “add”:

git add <fichero>

Y luego, volveríamos a repetir el “ammend”:

git commit --amend

Esto nos permitiría añadir el archivo los archivos omitidos en el commit anterior y corregir el mensaje si fuera necesario.

Reset - Revertir cambios de un commit

Que pasaría si hacemos lo contrario que en punto anterior, en vez de añadir un archivo, lo queremos eliminar. Muchas veces por error incluimos en el repositorio un archivo que no queríamos dado que contienen secretos o información importante.

Borrar del stage area

Si solo lo hemos mandado al “stage” area, podríamos quitarlo de ahí con el siguiente comando:

git reset <fichero a eliminar>

Revertir un commit

En caso de haberlo añadido al “stage area” y haber hecho un commit, podríamos revertir el cambio con el siguiente comando:

git reset --soft HEAD~1 #Revertir el último commit
git reset <archivo a eliminar> #Resturar el archivo del commiteado por error
rm <archivo a eliminar> #Eliminar el archivo del repositorio
git commit #Hacer el commit

Esto revertirá el último commit, eliminará el archivo y añadira un nuevo commit en su lugar.

Volver a un estado anterior tras muchos cambios

Podría pasar, ya en el peor de los casos, que hubieramos hecho muchos cambios mal y quisieramos volver a un estado anterior. Primero podríamos consultar el historial de commits con el comando “log” o “reflog” y ver la referencia del commit al que queremos volver:

git log
git reflog

Con la referencia del commit al que queremos volver, podemos revertir el commit con el siguiente comando:

git reset HEAD@{Referencia}

Volver al último commit rápidamente

Para volver al último commit sin tener que consultar el historial, podemos usar el comando “reset” con el parámetro “–hard”:

git reset --hard HEAD

Branch - Errores en ramas

En este apartado veremos los errores más comunes que pueden ocurrir en las ramas.

Nombre de rama equivocado

Es frecuente que con las prisas escribamos el nombre de una rama con un nombre equivocado. Aquí la solución es simple, dentro del comando branch esta el parámetro “-m” que nos permite cambiar el nombre de la rama:

git branch -m nombre-rama-equivocada nombre-rama-correcta

Commit a la rama principal

Podríamos hacer sin querer un commit en la rama principal, por ejemplo, main cuando nuestro sistema de organización es hacer ramas distintas para cada característica nueva que se desarrolla o trabajar primero en develop y luego integrar los cambios en main.

En varios pasos podríamos crear una rama con todos los cambios que acabamos de generar y luego, en el siguiente paso, resetear la rama principal al commit anterior:

git branch nombre-rama-nueva-con-los-cambios #Creamos una rama con los cambios
git reset HEAD~ --hard #Reseteamos la rama principal al commit anterior
git checkout nombre-rama-nueva-con-los-cambios #Cambiamos a la rama nueva

En el último paso, nos cambiaríamos a la rama nueva para seguir trabajando con los cambios habiendo dejado limpia la rama principal.

Eliminar secretos tanto en local como en remoto

En este apartado veremos como eliminar los secretos de un repositorio local o remoto. Es muy frecuente sin querer introducir tokens o contraseñas en un repositorio. Aunque los borremos posteriormente, estos, se mantendrán en el historial de git.

Podemos borrar un archivo de toda la historia con el siguiente comando:

 git filter-branch --force --index-filter \
  "git rm --cached --ignore-unmatch ARCHIVO-SENSIBLE" \
  --prune-empty --tag-name-filter cat -- --all
  git push --force --verbose --dry-run
  git push --force

3.1.2 - Configuración

Configuraciones básicas y esenciales

Elegir el editor de commit por defecto

Dependiendo del sistema operativo en el que nos encontremos Git utilizara un editor u otro para los mensajes de commit en el terminal. En algunos por defecto es nano, en otros vim, gedit… etc. Con el siguiente comando puedes elegir el que más se adapte a tus gustos y necesidades.

En mi caso, prefiero Vim y usaría el siguiente comando:

git config --global core.editor "vim"

También serviría para Neovim usando:

git config --global core.editor "nvim"

Si quisieras usar nano sería tan fácil como usar el siguiente:

git config --global core.editor "nano"

Configurar identidad

Si es la primera vez que utilizamos git en un sistema, al hacer un commit, es obligatorio que este quere registrado con el nombre y email de un usuario. Se puede configurar con los siguientes comandos:

git config --global user.name "John Doe"
git config --global user.email "johndoe@example.com"

3.1.3 - Index lock

Error index lock

Git tiene un sistema de funcionamiento muy estricto para evitar conflictos y ayudarnos a mantener nuestro bien versionado. Para ello, solo a un proceso realziar cambios a la vez para mantener la integridad de la información.

Cuando realizamos cualquier tarea en git, un commit, push, pull… este genera un archivo llamado “index.lock” y lo guarda dentro de la carpeta “.git” en la raiz del repositorio.

Este archivo bloquea el repositorio ante cualquier otro acceso o proceso simult�neo que quiera realizar cambios. En algunos casos, poco frecuentes, puede pasar que una acción o tarea nunca termine ( por fallo del SO u otros) y el repositorio se quede bloqueado.

Si tenemos claro lo que estamos haciendo, podríamos borrar simplemente este archivo con el comando:

rm .git/index.lock

Así de simple conseguiríamos quitar el bloqueo de git pero atención que no tengamos otro proceso ejecutando alguna tarea sobre git o podríamos corromper datos del repositorio.

3.1.4 - Claves ssh

Configuracion de múltiples claves o claves ssh específicas por repositorio

Usar una clave específica por repositorio

Puede ocurrir, por ejemplo usando github, que tengamos varias cuentas y no podamos usar la misma clave ssh que tenemos en el sistema para todos los repositorios. Este problema surge a raiz de que github no permite tener la misma clave ssh repetida en diferentes cuentas.

En nuestro local podríamos generar otra pareja de claves que tuvieran otro nombre y luego en nuestro repositorio de git, especificar manualmente que clave queremos que use nuestro repositorio.

git config --local core.sshCommand "ssh -i ~/.ssh/id_rsa_personal -F /dev/null"

3.2 - SQL

Servidor de bases de datos y SQL como lenguaje de consultas aplicado

3.2.1 - Comandos básicos

Acceder a MySQL

Sin especificar credenciales:

mysql # Sin login

Con usuario y contraseña (la password la pide por pantalla):

mysql -u root -p # Con usuario y contraseña

Bases de datos

Mostrar todas:

show databases;

Crear base de datos:

create database <base de datos>;

Borrar base de datos:

drop database <base de datos>;

Tablas

Consultar tablas:

show tables;

Describir los atributos de una tabla:

DESCRIBE <tabla>;

Crear tabla:

mysql> CREATE TABLE <tabla>(
         id CLAVE NOT NULL AUTO_INCREMENT,
         nombre CHAR(30) NOT NULL,
         edad INTEGER(30),
         salario INTEGER(30),
         PRIMARY KEY (id) );

Insertar datos:

INSERT INTO <tabla> (nombre, edad, salario) VALUES
("Pedro", 24, 21000),
        ("Maria", 26, 24000),
        ("Juan", 28, 25000),
        ("Luis", 35, 28000),
        ("Monica", 42, 30000),
        ("Rosa", 43, 25000),
        ("Susana", 45, 39000);

Actualizar datos:

UPDATE <tabla> SET nombre = "Pedro" WHERE id = 1;

Obtener datos:

SELECT * FROM <tabla>;

Borrar datos:

DELETE FROM <tabla> WHERE id = 1;

Salir de MySql

Para salir del cli interactivo de mysql se puede usar la opción quit o exit.

exit;

3.2.2 - Consultas y cláusulas

Cláusulas

Las consultas en SQL son una forma de acceder a la base de datos.

Estas consultas tienen diferentes cláusulas:

  • FROM: Selecciona la tabla.
  • WHERE: Selecciona los registros que cumplan con una condición.
  • ORDER BY: Ordena los registros por un atributo.
  • GROUP BY: Agrupa los registros por un atributo.
  • LIMIT: Limita el número de registros a devolver.
  • HAVING: Selecciona los registros que cumplan con una condición. Opera sobre los registros agrupados.

La siguiente consulta devolvería todos los registros de la tabla tabla.

SELECT * FROM tabla;

Ejemplo where: La siguiente consulta devolvería el registro con el id 1 de la tabla tabla.

SELECT * FROM tabla WHERE id = 1;

Ejemplo order by: Por defecto, ORDER BY devolvería todos los registros la tabla tabla ordenados por el id de forma ascendente (sin especificar nada), también podría usarte el DESC para ordenarlas de forma ascendent

SELECT * FROM tabla ORDER BY id DESC;

Ejemplo group by: La siguiente consulta devolvería todos los registros agrupados por el campo nombre.

SELECT * FROM tabla GROUP BY nombre;

Ejemplo limit: La siguiente consulta devolvería los primeros 5 registros de la tabla tabla.

SELECT * FROM tabla LIMIT 5;

Ejemplo having:

SELECT * FROM tabla GROUP BY nombre HAVING COUNT(*) > 1;

Operadores

Los operadores nos permiten establecer condiciones en las consultar, modificarlas o agruparlos.

Operadores de comparación

Existen diferentes operadores de comparación que nos permiten comparar dos valores.

Operador Función
< Menor que
> Mayor que
<> Distinto de
= Igual a
<= Menor o igual que
>= Mayor o igual que
IN Dentro de (filas de tabla)
NOT IN Fuera de (filas de tabla)
BETWEEN Entre (valores numéricos)
LIKE Contiene (valor de cadena)

Algunos ejemplos:

SELECT * FROM table WHERE precio > 10; /*Selecciona todos los registros con precio mayor a 10*/
SELECT * FROM table WHERE precio < 10; /*Selecciona todos los registros con precio menor a 10*/
SELECT * FROM table WHERE precio <> 10; /*Selecciona todos los registros con precio distinto a 10*/
SELECT * FROM table WHERE precio = 10; /*Selecciona todos los registros con precio igual a 10*/
SELECT * FROM table WHERE precio <= 10; /*Selecciona todos los registros con precio menor o igual a 10*/
SELECT * FROM table WHERE precio >= 10; /*Selecciona todos los registros con precio mayor o igual a 10*/
SELECT * FROM table WHERE precio IN (10, 20, 30); /*Selecciona todos los registros con precio 10, 20 o 30*/
SELECT * FROM table WHERE precio NOT IN (10, 20, 30); /*Selecciona todos los registros con precio distinto a 10, 20 o 30*/
SELECT * FROM table WHERE precio BETWEEN 10 AND 20; /*Selecciona todos los registros con precio entre 10 y 20*/

Comparación de cadenas

Concretamente el comando LIKE nos permite buscar dentro de una cadena de texto. Este comando se aplica a los campos de tipo varchar o char. Se apoya en los siguientes operadores:

Operador Función
% Comodín, representa cualquier cadena de 0 o más caracteres
_ Representa a un único carácter cualquiera

Combinando el LIKE con el % o con el _ podemos buscar por palabras completas o parciales.

Algunos ejemplos con %:

SELECT * FROM tabla WHERE nombre LIKE '%Pedro%'; /*Todas las filas que contengan la palabra Pedro*/
SELECT * FROM tabla WHERE nombre LIKE 'Pedro%'; /*Todas las filas que comiencen por Pedro*/
SELECT * FROM tabla WHERE nombre LIKE '%Pedro'; /*Todas las filas que terminen por Pedro*/

Algunos ejemplos con _:

Operadores lógicos

Nos permiten establecer condiciones en las consultas. También nos permite agrupar varias consultas y condiciones a su vez.

Los operadores lógicos son:

Operador Función
AND Y
OR O
NOT No

Algunos ejemplos:

SELECT * FROM tabla WHERE nombre LIKE '%Pedro%' AND precio < 10;
SELECT * FROM tabla WHERE nombre LIKE '%Pedro%' OR precio < 10;
SELECT * FROM tabla WHERE precio < 10 AND nombre LIKE '%Pedro%';
SELECT * FROM tabla WHERE nombre LIKE '%Pedro%' AND precio < 10 OR precio > 20;
SELECT * FROM tabla WHERE nombre LIKE '%Pedro%' AND (precio < 10 OR precio > 20);
SELECT * FROM tabla WHERE nombre LIKE '%Pedro%' OR (precio < 10 AND precio > 20);
SELECT * FROM tabla WHERE (nombre LIKE '%Pedro%' AND precio < 10) OR precio > 20;

Operadores de agrupación

Nos permiten agrupar registros por un campo.

Operador Función
COUNT() Cuenta los registros que cumplan con la condición
SUM() Suma los valores de un campo
MAX() Devuelve el valor máximo de un campo
MIN() Devuelve el valor mínimo de un campo
AVG() Devuelve la media de un campo

Algunos ejemplos:

SELECT COUNT(*) FROM tabla;
SELECT SUM(precio) FROM tabla;
SELECT MAX(precio) FROM tabla;
SELECT MIN(precio) FROM tabla;
SELECT AVG(precio) FROM tabla;

Subconsultas

A veces, para realizar alguna operación de consulta, necesitamos los datos devueltos por otra consulta.

Una subconsulta, que no es más que una sentencia SELECT dentro de otra SELECT.

Las subconsultas son aquellas sentencias SELECT que forman parte de una cláusula WHERE de una sentencia SELECT anterior. Una subconsulta consistirá en incluir una declaración SELECT como parte de una cláusula WHERE.

Un ejemplo de subconsultas:

SELECT apellido FROM empleados WHERE oficio = (SELECT oficio FROM empleados WHERE apellido ='gil');

3.2.3 - Insert, update y delete

Podemos insertar, modificar y borrar datos de las tablas a través de consultas de SQL. Podemos usar los comandos INSERT, UPDATE y DELETE.

Importante, antes de usar cualquiera de estos comandos, debemos indicar la base de datos sobre la que queremos aplicar la operación con el comando USE.

Ejemplo:

USE base_de_datos;

Insert - Insertar datos

Para insertar datos, podemos usar el comando INSERT.

INSERT INTO <tabla> (<atributos>) VALUES (<valores>);

Por ejemplo:

INSERT INTO persona (nombre, edad, salario) VALUES ('Juan', 30, 1000);

Las inserciones se podrían realizar con subconsultas, como por ejemplo, con un select anidado:

INSERT INTO persona (nombre, edad, salario) VALUES ( 'Juan', 30, (SELECT salario FROM persona WHERE nombre = 'Juan') );

Insert con select - Insertar datos con select

Para insertar datos con un select, podemos usar el comando INSERT con la siguiente sintaxis:

INSERT INTO <tabla> (<atributos>) SELECT <valores> FROM <tabla>;

Esto nos permitiría insertar datos de una tabla en otra, por ejemplo:

INSERT INTO persona (nombre, edad, salario) SELECT nombre, edad, salario FROM persona;

También podríamos combinar datos procedentes del SELECT con datos estáticos introducidos manualmente:

INSERT INTO persona (nombre, edad, salario) SELECT nombre, edad, 1600, FROM persona WHERE nombre = 'Juan';

O hacer operaciones con datos procedentes del SELECT, como por ejemplo, duplicarle el salario a Juan:

INSERT INTO persona (nombre, edad, salario) SELECT nombre, edad, salario * 2 FROM persona WHERE nombre = 'Juan';

Update - Modificar datos

Para modificar datos, podemos usar el comando UPDATE. Cabe destacar el uso del WHERE para especificar que valores queremos actualizar, si no lo especificamos, se actualizarán todos los datos de la tabla. La sintaxis es la siguiente:

UPDATE <tabla> SET <atributos> = <valores> WHERE <condiciones>;

Por ejemplo, para modificar un atributo de una tabla:

UPDATE persona SET nombre = 'Pedro' WHERE id = 1;

También podríamos actualizar múltiples valores de una fila separándolos por comas:

UPDATE persona SET nombre = 'Pedro', edad = 30 WHERE id = 1;

Delete - Borrar datos

Para borrar datos, podemos usar el comando DELETE.

DELETE FROM <tabla> WHERE <condiciones>;

Por ejemplo:

DELETE FROM persona WHERE id = 1;

3.2.4 - Funciones en consultas

Funciones en consultas de selección

Estas se utilizan para manipular y filtrar los datos que se devuelven en una consulta. Se pueden usar en las cláusulas WHERE y HAVING pero lo más común es usarlas en las cláusulas SELECT. Esta consulta de ejemplo nos permitiría obtener el salario redondeado de todos los empleados y modificando la salida (sin alterar el registro de la basa de datos):

SELECT nombre, ROUND(salario) FROM empleados;

Funciones aritméticas

Nos permiten realizar operaciones aritméticas sobre los valores de los campos.

Operador Función
ABS() Valor absoluto
ROUND(n,d) Redondea el valor “n” con con el número de decimales especificados en “d”
FLOOR() Redondea hacia abajo
CEIL() Redondea hacia arriba
SQRT() Raíz cuadrada
POW() Potencia, ejemplo POW(x,y) el valor x elevado a el exponente y

Funciones de cadenas de texto

Nos permiten manipular textos de las consultas.

Operador Función
CONCAT() Concatena dos o más cadenas de texto
SUBSTRING(c,m,n) Devuelve una sub-cadena obtenida de la cadena “c”, a partir de la posición “m” y tomando “n” caracteres.
LENGTH() Devuelve la longitud de una cadena de texto
UCASE() Convierte una cadena de texto a mayúsculas
LCASE() Convierte una cadena de texto a minúsculas
REPLACE(c,b,s) Reemplaza en la cadena “c” el valor buscado en “b” por el valor indicado en “s”
TRIM() Elimina los espacios en blanco de una cadena de texto
REPLICATE(c,n) Repite la cadena “c” tantas veces como indique la variable “n”

Funciones de fecha

Nos permiten manipular fechas de las consultas.

Operador Función
DATE() Convierte una fecha a una cadena de texto
DATE_FORMAT() Convierte una fecha a un formato de cadena de texto
NOW() Devuelve la fecha actual
YEAR() Devuelve el año de una fecha
MONTH() Devuelve el mes de una fecha
QUARTER() Devuelve el trimestre del año de una fecha
DAY() Devuelve el día de una fecha
HOUR() Devuelve la hora de una fecha
MINUTE() Devuelve los minutos de una fecha
SECOND() Devuelve los segundos de una fecha

3.2.5 - Combinación de tablas. Joins

Hay momentos en los que una consulta necesita columnas de varias tablas. En este caso podremos definir en el FROM todas las tablas que queremos usar.

SELECT * FROM tabla1, tabla2, tabla3;

Si lo hacemos de esta forma, nos devolverá todos los registros de las tablas pero al no haberlas relacionado nos mostrará los registros de cada unos de ellas combinados entre sí. Estos datos no tendrían coherencia.

Veamos un ejemplo, vamos a usar las tablas empleados y la tabla departamentos:

EMPLEADOS

id nombre apellidos departamento_id
1 Pepe Pérez 1
2 Ana Muñoz 1
3 Juan González 2
4 María Rubio 2

DEPARTAMENTOS

id nombre
1 Ingeniería
2 Finanzas

Si quisiéramos mostrar los empleados junto con los nombres de su departamento, podríamos hacerlo de la siguiente forma:

SELECT empleados.nombre, empleados.apellidos, departamentos.nombre FROM empleados, departamentos WHERE empleados.departamento_id = departamentos.id;

Cuando seleccionamos datos de varias tablas tenemos que difinir en el WHERE la relación entre ellas. En este caso, el id de la tabla empleados y el departamento_id de la tabla departamentos porque es la única forma de mostrar el nombre del departamento al que pertenece cada empleado.

JOIN

El JOIN es una forma de relacionar dos tablas y tiene varios comandos. La consulta anterior es un ejemplo de INNER JOIN realizado con un WHERE. Existen varios tipos de JOIN en función de la información que quieras obtener de dos tablas relacionadas.

El más común es el INNER JOIN que nos permite obtener los registros de una tabla que están relacionados con otra.

Representación visual de los diferentes tipos de joins

Aunque son los más comunes, hay más tipos de los que se muestran en la representación visual. De momento nos centraremos en los más comunes:

INNER JOIN

El INNER JOIN nos permite seleccionar los registros que tengan coincidencias en ambas tablas, es decir, que estén relacionados. Para ello, en el FROM seleccionamos la tabla de la que queremos obtener los registros y en el JOIN seleccionamos la tabla con la que queremos relacionar. Por último, tendríamos que definir en el ON del JOIN la relación entre ambas tablas de la misma forma que en el WHERE de la consulta anterior.

Ejemplo:

SELECT empleados.nombre, empleados.apellidos, departamentos.nombre FROM empleados JOIN departamentos ON empleados.departamento_id = departamentos.id;

LEFT JOIN

El LEFT JOIN nos permite seleccionar todos los registros de la tabla de la izquierda junto con los registros de la tabla de la derecha que tengan coincidencias.

Ejemplo de uso:

SELECT empleados.nombre, empleados.apellidos, departamentos.nombre FROM empleados LEFT JOIN departamentos ON empleados.departamento_id = departamentos.id;

RIGHT JOIN

El RIGHT JOIN nos permite seleccionar todos los registros de la tabla de la derecha junto con los registros de la tabla de la izquierda que tengan coincidencias.

Ejemplo de uso:

SELECT empleados.nombre, empleados.apellidos, departamentos.nombre FROM empleados RIGHT JOIN departamentos ON empleados.departamento_id = departamentos.id;

FULL JOIN

El FULL JOIN nos permite seleccionar todos los registros de la tabla de la izquierda junto con los registros de la tabla de la derecha aunque no tengan coincidencias.

Ejemplo de uso:

SELECT empleados.nombre, empleados.apellidos, departamentos.nombre FROM empleados FULL JOIN departamentos ON empleados.departamento_id = departamentos.id;

3.2.6 - Trigger o disparador

Un disparador o trigger es un tipo de procedimiento almacenado que se ejecuta cuando se intentan alterar los datos de una tabla o vista. La diferencia con los procedimientos almacenados es que los disparadores:

  • No pueden ser invocados directamente. El disparador se ejecuta automáticamente.
  • No reciben y devuelven parámetros.
  • Son apropiados para mantener la integridad de los datos, no para obtener resultados de consultas

Su función es ejecutarse cuando ocurre algún evento en una tabla. Estos eventos pueden ser insertar, modificar o borrar datos ( INSERT, UPDATE, DELETE ).

Sintaxis básica

Antes de empezar, hay que detallar algunos variables que se usarán en el siguiente ejemplo:

  • Nombre del disparador (name): Nombre del disparador.
  • Tiempo del disparador (time)[ AFTER o BEFORE ]: se puede especificar cuando se ejecuta el disparador. Antes o después de la ejecución de la consulta.
  • Evento (evento)[ INSERT, UPDATE o DELETE ]: se puede especificar el evento en el que se desea que se ejecute el disparador.
  • Nombre de la tabla (nombre_tabla): se puede especificar el nombre de la tabla en la que se desea que se ejecute el disparador.
  • Orden del trigger (orden opcional) [ FOLLOWS o PRECEDES ]: se puede especificar el orden en el que se desea que se ejecute el disparador.
  • Cuerpo del disparador (cuerpo): se puede especificar el cuerpo del disparador.

La sintaxis de un disparador es la siguiente:

CREATE TRIGGER <name> <time> <evento> 
ON <nombre_tabla> FOR EACH ROW 
BEGIN
<cuerpo>
END;

Por ejemplo:

CREATE TRIGGER tr_insert_persona AFTER INSERT ON persona FOR EACH ROW
BEGIN
	<cuerpo>
END;

Cuerpo del disparador

El cuerpo del disparador es el código que se ejecuta cuando se lanza el disparador. Dentro de un disparador nos podemos referir a los datos que se están insertando, modificando o borrando. Esto nos permite evitar que se borren o modifiquen datos indeseados; aplicar condiciones como por ejemplo, que haya stock suficiente para un producto.. etc.

El cuerpo del disparador se puede definir en varias partes. En cada parte se puede definir una sentencia SQL.

Por ejemplo, controlar la edad de una persona para evitar que se inserten menores de edad:

CREATE TRIGGER tr_insert_persona BEFORE UPDATE ON persona FOR EACH ROW
BEGIN
	IF NEW.edad < 18 THEN
		UPDATE persona SET edad = 18 WHERE id = NEW.id;
	END IF;
END;

En el cuerpo podemos definir variables, condiciones, sentencias SQL, etc.

Por ejemplo, un disparador en el que consultamos y almacenamos en variables datos de otras tablas:

CREATE TRIGGER tr_insert_persona AFTER INSERT ON persona FOR EACH ROW
BEGIN
	DECLARE nombre_mascota VARCHAR(50);

	SET nombre_mascota = (SELECT nombre FROM mascotas WHERE id = NEW.id);

	INSERT INTO persona_mascota (id, nombre, edad, fecha_nacimiento, ) VALUES (NEW.id, nombre_mascota, NEW.edad, NEW.fecha_nacimiento);
END;

#TODO: Diferencias entre NEW y OLD…

3.2.7 - Procedimientos y funciones

Introducción

Con el fin de mejorar la eficiencia y reutilizar las consultas SQL, se ha desarrollado una serie de procedimientos y funciones que permiten realizar operaciones de forma más eficiente.

Estos objetos se almacena en la base de datos y se pueden utilizar en las consultas SQL.

Procedimientos y funciones

Sus principales diferencias son:

  • Valores de retorno: Los procedimientos no tienen porque retornar ningún valor, mientras que las funciones siempre retornan un valor.
  • Tipos de valores de retorno: Los procedimientos pueden mostrar resultados de cualquier tipo (listas, tablas), mientras que las funciones siempre retornan un valor concreto (int, varchar, etc.).
  • Parámetros: Los procedimientos pueden tener parámetros múltiples valores de entrada y salida (in, out, inout), mientras que las funciones siempre tienen un solo parámetro de entrada y un valor de salida.

Procedimientos

Los procedimientos son rutinas o subprogramas compuestos por un conjunto nombrado de sentencias SQL agrupados lógicamente para realizar una tarea específica, que se guardan en la base de datos y que se ejecutan como una unidad cuando son invocados por su nombre. Es decir, nos permiten agrupar un conjuntos de sentencias para lanzarlas en bloque.

El procedimiento consta de las siguientes partes:

  • Definición del nombre del procedimiento.
  • Parámetros de entrada o salida
  • Sentencias SQL

Sintaxis básicas de procedimientos almacenados:

CREATE PROCEDURE <nombre_procedimiento>
(
	<parametro_entrada> <tipo>
	<parametro_entrada> <tipo>
	...
)
...

Los parámetros de entrada pueden ser de los siguientes tipos:

  • IN: De entrada
  • OUT: De salida
  • INOUT: De entrada y salida

Un ejemplo completo sería el siguiente:

CREATE PROCEDURE procedimiento_ejemplo (IN nombre VARCHAR(50), OUT edad INT)
BEGIN
	SELECT  edad INTO edad FROM usuarios WHERE nombre = nombre ;
END

Funciones

Las funciones son rutinas o subprogramas compuestos por un conjunto. Estas siempre tiene un valor de retorno, el cuál, cuyo tipo depende de la declaración de la función con la sintaxis RETURNS <tipo> y luego en el cuerpo de la función se devuelve con la instrucción RETURN <valor>.

Por ejemplo, la siguiente función devuelve el nombre de un usuario. Primero hacemos un select que asigna el resultado a una variable y luego hacemos que la función devuelva el valor de la variable.

CREATE FUNCTION nombre_usuario (id_usuario INT) RETURNS VARCHAR(50)
BEGIN
	DECLARE nombre_obtenido VARCHAR(50);

	SELECT nombre INTO nombre_obtenido FROM usuarios WHERE id_usuario = id_usuario;

	RETURN nombre_obtenido;
END

3.2.8 - Cursores

Un cursor es una consulta declarada que provoca que el servidor, cuando se realiza la operación de abrir cursor, cargue en memoria los resultados de la consulta en una tabla interna. Teniendo abierto el cursor, es posible, mediante una sentencia FETCH, leer una a una las filas correspondientes al cursor y, por tanto, correspondientes a la consulta definida. Los cursores deben declararse después de las variables locales.Los cursores nos permiten tras una consulta SQL, recuperar los resultados de la misma y poder trabajar con ellos de uno en uno.

Para hacer uso de un cursor, tendremos que:

  • Declarar el cursor (después de las variables locales).
  • Abrir el cursor.
  • Asignar las filas al cursor según tarea a realizar.
  • Cerrar el cursor una vez finalizada la tarea (CLOSE)

Los cursores se usan dentro de procedimientos y funciones

Sintaxis básica

Este cursor nos permite leer uno a uno los resultados de una consulta para trabajar con los datos. Cabe destacar la necesidad de abrir y cerrar el cursor (OPEN y CLOSE).

Podemos leer los valores de un cursor con el comando FETCH:

DECLARE <variable> <tipo>
DECLARE <nombre_cursor> CURSOR FOR <consulta>;


OPEN <nombre_cursor>;

FETCH <nombre_cursor> INTO <variable>;

CLOSE <nombre_cursor>;

Uso con repetición/bucles

Al leer los valores de un cursor, podemos repetir el proceso de leer los valores de un cursor. Para ello, podemos usar el comando FETCH repetidamente:

DECLARE <nombre_cursor> CURSOR FOR <consulta>;

OPEN <nombre_cursor>;


FETCH <nombre_cursor> INTO <variable>;
FETCH <nombre_cursor> INTO <variable>;
FETCH <nombre_cursor> INTO <variable>;
FETCH <nombre_cursor> INTO <variable>;

CLOSE <nombre_cursor>;

Pero si quisieramos leer todos los valores de un cursor, lo normal es hacer un bucle para no estar repitiendo la misma operación tantas veces como filas tenga el cursor.

Con repeat until - hasta que

Para leer valores hasta un momento específico, podemos usar el comando REPEAT UNTIL. Vamos a suponer en este caso que queremos sumar el precio de los 5 primeros resultados de un SELECT.

DECLARE total INT DEFAULT 0;
DECLARE precio INT DEFAULT 0;
DECLARE contador INT DEFAULT 1;
DECLARE cursor1 CURSOR FOR SELECT precio FROM productos;

OPEN cursor1;

REPEAT
    FETCH cursor1 INTO precio;
	SET total = total + precio;
	SET contador = contador + 1;
UNTIL contador > 5;
END REPEAT;

CLOSE cursor1;

Con loop - todos los valores

Para leer todos los valores de un cursor, podemos usar el comando LOOP. Vamos a suponer en este caso que queremos sumar el precio de todos los resultados de un SELECT. Para ello, podemos usar el comando LOOP. Aún así, tenemos que ayudar a nuestro programa para que sepa cuando termina de leer los valores. Para ello, utilizamos un HANDLER para saber cuando no hay valores y utilizar una variable auxiliar para decirle al programa que salga del LOOP.

Cuando el handler detecta que no hay valores, el HANDLER añade a la variable auxiliar el valor 1. Como el bucle LOOP al principio de la sentencia comprueba si la variable auxiliar es igual a uno, saldría del programa gracias a la intrucción LEAVE <nombre del bucle>.

DECLARE total INT DEFAULT 0;
DECLARE precio INT DEFAULT 0;
DECLARE auxiliar INT DEFAULT 0;
DECLARE cursor1 CURSOR FOR SELECT precio FROM productos;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET auxiliar = 1; 

OPEN cursor1;

bucle:LOOP
	FETCH cursor1 INTO precio;

	IF auxiliar = 1 THEN
		LEAVE bucle;
	END IF;

	SET total = total + precio;
END LOOP:bucle;

CLOSE cursor1;

3.2.9 - Exportar e importar

Exportar e importar bases de datos es un proceso crítico y totalmente necesario para proteger nuestros datos.

Estas exportaciones se puede hacer con clientes como mysql workbench, phpmyadmin, etc. En la práctica, suele haber incompatibilidades entre algunas vesiones de los clientes con el servidor de bases de datos. Personalmente, siempre recomiendo utilizar la utilidad de terminal que lleva el propio servidor para minimizar la posibilidad de errores.

A continuación, una lista de como exportar e importar bases de datos sql clasificados por tecnología:

MySQL

Exportar

Podemos exportar con el siguiente comando:

mysqldump -u USER -p DATABASE > salida.sql #La contraseña nos la pedirá interactivamente

Ejemplo real:

mysqldump -u root -p database1 > database1.sql

Exportar múltiples bases de datos:

mysqldump -u root -p --databases database1 database2 database3 > databases.sql

Exportar tablas específicas:

mysqldump -u root -p --databases database1 --tables table1 table2 table3 > tables.sql

Importar

Podemos importar con el siguiente comando:

mysql -u USER -p DATABASE < salida.sql

3.2.10 -

4 - Linux

Aquí se agrupa todo lo que tenga que ver con gnu/linux como errores, utilidades o funcionalidades

4.1 - Redes

Utilidades de red

Existen varias herramientas que nos ayudan a trabajar con redes:

# Ping - comprueba la conexión con un hosts y si está acttivo
ping -c 4 google.com # Con el -c 4 el número de veces que se ejecuta el ping

# Traceroute - identifica la ruta que se ha recorrido para llegar a un host
traceroute google.com

# Netstat - muestra los puertos abiertos en el sistema
netstat -l

# ARP - muestran la tabla ARP del sistema que actúa como una cache.
arp -a # Muestra la relación entre direcciones IP y direcciones MAC

Configuración de una red - Comando IP

Con el comando ip podemos alterar la configuración de una interfaz de red. Para ello hay múltiples opciones de este comando que debemos conocer previamente:

Listar las interfaces de red, información y estado:

ip addr

# O su versión corta
ip a

Ver la configuración de una interfaz de red:

ip addr show ens33

# O su versión corta
ip a s ens33

Habilitar o deshabilitar una interfaz de red:

#Habilitar
ip link set ens33 up

#Deshabilitar
ip link set ens33 down

El estado de una interfaz de red lo podríamos ver tras el state de la configuración de la interfaz marcado como UP o DOWN.:

ip a s ens33
ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether 00:15:5d:cc:35:ff brd ff:ff:ff:ff:ff:ff
inet 172.17.71.94/20 brd 172.17.79.255 scope global eth0
	valid_lft forever preferred_lft forever
inet6 fe80::215:5dff:fecc:35ff/64 scope link
	valid_lft forever preferred_lft forever

Asignar una dirección IP a una interfaz de red

Para asignar una dirección IP a una interfaz de red debemos usar el comando ip addr add:

ip addr add <IP>/<MASCARA> dev <INTERFACE>

#Por ejemplo:
ip addr add 192.168.1.56/255.255.255.0 dev ens33
#o
ip addr add 192.168.1.56/24 dev ens33

También podríamos añadir la dirección de broadcast:

ip addr add <IP>/<MASCARA> broadcast <BROADCAST> dev <INTERFACE>

# O su versión corta
ip a a <IP>/<MASC

Comprobando la puerta de enlace

Para comprobar si una interfaz de red tiene una puerta de enlace activa podemos usar el comando ip route show:

ip route show

También podemos obtener la información de enrutamiento a una IP particular usando:

ip route get <IP>

Configuración de una red - NetworkManager

Antes de comenzar, vamos a parar el servicio ‘network-manager’ para que no interfiera con nuestra configuración. Este servicio es el encargado de gestionar las redes. Esto lo podemos hacer con el comando:

systemctl stop network-manager

Para configurar una red en Ubuntu Server tendremos que crear un fichero llamado 01-netcfg.yaml en la carpeta /etc/netplan. El fichero especifica por cada red si queremos utilizar DHCP, dirección IP manual (en caso de no usar DHCP), gateays y servidores DNS nameservers. Ejemplo (obviar las líneas que comienzan por # ya que son comentarios para explicar el fichero):

network:
  version: 2
  renderer: NetworkManager
  # Se especifiaca cada una de las redes que queremos configurar
  ethernets:
    # Ejemplo de red con DHCP que recibirá la dirección IP automáticamente
	ens33:
	  dhcp4: yes
	  dhcp6: yes
	  nameservers:
		addresses: [8.8.8.8, 8.8.4.4]
	# Ejemplo de una red con IP manual
	ens38:
	  dhcp4: no
	  dhcp6: no
	  addresses: [192.168.1.120/24]
	  # Se especifica la puerta de enlace. IP del router.
	  gateway4: 192.168.1.1
	  nameservers:
	    addresses: [8.8.8.8, 8.8.4.4]

Una vez terminado de editar el fichero, tenemos que aplicar los cambios con el comando:

netplan apply

Finalmente, volveríamos a arrancar el servicio ‘network-manager’ para que los cambios surjan efecto:

systemctl start network-manager

4.2 - Grub

GNU GRUB es un cargador de arranque múltiple, desarrollado por el proyecto GNU que nos permite elegir qué Sistema Operativo arrancar de los instalados. Se usa principalmente en sistemas operativos GNU/Linux.

Configurar el tiempo de espera

Una configuración que siempre me gusta ajustar es el tiempo de espera, normalmente unos 5 segundos.

Para configuraciones con múltiples sistemas operativos esta bien que haya cierto margen para elegir el que queramos pero, al menos en mi caso, usando solo linux no veo necesidad de demorar el tiempo de arranque.

Esta configuración se puede editar en el fichero:

/etc/default/grub

Cambiando el parámetro “GRUB_TIMEOUT” por el valor que deseemos:

GRUB_DEFAULT=0 
GRUB_TIMEOUT=5 # <-- CAMBIAR POR EL VALOR QUE QUIERAS
GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian`
GRUB_CMDLINE_LINUX_DEFAULT="quiet"
GRUB_CMDLINE_LINUX=""

En mi caso, lo pongo a 0 para agilizar lo máximo posible.

4.3 - NFS

El servicio de NFS es un servicio de red que nos permite compartir archivos entre distintos equipos. Este funciona de forma similar a una carpeta compartida en Windows. Esta pensado para funcionar como servidor o cliente. El servidor comparte una carpeta con uno o más clientes y los clientes acceden a los archivos de la carpeta ( montando las carpetas en su sistema de ficheros ).

Esta guía se parte por un lado en la configuración de un servidor NFS y por otro en la configuración de un cliente NFS.

Servidor

Instalación de los requisitos

Para funcionar NFS sobre el servidor debemos instalar los siguientes paquetes:

apt install nfs-kernel-server nfs-common

Con esto ya podemos para a la configuración.

Preparando la carpeta a compartir

Primero tenemos que crear la carpeta que vamos a compartir (en el caso de que ya la tengas puedes omitir este paso):

mkdir <carpeta_compartida>

Luego tenemos que asignar un propietario especial a esta carpeta. Este usuario nobody y grupo nogroup se utilizan para que los clientes remotos puedan acceder a los archivos de la carpeta compartida.

chown nobody:nogroup <carpeta_compartida>

Configurando el servidor

Para configurar el servidor NFS, debemos modificar el archivo de configuración /etc/exports. Al final del fichero de configuración debemos añadir la siguiente línea:

/ruta/carpeta/compartida <origenes permitidos>(<opciones>)

# Ejemplo, los orígenes son todos los clientes, especificado con *.
/home/usuario/carpeta_compartida *(rw,sync,no_subtree_check,no_root_squash)

# Podíamos especificar múltiples clientes con distintas opciones por cada uno de ellos
/ruta/carpeta/compartida 192.168.1.56(rw,sync) 192.168.1.68(rw,sync)

Podemos especificar los origenes permitidos al servidor de las siguientes maneras:

  • Host único: podríamos especificar una IP o un nombre de host único como origen y repetir las opciones por cada uno de ellos.
  • Redes de IPs: Podríamos especificar rangos de IPs especificando la IP con máscara de red. Ejemplo, 192.168.1.0/24 permitiría todas las IPs de la red desde 192.168.1.0 hasta 192.168.1.255.
  • Comodines: Podríamos usar * para indicar que todos los clientes están permitidos y tambien se puede combinar con nombres de dominios. Por ejemplo, *.example.com permitiría todos los clientes que tengan con subdominio de example.com.
  • Grupos de red: Se podría definir un grupo de equipos de red especificándolo de la siguiente manera @grupo_de_red.

Veamos las diferentes opciones que podemos configurar por cada origen o grupo de orígenes:

  • Permisos de lectura: rw para lectura y escritura y ro para solo lectura.
  • Opciones de sincronización: sync para sincronizar los archivos y async para no sincronizar los archivos.
  • Opciones de compartición: no_subtree_check para que no se compruebe el subdirectorio y no_root_squash para que no se comparta el root.

Una vez configuradas las carpetas a compartir tendríamos que reiniciar el servicio para que se aplique la nueva configuración.

systemctl restart nfs-kernel-server

Permitir conexiones remotas en el firewall

Para permitir conexiones remotas en el firewall debemos permitir conexiones al puerto 2049 y 111 que utiliza el servicio NFS. Con ufw en ubuntu se puede hacer de la siguiente manera:

# Permitir orígenes específicos en el firewall
ufw allow from <origen> to any port nfs

# Permitir todos los orígenes en el firewall
ufw allow from any to any port nfs

Cliente

Instalación de los requisitos

Para funcionar NFS como cliente debemos instalar el siguiente paquete:

apt install nfs-common

Punto de montaje o carpeta donde montar los archivos remotos

Debemos espesificar la carpeta donde vamos a montar los archivos remotos. Esto se puede hacer con el comando mount:

mount <IP del servidor>:/ruta/carpeta/remota /ruta/carpeta/local 

Una vez montada la carpeta podemos listar todos los puntos de montaje del servidor con el siguiente comando:

showmount -e <IP del servidor>

También podríamos listar los montajes locales con el comando:

df -h

Por último, podríamos desmontar un punto de montaje con el comando:

umount /ruta/carpeta/local

Montaje automático en el arranque - Fstab

Podemos montar automáticamente la carpeta remota en el arranque añadiendo una línea de configuración en el archivo /etc/fstab:

La línea tendría el siguiente formato:

<IP del servidor>:<ruta/carpeta/remota> /ruta/carpeta/local nfs <opciones> 0 0

# Por ejemplo:
192.168.1.56:/home/user/compartida /home/cliente/compartida nfs rw,nofail,noatime,nolock,intr,tcp,actimeo=1800 0 0

Ahora podríamos reiniciar el servidor y comprobar que la carpeta se ha montado automáticamente. A veces tarda unos segundos/minutos en realizar el montaje.

4.4 - Zsh

En lo personal me encanta el terminal y en lo profesional lo veo indispensable para ciertas tareas.

Adaptarlo a nuestras necesidades y potenciar sus utilidades de caja me parece vital si pasas muchas horas delante de una interfaz de comandos.

En esta guía explico como configurar zsh a mis gustos personales, (en este vídeo tenéis todo el proceso más detallado):

Instalación de requisitos

Con el siguiente comando podéis instalar todos los requisitos necesarios para instalar zsh:

apt install curl zsh

Instalación de ohmyzsh

Ahora que tenemos instalado zsh podemos instalar el plugin de ohmyzsh:

sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

e voila!

Opción 1 - Configuración con antigen (recomendada)

Antigen es un plugin de ohmyzsh que nos permite configurar zsh con una interfaz más sencilla, funcionando como gestor de plugins y temas.

Podemos descargarlo con el siguiente comando:

curl -L git.io/antigen > antigen.zsh

Es importante que tengamos localizado el archivo antigen.zsh, personalmente lo suelo guardar dentro de la carpeta .oh-my-zsh por limpieza.

Configuración de antigen

Para configurar antigen debemos añadir el siguiente comando en el fichero .zshrc:

source <ubicación del archivo antigen.zsh>

Instalación de plugin y temas

Para configurar un plugin podemos usar el comando ‘antigen bundle’ y para seleccionar un tema se usa el comando ‘antigen theme’.

Por ejemplo:

# Definición de plugins
antigen bundle git
antigen bundle git-prompt

# Definición de un tema
antigen theme robbyrussell

Para finalizar la configuración de antigen debemos añadir el siguiente comando en el fichero .zshrc:

antigen apply

La principal ventaja que nos aporta antigen es que deja un fichero de configuración mucho más limpio y, además, nos instala automaticamente los plugins y temas que hemos defino.

Las próxima vez que hagamos source .zshrc antigen se encargará de gestionarlo todo.

Así quedaría el fichero .zshrc completo:

source ~/.oh-my-zsh/antigen.zsh

# Load the oh-my-zsh's library.
antigen use oh-my-zsh

# Bundles from the default repo (robbyrussell's oh-my-zsh).
antigen bundle git
antigen bundle git-prompt
antigen bundle z 
antigen bundle pip
antigen bundle kubectl 
antigen bundle git-prompt
antigen bundle vi-mode
antigen bundle docker
antigen bundle docker-compose

# Syntax highlighting bundle.
antigen bundle zsh-users/zsh-syntax-highlighting
antigen bundle zsh-users/zsh-autosuggestions

# Load the theme.
antigen theme bureau 

# Tell Antigen that you're done.
antigen apply

Opción 2 - Configuración de ohmyzsh (Sin antigen)

Recuerda que todas las configuraciones que hagas en zsh se guardarán en el archivo .zshrc. Aquí podemos editar el tema, los plugins, añadir alias.. etc.

Tema

Se puede editar el tema de zsh en el parámentro ZSH_THEME: del fichero .zshrc. Por ejemplo:

ZSH_THEME="robbyrussell"

Así se cambia al tema por defecto de ohmyzsh.

Plugins

Para añadir un plugin a zsh, debemos añadirlo en la sección plugins del fichero .zshrc separados por espacios. Por ejemplo:

plugins=(git sudo docker z )

Recargar cambios

Recuerda que cada vez que modifiques el fichero .zshrc tendras que cargar de nuevo la configuración con el comando:

source ~/.zshrc

Autocompletado y resaltado de color

Estas funcionalidades no vienen por defecto pero si podemos intalar los plugins para activarlos posteriormente. Aquí los enlaces a sus repositorios: autocomplete highlight

Los podríamos instalar respectivamente con los siguientes comandos:

#Instalar autocompletado
git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions

#Instalar resaltado de color
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting

Los podríamos activar añadiéndolos en el parámetro de plugins en el fichero .zshrc:

plugins=(zsh-syntax-highlighting zsh-autosuggestions)

4.5 - En navegador

Hoy en día tenemos aplicación cualquier aplicación la podemos ejecutar en nuestro navegador web. Pero porque tener aplicaciones sueltas si podemos usarlo para acceder a nuestro sistema operativo completo. Es verdad que existen soluciones como Microsoft 365, Horizon y demás, las cuales están enfocadas principalmente en entornos corporativos windows y la mayoría requieren de clientes para poder usarlos.

El objetivo de esta guía es explicar como ejecutar una distribución linux en tu navegador. Bueno, siendo sinceros, esto tiene un poco de truco. Efectivamente vamos a conseguir tener acceso a una distribución linux en el navegador, pero realmente no lo esta ejecutando el mismo.

Normalmente este tipo de sistemas funcionan en linux gracias a un proyecto llamado Apache Guacamole. Esta es una herramienta de conexión remota que permite hacer de cliente para conectarse a un sistema a través de RDP o VNC.

Este es el esquema de lo que vamos a hacer:

Jerarquía de servicios

El objetivo es que nuestro contenedor lance un servicio de VNC o RDP que no esté expuesto y por otra parte un servidor de Apache Guacamole que exponha un servidor web al que podemos conectarnos con el navegador web. Desde esta página podremos gestionar entornos remotos y por debajo otro servicio hace de cliente contra estos entornos remotos.

Veremos dos formas de conseguirlo, la primera totalmente automática y la segunda haremos de forma manual para desglosar los pasos.

Forma fácil y automática (Recomendada para todos)

De la mano de las imágenes de docker de linuxserver.io podemos ejecutar contenedores de linux con todo lo hablado anteriormente, en diferentes distribuciones y con diferentes gestores de ventanas.

Ofrecen las principales distribuciones de linux, como ubuntu, alpine, fedora, arch en combinación con los gestores de ventanas más populares XFCE, KDE, i3, Openbox, IceWM… etc.

Podéis consultar el listado completo en linuxserver.io. Este es un breve resumen del tag que tendrías que usar en la imagen de tu contenedor en función del sabor que prefieras:

Tag Description
latest XFCE Alpine
ubuntu-xfce XFCE Ubuntu
fedora-xfce XFCE Fedora
arch-xfce XFCE Arch
alpine-kde KDE Alpine
ubuntu-kde KDE Ubuntu
fedora-kde KDE Fedora
arch-kde KDE Arch

Para ejecutar un contenedor de esta forma, simplemente ejecutamos el comando con la configuración de los diferentes parámetros. Importante entender los parámetros opciones, concretamente, el montaje del socket. Este nos permitirá usar docker dentro del contenedor pero pondrá en riesgo la seguridad de nuestro entorno en caso de ser vulnerado.

Sobre la seguridad en contenedores hablo más extendidamente en esta video entrada Seguridad en contenedores.

Comando de ejemplo (en la sección de Parameters de DockerHub se explica cada uno de ellos):

docker run -d \
  --name=webtop \
  --security-opt seccomp=unconfined `#optional` \
  -e PUID=1000 \
  -e PGID=1000 \
  -e TZ=Europe/London \
  -e SUBFOLDER=/ `#optional` \
  -e KEYBOARD=en-us-qwerty `#optional` \
  -p 3000:3000 \
  -v /path/to/data:/config \
  -v /var/run/docker.sock:/var/run/docker.sock `#optional` \
  --device /dev/dri:/dev/dri `#optional` \
  --shm-size="1gb" `#optional` \
  --restart unless-stopped \
  lscr.io/linuxserver/webtop

Personalmente, si no necesitas usar docker dentro de este contenedor ni montar ningún archivo lo ejecutaría así (para potenciar su seguridad, poner el teclado en español y la hora local):

docker run -d \
  --name=pabpereza.dev \
  --security-opt seccomp=unconfined `# Activar solo si no te funciona correctamente` \
  -e PUID=1000 \
  -e PGID=1000 \
  -e TZ=Europe/Madrid `# Hora local` \
  -e KEYBOARD=es-es-qwerty `# Teclado en castellano` \
  -e AUTO_LOGIN=true `# Poner a false para quitar el autologin (recuerda cambiar la pass)` \
  -p 3000:3000 \
  -v /var/run/docker.sock:/var/run/docker.sock `#Borrar si no queremos usar docker dentro del contenedor` \
  --shm-size="1gb" `#Previene a los navegadores modernos fallar por límites de memoria` \
  --restart unless-stopped \
  lscr.io/linuxserver/webtop `# Despues de webtop con : podríamos añadir el tag para usar la distribución que queramos por defecto Alpine con XFCE`

Ya lo tendríamos funcionando. También podemos mejorar el rendimiento siguiendo la guía de Hardware acceleration (en la página de DockerHub) aunque solo funciona en el contenedor de ubuntu.

Más que aconsejable cambiar la contraseña al usuario del contenedor para que nadie pueda acceder a él directamente o poner un proxy previo con autenticación de algún tipo. Además, puedes desactivar el autologin (Variable de entorno AUTO_LOGIN en el comando de docker run).

Esto lo podemos hacer con el comando (en un terminal dentro del contenedor):

sudo passwd <usuario>

Seguidamente se nos pedirá la nueva contraseña para el usuario (si os pide la anterior por defecto es abc como el usuario). Las próximas veces que entréis al contenedor a través del navegador os pedirá la contraseña para conectarse.

4.6 - Desactivar swap

Para ciertos escenarios es recomendable desactivar el swap o incluso obligatorio. Por ejemplo, en el caso de kubernetes si o si se debe desactivar el swap.

Ubuntu 20.04

En ubuntu 20.04 podemos ver el estado de la memoria swap con el siguiente comando:

sudo swap --show

# También nos serviría el comando free
sudo free -h

Para desactivarlas, podemos usar el comando:

sudo swapoff -a #Esto la desactiva, pero solo de forma temporal

# Luego tendríamos que borrar el archivo de intercambio
sudo rm /swap.img

# Por último, borramos la línea de swap del fichero de configuración de linux fstab para que no se recree cuando el sistema se reinicie
sudo sed -i '/swap/d' /etc/fstab

5 - Windows

Herramienta, utilidades y configuración para el sistema operativo de Microsoft

5.1 - Oh my posh

Página oficial: Oh My Posh

Aquí tenéis también el video tutorial:

Instalación

El módulo de oh my posh se puede instalar desde la documentación de este enlace: Instalación en windows

Mediante winget (el gestor de paquetes nativo de windows) podemos instalarlo con un solo comando:

winget install JanDeDobbeleer.OhMyPosh -s winget

Una vez instalado, nos queda configurarlo para que arranque cada vez que iniciamos powershell. Para ello, abrimos el fichero profile de powershell:

 notepad $PROFILE

Una vez abierto notepad, añadimos las siguientes líneas y guardamos:

oh-my-posh init pwsh  | Invoke-Expression

Esto permitirá que se arranque solo cada vez que abramos el terminal de powershell.

Fuentes ( Nerd Fonts )

Podemos instalar fuentes con el comando (Requiere permisos de administrador):

oh-my-posh font install

Esto nos abrirá un prompt interactivo para elegir la fuente que queremos que nos instale.

También podríamos instalarlas desde la página de Nerd Fonts

Una vez instalada la fuente que nos interesa usar, no nos olvidemos de seleccionarla en nuestra aplicación o gestor de terminales favorito.

En el caso de windows terminal: Configuración (Control , ) -> Perfiles -> Valores predeterminados -> Apariencia -> Tipo de Fuente

Módulos de terceros

PSReadLine

Este módulo nos permitirá activar el autocompletado de comandos en base a nuestro historial de una forma gráfica y cómoda:

Instalación:

Install-Module -Name PSReadLine -AllowPrerelease -Scope CurrentUser -Force -SkipPublisherCheck

Además de la instalación tendremos que añadir al nuestro script ubicado $PROFILE, por defecto, ubicado en ~/Documents/PowerShell/Microsoft.PowerShell_profile.ps1:

Set-PSReadLineOption -PredictionSource History
Set-PSReadLineOption -PredictionViewStyle ListView
Set-PSReadLineOption -EditMode Windows
oh-my-posh init pwsh --config 'https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/material.omp.json' | Invoke-Expression

Este es el resultado final de mi configuración importando remotamente el tema de oh-my-posh a utilizar.