Automatiser la création de version d'une application avec semantic-release
Dans cet article, découvrez comment automatiser une création de version de votre application grâce à Semantic-Release : nommage des commits et configurations
Sommaire
Avec l'augmentation du nombre de projets et de contributeurs sur notre organisation Github ont émergé des sujets d'onboarding / outboarding et de gouvernance. Jusque là, les dépôts et les contributeurs étaient créés manuellement par un administrateur de l'organisation, mais le manque de disponibilité de ces administrateurs ne permettait pas une gestion optimale. Il devenait évident qu'une autre solution devait être envisagée.
Comme expliqué précédemment, notre obstacle sur le chemin d'une gestion optimale se trouve au niveau des actions manuelles réalisables par un ensemble restreint de personnes.
Pour outrepasser cet obstacle, deux solutions s'offraient à nous
La première solution, consistant à augmenter le nombre d'administrateurs, a très vite été abandonnée. Elle soulevait plus de questions qu'elle apportait de réponses (sécurité, gouvernance, perte de l'information).
La deuxième solution, au contraire, s'est très vite révélée être la solution dont nous avions besoin.
Avec cet ensemble de besoins identifiés, j'ai tout de suite vu une nouvelle occasion d'utiliser un de mes outils favoris Terraform.
Terraform nous permet d'avoir un projet déclaratif et open source versionné sur Github (Githubception) pour favoriser la collaboration, simplifier le process de validation et déclencher automatiquement des actions au merge d'une pull request.
Commençons par la configuration du provider Github pour Terraform.
La documentation du provider Github pour Terraform est disponible sur le site officiel Terraform.
Pour cela vous allez avoir besoin :
your_organisation_name
ff34885...
Créons à la racine de notre projet un fichier terraform.tf
qui contiendra la configuration du provider
# ./terraform.tf provider "github" { organization = "your_organization_name" token = "ff34885..." }
Vous pouvez maintenant initialiser le projet avec la commande
terraform init
Notre projet Terraform étant initialisé et prêt à communiquer avec l'API de Github, voyons comment créer et récupérer les différentes resources et data sources dont nous avons besoin.
Commençons par un exemple simplifié de gestion de repository avec la resource github_repository
Nous verrons plus tard un exemple plus complet
La resource github_repository
requiert comme argument
my_awesome_repo
Créons à la racine du projet un fichier repository.tf
qui contiendra la déclaration de notre repository
# ./repository.tf resource "github_repository" "example" { name = "my_awesome_repo" }
Vous pouvez maintenant vérifier les changements que Terraform apportera à votre organisation avec la commande
terraform plan
Puis appliquez ces changements avec la commande
terraform apply
Votre nouveau repository est maintenant disponible dans votre organisation Github
Intéressons-nous maintenant à la partie utilisateur.
Pour les utilisateurs, notre but n'est pas de créer de nouveaux utilisateurs Github mais de récupérer les utilisateurs
qui nous intéressent pour ensuite les ajouter à notre organisation.
Pour cette raison nous utilisons la data source github_user
pour récupérer les utilisateurs et la resource
github_membership
afin de les ajouter à l'organisation.
La data source github_user
requiert en argument
VEBERAranud
Créons à la racine du projet un fichier user.tf
# ./user.tf data "github_user" "example" { username = "VEBERArnaud" }
La resource github_membership
requiert en arguments
github_user
.member
ou admin
en fonction des
permissions que vous souhaitez lui attribuer.Ajoutons au fichier user.tf
# ./user.tf # ... (github_user data source) resource "github_membership" "example" { username = data.github_user.example.login role = "admin" }
Vous pouvez maintenant faire un plan et un apply de vos changements, avec les commandes
terraform plan terraform apply
L'utilisateur reçoit alors un mail de Github l'invitant à rejoindre votre organisation.
Le dernier domaine que nous verrons dans cet article concerne les teams, incluant la création, l'ajout d'utilisateurs et l'attribution de repositories à ces teams.
Pour cela nous utiliserons les resources github_team
pour la création de teams, github_team_membership
pour l'ajout
d'utilisateurs aux teams et github_team_repository
pour l'attribution de repositories aux teams.
La resource github_team
requiert en argument
BackEnd
Créons à la racine du projet un fichier team.tf
# ./team.tf resource "github_team" "example" { name = "BackEnd" }
La resource github_team_membership
requiert en arguments
github_team
github_user
member
et maintainer
Ajoutons au fichier team.tf
# ./team.tf # ... (github_team resource) resource "github_team_membership" "example" { team_id = github_team.example.id username = data.github_user.example.login role = "maintainer" }
La resource github_team_repository
requiert en arguments
github_team
github_repository
pull
, triage
, push
, maintain
or admin
Ajoutons au fichier team.tf
# ./team.tf # ... (github_team resource) # ... (github_team_membership resource) resource "github_team_repository" "example" { team_id = github_team.example.id repository = github_repository.example.name permission = "admin" }
Vous pouvez maintenant faire un plan et un apply de vos changements, avec les commandes
terraform plan terraform apply
Votre nouvelle team devrait maintenant exister, contenir votre utilisateur et avoir les droits admin sur votre nouveau repository.
Maintenant que nous savons gérer les repositories, les utilisateurs et les teams, voyons comment créer des modules réutilisables pour abstraire une partie de la complexité.
Nous en profiterons pour ajouter de nouvelles resources à ces modules afin d'ajouter les arguments optionnels sur les resources ainsi que la création des resources de protection de branches et des webhooks sur les repositories.
Le premier module que nous allons réaliser est le module de gestion de repository que nous nommerons repository
.
Ce module est composé des fichiers
./module/repository/variables.tf
pour regrouper les différentes variables du module./module/repository/main.tf
pour la déclaration du repository./module/repository/branch_protection.tf
pour la déclaration des branches protégées du repository./module/repository/webhook.tf
pour la déclaration des webhooks du repository./module/repository/outputs.tf
pour l'exposition d'attributs à l'extérieur du module# ./module/repository/variables.tf
variable "repository-name" {
type = string
}
variable "repository-description" {
type = string
default = null
}
variable "repository-homepage_url" {
type = string
default = null
}
variable "repository-topics" {
type = list(string)
default = []
}
variable "repository-private" {
type = bool
default = true
}
variable "repository-has_issues" {
type = bool
default = true
}
variable "repository-has_projects" {
type = bool
default = true
}
variable "repository-has_wiki" {
type = bool
default = true
}
variable "repository-has_downloads" {
type = bool
default = true
}
variable "repository-allow_merge_commit" {
type = bool
default = true
}
variable "repository-allow_squash_merge" {
type = bool
default = true
}
variable "repository-allow_rebase_merge" {
type = bool
default = true
}
variable "repository-auto_init" {
type = bool
default = false
}
variable "repository-gitignore_template" {
type = string
default = null
}
variable "repository-license_template" {
type = string
default = null
}
variable "repository-default_branch" {
type = string
default = null
}
variable "repository-archived" {
type = bool
default = false
}
variable "branches_protection" {
type = list(
object({
branch = string,
enforce_admins = bool,
require_signed_commits = bool,
status_check-strict = bool,
status_check-contexts = list(string),
pr_reviews-required_approving_review_count = number
pr_reviews-require_code_owner_reviews = bool,
pr_reviews-dismiss_stale_reviews = bool,
pr_reviews-dismissal_users = list(string),
pr_reviews-dismissal_teams = list(string),
restrictions-users = list(string),
restrictions-teams = list(string)
})
)
default = []
}
variable "webhooks" {
type = list(
object({
url = string,
secret = string,
content_type = string,
insecure_ssl = bool,
active = bool,
events = list(string)
})
)
default = []
}
# ./module/repository/main.tf resource "github_repository" "main" { name = var.repository-name description = var.repository-description homepage_url = var.repository-homepage_url topics = var.repository-topics private = var.repository-private has_issues = var.repository-has_issues has_projects = var.repository-has_projects has_wiki = var.repository-has_wiki has_downloads = var.repository-has_downloads allow_merge_commit = var.repository-allow_merge_commit allow_squash_merge = var.repository-allow_squash_merge allow_rebase_merge = var.repository-allow_rebase_merge auto_init = var.repository-auto_init gitignore_template = var.repository-gitignore_template license_template = var.repository-license_template default_branch = (var.repository-default_branch != "master" ? var.repository-default_branch : null) archived = var.repository-archived lifecycle { prevent_destroy = true } }
# ./module/repository/branch_protection.tf resource "github_branch_protection" "main" { count = length(var.branches_protection) repository = github_repository.main.name branch = var.branches_protection[count.index].branch enforce_admins = var.branches_protection[count.index].enforce_admins require_signed_commits = var.branches_protection[count.index].require_signed_commits required_status_checks { strict = var.branches_protection[count.index].status_check-strict contexts = var.branches_protection[count.index].status_check-contexts } required_pull_request_reviews { required_approving_review_count = var.branches_protection[count.index].pr_reviews-required_approving_review_count dismiss_stale_reviews = var.branches_protection[count.index].pr_reviews-dismiss_stale_reviews dismissal_users = var.branches_protection[count.index].pr_reviews-dismissal_users dismissal_teams = var.branches_protection[count.index].pr_reviews-dismissal_teams require_code_owner_reviews = var.branches_protection[count.index].pr_reviews-require_code_owner_reviews } restrictions { users = var.branches_protection[count.index].restrictions-users teams = var.branches_protection[count.index].restrictions-teams } }
# ./module/repository/webhook.tf resource "github_repository_webhook" "main" { count = length(var.webhooks) repository = github_repository.main.name configuration { url = var.webhooks[count.index].url content_type = var.webhooks[count.index].content_type insecure_ssl = var.webhooks[count.index].insecure_ssl secret = var.webhooks[count.index].secret } active = var.webhooks[count.index].active events = var.webhooks[count.index].events }
# ./module/repository/outputs.tf output "name" { value = github_repository.main.name } output "full_name" { value = github_repository.main.full_name } output "html_url" { value = github_repository.main.html_url } output "ssh_clone_url" { value = github_repository.main.ssh_clone_url } output "http_clone_url" { value = github_repository.main.http_clone_url } output "svn_url" { value = github_repository.main.svn_url }
Pour utiliser ce module, éditons le fichier ./repository.tf
et remplaçons son contenu par
# ./repository.tf module "my_awesome_blog" { source = "./module/repository/" # repository repository-name = "my_awesome_blog" repository-description = "My Awesome Blog" repository-homepage_url = "https://my-awesome-blog.com" repository-topics = ["blog", "tech", "awesome"] repository-private = false repository-has_projects = false repository-auto_init = false repository-default_branch = "master" # branches protection branches_protection = [ { branch = "master" enforce_admins = false require_signed_commits = false status_check-strict = true status_check-contexts = ["continuous-integration/travis-ci"] pr_reviews-required_approving_review_count = 1 pr_reviews-require_code_owner_reviews = false pr_reviews-dismiss_stale_reviews = false pr_reviews-dismissal_users = [] pr_reviews-dismissal_teams = [] restrictions-users = [] restrictions-teams = [] } ] # webhooks webhooks = [ { url = "https://notify.travis-ci.com" secret = null content_type = "form" insecure_ssl = false active = true events = ["create", "delete", "issue_comment", "member", "public", "pull_request", "push", "repository"] } ] }
Intéressons-nous maintenant au module de gestion d'utilisateurs que nous nommerons user
.
Ce module est composé des fichiers
./module/user/variables.tf
pour regrouper les différentes variables du module./module/user/main.tf
pour la déclaration des utilisateurs./module/user/membership.tf
pour l'attribution des utilisateurs à l'organisation./module/user/outputs.tf
pour l'exposition d'attributs à l'extérieur du module# ./module/user/variables.tf
variable "user-name" {
type = string
}
variable "user-role" {
type = string
default = "member"
}
# ./module/user/main.tf data "github_user" "main" { username = var.user-name }
# ./module/user/membership.tf resource "github_membership" "main" { username = data.github_user.main.login role = var.user-role }
# ./module/user/outputs.tf output "login" { value = data.github_user.main.login } output "avatar_url" { value = data.github_user.main.avatar_url } output "gravatar_id" { value = data.github_user.main.gravatar_id } output "site_admin" { value = data.github_user.main.site_admin } output "name" { value = data.github_user.main.name } output "company" { value = data.github_user.main.company } output "blog" { value = data.github_user.main.blog } output "location" { value = data.github_user.main.location } output "email" { value = data.github_user.main.email } output "gpg_keys" { value = data.github_user.main.gpg_keys } output "ssh_keys" { value = data.github_user.main.ssh_keys } output "bio" { value = data.github_user.main.bio } output "public_repos" { value = data.github_user.main.public_repos } output "public_gists" { value = data.github_user.main.public_gists } output "followers" { value = data.github_user.main.followers } output "following" { value = data.github_user.main.following } output "created_at" { value = data.github_user.main.created_at } output "updated_at" { value = data.github_user.main.updated_at }
Pour utiliser ce module, éditons le fichier ./user.tf
et remplaçons son contenu par
# ./user.tf
module "VEBERArnaud" {
source = "./module/user/"
user-name = "VEBERArnaud"
user-role = "admin"
}
Pour finir avec les modules, regardons la gestion des teams dans un module nommé team
.
Ce module est composé des fichiers
./module/team/variables.tf
pour regrouper les différentes variables du module./module/team/main.tf
pour la déclaration de la team./module/team/team_membership.tf
pour l'ajout des utilisateurs à la team./module/team/team_repository.tf
pour l'ajout des repositories à la team./module/team/outputs.tf
pour l'exposition d'attributs à l'extérieur du module# ./module/team/variables.tf
variable "team-name" {
type = string
}
variable "team-description" {
type = string
default = null
}
variable "team-privacy" {
type = string
default = "secret"
}
variable "team-parent_team_id" {
type = string
default = null
}
variable "team-ldap_dn" {
type = string
default = null
}
variable "team-members" {
type = list(string)
default = []
}
variable "team-members_role" {
type = map
default = {}
}
variable "team-repositories" {
type = list(string)
default = []
}
variable "team-repositories_permission" {
type = map
default = {}
}
# ./module/team/main.tf resource "github_team" "main" { name = var.team-name description = var.team-description privacy = var.team-privacy parent_team_id = var.team-parent_team_id ldap_dn = var.team-ldap_dn }
# ./module/team/team_membership.tf resource "github_team_membership" "members" { for_each = toset(var.team-members) team_id = github_team.main.id username = each.value role = var.team-members_role[each.value] }
# ./module/team/team_repository.tf resource "github_team_repository" "repositories" { for_each = toset(var.team-repositories) team_id = github_team.main.id repository = each.value permission = var.team-repositories_permission[each.value] }
# ./module/team/outputs.tf output "id" { value = github_team.main.id } output "slug" { value = github_team.main.slug }
Pour utiliser ce module, éditons le fichier ./team.tf
et remplaçons son contenu par
# ./team.tf module "core" { source = "./module/team/" team-name = "FrontEnd" team-description = "FrontEnd Developers" team-privacy = "secret" team-members = [ module.VEBERArnaud.login, ] team-members_role = { (module.VEBERArnaud.login) = "maintainer", } team-repositories = [ module.my_awesome_blog.name, ] team-repositories_permission = { (module.my_awesome_blog.name) = "admin", } }
Utilisons maintenant nos commandes Terraform pour vérifier les changements qui vont être apportés à notre organisation Github et les appliquer.
terraform plan terraforn apply
Afin de favoriser la collaboration, il est important de partager le state Terraform entre les différentes exécutions et garantir qu'une seule exécution se fait à un instant t
Pour cela, il est possible de configurer le stockage distant des fichiers de state Terraform, plusieurs types de backend sont disponible en fonction de vos préférences.
La documentation pour ces fonctionnalités est disponible sur la documentation Terraform.
La dernière étape pour que notre projet corresponde aux besoins de départ est la mise en place d'une pipeline de CI/CD.
Pour l'exemple nous utiliserons travis-ci mais vous pouvez utiliser la techno de votre choix.
Notre pipeline se chargera à chaque run
L'application des changements ne devant être exécutée que dans le cas d'un merge sur la branche master.
Pour cela nous utilisons la configuration travis suivante
Pensez à mettre à jour la version de Terraform dans la variable d'env globale
TERRAFORM_VERSION
en fonction de votre installation
language: generic os: linux version: ~> 1.0 env: global: - TERRAFORM_VERSION=0.12.24 - TERRAFORM_PATH=$HOME/bin before_install: - wget "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip" -O /tmp/terraform.zip install: - unzip -d "${TERRAFORM_PATH}" /tmp/terraform.zip before_script: - terraform init -input=false script: - terraform validate - terraform fmt -check=true -diff - terraform plan -input=false -out=.terraform/tfplan deploy: provider: script edge: true script: terraform apply -input=false .terraform/tfplan on: branch: master
Vous avez maintenant toutes les clés pour gérer votre organisation Github en collaboratif et scalable, s'adaptant à la taille de votre organisation. Un exemple de première Pull Request pour vos nouveaux collaborateurs pourrait être de leurs faire gérer leur propre onboarding dans l'organisation.
Vous pouvez jeter un oeil à notre repository pour voir un "real world example".
Auteur(s)
Arnaud Veber
Cloud Solutions Architect & Fullstack Developer depuis 10 ans.
Je travaille aujourd'hui principalement sur des architectures web serverless & event driven hébergées sur AWS.
Vous souhaitez en savoir plus sur le sujet ?
Organisons un échange !
Notre équipe d'experts répond à toutes vos questions.
Nous contacterDécouvrez nos autres contenus dans le même thème
Dans cet article, découvrez comment automatiser une création de version de votre application grâce à Semantic-Release : nommage des commits et configurations
Dans le domaine de la data, la qualité de la donnée est primordiale. Pour s'en assurer, plusieurs moyens existent, et nous allons nous attarder dans cet article sur l'un d'entre eux : tester unitairement avec Pytest.
Le domaine de la data est présent dans le quotidien de chacun : la majorité de nos actions peut être traduite en données. Le volume croissant de ces données exploitables a un nom : "Big Data". Dans cet article, nous verrons comment exploiter ce "Big data" à l'aide du framework Apache Spark.