Aller au contenu

Déploiement sur AWS avec Terraform & Ansible

logo terraform

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 :

Terminal window
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.

Terminal window
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 :

Terminal window
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!) :

Terminal window
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 :

gitlab terraform pipeline

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 :

Terminal window
/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_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 est 679593333241. 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 vpc default
  • 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 :

Terminal window
export TF_VAR_SSH_KEY=`cat ~/.ssh/id_ed25519.pub`

Maintenant vous pouvez tester en local votre configuration :

Terminal window
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.

Terminal window
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.

Terminal window
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

Terminal window
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 :

Terminal window
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 :

Terminal window
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_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.

Terminal window
$ 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.