Kubernetes Network Policy

On this post, I will show how you can isolate services within the same namespace in Kubernetes.

Why would you want to do that? Think of this as if you want to test your app behind a firewall.

To achive this, Kubernetes provides Network Policy, it allows us to protect who can connect to a service. So, the first step will be deny all traffic in our namespace:

---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
spec:
  podSelector: {}
  policyTypes:
  - Ingress

Now, every service you deploy won’t be reachable within the cluster.

Let’s deploy now an Apache Server.

---
kind: Service
apiVersion: v1
metadata:
  name: apache1
  labels:
    app: web1
spec:
  selector:
    app: web1
  ports:
  - protocol: TCP
    port: 80
    name: http
---
apiVersion: apps/v1 # for versions before 1.8.0 use apps/v1beta2
kind: Deployment
metadata:
  name: apache1
  labels:
    app: web1
spec:
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: web1
  replicas: 1 
  template: 
    metadata:
      labels:
        app: web1
    spec:
      containers:
      - name: apache-container
        image: httpd:2.4
        ports:
        - containerPort: 80

And a simple container to test the connection:

---
apiVersion: apps/v1 # for versions before 1.8.0 use apps/v1beta1
kind: Deployment
metadata:
  name: ubuntu1
  labels:
    allow-access: "true"
spec:
  strategy:
    type: Recreate
  selector:
    matchLabels:
      allow-access: "true"
  replicas: 1 
  template: 
    metadata:
      labels:
        allow-access: "true"
    spec:
      containers:
      - name: nordri-container
        image: nordri/nordri-dev-tools
        command: ["/bin/sleep"]
        args: ["3600"]

Here, you can see a label allow-access: “true” we’ll use that to grant access to the service.

And, finally, the Network Policy.

---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: net1
  namespace: default
spec:
  podSelector:
    matchLabels:
      app: web1
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          allow-web1: "true"
    ports:
    - protocol: TCP
      port: 80

As you can see, we can protect the Service web1 allowing the access only from those pods with label allow-access: “true”.

At this point you have something like that

NAME                         READY     STATUS              RESTARTS   AGE
po/apache1-5565b647c-dt6kk   1/1       Running   0          38s
po/ubuntu1-c8cffc57b-q62tw   1/1       Running   0          38s

NAME             TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
svc/apache1      ClusterIP   10.111.18.130           80/TCP    39s
svc/kubernetes   ClusterIP   10.96.0.1               443/TCP   7m

NAME             DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deploy/apache1   1         1         1            0           38s
deploy/ubuntu1   1         1         1            0           38s

NAME                   POD-SELECTOR   AGE
netpol/default-deny             59s
netpol/deny-metadata            6m
netpol/net1            app=web1       38s

And you can test from the container:

kubectl exec ubuntu1-c8cffc57b-q62tw -- curl apache1
<html><body><h1>It works!</h1></body></html>

But, from any other pod

kubectl run nordri-dev-tools --rm -ti --image=nordri/nordri-dev-tools /bin/bash
If you don't see a command prompt, try pressing enter.
root@nordri-dev-tools-766cc58546-kmhbf:/# 
root@nordri-dev-tools-766cc58546-kmhbf:/# curl apache1
curl: (7) Failed to connect to apache1 port 80: Connection timed out
root@nordri-dev-tools-766cc58546-kmhbf:/# 

Cool, isn’t it?

Let’s do something more complex and interesting. Check out this scenario:

What we want is:

Master  -> apache1 OK
Master  -> apache2 OK
Ubuntu1 -> apache1 OK
Ubuntu2 -> apache2 OK
Ubuntu1 -> apache2 Time out
Ubuntu2 -> apache1 Time out

So, clone this GitHub repository

git clone https://github.com/nordri/kubernetes-experiments

And run…

cd NetworkPolicy
./up.sh

This script will deploy all the components of our experiment. Now, there’s something like this in your Kubernetes:

NAME                          READY     STATUS    RESTARTS   AGE
po/apache1-5565b647c-fxcpc    1/1       Running   0          1m
po/apache2-587775d7bd-pjg98   1/1       Running   0          1m
po/master-6df9c89f5b-pqtl2    1/1       Running   0          1m
po/ubuntu1-588f5bdbfc-qkh8t   1/1       Running   0          1m
po/ubuntu2-f689cc5d-ncbzh     1/1       Running   0          1m

