Terraform AWS EC2

1. Provider AWS

AWS Provider

On crée un fichier main.tf dans un dossier de travail :

provider "aws" {
  profile = "default"
  region  = "eu-west-1"
}

resource "aws_instance" "server" {
  ami           = "ami-04c58523038d79132" # Ubunutu Bionic
  instance_type = "t2.micro"
}

Un bloc “provider” qui définit les paramètres de connexion au service AWS.

L’attribut profile fait ici référence au fichier de configuration AWS dans ~/.aws/credentials sur MacOS et Linux ou %UserProfile%\.aws\credentials sur un système Windows. Il est recommandé par HashiCorp de ne jamais coder en dur les informations d’identification dans les fichiers de configuration *.tf. Nous définissons ici explicitement le profil de configuration AWS par défaut pour illustrer la façon dont Terraform accède aux informations d’identification sensibles.

Plusieurs providers peuvent exister dans une configuration.

Le bloc ressource “aws_instance” dépend des paramètres du provider “aws”. Deux arguments obligatoires sont définis :

  • “ami”
  • “instance_type”

2. Initialisation

La première commande à exécuter pour une nouvelle configuration, ou après avoir vérifié une configuration existante à partir du contrôle de version, est terraform init, qui initialise divers paramètres locaux et des données qui seront utilisées par les commandes suivantes.

Terraform utilise une architecture basée sur des plugins pour supporter les nombreux fournisseurs d’infrastructure et de services disponibles. À partir de la version 0.10.0 de Terraform, chaque provider est son propre binaire encapsulé distribué séparément de Terraform lui-même. La commande init de Terraform téléchargera et installera automatiquement tout binaire de fournisseur pour les fournisseurs utilisés dans la configuration, qui dans ce cas n’est que le fournisseur aws :

terraform init
Initializing the backend...

Initializing provider plugins...
- Checking for available provider plugins...
- Downloading plugin for provider "aws" (hashicorp/aws) 2.43.0...

The following providers do not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.

* provider.aws: version = "~> 2.43"

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.

Le plugin du fournisseur aws est téléchargé et installé dans un sous-dossier du dossier de travail courant, avec divers autres fichiers de comptabilité.

La sortie spécifie quelle version du plugin a été installée, et suggère de spécifier cette version dans la configuration pour s’assurer que terraform init installera une version compatible à l’avenir. Cette étape n’est pas nécessaire pour suivre la suite, puisque cette configuration sera rejetée à la fin.

3. Formatage et validation des configurations

Pour respecter les conventions de style, nous recommandons une cohérence de langage entre les fichiers et les modules écrits par les différentes équipes. La commande terraform fmt permet une standardisation qui met automatiquement à jour les configurations dans le répertoire courant pour une meilleure lisibilité et cohérence.

Si vous copiez des bribes de configuration ou si vous voulez simplement vous assurer que votre configuration est syntaxiquement valide et cohérente en interne, la commande intégrée terraform validate vérifiera et rapportera les erreurs dans les modules, les noms d’attributs et les types de valeurs.

4. Application des changements

Dans le même dossier que le fichier main.tf que vous avez créé, exécutez terraform apply. Vous devriez voir une sortie similaire à celle ci-dessous, bien que nous ayons tronqué une partie de la sortie pour économiser de l’espace :

