Le Labo #26 | Deployer sur Openstack via Terraform, Jenkins et Ansible

Pour une fois, je ne vais pas aborder le déploiement sur DigitalOcean, Azure ou même Google Cloud…Non, c’est fois ci, ce sera Openstack. Mais pas n’importe comment, ce sera toujours avec Terraform et sur plusieurs environnements différents impliquant donc plusieurs fichiers de variables différents.
Je n’avais pas encore démontré l’utilisation des dépendances implicites ou d’utilisations de l’instruction lookup pour ittérer sur les listes de variables…Mais assez de tricotage/brodage, passons à l’action proprement dite.

Terraform in action

Entrons dans le vif du sujet assez rapidement car j’imagine que vous connaissez déjà assez bien la technologie.
Sur Openstack avant de pouvoir créer des serveurs, il est nécessaire de commencer par l’infrastructure, en l’occurence :

  • Network

    resource "openstack_networking_network_v2" "network" {
    count			 = "${length(var.network)}"
    name           = "${lookup(var.network[count.index],"name")}"
    admin_state_up = "${lookup(var.network[count.index],"admin_state_up")}"
    region         = "${lookup(var.network[count.index],"region")? 1 : 0}"
    }
    
  • Subnet

    resource "openstack_networking_subnet_v2" "subnet" {  
    count		 = "${length(var.subnet)}"  
    name       = "${lookup(var.subnet[count.index],"name")}"  
    cidr       = "${lookup(var.subnet[count.index],"cidr")}"  
    network_id = "${element(openstack_networking_network_v2.network.*.id,lookup(var.subnet[count.index],network_id))}"  
    ip_version = "${lookup(var.subnet[count.index],"ip_version")}"  
    region     = "${lookup(var.network[count.index],"region")}"  
    }
    
  • Router

    resource "openstack_networking_router_v2" "router" {  
    count				  = "${length(var.router)}"  
    name                = "${lookup(var.router,"name")}"  
    admin_state_up      = "${lookip(var.router,"admin_state_up")}"  
    external_network_id = "${element(openstack_networking_network_v2.network.*.id,lookup(var.router[count.index],"network_id"))}"  
    region              = "${lookup(var.network,"region")}"  
    }
    
  • Router Interface

    resource "openstack_networking_router_interface_v2" "router_interface" {  
    count     = "${ "${length(var.router)}" == "0" ? "0" : "${lenght(var.subnet)}" }"  
    router_id = "${element(openstack_networking_router_v2.routeur.*.id,lookup(var.router_interface[count.index],"routeur_id"))}"  
    subnet_id = "${element(openstack_networking_subnet_v2.subnet.*.id,lookup(var.router_interface[count.index],"subnet_id"))}"  
    }
    
  • Floating IP

    resource "openstack_networking_floatingip_v2" "floating_ip" {  
    count  = "${lenght(var.floating_ip)}"  
    pool   = "${lookup(var.floating_ip[count.index],"pool")}"  
    region = "${lookup(var.network,"region")}"  
    }
    

Une fois la partie réseau définie, autant passer à l’étape suivante : Les sec_group et sec_group_rule :

resource "openstack_compute_secgroup_v2" "sec_group" {
  count       = "${length(var.sec_group)}"
  description = "${lookup(var.sec_group[count.index],"description")}"
  name        = "${lookup(var.sec_group[count.index],"name")}"
  region      = "${lookup(var.network,"region")}"
}
resource "openstack_networking_secgroup_rule_v2" "sec_group_rule" {
  count             = "${ "${length(var.sec_group)}" == "0" ? "0" : "${length(var.sec_group_rule)}" }"
  security_group_id = "${element(openstack_networking_secgroup_v2.sec_group.*.id,lookup(var.sec_group_rule[count.index],"sec_group_id"))}"
  direction         = "${lookup(var.sec_group_rule[count.index],"direction")}"
  ethertype         = "${lookup(var.sec_group_rule[count.index],"ethertype")}"
  protocol          = "${lookup(var.sec_group_rule[count.index],"protocol")}"
  port_range_min    = "${lookup(var.sec_group_rule[count.index],"port_range_min")}"
  port_range_max    = "${lookup(var.sec_group_rule[count.index],"port_range_max")}"
  remote_ip_prefix  = "${lookup(var.sec_group_rule[count.index],"remote_ip_prefix")}"
  region            = "${lookup(var.sec_group_rule[count.index],"region")}"
}

