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.

Objectif du lab outscale

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

Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...
- Finding outscale/outscale versions matching "0.10.0"...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing outscale/outscale v0.10.0...
- Installed outscale/outscale v0.10.0 (signed by a HashiCorp partner, key ID E88D052B326BFCEE)
- Installing hashicorp/aws v5.33.0...
- Installed hashicorp/aws v5.33.0 (signed by HashiCorp)
...

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

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 :

data.outscale_security_group.sg-ssh-all: Reading...
data.outscale_images.images: Reading...
data.outscale_subnet.public_subnet: Reading...
outscale_keypair.keypair01: Refreshing state... [id=bastion]
outscale_public_ip.my_public_ip: Refreshing state... [id=eipalloc-c155cb7c]
data.outscale_security_group.sg-ssh-all: Read complete after 0s [id=sg-aad0fec2]
data.outscale_images.images: Read complete after 1s [id=terraform-20240124071102547700000001]
data.outscale_subnet.public_subnet: Read complete after 5s [id=subnet-3366f4d8]
outscale_vm.bastion: Refreshing state... [id=i-d76e1412]
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": [
{
"AmiLaunchIndex": 0,
"ImageId": "ami-0dcb71d6",
"InstanceId": "i-d76e1412",
"InstanceType": "tinav5.c2r4p2",
"KernelId": "",
"KeyName": "bastion",
"LaunchTime": "2024-01-22T15:35:25.526000+00:00",
"Monitoring": {
"State": "disabled"
},
"Placement": {
"AvailabilityZone": "eu-west-2a",
"GroupName": "",
"Tenancy": "default"
},
"PrivateDnsName": "ip-192-168-2-113.eu-west-2.compute.internal",
"PrivateIpAddress": "192.168.2.113",
"ProductCodes": [],
"PublicDnsName": "ows-80-247-5-193.eu-west-2.compute.outscale.com",
"PublicIpAddress": "80.247.5.193",
"State": {
"Code": 16,
"Name": "running"
},
"SubnetId": "subnet-3366f4d8",
"VpcId": "vpc-deb92530",
"Architecture": "x86_64",
"BlockDeviceMappings": [
{
"DeviceName": "/dev/sda1",
"Ebs": {
"AttachTime": "2024-01-22T15:35:25.526000+00:00",
"DeleteOnTermination": true,
"Status": "attached",
"VolumeId": "vol-e239ccb8"
}
}
],
"ClientToken": "",
"EbsOptimized": false,
"Hypervisor": "xen",
"NetworkInterfaces": [
{
"Association": {
"IpOwnerId": "300446419788",
"PublicDnsName": "ows-80-247-5-193.eu-west-2.compute.outscale.com",
"PublicIp": "80.247.5.193"
},
"Attachment": {
"AttachTime": "2024-01-22T15:35:25.526000+00:00",
"AttachmentId": "eni-attach-bfdb6b89",
"DeleteOnTermination": true,
"DeviceIndex": 0,
"Status": "attached"
},
"Description": "Primary network interface",
"Groups": [
{
"GroupName": "seg-ssh-all",
"GroupId": "sg-aad0fec2"
}
],
"MacAddress": "aa:cd:f8:8e:3c:35",
"NetworkInterfaceId": "eni-0c43edc9",
"OwnerId": "300446419788",
"PrivateDnsName": "ip-192-168-2-113.eu-west-2.compute.internal",
"PrivateIpAddress": "192.168.2.113",
"PrivateIpAddresses": [
{
"Association": {
"IpOwnerId": "300446419788",
"PublicDnsName": "ows-80-247-5-193.eu-west-2.compute.outscale.com",
"PublicIp": "80.247.5.193"
},
"Primary": true,
"PrivateDnsName": "ip-192-168-2-113.eu-west-2.compute.internal",
"PrivateIpAddress": "192.168.2.113"
}
],
"SourceDestCheck": true,
"Status": "in-use",
"SubnetId": "subnet-3366f4d8",
"VpcId": "vpc-deb92530"
}
],
"RootDeviceName": "/dev/sda1",
"RootDeviceType": "ebs",
"SecurityGroups": [
{
"GroupName": "seg-ssh-all",
"GroupId": "sg-aad0fec2"
}
],
"SourceDestCheck": true,
"Tags": [
{
"Key": "Application",
"Value": "bastion"
},
{
"Key": "Env",
"Value": "robert"
},
{
"Key": "Group",
"Value": "bastions"
},
{
"Key": "Name",
"Value": "bastion"
}
],
"VirtualizationType": "hvm"
}
],
"OwnerId": "300446419788",
"ReservationId": "r-a9e30fe9"
}
]
}

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.

