Aller au contenu principal

Création d'une Infrastructure avec Terraform, Ansible et Packer

Aujourd'hui, je vais partager avec vous un projet passionnant : la création d'une infrastructure sur le cloud Outscale. Ce projet utilise des outils d'infra-as-code tels que Terraform, Ansible et Packer. Ces outils ne sont pas seulement puissants, mais aussi essentiels dans le monde du DevOps pour automatiser et optimiser la gestion des infrastructures cloud.

Objectif du Lab

L'objectif ici est de construire un réseau composé d'un VPC (Virtual Private Cloud) qui contient deux sous-réseaux, un public et un privé. Dans le sous-réseau public, nous intégrerons un bastion exposé avec une IP Publique et une combinaison de NAT (Network Address Translation) Gateway et Internet Gateway pour gérer le trafic. Le sous-réseau privé, quant à lui, hébergera un service web, exposé sur internet via un load balancer. Pour sécuriser le tout, je ferais appel aux Security Group.

Configuration de l'Environnement de Travail

Après avoir introduit les objectifs de mon projet, il est essentiel de préparer correctement notre environnement de travail. Pour cela, je vais vous montrer comment configurer votre environnement en utilisant asdf, direnv.

asdf-vm est un gestionnaire de versions pour de multiples langages et outils DevOps. Il permet de gérer facilement les versions de divers outils et langages, tels que Python, Ansible, Terraform et Packer, pour n'en nommer que quelques-uns.

direnv est un outil d'automatisation d'environnement. Il charge et décharge les variables d'environnement selon le répertoire dans lequel vous vous trouvez. C'est particulièrement utile pour gérer des configurations spécifiques à de multiples projets.

Commençons par créer le répertoire de l'environnement :

mkdir -p ~/Projets/perso/lab
cd $_

Installation de direnv :

asdf direnv setup --shell zsh --version 2.32.1
cat <<EOF> .envrc
use asdf
EOF

Création d'un fichier .envrc dans votre projet :

pyenv install 3.11.6
echo 'layout pyenv 3.11.6' >> .envrc
direnv allow
direnv install

Installation des outils

La CLI AWS est essentielle pour interagir avec les services AWS. Grâce à asdf, nous pouvons gérer sa version plus efficacement.

Installation de la CLI AWS :

asdf plugin-add awscli
asdf install awscli 2.15.11
asdf local awscli 2.15.11

Vous devriez retrouver un fichier .tool-versions contenant ceci :

awscli 2.15.11

Ajoutons-y Terraform 1.5.7 et Packer 1.8.7 :

awscli 2.15.11
terraform 1.5.7
packer 1.8.7

Configuration de la CLI AWS pour gérer le cloud OutScale :

aws configure

Saisissez vos Access et Secret Key que vous aurez au préalable créé dans le Cockpit Outscale. Pour la région, j'ai utilisé eu-west-2. Éditez le fichier .aws/config et ajouter cette ligne dans la section endpoint_url = https://fcu.eu-west-2.outscale.com en indiquant bien cette région.

Un petit test :

aws ec2 describe-images --profile default

