Aller au contenu principal

Déploiement sur AWS avec Terraform & Ansible

· 9 minutes de lecture
Stéphane ROBERT
Consultant DevOps

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 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_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 :

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