NAME             TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
svc/apache1      ClusterIP   10.110.39.59            80/TCP    1m
svc/apache2      ClusterIP   10.109.61.240           80/TCP    1m
svc/kubernetes   ClusterIP   10.96.0.1               443/TCP   23m

NAME             DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deploy/apache1   1         1         1            1           1m
deploy/apache2   1         1         1            1           1m
deploy/master    1         1         1            1           1m
deploy/ubuntu1   1         1         1            1           1m
deploy/ubuntu2   1         1         1            1           1m

NAME                   POD-SELECTOR   AGE
netpol/default-deny             1m
netpol/deny-metadata            22m
netpol/etm-net2        app=web2       1m
netpol/net1            app=web1       1m

And we can check that apache1 and apache2 only can be reached from master and the container sharing the Network policy.

To clean the house just run:

cd NetworkPolicy
./down.sh

References:

Lanzar Workers de Jenkins en AWS

En esta entrada veremos lo sencillo que resulta configurar Jenkins para levantar instancias en AWS EC2 para el entorno de CI.

1. Crear la AMI con packer.

Tenemos ya algunos nodos en el entorno de CI que se provisionan con Ansible. Utilizamos packer y la receta de ansible para crear una imagen con todo lo necesario para los jobs de Jenkins.

El fichero Json para packer

{
  "variables": {
    "aws_access_key": "",
    "aws_secret_key": ""
  },
  "builders": [{
    "type": "amazon-ebs",
    "access_key": "{{user `aws_access_key`}}",
    "secret_key": "{{user `aws_secret_key`}}",
    "region": "eu-west-1",
    "source_ami": "ami-a8d2d7ce",
    "instance_type": "t2.small",
    "ssh_username": "ubuntu",
    "ami_name": "codeurjc-jenkins-worker {{ timestamp }}"
  }],
  "provisioners": [
    {
      "type": "shell",
      "inline": [
        "sudo apt-get update",
        "sudo apt-get install -y python"
      ]
    },
    {
      "type": "ansible",
      "playbook_file": "./playbook.yml",
      "user": "ubuntu"
    }]
}
$ packer build -var 'aws_access_key=ACCESS_KEY' -var 'aws_secret_key=SECRET_KEY' packer-jenkins-worker.json

2. Configuración de seguridad

Por un lado tenemos que crear un usuario para manejar la API. Vamos a nuestra consola de AWS y clickamos AMI, en Users le damos a Add user, le damos un nombre por ejemplo jenkins y en Access type elegimos Programmatic access. En el siguiente paso otorgamos permisos al usuario para manejar el servicio de EC2 creando un grupo especifico para ello, si no lo tenemos creado previamente. Repasamos y creamos el usuario, veremos las claves para acceder, copiar y guardarlas en lugar seguro.

Ahora vamos a crear un grupo de seguridad para las instancias que levantemos de Jenkins. Tenemos que permitir acceso por SSH a la instancia, asi que nos vamos a la consola de EC2 en el apartado de Security group y creamos uno nuevo (si no está creado previamente) y damos permisos para acceder por SSH.

Por último tenemos que importar la clave pública que permitirá a Jenkins configurar los nodos que despleguemos en AWS. Vamos a la consola de EC2, Key Pairs y hacemos click en Import Key Pair, en el cuadro de diálogo pegamos la clave pública que usamos en los nodos de nuestra infraestructura. Le ponemos un nombre por ejemplo Jenkins

3. Plugin de Jenkins

En Jenkins necesitamos este plugin.

La configuración es secilla, vamos a Manage Jenkins -> Cloud -> New cloud

Le ponemos un nombre descriptivo.

Añadimos las credenciales que hemos creado en el paso 2.

Seleccionamos la región, en mi caso eu-west-1

Para la clave, como tenemos workers en nuestra infraestructura tenemos que pegar la clave privada que tiene Jenkins.

Podemos ahora probar que la conexión a la API de AWS funciona.

Pasamos a la configuración de la AMI.

De nuevo un nombre descriptivo

La ID de la AMI que hemos creado con Packer anteriormente.

El tipo de instancia, como tenemos mucha carga usamos T2Large.

De nuevo la zona de disponiblidad, eu-west-1a

Introducimos el grupo de seguridad que hemos creado en AWS para que nos permita acceder por SSH a la instancia.

En el usuario remoto, nosotros usamos jenkins.

El tipo de AMI es obviamente una UNIX, esto es necesario porque dependiendo del tipo, la conexión se hace de una forma u otra.

