Tag Archives: docker

Azure Batch: Task in Containers

In today’s post I’ll be talking about how to send tasks to Microsoft Azure Batch able to run in containers. The task I want to solve in this example is calculating whether a number is prime or not. This Python code does the work for us. Then, I’ve write a Dockerfile adding the piece of code to the image. Now, we’re able to run the script from a Docker container.

Let’s move forward to Azure Batch, you need to create a Docker registry where you’ll push the Docker image and an Azure Batch account.

Docker Registry

Login into your Azure account and move to All resources, click Add and look for Container Registry. Then click in Create. Fill up the information with the name of the registry, the resource group, choose a location closer to you, enable Admin user to be able to push to the repo and the SKU (choose standard here).

Then Create

In a few seconds the registry will be ready. So go to the Dashboard and click on the registry name (the one you chose before). Click in Settings -> Access keys. Here are the credentials you’ll need to manage the registry.

Batch Account

From the All resources look for Batch service. Fill up the information with the Account name and Location, Subscription and Resource group should be ready.

Click Review + create and then Create. In a few seconds the service should be ready.

Building the Container

Clone the repo

git clone https://github.com/nordri/microsoft-azure

and build and push the container

cd batch-containers
docker build -t YOUR_REGISTRY_SERVER/YOUR_REGISTRY_NAME/YOUR_IMAGE_NAME .
# for example:
docker build -t pythonrepo.azurecr.io/pythonrepo/is_prime:latest .
# Check the image works:
docker run -ti --rm pythonrepo.azurecr.io/pythonrepo/is_prime python is_prime.py 7856
The number: 7856 is not prime
docker run -ti --rm pythonrepo.azurecr.io/pythonrepo/is_prime python is_prime.py 2237
The number: 2237 is prime
# login first
docker login pythonrepo.azurecr.io
Username: pythonrepo
Password: 
# Push
docker push pythonrepo.azurecr.io/pythonrepo/is_prime

Azure Batch

Now it’s time to send the task to Azure Batch. To do this, I’ve worked over this Python script. This script creates a pool, a job and three tasks to upload files to Azure Storage. So, I’ve made some modifications to fit it to my needs.

Creating the Pool

I need my pool to be created using instances able to run containers

...
def create_pool(batch_service_client, pool_id):
    print('Creating pool [{}]...'.format(pool_id))

    image_ref_to_use = batch.models.ImageReference(
        publisher='microsoft-azure-batch',
        offer='ubuntu-server-container',
        sku='16-04-lts',
        version='latest'
    )

    # Specify a container registry
    # We got the credentials from config.py
    containerRegistry = batchmodels.ContainerRegistry(
        user_name=config._REGISTRY_USER_NAME, 
        password=config._REGISTRY_PASSWORD, 
        registry_server=config._REGISTRY_SERVER
    )

    # The instance will pull the images defined here
    container_conf = batchmodels.ContainerConfiguration(
        container_image_names=[config._DOCKER_IMAGE],
        container_registries=[containerRegistry]
    )

    new_pool = batch.models.PoolAddParameter(
        id=pool_id,
        virtual_machine_configuration=batchmodels.VirtualMachineConfiguration(
            image_reference=image_ref_to_use,
            container_configuration=container_conf,
            node_agent_sku_id='batch.node.ubuntu 16.04'),
        vm_size='STANDARD_A2',
        target_dedicated_nodes=1
    )

    batch_service_client.pool.add(new_pool)
...

The key is the ImageReference where we set the instances to run with an OS able to run Docker. You must set the registry credentials and the default Docker image that will be pulled when the instance boots.

Creating the Task

I’ve also changed the Task for the same reason. This task is ready to launch a container in the instance.

...
def add_tasks(batch_service_client, job_id, task_id, number_to_test):
    print('Adding tasks to job [{}]...'.format(job_id))

    # This is the user who run the command inside the container.
    # An unprivileged one
    user = batchmodels.AutoUserSpecification(
        scope=batchmodels.AutoUserScope.task,
        elevation_level=batchmodels.ElevationLevel.non_admin
    )

    # This is the docker image we want to run
    task_container_settings = batchmodels.TaskContainerSettings(
        image_name=config._DOCKER_IMAGE,
        container_run_options='--rm'
    )
    
    # The container needs this argument to be executed
    task = batchmodels.TaskAddParameter(
        id=task_id,
        command_line='python /is_prime.py ' + str(number_to_test),
        container_settings=task_container_settings,
        user_identity=batchmodels.UserIdentity(auto_user=user)
    )

    batch_service_client.task.add(job_id, task)
...

You can see how I’ve defined the user inside the container as a non admin user. The Docker image we want to use and the arguments we need to pass in the command line, remember we launch the container like:

docker ... imagename python /is_prime.py number

Launching the Script

Configure

In order to launch the script we need to fill up some configuration. Open the config.py file and write all the credentials needed. Remember, all the credentials are in the Access keys section.

Installing Dependencies

You need Azure Python SDK installed to run the script.

pip install -r requirements.txt

Let’s go

Now we’re ready to launch the script:

python batch_containers.py 89
Sample start: 2018-11-10 10:11:11

Creating pool [ContainersPool]...
No handlers could be found for logger "msrest.pipeline.requests"
Creating job [ContainersJob]...
Adding tasks to job [ContainersJob]...
Monitoring all tasks for 'Completed' state, timeout in 0:30:00.....................................................................................................................................................................
  Success! All tasks reached the 'Completed' state within the specified timeout period.
Printing task output...
Task: ContainersTask
Standard output:
The number: 89 is prime

