# Virtualización a nivel de sistema operativo

La virtualización a nivel de sistema operativo, también llamada virtualización basada en contenedores, contenerización o contenedorización, es un método de virtualización en el que, sobre el núcleo del sistema operativo, se ejecuta una capa de virtualización que permite que existan múltiples instancias aisladas de espacios de usuario, en lugar de solo uno. Tales instancias, las cuales son llamadas contenedores, contenedores de software, jaulas o prisiones, pueden verse y sentirse como un servidor real desde el punto de vista de sus dueños y usuarios. Al software que permite el alojamiento de distintos contenedores se le llama motor de contenedores. Además de mecanismos de aislamiento, el kernel a menudo proporciona mecanismos de administración de recursos para limitar el impacto de las actividades de un contenedor sobre otros contenedores.
Con la virtualización basada en contenedores, no existe la sobrecarga asociada con tener a cada huésped ejecutando un sistema operativo completamente instalado. Este enfoque también puede mejorar el rendimiento porque hay un solo sistema operativo encargándose de los avisos de hardware. Una desventaja de la virtualización basada en contenedores, sin embargo, es que cada invitado debe utilizar el mismo sistema operativo que utiliza el host.

## Historia

Como origen de la virtualización a nivel de sistema operativo podemos señalar 1982 con la creación de chroot por parte de Bill Joy. A chroot se le pasan dos parámetros, uno que indica un directorio y el otro el comando a ejecutar. Lo que hace chroot es ejecutar el comando pasado como parámetro impidiendo que él y sus procesos hijos accedan a cualquier ruta fuera del directorio pasado también como parámetro. Se establece lo que se llama jaula chroot. Estas restricciones son relativamente fáciles de superar. Lo que se persigue es crear una zona con algo más de seguridad donde poder ejecutar un programa del cual se desconfía y que puede presentar un comportamiento peligroso para la integridad del sistema.
En los años 90 Poul-Henning Kamp introduce las llamadas jaulas FreeBSD, a veces simplemente llamadas jaulas BSD. Son el primer ejemplo de virtualización a nivel de sistema operativo. Su objetivo es solventar la limitación de chroot de que sólo limita la parte del sistema de ficheros a la que pueden acceder los procesos dentro de la jaula. El resto de recursos del sistema (conjunto de usuarios del sistema, los procesos en ejecución, el subsistema de red,...) están compartidos entre el sistema alojado y el servidor. Las jaulas BSD extienden este modelo virtualizando no solamente el acceso al sistema de ficheros, sino al conjunto de usuarios, al subsistema de red del kernel de FreeBSD y unas cuantas cosas más. Para ello elimina todos los privilegios de superusuario que podría afectar a objetos que no están enteramente dentro de la jaula. Además cada jaula puede tener su propio conjunto de usuarios e incluso su propio usuario root (limitado al entorno de la jaula no pudiendo realizar operaciones fuera del mismo). Es un sistema más avanzado que crea un entorno virtual prácticamente indistinguible de una máquina real (o máquina virtual real). Tienen como limitación, sin embargo, la obligación de ejecutar la misma versión del núcleo del sistema.
De manera prácticamente paralela a las jaulas FreeBSD, aparece Linux VServer un sistema de virtualización para plataformas Linux que también permite aislar los recursos (sistemas de archivos, direcciones de red, memoria,..). Este sistema fue añadido directamente al kernel de Linux en el año 2001. Posteriormente, también para Linux, se lanzó OpenVZ y su versión comercial Virtuozzo).
Dentro de la familia de sistemas operativos Solaris aparece una tecnología equivalente a las jaulas BSD llamada zonas Solaris. La principal diferencia con las jaulas BSD es la baja sobrecarga que le añaden al sistema operativo y el hecho de que se les puedan asignar recursos específicos.
En 2002 se introducen en el kernel de linux los namespaces. Estos namespaces permiten particionar los recursos del kernel de modo que un grupo de procesos vea un conjunto de recursos y otro conjunto de procesos vea otro conjunto diferente de recursos. La implementación de namespaces fue mejorándose y ampliándose en versiones sucesivas del kernel.
En el año 2006 Google lanza para plataformas Linux su herramienta Process Containers que fue diseñada para limitar y aislar los accesos a recursos de la máquina como CPU, memoria, I/O de disco, red, etc., por parte de un grupo de procesos. Este proyecto fue renombrado posteriormente al nombre Grupos de control o simplemente cgroups en 2007 y finalmente se fusionó con el kernel de Linux.
Unos años más tarde nace para plataformas Linux el sistema LinuX Containers (LXC). Este sistema, apoyándose sobre namespaces y cgroups que ofrece el kernel de Linux, consiguió la primera implementación estable de un gestor de contenedores sobre Linux. Sobre LXC se ha construido LXD, un administrador de contenedores del sistema que proporciona una mejor experiencia de usuario. LXD ofrece un servicio similar a una máquina virtual en el sentido de que ofrece al usuario un sistema operativo completo con interfaces de red y almacenamiento pero, en este caso, con ciertas restricciones de acceso.
Entre 2011 y 2013 surgen diversas tecnologías de gestión de contenedores como Warden, LMCTFY (siglas de Let Me Contain That For You) y Docker. Warden desarrolló el primer modelo cliente-servidor para administrar contenedores distribuidos en diferentes equipos. LMCTFY permitía que las aplicaciones propiamente dichas tuvieran capacidad para controlar el contenedor, administrando sus propios subcontenedores o contenedores hijos. En 2013 surge Docker, el cual permite gestionar el ciclo de vida de los contenedores de forma sencilla en comparación con las herramientas anteriores. Esto provoca que Docker, y por tanto los contenedores, se empiecen a usar de forma masiva.
La gran ventaja de Docker es que está orientado a empaquetar dentro de los contenedores aplicaciones aisladas entre sí, con todas las funcionalidades que necesitan para ser ejecutadas permitiendo integrarlas en los flujos de integración/distribución continua. De esta forma obtiene una cadena unificada desde el desarrollo de aplicaciones hasta su puesta en producción. Teniendo un modelo de único despliegue de las aplicaciones, consigue reducir los tiempos de configuración, los tiempos de despliegue,… y mejorando, en ese sentido, el tiempo de puesta en producción de las aplicaciones. De esta manera independientemente del entorno en el que ejecutemos el contenedor (local, desarrollo o entornos productivos) siempre nos vamos a asegurar que se ejecuta de la misma manera. Para ello usa instantáneas de un contenedor, a las que llama imágenes, que se ejecutan instanciándolas en lo que ya sería el contenedor. Podremos crear tantas instancias de una imagen como queramos y en los entornos que queramos. Típicamente se tienen contenedores mínimos efímeros, sin estado, que normalmente no se actualizan o reconfiguran, sino que simplemente se reemplazan por completo. Si queremos datos persistentes se montan rutas del sistema subyacente en los contenedores por ejemplo usando los llamados volúmenes.
Inicialmente Docker usó como entorno de ejecución por defecto LXC, sin embargo más tarde fue reemplazado por libcontainer. De esta forma consiguió poder ser usado con otras tecnologías de aislamiento distintas a LXC y poder acceder directamente a las APIs del kernel del sistema operativo y así poder reducir las dependencias de librerías y aumentar la eficiencia del sistema. Sobre libcontainer Docker creó runC, un motor de contenedores ligero de línea de comandos
Aunque Docker sigue siendo, con diferencia, la solución más usada, durante estos últimos años han surgido otras alternativas como:
* rkt (antes llamado Rocket). Creado por Container Linux para proporcionar más seguridad e interoperabilidad. No obligaba a ejecutar todo como root, no tenía demonios, estaba controlado por línea de comandos CLI, y tenía comodidades como verificación criptográfica y compatibilidad completa con imágenes de Docker. Docker fue ganando popularidad y rkt finalmente fue abandonado.
* Apache Mesos. Permite ejecutar frameworks sobre los cuales se ejecuta la aplicación y está especialmente orientado a entornos de Big Data
* Windows Server Container. Las instancias de los contenedores se ejecutan a la vez sobre el mismo núcleo del anfitrión.
* Hyper V Container Los contenedores no comparten el mismo núcleo sino que se aísla entre cada uno utilizando la tecnología de virtualización Hyper-V.
En 2015 se funda la Open Container Initiative (OCI), una asociación de instituciones y empresas asociadas para diseñar estándares abiertos sobre virtualización a nivel de sistema operativo para, de esta forma, asegurar que las plataformas de contenedores no estén vinculadas a ninguna empresa o proyecto concreto.
En 2016 el más importante orquestador de contenedores Kubernetes propuso Container Runtime Interface como la interfaz (API) para los motores de ejecución de contenedores, para de esta manera proponer una forma de conectarse a otros motores de ejecución distintos de Docker. De esta forma podía integrarse con múltiples motores de ejecución de manera transparente. Poco a poco empezaron a salir motores de ejecución que cumplían la especificación de forma nativa y que a la vez soportaban las imágenes Docker. Por ejemplo cri-o (por debajo utiliza típicamente runC o crun como motor de contenedores de bajo nivel, ambas implementaciones OCI siendo crun más ligera) y cri-containerd (plugin añadido a containerd, el motor de ejecución de alto nivel de Docker y que por debajo utiliza típicamente runC). Sin embargo, Docker no cumplía CRI (aunque desde la versión utiliza ContainerD de forma interna) y por eso se desarrolló el componente docker-shim que hacía de interlocutor entre las dos partes para que sea posible utilizar Docker dentro de Kubernetes. En diciembre de 2020 Kubernetes anunció que deprecaba el soporte a Docker en la próxima versión, y que en futuras versiones solo estarían disponibles los motores de ejecución de contenedores que cumplieran CRI de manera nativa.