Añadir el label para asignar trabajos al nodo.

En el uso ponemos que solo ejecute jobs que coincidan con el nodo.

En Idle termination time establecemos los minutos que tiene que estar la instancia ociosa antes de que se apague (stop o terminate).

En Init script podemos escribir algunas instrucciones necesarias en tiempo de arranque para la instancia, por ejemplo podríamos provisionar la instancia haciendo que se clonara el repo de Ansible y ejecutarlo con conexión local, en nuestro caso la AMI ya viene completa y no necesitamos nada.

Desplegamos más opciones haciendo click en Advance. Las opciones que más nos interesan son:

Override temporary dir location nos permite establecer un directorio temporal donde copiar el slave.jar para ejecutar Jenkins.

User data, para pasar información a la instancia.

Number of Executors, depende del Instance type que hayamos elegido, en nuestro caso 8.

Stop/Disconnect on Idle Timeout, si elegimos esta opción la instancia se parará, no se destruirá, esto con los disco EBS puede repercutir en cargos adicionales a la factura.

Subnet ID for VPC, para establecer la red a la que debe estar conectada la instancia.

Instance Cap, el limite de instancias a levantar en AWS, a más instancias más factura.

Block device mapping, elegimos un volumen EBS personalizado porque el de 8 GB que viene de serie es demasiado pequeño, la línea queda de esta manera.

/dev/sda1=:120:true:::

De forma que el disco raiz se mapea a un dispositivo que se creara en el momento de lanzar la instancia con 120GB y se borrará cuando se destruya la instancia.

Podemos conservar el volumen con false. Si ya tenemos algún disco provisionado podemos poner el nombre entre el igual y los dos puntos.

4. Configurar el Job

Ahora solo nos quedaría configurar un job con el label que hemos elegido para las AMIs, esto se entrando en la configuración de Job en cuestión y en el campo Restrict where this project can be run colocar el label.

Docker: Mapear usuarios dentro del contenedor

En ocasiones que trabajamos con Docker necesitamos generar ficheros. Los ficheros que se generan en el contenedor por defecto pertenecen a root. Vamos a ver una forma para mapear usuarios del sistema dentro del contenedor. En concreto nos interesa mapear nuestro propio usuario deforma que el propietario de un fichero sea el mismo dentro y fuera del contenedor.

Tenemos al usuario foo que tiene una entrada en /etc/passwd como esta:

foo:x:1001:1001::/home/foo:/bin/bash

Empecemos por preparar un directorio de trabajo para foo dentro del contenedor.

$ mkdir ~/docker_home
$ cp /etc/skel/{.bash_logout,.bashrc,.profile} ~/docker_home

Ahora cuando vayamos a lanzar el contenedor, debemos mapear el usuario:

$ docker run -ti \
 -v /etc/passwd:/etc/passwd \
 -v /etc/group:/etc/group \
 -v /etc/shadow:/etc/shadow \
 -v /home/foo/docker_home:/home/foo ubuntu:16.04

La entrada al contenedor seguirá siendo con el usuario root pero ahora podemos hacer:

su - foo

Para trabajar con nuestro usuario.

Si queremos entrar directamente con nuestro usuario hacemos así:

$ docker run -ti \
 -v /etc/passwd:/etc/passwd \
 -v /etc/group:/etc/group \
 -v /etc/shadow:/etc/shadow \
 -v /home/foo/docker_home:/home/foo ubuntu:16.04 \
 su -s /bin/bash -c "/bin/bash" foo

Por último si queremos que el contenedor sea más caja negra debemos tener en cuenta que hay que modificar el Dockerfile a la hora de construir la imagen.

Owncloud con almacenamiento en S3 (con replicación de zona) y Let’s Encrypt

Una entrada completita para antes de irnos de vacaciones

Contendores y Proxy Pass

Comenzamos creando una instancia en OpenStack a la que provisionamos con un volumen para la persistencia de datos. El volumen lo podemos montar en /mnt y será nuestro directorio de trabajo.

En el directorio de trabajo creamos la siguiente estructura de directorios:

.
├── docker-compose
├── mysql
└── owncloud

En el directorio de docker-compose creamos el fichero yaml con el estado deseado de los contendedores:

---
version: '2'
services:
  owncloud:
    image: owncloud
    ports:
      - 8080:80
    volumes:
      - /mnt/volumen/owncloud:/var/www/html
  mysql:
    image: mariadb
    volumes:
      - /mnt/volumen/mysql/db:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: s3cr3t