Configuration de l'inventaire dynamique Ansible

Maintenant que les bases de notre infrastructure sont en place, je vais vous présenter comment utiliser Ansible pour automatiser la configuration des instances dans notre environnement cloud. L'aspect le plus intéressant et puissant ici est l'utilisation d'un inventaire dynamique, qui permet à Ansible de s'adapter automatiquement aux changements dans notre infrastructure cloud.

L'inventaire dynamique est une fonctionnalité d'Ansible qui permet de générer automatiquement un inventaire des instances à partir de sources comme AWS, Azure, et dans notre cas, Outscale.

Pour configurer l'inventaire dynamique, nous utilisons le plugin AWS, Outscale étant compatible avec l'API Outscale cela va fonctionner. C'est ce plugin qui se chargera d'obtenir les informations sur les instances.

Créer un nouveau répertoire nommé playbooks pour y déposer vos fichiers Ansible.

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

Créer un fichier yaml dont le nom doit se terminer par .aws_ec2.yml. Ici, je vais l'appeler demo.aws_ec2.yml :

plugin: amazon.aws.aws_ec2
aws_profile: outscale
regions:
- eu-west-2
keyed_groups:
- key: tags.Application
separator: ''
- key: tags.Group
separator: ''
- key: tags.Env
separator: ''
hostnames:
- ip-address
- private-ip-address

Quelques explications :

  • plugin: amazon.aws.aws_ec2 : Indique qu'Ansible utilise le plugin aws_ec2 pour interagir avec AWS EC2.
  • aws_profile: outscale : Spécifie le profil AWS à utiliser pour l'authentification. Cela correspond à un profil défini précédemment.
  • regions: - eu-west-2 : Définit la région AWS où le plugin doit chercher les instances.
  • keyed_groups: Permet de créer des groupes dynamiques basés sur des tags spécifiques. Dans votre cas, il crée des groupes basés sur les tags Application, Group, et Env sans séparateur (separator: '').
  • hostnames: - ip-address - private-ip-address : Définit les attributs à utiliser comme noms d'hôtes dans l'inventaire, ici les adresses IP publiques et privées des instances.

Pour que cela fonctionne, il faut créer un fichier de configuration contenant ces lignes :

[defaults]
inventory = demo.aws_ec2.yml
remote_user = outscale
callbacks_enabled = timer, profile_tasks, profile_roles
vault_password_file = .vault_pass

[inventory]
enable_plugins = aws_ec2

Cela permet de définir l'inventaire à utiliser, ainsi que l'utilisateur distant qui sera chargé d'appliquer les taches Ansible.

Testons-le :

@all:
|--@ungrouped:
|--@aws_ec2:
| |--80.247.5.193
|--@robert:
| |--80.247.5.193
|--@bastion:
| |--80.247.5.193
|--@bastions:
| |--80.247.5.193

Cela fonctionne !

Voici le playbook que je vais lancer dessus qui va configurer une certain nombre de choses :