## Usos

Escenarios típicos de uso de la virtualización a nivel de sistema operativo son:
* En ambientes de alojamiento virtual, permiten distribuir recursos de hardware finitos de forma segura entre un número grande de usuarios mutuamente desconfiados.
* Administradores de sistema lo usan para ahorrar hardware, moviendo servicios que se encuentran en servidores distintos hacia un mismo servidor.
* Permiten la separación de varias aplicaciones en contenedores distintos para mejorar la seguridad, independencia de hardware y brindar mecanismos de administración de recurso adicionales. La mejora de seguridad proporcionada, aun así, no es de ninguna forma infalible.
* Las implementaciones de virtualización a nivel de sistema operativo capaces de hacer migraciones en vivo pueden ser utilizadas para realizar balanceo de carga dinámico de contenedores entre nodos en un grupo.

## Características
### Sobrecarga

La virtualización a nivel de sistema operativo normalmente impone poca o ninguna sobrecarga, porque los programas en particiones virtuales utilizan la interfaz de llamada de sistema normal del sistema operativo y no necesitan de emulación o ser ejecutados en una máquina virtual intermedia, como es el caso con virtualizadores a sistema completo (como VMware ESXi, QEMU o Hyper-V) y paravirtualizadores (como Xen o UML). Esta forma de virtualización además no requiere soporte en hardware para actuar eficientemente.