Por no liar con los grupos de seguridad de OpenStack solo permito acceso al puerto 80/tcp así que vamos a levantar un Nginx delante de los contenedores.

# apt install nginx
# vim /etc/nginx/sites-enable/default
## Añadir este contenido ##
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name owncloud.example.com;

    root /var/www/html;

    # Add index.php to the list if you are using PHP
    index index.html index.htm index.nginx-debian.html;

    location / {
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_pass http://localhost:8080;
    }
}
## Reiniciar el servidor ##
# systemctl restart nginx

Podemos acceder a la url con el navegador y configuramos el usuario y la password para Owncloud. La conexión a la base de datos que podemos acceder por el host mysql con la contraseña que hemos elegido previamente.

Una vez dentro de Owncloud instalamos la App External storage support y lo dejamos aquí de momento.

Configuración de S3

Entramos en la consola de AWS y creamos un usuario, necesitaremos el AccessKeyID y el SecretAccessKey, este usuario podrá acceder a los buckets. A continuacion creamos dos buckets S3. Como vamos a replicarlos entre zonas tenemos que actuvar Versioning. En la pestaña de permisos eliminamos al owner ya que usaremos Policy. Con el recurso creado vamos la pestaña Permissions -> Bucket Policy y añadimos las siguente politica:

{
            "Sid": "AllowAccess",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::ACCOUNT:user/USER"
            },
            "Action": [
                "s3:DeleteObject",
                "s3:GetObject",
                "s3:PutObject"
            ],
            "Resource": "arn:aws:s3:::BUCKET_NAME/*"
        },
        {
            "Sid": "AllowList",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::ACCOUNT:user/USER"
            },
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::BUCKET_NAME"
        },
         {
            "Sid": "IPAllow",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:*",
            "Resource": "arn:aws:s3:::BUCKER_NAME/*",
            "Condition": {
            "IpAddress": {"aws:SourceIp": "IP de OWNCLOUD"}
         } 
       } 
    ]
}

Nos movemos ahora a la pestaña de Properties y activamos Cross-region replication completamos el formulario que nos aparece. En source el bucket de origen, en Destination ponemos la zona y el segundo bucket que hemos creado. en Destination storage class establecemos con Same as source object y en Select role ponemos Create new.

En el segundo bucket establecemos la política de acceso cambiando el nombre del bucket

Configurar Owncloud

Volvemos a nuestro Owncloud y entramos a la pestaña de configuración donde configuramos la cadena de conexión al bucket es decir, le ponemos el S3 access key el S3 secret key y el nombre del bucket. Si todo está correcto un punto verde debería aparecer a la izquierda del cuadro de diálogo. También podéis probar a subir y borrar archivos también para comprobar que replican a la otra zona de disponibilidad.

Let’s Encrypt

Vamos a instalar certbot para gestionar automáticamente los certificados digitales. Para ello seguimos estos pasos:

# Instalamos:
$ sudo add-apt-repository ppa:certbot/certbot
$ sudo apt-get update
$ sudo apt-get install certbot 
# Generamos los certificados
$ certbot certonly --webroot -w /mnt/volumen/owncloud -d owncloud.example.com
# Este comando crea un fichero en la raiz del servidor para verificar que somos propietarios del dominio.
# A continuación generará el certificado en
# /etc/letsencrypt/archive/owncloud.example.com/fullchain1.pem

Ya con el certificado configuramos Nginx:

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name owncloud.example.com;

    root /var/www/html;

    # Add index.php to the list if you are using PHP
    index index.html index.htm index.nginx-debian.html;

    location / {
        return 301 https://$host$request_uri;
    }
}
server {
    listen 443 ssl;
    server_name owncloud.example.com;

        root /var/www/html;

    ssl_certificate     /etc/letsencrypt/archive/owncloud.example.com/fullchain1.pem;
    ssl_certificate_key /etc/letsencrypt/keys/0000_key-certbot.pem;
    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers         HIGH:!aNULL:!MD5;

    location / {
        proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_pass http://localhost:8080;
    }
}

Reiniciar Nginx

# systemctl restart nginx

Habilitar docker-compose para que arranque con el sistema

Ahora configuramos para que si se tiene que reiniciar la máquina por algún motivo los contenedores arranquen automáticamente. Para ello añadimos un nuevo servicio a systemd

