Ici, on invente un nouveau verbe et oui ! Mais on va principalement parler de Packer avec AWS. Nous allons industrialiser la création de vos images machine avec la mise en place d’une pipeline de CI/CD, d’un modèle Packer et d’une post configuration avec Ansible, le tout sur AWS.

Les bases de Packer

Packer est un logiciel open source fourni par Hashicorp. En une phrase, c’est un logiciel qui vous sert à faire des images de manière automatisée. Il introduit un modèle pour vos images vous permettant d’uniformiser vos déploiements. Il est multi-providers et se déploie très facilement sur AWS, GCP et Azure. Il est stable et simple d'utilisation, en une commande, vous pourrez avoir votre instance déployée, configurée et uniformisée. Tout ça, en un rien de temps !

HCL vs JSON

Les fichiers Packer sont reconnaissables par leur extension “.pkr.hcl”. Le langage utilisé pour rédiger ces fichiers est l'HashiCorp Configuration Language 2 (HCL2). Si vous faites du Terraform, vous ne serez pas perdu car il s’agit du même formalisme ! C’est l’un des avantages de l’uniformisation des logiciels HashiCorp.
Nous préférons le HCL2 au json du fait qu’il soit plus léger à lire et écrire. Depuis la version 1.5.0 il est possible de créer les fichiers Packer en HCL2. Auparavant l’ensemble des modèles Packer était en json. Si vous avez déjà mis en place des fichiers Packer en json, pas de panique ! HashiCorp a tout prévu : il existe une commande Packer permettant de mettre à jour vos fichiers json en HCL2.


packer hcl2_upgrade -with-annotations docker-ubuntu.pkr.hcl

Rien de plus simple !

Build d’une image

Pour la construction de vos images, il y a deux commandes principales.
Tout d’abord, cette commande :


packer validate docker-ubuntu.pkr.hcl

(packer validate) permet de valider le fichier Packer avant de build. C’est toujours utile ;)

Et, la commande :


packer build docker-ubuntu.pkr.hcl

(packer build). Comme son nom l’indique, elle sert à construire le modèle que vous allez envoyer. Vous aurez seulement besoin de ces commandes ! Pour le reste, je vous invite à regarder en faisant un :


packer --help
Usage: packer [--version] [--help] <command> [<args>]

Available commands are:
    build           build image(s) from template
    console         creates a console for testing variable interpolation
    fix             fixes templates from old versions of packer
    fmt             Rewrites HCL2 config files to canonical format
    hcl2_upgrade    transform a JSON template into an HCL2 configuration
    init            Install missing plugins or upgrade plugins
    inspect         see components of a template
    plugins         Interact with Packer plugins and catalog
    validate        check that a template is valid
    version         Prints the Packer version

Les différents types de bloc

Ici, je vais référencer les blocs et leur utilité en HCL2.

Le bloc build

C’est le bloc qui indique quelles sont les actions à mener pour construire l’image machine finale. Dans ce bloc, vous avez la possibilité de renseigner des providers. Ils indiquent comment ajouter le contenu additionnel et vos configurations sur l’image machine finale. Vous avez la possibilité d’utiliser le provider shell pour lancer des commandes shell sur votre instance, le provider PowerShell sous Windows, le provider file pour envoyer des fichiers, des dossiers ou des zips sur l’instance. Ce sont les providers de base. Voici un exemple de bloc build utilisant un provider Shell :


build {
    name = "learn-packer"

    sources = [
        "source.docker.ubuntu",
    ]

    provisioner "shell" {
        environnement_vars = [
            "FOO=hello world",
        ]
        inline = [
            "echo Addind file to Docker container",
            "echo \"FOO is $FOO\" > example.txt"
        ]
    }
}


Point positif, la communauté, gentille comme tout, vous met à disposition tout plein d'autres providers dont UN qui nous intéresse tout particulièrement 😏 Le provider Ansible ! 🥳

Le bloc variable


variable "region" {
  type    = string
  default = "${env("AWS_REGION")}"
}

Vous devez sûrement le reconnaître, il est semblable à celui sur Terraform. En effet, les configurations des blocs variables sont les mêmes.
Il vous faut mettre le type de variable et, éventuellement, une valeur par défaut.
Mais vous pouvez également indiquer que la variable est sensible, pour ne pas faire apparaître sa valeur lors de l'exécution de Packer comme ceci :


variable "foo" {
    sensitive = true
    default   = {
        key = "SECR3TP4SSW0RD"
    }
}

Le bloc amazon-ami

Ce bloc vous permet de trouver votre AMI (Amazon Machine Image) de base sur Amazon AWS. Il ressemble à ça :


data "amazon-ami" "basic-example" {
    filters = {
        virtualization-type = "hvm"
        name = "ubuntu/images/*ubuntu-xenial-16.04-amd64-server-*"
        root-device-type = "ebs"
    }
    owners = ["099720109477"]
    most_recent = true
}

Ce bloc à pour but de chercher votre image de base que vous allez configurer par la suite. Vous allez trouver l’image grâce à des filtres. Vous pouvez également utiliser des RegExp pour pouvoir avoir accès à la dernière version de l’image.

Le bloc source amazon-ebs

Ce bloc va vous permettre de configurer l’environnement cible où sera construite l’instance. Vous allez définir tout le réseau ou déployer votre instance, le futur nom de l’ami, la configuration du stockage et le type d’instance.


source "amazon-ebs" "instance" {
  ami_name                    = "${var.ami_name}"
  ami_users                   = ["${var.user_id}"]
  associate_public_ip_address = "true"
  instance_type               = "t3.medium"
  launch_block_device_mappings {
    delete_on_termination = true
    device_name           = "/dev/xvda"
    encrypted             = true
    kms_key_id            = "alias/${var.aws_target_account}"
    volume_size           = 100
  }
  max_retries  = "2"
  source_ami   = "${data.amazon-ami.basic-example.id}"
  ssh_username = "ec2-user"
}

