Il y a 2 ans -
Temps de lecture 20 minutes
Terraform sur Google Cloud Platform
On ne remet plus en question aujourd’hui la nécessité de déployer son infrastructure de manière automatisée.
Sur Google Cloud Platform plusieurs solutions sont possibles :
- Le client en ligne de commande gcloud, mais déployer toute son infrastructure en shell n’est clairement pas une solution
- Deployment Manager, le service fourni sur GCP pour faire de l’infra as code
- Terraform, qui est une référence aujourd’hui en terme de déploiement de ressource cloud, mais est-ce un choix judicieux ?
- D’autres outils comme Pulumi, qui sont des choix intéressants mais pas abordés dans cet article
Vous avez ouvert cet article, vous savez donc que l’objectif est de parler de Terraform, je vous propose donc ce petit guide pour bien démarrer :
- Terraform ou Deployment manager ?
- Terraform 101
- Configuration de l’authentification
- Gestion du state Terraform
- Ressources et Organisation
- Gestion des projets et folders Google Cloud
- Gestion des permissions IAM
- Conclusion
Les exemples qui vont illustrer les différents chapitres sont disponibles sur https://github.com/ibeauvais/terraform-getting-started
Terraform ou Deployment manager ?
Évidemment lorsqu’on souhaite avoir une approche infra as code sur Google Cloud Platform, la question suivante se pose :
Pourquoi ne pas utiliser l’outil natif fourni ? N’est il pas plus à jour et mieux intégré à l’écosystème GCP ?
Sans entrer dans les considérations de syntaxe ou de comparaison de fonctionnalité tous les deux ont leurs avantages et inconvénients. Terraform nécessitera des ressources déjà déployées avant de pouvoir commencer, notamment pour la gestion du state mais il est plus utilisé et mieux documenté. Comme c’est un projet open source et qu’il n’est pas utilisé que pour Google Cloud Platform, il bénéficie d’une communauté plus importante. Et dans le cas d’une architecture multi-cloud ou l’utilisation de solution gérée par un des nombreux providers terraform il sera donc possible de déployer avec le même outil.
Pour ce qui est des mises à jour, spécifiquement pour GCP, le provider est maintenu conjointement par Hashicorp et Google. Et grâce à la génération automatique du code directement à partir des API Google à l’aide de Magic Module, il est possible de déployer des services avec Terraform alors même qu’ils viennent de sortir en beta.
Terraform 101
Avec terraform, on décrit son infrastructure en HCL (HashiCorp Configuration Language) , un langage dédié conçu par Hashicorp, avec un système de variable, types, interpolations, boucles, etc…
Illustrons cela avec un exemple simple : je souhaite créer un bucket cloud storage :
resource "google_storage_bucket" "simple-bucket" { name = "simple-bucket-abc" location = "europe-west1" }
Cela ne suffit pas puisque Terraform n’est pas fait uniquement pour déployer sur Google Cloud Platform : il est possible de déployer un grand nombre de ressources différentes grâce à son système de provider.
Dans notre cas je vais donc ajouter le provider Google Cloud platform :
provider "google" { project = "my-gcp-project-id" }
A noter, qu’il faut spécifier un identifiant projet qui va être le projet gcp par défaut où sont déployées les ressources mais aussi dans lequel les appels d’API vont être fait.
Une fois le code écrit dans un fichier .tf, je récupère terraform
, un simple exécutable écrit en go (donc, sans dépendance supplémentaire) permettant d’exécuter les commandes suivantes
:
- terraform init pour initialiser l’espace de travail et télécharger la dernière version du provider.
- terraform plan pour voir le plan d’exécution contenant toutes les actions nécessaires pour que l’état sur le projet GCP corresponde à ce qui se trouve dans les sources .tf.
Avant de lancer le plan je dois authentifier mes appels terraform avec mon compte sur google cloud et avoir les droits nécessaires pour créer un bucket sur ce projet :
gcloud auth application-default login
Résultat du plan :
Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # google_storage_bucket.simple-bucket will be created + resource "google_storage_bucket" "simple-bucket" { + bucket_policy_only = (known after apply) + force_destroy = false + id = (known after apply) + location = "EUROPE-WEST1" + name = "simple-bucket-abc" + project = (known after apply) + self_link = (known after apply) + storage_class = "STANDARD" + url = (known after apply) } Plan: 1 to add, 0 to change, 0 to destroy.
- terraform apply pour appliquer ce plan, terraform fera de nouveau un plan, et demandera confirmation avant d’appliquer la modification.
A chaque exécution Terraform va donc faire une différence entre ce qu’il a déjà déployé et ce qui se trouve dans les sources, puis :
- Créer les ressources en plus.
- Supprimer les ressources en moins.
- Mettre à jour les ressources qui ont changé.
Pour être capable de faire cette différence, Terraform lors de la première exécution va générer un état le « state terraform » qui contient l’ensemble des ressources qui ont été créées. C’est un peu la base de données de terraform, sous forme d’un simple fichier json.
Si je veux voir ce state, j’ouvre le fichier terraform.tfstate qui se trouve, dans cet exemple, là où j’ai lancé la commande terraform:
{ "version": 4, "terraform_version": "0.12.18", "serial": 7, "lineage": "dedc9f7b-14f8-25dc-9752-3a6306a69b19", "outputs": {}, "resources": [ { "mode": "managed", "type": "google_storage_bucket", "name": "simple-bucket", "provider": "provider.google", "instances": [ { "schema_version": 0, "attributes": { "bucket_policy_only": false, "cors": [], "default_event_based_hold": false, "encryption": [], "force_destroy": false, "id": "simple-bucket-abc", "labels": null, "lifecycle_rule": [], "location": "EUROPE-WEST1", "logging": [], "name": "simple-bucket-abc", "project": "my-gcp-project-id", "requester_pays": false, "retention_policy": [], "self_link": "https://www.googleapis.com/storage/v1/b/simple-bucket-abc", "storage_class": "STANDARD", "url": "gs://simple-bucket-abc", "versioning": [], "website": [] }, "private": "bnVsbA==" } ] } ] }
Configuration de l’authentification
Dans l’exemple précédent, un choix simple a été fait de lancer terraform avec son propre compte personnel en s’identifiant avec la commande :
gcloud auth application-default login
Ce n’est en fait pas la solution recommandée car certaines API ne sont pas compatibles avec ce mode d’authentification.
Sur Google Cloud Platform, la bonne manière de s’authentifier est d’utiliser un compte de service. Ce compte doit avoir les permissions et donc les rôles lui permettant de créer les ressources. Il est donc possible et conseillé de limiter ces permissions à ce qu’il doit avoir besoin de faire. Par exemple, un compte de service qui ne peut pas supprimer de projet GCP évitera certaines surprises en cas d’erreur dans le code.
Plusieurs moyens sont possibles pour utiliser ce compte de service :
- Renseigner les variables d’environnement GOOGLE_CREDENTIALS, GOOGLE_CLOUD_KEYFILE_JSON ou GCLOUD_KEYFILE_JSON avec le chemin du compte de service
- Si le terraform est exécuté sur une VM
Compute Engine
il utilisera directement le compte de service de la VM - Lui spécifier le path de la clé json du compte de service dans la configuration du provider :
provider "google" { credentials = file("sa/my-project-service-account.json") }
Cette dernière solution est à éviter, vous ne voulez pas avoir dans votre code un chemin en dur de clé sans compter le risque dans cet exemple d’envoyer sa clé dans son repository git.
Le problème avec cela, c’est que très souvent, on préférait faire un plan avant de pousser son code. Et pour cela, télécharger sur sa machine la clé du compte ce qui n’est pas souhaitable pour des raisons de sécurité.
Pour résoudre ce problème il est possible d’utiliser une autre manière de s’authentifier, le mécanisme d’impersonation et de création de token temporaire des comptes de service :
En pré-requis : un compte de service qui porte les droits pour déployer les ressources que je vais appeler pour l’exemple terraform-deployer
- On déclare un premier provider google : ce provider sera authentifié avec le compte qui lance le terraform : soit l’utilisateur authentifié avec gcloud auth application-default login, soit le compte de service de la CI. Cependant, pour avoir le droit de faire cela il est nécéssaire d’avoir le rôle iam.serviceAccountTokenCreator sur le compte de service de déploiement terraform-deployer
provider "google" { project = var.project alias = "token_gen" scopes = [ "https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/userinfo.email", ] }
- Ce premier provider va servir à récupérer un token d’authentification temporaire pour le compte de service terraform-deployer :
data "google_service_account_access_token" "default" { provider = google.token_gen target_service_account = "terraform-deployer@xxx-id-project. scopes = [ "userinfo-email", "cloud-platform", ] lifetime = "300s" }
- On déclare ensuite un second provider qui va utiliser ce token pour authentifier les appels de terraform. Ce sera ce provider qui sera utilisé pour déployer les ressources avec le compte de service terraform-deployer :
provider "google" { project = "xxx-id-project" access_token = data.google_service_account_access_token.default.access_token }
Plus de détail sur l’impersonation avec terraform ici : How to generate and use temporary credentials on Google Cloud Platform
Gestion du state Terraform
Dans le premier exemple le ‘state terraform’
était en local, ce qui n’est pas une solution viable pour la gestion de mon infrastructure. Il est important d’avoir le state persisté et si également versionné.
Dans Terraform, il est possible de configurer le stockage du state de plusieurs manières, grace à la configuration de « backends ». Par exemple dans une base de donnée, via une api rest ..etc
Il existe justement un backend google cloud storage qui va permettre de stocker ce state sur un bucket à condition d’avoir les droits en écriture sur ce bucket.
Bien sur, ce bucket ne peut pas être créé par le code terraform qui l’utilise comme backend, il faut donc créer ce bucket avant : soit manuellement, soit avec un terraform de plus haut niveau, soit avec une autre solution d’infra as code, deployment manager par exemple sur google cloud platform.
Pour l’exemple partons sur la solution manuelle :
#Bucket creation gsutil mb -p xxx-my-project-id gs://terraform-state-project-xxx/
Une fois le bucket créé il me suffit de spécifier dans le code terraform que j’utilise un backend cloud storage:
terraform { backend "gcs" { bucket = "terraform-state-xxxx-my-project-id" prefix = "simple" } }
A noter, le paramètre prefix pour écrire ce state dans un « sous répertoire ». En faisant un terraform init j’aurai alors dans ce bucket :
Tips : Versioning du state Activez le versioning sur le bucket de state, cela vous permettra de revenir en arrière en cas d’erreur, mise à jour malheureuse de terraform ou corruption du fichier: gsutil versioning set on gs://terraform-state-project-xxx/ Ensuite pour voir l’historique : gsutil ls -a gs://terraform-state-xxxx-my-project-id/simple/default.tfstate gs://terraform-state-xxxx-my-project-id/simple/default.tfstate#1591280095904936 gs://terraform-state-xxxx-my-project-id/simple/default.tfstate#1591280172775505 gs://terraform-state-xxxx-my-project-id/simple/default.tfstate#1591280187357402 gs://terraform-state-xxxx-my-project-id/simple/default.tfstate#1591280195113456 |
---|
Tips: Configuration du backend Il est possible de configurer le backend via des arguments passés à la commande terraform afin de rendre indépendant le code et l’emplacement de stockage du state : terraform { backend "gcs" { } } Ensuite : terraform init -backend-config=”bucket=terraform-state-xxxx-my-project-id" -backend-config=”prefix=simple" ou via un fichier de config externe : terraform init -backend-config=/home/xxx/config/backend-config.tf |
---|
Ressources et Organisation
Nous avons vu que pour déployer avec terraform dans un projet Google Cloud Platform il est nécéssaire d’avoir 3 pré-requis :
- Un compte de service
- Des rôles associées à ce compte de service pour avoir le de droit de créer/modifier/supprimer les ressources
- Un bucket pour le state
Et donc un projet GCP pour héberger cela.
3 solutions sont possibles :
1. Le compte de service et le bucket pour le state sont dans le projet où je souhaite déployer les ressources
Cela revient à dire que le terraform ne porte pas la création de notre projet mais va s’occuper uniquement d’ajouter des ressources dans celui ci. En effet, le projet et les pré-requis doivent être créés avant de pouvoir commencer à utiliser terraform. Cette solution est simple à mettre en place car ne nécessite que un seul projet.
2. Le compte de service et le bucket pour le state sont dans un autre projet
Cette solution permet un scope plus large d’automatisation puisque en fonction des droits accordés au compte de service, il sera possible de gérer tout le cycle de vie d’un projet, voir gérer plusieurs projets, par exemple les n environnements d’une même application.
Un exemple simple pourrait être ainsi :
Mais on peut aller bien plus loin en donnant des droits sur un folder pour pouvoir gérer tous les projets enfants :
Le projet A peut très bien être également dans le même folder.
3. Un mix entre les 2 solutions :
Rien n’oblige à gérer toute l’infrastructure d’un projet avec un seul terraform. Il est possible de gérer la création et les configurations de base d’un projet avec un terraform géré dans un projet externe et de déléguer la création des ressources simples à un autre terraform avec un scope plus réduit de permissions. Le premier terraform ajoutera le compte de service et le bucket pour le state pour le second. Cette solution est la plus complexe : elle offre une séparation des responsabilités plus fine, permet de gérer avec un cycle de vie différent les configurations qui ne changent pas souvent et qui nécessitent des droits importants comme les permissions IAM du projet ou l’activation des services et les configurations qui vont évoluer régulièrement comme les buckets, les VM ou les services managés. Par contre l’infrastructure du projet est séparée en 2 codes avec un lien fort entre les 2.
Dans l’exemple ci-dessus
- Le terraform dans Project A va créer project B et project C dans le parent Folder attribuer les rôles et activer les services.
- Le terraform dans Project B va créér les VM / Buckets / Clusters GKE dans le Project B
- Le terraform dans Project C va créér les VM / Buckets / Clusters GKE dans le Project C
Cela veut donc dire que le terraform dans Project B aura parfois besoin de ressources créées dans le terraform du Project A, (et pas l’inverse il faut garder en tête que le projet A est le parent)
Gestion des projets et folders Google Cloud
Nous avons vu dans le chapitre précédent qu’il est possible de créer et gérer entièrement un projet avec terraform. Pour cela il existe la ressource terraform google_project, mais gérer via infra as code un projet implique un certain nombre de choses :
- Le projet doit être rattaché à un parent, organisation ou folder.
- Il doit être également rattaché à un compte de billing pour la facturation.
- Il doit avoir un ID unique sur GCP, de maximum 30 caractères alphanumériques ou tiret.
- Pour être opérationnel il doit avoir des API d’activées
Cela entraine donc un certain nombre de pré-requis en terme de rôle pour le compte de service qui va exécuter le terraform et en terme de service activé :
- Au minimum, le role roles/billing.user sur le compte de billing à rattacher au projet
- L’API cloudbilling.googleapis.com d’activé sur le projet que l’on utilise avec le provider
- Au minimum, le role roles/resourcemanager.projectCreator sur le parent dans lequel on va créer le projet (folder ou organisation)
A partir de là, créer un projet est très simple :
resource "google_project" "my_project_in_a_folder" { name = "my_unique_project_id" project_id = "my_unique_project_id" folder_id = "1234567891011" billing_account = "0123456-ABCDEF-GHIJK" }
Et activer les services que l’on souhaite utiliser sur ce projet :
locals { services = ["storage-component.googleapis.com", "compute.googleapis.com"] } resource "google_project_service" "services" { for_each = toset(local.services) project = google_project.my_project_in_a_folder.project_id service = each.value }
Tips: Activation des services Dans l’exemple précédent, j’active les services Cloud Storage (storage-component.googleapis.com) et Compute Engine (compute.googleapis.com) La liste des services activables peut être trouvé dans API & Services Si j’affiche la liste des services activés après le apply du terraform : gcloud services list Le nombre de services activés du projet est plus important que dans le terraform : NAME TITLE compute.googleapis.com Compute Engine API oslogin.googleapis.com Cloud OS Login API storage-component.googleapis.com Cloud Storage En effet, activer certains services va entrainer automatiquement certaines actions, comme activer d’autres services nécessaires ou ajouter des comptes de service et des rôles pour des taches particulières du service. Par exemple activer un service important comme Kubernetes Engine API (container.googleapis.com) va :
Cet ensemble d’actions induites par l’activation de service ne sont pas gérées par terraform il faut donc être vigilant de ne pas supprimer ces ressources nécessaires au bon fonctionnement du service |
---|
La création de « folder » est similaire mais en plus simple :
- Le folder doit être rattaché à un parent, organisation ou folder.
- Il doit avoir un ID unique dans le parent où il se trouve, maximum 30 caractères alphanumériques, espace, underscore, quote simple et tiret.
- Maximum 10 niveaux hiérarchiques
Pour créer un folder à la racine de l’organisation :
data "google_organization" "org" { domain = var.org_domain } resource "google_folder" "my_folder" { display_name = "my_folder" parent = data.google_organization.org.name }
Pour faire cela je dois avoir les 2 rôles suivants (au minimum) :
- roles/resourcemanager.organizationViewer pour pouvoir récupérer l’identifiant de l’organisation avec la datasource google_organization
- foles/resourcemanager.folderCreator au niveau du parent, donc dans cet exemple, l’organisation
Gestion des permissions IAM
Avant de se lancer dans la gestion des permissions avec Terraform il faut avoir intégré la notion d’héritage des permissions sur GCP.
Il est possible d’ajouter des rôles, sur
- L’organisation
- un Folder
- un Projet
- Certaine ressource (Bucket par exemple)
Un projet va hériter des permissions des parents :
Dans terraform il existe donc des ressources terraform pour chaque niveau de permission :
- Organisation => google_organization_iam_*
- Folder => google_folder_iam_*
- Project => google_project_iam_*
- Bucket => google_storage_bucket_iam_*
- Instance compute engine => google_compute_instance_iam_*
Ces ressources terraform sont déclinées en 3 catégories qui vont avoir des comportements différents :
-
policy :
On définit tous les rôles avec cette ressource terraform qui va synchroniser exactement ce qui se trouve dans terraform avec les permissions sur Google Cloud Platform. Cela nous assure qu’aucun autre rôle ne peut être ajouté ou qu’il sera supprimé dès qu’on fera le ‘apply terraform’. On parle de ressource « Authoritative ». Généralement compliqué et parfois dangereux à utiliser, notamment dans le cas des permissions d’un projet car un certain nombre de rôles sont attribués de manière automatique par la plateforme et donc peuvent être écrasés par terraform ce qui peut créer des dysfonctionnements.
Je vous conseille de lire les mises en garde associées, dans la documentation, avant de l’utiliser par exemple pour google_project_iam_policy.
Exemple pour un projet :resource "google_project_iam_policy" "project" { project = google_project.my_project_in_a_folder.project_id policy_data = data.google_iam_policy.iam_policy.policy_data } data "google_iam_policy" "iam_policy" { binding { role = "roles/viewer" members = [ "user:user@xxxxx.xx", ] } binding { role = "roles/owner" members = [ "group:admin@xxxxxx.xxx", ] } }
Dans l’exemple ci dessus le contenu de iam_policy est l’équivalent de ce qu’on aurait avec la commande gcloud projects get-iam-policy
-
binding:
On définit tous les membres d’un seul rôle, qui seront synchronisés lors du ‘apply terraform’. Assure que ce rôle est donné uniquement à la liste de membres fournie. On parle de ressource « Authoritative » par rôle.
Exemple pour un folder :
resource "google_folder_iam_binding" "folder_viewers" { folder = google_folder.my_folder.name role = "roles/viewer" members = [ "user:user@xxxxx.xx", ] } resource "google_folder_iam_binding" "folder_owners" { folder = google_folder.my_folder.name role = "roles/owner" members = [ "group:admin@xxxxxx.xxx", ] }
-
Member :
On définit un ajout de rôle pour un membre donné. On parle de ressource « non Authoritative ». Ne supprime pas les rôles qui auraient été ajoutés sans terraform.
Exemple pour un bucket :
resource "google_storage_bucket_iam_member" "user_can_view_simple_bucket_object" { bucket = google_storage_bucket.simple-bucket.name role = "roles/storage.objectViewer" member = "user:user@xxxxx.xx", } resource "google_storage_bucket_iam_member" "admin_is_storage_admin_in_simple_bucket" { bucket = google_storage_bucket.simple-bucket.name role = "roles/storage.admin" member = "group:admin@xxxxxx.xxx", }
Tips : google_project_iam_policy et Kubernetes Engine Comme vu précédemment au sujet de l’activation des services, activer GKE ajoutera automatiquement des comptes de service techniques ainsi que des rôles associés. Utiliser google_project_iam_policy dans ces conditions va supprimer les rôles des comptes de service et bloquer le service GKE. Le code suivant : resource "google_project_iam_policy" "project" { project = google_project.my_project_in_a_folder.project_id policy_data = data.google_iam_policy.admin.policy_data } data "google_iam_policy" "admin" { binding { role = "roles/viewer" members = [ "user:user@xxxx.xxx", ] } binding { role = "roles/owner" members = [ "group:admin-group@xxxxxx.xx", ] } } locals { services = ["container.googleapis.com"] } resource "google_project_service" "services" { for_each = toset(local.services) project = google_project.my_project_in_a_folder.project_id service = each.value } Lors du premier apply rien d’anormal :
Mais si je fais un second apply terraform souhaitera supprimer les mappings rôles/comptes de service suivant :
Appliquer ces modifications entrainera l’impossibilité du service de fonctionner, il ne sera plus possible, non plus, de créer des clusters ou en supprimer 2 solutions possibles :
|
---|
Conclusion
Terraform est un bon choix pour gérer son infrastructure sur Google Platform. Cependant, même si au premier abord cela semble très simple il reste nécessaire de comprendre un minimum le fonctionnement des services, des permissions IAM et de l’organisation des resources.
Il est également recommandé de comprendre comment Terraform agit et de vérifier ce que l’on modifie avec un « terraform plan »
Dans cet article, nous avons exploré un certain nombre de concepts et également de pièges à éviter avec les ressources de base cependant il existe aussi des modules de plus hauts niveaux maintenus par Google pour simplifier la configuration de certains services : https://registry.terraform.io/modules/terraform-google-modules.
Commentaire