# vim /etc/systemd/system/docker-compose.service
## Añadimos este contenido:
[Unit]
Description=Owncloud on Docker Compose
Requires=docker.service
After=network.target docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/mnt/volumen/docker-compose
ExecStart=/usr/local/bin/docker-compose -f /mnt/volumen/docker-compose/docker-compose.yaml start 
ExecStop=/usr/local/bin/docker-compose -f /mnt/volumen/docker-compose/docker-compose.yaml stop

[Install]
WantedBy=multi-user.target
## Y recargamos la configuración
# systemctl daemon-reload

Y listo.

OpenStack & Kubernetes: LBaaS y Self Service Provisioning

Ya que tenemos desplegado Kubernetes en OpenStack vamos a aprovechar las capacidades de este para desplegar nuestras aplicaciones.

Configurar Kubernetes para utilizar OpenStack

El primer paso que tenemos que dar es configurar nuestras credenciales de OpenStack en Kubernetes. Para ello editamos el fichero /etc/kubernetes/cloud_config y añadimos las siguientes líneas:

[Global]
auth-url=http://AUTH_URL:5000/v3
username=demo
password=s3cr3t
region=RegionOne
tenant-id=d245f207e6fc4e56b5a1d23e0716ad70
domain-name=Default

De esta forma el API de Kubernetes puede comunicar con el API de OpenStack y configurar los servicios.

El campo domain-name solo es necesario si usamos API v3 de OpenStack, como indica la AUTH_URL.

Ahora debemos cambiar añadir lo siguiente:

KUBE_API_ARGS=[...] --cloud-provider=openstack --cloud-config=/etc/kubernetes/cloud_config

al arranque de nuestro Kubernetes apiserver.

Igualmente en /etc/kubernetes/manifests/kube-controller-manager.yaml:

- [...]
- --cloud_config=/etc/sysconfig/kube_openstack_config
- --cloud_provider=openstack

Aquí la cosa se complica y depende de tu instalación. Si tienes el binario en el sistema solo tienes que ajustar en /etc/kubernetes/* pero si estás usando contenedores, deberás volver a construir los contenedores y añadir estos parámetros.

Utilizar el LBaaS de OpenStack en Kubernetes

Para habilitar el balanceador debemos añadir los siguientes datos en nuestro fichero de cloud_config:

  1. La subred donde se va a construir el balanceador.
  2. Debemos indicar que maneje los security groups.
  3. El security group que se debe aplicar al puerto del balanceador. Es el vip_port_id anterior.
  4. Y por último la id de la red de la que tomará la IP flotante.

Deberiamos tener algo así:

[LoadBalancer]
lb-version=v2
subnet-id=82f277d7-1b29-49b3-94ee-40424c4e36d9
manage-security-groups=true
node-security-group=b063453a-41fb-47e5-ac3e-fbe270fc2232
floating-network-id=ccee1f8b-c813-4a69-a072-ee1e10aba677

¿Qué tiene que pasar? Ahora, cuando en el service pongamos LoadBalancer Kubernetes utilizará las credenciales que le hemos proporcionado y construirá un balanceador completo para la aplicación. De la red que le hemos proporcionado cogerá un IP flotante libre. Esta es la IP a la que debemos atacar cuando queramos utilizar el servicio recién publicado.

Self Service Provisioning

Para que nuestras aplicaciones conserven los datos que generen en su tiempo de vida es necesario proporcionarles volúmenes de persistencia. Para ello comenzamos creando un nuevo objeto de Kubernetes llamado StorageClass donde vamos a definir como debe pedir el volumen a OpenStack y como vamos a llamarlo desde nuestra aplicación. Podemos definir tantos como sistemas de aprovisionamiento tengamos. El StorageClass tiene esta pinta:

kind: StorageClass
apiVersion: storage.k8s.io/v1beta1
metadata:
  name: gold #-> este es el nombre con el que nos referiremos a el cuando hagamos el claim
provisioner: kubernetes.io/cinder
parameters:
  type: kube-storage #-> Esto hay que definirlo en openstack
  availability: nova

Como vemos los dos valores que tenemos que definir son name y type. Entonces, cuando queramos que nuestra aplicación haga uso de esta persistencia dinámica usaremos algo como esto:

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: claim1
  annotations:
    volume.beta.kubernetes.io/storage-class: gold #-> Este es el nombre que le hemos dado
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 30Gi

Antes había que hacer uso de PersistentVolume pero está siendo reemplazado por este nuevo objeto que creará el volumen dinámicamente.

Referencias