Déploiement sur AWS avec Terraform & Ansible
Gitlab continue son travail d’intégration de Terraform sur sa plateforme. En effet, leur objectif est de proposer une solution simple et sécurisée pour mettre des workflows d’Infrastructure As Code. Voyons tout cela ensemble.
Je vais reprendre mon exemple du précédent billet mais en le portant cette fois sur Amazon Web Service et non Google Cloud Platform. Pour rappel l’objectif était d’arriver à récupérer un inventaire dynamique du projet.
Installation des prérequis
Installation de la CLI AWS
Cette fois je vais respecter les bonnes pratiques en utilisant un utilisateur
IAM ↗
et non le compte root
.
On lance la commande de configuration de la CLI AWS pour ajouter le compte administrator avec la sortie au format JSON :
aws configureAWS Access Key ID [None]: xxxxxxxxxxAWS Secret Access Key [None]: xxxxx/xx/xxxxxxxxxxxxxxxxxxxxDefault region name [None]: eu-west-3Default output format [None]: json
Installation de Terraform
De la même manière procéder à l’installation de Terraform. ↗
Installation d’Ansible
Ansible est très simple à installer avec pyenv.
pyenv install 3.9.8pyenv virtualenv 3.9.8 ansiblepyenv local ansiblepip install ansible
Initialisation de la configuration Terraform
On va simplement créé les deux premiers fichiers
main.tf
:
terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 3.64" } }}
provider "aws" { region = var.aws_zone}
On initialise le projet :
terraform init
Initializing the backend...
Initializing provider plugins...- Finding latest version of hashicorp/aws...- Installing hashicorp/aws v3.64.2...- Installed hashicorp/aws v3.64.2 (signed by HashiCorp)```
Création du projet Gitlab
Nous allons initialiser le projet en créant de suite le fichier .gitignore
pour ne pas stocker de suite dans le repo des données sensibles. La version officielle ↗
On créé aussi notre fichier .gitlab-ci.yml avec ce contenu :
include: - template: Terraform.gitlab-ci.ymlvariables: TF_STATE_NAME: default TF_CACHE_KEY: defaultstages: - init - validate - build - deploy
init: extends: .init
validate: extends: .validate
build: extends: .build
deploy: extends: .deploy dependencies: - build
On créé le project sur gitlab.com (remplacez le repo avec le votre!) :
8281 git init --initial-branch=main 8282 git remote add origin git@gitlab.com:Bob74/test-aws.git 8283 git add . 8284 git commit -m "Initial commit"
Si on se rend dans pipeline vous devriez voir ceci :
Cool. Tout est directement utilisable sans rien à faire. Vous remarquerez que le deploy est en activation manuelle.
Mais comment est géré le state de terraform dans Gitlab?
Il suffit de regarder ce qui se passe dans le stage init :
/builds/Bob74/test-aws/.terraform/: found 8 matching files and directoriesUploading cache.zip to https://storage.googleapis.com/gitlab-com-runners-cache/project/31197695/builds/Bob74/test-aws
Il est stocké dans un Google Cloud Storage
. Nous verrons comment le modifier
une autre fois, pour le stocker dans un S3 d'Amazon
ou directement dans
gitlab
si vous utilisez une solution auto-hébergée.
Création de notre VM EC2
Vous allez voir ici les choses vont être un peu plus complexe.
Ajoutons d’abord ces lignes au fichier main.tf :
terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 3.64" } }}
provider "aws" { region = var.aws_zone}
data "aws_ami" "latest-rocky" { most_recent = true owners = ["679593333241"]
filter { name = "root-device-type" values = ["ebs"] }
filter { name = "virtualization-type" values = ["hvm"] } filter { name = "name" values = ["Rocky Linux 8.4-*"] }}
resource "aws_key_pair" "ssh-key" { key_name = "ssh-key" public_key = file("~/.ssh/id_ed25519.pub")}
resource "aws_instance" "gitlab-runner" { ami = data.aws_ami.latest-rocky.id instance_type = var.gitlab_runner_instance_type // user_data = data.template_file.user_data.rendered associate_public_ip_address = true connection { type = "ssh" host = "${self.private_ip}" } tags = { Name = "gitlab-runner" }}
resource "aws_default_vpc" "default" { tags = { Name = "Default VPC" }}
resource "aws_default_security_group" "default" { vpc_id = "${aws_default_vpc.default.id}" ingress { # TLS (change to whatever ports you need) from_port = 22 to_port = 22 protocol = "tcp" # Please restrict your ingress to only necessary IPs and ports. # Opening to 0.0.0.0/0 can lead to security vulnerabilities. cidr_blocks = ["0.0.0.0/0"] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] }}output "ip" { value = "${aws_instance.gitlab-runner.public_ip}"}
Quelques explications :
On retrouve plusieurs blocs resource et un data.
- Le
data
latest_rocky permet de récupérer la dernière image Rocky Linux 8 dont le fournisseur est679593333241
. Cette image sera celle utilisé dans l’instance. - un bloc pour stocker la clé pour se connecter
- un bloc
aws_default_vpc
qui permet de sélectionner le vpcdefault
- un bloc
aws_default_security_group
qui permet d’ouvrir le flux SSH sur le vpc default afin que de permettre les connections sur la machine provisionnée. - un bloc
aws_instance
qui est la vm et qui utiliser tous les blocs précédent.
On créé le fichier de variables comme suite :
variable.tf
:
variable "aws_project" { type = string default = "gitlab-runner-sr" description : "The aws project to deploy the runner into."}
variable "aws_zone" { type = string default = "eu-west-3" description : "The aws region to deploy the runner into."}
variable "gitlab_runner_instance_type" { type = string default = "t2.micro"}
variable "SSH_KEY" { type = string}
On initialise la variable TV_VAR_SSH_KEY :
export TF_VAR_SSH_KEY=`cat ~/.ssh/id_ed25519.pub`
Maintenant vous pouvez tester en local votre configuration :
terraform apply
....
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
Outputs:
ip = "13.36.xxx.xxx"
Même si on récupère rapidement l’adresse IP il faudra patienter que la machine
soit disponible: dans la console dans le contrôle de statuts on ne doit plus
voir initialisation en cours
.
Une fois prête vous pouvez vous connecter sur la machine avec votre clé SSH.
ssh rocky@13.36.xxx.xxx
Warning: Permanently added '13.36.xxx.xxx' (ECDSA) to the list of known hosts.Activate the web console with: systemctl enable --now cockpit.socket
[rocky@ip-13.36.xxx.xxx ~]$
Test de l’approvisionnement depuis gitlab
Avant de pousser notre configuration dans gitlab il faut avant créer les secrets qui seront utilisé dans le CI. Allez dans le menu Settings/CI/CD de Gitlab et ajoutez trois variables:
- AWS_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEY
- SSH_KEY (avec le contenu de la clé publique)
On pousse nos modifications et regardons si tout se passe bien dans le pipeline sous gitlab.
git add main.tf variables.tfgit commit -m 'create ec2 instance'git push
Et on lance le deploy (tache manuelle), on attend … on teste la connexion ssh
aws_instance.gitlab-runner: Still creating... [20s elapsed]aws_instance.gitlab-runner: Still creating... [30s elapsed]aws_instance.gitlab-runner: Still creating... [40s elapsed]aws_instance.gitlab-runner: Creation complete after 46s [id=i-00984783eed6e55e5]Apply complete! Resources: 4 added, 0 changed, 0 destroyed.Outputs:ip = "xx.xx.xxx.xxxx"Saving cache for successful jobCreating cache /builds/Bob74/test-aws.../builds/Bob74/test-aws/.terraform/: found 8 matching files and directoriesUploading cache.zip to https://storage.googleapis.com/gitlab-com-runners-cache/project/31197695/builds/Bob74/test-awsCreated cacheCleaning up project directory and file based variables00:01Job succeeded
ssh xx.xx.xxx.xxxxActivate the web console with: systemctl enable --now cockpit.socket
[rocky@ip-172-31-28-212 ~]$
Ca c’est fait. Maintenant regardons comment utiliser l’inventaire Ansible.
Utilisation de l’inventaire dynamique Ansible aws_ec2
Il suffit de créer un fichier ansible.cfg contenant ceci:
[defaults]host_key_checking = Falseinventory=aws_ec2.ymlinterpreter_python=auto_silentremote_user=rocky[inventory]enable_plugins = aws_ec2, auto, host_list, yaml, ini, toml, script
On utilise le plugin aws_ec2
qui fait appel à l’inventaire aws_ec2.yml
dont
le contenu est (modifier les clés AWS avec les vôtres):
---plugin: aws_ec2aws_access_key: xxxxxxxxxxxxxxxxxxxxxxxxaws_secret_key: xxxx/xx/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxkeyed_groups: - key: tags prefix: tag
On teste si on récupère les machines provisionnées :
ansible-inventory --graph@all: |--@aws_ec2: | |--ec2-13-36-xxx-xxx.eu-west-3.compute.amazonaws.com |--@tag_Name_gitlab_runner: | |--ec2-13-36-xxx-xxx.eu-west-3.compute.amazonaws.com |--@ungrouped:
On test le module ping :
ansible -m ping allec2-xx.xx.xxx.xxx.eu-west-3.compute.amazonaws.com | SUCCESS => { "ansible_facts": { "discovered_interpreter_python": "/usr/libexec/platform-python" }, "changed": false, "ping": "pong"}
Intégration dans le CI Gitlab
L’objectif est de pouvoir jouer un playbook de test qui va installer le repo EPEL sur notre rocky linux 8:
---- hosts: all gather_facts: true check_mode: false become: true tasks: - name: install epel-release ansible.builtin.package: name: epel-release state: present...
Pour cela on va modifier notre fichier aws_ec2 en remplacant les valeurs des infos AWS par les variables que nous avons créé dans les paramètres CI/CD:
---plugin: aws_ec2aws_access_key: ${AWS_ACCESS_KEY_ID}aws_secret_key: ${AWS_SECRET_ACCESS_KEY}
Et on vient ajouter un stage à notre CI:
include: - template: Terraform.gitlab-ci.ymlvariables: # If not using GitLab's HTTP backend, remove this line and specify TF_HTTP_* variables # If your terraform files are in a subdirectory, set TF_ROOT accordingly # TF_ROOT: terraform/production
stages: - init - validate - build - deploy - provision - destroy
init: extends: .init
validate: extends: .validate
build: extends: .build
deploy: extends: .deploy before_script: - terraform refresh || true dependencies: - build
provision: stage: provision image: name: registry.gitlab.com/dockerfiles6/ansible-lint:latest entrypoint: [""] script: - chmod 755 . - mkdir /root/.ssh - cp /builds/Bob74/test-aws.tmp/id_ed25519 /root/.ssh/id_ed25519 - chmod 0600 /root/.ssh/id_ed25519 - ansible -m ping all when: manual
destroy: stage: destroy extends: .destroy
Remarquez la commande ajouté dans le stage deploy
, qui permet d’éviter les
erreurs sur les clés existantes. Lors de la seconde soumission. Il faut créer
une nouvelle variable de type file
id_ed25519 avec la clé privé.
Poussons le tout dans gitlab et vérifions.
$ ansible-inventory --graph@all: |--@aws_ec2: | |--ec2-xx-xx-xx-xxx.eu-west-3.compute.amazonaws.com |--@tag_Name_gitlab_runner: | |--ec2-xx-xx-xx-xxx.eu-west-3.compute.amazonaws.com |--@ungrouped:Saving cache for successful job00:09Creating cache /builds/Bob74/test-aws.../builds/Bob74/test-aws/.terraform/: found 8 matching files and directoriesUploading cache.zip to https://storage.googleapis.com/gitlab-com-runners-cache/project/31197695/builds/Bob74/test-awsCreated cacheCleaning up project directory and file based variables00:00Job succeeded
Après plusieurs lancements de la phase apply je me trouve avec plusieurs VM. Pas
vraiment le résultat attendu. Je tente d’activer un backend de type S3 (qu’il
faut créer avant) pour stocker le state Terraform. Pour cela il suffit de
modifier le premier bloc du fichier main.tf
:
terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 3.64" } } backend "s3" { bucket = "terraform-state-sr" key = "global/s3/terraform.tfstate" region = "eu-west-3" }}
Ca fonctionne.
Initializing the backend...Successfully configured the backend "s3"! Terraform will automaticallyuse this backend unless the backend configuration changes.
Content du résultat.