Module 6 : Patterns & Modules Terraform
Objectifs du Module
À la fin de ce module, vous serez capable de :
- Organiser un projet Terraform ACI
- Créer des modules réutilisables
- Utiliser variables, locals et outputs efficacement
- Exploiter for_each et count pour le DRY
- Gérer les workspaces pour multi-environnements
- Configurer un backend remote pour le state
Durée estimée : 3 heures
Organisation d'un Projet
Structure Recommandée
aci-infrastructure/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── terraform.tfvars
│ │ └── backend.tf
│ ├── staging/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── terraform.tfvars
│ │ └── backend.tf
│ └── prod/
│ ├── main.tf
│ ├── variables.tf
│ ├── terraform.tfvars
│ └── backend.tf
├── modules/
│ ├── tenant/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── epg/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── contract/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── shared/
│ └── filters.tf
└── README.md
Convention de Nommage
| Élément | Convention | Exemple |
|---|---|---|
| Fichiers | lowercase, tirets | bridge-domain.tf |
| Ressources | snake_case | aci_tenant.prod_main |
| Variables | snake_case | tenant_name |
| Modules | lowercase, tirets | modules/aci-tenant |
| Outputs | snake_case | tenant_dn |
Variables et Locals
Variables Typées
# variables.tf
variable "tenant_name" {
description = "Nom du Tenant ACI"
type = string
validation {
condition = can(regex("^[a-zA-Z][a-zA-Z0-9_-]{0,63}$", var.tenant_name))
error_message = "Le nom du Tenant doit commencer par une lettre et contenir max 64 caractères."
}
}
variable "environment" {
description = "Environnement (dev, staging, prod)"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "L'environnement doit être: dev, staging ou prod."
}
}
variable "vrfs" {
description = "Liste des VRFs à créer"
type = list(object({
name = string
description = string
enforced = optional(bool, true)
}))
default = []
}
variable "epgs" {
description = "Configuration des EPGs"
type = map(object({
name = string
bd_name = string
description = optional(string, "")
contracts = object({
consumed = optional(list(string), [])
provided = optional(list(string), [])
})
}))
}
Locals pour Calculs
# locals.tf
locals {
# Préfixe basé sur l'environnement
prefix = "${var.tenant_name}-${var.environment}"
# Tags communs
common_tags = {
Environment = var.environment
ManagedBy = "terraform"
Project = var.tenant_name
}
# Annotation standard
annotation = "orchestrator:terraform,env:${var.environment}"
# Calcul dynamique des subnets
subnet_configs = {
for name, epg in var.epgs : name => {
subnet = cidrsubnet(var.base_cidr, 8, index(keys(var.epgs), name))
gateway = cidrhost(cidrsubnet(var.base_cidr, 8, index(keys(var.epgs), name)), 1)
}
}
# Liste plate de tous les contracts
all_contracts = distinct(flatten([
for epg in var.epgs : concat(
epg.contracts.consumed,
epg.contracts.provided
)
]))
}
Fichier tfvars
# terraform.tfvars
tenant_name = "Worldline-Payment"
environment = "prod"
vrfs = [
{
name = "Production"
description = "VRF Production PCI-DSS"
enforced = true
},
{
name = "Management"
description = "VRF Management réseau"
enforced = true
}
]
epgs = {
frontend = {
name = "Frontend"
bd_name = "BD-Web"
description = "Serveurs web frontend"
contracts = {
consumed = ["web-to-app"]
provided = ["inet-to-web"]
}
}
api = {
name = "API"
bd_name = "BD-App"
description = "API backend"
contracts = {
consumed = ["app-to-db"]
provided = ["web-to-app"]
}
}
database = {
name = "Database"
bd_name = "BD-Data"
description = "PostgreSQL servers"
contracts = {
consumed = []
provided = ["app-to-db"]
}
}
}
For_each et Count
For_each avec Map
# Créer plusieurs EPGs à partir d'une map
resource "aci_application_epg" "epgs" {
for_each = var.epgs
application_profile_dn = aci_application_profile.main.id
name = each.value.name
description = each.value.description
relation_fv_rs_bd = aci_bridge_domain.bds[each.value.bd_name].id
annotation = local.annotation
}
# Créer les Bridge Domains correspondants
resource "aci_bridge_domain" "bds" {
for_each = toset([for epg in var.epgs : epg.bd_name])
tenant_dn = aci_tenant.main.id
name = each.value
relation_fv_rs_ctx = aci_vrf.production.id
annotation = local.annotation
}
For_each avec Nested Loops
# Créer les associations EPG → Contract Consumer
locals {
epg_contract_consumers = flatten([
for epg_key, epg in var.epgs : [
for contract in epg.contracts.consumed : {
epg_key = epg_key
contract_name = contract
}
]
])
}
resource "aci_epg_to_contract" "consumers" {
for_each = {
for item in local.epg_contract_consumers :
"${item.epg_key}-${item.contract_name}" => item
}
application_epg_dn = aci_application_epg.epgs[each.value.epg_key].id
contract_dn = aci_contract.contracts[each.value.contract_name].id
contract_type = "consumer"
}
Count pour Ressources Conditionnelles
# Créer un L3Out seulement en production
resource "aci_l3_outside" "internet" {
count = var.environment == "prod" ? 1 : 0
tenant_dn = aci_tenant.main.id
name = "L3Out-Internet"
# ...
}
# Référencer avec count
resource "aci_external_network_instance_profile" "external" {
count = var.environment == "prod" ? 1 : 0
l3_outside_dn = aci_l3_outside.internet[0].id
name = "External-Internet"
}
Création de Modules
Module : Tenant Complet
# modules/tenant/variables.tf
variable "name" {
description = "Nom du Tenant"
type = string
}
variable "description" {
description = "Description du Tenant"
type = string
default = ""
}
variable "vrfs" {
description = "VRFs à créer dans le Tenant"
type = list(object({
name = string
description = optional(string, "")
enforced = optional(bool, true)
}))
default = []
}
variable "annotation" {
description = "Annotation pour traçabilité"
type = string
default = "orchestrator:terraform"
}
# modules/tenant/main.tf
resource "aci_tenant" "this" {
name = var.name
description = var.description
annotation = var.annotation
}
resource "aci_vrf" "vrfs" {
for_each = { for vrf in var.vrfs : vrf.name => vrf }
tenant_dn = aci_tenant.this.id
name = each.value.name
description = each.value.description
pc_enf_pref = each.value.enforced ? "enforced" : "unenforced"
annotation = var.annotation
}
# modules/tenant/outputs.tf
output "tenant_dn" {
description = "DN du Tenant créé"
value = aci_tenant.this.id
}
output "tenant_name" {
description = "Nom du Tenant"
value = aci_tenant.this.name
}
output "vrf_dns" {
description = "Map des DNs des VRFs"
value = { for k, v in aci_vrf.vrfs : k => v.id }
}
Module : EPG avec Contracts
# modules/epg/variables.tf
variable "tenant_dn" {
description = "DN du Tenant parent"
type = string
}
variable "application_profile_dn" {
description = "DN de l'Application Profile"
type = string
}
variable "name" {
description = "Nom de l'EPG"
type = string
}
variable "bridge_domain_dn" {
description = "DN du Bridge Domain"
type = string
}
variable "vmm_domain_dn" {
description = "DN du VMM Domain (optionnel)"
type = string
default = null
}
variable "contracts_consumed" {
description = "Liste des DNs de Contracts consommés"
type = list(string)
default = []
}
variable "contracts_provided" {
description = "Liste des DNs de Contracts fournis"
type = list(string)
default = []
}
# modules/epg/main.tf
resource "aci_application_epg" "this" {
application_profile_dn = var.application_profile_dn
name = var.name
relation_fv_rs_bd = var.bridge_domain_dn
annotation = "orchestrator:terraform"
}
# Association VMM Domain si fourni
resource "aci_epg_to_domain" "vmm" {
count = var.vmm_domain_dn != null ? 1 : 0
application_epg_dn = aci_application_epg.this.id
tdn = var.vmm_domain_dn
instr_imedcy = "immediate"
res_imedcy = "immediate"
}
# Contracts consommés
resource "aci_epg_to_contract" "consumed" {
for_each = toset(var.contracts_consumed)
application_epg_dn = aci_application_epg.this.id
contract_dn = each.value
contract_type = "consumer"
}
# Contracts fournis
resource "aci_epg_to_contract" "provided" {
for_each = toset(var.contracts_provided)
application_epg_dn = aci_application_epg.this.id
contract_dn = each.value
contract_type = "provider"
}
# modules/epg/outputs.tf
output "epg_dn" {
description = "DN de l'EPG créé"
value = aci_application_epg.this.id
}
output "epg_name" {
description = "Nom de l'EPG"
value = aci_application_epg.this.name
}
Utiliser les Modules
# environments/prod/main.tf
module "tenant" {
source = "../../modules/tenant"
name = "Production"
description = "Tenant de production"
vrfs = [
{
name = "Prod-VRF"
description = "VRF principale"
enforced = true
}
]
}
module "frontend_epg" {
source = "../../modules/epg"
tenant_dn = module.tenant.tenant_dn
application_profile_dn = aci_application_profile.main.id
name = "Frontend"
bridge_domain_dn = aci_bridge_domain.web.id
vmm_domain_dn = data.aci_vmm_domain.vcenter.id
contracts_consumed = [aci_contract.web_to_app.id]
contracts_provided = [aci_contract.inet_to_web.id]
}
module "backend_epg" {
source = "../../modules/epg"
tenant_dn = module.tenant.tenant_dn
application_profile_dn = aci_application_profile.main.id
name = "Backend"
bridge_domain_dn = aci_bridge_domain.app.id
vmm_domain_dn = data.aci_vmm_domain.vcenter.id
contracts_consumed = [aci_contract.app_to_db.id]
contracts_provided = [aci_contract.web_to_app.id]
}
Workspaces
Gérer Multi-Environnements
# Créer des workspaces
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod
# Lister les workspaces
terraform workspace list
default
* dev
staging
prod
# Changer de workspace
terraform workspace select prod
Variables par Workspace
# variables.tf
locals {
# Configuration par environnement
env_config = {
dev = {
tenant_prefix = "Dev"
vrf_enforced = false
create_l3out = false
subnet_base = "10.10.0.0/16"
}
staging = {
tenant_prefix = "Staging"
vrf_enforced = true
create_l3out = false
subnet_base = "10.20.0.0/16"
}
prod = {
tenant_prefix = "Prod"
vrf_enforced = true
create_l3out = true
subnet_base = "10.30.0.0/16"
}
}
# Configuration courante basée sur le workspace
current_config = local.env_config[terraform.workspace]
}
# Utilisation
resource "aci_tenant" "main" {
name = "${local.current_config.tenant_prefix}-${var.project_name}"
}
resource "aci_vrf" "main" {
tenant_dn = aci_tenant.main.id
name = "Main-VRF"
pc_enf_pref = local.current_config.vrf_enforced ? "enforced" : "unenforced"
}
Backend Remote
Configuration S3 + DynamoDB (AWS)
# backend.tf
terraform {
backend "s3" {
bucket = "worldline-terraform-state"
key = "aci/production/terraform.tfstate"
region = "eu-west-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
Configuration Azure Blob
# backend.tf
terraform {
backend "azurerm" {
resource_group_name = "rg-terraform-state"
storage_account_name = "wlterraformstate"
container_name = "tfstate"
key = "aci/prod.terraform.tfstate"
}
}
Configuration GitLab CI avec HTTP Backend
# backend.tf
terraform {
backend "http" {
address = "https://gitlab.example.com/api/v4/projects/123/terraform/state/aci-prod"
lock_address = "https://gitlab.example.com/api/v4/projects/123/terraform/state/aci-prod/lock"
unlock_address = "https://gitlab.example.com/api/v4/projects/123/terraform/state/aci-prod/lock"
lock_method = "POST"
unlock_method = "DELETE"
retry_wait_min = 5
}
}
Patterns Avancés
Pattern : Application Stack
Créer une application complète avec un seul module :
# modules/application-stack/variables.tf
variable "tenant_dn" {
type = string
}
variable "name" {
description = "Nom de l'application"
type = string
}
variable "tiers" {
description = "Tiers de l'application"
type = map(object({
subnet = string
ports = list(number)
depends_on = optional(list(string), [])
}))
}
# modules/application-stack/main.tf
# Application Profile
resource "aci_application_profile" "this" {
tenant_dn = var.tenant_dn
name = var.name
annotation = "orchestrator:terraform"
}
# Bridge Domains pour chaque tier
resource "aci_bridge_domain" "tiers" {
for_each = var.tiers
tenant_dn = var.tenant_dn
name = "BD-${var.name}-${each.key}"
relation_fv_rs_ctx = var.vrf_dn
}
# Subnets
resource "aci_subnet" "tiers" {
for_each = var.tiers
parent_dn = aci_bridge_domain.tiers[each.key].id
ip = each.value.subnet
scope = ["public"]
}
# EPGs
resource "aci_application_epg" "tiers" {
for_each = var.tiers
application_profile_dn = aci_application_profile.this.id
name = each.key
relation_fv_rs_bd = aci_bridge_domain.tiers[each.key].id
}
# Filters dynamiques
resource "aci_filter" "tiers" {
for_each = var.tiers
tenant_dn = var.tenant_dn
name = "filter-${var.name}-${each.key}"
}
resource "aci_filter_entry" "ports" {
for_each = {
for item in flatten([
for tier_name, tier in var.tiers : [
for port in tier.ports : {
tier = tier_name
port = port
}
]
]) : "${item.tier}-${item.port}" => item
}
filter_dn = aci_filter.tiers[each.value.tier].id
name = "port-${each.value.port}"
ether_t = "ipv4"
prot = "tcp"
d_from_port = tostring(each.value.port)
d_to_port = tostring(each.value.port)
stateful = "yes"
}
# Contracts basés sur les dépendances
resource "aci_contract" "tier_contracts" {
for_each = {
for item in flatten([
for tier_name, tier in var.tiers : [
for dep in tier.depends_on : {
consumer = tier_name
provider = dep
}
]
]) : "${item.consumer}-to-${item.provider}" => item
}
tenant_dn = var.tenant_dn
name = "${each.value.consumer}-to-${each.value.provider}"
scope = "context"
}
Utilisation :
module "ecommerce" {
source = "./modules/application-stack"
tenant_dn = aci_tenant.prod.id
vrf_dn = aci_vrf.main.id
name = "E-Commerce"
tiers = {
frontend = {
subnet = "10.1.1.1/24"
ports = [443]
}
api = {
subnet = "10.1.2.1/24"
ports = [8080, 8443]
depends_on = ["frontend"]
}
database = {
subnet = "10.1.3.1/24"
ports = [5432]
depends_on = ["api"]
}
}
}
Pattern : Environment Factory
# modules/environment/main.tf
# Ce module crée un environnement complet
# avec isolation tenant, VRF, et contracts standards
variable "name" {
type = string
}
variable "cidr" {
type = string
}
variable "apps" {
type = map(object({
tiers = list(string)
}))
}
# Tenant isolé pour l'environnement
resource "aci_tenant" "env" {
name = "Env-${var.name}"
annotation = "env:${var.name},orchestrator:terraform"
}
# VRF dédié
resource "aci_vrf" "env" {
tenant_dn = aci_tenant.env.id
name = "${var.name}-VRF"
pc_enf_pref = "enforced"
}
# Contracts standards (DNS, NTP, etc.)
module "standard_contracts" {
source = "../standard-contracts"
tenant_dn = aci_tenant.env.id
}
# Applications
module "apps" {
source = "../application-stack"
for_each = var.apps
tenant_dn = aci_tenant.env.id
vrf_dn = aci_vrf.env.id
name = each.key
# ...
}
Exercice Pratique
Lab 6.1 : Créer un Module EPG Réutilisable
Objectif : Créer un module epg-with-contracts réutilisable.
Spécifications :
-
Le module doit créer :
- Un EPG
- Ses associations Contract (consumer/provider)
- Son association VMM Domain (optionnelle)
-
Variables requises :
name(string)application_profile_dn(string)bridge_domain_dn(string)vmm_domain_dn(string, optional)contracts(object avec consumed/provided lists)
-
Outputs :
epg_dnepg_name
Structure :
Solution Lab 6.1
# lab6/modules/epg-with-contracts/variables.tf
variable "name" {
description = "Nom de l'EPG"
type = string
validation {
condition = length(var.name) > 0 && length(var.name) <= 64
error_message = "Le nom doit faire entre 1 et 64 caractères."
}
}
variable "application_profile_dn" {
description = "DN de l'Application Profile parent"
type = string
}
variable "bridge_domain_dn" {
description = "DN du Bridge Domain"
type = string
}
variable "description" {
description = "Description de l'EPG"
type = string
default = ""
}
variable "vmm_domain_dn" {
description = "DN du VMM Domain (VMware)"
type = string
default = null
}
variable "contracts" {
description = "Contracts à associer"
type = object({
consumed = optional(list(string), [])
provided = optional(list(string), [])
})
default = {
consumed = []
provided = []
}
}
variable "annotation" {
description = "Annotation pour traçabilité"
type = string
default = "orchestrator:terraform"
}
# lab6/modules/epg-with-contracts/main.tf
resource "aci_application_epg" "this" {
application_profile_dn = var.application_profile_dn
name = var.name
description = var.description
relation_fv_rs_bd = var.bridge_domain_dn
annotation = var.annotation
}
# Association VMM Domain
resource "aci_epg_to_domain" "vmm" {
count = var.vmm_domain_dn != null ? 1 : 0
application_epg_dn = aci_application_epg.this.id
tdn = var.vmm_domain_dn
vmm_allow_promiscuous = "reject"
vmm_forged_transmits = "reject"
vmm_mac_changes = "reject"
instr_imedcy = "immediate"
res_imedcy = "immediate"
}
# Contracts consommés
resource "aci_epg_to_contract" "consumed" {
for_each = toset(var.contracts.consumed)
application_epg_dn = aci_application_epg.this.id
contract_dn = each.value
contract_type = "consumer"
}
# Contracts fournis
resource "aci_epg_to_contract" "provided" {
for_each = toset(var.contracts.provided)
application_epg_dn = aci_application_epg.this.id
contract_dn = each.value
contract_type = "provider"
}
# lab6/modules/epg-with-contracts/outputs.tf
output "epg_dn" {
description = "Distinguished Name de l'EPG"
value = aci_application_epg.this.id
}
output "epg_name" {
description = "Nom de l'EPG"
value = aci_application_epg.this.name
}
output "contracts_consumed" {
description = "Nombre de contracts consommés"
value = length(var.contracts.consumed)
}
output "contracts_provided" {
description = "Nombre de contracts fournis"
value = length(var.contracts.provided)
}
# lab6/main.tf
terraform {
required_providers {
aci = {
source = "CiscoDevNet/aci"
version = "~> 2.13"
}
}
}
provider "aci" {
username = var.apic_username
password = var.apic_password
url = var.apic_url
insecure = true
}
# Structure de base
resource "aci_tenant" "lab" {
name = "Lab-Modules"
}
resource "aci_vrf" "main" {
tenant_dn = aci_tenant.lab.id
name = "Main-VRF"
pc_enf_pref = "enforced"
}
resource "aci_bridge_domain" "web" {
tenant_dn = aci_tenant.lab.id
name = "BD-Web"
relation_fv_rs_ctx = aci_vrf.main.id
}
resource "aci_application_profile" "app" {
tenant_dn = aci_tenant.lab.id
name = "MyApp"
}
# Contracts
resource "aci_contract" "web" {
tenant_dn = aci_tenant.lab.id
name = "Web-Contract"
scope = "context"
}
# Utilisation du module
module "frontend_epg" {
source = "./modules/epg-with-contracts"
name = "Frontend"
application_profile_dn = aci_application_profile.app.id
bridge_domain_dn = aci_bridge_domain.web.id
description = "Web frontend servers"
contracts = {
consumed = []
provided = [aci_contract.web.id]
}
}
# Output
output "frontend_dn" {
value = module.frontend_epg.epg_dn
}
Points Clés à Retenir
Résumé du Module 6
Organisation Projet
project/
├── environments/ # Configs par environnement
├── modules/ # Modules réutilisables
└── shared/ # Ressources partagées
Variables Best Practices
- Toujours typer les variables
- Utiliser
validationblocks - Valeurs par défaut sensées
sensitive = truepour secrets
For_each vs Count
| Critère | for_each | count |
|---|---|---|
| Clé | String/Map | Index numérique |
| Suppression | Propre | Cascade |
| Recommandé | Maps/Sets | Conditionnel |
Modules
- Un module = une responsabilité
- Variables explicites
- Outputs utiles
- Documentation intégrée
Workspaces
- Isolation par environnement
- Même code, configs différentes
- State séparé par workspace
Backend Remote
- Obligatoire en équipe
- Locking pour éviter conflits
- Encryption du state
Exercice : À Vous de Jouer
Mise en Pratique
Objectif : Créer un module Terraform réutilisable pour déployer une architecture EPG standard
Contexte : Votre équipe déploie régulièrement des applications 3-tiers dans ACI. Pour éviter de dupliquer le code, vous devez créer un module réutilisable qui déploie automatiquement : 1 Application Profile + 3 EPGs (Web, App, DB) + 3 Bridge Domains. Le module doit être paramétrable (noms, subnets, annotations) et utilisable dans différents environnements (dev, staging, prod).
Tâches à réaliser :
- Créer un module
aci-3tier-appavec : main.tf, variables.tf, outputs.tf - Le module doit créer : 1 Application Profile, 3 Bridge Domains (avec subnets), 3 EPGs
- Utiliser
for_eachpour créer les BDs et EPGs de manière DRY - Appeler le module depuis l'environnement de production avec des valeurs spécifiques
- Créer un second environnement (dev) réutilisant le même module avec d'autres valeurs
Critères de validation :
- [ ] Le module est dans un dossier séparé
modules/aci-3tier-app - [ ] Les variables sont bien typées et documentées
- [ ]
for_eachest utilisé pour éviter la répétition - [ ] Le module est appelé depuis 2 environnements différents (dev et prod)
- [ ] Les outputs du module remontent les DNs des ressources créées
- [ ] Le code passe
terraform validatesur les 2 environnements
Solution
Structure du projet :
terraform-aci/
├── modules/
│ └── aci-3tier-app/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ └── prod/
│ ├── main.tf
│ ├── variables.tf
│ └── terraform.tfvars
└── README.md
modules/aci-3tier-app/variables.tf
variable "tenant_dn" {
description = "DN du tenant parent"
type = string
}
variable "vrf_dn" {
description = "DN du VRF pour les Bridge Domains"
type = string
}
variable "app_name" {
description = "Nom de l'application (utilisé pour l'Application Profile)"
type = string
validation {
condition = length(var.app_name) > 0 && length(var.app_name) <= 64
error_message = "Le nom de l'application doit contenir entre 1 et 64 caractères."
}
}
variable "environment" {
description = "Environnement de déploiement"
type = string
default = "production"
}
variable "tiers" {
description = "Configuration des tiers (BD + EPG)"
type = map(object({
bd_name = string
epg_name = string
subnet = string
scope = list(string)
}))
default = {
web = {
bd_name = "BD-Web"
epg_name = "Web-Tier"
subnet = "10.1.1.1/24"
scope = ["public"]
}
app = {
bd_name = "BD-App"
epg_name = "App-Tier"
subnet = "10.1.2.1/24"
scope = ["public"]
}
db = {
bd_name = "BD-Database"
epg_name = "Database-Tier"
subnet = "10.1.3.1/24"
scope = ["private"]
}
}
}
modules/aci-3tier-app/main.tf
# Application Profile
resource "aci_application_profile" "three_tier" {
tenant_dn = var.tenant_dn
name = var.app_name
description = "Application Profile 3-tier pour ${var.app_name} - ${var.environment}"
annotation = "managed-by:terraform,environment:${var.environment}"
}
# Bridge Domains (DRY avec for_each)
resource "aci_bridge_domain" "tiers" {
for_each = var.tiers
tenant_dn = var.tenant_dn
name = each.value.bd_name
description = "Bridge Domain pour ${each.key} tier"
relation_fv_rs_ctx = var.vrf_dn
arp_flood = "no"
unicast_route = "yes"
unk_mac_ucast_act = "proxy"
limit_ip_learn_to_subnets = "yes"
annotation = "managed-by:terraform,tier:${each.key}"
}
# Subnets
resource "aci_subnet" "tiers" {
for_each = var.tiers
parent_dn = aci_bridge_domain.tiers[each.key].id
ip = each.value.subnet
scope = each.value.scope
description = "Subnet pour ${each.key} tier"
}
# EPGs
resource "aci_application_epg" "tiers" {
for_each = var.tiers
application_profile_dn = aci_application_profile.three_tier.id
name = each.value.epg_name
description = "EPG pour ${each.key} tier"
relation_fv_rs_bd = aci_bridge_domain.tiers[each.key].id
pref_gr_memb = "exclude"
annotation = "managed-by:terraform,tier:${each.key}"
}
modules/aci-3tier-app/outputs.tf
output "application_profile_dn" {
description = "DN de l'Application Profile créé"
value = aci_application_profile.three_tier.id
}
output "bridge_domains" {
description = "DNs des Bridge Domains créés"
value = {
for k, bd in aci_bridge_domain.tiers : k => {
dn = bd.id
name = bd.name
subnet = var.tiers[k].subnet
}
}
}
output "epgs" {
description = "DNs des EPGs créés"
value = {
for k, epg in aci_application_epg.tiers : k => {
dn = epg.id
name = epg.name
}
}
}
output "summary" {
description = "Résumé du déploiement"
value = {
app_name = var.app_name
environment = var.environment
tiers_count = length(var.tiers)
}
}
environments/prod/main.tf
terraform {
required_version = ">= 1.0"
required_providers {
aci = {
source = "CiscoDevNet/aci"
version = "~> 2.13"
}
}
}
provider "aci" {
username = var.apic_username
password = var.apic_password
url = var.apic_url
insecure = true
}
# Tenant
resource "aci_tenant" "prod" {
name = "Prod-Tenant"
description = "Tenant de production"
}
# VRF
resource "aci_vrf" "prod" {
tenant_dn = aci_tenant.prod.id
name = "Production-VRF"
pc_enf_pref = "enforced"
}
# Module : Application E-Commerce
module "ecommerce_app" {
source = "../../modules/aci-3tier-app"
tenant_dn = aci_tenant.prod.id
vrf_dn = aci_vrf.prod.id
app_name = "ECommerce-App"
environment = "production"
tiers = {
web = {
bd_name = "BD-Web-Prod"
epg_name = "Web-Frontend"
subnet = "10.10.1.1/24"
scope = ["public"]
}
app = {
bd_name = "BD-App-Prod"
epg_name = "App-Backend"
subnet = "10.10.2.1/24"
scope = ["public"]
}
db = {
bd_name = "BD-DB-Prod"
epg_name = "Database"
subnet = "10.10.3.1/24"
scope = ["private"]
}
}
}
environments/prod/outputs.tf
output "ecommerce_summary" {
description = "Résumé de l'application E-Commerce"
value = module.ecommerce_app.summary
}
output "ecommerce_epgs" {
description = "EPGs de l'application E-Commerce"
value = module.ecommerce_app.epgs
}
environments/dev/main.tf
terraform {
required_version = ">= 1.0"
required_providers {
aci = {
source = "CiscoDevNet/aci"
version = "~> 2.13"
}
}
}
provider "aci" {
username = var.apic_username
password = var.apic_password
url = var.apic_url
insecure = true
}
# Tenant
resource "aci_tenant" "dev" {
name = "Dev-Tenant"
description = "Tenant de développement"
}
# VRF
resource "aci_vrf" "dev" {
tenant_dn = aci_tenant.dev.id
name = "Dev-VRF"
pc_enf_pref = "unenforced" # Plus permissif en dev
}
# Module : Application E-Commerce (environnement dev)
module "ecommerce_app_dev" {
source = "../../modules/aci-3tier-app"
tenant_dn = aci_tenant.dev.id
vrf_dn = aci_vrf.dev.id
app_name = "ECommerce-App-Dev"
environment = "development"
tiers = {
web = {
bd_name = "BD-Web-Dev"
epg_name = "Web-Frontend-Dev"
subnet = "10.20.1.1/24"
scope = ["public"]
}
app = {
bd_name = "BD-App-Dev"
epg_name = "App-Backend-Dev"
subnet = "10.20.2.1/24"
scope = ["public"]
}
db = {
bd_name = "BD-DB-Dev"
epg_name = "Database-Dev"
subnet = "10.20.3.1/24"
scope = ["private"]
}
}
}
Déploiement :
# Environnement Prod
cd environments/prod
terraform init
terraform validate
terraform plan
terraform apply
# Environnement Dev
cd ../dev
terraform init
terraform validate
terraform plan
terraform apply
Résultat attendu :
Le module aci-3tier-app est réutilisé dans 2 environnements différents (dev et prod) avec des configurations spécifiques. Le code est DRY (Don't Repeat Yourself) et maintenable.
Navigation
| Précédent | Suivant |
|---|---|
| ← Module 5 : Provider ACI | Module 7 : Flux Nord-Sud (L3Out) → |
Navigation
| ← Module 5 : Provider Terraform ACI | Module 7 : Flux Nord-Sud (L3Out) → |