Comment générer ses images

Nous cherchons à construire des images machine pour plusieurs besoins sur différents environnements. Nous gérons par exemple des instances de proxy ou des instances pour notre orchestrateur de containers. Pour ça nous avons des configurations différentes pour chaque type d'image, c’est pour cette raison que nous utilisons les modèles Packer. Finalement, tout dépend de votre infrastructure et de vos besoins !

Schématisation

Voici un grand schéma, complexe et complet, que nous allons expliquer dans la suite.

Pré-requis

Tout d’abord, nous devons créer le conteneur que nous allons utiliser dans GitLab CI.
Nous avons besoin de tous les outils nécessaires pour lancer nos images. Pour cela nous allons installer Ansible, Packer, l’AWS Cli et toutes les librairies nécessaires au bon fonctionnement de ces outils dont jinja2, netaddr, Boto3. Ce conteneur, nous allons l’appeler notre Workstation.

Il vous faut également créer un utilisateur AWS IAM. Voici les droits qui lui seront associés :
Les droits pour utiliser la clé KMS qui chiffrera le volume EBS créé par Packer
Les droits EC2 pour créer l’instance EC2 utilisée par Packer
Les droits EBS pour créer et gérer le volume utilisé par l’instance
Les droits IAM pour associer un rôle à cette instance

Il ne faudra pas oublier de renseigner cet utilisateur dans la CI (Sous forme de variable masquée par exemple).

Le CI / CD

Au niveau de notre CI, nous avons plusieurs points importants à prendre en compte. Nous devons faire une pipeline de CI/CD qui nous permet de déployer tout type d’image et dans tout type d’environnement. Pour cela, nous allons utiliser les Anchors et les Parallel sur Gitlab CI.


.build: &build-job
  image: registry.gitlab.com:4567/elodie.billiot/packer-aws-weshare/workstation:xxx
  stage: build
  environment:
    name: $ENVIRONEMENT
  script:
    - ansible-galaxy collection install -r ansible/requirements.yml
    - ansible-galaxy install -r ansible/requirements.yml -p ./ansible/roles --force
    - packer validate -var-file=packer/variables.pkr.hcl -var "ami_name=$ENVIRONEMENT-$AMI-$CI_COMMIT_SHORT_SHA-ami" -var "aws_target_account=$ENVIRONEMENT" -var "user_id=$ACCOUNT" packer/$AMI.pkr.hcl
    - packer build -var-file=packer/variables.pkr.hcl -var "ami_name=$ENVIRONEMENT-$AMI-$CI_COMMIT_SHORT_SHA-ami" -var "aws_target_account=$ENVIRONEMENT" -var "user_id=$ACCOUNT" packer/$AMI.pkr.hcl
  parallel:
    matrix:
      - AMI: ["master", "front", "back"]

Nous n'avons qu'un seul stage, le stage de build qui va construire notre AMI. Pour cet Anchors, qui est notre modèle de job pour tous les environnements et les types d’instance, nous avons besoin de télécharger les collections et rôles Ansible (2 premières lignes du script) et ensuite nous effectuons les commandes Packer de validation du modèle puis le build.

Dans les parallel nous avons comme paramètre la variable AMI. C’est sur cette valeur que nous allons pouvoir lancer plusieurs jobs en parallèle.

Voici les jobs spécifiques aux environnements, (ici nous prendrons l’exemple de l’environnement de sandbox et de dev).


build:sandbox:
  <<: *build-job
  variables:
    ENVIRONEMENT: sandbox
    ACCOUNT: $SANDBOX_ACCOUNT
    AWS_REGION: eu-west-1
  rules:
    - if: $CI_COMMIT_TAG == "sandbox"
      changes:
        - packer/$AMI.pkr.hcl
        - ansible/$AMI.yml
        - ansible/requirement.yml
      when: manual

build:dev:
  <<: *build-job
  variables:
    ENVIRONEMENT: dev
    ACCOUNT: $DEV_ACCOUNT
    AWS_REGION: eu-west-1
  rules:
    - if: $CI_COMMIT_TAG == "develop"
      changes:
        - packer/$AMI.pkr.hcl
        - ansible/$AMI.yml
        - ansible/requirement.yml
        - config
      when: manual

Ces jobs se lancent seulement si certains fichiers ont été modifiés sur une branche particulière.

Après que le build soit passé, vous avez enfin une AMI !

Vous pouvez maintenant aller dans votre code Terraform et utiliser cette nouvelle AMI.

Voici un exemple avec la data aws_ami et le remplacement dans un launch template AWS :


data "aws_ami" "new-linux" {
  most_recent = true

  filter {
    name   = "name"
    values = ["staging-new-*-ami"]
  }

  owners = ["123456789"]
}


resource "aws_launch_template" "new_launch_template" {
  name_prefix                          = "${terraform.workspace}-new-lt"
  image_id                             = data.aws_ami.new-linux.id
  instance_initiated_shutdown_behavior = "stop"
  instance_type                        = "t3.medium"
  key_name                             = "test-ssh"
  vpc_security_group_ids               = [aws_vpc.id]
}

Et voilà ! Vous avez industrialisé la construction de vos images.
Pour finir, c’est un projet qui peut clairement vous aider pour industrialiser la création de vos images machines. C’est un super projet d’automatisation qui peut vous simplifier la vie. En tant que DevOps, ce sujet peut devenir rapidement une priorité ! Si vous voulez mener ce projet, les sources sont disponibles ici https://gitlab.com/elodie.billiot/packer-x-aws