Standard error:


Sample end: 2018-11-10 10:14:31
Elapsed time: 0:03:20

Delete job? [Y/n] y
Delete pool? [Y/n] y

Press ENTER to exit...

If there’s a problem with the script we’ll see the error on stderr.txt.

Sample start: 2018-11-10 11:29:56

Creating pool [ContainersPool]...
No handlers could be found for logger "msrest.pipeline.requests"
Creating job [ContainersJob]...
Adding tasks to job [ContainersJob]...
Monitoring all tasks for 'Completed' state, timeout in 0:30:00..................................................................................................................................................................
  Success! All tasks reached the 'Completed' state within the specified timeout period.
Printing task output...
Task: ContainersTask
Standard output:

Standard error:
usage: is_prime.py [-h] number
is_prime.py: error: argument number: invalid int value: 'o'


Sample end: 2018-11-10 11:33:10
Elapsed time: 0:03:14

Delete job? [Y/n] y
yDelete pool? [Y/n] y

Press ENTER to exit...

Remember at the end to eliminate resources so that they do not infringe costs.

References

batch-python-quickstart
Run container applications on Azure Batch

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.

Figlet as a Service

Todo comenzó cuando quise poner un ASCII Art a las máquinas para el fichero /etc/motd. Para hacerlo quería usar un playbook de Ansible que me permitiera generar el código ASCII utilizando como entrada en nombre del host.

Me puse a investigar y a sopesar las opciones:

  1. Podía instalar figlet en cada máquina y generar el ASCII Art.
  2. Podía usar alguna API que proporcionara algún método para obtener los ASCII Art en remoto

La primera opción no me convence porque tengo que instalar un paquete que solo se va a usar una vez y la segunda menos porque las opciones que encontré eran de pago y algunas máquinas no tienen salida a Internet.

Así pensé utilizar una combinación de ambas y crearme mi propio servicio de figlet. No ha sido demasiado complicado crearme un par de contenedores docker que para ofrecer el servicio a la infraestructura.

Dockerizando Figlet

Lo primero que hemos hecho ha sido buscar en Docker Hub el contenedor para PHP que ejecute FPM. Delante de este contenedor colocamos otro contenedor NGINX que actúa como frontend de la aplicación.

Esto no es complicado, porque ya está hecho, el hub oficial de PHP tiene un tag para FPM. A este contenedor hay que instalarle el paquete de figlet. Con apt se hace. Se escribe este cambio en el Dockerfile y se construye la imagen.

El contenedor de NGINX hay que configurarlo para que haga de proxy al php-fpm

Escribimos el código para leer la URL de la que sacaremos el nombre del host. Tenemos en cuenta si viene vacía, o si es mayor a 32 caracteres, ya que la RFC considera que el tamaño máximo de nombre de host es de 32 caracteres. También para este caso se pasa un ancho de columna a figlet de 160 caracteres, para que no salga cortado, en cualquier caso es raro que un nombre de host sobrepase los 15 caracteres, al menos en las infraestructuras que yo administro.

Construida la imagen, se escribe el docker-compose.yaml para vincular el nginx con el php-fpm. Se establece la política de restart, se exponen los puertos y se habilitan los volumenes.

Publicado

Está publicado aquí

Fuentes

Y las fuentes en mi github por si alguien quiere investigar o montarlo en su infraestructura.

Publicar una Aplicación de DJango en un Contenedor Docker

Podemos estar tentados de pasar a producción el contenedor Docker con el runserver que proporciona el manager.py de DJango. Pero esto es un gravísimo error de seguridad. En la siguiente entrada explicaremos como crear el contenedor Docker con el módulo wsgi de Apache.

En la siguiente entrada veremos lo sencillo que es construir nuestra imagen para Docker de nuestra App de DJango con el Dockerfile que nos proporciona Graham Dumpleton.

Lo único que necesitamos es el código DJango de la aplicación (obviamente) y el fichero requirements.txt que debe estar en la raiz donde vamos a colocar nuestro Dockerfile.

Pongamos que nuestro requirements.txt tiene este aspecto:

numpy==1.6.2
Django==1.4.2
django-tastypie==0.9.14
pyes==0.19.1

Lo siguiente que necesitamos conocer es el dónde colocar los estáticos tanto la URL como la ubicación física en el directorio.

Lo típico es poner los estáticos bajo la URL /static/ y en el disco en el directorio static de nuestra aplicación. Debemos conocer estos datos para pasarlo al comando con el que arrancar la imagen.

Bien, como decíamos, partimos de la imagen siguiente

FROM grahamdumpleton/mod-wsgi-docker:python-2.7-onbuild

Usaremos onbuild porque tiene dos disparadores interesantes que nos simplifican muchísimo la vida. El primero copia el contenido del directorio actual a /app y el segundo ejecuta

pip install -r requirements.txt

Con el contenido de nuestro fichero de requirements.

La única línea que tenemos que añadir a nuestro Dockerfile sería la siguiente:

CMD ["--url-alias", "/static", "awesomeapp/static", "awesomeapp/wsgi.py"]

Y la construimos

# docker build -t awesomeapp-wsgi .

Ya podemos ponerla en producción sin riesgo.

# docker run -d -p 80:80 --name awesomeapp-pro awesomeapp-wsgi

Referencias:

  1. http://blog.dscpl.com.au/2014/12/hosting-python-wsgi-applications-using.html
  2. https://hub.docker.com/r/grahamdumpleton/mod-wsgi-docker/
  3. https://github.com/GrahamDumpleton/mod_wsgi-docker