### Flexibilidad

La virtualización a nivel de sistema operativo no es tan flexible como otros enfoques de virtualización porque no puede hospedar un sistema operativo diferente del anfitrión o un kernel distinto. Por ejemplo, con Linux, no hay problemas con las distribuciones diferentes, pero otros sistemas operativos como Windows no puede ser virtualizados.
Solaris vence parcialmente la limitación descrita anteriormente con su característica de zonas marcadas, la cual proporciona la capacidad de ejecutar un entorno dentro de un contenedor que emula un Solaris versión 8 o 9 en un Solaris 10 anfitrión. Las zonas marcadas de Linux también están disponibles en sistemas Solaris basados en x86, proporcionando un espacio de usuario de Linux completo y soporte para la ejecución de aplicaciones de Linux; además, Solaris proporciona las herramientas necesarias para instalar otras distribuciones de Linux como Red Hat Enterprise Linux 3.x o CentOS 3.x dentro de zonas marcadas de Linux. Sin embargo, en 2010 las zonas marcadas de Linux fueron eliminadas de Solaris; en 2014 eran reintroducidas en Illumos, la rama de código abierto de Solaris, brindando soporte a los kernels de Linux de 32-bits.

### Almacenamiento

Algunas implementaciones de virtualización a nivel de sistema operativo proporcionan mecanismos copy-on-write a nivel de archivos. (En la mayoría de los casos, un sistema de ficheros estándar es compartido entre particiones, y aquellas particiones que cambian los archivos automáticamente crean sus propias copias). Con este sistema es más sencillo realizar copias de seguridad, además, hace un uso más eficiente del espacio en disco y resulta más sencillo de guardar en caché que los esquemas copy-on-write a nivel de bloque, comunes en virtualizadores a sistema completo. Los virtualizadores a sistema completo, sin embargo, pueden trabajar con sistemas de archivos no nativos y crear y restaurar copias del estado actual completo del sistema.

