L’objectif de cet article est de proposer une solution de déploiement d’une application dans un cluster Kubernetes déployé sur AWS.
Nous allons surtout aborder l’exposition du service sur Internet (enregistrement du nom de domaine, création du certificat SSL) par configuration d’une ressource Kubernetes Ingress et expliciter sa prise en compte par des modules Kubernetes choisis.
L’application à déployer est une application Java autonome qui fournit une API REST de gestion de livres (nommée books-api).
La lecture de cet article nécessite une connaissance basique des concepts Kubernetes (Pod, Service, Deployment).
Plan
Dans la section Déploiement Initial, nous commençons par réaliser un premier déploiement de l’application ; l’application est accessible à l’intérieur du cluster.
Dans la section Exposition du Service, nous exposons le service sur Internet ; le service est accessible mais pas via le bon nom de domaine.
Dans la section Enregistrement du Nom DNS, nous créons un enregistrement DNS pour que le service soit accessible via le nom de domaine attendu.
Dans la section Création d’un certificat, nous créons un certificat SSL pour exposer l’application en HTTPS.
Les 4 sections Déploiement Initial → Création d’un certificat sont structurées de manière uniforme : Choix d’un module Kubernetes qui répond à la problématique, Configuration Ingress et Validation de la solution.
Dans la section Schéma d’architecture, nous donnons une vue globale de l’architecture déployée en termes de briques techniques AWS et composants K8s.
Déploiement Initial
Pour déployer l’Application, nous avons configuré les ressources K8s suivantes :
- 1 Deployment qui spécifie comment déployer l’application
- 1 Service qui fournit un point d’accès à l’application, interne au cluster
Comme indiqué par la ligne de commande kubectl ci-dessous, le Service a une adresse IP privée au cluster (CLUSTER-IP en 10.xx.xx.xx) et ne possède pas d’adresse IP externe (EXTERNAL-IP
$> kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
books-api ClusterIP 10.111.224.177 <pending> 80:31892/TCP 9s
Validation
Voici un exemple d’appel du service à partir d’un noeud du cluster
$> curl http://10.111.224.177/api/books
[{"id":1,"title":"Design Pattern","author":"Erich Gamma, John Vlissides, Ralph E.. Johnson et Richard Helm"},{"id":2,"title":"Effective Java","author":"Joshua Bloch"}]
Exposition du Service
Comme expliqué par la documentation K8s, la solution préconisée pour exposer un Service K8s en dehors du cluster, est de configurer une ressource Ingress.
Nous utilisons l’implémentation ingress-nginx basée sur NGinx.
Nous avons installé ingress-nginx en utilisant ce package helm (format de package kubernetes) trouvé sur Helm Hub (référentiel de packages Helm prêts à l’emploi).
- Un package helm s’installe dans une ligne de commande (via un helm install) avec possibilité de surcharger des valeurs par défaut du package (option --set).
- Pour l’installation de ce package helm, nous n’avons pas eu besoin de surcharger les valeurs par défaut du package.
Pour exposer les services du cluster (déployé dans un cloud AWS), le Service ingress-nginx :
- Déploie un serveur NGinx dans le cluster
- Crée un Load Balancer AWS “classic” en frontal du serveur NGinx créé.
Pour que le Service ingress-nginx gère un Load Balancer AWS, nous avons associé au profil des instances EC2 du cluster les autorisations nécessaires (lister/lire/écrire) sur les ressources elasticloadbalancing.
La ligne de commande ci-dessous affiche les informations du contrôleur ingress-nginx
- Le contrôleur possède une adresse IP externe (en xx.elb.amazonaws.com).
- L’adresse externe du contrôleur NGinx est l’adresse du Load Balancer AWS
$> kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx-controller LoadBalancer 10.110.129.140 aab0825d933a14332bb50426fac23040-116335780.us-east-1.elb.amazonaws.com 80:31147/TCP,443:32405/TCP 2m28s
Configuration Ingress
Dans la configuration de la ressource Ingress, il faut spécifier :
- Le nom de domaine de l’application (books-api.ippon.fr)
- Les règles de redirection des requêtes HTTP reçues vers le Service K8s.
- L’implémentation Ingress à utiliser
Voici la configuration Ingress de notre application.
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: nginx (implémentation ingress-nginx)
name: books-api
spec:
rules: (règles de redirection)
- host: books-api.ippon.fr (nom de domaine de l'application)
http:
paths:
- backend:
serviceName: books-api (nom du Service)
servicePort: 80
path: /
Au déploiement de l’Ingress, la configuration du serveur NGinx est modifiée pour intégrer les règles de redirection HTTP. Ainsi, toutes les requêtes envoyées au host books-api.ippon.fr sont envoyées au Service books-api
Validation
Voici deux appels du service exposé envoyés au Load Balancer en passant ou pas le nom de domaine de l’application.
- Le premier appel est KO car il ne respecte pas les règles de routage de l’Ingress (requête non envoyée au Host books-api.ippon.fr)
- Le second est OK car le Host est passé via un header HTTP
# appel du service exposé via le Load Balancer (Routage KO)
$> curl https://aab0825d933a14332bb50426fac23040-116335780.us-east-1.elb.amazonaws.com/api/books
default backend - 404
# appel du service avec le nom de domaine passé en Header (Routage OK)
$> curl -k -H "Host: books-api.ippon.fr" https://aab0825d933a14332bb50426fac23040-116335780.us-east-1.elb.amazonaws.com/api/books
[{"id":1,"title":"Design Pattern","author":"Erich Gamma, John Vlissides, Ralph E.. Johnson et Richard Helm"},{"id":2,"title":"Effective Java","author":"Joshua Bloch"}]
Enregistrement du Nom DNS
Faute de pouvoir configurer le nom de domaine du Load Balancer créé par ingress-nginx, nous avons créé un enregistrement DNS qui associe le nom de domaine de notre application (i.e. books-api.ippon.fr) au nom de domaine du Load Balancer (du contrôleur ingress-nginx créé à l’étape précédente).
Le module external-dns permet justement de créer ces enregistrements dans une zone hébergée Route 53 (DNS Amazon).
Nous avons installé external-dns en utilisant ce package helm en spécifiant le fournisseur DNS (aws), le type de zone hébergée (public) et l’id de la zone hébergée ({{hosted_zone}}).
# installation du package helm
helm install --name external-dns stable/external-dns --set provider=aws --set aws.zoneType=public --set txtOwnerId={{hosted_zone}} --set rbac.create=true
Configuration Ingress
La mise en place d’external-dns ne nécessite pas de changer la ressource Ingress.
Au déploiement de l’Ingress, le contrôleur external-dns crée deux enregistrements DNS (dans la zone hébergée indiquée)
- Un enregistrement ALIAS vers l’adresse externe de l’Ingress
- Un enregistrement TXT qui spécifie l’origine de l’enregistrement (le nom de l’Ingress)
Pour que le Service external-dns soit capable de gérer un Load Balancer AWS, nous avons associé au profil des instances EC2 du cluster les autorisations nécessaires sur les ressources route53 . Cette information se trouve dans la documentation officielle.
Validation
Voici un exemple d’appel du service exposé en utilisant le nom de domaine :
- L’option “-k” est nécessaire car nous n’avons pas encore créé le certificat SSL
# appel du service exposé avec le nom de domaine (Routage OK)
$> curl -k https://books-api.ippon.fr/api/books
[{"id":1,"title":"Design Pattern","author":"Erich Gamma, John Vlissides, Ralph E.. Johnson et Richard Helm"},{"id":2,"title":"Effective Java","author":"Joshua Bloch"}]
Création du Certificat
Dans cette section, nous allons créer le certificat via le module cert-manager. Nous avons installé cert-manager en utilisant ce package helm (sans surcharge de paramètres du package helm).
Nous configurons cert-manager de manière à ce qu’il crée les certificats via l’autorité de certification Let’s Encrypt.
Let’s Encrypt implémente le protocole ACME HTTP qui permet à un client de demander la création d’un certificat pour un nom de domaine. Le client doit prouver (via un challenge) qu’il est propriétaire du nom de domaine. Il existe différents challenges (HTTP-01, DNS-01, …) qui ont des contraintes différentes.
Dans notre cas, nous avons choisi d’utiliser le challenge DNS-01. Dans ce challenge, le client crée un enregistrement TXT de nom _acme-challenge.<YOUR_DOMAIN> valorisé avec un token fourni par Let’s Encrypt.
Pour que cert-manager puisse demander la création de certificats, il faut configurer un Issuer (ou un ClusterIssuer).
- Issuer : Autorité de Certification (i.e. Let’s Encrypt).
- ClusterIssuer : Autorité de Certification visible au niveau du cluster (et pas seulement dans un namespace)
apiVersion: certmanager.k8s.io/v1alpha1
kind: ClusterIssuer
metadata:
name: letsencrypt-issuer
spec:
acme:
# The ACME server URL
server: https://acme-v02.api.letsencrypt.org/directory
# Email address used for ACME registration
email: melkouhen@ippon.fr
# Name of a secret used to store the ACME account private key
privateKeySecretRef:
name: letsencrypt-issuer
# Enable the DNS-01 challenge provider
dns01:
providers:
- name: dns
route53:
region: us-east-1
Configuration Ingress
Pour générer le certificat, il faut d’abord créer une ressource Certificate.
Le Certificate représente une demande de création du certificat (envoyé à l’Issuer). Si les challenges passent, cette demande se conclue par la création d’un certificat stocké dans un secret kubernetes (books-api-cert).
kind: Certificate
spec:
acme:
config:
- dns01:
provider: dns (Provider DNS)
domains:
- books-api.ippon.fr
commonName: books-api.ippon.fr
dnsNames:
- books-api.ippon.fr
issuerRef:
kind: ClusterIssuer
name: letsencrypt-issuer
secretName: books-api-cert (secret contenant le certificat SSL)
Au niveau de la configuration de l’Ingress, il faut spécifier
- La liste des hosts à exposer en HTTPS
- Le nom du Issuer à utiliser (letsencrypt-issuer)
- Le nom du secret contenant le certificat SSL à utiliser (généré par le Certificate)
kind: Ingress
metadata:
annotations:
certmanager.k8s.io/issuer: letsencrypt-issuer
kubernetes.io/ingress.class: nginx
kubernetes.io/tls-acme: "true"
labels:
app: books-api
name: books-api
spec:
rules:
- host: books-api.ippon.fr
http:
paths:
- backend:
serviceName: books-api
servicePort: 80
path: /
tls:
- hosts:
- books-api.ippon.fr
secretName: books-api-cert (secret contenant le certificat SSL)
Validation
Ci-dessous le résultat d’une demande de connexion SSL via openssl : le certificat retourné est bien certifié par Let's Encrypt.
$> openssl s_client -servername books-api.ippon.fr -connect books-api.ippon.fr:443
CONNECTED(00000006)
depth=2 O = Digital Signature Trust Co., CN = DST Root CA X3
verify return:1
depth=1 C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
verify return:1
depth=0 CN = books-api.ippon.fr
verify return:1
---
Certificate chain
0 s:/CN=books-api.ippon.fr
i:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
1 s:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
i:/O=Digital Signature Trust Co./CN=DST Root CA X3
Schéma d’architecture
Le schéma d’architecture est simple :
- Route 53 gère les enregistrements DNS de l’application (dans une zone hébergée)
- ELB redirige les requêtes HTTP issues d’Internet vers le cluster K8s
- K8s est le cluster où est déployé l’application
Conclusion
Dans cet article, nous avons déployé une application dans un cluster Kubernetes. L’application est accessible en HTTPS sur un nom de domaine spécifique.
Kubernetes est extensible ! L’exposition de services est réalisée par trois contrôleurs (ingress-nginx, external-dns, cert-manager) qui se basent sur un même objet Ingress. Le contrôleur cert-manager étend le système en définissant de nouveaux types de ressources nécessaires à la gestion de certificats.
Kubernetes s’intègre au cloud ! Le déploiement se base sur l’utilisation de services Cloud publics (DNS Route 53, Autorité de Certification Let’s Encrypt).