Introduction
Depuis plusieurs années maintenant, la conteneurisation est devenue un standard pour packager de manière uniforme ses applications. Environnements d’exécution isolés, portabilité et simplicité sont, entre autres, autant d’avantages que permettent les conteneurs. Cependant, une fois son application packagée, déployer son image de conteneur et assurer son « run » doit se faire à l’aide de technologies adaptées.
Sur Google Cloud Platform, plusieurs solutions peuvent être envisagées :
- Google Kubernetes Engine (GKE) permet d’avoir toutes les fonctionnalités de Kubernetes en service managé. Cependant, GKE nécessite toujours de maintenir un cluster de VMs (Virtual Machines). Aussi, l’utilisation de Kubernetes, même managé, représente une certaine complexité
- Cloud Run permet de déployer un container en mode serverless. Mais son usage est plus orienté vers le déploiement de services web stateless
- Enfin, Container-Optimized OS, le sujet de cet article, est un OS (Operating System) optimisé pour les applications conteneurisées. Couplé à Compute Engine, il va nous permettre de déployer simplement des conteneurs sans les contraintes évoquées de GKE et de Cloud Run
Conteneurs vs Compute Engine Images?
Lorsque l’on souhaite déployer une application facilement sur Compute Engine, on peut utiliser le système d’images. Grâce à des outils comme Packer, il est possible de créer une image réutilisable, qui va servir ensuite de base aux VMs. Néanmoins, ce système d’images possède des inconvénients, notamment la difficulté de tester son image localement ou encore le fait que ces images ne soient pas portables.
Prenons l’exemple d’une application classique (serveur web). Pour déployer cette application, il existe au moins 2 solutions pour créer son image :
- installer les dépendances systèmes de l’application directement sur la VM, mettre en place un service (avec systemd par exemple) qui va démarrer l’application au démarrage de l’instance
- packager son application dans un conteneur (pas besoin d’installer les dépendances systèmes directement sur la VM), installer Docker, et mettre en place un service qui va démarrer l’application automatiquement
La 1ère solution n’est pas portable (dépendances sur la VM), tandis que la 2ème solution nécessite un setup dont on souhaiterait bien se passer (installation dans le Docker). De plus, dans les 2 cas, il est nécessaire de gérer soi-même le démarrage de son application.
Pour éviter toutes ces contraintes, il est possible d’utiliser une image de base appropriée : Container-Optimized OS, où Docker est déjà installé, et où le lancement du conteneur est automatisé au démarrage de l’instance.
Container-Optimized OS
Container-Optimized OS (COS dans la suite de l’article) est l’OS principal utilisé par GCP pour faire tourner des conteneurs. Basé sur Chromium OS et successeur de container-vm, il est utilisé en interne par Google pour des produits comme GKE ou Cloud SQL : pas de soucis donc sur sa stabilité! Mais Google offre aussi la possibilité de l’utiliser pour nos propres VMs sur Compute Engine. L’idée principale derrière COS est d’avoir un OS minimal, avec le strict nécessaire pour faire tourner des conteneurs, rien de plus. Sur ce point, il est à rapprocher de CoreOS. Dans cet OS est donc inclus par défaut :
docker
cloud-init
(pour le provisioning)kubectl
. Bien que cela n’ait pas d’intérêt dans notre cas, cela est dû au fait que COS est utilisé par défaut par GKE pour les nodes
Par abus de langage, COS désigne un OS mais aussi une image au sens « Compute Engine Image » ‘voir section précédente). Comme pour toutes les images, il existe différentes versions disponibles :
$ gcloud compute images list --filter="selfLink=cos-cloud" NAME PROJECT FAMILY DEPRECATED STATUS cos-69-10895-385-0 cos-cloud cos-69-lts READY cos-73-11647-348-0 cos-cloud cos-73-lts READY cos-77-12371-114-0 cos-cloud cos-77-lts READY cos-beta-78-12499-46-0 cos-cloud cos-beta READY cos-dev-79-12607-7-0 cos-cloud cos-dev READY cos-stable-77-12371-114-0 cos-cloud cos-stable READY
Pour des environnements de production, choisir l’image de la famille cos-stable
est bien sûr recommandé.
Création d’une instance
Comme n’importe quelle image Compute Engine, il est possible de démarrer une VM basée sur COS, via la console ou la ligne de commande (gcloud
). Des solutions s’offrent aussi à nous pour automatiser cette création, comme nous le verrons dans la suite.
Démarrage d’une instance COS
Via la Console, il est possible de choisir l’image COS au choix du disque de démarrage :
Cela est équivalent à la commande gcloud
suivante :
gcloud compute instances create instance-cos \ --zone=europe-west1-b \ --machine-type=n1-standard-1 \ --image-project=cos-cloud \ --image=cos-stable-77-12371-114-0
Une fois la VM créée, il suffit alors :
- de se connecter en SSH pour démarrer des conteneurs (via la CLI
docker
) - ou bien de la provisionner à distance avec des outils comme Ansible
pour lancer un ou plusieurs conteneurs (son application) au sein de l’instance.
La VM créée ici est une VM « classique », dans le sens où le démarrage des conteneurs, leur extinction, ou leur redémarrage doit se faire à l’aide d’outils externes.
Nous ne rentrerons pas plus dans le détail sur ce cas d’utilisation car les vrais apports de l’utilisation de Container-Optimized OS résident dans le fait de pouvoir déployer simultanément une VM et son conteneur (application).
Déploiement automatique d’une image de conteneur
Une des particularités de COS est donc la possibilité de déployer automatiquement au démarrage de l’instance une image de conteneur. Pour cela, il suffit dans la Console de cocher la case correspondante :
et de renseigner les différents paramètres : image, restart policy, surcharge de l’ENTRYPOINT (command), arguments, variables d’environnement, ou bien points de montage.
La commande gcloud
équivalente est la suivante (exemple de déploiement d’une image nginx:1.17.5-alpine
) :
gcloud compute instances create-with-container instance-container \ --zone=europe-west1-b \ --machine-type=n1-standard-1 \ --image-project=cos-cloud \ --image=cos-stable-77-12371-114-0 \ --container-image=nginx:1.17.5-alpine \ --container-restart-policy=always
Source de l’image
Lorsque l’on choisit une image de conteneur, 4 cas sont possibles.
L’image est publique
C’est le cas dans notre exemple ci-dessus. La VM a alors juste besoin d’un accès à Internet pour récupérer l’image (docker pull
).
L’image est privée mais se trouve dans le Container Registry du même projet GCP
Par défaut, le service account (Compute Engine default service account
) est capable de récupérer l’image car il dispose du rôle editor
. Si un service account différent est utilisé, alors ce service account doit avoir le rôle roles/storage.objectViewer
sur le projet, ou du moins sur le bucket contenant les images (sous Container Registry se trouve en réalité un bucket, mais nous ne rentrerons pas plus dans le détail ici).
L’image est privée et se trouve dans le Container Registry d’un autre projet GCP
Le service account doit avoir le rôle roles/storage.objectViewer
sur le projet (ou bucket) où se trouve l’image.
L’image est privée et ne se trouve pas dans Container Registry
Il faut d’abord que la VM s’authentifie au registry privé au démarrage (docker login
avant le docker pull
). Cela peut être fait à travers cloud-init
ou bien un script de démarrage (ces concepts sont abordés plus bas).
Autres options
Les autres options (commande, arguments, variables d’environnement) peuvent être passées aussi à la commande gcloud
:
--container-command=/other/entrypoint \ --container-arg=first_arg \ --container-arg=second_arg \ --container-env=VAR1=something
Nous verrons dans la suite comment faire des points de montage.
Une fois l’instance démarrée, et après quelques secondes (démarrage complet de l’instance, téléchargement des images, démarrage des conteneurs), le résultat est le suivant :
Le conteneur spécifié en argument a bien été démarré automatiquement (klt-instance-container-jnyw
) après le démarrage de l’instance. Notons aussi la présence d’un autre conteneur (stackdriver-logging-agent
), sur lequel nous reviendrons dans la partie logging.
Par défaut, les conteneurs démarrés utilisent le host network (équivalent à docker run ... --net=host
). Il n’est donc pas nécessaire de publier des ports pour accéder au conteneur, accessible directement sur les ports de la VM :
Automatisation
Créer une instance Compute Engine avec un conteneur est plutôt simple via la Console ou la commande gcloud
. Au niveau des APIs Google Cloud, il n’existe pas à proprement parler de ressource « VM avec Container-Optimized OS ». Comme indiqué plus tôt, il s’agit uniquement d’une VM Compute Engine, configurée avec une image de la famille cos-cloud
et la métadonnée gce-container-declaration
renseignée avec la configuration du conteneur dans un format précis.
Par exemple, regardons la configuration d’une instance COS nouvellement créée :
metadata: fingerprint: Z713-OPoCSY= items: - key: gce-container-declaration value: |- spec: containers: - name: instance-1 image: 'nginx:1.17.5-alpine' env: - name: env value: dev stdin: false tty: false restartPolicy: Always [3/121] # This container declaration format is not public API and may change without notice. Please # use gcloud command-line tool or Google Cloud Console to run Containers on Google Compute Engine. - key: google-logging-enabled value: 'true'
Nous remarquons que la syntaxe de gce-container-declaration
est au même format qu’un manifeste Kubernetes. Celui-ci va être interprété par konlet, un petit outil en Go présent sur COS sous forme d’un service, qui va se charger de démarrer le container au démarrage du système.
Attention, le format de gce-container-declaration
n’est pas public, et n’a vocation qu’à être utilisé par konlet. Cependant, pour automatiser, il est nécessaire de passer par ce chemin. Automatiser la création d’une instance avec COS revient donc à écrire cette métadonnée au bon format.
Création d’une instance
Avec Terraform, Google propose un module (terraform-google-container-vm) simplifiant cela.
J’ajoute mon module avec la configuration de mon container :
module "nginx_container_configuration" { source = "github.com/terraform-google-modules/terraform-google-container-vm" container = { image = "nginx:1.17.5-alpine" env = [ { name = "env" value = "dev" } ], } }
Ensuite, j’utilise mon module afin de remplir l’image utilisée, la méta-donnée gce-container-declaration
, ainsi que les labels ajoutés par défaut lorsque je crée ma VM avec COS :
resource "google_compute_instance" "nginx_cos" { machine_type = "n1-standard-2" name = "nginx-cos" zone = "europe-west1-b" boot_disk { initialize_params { image = module.nginx_container_configuration.source_image type = "pd-standard" size = "50" } } network_interface { network = "default" access_config { // Ephemeral IP } } metadata = { gce-container-declaration = module.nginx_container_configuration.metadata_value google-logging-enabled = "true" } labels = { container-vm = module.nginx_container_configuration.vm_container_label } }
Notons qu’il est aussi possible d’utiliser Deployment Manager à la place de Terraform.
Provisioning
Dans certains cas, il peut être nécessaire de configurer des choses au niveau de l’hôte au démarrage. Deux solutions sont possibles :
- Utiliser le mécanisme de script de démarrage de Compute Engine :
resource "google_compute_instance" "single-node-cluster" { ... metadata_startup_script = "mkdir /var/www" }
- Utiliser
cloud-init
. Présent par défaut sur COS,cloud-init
est un système pour initialiser des VM, indépendamment du système et de la plateforme Cloud. La configuration passe par un fichier de configuration avec un format dédié, et cette configuration est passée à la VM via la métadonnéeuser-data
.cloud-init
offre certaines fonctionnalités plus avancées, notamment la possibilité de lancer une commande seulement au premier démarrage de la VM avec la commandecloud-init-per
. Vous trouverez plus de détails surcloud-init
dans leur documentation. Attention, la version decloud-init
packagée avec COS n’est pas forcément la dernière (actuellement la 0.7.6). L’exemple précédent se présente de cette manière aveccloud-init
:
#cloud-config runcmd: - cloud-init-per instance folder_data mkdir /var/data
Logging
Le logging est une partie très importante dans le run des applications. Sans logging, il est très difficile de savoir ce qu’il se passe au sein de son application. Pour les applications modernes – et encore plus pour les applications conteneurisées – il est de bon ton de ne pas s’encombrer de la gestion du logging au sein de l’application, mais plutôt d’écrire sur les sorties standard et d’erreur (stdout
/stderr
), et de se reposer sur une application de traitement des logs (séparer la génération des logs du traitement). C’est le cas par défaut lorsque l’on déploie une image de conteneur sur une VM avec COS. Lorsque l’application conteneurisée va écrire des messages sur stdout
/stderr
, ces messages seront automatiquement redirigés vers Stackdriver. C’est un conteneur dédié (stackdriver-logging-agent
, voir l’exemple ci-dessus) qui est en charge de cette redirection. Dans ce conteneur se trouve en réalité un fluentd légèrement modifié pour les besoins de COS.
Par défaut, un metadata google-logging-enabled=true est ajoutée à la VM. C’est cette metadata qui permet d’indiquer à Konlet, l’outil qui gère le déploiement des conteneurs, de démarrer un conteneur sidecar pour le forward de logs. Nous reviendrons plus en détail dans la suite (Internals).
Ce mécanisme ne donne pas la possibilité de faire un logging avancé (notamment au niveau de la sévérité des logs), mais cela est souvent suffisant.
Après écriture dans Stackdriver, un log ressemble à peu près à :
Le contenu du message est renseigné dans le champ jsonPayload.message de l’entrée Stackdriver.
Toolbox
Il n’existe pas de package manager par défaut (comme aptitude
sur Debian, yum
sur CentOS, apk
sur Alpine, …). Pour limiter la surface d’attaque (voir partie sécurité), Il n’est pas possible d’installer des packages tiers mais seulement de passer par des conteneurs. Néanmoins, Container-Optimized OS met à disposition une toolbox (la même que CoreOS). Il s’agit en réalité d’un conteneur (gcr.io/google-containers/toolbox
) lancé via le script /usr/bin/toolbox
et démarré avec systemd-spawn
, dans lequel vous êtes root
, offrant ainsi la possibilité d’installer n’importe quel outil à l’intérieur de cette toolbox. La configuration de la toolbox est décrite dans le fichier /etc/default/toolbox
.
Par défaut dans cette toolbox se trouvent installées les commandes du Google Cloud SDK (gcloud
, gsutil
, …) pour vous permettre de vous interfacer rapidement avec GCP. Un point de montage (défini par la variable TOOLBOX_BIND
) est réalisé : par défaut le filesystem /
du conteneur est monté sur le dossier /media/root
de la VM hôte. Tous les disques supplémentaires montés au niveau de la VM sont aussi présents dans la toolbox au niveau de /mnt/disks
.
Cette toolbox est globalement utilisée pour débugger (installer htop
par exemple, ou transférer des fichiers grâce à gsutil
).
Stockage
Dans certains cas, il peut être nécessaire de stocker des données persistantes. À l’intérieur d’un conteneur, si je veux que mes données survivent lorsque celui ci s’arrête, je dois utiliser un volume. Cela tombe bien, les volumes sont supportés soit directement dans la Console, soit par le module Terraform ou bien encore en ligne de commande en ajoutant les paramètres suivants :
--container-mount-host-path=mount-path=/data,host-path=/var/data,mode=rw
Cependant, l’emplacement sur le hôte doit se trouver soit dans /var
soit dans /home
. En effet, les autres emplacements sont soit en lecture seule, soit tout simplement pas fait pour cela. Pour plus de détails, un tableau dans la documentation de COS décrit très bien les différents points de montage sur une image COS et leurs rôles.
Une autre solution peut être aussi d’utiliser un disque supplémentaire dédié pour le stockage de l’état. Utiliser un disque à part permet d’avoir un disque moins lié au cycle de vie de l’instance, attachable à une autre instance. Pour rappel, par défaut le disque de démarrage de la VM est supprimé automatiquement avec la VM (comportement désactivable), et bien sûr, ce n’est pas le cas pour les disques supplémentaires.
Monter au démarrage un disque attaché à l’instance peut être fait en quelques lignes avec cloud-init :
#cloud-config bootcmd: - fsck.ext4 -tvy /dev/[DEVICE_ID] - mkdir -p /mnt/disks/[MNT_DIR] - mount -t ext4 -O ... /dev/[DEVICE_ID] /mnt/disks/[MNT_DIR]
Droits
Par défaut, l’application au sein du conteneur a les mêmes droits niveau IAM (Identity Access Management) que la VM. Le service account utilisé par la VM est propagé dans le conteneur, comme le montre l’exemple suivant (la VM a été démarrée avec le service account special-sa-for-my-instance@PROJECT.iam.gserviceaccount.com
) :
Cela simplifie beaucoup l’authentification aux services Google Cloud.
Sécurité
Étant donné que les images Container Optimized OS sont utilisées par les services managés sur Google Cloud Platform, elles sont construites de manière à garantir la sécurité :
- basée sur Chrome OS
- seuls les packages nécessaires sont installés pour réduire la surface d’attaque
- root file system monté en lecture seule
- kernel avec fonctionnalité de sécurité renforcée activée
- configuration par défaut du système axé sécurité
- mises à jour automatiques, bien que nécessite de redémarrer l’instance pour être pris en compte
Les accès réseaux (externes ou internes) sont régis par les règles de firewall du VPC qu’utilise la VM. Par défaut, seule la connection sur le port SSH est autorisée, comme pour n’importe quelle VM classique. Il est possible d’aller encore plus loin niveau sécurité en utilisant AppArmor.
Conclusion
Container-Optimized OS peut-être utilisé comme un moyen simple de déployer son application conteneurisée sans sortir immédiatement l’artillerie lourde Kubernetes. Malgré sa simplicité apparente, nous avons vu qu’il était possible d’aller très loin en terme de configuration et de tuning. Couplé avec les services fournis avec Compute Engine (instance group, load balancer, health checks), on peut bénéficier de certaines fonctionnalités d’un orchestrateur : auto-réparation, auto-scaling, et rolling update.
Il est même possible de lancer plusieurs conteneurs en utilisant docker-compose, cependant nous ne recommandons pas forcément cette solution car basée sur Docker-in-Docker (ou plus précisément Docker-compose-in-Docker). ll peut parfois être pratique d’utiliser cette solution, mais une décomposition avec plusieurs VM COS (un conteneur par VM) est aussi envisageable.
Quoi qu’il en soit, à partir du moment où déployer avec COS devient trop complexe, ou bien que le besoin d’orchestrer de plus en plus d’applications conteneurisées apparaît, peut-être est-ce le moment d’envisager GKE?