## Tipos

Podemos clasificar los sistemas de virtualización de sistemas operativos según el tipo de servicios que ofrecen en:
* Contenedores de infraestructura o sistema de contenedores. Ofrecen un servicio que permite ejecutar múltiples instancias de sistemas operativos de manera aislada. Ofrece todo lo necesario para que el sistema “contenido” pueda trabajar, tales como CPU, network, I/O. Es similar a una máquina virtual pero más rápido y ligero. Ejemplos de estos sistemas son LXC y LXD.
* Contenedores de procesos o Contenedores de aplicaciones. Ofrecen un sistema para empaquetar aplicaciones y todas sus dependencias y, por tanto, son muy útiles para el desarrollo y distribución de aplicaciones. Por ejemplo, en integración continua es habitual desarrollar con contenedores para eliminar las diferencias de entorno entre producción y desarrollo y facilitar la migración del entorno. Este tipo de contenedores son muy usados en la computación en la nube para distribuir las aplicaciones. Ejemplos de estos sistemas son Docker y systemd-nspawn. Típicamente se tienen contenedores mínimos efímeros, sin estado, que normalmente no se actualizan o reconfiguran, sino que simplemente se reemplazan por completo.
* Contenedores sandbox. Enfocados en proveer aislamiento mediante un entorno que permita ejecutar contenedores en un espacio escapsulado donde tiene acceso restringido a recursos del sistema operativo o datos del usuario (sandbox). Ejemplos de estos sistemas son Firejail, nsroot, nsjail, FreeBSD jail, sandboxie y Bubblewrap.
El que un sistema esté más enfocado en proveer un tipo de servicio no quiere decir que no pueda proveer el otro. Por ejemplo Docker es un contenedor de aplicaciones pero permite proporcionar espacio aislado. Sin embargo al ser un software bastante grande y con muchas funciones, expande la superficie de ataque innecesariamente y por tanto, no es la mejor forma para proporcionar espacio aislado.
Además se pueden combinar, por ejemplo se puede usar LXD para proporcionar sistemas Linux completos a sus usuarios que luego pueden instalar Docker dentro de su contenedor LXD para ejecutar el software que desean.

## Comparación con la virtualización de plataforma

Comparado con la virtualización de plataforma, la virtualización a nivel de sistema operativo:
* Es mucho más ligera. Puede llegar a tener un rendimiento en la ejecución muy próximo al nativo reduciendo, por ejemplo, el consumo de memoria por servidor virtual u optimizando su entrada y salida debido a que un solo kernel controla el acceso a los dispositivos y recursos. Esta ligereza permite un tiempo de arranque más reducido, y que la misma máquina permita admitir más contenedores que máquinas virtuales
* No requiere un hipervisor para funcionar
* No requiere ningún mecanismo hardware.
* Permite un mayor control desde fuera (desde el anfitrión) del que se pueda tener usando máquinas virtuales.
* El soporte de contenedores implica que haya que realizar diversas modificaciones en el kernel del sistema operativo anfitrión, sobre todo para hacer creer a los contenedores que son ejecutadas en un entorno exclusivo.
* Al compartir el mismo núcleo con el servidor físico anfitrión:
** Un fallo en el kernel puede provocar la caída de la totalidad de los contenedores alojados (único punto de fallo).
** Por lo general, los contenedores no pueden correr sistemas operativos que difieran del instalado en el sistema anfitrión, aunque en algunos casos es posible que ejecuten diferentes versiones de la misma distribución o incluso distintas distribuciones Linux. Por ejemplo, FreeBSD jails permite ejecutar diferentes versiones de FreeBSD sobre un único kernel FreeBSD, por lo tanto permitiendo tener diferentes versiones de aplicaciones, procesos, librerías, etc. Otro ejemplo: Linux V-Server, FreeVPS y OpenVZ pueden ejecutar diferentes distribuciones Linux en los servidores virtuales, aunque siempre compartiendo el mismo kernel. Con docker en Windows se pueden ejecutar contenedores Linux gracias a la capa de compatibilidad WSL que permite ejecutar ejecutables de Linux nativamente en Windows.
** Las librerías, herramientas, utilidades,... que ejecuten los servidores virtuales deben estar compilados para el mismo juego de instrucciones y hardware que utiliza el sistema operativo anfitrión.
* Mayor portabilidad Las aplicaciones que se ejecutan en contenedores se pueden poner en marcha fácilmente en sistemas operativos y plataformas de hardware diferentes. Funcionamiento más constante • Los equipos de DevOps saben que las aplicaciones en contenedores van a ejecutarse igual, independientemente de dónde se pongan en marcha. • Mayor eficiencia Los contenedores permiten poner en marcha, aplicar parches o escalar las aplicaciones con mayor rapidez. • Mejor desarrollo de aplicaciones Los contenedores respaldan los esfuerzos ágiles y de DevOps para acelerar los ciclos de desarrollo, prueba y producción.