Pour le reste (les serveurs), je vous laisse vous en occuper…Après tout, vous devriez avoir compris le principe des dépendances implicites et des fichiers de variables externes (les .tfvars).
Mais si vous avez un peu de mal, voici le module Server que j’utilise :

data "openstack_networking_secgroup_v2" "os_sec_group" {
  count = "${length(var.os_instance)}"
  name  = "${lookup(var.os_instance[count.index],"name")}"
}

data "openstack_networking_network_v2" "os_network" {
  count = "${length(var.network)}"
  name  = "${lookup(var.network[count.index],"name")}"
}

resource "openstack_compute_keypair_v2" "os_keypair" {
  count      = "${length(var.os_keypair)}"
  name       = "${lookup(var.os_keypair[count.index],"name")}"
  public_key = "${lookup(var.os_keypair[count.index],"key_file")}"
}

resource "openstack_compute_instance_v2" "os_instance" {
  count       = "${length(var.os_instance)}"
  name        = "${lookup(var.os_instance[count.index],"name")}"
  image_name  = "${lookup(var.os_instance[count.index],"image_name")}"
  flavor_name = "${lookup(var.os_instance[count.index],"flavor_name")}"
  key_pair    = "${element(openstack_compute_keypair_v2.os_keypair.*.id,lookup(var.os_instance[count.index],"key_pair_id"))}"

  security_groups = [
    "${var.default_sec_group}",
    "${element(data.openstack_networking_secgroup_v2.os_sec_group.*.id,lookup(var.os_instance[count.index],"sec_group_id"))}",
  ]

  network {
    name        = "${data.openstack_networking_network_v2.os_network.name}"
    fixed_ip_v4 = "${lookup(var.os_instance[count.index],"fixed_ip_v4")}"
  }
}

resource "openstack_compute_floatingip_associate_v2" "os_floatip_assoc" {
  count       = "${length(var.float_ip)}"
  floating_ip = "${lookup(var.float_ip[count.index],"floating_ip")}"
  instance_id = "${element(openstack_compute_instance_v2.os_instance.*.id,lookup(var.float_ip[count.index],"id_instance"))}"
}

Travailler sur Openstack est relativement simple :
- Un réseau de base sur lequel brancher celui que vous allez créer (vous ne pourrez pas brancher vos serveurs dessus),
- Un Security Group de base…à ne surtout pas oublier de définir pour vos serveurs (sinon, vous ne pourrez pas vous connecter dessus ni même essayer de contacter chacun d’entre eux),
- Un fichier à télécharger pour pouvoir déployer tout ce dont vous avez besoin pour travailler et qui évite d’avoir a hardcoder des variables d’environnement dans vos states Terraform.

Lets industrialize it

Passons à Jenkins…Pour le moment et contrairement à d’autres outils/Providers Cloud, il n’existe pas encore de plugin Terraform, ce qui fait que pour Terraformer son infrastructure Cloud, il faut le faire manuellement dans Jenkins :

stage("Terraform") {
	steps {
		sh '''
		cd terraform/resources
		terraform init
		terraform apply -var-files vars.tfvars -auto-apply
		'''
	}
}

Sans biensûr oublier de définir les variables d’environnement adequates…ce qui revient à les hardcoder dans le pipeline…Mais il y a un autre moyen : Déployer via Jenkins un conteneur Docker qui s’occupera de tout le travail de Terraform.
Non, ce n’est pas tiré par les cheveux…Le Dockerfile est extrêment simple :

FROM node:10.10.0-jessie

WORKDIR /opt/

ARG URL_PROXY
ARG LOGIN_PROXY
ARG PASS_PROXY
ARG TARGET
ARG TENANT
ARG TF_VERSION

ENV http_proxy http://${LOGIN_PROXY}:${PASS_PROXY}@${URL_PROXY}
ENV https_proxy http://${URL_PROXY}
ENV HTTPS_PROXY http://${URL_PROXY}
ENV HTTP_PROXY http://${URL_PROXY}

COPY ${TARGET}01_${TENANT}-openrc.sh .
COPY terraform.sh terraform.sh

RUN echo Acquire::http::proxy \"http://${LOGIN_PROXY}:${PASS_PROXY}@${URL_PROXY}\"; > /etc/apt/apt.conf.d/Proxy && \
    echo Acquire::https::proxy \"http://${LOGIN_PROXY}:${PASS_PROXY}@${URL_PROXY}\"; >> /etc/apt/apt.conf.d/Proxy && \
    apt-get update && \
    apt-get install -y git unzip && \
    curl https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip && \
    unzip terraform_${TF_VERSION}_linux_amd64.zip && \
    chmod +x terraform && chmod +x terraform.sh && chmod +x ${TARGET}01_${TENANT}-openrc.sh