---
- name: Test Ansible
hosts: aws_ec2
gather_facts: true
roles:
- role: stephrobert.motd
vars:
motd_banner: " {{ tags.Name }} "
pre_tasks:
- name: Upgrade packages
ansible.builtin.apt:
upgrade: true
update_cache: true
become: true
- name: Change Hostname
ansible.builtin.hostname:
name: "{{ hostvars[inventory_hostname].tags.Name }}.{{ hostvars[inventory_hostname].tags.Env }}.local"
tags: name
become: true
- name: Add IP address of all hosts to all hosts | {{ inventory_hostname }}
ansible.builtin.lineinfile:
dest: /etc/hosts
regexp: "^.*{{ hostvars[item].tags.Name }}.{{ hostvars[item].tags.Env }}.local"
line: "{{ (hostvars[item].ansible_all_ipv4_addresses | select('match', '192.168.') | list)[0] }} {{ hostvars[item].tags.Name }}.{{ hostvars[item].tags.Env }}.local {{ hostvars[item].tags.Name }}"
state: present
when: hostvars[item].ansible_nodename is defined
with_items: "{{ groups.aws_ec2 }}"
become: true
- name: Add IP address of all hosts to all hosts | {{ inventory_hostname }}
vars:
ansible_python_interpreter: /usr/bin/python3
ansible.builtin.lineinfile:
dest: /etc/hosts
regexp: "^.*{{ hostvars[item].tags.Name }}.{{ hostvars[item].tags.Env }}.local"
line: "{{ (hostvars[item].ansible_all_ipv4_addresses | select('match', '192.168.') | list)[0] }} {{ hostvars[item].tags.Name }}.{{ hostvars[item].tags.Env }}.local {{ hostvars[item].tags.Name }}"
state: present
when: hostvars[item].ansible_nodename is defined
with_items: "{{ groups.all }}"
delegate_to: localhost
run_once: true
become: true

Ce playbook modifie le message d'accueil, fais la mise à jour des packages, change le nom de la machine et créé des entrées dans le fichier /etc/hosts des machines distantes et locales (je n'ai pas de serveur DNS). Testons son exécution :

ansible-playbook provision.yml --limit bastion

PLAY [Test Ansible] ***

TASK [Gathering Facts] ***
mercredi 24 janvier 2024 08:48:52 +0100 (0:00:00.007) 0:00:00.008 ******
mercredi 24 janvier 2024 08:48:52 +0100 (0:00:00.007) 0:00:00.007 ******
ok: [80.247.5.193]

Playbook run took 0 days, 0 hours, 0 minutes, 38 seconds
mercredi 24 janvier 2024 08:49:31 +0100 (0:00:00.814) 0:00:38.331 ******
==============================================================================
Upgrade packages ------------------------------------------------------- 30.58s
Gathering Facts --------------------------------------------------------- 1.72s
stephrobert.motd : Install Figlet --------------------------------------- 1.12s
stephrobert.motd : Disable Default motd --------------------------------- 0.93s
stephrobert.motd : Copy config to Disable Motd in sshd ------------------ 0.87s
Change Hostname --------------------------------------------------------- 0.85s
stephrobert.motd : Configure New Dynamic Motd in PAM -------------------- 0.82s
stephrobert.motd : Copy new dynamic motd -------------------------------- 0.73s
Add IP address of all hosts to all hosts | 80.247.5.193 ----------------- 0.52s
Add IP address of all hosts to all hosts | 80.247.5.193 ----------------- 0.19s
mercredi 24 janvier 2024 08:49:31 +0100 (0:00:00.815) 0:00:38.330 ******
===============================================================================
ansible.builtin.apt ---------------------------------------------------- 30.58s
stephrobert.motd -------------------------------------------------------- 4.46s
gather_facts ------------------------------------------------------------ 1.72s
ansible.builtin.hostname ------------------------------------------------ 0.85s
ansible.builtin.lineinfile ---------------------------------------------- 0.71s
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
total ------------------------------------------------------------------ 38.32s

On se connecte à notre bastion :