## Orquestadores de contenedores

Las herramientas que realizan orquestación de contenedores, llamados Orquestadores de contenedores son herramientas que dirigen el comportamiento de los contenedores pudiendo automatizar el despliegue, la gestión y el escalado de las aplicaciones basadas en contenedores. Estas herramientas son necesarias en entornos en los que tenemos que manejar un sistema con muchos contenedores, que dan distintos servicios (base de datos, servidor web, métricas, la propia aplicación, ...) y desplegados sobre distintos servidores. Estos contenedores atienden una demanda determinada que tiene que ser satisfecha por unos recursos los cuales se tienen que escalar, actualizar, etc. sin repercutir en el usuario de la aplicación. Por tanto hay que controlar y dirigir la creación de contenedores, verificar su correcta ejecución, gestionar los errores,...
Ejemplos de orquestadores de contenedores son docker-compose, Docker Swarm, Rancher y Kubernetes. Algunos gestores de contenedores como Apache Mesos aportan sus propios mecanismos de orquestación y no necesitan un herramienta externa para realizar esa tarea.

## Contenedores y sistemas distribuidos

Un Sistema distribuido es aquel en el que los componentes, localizados en la red se comunican y coordinan sus acciones mediante el envío de mensajes. Los Sistemas Distribuidos permiten que los recursos de la red no se encuentren centralizados en una sola máquina, pudiendo estar en varias, e incluso en lugares diferentes. Como se mencionó anteriormente, los contenedores son una forma de virtualización del sistema operativo. Estos pueden llegar a utilizarse para ejecutar cualquier cosa, desde un micro servicio hasta una aplicación de mayor nivel. La manera más sencilla de relacionarlos es tomando en cuenta que si el objetivo de los sistemas distribuidos es no centralizar los recursos en una sola máquina, los contenedores hacen más sencilla esta distribución, al requerir menos recursos que un sistema operativo completo, facilitar una mayor portabilidad al usuario, ya que como se mencionó, las aplicaciones que se ejecutan en estos se pueden poner en operación de manera más sencilla y rápida que en un sistema operativo. Otro punto fuerte es que se puede poner a operar en plataformas de hardware diferentes, facilitando aún más lo que se busca con los sistemas distribuidos.

## ¿Cuál es la relación que docker y kubernetes tienen con los contenedores?

Docker es un popular entorno en tiempo de ejecución que se usa para crear y construir software dentro de contenedores. Usa imágenes de Docker (instantáneas de copia en escritura) para poner en marcha aplicaciones o software en contenedores en varios entornos, desde el desarrollo hasta las pruebas y la producción. Docker se basa en estándares abiertos y funciona en la mayoría de los entornos operativos más comunes, incluidos Linux, Microsoft Windows y otras infraestructuras locales o basadas en la nube.
Sin embargo, las aplicaciones en contenedores pueden ser complicadas. Durante la producción, muchas pueden requerir cientos o miles de contenedores independientes. Es en este punto donde los entornos en tiempo de ejecución de contenedores, como Docker, se benefician del uso de otras herramientas para orquestar o gestionar todos los contenedores en funcionamiento.
Una de las herramientas más populares para este fin es Kubernetes, un orquestador de contenedores que reconoce varios entornos en tiempo de ejecución de contenedores, incluido Docker.
Kubernetes orquesta el funcionamiento de varios contenedores juntos de forma armónica. Gestiona áreas como el uso de recursos de infraestructura subyacentes para aplicaciones en contenedores (por ejemplo, la cantidad de recursos de computación, red y almacenamiento necesarios). Las herramientas de orquestación como Kubernetes facilitan la automatización y el escalado de cargas de trabajo basadas en contenedores para entornos de producción activos.