terraform apply
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_instance.server will be created
  + resource "aws_instance" "server" {
      + ami                          = "ami-04c58523038d79132"
      + arn                          = (known after apply)
      + associate_public_ip_address  = (known after apply)
      + availability_zone            = (known after apply)
      + cpu_core_count               = (known after apply)
      + cpu_threads_per_core         = (known after apply)
      + get_password_data            = false
      + host_id                      = (known after apply)
      + id                           = (known after apply)
      + instance_state               = (known after apply)
      + instance_type                = "t2.micro"
      + ipv6_address_count           = (known after apply)
      + ipv6_addresses               = (known after apply)
      + key_name                     = (known after apply)
      + network_interface_id         = (known after apply)
      + password_data                = (known after apply)
      + placement_group              = (known after apply)
      + primary_network_interface_id = (known after apply)
      + private_dns                  = (known after apply)
      + private_ip                   = (known after apply)
      + public_dns                   = (known after apply)
      + public_ip                    = (known after apply)
      + security_groups              = (known after apply)
      + source_dest_check            = true
      + subnet_id                    = (known after apply)
      + tenancy                      = (known after apply)
      + volume_tags                  = (known after apply)
      + vpc_security_group_ids       = (known after apply)

      + ebs_block_device {
          + delete_on_termination = (known after apply)
          + device_name           = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + snapshot_id           = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }

      + ephemeral_block_device {
          + device_name  = (known after apply)
          + no_device    = (known after apply)
          + virtual_name = (known after apply)
        }

      + network_interface {
          + delete_on_termination = (known after apply)
          + device_index          = (known after apply)
          + network_interface_id  = (known after apply)
        }

      + root_block_device {
          + delete_on_termination = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Cette sortie montre le plan d’exécution, décrivant les actions que Terraform prendra afin de modifier l’infrastructure réelle pour qu’elle corresponde à la configuration. Le format de sortie est similaire au format diff généré par des outils tels que Git. La sortie comporte un + à côté de aws_instance.server, ce qui signifie que Terraform va créer cette ressource. En dessous, il indique les attributs qui seront définis. Lorsque la valeur affichée est “(known after apply)”, cela signifie que la valeur ne sera pas connue tant que la ressource n’aura pas été créée.

Si l’application de Terraform a échoué avec une erreur, lisez le message d’erreur et corrigez l’erreur qui s’est produite. A ce stade, il est probable qu’il s’agisse d’une erreur de syntaxe dans la configuration.

Si le plan a été créé avec succès, Terraform va maintenant faire une pause et attendre l’approbation avant de continuer. Si quelque chose dans le plan semble incorrect ou dangereux, vous pouvez y mettre fin en toute sécurité sans apporter de modifications à votre infrastructure. Dans ce cas, le plan semble acceptable, alors tapez “yes” à la demande de confirmation pour continuer.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

L’exécution du plan prendra quelques minutes car Terraform attend que l’instance EC2 soit disponible :

aws_instance.server: Creating...
	aws_instance.server: Still creating... [10s elapsed]
aws_instance.server: Still creating... [20s elapsed]
aws_instance.server: Still creating... [30s elapsed]
aws_instance.server: Creation complete after 36s [id=i-08c33e154950b904b]

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

Après cela, l’oeuvre de Terraform est terminée ! Vous pouvez aller dans la console EC2 pour voir l’instance EC2 créée. (Assurez-vous que vous regardez la même région que celle qui a été configurée dans la configuration du fournisseur) !

Terraform a également écrit quelques données dans le fichier terraform.tfstate. Ce fichier d’état est extrêmement important ; il garde la trace des ID des ressources créées afin que Terraform sache ce qu’il gère. Ce fichier doit être sauvegardé et distribué à toute personne susceptible d’exécuter Terraform. Il est généralement recommandé de configurer l’état distant lorsque vous travaillez avec Terraform, pour partager l’état automatiquement, mais ce n’est pas nécessaire pour des situations simples comme cette première activité.

Vous pouvez inspecter l’état actuel en utilisant terraform show :

terraform show
# aws_instance.server:
resource "aws_instance" "server" {
    ami                          = "ami-04c58523038d79132"
    arn                          = "arn:aws:ec2:eu-west-1:733718180495:instance/i-08c33e154950b904b"
    associate_public_ip_address  = true
    availability_zone            = "eu-west-1b"
    cpu_core_count               = 1
    cpu_threads_per_core         = 1
    disable_api_termination      = false
    ebs_optimized                = false
    get_password_data            = false
    id                           = "i-08c33e154950b904b"
    instance_state               = "running"
    instance_type                = "t2.micro"
    ipv6_address_count           = 0
    ipv6_addresses               = []
    monitoring                   = false
    primary_network_interface_id = "eni-03e1bf7288b57125f"
    private_dns                  = "ip-172-31-5-168.eu-west-1.compute.internal"
    private_ip                   = "172.31.5.168"
    public_dns                   = "ec2-63-32-118-158.eu-west-1.compute.amazonaws.com"
    public_ip                    = "63.32.118.158"
    security_groups              = [
        "default",
    ]
    source_dest_check            = true
    subnet_id                    = "subnet-f3402595"
    tenancy                      = "default"
    volume_tags                  = {}
    vpc_security_group_ids       = [
        "sg-0edd0a7f",
    ]

    credit_specification {
        cpu_credits = "standard"
    }

    root_block_device {
        delete_on_termination = true
        encrypted             = false
        iops                  = 100
        volume_id             = "vol-04714436694a7a703"
        volume_size           = 8
        volume_type           = "gp2"
    }
}

5. Modification d’une ressource

Imaginons que nous modifions l’argument “ami” du fichier main.tf :

provider "aws" {
  profile = "default"
  region  = "eu-west-1"
}

resource "aws_instance" "server" {
  ami           = "ami-0665161784d935578" # Ubunutu Eoan
  instance_type = "t2.micro"
}

j’ai changé l’AMI d’Ubuntu 18.04 LTS AMI avec celle d’Ubuntu 19.10 . Les configurations de Terraform doivent être modifiées comme suit. Vous pouvez également supprimer complètement des ressources et Terraform saura détruire l’ancienne.

Après avoir modifié la configuration, exécutez à nouveau l’application de Terraform pour voir comment Terraform appliquera ce changement aux ressources existantes.

terraform apply
aws_instance.server: Refreshing state... [id=i-08c33e154950b904b]

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # aws_instance.server must be replaced
-/+ resource "aws_instance" "server" {
      ~ ami                          = "ami-04c58523038d79132" -> "ami-0665161784d935578" # forces replacement
      ~ arn                          = "arn:aws:ec2:eu-west-1:733718180495:instance/i-08c33e154950b904b" -> (known after apply)
      ~ associate_public_ip_address  = true -> (known after apply)
      ~ availability_zone            = "eu-west-1b" -> (known after apply)
      ~ cpu_core_count               = 1 -> (known after apply)
      ~ cpu_threads_per_core         = 1 -> (known after apply)
      - disable_api_termination      = false -> null
      - ebs_optimized                = false -> null
        get_password_data            = false
      + host_id                      = (known after apply)
      ~ id                           = "i-08c33e154950b904b" -> (known after apply)
      ~ instance_state               = "running" -> (known after apply)
        instance_type                = "t2.micro"
      ~ ipv6_address_count           = 0 -> (known after apply)
      ~ ipv6_addresses               = [] -> (known after apply)
      + key_name                     = (known after apply)
      - monitoring                   = false -> null
      + network_interface_id         = (known after apply)
      + password_data                = (known after apply)
      + placement_group              = (known after apply)
      ~ primary_network_interface_id = "eni-03e1bf7288b57125f" -> (known after apply)
      ~ private_dns                  = "ip-172-31-5-168.eu-west-1.compute.internal" -> (known after apply)
      ~ private_ip                   = "172.31.5.168" -> (known after apply)
      ~ public_dns                   = "ec2-63-32-118-158.eu-west-1.compute.amazonaws.com" -> (known after apply)
      ~ public_ip                    = "63.32.118.158" -> (known after apply)
      ~ security_groups              = [
          - "default",
        ] -> (known after apply)
        source_dest_check            = true
      ~ subnet_id                    = "subnet-f3402595" -> (known after apply)
      - tags                         = {} -> null
      ~ tenancy                      = "default" -> (known after apply)
      ~ volume_tags                  = {} -> (known after apply)
      ~ vpc_security_group_ids       = [
          - "sg-0edd0a7f",
        ] -> (known after apply)

      - credit_specification {
          - cpu_credits = "standard" -> null
        }

      + ebs_block_device {
          + delete_on_termination = (known after apply)
          + device_name           = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + snapshot_id           = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }

      + ephemeral_block_device {
          + device_name  = (known after apply)
          + no_device    = (known after apply)
          + virtual_name = (known after apply)
        }

      + network_interface {
          + delete_on_termination = (known after apply)
          + device_index          = (known after apply)
          + network_interface_id  = (known after apply)
        }

      ~ root_block_device {
          ~ delete_on_termination = true -> (known after apply)
          ~ encrypted             = false -> (known after apply)
          ~ iops                  = 100 -> (known after apply)
          + kms_key_id            = (known after apply)
          ~ volume_id             = "vol-04714436694a7a703" -> (known after apply)
          ~ volume_size           = 8 -> (known after apply)
          ~ volume_type           = "gp2" -> (known after apply)
        }
    }

Plan: 1 to add, 0 to change, 1 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value:

Le préfixe -/+ signifie que Terraform détruira et recréera la ressource, plutôt que de la mettre à jour sur place. Alors que certains attributs peuvent être mis à jour sur place (qui sont indiqués par le préfixe ~), changer l’AMI pour une instance d’EC2 nécessite de la recréer. Terraform gère ces détails pour vous, et le plan d’exécution indique clairement ce que Terraform fera.

De plus, le plan d’exécution montre que la modification de l’AMI est la ressource à remplacer. À l’aide de ces informations, vous pouvez ajuster vos changements pour éviter éventuellement de détruire/créer des mises à jour si elles ne sont pas acceptables dans certaines situations.

Une fois de plus, Terraform vous demande d’approuver le plan d’exécution avant de poursuivre. Répondez “yes” pour exécuter les étapes prévues :

aws_instance.server: Destroying... [id=i-08c33e154950b904b]
aws_instance.server: Still destroying... [id=i-08c33e154950b904b, 10s elapsed]
aws_instance.server: Still destroying... [id=i-08c33e154950b904b, 20s elapsed]
aws_instance.server: Still destroying... [id=i-08c33e154950b904b, 30s elapsed]
aws_instance.server: Destruction complete after 31s
aws_instance.server: Creating...
aws_instance.server: Still creating... [10s elapsed]
aws_instance.server: Still creating... [20s elapsed]
aws_instance.server: Still creating... [30s elapsed]
aws_instance.server: Creation complete after 35s [id=i-04db917b8343a9af9]

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

6. Destruction

Les ressources peuvent être détruites à l’aide de la commande terraform destroy, qui est similaire à la commande terraform apply mais qui se comporte comme si toutes les ressources avaient été supprimées de la configuration.

terraform destroy
aws_instance.server: Refreshing state... [id=i-04db917b8343a9af9]

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # aws_instance.server will be destroyed
  - resource "aws_instance" "server" {
      - ami                          = "ami-0665161784d935578" -> null
      - arn                          = "arn:aws:ec2:eu-west-1:733718180495:instance/i-04db917b8343a9af9" -> null
      - associate_public_ip_address  = true -> null
      - availability_zone            = "eu-west-1b" -> null
      - cpu_core_count               = 1 -> null
      - cpu_threads_per_core         = 1 -> null
      - disable_api_termination      = false -> null
      - ebs_optimized                = false -> null
      - get_password_data            = false -> null
      - id                           = "i-04db917b8343a9af9" -> null
      - instance_state               = "running" -> null
      - instance_type                = "t2.micro" -> null
      - ipv6_address_count           = 0 -> null
      - ipv6_addresses               = [] -> null
      - monitoring                   = false -> null
      - primary_network_interface_id = "eni-0791e3a10a19236ee" -> null
      - private_dns                  = "ip-172-31-13-40.eu-west-1.compute.internal" -> null
      - private_ip                   = "172.31.13.40" -> null
      - public_dns                   = "ec2-52-213-154-71.eu-west-1.compute.amazonaws.com" -> null
      - public_ip                    = "52.213.154.71" -> null
      - security_groups              = [
          - "default",
        ] -> null
      - source_dest_check            = true -> null
      - subnet_id                    = "subnet-f3402595" -> null
      - tags                         = {} -> null
      - tenancy                      = "default" -> null
      - volume_tags                  = {} -> null
      - vpc_security_group_ids       = [
          - "sg-0edd0a7f",
        ] -> null

      - credit_specification {
          - cpu_credits = "standard" -> null
        }

      - root_block_device {
          - delete_on_termination = true -> null
          - encrypted             = false -> null
          - iops                  = 100 -> null
          - volume_id             = "vol-0d07193c1c7bf999b" -> null
          - volume_size           = 8 -> null
          - volume_type           = "gp2" -> null
        }
    }

Plan: 0 to add, 0 to change, 1 to destroy.

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value:

Le préfixe - indique que l’instance sera détruite. Comme pour les applications, Terraform montre son plan d’exécution et attend l’approbation avant de faire des changements.

Répondez “yes” pour exécuter ce plan et détruire l’infrastructure :

aws_instance.server: Destroying... [id=i-04db917b8343a9af9]
aws_instance.server: Still destroying... [id=i-04db917b8343a9af9, 10s elapsed]
aws_instance.server: Still destroying... [id=i-04db917b8343a9af9, 20s elapsed]
aws_instance.server: Still destroying... [id=i-04db917b8343a9af9, 30s elapsed]
aws_instance.server: Destruction complete after 30s

Destroy complete! Resources: 1 destroyed.

Tout comme pour l’application, Terraform détermine l’ordre dans lequel les choses doivent être détruites. Dans ce cas, il n’y avait qu’une seule ressource, donc aucune commande n’était nécessaire. Dans les cas plus compliqués où il y a plusieurs ressources, Terraform les détruira dans un ordre approprié pour respecter les dépendances.

7. Ajout de ressources

Dans cette configuration on crée quatre ressources AWS :

  • aws_instance
  • aws_key_pair dans laquelle on retrouve l’usage d’une fonction file (Fonction File)
  • aws_default_vpc
  • aws_default_security_group

Fichier main.tf

provider "aws" {
  profile = "default"
  region  = "eu-west-1"
}

resource "aws_instance" "server" {
  ami           = "ami-04c58523038d79132" # Ubunutu Bionic
  instance_type = "t2.micro"
  key_name      = "admin-server"
  root_block_device {
    volume_size           = "10"
    volume_type           = "standard"
    delete_on_termination = "true"
  }
  tags = {
    Name = "server"
  }
}

resource "aws_key_pair" "admin-server" {
  key_name   = "admin"
  public_key = file("~/.ssh/id_rsa.pub")
}

resource "aws_default_vpc" "default" {
  tags = {
    Name = "Default VPC"
  }
}

resource "aws_default_security_group" "default" {
  vpc_id = aws_default_vpc.default.id

  ingress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
terraform apply -auto-approve
aws_default_vpc.default: Creating...
aws_key_pair.admin-server: Creating...
aws_instance.server: Creating...
aws_key_pair.admin-server: Creation complete after 0s [id=admin]
aws_default_vpc.default: Creation complete after 2s [id=vpc-9fd0f8f9]
aws_default_security_group.default: Creating...
aws_default_security_group.default: Creation complete after 3s [id=sg-0edd0a7f]
aws_instance.server: Still creating... [10s elapsed]
aws_instance.server: Still creating... [20s elapsed]
aws_instance.server: Still creating... [30s elapsed]
aws_instance.server: Creation complete after 33s [id=i-0553b185417a50cdc]

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

8. Variables d’entrée

Input Variables et GETTING STARTED - AWS Input Variables

Deux variables d’entrées ajoutées :

  • region appelée dans les arguments du plugin de fournisseur aws
  • key_pair_path appelée dans la ressource aws_key_pair “admin-server” utilisée par la ressource aws_instance “server”

Fichier main.tf

variable "region" {
  default = "eu-west-1"
}

variable "key_pair_path" {
  type = map(string)
  default = {
    public_key_path = "~/.ssh/id_rsa.pub"
  }
}

provider "aws" {
  profile = "default"
  region  = var.region
}

resource "aws_instance" "server" {
  ami           = "ami-04c58523038d79132" # Ubunutu Bionic
  instance_type = "t2.micro"
  key_name      = "admin-server"
  root_block_device {
    volume_size           = "10"
    volume_type           = "standard"
    delete_on_termination = "true"
  }
  tags = {
    Name = "server"
  }
}

resource "aws_key_pair" "admin-server" {
  key_name   = "admin"
  public_key = file(var.key_pair_path["public_key_path"])
}

resource "aws_default_vpc" "default" {
  tags = {
    Name = "Default VPC"
  }
}

resource "aws_default_security_group" "default" {
  vpc_id = aws_default_vpc.default.id

  ingress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

9. Priorité de la définition des variables

Les mécanismes de réglage des variables ci-dessus peuvent être utilisés ensemble dans n’importe quelle combinaison. Si plusieurs valeurs sont attribuées à une même variable, Terraform utilise la dernière valeur trouvée, remplaçant toute valeur précédente. Notez qu’il n’est pas possible d’attribuer plusieurs valeurs à une même variable au sein d’une même source.

Terraform charge les variables dans l’ordre suivant, les sources ultérieures ayant la priorité sur les sources antérieures :

  • Variables d’environnement
  • Le fichier terraform.tfvars, si présent.
  • Le fichier terraform.tfvars.json, s’il existe.
  • Tout fichier *.auto.tfvars ou *.auto.tfvars.json, traité dans l’ordre lexical de leur nom de fichier.
  • Toutes les options -var et -var-file sur la ligne de commande, dans l’ordre où elles sont fournies. (Cela inclut les variables définies par un espace de travail Terraform Cloud).

10. Valeurs de sortie

Les valeurs de sortie sont comme les valeurs de retour d’un module Terraform, et ont plusieurs utilisations :

  • Un module enfant peut utiliser les sorties pour exposer un sous-ensemble de ses attributs de ressources à un module parent.
  • Un module racine peut utiliser des sorties pour imprimer certaines valeurs dans la sortie de l’interface utilisateur après l’exécution de l’application Terraform.
  • Lors de l’utilisation de l’état distant, les sorties du module racine peuvent être accessibles par d’autres configurations via une source de données terraform_remote_state.

Les instances de ressources gérées par Terraform exportent chacune des attributs dont les valeurs peuvent être utilisées ailleurs dans la configuration. Les valeurs de sortie sont un moyen d’exposer certaines de ces informations à l’utilisateur de votre module.

Par exemple, on ajoute un objet output “server_ip” au fichier main.tf :

output "server_ip" {
  value = aws_instance.server.public_ip
}
terraform apply
aws_default_vpc.default: Refreshing state... [id=vpc-9fd0f8f9]
aws_key_pair.admin-server: Refreshing state... [id=admin]
aws_instance.server: Refreshing state... [id=i-0ff09c2c00b29bd4b]
aws_default_security_group.default: Refreshing state... [id=sg-0edd0a7f]

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

Outputs:

server_ip = 34.253.235.140

On peut rappler les valeurs de sortie avec la commande terraform output :

terraform output
server_ip = 34.253.235.140
terraform output -json
{
  "server_ip": {
    "sensitive": false,
    "type": "string",
    "value": "34.253.235.140"
  }
}

11. Source de données

Les sources de données permettent d’extraire ou de calculer des données pour les utiliser ailleurs dans la configuration de Terraform. L’utilisation des sources de données permet à une configuration Terraform d’utiliser des informations définies en dehors de Terraform ou définies par une autre configuration Terraform distincte.

Chaque fournisseur peut proposer des sources de données en plus de son ensemble de types de ressources.

Dans le fichier main.tf, on utilise une source de données “aws_ami” qui cherche l’ID de l’AMI la plus récente d’Ubuntu Bionic dans la région AWS. Le résultat valorise l’argument “ami” de la rssource “aws_instance”.

data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"] # Canonical
}

output "server_ip" {
  value = aws_instance.server.public_ip
}

variable "region" {
  default = "eu-west-1"
}

variable "key_pair_path" {
  type = map(string)
  default = {
    public_key_path = "~/.ssh/id_rsa.pub"
  }
}

provider "aws" {
  profile = "default"
  region  = var.region
}

resource "aws_instance" "server" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"
  key_name      = "admin-server"
  root_block_device {
    volume_size           = "10"
    volume_type           = "standard"
    delete_on_termination = "true"
  }
  tags = {
    Name = "server"
  }
}

resource "aws_key_pair" "admin-server" {
  key_name   = "admin"
  public_key = file(var.key_pair_path["public_key_path"])
}

resource "aws_default_vpc" "default" {
  tags = {
    Name = "Default VPC"
  }
}

resource "aws_default_security_group" "default" {
  vpc_id = aws_default_vpc.default.id

  ingress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

Une source de données pour les images Centos 7 :

data "aws_ami" "centos" {
owners      = ["679593333241"]
most_recent = true

  filter {
      name   = "name"
      values = ["CentOS Linux 7 x86_64 HVM EBS *"]
  }

  filter {
      name   = "architecture"
      values = ["x86_64"]
  }

  filter {
      name   = "root-device-type"
      values = ["ebs"]
  }
}

12. Ajout d’instances multiples

Note : Un bloc de ressources donné ne peut pas utiliser à la fois count et for_each.

Par défaut, un bloc de ressources configure un objet d’infrastructure réel. Cependant, il peut arriver que vous souhaitiez gérer plusieurs objets similaires, comme un pool fixe d’instances de calcul. Terraform a deux façons de le faire : count et for_each.

Le méta-argument count accepte un nombre entier, et crée autant d’instances de la ressource. Chaque instance a un objet d’infrastructure distinct qui lui est associé et chacune est créée, mise à jour ou détruite séparément lorsque la configuration est appliquée.

Dans ce fichier main.tf, deux instances identiques sont créées :

resource "aws_instance" "server" {
  count = 2
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"
  key_name      = "admin-server"
  root_block_device {
    volume_size           = "10"
    volume_type           = "standard"
    delete_on_termination = "true"
  }
  tags = {
    Name = "server${count.index}"
  }
}

output "server_ip" {
  value = aws_instance.server.*.public_ip
}

data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"] # Canonical
}

variable "region" {
  default = "eu-west-1"
}

variable "key_pair_path" {
  type = map(string)
  default = {
    public_key_path = "~/.ssh/id_rsa.pub"
  }
}

provider "aws" {
  profile = "default"
  region  = var.region
}

resource "aws_key_pair" "admin-server" {
  key_name   = "admin"
  public_key = file(var.key_pair_path["public_key_path"])
}

resource "aws_default_vpc" "default" {
  tags = {
    Name = "Default VPC"
  }
}

resource "aws_default_security_group" "default" {
  vpc_id = aws_default_vpc.default.id

  ingress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

Dans les blocs de ressources où le comptage est défini, un objet comptabilisé supplémentaire est disponible dans les expressions, de sorte que vous pouvez modifier la configuration de chaque instance. Cet objet a un attribut :

count.index - Le numéro d’index distinct (commençant par 0) correspondant à cette instance.

Lorsque le compte est défini, Terraform fait la distinction entre le bloc de ressources lui-même et les multiples instances de ressources qui lui sont associées. Les instances sont identifiées par un numéro d’index, commençant par 0.

  • <TYPE>.<NOM> (par exemple, aws_instance.server) fait référence au bloc de ressources.
  • <TYPE>.<NOM>[<INDEX>] (par exemple, aws_instance.server[0], aws_instance.server[1], etc.) fait référence à des instances individuelles.

Ceci est différent des ressources sans comptage ou for_each, qui peuvent être référencées sans index ni clé.

13. Provisioner Local-Exec

Dans le même dossier on placera une déclaration main.tf, un livre de jeu playbook.yml et son fichier de configuration en tant que “provisioner” Terraform. Le pré-requis est bien sûr qu’Ansible soit localement installé.

La déclaration main.tf crée autant d’instances t2.micro “Ubuntu Bionic” dans la région eu-west-1. Un argument provision “local-exec” exécute une action locale : ansible contre l’adresse IPv4 de l’instance à titre d’inventaire (variable '${self.public_ip}')

resource "aws_instance" "server" {
  count = 2
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"
  key_name      = "admin-server"
  root_block_device {
    volume_size           = "10"
    volume_type           = "standard"
    delete_on_termination = "true"
  }
  tags = {
    Name = "server${count.index}"
  }
  provisioner "local-exec" {
    command = "ansible-playbook -i '${self.public_ip},' playbook.yml --extra-vars ipv4='${self.public_ip}'"
  }
}

output "server_ip" {
  value = aws_instance.server.*.public_ip
}

data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"] # Canonical
}

variable "region" {
  default = "eu-west-1"
}

variable "key_pair_path" {
  type = map(string)
  default = {
    public_key_path = "~/.ssh/id_rsa.pub"
  }
}

provider "aws" {
  profile = "default"
  region  = var.region
}

resource "aws_key_pair" "admin-server" {
  key_name   = "admin"
  public_key = file(var.key_pair_path["public_key_path"])
}

resource "aws_default_vpc" "default" {
  tags = {
    Name = "Default VPC"
  }
}

resource "aws_default_security_group" "default" {
  vpc_id = aws_default_vpc.default.id

  ingress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

Le livre de jeu playbook.yml configure l’approvisionnement au minimum d’une gestion Ansible :

---
- name: "Ansible as Terraform provisioner sample"
  hosts: all
  gather_facts: False
  remote_user: ubuntu
  vars:
  tasks:
    - name: "Wait 600 seconds for port 22 to become open and contain OpenSSH"
      wait_for:
        port: 22
        host: '{{ inventory_hostname }}'
        search_regex: OpenSSH
        delay: 10
        timeout: 600
      vars:
        ansible_connection: local
    - name: "install python req for ansible as raw"
      raw: sudo apt update && sudo apt -y install python-minimal
      ignore_errors: yes
      register: req_result
      changed_when: "req_result.stdout is not search('0 newly installed') or req_result.stdout is not search('already installed')"
    - name: "Wait 600 seconds, start checking after 10 seconds"
      wait_for_connection:
        delay: 10
        timeout: 600
        connect_timeout: 10
        sleep: 5
    - name: "Get the ipv4 public address on the host"
      block:
        - name: Gathering facts
          setup:
        - debug: var=ansible_facts
        - name: "Set the the ipv4 public address by https://ipinfo.io/"
          uri:
            url: https://ipinfo.io/ip
            return_content: yes
          register: ipinfo_content
        - name: "Set the IPv4 address as variable"
          set_fact:
            ip_address: "{{ ipinfo_content.content | replace('\n', '') }}"
  post_tasks:
    - name: "Print the connexion informations"
      debug:
        msg: |
             "Please connect with 'ssh ubuntu@{{ ip_address }}'
             The value of the `ipv4` var passed by terraform is {{ ipv4 }}
             "

Et son fichier local de configuration ansible.cfg :

[defaults]
inventory = ./hosts
private_key_file = ~/.ssh/id_rsa
callback_whitelist = profile_tasks
forks = 20
#strategy = free
gathering = explicit
become = True
host_key_checking = False
log_path = ./ansible.log
enable_plugins = host_list, script, yaml, ini, auto
[callback_profile_tasks]
task_output_limit = 100

14. Provisioner Remote-Exec et File

Dans cette configuration, il s’agit d’utiliser les “provisionner” “file” et “remote-exec” avec des paramètres de connexion comme argument du module “aws_instance”. Le “provisionner” “file” copie le fichier init.sh à travers SSH et l’exécute à distance toujours via SSH.

Fichier init.sh

#!/bin/bash
export DEBIAN_FRONTEND="noninteractive"
sudo apt update
sudo apt-get upgrade --yes --force-yes -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold"
sudo apt -y install python-minimal git
echo "doing something" > hello.txt

Fichier main.tf :

# Provision with full in-build Terraform plan an EC2 instance

# providers.tf

provider "aws" {
  profile = "default"
  region  = "eu-west-1"
  version = "~> 2.42"
}

# resources.tf

resource "aws_instance" "server" {
  count = 1
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"
  key_name      = "admin-server"
  root_block_device {
    volume_size           = "10"
    volume_type           = "standard"
    delete_on_termination = "true"
  }
  tags = {
    Name = "server${count.index}"
  }
#  provisioner "local-exec" {
#    command = "ansible-playbook -i '${self.public_ip},' playbook.yml --extra-vars ipv4='${self.public_ip}'"
#  }
  connection {
    type        = "ssh"
    host        = self.public_ip
    user        = "ubuntu"
    private_key = file("~/.ssh/id_rsa")
  }
  provisioner "file" {
    source      = "./init.sh"
    destination = "/home/ubuntu/init.sh"
  }
  provisioner "remote-exec" {
    inline = [
      "chmod +x /home/ubuntu/init.sh",
      "/home/ubuntu/init.sh",
    ]
  }
}

resource "aws_key_pair" "admin" {
  key_name   = "admin-server"
  public_key = file(var.key_pair_path["public_key_path"])
}

resource "aws_default_vpc" "default" {
  tags = {
    Name = "Default VPC"
  }
}

resource "aws_default_security_group" "default" {
  vpc_id = aws_default_vpc.default.id

  ingress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# variables.tf

variable "key_pair_path" {
  type = map(string)
  default = {
    public_key_path = "~/.ssh/id_rsa.pub"
  }
}

data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"] # Canonical
}

# output.tf

output "server_ip" {
  value = aws_instance.server.*.public_ip
}

15. Dépendances