Terraform - Provisionner et configurer des machines sur AWS avec Gitlab et Ansible
Publié le : 10 novembre 2021 | Mis à jour le : 12 janvier 2023Gitlab 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 configure
AWS Access Key ID [None]: xxxxxxxxxx
AWS Secret Access Key [None]: xxxxx/xx/xxxxxxxxxxxxxxxxxxxx
Default region name [None]: eu-west-3
Default 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.8
pyenv virtualenv 3.9.8 ansible
pyenv local ansible
pip 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.yml
variables:
TF_STATE_NAME: default
TF_CACHE_KEY: default
stages:
- 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 directories
Uploading 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_rsa.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_rsa.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.tf
git 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 job
Creating cache /builds/Bob74/test-aws...
/builds/Bob74/test-aws/.terraform/: found 8 matching files and directories
Uploading cache.zip to https://storage.googleapis.com/gitlab-com-runners-cache/project/31197695/builds/Bob74/test-aws
Created cache
Cleaning up project directory and file based variables
00:01
Job succeeded
ssh xx.xx.xxx.xxxx
Activate 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 = False
inventory=aws_ec2.yml
interpreter_python=auto_silent
remote_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_ec2
aws_access_key: xxxxxxxxxxxxxxxxxxxxxxxx
aws_secret_key: xxxx/xx/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
keyed_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 all
ec2-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_ec2
aws_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.yml
variables:
# 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_RSA /root/.ssh/id_rsa
- chmod 0600 /root/.ssh/id_rsa
- 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_RSA 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 job
00:09
Creating cache /builds/Bob74/test-aws...
/builds/Bob74/test-aws/.terraform/: found 8 matching files and directories
Uploading cache.zip to https://storage.googleapis.com/gitlab-com-runners-cache/project/31197695/builds/Bob74/test-aws
Created cache
Cleaning up project directory and file based variables
00:00
Job 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 automatically
use this backend unless the backend configuration changes.
Content du résultat.