## Implementaciones

A continuación se muestra tabla con algunas implementaciones de motores de contenedores:
| Mecanismo | Sistema operativo | Licencia | Disponible desde o entre | Características | Características | Características | Características | Características | Características | Características | Características | Características | Características |
| Mecanismo | Sistema operativo | Licencia | Disponible desde o entre | Aislamiento de sistema de ficheros | Copy-on-write | Cuotas de disco | Límite de entrada/salida | Límites de memoria | Cuotas de CPU | Aislamiento de red | Virtualización anidada | Puntos de control de particiones y migraciones en vivo | Aislamiento de privilegios de administración |
| chroot | La mayoría de los sistemas operativos estilo Unix | Cambia de acuerdo al sistema operativo | 1982 | Parcial | No | No | No | No | No | No | Sí | No | No |
| Docker | Linux | Licencia Apache 2.0 | 2013 | Sí | Sí | No directamente | Sí (desde la versión 1.10) | Sí | Sí | Sí | Sí | No | Sí (desde la versión 1.10) |
| Linux-VServer (contexto de seguridad) | Linux, Windows Server 2016 | GNU GPLv2 | 2001 | Sí | Sí | Sí | Sí | Sí | Sí | Parcial | ? | No | Partial |
| lmctfy | Linux | Licencia Apache 2.0 | 2013 | Sí | Sí | Sí | Sí | Sí | Sí | Parcial | ? | No | Partial |
| LXC | Linux | GNU GPLv2 | 2008 | Sí | Sí | Parcial | Parcial | Sí | Sí | Sí | Sí | No | Sí |
| LXD | Linux | Licencia Apache 2.0 | 2015 | Sí | Sí | Parcial (ver LXC) | Parcial (ver LXC) | Sí | Sí | Sí | Sí | Parcial | Sí |
| OpenVZ | Linux | GNU GPLv2 | 2005 | Sí | Sí (ZFS) | Sí | Sí | Sí | Sí | Sí | Parcial | Sí | Yes |
| Virtuozzo | Linux, Windows | Propietaria | 2000 | Sí | Sí | Sí | Sí | Sí | Sí | Sí | Parcial | Sí | Sí |
| Contenedores Solaris (Zones) | illumos (OpenSolaris), Solaris | CDDL, Propietaria | 2004 | Sí | Sí (ZFS) | Sí | Parcial | Sí | Sí | Sí | Parcial | Parcial | Yes |
| Prisión FreeBSD | FreeBSD | Licencia BSD | 2000 | Sí | Sí (ZFS) | Sí | Sí | Sí | Sí | Sí | Sí | Parcial | Sí |
| sysjail | OpenBSD, NetBSD | Licencia BSD | 2006–2009 | Sí | No | No | No | No | No | Sí | No | No | ? |
| WPARs | AIX | Propietaria | 2007 | Sí | No | Sí | Sí | Sí | Sí | Sí | No | Sí | ? |
| HP-UX Containers (SRP) | HPUX | Propietaria | 2007 | Sí | No | Parcial | Sí | Sí | Sí | Sí | ? | Sí | ? |
| Cuentas virtuales de iCore | Windows XP | Propietaria: Freeware | 2008 | Sí | No | Sí | No | No | No | No | ? | No | ? |
| Sandboxie | Windows | Propietaria: Freeware | 2004 | Sí | Sí | Parcial | No | No | No | Parcial | No | No | Sí |
| Spoon | Windows | Propietaria | 2012 | Sí | Sí | No | No | No | No | Sí | No | No | Sí |
| systemd-nspawn | Linux | GNU LGPLv2.1+ | 2010 | Sí | Sí | Sí | Sí | Sí | Sí | Sí | ? | ? | Sí |
| VMware ThinApp | Windows | Propietaria | 2008 | Sí | Sí | No | No | No | No | Sí | No | No | Sí |