CMD ["./terraform.sh"]

J’ai sélectionné une image avec nodejs, mais on peut utiliser n’importe quoi…je vous rassure. Autre chose : J’ai défini des build-arg pour gérer des informations relatives à un éventuel proxy à intégrer dans votre conteneur, le ficheir contenant les variable d’environnement ainsi que la version de Terraform à télécharger depuis le site officiel.
Comme vous pouvez le constater, il y a aussi un terraform.sh qui se lancera au démarrage du conteneur…et c’est lui qui va gérer tout le déploiement.

#!/bin/sh
set -x

pid=0

_stop() {
  echo "Stopping"
  kill -SIGINT "$PID"
  wait "$PID"
  exit 143;
}

trap 'kill ${!}; _stop' SIGTERM SIGINT

cd /tmp/

ACTION=${ACTION}
PROXY_LOGIN=${PROXY_LOGIN}
PROXY_PASSWORD=${PROXY_PASSWORD}
PROXY_URL=${PROXY_URL}
GIT_URL=${GIT_URL}
PROJECT=${PROJECT}
TARGET=${TARGET}
TENANT=${TENANT}
ELEMENT=${ELEMENT}

VARFILE="$(echo $TARGET | awk '{print tolower($0)}')_vars.tfvars"
ENVFILE="$(echo $TARGET)01_$(echo $TENANT)-openrc.sh"

export HTTP_PROXY="http://$PROXY_LOGIN:$PROXY_PASSWORD@${PROXY_URL}"
export HTTPS_PROXY="http://$PROXY_LOGIN:$PROXY_PASSWORD@${PROXY_URL}"
export http_proxy="http://$PROXY_LOGIN:$PROXY_PASSWORD@${PROXY_URL}"
export https_proxy="http://$PROXY_LOGIN:$PROXY_PASSWORD@${PROXY_URL}"

git clone https://$PROXY_LOGIN:$PROXY_PASSWORD@$GIT_URL/$PROJECT/terraform.git 

. /opt/$ENVFILE

for element in $ELEMENT; do
	cd /tmp/terraform/resources/$element && \
		/opt/terraform init && \
		/opt/terraform plan -var-file=/tmp/terraform/resources/$VARFILE && \
		/opt/terraform $ACTION -var-file=/tmp/terraform/resources/$VARFILE
done

pid="$!"

En démarrant Terraform depuis un conteneur Docker, il devient possible de ne plus être dépendant de diverses limites techniques relative au serveur Jenkins et à ses slaves…surtout si ce n’est pas vous qui les gérez.

Let’s Ansible it

Contrairement à Terraform, Ansible dispose d’un plugin pour Jenkins et est extrêmement bien documenté…On peut l’utiliser aussi bien avez un pipelnine déclaratif que scripté (qui eux sont relativement compliqué à monter car necessitant une connaissanc)e relativement étendue de Groovy - Voir ci-dessous un petit exemple).

ansiblePlaybook(
        playbook: 'playbook.yml',
        inventory: 'inventory.ini',
        tags: 'tags',
        colorized: true,
        sudoUser: 'root',
        extraVars: 'ansible_python_interpreter=/usr/bin/python'
)

Par contre, il est tout à fait possible d’utiliser un conteneur Docker, bien que pour Openstack il y ait besoin de nombreux packages installables via pip :

python-openstackclient  
shade   
argparse   
python-swiftclient  
appdirs   
iso8601   
os_service_types   
requestsexceptions   
munch   
deprecation  
ipaddress   
jsonpatch   
dogpile.cache   
jmespath   
netifaces   
decorator  

Selon L’infrastructure sur laquelle vous êtes, cette image Docker peut être très longue à builder…je recommande de le faire localement et de la publier sur un Docker Registry avant de l’utiliser dans Jenkins.

Return to Jenkins

Maintenant que vous avez vos images Docker pour Terraform et Ansible, retournons sur Jenkins et son plugin Docker. Son utilisation est assez simple :
- Pour build une image : docker.build("name","/build_args/ --build-arg ENVIRONMENT=${TARGET} ./Ansible/)
- Pour démarrer un conteneur : docker.image("image_name").withRun(/run_args/)

Let’s rock

Ca faisait longtemps que je n’avais pas écrit d’article sur Jenkins…Je me suis dis ca pouvait être une idée d’aborder cette technologie malgré la percée effectuée par Bamboo, Circle-CI et autres.