Le blog tech de Nicolas Steinmetz (Time Series, IoT, Web, Ops, Data)
Depuis que j’ai migré des sites sous kubernetes, j’avais perdu l’automatisation du déploiement de mes conteneurs. Pour ce site, je modifiais donc le site et une fois le git push
realisé, j’attendais que Gitlab-CI crée mon conteneur. Je récupérai alors le tag du conteneur que je mettais dans le dépôt git où je stocke mes fichiers de configuration pour kubernetes. Une fois le tag mis à jour, je pouvais procéder au déploiement de mon conteneur. Il était temps d’améliorer ce workflow.
Gitlab propose depuis un moment une intégration avec kubernetes mais je lui trouve quelques inconvénients au regard de mes besoins :
Mon besoin pourrait se résumer à pouvoir interagir avec mon cluster au travers de kubectl et de pouvoir y déployer la nouvelle version du conteneur que je viens de créer. Cela suppose alors d’avoir 3 choses :
kubeconfig
pour m’authentifier auprès du cluster et interagir avec,Utilisant le service managé d’OVH, je n’ai pas accès à tous les certificats du cluster permettant de créer de nouveaux comptes utilisateurs. Par ailleurs, pour les intégrations comme Gitlab, il est recommandé d’utiliser des Service Accounts. C’est ce que nous allons faire.
En plus du Service Account, il nous faut donner un rôle à notre compte pour qu’il puisse réaliser des actions sur le cluster. Par simplicité pour ce billet, je vais lui donner les droits d’admin au sein d’un namespace. Le compte de service pourra alors faire ce qu’il veut mais uniquement au sein du namespace en question. En cas de fuite du compte, les dégats potentiels sont donc moindres qu’avec un compter qui est admin global du cluster. Le rôle admin existe déjà sous kubernetes, il s’agit du ClusteRole admin mais qui est restreint à un namespace via le RoleBinding.
Créons le fichier gitlab-integtration.yml
avec ces éléments :
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: gitlab-example
namespace: example
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: gitlab-admin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: admin
subjects:
- kind: ServiceAccount
name: gitlab-example
namespace: example
Déployons notre configuration sur le cluster :
# Apply yml file on the cluster
kubectl apply -f gitlab-integration.yml
serviceaccount/gitlab-example created
rolebinding.rbac.authorization.k8s.io/gitlab-admin created
Pour alimenter notre fichier kubeconfig, il nous faut récupérer le token :
# Get secret's name from service account
SECRETNAME=`kubectl -n example get sa/gitlab-example -o jsonpath='{.secrets[0].name}'`
# Get token from secret, encoded in base64
TOKEN=`kubectl -n example get secret $SECRETNAME -o jsonpath='{.data.token}'`
# Decode token
CLEAR_TOKEN=`echo $TOKEN |base64 --decode`
En prenant votre fichier kubeconfig de référence, vous pouvez alors créer une copie sous le nom kubeconfig-gitlab-example.yml
et l’éditer de la façon suivante :
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: <Existing certificate in a base64 format from your original kubeconfig file>
server: <url of your k8s http endpoint like https://localhost:6443/ >
name: kubernetes # adjust your cluster name
contexts:
- context:
cluster: kubernetes # adjust your cluster name
namespace: example # adjust your namespace
user: gitlab-example # adujust your user
name: kubernetes-ovh # adujust your context
current-context: kubernetes-ovh # adujust your context
kind: Config
preferences: {}
users:
- name: gitlab-example # adujust your user
user:
token: <Content of the CLEAR_TOKEN variable>
Vous pouvez tester son bon fonctionnement via :
# Fetch example resources if any:
kubectl --kubeconfig=./kubeconfig-gitlab-example.yml get all
...
# Check you can't access other namespaces information, like kube-system:
kubectl --kubeconfig=./kubeconfig-gitlab-example.yml get all -n kube-system
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:example:gitlab-example" cannot list resource "pods" in API group "" in the namespace "kube-system"
Error from server (Forbidden): replicationcontrollers is forbidden: User "system:serviceaccount:example:gitlab-example" cannot list resource "replicationcontrollers" in API group "" in the namespace "kube-system"
Error from server (Forbidden): services is forbidden: User "system:serviceaccount:example:gitlab-example" cannot list resource "services" in API group "" in the namespace "kube-system"
Error from server (Forbidden): daemonsets.apps is forbidden: User "system:serviceaccount:example:gitlab-example" cannot list resource "daemonsets" in API group "apps" in the namespace "kube-system"
Error from server (Forbidden): deployments.apps is forbidden: User "system:serviceaccount:example:gitlab-example" cannot list resource "deployments" in API group "apps" in the namespace "kube-system"
Error from server (Forbidden): replicasets.apps is forbidden: User "system:serviceaccount:example:gitlab-example" cannot list resource "replicasets" in API group "apps" in the namespace "kube-system"
Error from server (Forbidden): statefulsets.apps is forbidden: User "system:serviceaccount:example:gitlab-example" cannot list resource "statefulsets" in API group "apps" in the namespace "kube-system"
Error from server (Forbidden): horizontalpodautoscalers.autoscaling is forbidden: User "system:serviceaccount:example:gitlab-example" cannot list resource "horizontalpodautoscalers" in API group "autoscaling" in the namespace "kube-system"
Error from server (Forbidden): jobs.batch is forbidden: User "system:serviceaccount:example:gitlab-example" cannot list resource "jobs" in API group "batch" in the namespace "kube-system"
Error from server (Forbidden): cronjobs.batch is forbidden: User "system:serviceaccount:example:gitlab-example" cannot list resource "cronjobs" in API group "batch" in the namespace "kube-system"
Gitlab permet de stocker des variables. Dans le cas d’un fichier kubeconfig, on va vouloir ne jamais afficher son contenu dans les logs ou autre. Pour cela il est possible de masquer vos variables en respectant quelques contraintes et notamment que la valeur de la variable tienne sur une seule ligne.
Nous allons donc encoder le fichier en base64 et rajouter un argument pour que tout soit sur une seule ligne (et non pas sur plusieurs lignes par défaut):
# create a one line base64 version of kubeconfig file
cat kubeconfig-gitlab-example.yml | base64 -w 0
Copier le contenu obtenu dans une variable que nous appelerons KUBECONFIG
et dont on cochera bien la case “Mask variable”. Une fois la variable sauvée, vous avez ceci :
Soit le fichier .gitlab-ci.yml
suivant:
---
stages:
- publish
- image
- deploy
publish:
image: $CI_REGISTRY/nsteinmetz/hugo:latest
artifacts:
paths:
- public
expire_in: 1 day
only:
- master
- web
script:
- hugo
stage: publish
tags:
- go
docker:
stage: image
image: docker:stable
services:
- docker:dind
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_DRIVER: overlay2
RELEASE_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA-$CI_PIPELINE_ID-$CI_JOB_ID
before_script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
script:
- echo "IMAGE=${RELEASE_IMAGE}" >> docker.env
- docker build --pull -t $RELEASE_IMAGE .
- docker push $RELEASE_IMAGE
when: on_success
tags:
- go
artifacts:
reports:
dotenv: docker.env
kube:
stage: deploy
script:
- echo $KUBECONFIG | base64 --decode > kubeconfig
- export KUBECONFIG=`pwd`/kubeconfig
- sed -i -e "s|IMAGE|${IMAGE}|g" deployment.yml
- kubectl apply -f deployment.yml
needs:
- job: docker
artifacts: true
when: on_success
tags:
- shell
Petite explication rapide :
publish
va générer la version html du site et la stocker sous la forme d’un artefact qui sera passé aux jobs suivants,docker
va créer l’image en mettant l’artefact du job précédent dans un conteneur nginx et le publier dans la registry gitlab avec le nom suivant gitlab.registry/group/project:<short commit>-<pipeline id>-<job id>
kube
va récupérer le contenu de la variable KUBECONFIG
, le décoder et créer un fichier kubeconfig
. On initialise la variable d’environnement KUBECONFIG
pour que kubectl puisse l’utiliser. On met à jour la référence de l’image docker obtenue précédemment dans le fichier deployment.yml
qui sert de modèle de déploiement. On applique le fichier obtenu sur le cluster kubernetes pour mettre à jour le déploiement.Le point d’attention ici est que le passage de la variable RELEASE_IMAGE
se fait via un dotenv
qui est créé sous la forme d’un artefact à l’étape docker
et est donc disponible à l’étape kube
. Cela devrait être automatique mais j’ai ajouté une dépendance explicite via la directive needs
. Lors de l’étape kube
, le contenu du fichier docker.env
est disponible sous la forme de variables d’environnement. On peut alors faire la substitution de notre placeholder par la valeur voulue dans deployment.yml
.
Tout ce mécanisme est expliqué dnas la doc des variables gitlab et sur les variables d’environnement héritées. Attention, il vous faut Gitlab 13.0+ pour avoir cette fonctionnalité et en plus, il faut préalablement activer ce feature flag.
sudo gitlab-rails console
Feature.enable(:ci_dependency_variables)
En conclusion, nous avons vu comment :
kubeconfig
utilisant notre service account dans Gitlab sous la forme d’une variable masquée,Ainsi, toute mise à jour de master conduira à une mise à jour du déploiement associé au sein du cluster kubernetes et ne nécessitera plus d’interventions manuelles comme précédemment. Avec un service account lié à un namespace, on évite aussi de s’exposer inutilement en cas de fuite des identifiants.
Pour faire suite au billet sur le déploiement de Traefik sous la forme d’un DaemonSet chez OVH, j’ai profité de la sortie en mode beta des Load Balancers pour revoir ma copie :
Par simplicité, je n’ai toujours qu’une node en plus du master fourni par OVH. Cela m’évite la problématique du stockage distribué des certificats. Cela fera l’objet d’un autre billet.
Créons le namespace traefik :
# Create namespace
kubectl create ns traefik
# Change context to this namespace so that all commands are by default run for this namespace
# see https://github.com/ahmetb/kubectx
kubens traefik
Commençons par traefik/rbac.yml
- le fichier défini le compte de service (Service Account
), le rôle au niveau du cluster (Cluster Role
) et la liaison entre le rôle et le compte de service (Cluster Role Binding
)
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: traefik-ingress-controller
namespace: traefik
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: traefik-ingress-controller
rules:
- apiGroups:
- ""
resources:
- services
- endpoints
- secrets
verbs:
- get
- list
- watch
- apiGroups:
- extensions
resources:
- ingresses
verbs:
- get
- list
- watch
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: traefik-ingress-controller
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: traefik-ingress-controller
subjects:
- kind: ServiceAccount
name: traefik-ingress-controller
namespace: traefik
Ensuite, pour Traefik, j’ai besoin d’un fichier traefik.toml
avec la configuration que je mets à disposition sous la forme d’une ConfigMap
dans un fichier traefik/traefik-toml-configmap.yml
:
apiVersion: v1
kind: ConfigMap
metadata:
name: traefik-conf
data:
traefik.toml: |
defaultEntryPoints = ["http", "https"]
logLevel = "INFO"
insecureSkipVerify = true
[entryPoints]
[entryPoints.http]
address = ":80"
[entryPoints.http.redirect]
entryPoint = "https"
[entryPoints.https]
address = ":443"
[entryPoints.https.tls]
[entryPoints.api]
address = ":8080"
[acme]
email = "contact@cerenit.fr"
storage = "/acme/acme.json"
entryPoint = "https"
onHostRule = true
[acme.httpChallenge]
entryPoint = "http"
[api]
entryPoint = "api"
dashboard = true
debug = false
[kubernetes]
Le dashboard est à protéger par une authentification pour éviter tout accès non souhaité. Je l’ai supprimé de la configuration par simplicité.
Ensuite, pour stocker mes certificats, il me faut un volume que je défini via le fichier traefik/traefik-certificates-pvc.yml
:
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: traefik-certificates
spec:
accessModes:
- ReadWriteOnce
volumeMode: Filesystem
resources:
requests:
storage: 1Gi
storageClassName: cinder-classic
1 Go pour des certificats, c’est clairement trop mais il n’est pas possible pour le moment d’avoir un stockage plus réduit.
Je peux donc enfin déployer Traefik via le fichier traefik/traefik-deployment.yml
:
---
kind: Deployment
apiVersion: extensions/v1beta1
metadata:
name: traefik-ingress-controller
labels:
k8s-app: traefik-ingress-lb
spec:
replicas: 1
selector:
matchLabels:
k8s-app: traefik-ingress-lb
template:
metadata:
labels:
k8s-app: traefik-ingress-lb
name: traefik-ingress-lb
spec:
serviceAccountName: traefik-ingress-controller
terminationGracePeriodSeconds: 60
containers:
- image: traefik:1.7.7
name: traefik-ingress-lb
volumeMounts:
- mountPath: /config
name: traefik-config
- mountPath: /acme
name: certificates
ports:
- name: http
containerPort: 80
- name: admin
containerPort: 8080
- name: secure
containerPort: 443
args:
- --configfile=/config/traefik.toml
volumes:
- name: traefik-config
configMap:
name: traefik-conf
- name: certificates
persistentVolumeClaim:
claimName: traefik-certificates
Nous déployons donc :
Pour permettre au cluster d’accéder aux différents ports, il faut définir un service via le fichier traefik-service-clusterip.yml
:
---
kind: Service
apiVersion: v1
metadata:
name: traefik-ingress-service-clusterip
spec:
selector:
k8s-app: traefik-ingress-lb
ports:
- protocol: TCP
port: 80
name: web
- protocol: TCP
port: 8080
name: admin
- protocol: TCP
port: 443
name: secure
type: ClusterIP
Et pour avoir un accès de l’extérieur, il faut instancier un load-balancer via le fichier traefik/traefik-service-loadbalancer.yml
kind: Service
apiVersion: v1
metadata:
name: traefik-ingress-service-lb
spec:
selector:
k8s-app: traefik-ingress-lb
ports:
- protocol: TCP
port: 80
name: web
- protocol: TCP
port: 443
name: secure
type: LoadBalancer
Pour donner l’accès au dashboard via une url sécurisée par un certificat Let’s Encrypt, il faut déclarer un Ingress, dans le fichier traefik/traefik-api-ingress.yml
:
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: traefik-web-ui
spec:
rules:
- host: traefik.k8s.cerenit.fr
http:
paths:
- path: /
backend:
serviceName: traefik-ingress-service
servicePort: admin
Il ne nous reste plus qu’à faire :
# Create k8s ressources for traefik
kubectl create -f traefik/
# Watch service to get IPs
kubectl get svc -w
Une fois votre IP obtenue, il suffit de faire pointer votre entrée DNS vers cette IP ou de tester via :
curl -H "Host: traefik.k8s.cerenit.fr" https://xxx.xxx.xxx.xxx/
Pour l’obtention du certificat Let’s Encrypt, il faut que votre enregistrement DNS soit à jour préalablement. Sinon vous aurez un certificat autosigné par Traefik en attendant.
Dès lors, vous pouvez accéder au dashboard de Traefik via l’url définie. Pour donner accès à d’autres sites, il faut déclarer d’autres ingress sur le même modèle et le tour est joué.
Comparativement au dernier tutoriel :
Il reste encore le problème des stockage des certificats à résoudre pour passer à un contexte multi-nodes. Ce sera l’objet d’un prochain billet avec idéalement l’intégration de Traefik avec cert-manager (plutôt que de devoir déployer une base clé/valeur comme etcd ou consul pour y stocker les infos de traefik).
N’hésitez pas à me faire part de vos retours.