ssh bastion
_ _ _
| |__ __ _ ___| |_(_) ___ _ __
| '_ \ / _` / __| __| |/ _ \| '_ \
| |_) | (_| \__ \ |_| | (_) | | | |
|_.__/ \__,_|___/\__|_|\___/|_| |_|

System info:
Hostname····: bastion.robert.local
Distro······: Ubuntu 22.04.3 LTS
Kernel······: Linux 5.15.0-91-generic
Uptime······: up 24 minutes
Load········: 0.10 (1m), 0.09 (5m), 0.03 (15m)
Processes···: 101 (root), 7 (user), 108 (total)
CPU·········: Intel Xeon Processor (Skylake) (2 vCPU)
Memory······: 149Mi used, 3.4Gi avail, 3.8Gi total

Disk usage:
/ 7% used out of 42G
[===···············································]
/boot/efi 6% used out of 110M
[===···············································]

Last login: Wed Jan 24 07:49:30 2024 from 90.110.185.174
outscale@bastion:~$

Nous jouerons ce playbook à chaque fois que nous créerons une nouvelle instance !

Création d'une instance dans le réseau privé

Après avoir configuré le bastion, je me concentre maintenant sur le sous-réseau privé. C'est ici que notre service web sera hébergé, isolé du trafic Internet direct pour une sécurité accrue. Cette section est essentielle pour assurer que notre service web fonctionne de manière optimale tout en étant protégé.

On crée un nouveau dossier terraform appelé test :

├── playbooks
└── terraform
├── bastion
├── infrastructure
└── test

On procède comme précédemment. Éditez le fichier providers.tf et changer la clé du backend :

terraform {
backend "s3" {
profile = "outscale"
bucket = "lab-terraform"
key = "service/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 = {
}
}
}

On crée notre instance test dans le réseau privé avec ce code :

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

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

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

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

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

On peut appliquer :

terraform apply
...

Changes to Outputs:
+ public_name = (known after apply)
outscale_keypair.keypair01: Creating...
outscale_keypair.keypair01: Creation complete after 1s [id=test]
outscale_vm.test: Creating...
outscale_vm.test: Still creating... [10s elapsed]
outscale_vm.test: Still creating... [20s elapsed]
outscale_vm.test: Creation complete after 28s [id=i-04ed2427]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

On applique notre playbook Ansible :

cd ../../playbooks

ansible-playbook provision.yml --limit test

On teste la connexion :

ssh 192.168.3.200

_ _
| |_ ___ ___| |_
| __/ _ \/ __| __|
| || __/\__ \ |_
\__\___||___/\__|

System info:
Hostname····: test.robert.local
Distro······: Ubuntu 22.04.3 LTS
Kernel······: Linux 5.15.0-91-generic
Uptime······: up 11 minutes
Load········: 0.09 (1m), 0.19 (5m), 0.10 (15m)
Processes···: 86 (root), 7 (user), 93 (total)
CPU·········: Intel Xeon Processor (Skylake) (1 vCPU)
Memory······: 167Mi used, 634Mi avail, 957Mi total

Disk usage:
/ 7% used out of 42G
[===···············································]
/boot/efi 6% used out of 110M
[===···············································]

Last login: Wed Jan 24 08:32:07 2024 from 192.168.2.113

On installe nginx :

sudo apt install -y nginx

On teste que le port 80 est bien démarré :

ss -tlnp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:*
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 511 [::]:80 [::]:*
LISTEN 0 128 [::]:22 [::]:*

Création d'un load balancer

Un load balancer distribue le trafic réseau entrant entre plusieurs machines virtuelles (VM) du Cloud public ou d’un réseau. Les VM enregistrées auprès d’un load balancer sont appelées des VM back-ends. Vous pouvez enregistrer autant de VM back-ends que nécessaire auprès d’un load balancer, et vous pouvez enregistrer ou retirer des VM back-ends à tout moment selon vos besoins.