{
    "Images": [
        {
            "Architecture": "x86_64",
            "ImageId": "ami-7eb4b7be",
            "ImageLocation": "896571521098/Gentoo (20180223)",
            "ImageType": "machine",
            "Public": true,
            "OwnerId": "896571521098",
            "Platform": "",

...

Si vous utilisez plusieurs profils, ce que je fais en créant un profil par projet, remplacez défaut par le vôtre !

aws ec2 describe-images --profile outscale

Configuration de Terraform

Comme je vais découper mon projet en sous partie, je crée cette arborescence :

mkdir -p terraform/infrastructure
cd $_

Créez un fichier provider.tf avec ce contenu :

terraform {
  backend "s3" {
    profile = "outscale"
    bucket  = "lab-terraform"
    key     = "infrastructure/terraform.state"
    region  = "eu-west-2"
    endpoint = "https://oos.eu-west-2.outscale.com"
    skip_credentials_validation = true
    skip_region_validation      = true
  }
  required_providers {
    outscale = {
      source  = "outscale/outscale"
      version = "0.10.0"
    }
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "outscale" {
  access_key_id = var.access_key_id
  secret_key_id = var.secret_key_id
  region        = var.region
}

Créez aussi un fichier de variables variables.tf :

variable "region" {
  default = "eu-west-2"
  type = string
  description = "The outscale region"
}

variable "access_key_id" {
  type = string
}

variable "secret_key_id" {
  type = string
}

Avant de lancer l'initialisation, créons le backend S3 avec la CLI AWS :

aws s3 mb s3://lab-terraform --profile outscale --endpoint https://oos.eu-west-2.outscale.com --region eu-west-2

make_bucket: lab-terraform

Il faut aussi ajouter les variables suivantes dans le fichier .envrc et lancer la commande direnv allow :

export AWS_PROFILE="outscale"
export OUTSCALE_ACCESSKEYID="xxxxxxxx"
export OUTSCALE_SECRETKEYID="xxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

export TF_VAR_access_key_id="${OUTSCALE_ACCESSKEYID}"
export TF_VAR_secret_key_id="${OUTSCALE_SECRETKEYID}"

On peut lancer l'initialisation de notre code Terraform :

❯ terraform init

Initializing the backend...

...

Terraform has been successfully initialized!

Création du VPC et Sous-réseaux

Après avoir configuré notre environnement de travail et Terraform, il est temps de plonger dans le cœur du sujet : la création du VPC (Virtual Private Cloud) et des sous-réseaux sur OutScale.

Création du VPC

Je commence par définir notre VPC sur OutScale. Un VPC est un réseau virtuel dédié à notre environnement cloud. Il va fournir le cadre dans lequel nos sous-réseaux et ressources cloud vont interagir.

Créons un fichier networks.tf et y définissons notre VPC :

resource "outscale_net" "net_steph" {
  ip_range = "192.168.0.0/16"
  tags {
    key   = "name"
    value = "net_steph"
  }
}

Ajoutons l'Internet Gateway à notre VPC, qui va connecter notre réseau à internet :

resource "outscale_internet_service" "internet_gateway" {
}

resource "outscale_internet_service_link" "internet_gateway_link" {
  internet_service_id = outscale_internet_service.internet_gateway.internet_service_id
  net_id              = outscale_net.net_steph.net_id
}

Exécutez terraform apply pour créer le VPC sur OutScale.

Configuration des Sous-réseaux

Maintenant, créons deux sous-réseaux : un public et un privé. Le sous-réseau public accueillera notre bastion et notre NAT Gateway, tandis que le sous-réseau privé hébergera des applications.

Ajoutons la configuration des sous-réseaux dans networks.tf :

resource "outscale_subnet" "public_subnet" {
  subregion_name = "${var.region}a"
  ip_range       = "192.168.2.0/24"
  net_id         = outscale_net.net_steph.net_id
  tags {
    key   = "name"
    value = "public_subnet"
  }
}

resource "outscale_subnet" "private_subnet" {
  subregion_name = "${var.region}a"
  ip_range       = "192.168.3.0/24"
  net_id         = outscale_net.net_steph.net_id
  tags {
    key   = "name"
    value = "private_subnet"
  }
}

Exécutez terraform apply pour créer les sous-réseaux dans notre VPC.

Mise en Place du Sous-réseau Public

Dans cette partie, je vais vous expliquer comment configurer le sous-réseau public de notre VPC sur OutScale. Ce sous-réseau joue un rôle important, car il contiendra notre bastion ssh et les composants essentiels pour la connectivité Internet, comme la NAT Gateway et l'Internet Gateway.

La NAT Gateway est nécessaire pour permettre aux ressources du sous-réseau privé d'accéder à Internet de manière contrôlée et pour permettre le trafic entrant vers le sous-réseau public.

Déployons la NAT Gateway dans le sous-réseau public :

resource "outscale_public_ip" "public_nat_gateway_ip" {}

resource "outscale_nat_service" "public_nat_gateway" {
  subnet_id    = outscale_subnet.public_subnet.subnet_id
  public_ip_id = outscale_public_ip.public_nat_gateway_ip.public_ip_id
  tags {
    key   = "name"
    value = "public_nat_gateway"
  }
}

Pour que notre réseau fonctionne, il faut ajouter des règles appelées routes qui indiquent où le trafic réseau est dirigé. Elles sont créées pour un Net et sont utilisées par un routeur, qui est automatiquement créé au sein de votre Net, afin de déterminer comment router le trafic.

Chaque Subnet d’un Net doit être associé à une route table, qui contrôle le routage pour toutes les machines virtuelles (VM) placées dans ce Subnet. Une même route table peut être associée à plusieurs Subnets, mais un Subnet ne peut être associé qu’à une seule route table. Plus d'infos sur les routes table

Nous devons aussi ajouter une route avec le bloc CIDR 0.0.0.0/0 en destination et l’ID de la NAT Gateway en destination.

Dans le fichier networks.tf, ajoutez ces lignes :

resource "outscale_route_table" "private_route_table" {
  net_id = outscale_net.net_steph.net_id
  tags {
    key   = "name"
    value = "private_route_table"
  }
}

resource "outscale_route_table" "public_route_table" {
  net_id = outscale_net.net_steph.net_id
  tags {
    key   = "name"
    value = "public_route_table"
  }
}

resource "outscale_route" "route-IGW" {
  destination_ip_range = "0.0.0.0/0"
  gateway_id           = outscale_internet_service.internet_gateway.internet_service_id
  route_table_id       = outscale_route_table.public_route_table.route_table_id
}

resource "outscale_route" "to_nat_gateway" {
  nat_service_id       = outscale_nat_service.public_nat_gateway.nat_service_id
  destination_ip_range = "0.0.0.0/0"
  route_table_id       = outscale_route_table.private_route_table.route_table_id
}

resource "outscale_route_table_link" "private_subnet_private_route_table_link" {
  subnet_id      = outscale_subnet.private_subnet.subnet_id
  route_table_id = outscale_route_table.private_route_table.route_table_id
}

resource "outscale_route_table_link" "public_subnet_public_route_table_link" {
  subnet_id      = outscale_subnet.public_subnet.subnet_id
  route_table_id = outscale_route_table.public_route_table.route_table_id
}

Mise en place des Security Group

Un security group est un appareil réseau virtuel agissant comme un pare-feu et un switch, qui autorise ou interdit les flux entrants ou sortants d’une ou plusieurs VM. Ils permettent donc aux VM de communiquer entre elles ou avec des services ou appareils externes selon les règles que vous spécifiez.

Les security groups sont soit alloués pour le Cloud public, soit pour un Net que vous spécifiez. Ici, je vais en créer deux générales pour mon VPC :

resource "outscale_security_group" "sg-ssh-all" {
  description         = "Permit SSH from All"
  security_group_name = "seg-ssh-all"
  net_id              = outscale_net.net_steph.net_id
}

resource "outscale_security_group" "sg-all-all" {
  description         = "Permit All from All"
  security_group_name = "seg-all-all"
  net_id              = outscale_net.net_steph.net_id
}

Ajoutons les règles qui définissent quels flux entrants, Inbound, sont autorisés à atteindre les VM et quels flux sortants, Outbound, sont autorisés à les quitter. Je vais créer ici que des règles entrantes. Dans un premier temps, je crée une première règle qui va autoriser toutes les IP à utiliser le flux ssh. Par la suite, je limiterai aux seuls IP connues.

Je créé une seconde règle qui autorisera toutes les IP à utiliser les flux http, https et ssl.

Dans le fichier networks.tf, ajoutez ces lignes :

resource "outscale_security_group_rule" "security_group_rule01" {
  flow              = "Inbound"
  security_group_id = outscale_security_group.sg-ssh-all.id
  from_port_range   = "22"
  to_port_range     = "22"
  ip_protocol       = "tcp"
  ip_range          = "0.0.0.0/0"
}

resource "outscale_security_group_rule" "security_group_rule02" {
  flow              = "Inbound"
  security_group_id = outscale_security_group.sg-all-all.id

  ip_protocol = "-1"
  ip_range    = "0.0.0.0/0"
}

resource "outscale_security_group_rule" "security_group_rule03" {
  flow              = "Inbound"
  security_group_id = outscale_security_group.sg-all-all.id
  from_port_range   = "80"
  to_port_range     = "80"
  ip_protocol       = "tcp"
  ip_range          = "0.0.0.0/0"
}

resource "outscale_security_group_rule" "security_group_rule04" {
  flow              = "Inbound"
  security_group_id = outscale_security_group.sg-all-all.id
  from_port_range   = "443"
  to_port_range     = "443"
  ip_protocol       = "tcp"
  ip_range          = "0.0.0.0/0"
}

Exécutez terraform apply pour créer tous ces éléments de réseau.

Installation et Configuration du Bastion

Le bastion agit comme un point d'accès sécurisé pour la gestion de notre infrastructure cloud.

Initialisation

Pour ne pas créer qu'une seule stack Terraform, je crée un nouveau dossier nommé bastion, ce qui donne :

.
└── terraform
   ├── bastion
   └── infrastructure

Je recopie les fichiers providers.tf et variables.tf de la stack infra. Éditez-les et remplacer la key du backend S3 pour ne pas écraser celle du réseau :

terraform {
  backend "s3" {
    profile = "outscale"
    bucket  = "lab-terraform"
    key     = "bastion/terraform.state"
    region  = "eu-west-2"
    endpoint = "https://oos.eu-west-2.outscale.com"
    skip_credentials_validation = true
    skip_region_validation      = true
  }
  required_providers {
    outscale = {
      source  = "outscale/outscale"
      version = "0.10.0"
    }
    aws = {
    }
  }
}

Ajoutons une instance compute dans notre sous-réseau public qui va servir de bastion, mais pour cela, nous allons avoir besoin de retrouver plusieurs informations comme l'id de l'image, l'id du security group, l'id du subnet. Pour cela, nous utilisons les datasources de Terraform :

data "outscale_images" "images" {
  filter {
      name   = "image_names"
      values = ["ubuntu_2204_steph"]
  }
}

data "outscale_subnet" "public_subnet" {
  filter {
    name   = "tag_values"
    values = ["public_subnet"]
  }
}

data "outscale_security_group" "sg-ssh-all" {
  security_group_name = "seg-ssh-all"
}

Vous remarquez que j'utilise une OMI personnalisée que j'ai créé au préalable avec Packer. J'utilise la règle ssh pour n'autorisez que le flux du même nom.

J'ai besoin aussi d'une clé ssh qui me permettra d'accéder à notre VM :

resource "outscale_keypair" "keypair01" {
  keypair_name = "bastion"
  public_key   = file("~/.ssh/id_ed25519.pub")
}

Mais aussi d'une IP publique :

resource "outscale_public_ip" "my_public_ip" {
}

resource "outscale_public_ip_link" "my_public_ip_link" {
  vm_id     = outscale_vm.bastion.vm_id
  public_ip = outscale_public_ip.my_public_ip.public_ip
}

Nous avons toutes les informations nécessaires à la création de la VM :

resource "outscale_vm" "bastion" {
  image_id           = data.outscale_images.images.images[0].image_id
  vm_type            = "tinav5.c1r2p2"
  keypair_name       = outscale_keypair.keypair01.keypair_name
  security_group_ids = [data.outscale_security_group.sg-ssh-all.security_group_id]
  subnet_id          = data.outscale_subnet.public_subnet.id
  state              = "running"
  tags {
    key   = "Name"
    value = "bastion"
  }
  tags {
    key   = "Application"
    value = "bastion"
  }
  tags {
    key   = "Group"
    value = "bastions"
  }
  tags {
    key   = "Env"
    value = "robert"
  }
}

Pour récupérer l'adresse IP publique créons un fichier output.tf dans lequel vous ajouterez ces lignes :

output "public_ip" {
  value = outscale_vm.bastion.public_ip
}

Exécutez terraform apply pour créer notre première instance. L'IP ne s'affichera pas la première fois, vous pouvez la récupérer en lançant la commande terraform refresh :

...
outscale_public_ip_link.my_public_ip_link: Refreshing state... [id=eipassoc-bbc51e90]

Outputs:

public_ip = "80.247.5.193"

On va pouvoir se connecter.

Configuration du Jump vers le réseau privé

Pour nous connecter aux futures instances présentes dans notre réseau privé, nous devrons passer par le bastion. Écrivons tout de suite la configuration SSH. Éditez le fichier ~/.ssh/config et ajoutez ces lignes :

Host bastion.robert.local bastion
  HostName 80.247.5.193
  User outscale
  ForwardAgent yes

Host 192.168.3.* *.robert.local !bastion.robert.local !bastion
  User outscale
  ProxyJump bastion

Comme vous le remarquez, je vais utiliser l'agent SSH pour me connecter. Donc dans votre fichier .bashrc ajoutez ces lignes :

eval $(ssh-agent)
ssh-add ~/.ssh/id_ed25519

Je charge la clé ssh défini dans la keypair.

Pour vérifier, que notre instance est démarrée, utilisons la cli AWS :

aws ec2 describe-instances --filters Name=tag:Name,Values=bastion

{
    "Reservations": [
        {
            "Groups": [
                {
                    "GroupName": "seg-ssh-all",
                    "GroupId": "sg-aad0fec2"
                }
            ],
            "Instances": [
                {
...
                    "State": {
                        "Code": 16,
                        "Name": "running"
                    },
...
        }
    ]
}

J'utilise un filtre sur le tag Name. Son status est running, on peut donc se connecter dessus. Mais avant, nous allons la configurer avec Ansible en utilisant l'inventaire dynamique AWS.

La suite

Retrouvez la suite de ce guide dans une deuxième partie