Le Labo #26 | Deployer sur Openstack via Terraform, Jenkins et Ansible
21/Oct 2018
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.