En fonction de ses security groups, une VM back-end peut recevoir soit le trafic venant uniquement du load balancer soit le trafic venant à la fois du load balancer et d’une autre source (par exemple un autre load balancer, internet, ou autres).

Le load balancer vérifie la santé des VM back-ends pour déterminer les VM saines vers lesquelles il peut distribuer du trafic.

Ici, je vais utiliser un load balancer de type internet. Dans le fichier main.tf du répertoire test ajoutez ces lignes :

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

resource "outscale_load_balancer" "load_balancer" {
load_balancer_name = "test-balancer"
listeners {
backend_port = 80
backend_protocol = "TCP"
load_balancer_protocol = "TCP"
load_balancer_port = 80
}
# subregion_names = ["eu-west-2a"]
security_groups = [data.outscale_security_group.sg-all-all.security_group_id]
load_balancer_type = "internet-facing"
subnets = [data.outscale_subnet.public_subnet.subnet_id]
tags {
key = "name"
value = "test"
}
}

resource "outscale_load_balancer_vms" "outscale_load_balancer_test" {
load_balancer_name = outscale_load_balancer.load_balancer.load_balancer_name
backend_vm_ids = [outscale_vm.test.id]
}

Dans le fichier output.tf ajoutez ces lignes :

output "public_name" {
value = outscale_load_balancer.load_balancer.dns_name
}

Vous remarquez que je déploie le load balancer dans le sous-réseau public et je le lie à l'instance test qui se trouve dans le sous réseau privé !

On le déploie :

terraform apply -auto-approve
...

public_name = "test-balancer-964931616.eu-west-2.lbu.outscale.com"

On peut vérifier l'état du load balancer avec la CLI d'AWS :

aws elb describe-instance-health --load-balancer-name test-balancer --instance i-04ed2427 --endpoint https://lbu.eu-west-2.outscale.com

{
"InstanceStates": [
{
"InstanceId": "i-04ed2427",
"State": "InService",
"ReasonCode": "N/A",
"Description": "N/A"
}
]
}

Il est bien en service. Si ce n'est pas le cas vérifier que depuis le réseau public, vous pouvez atteindre le port 80 de l'instance test avec la commande netcat ou un simple.

On teste depuis l'accès depuis internet :

curl http://test-balancer-964931616.eu-west-2.lbu.outscale.com

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

Cela fonctionne 👍

Faire le ménage

Dans chaque répertoire Terraform, en commençant par l'instance test, puis l'instance bastion et pour finir l'infrastructure :

terraform destroy -auto-approve

Conclusion

En conclusion, la création d'une infrastructure réseau sur le cloud Outscale en utilisant Terraform, Ansible et Packer est une démarche complexe, mais enrichissante. Nous avons parcouru les étapes de configuration de l'environnement, création du VPC et des sous-réseaux, mise en place du sous-réseau public et privé, intégration du Load Balancer, et l'automatisation avec Ansible. Nous avons également abordé les bonnes pratiques et les techniques de dépannage et de restauration.

Ce projet illustre l'importance d'une approche structurée et réfléchie dans la gestion d'infrastructures cloud. Les compétences et outils que nous avons utilisés sont essentiels pour tout professionnel DevOps souhaitant évoluer dans l'environnement cloud moderne.

Je vous encourage à explorer davantage ces outils et à les adapter à vos propres besoins en infrastructure. Le potentiel d'apprentissage et d'optimisation est immense dans le domaine du cloud computing.

important

Ce lab n'est pas une solution exploitable en l'état, cela demande encore pas mal de travail d'industrialisation de l'ensemble. On peut imaginer la création de modules Terraform pour éviter de dupliquer le code, d'une collection Ansible pour prendre en charge les days-2, à créer des images optimisées avec Packer, à améliorer la sécurité, à utiliser d'autres objets cloud fournis par Outscale, ...