• 14 novembre 2016

    Gitlab sur Kubernetes

    Introduction

    Dans le dernier post, nous avons vu comment créer un cluster Kubernetes (K8s) prêt pour la production sur AWS avec Kops. Maintenant voyons comment, couplé avec des services managés AWS, on peut utiliser ce cluster pour héberger une application hautement disponible : Gitlab.

    Connaitre Terraform, AWS et Kubernetes sera un plus pour la compréhension de cet article.

    Tout le code source utilisé dans ce post est disponible sur Github.

    Architecture de Gitlab

    Gitlab est un concurrent open-source de Github. Il est composé de plusieurs parties :

    • Une base de données relationnelle (PostgreSQL est le défaut),
    • Un système de fichiers « distribué » pour les dépôts Git,
    • Un serveur Redis pour le cache et les sessions,
    • Une application « core » avec des serveurs Unicorn, SSH et et Sidekiq (tout cela est dans l’image Docker créée par Sameer Naik – merci !).

    Problème

    Le principal problème lorsque l’on crée une application hautement disponible sur K8s/AWS est le stockage, que ce soit sous la forme d’une base de données ou d’un filesystem. Les disques EBS que l’on peut attacher aux instances EC2 auraient été les candidats naturels car Kubernetes sait les gérer. Mais un disque EBS est lié à une availability zone et n’est donc pas hautement disponible. Vous pouvez transporter des données d’une AZ à une autre avec un snapshot, mais ce n’est vraiment pas pratique.

    Architecture cible

    Pour assurer la haute disponibilité sur notre installation Gitlab, nous allons utiliser 2 services AWS :

    • AWS RDS (Relational Database Service) pour fournir une base PostgreSQL HA,
    • AWS EFS (Elastic Filesystem) pour un filesystem HA, accessible via NFS.

    Cela a l’air simple, mais l’implémentation EFS a un petit piège : il y a un point de montage différente pour chaque AZ.

    Redis et Gitlab en tant que tels seront des déploiements K8s.

    Voici à quoi ça ressemble :

    gliphy

    Comme vous pouvez le voir sur ce schéma, nous aurons en fait une instance Gitlab par AZ. Dans chaque AZ, Gitlab a besoin d’accéder au stockage EFS, mais le point de montage change d’une AZ à une autre. Donc pour fournir de la haute disponibilité, nous avons besoin d’au moins 2 instances Gitlab qui utilisent 2 points de montage différents.

    Implémentation

    Ressources AWS – Terraform

    Import des ressources Kops

    Nous allons utiliser Terraform pour créer les ressources AWS nécessaires pour Gitlab. Nous allons avoir besoin d’interagir avec les ressources créées par Kops (le VPC et les subnets) : nous devons donc les importer dans Terraform. Heureusement, la fonctionnalité import a été récemment ajoutée à Terraform ! Il faut juste exécuter les commandes suivantes avec les bons IDs :

    terraform import aws_vpc.kops_vpc vpc-xxxxxx
    terraform import aws_subnet.kops_suba subnet-xxxxxx
    terraform import aws_subnet.kops_subb subnet-xxxxxx
    terraform import aws_subnet.kops_subc subnet-xxxxxx

    Une fois les commandes exécutées, vous devez créer un fichier kops.tf qui contient ces ressources, sinon Terraform essayera de les détruire au prochain run :

    # Resource managed by KOPS DO NOT TOUCH
    resource "aws_vpc" "kops_vpc" {
      cidr_block = "10.0.0.0/22"
      tags {
        Name = "k8s.myzone.net"
        KubernetesCluster = "k8s.myzone.net"
      }
    }
    # Resource managed by KOPS DO NOT TOUCH
    resource "aws_subnet" "kops_suba" {
      vpc_id = "${aws_vpc.kops_vpc.id}"
      cidr_block = "10.0.0.128/25"
      tags {
        Name = "eu-west-1a.k8s.myzone.net"
        KubernetesCluster = "k8s.myzone.net"
      }
    }
    # Resource managed by KOPS DO NOT TOUCH
    resource "aws_subnet" "kops_subb" {
      vpc_id = "${aws_vpc.kops_vpc.id}"
      cidr_block = "10.0.1.0/25"
      tags {
        Name = "eu-west-1b.k8s.myzone.net"
        KubernetesCluster = "k8s.myzone.net"
      }
    }
    # Resource managed by KOPS DO NOT TOUCH
    resource "aws_subnet" "kops_subc" {
      vpc_id = "${aws_vpc.kops_vpc.id}"
      cidr_block = "10.0.1.128/25"
      tags {
        Name = "eu-west-1c.k8s.myzone.net"
        KubernetesCluster = "k8s.myzone.net"
      }
    }

    Une fois ce fichier créé, vous devriez pouvoir exécuter un terraform plan et Terraform ne devrait rien modifier.

    Création des ressources RDS et EFS

    Une fois les ressources réseau de Kops importées, vous pouvez provisionner ce dont on a besoin pour Gitlab dans un fichier gitlab.tf :

    variable "node_sg_id" {
      # Here, the security group created by Kops for the worker nodes
      default = "sg-xxxxxx"
    }
    variable "master_sg_id" {
      # Here, the security group created by Kops for the master nodes
      default = "sg-xxxxxx"
    }
    resource "aws_efs_file_system" "gitlab_nfs" {
      tags {
        Name = "k8s.myzone.net"
        KubernetesCluster = "k8s.myzone.net"
      }
    }
    resource "aws_security_group" "EFS_K8s" {
      name = "EFS_K8s"
      description = "Allow NFS inbound traffic"
      vpc_id = "${aws_vpc.kops_vpc.id}"
      ingress {
        from_port = 0
        to_port = 0
        protocol = "-1"
        security_groups =  ["${var.node_sg_id}", "${var.master_sg_id}"]
      }
      egress {
        from_port = 0
        to_port = 0
        protocol = "-1"
        cidr_blocks = ["0.0.0.0/0"]
      }
      tags {
        Name = "EFS_K8s"
        KubernetesCluster = "k8s.myzone.net"
      }
    }
    resource "aws_efs_mount_target" "gitlab_nfsa" {
      file_system_id = "${aws_efs_file_system.gitlab_nfs.id}"
      subnet_id = "${aws_subnet.kops_suba.id}"
      security_groups = ["${aws_security_group.EFS_K8s.id}"]
    }
    resource "aws_efs_mount_target" "gitlab_nfsb" {
      file_system_id = "${aws_efs_file_system.gitlab_nfs.id}"
      subnet_id = "${aws_subnet.kops_subb.id}"
      security_groups = ["${aws_security_group.EFS_K8s.id}"]
    }
    resource "aws_efs_mount_target" "gitlab_nfsc" {
      file_system_id = "${aws_efs_file_system.gitlab_nfs.id}"
      subnet_id = "${aws_subnet.kops_subc.id}"
      security_groups = ["${aws_security_group.EFS_K8s.id}"]
    }
    output "NFS_mount_points" {
      value = "${aws_efs_mount_target.gitlab_nfsa.dns_name} ${aws_efs_mount_target.gitlab_nfsb.dns_name} ${aws_efs_mount_target.gitlab_nfsc.dns_name}"
    }
    resource "aws_db_subnet_group" "gitlab_pgsql" {
      name = "gitlab_pgsql"
      subnet_ids = ["${aws_subnet.kops_suba.id}", "${aws_subnet.kops_subb.id}", "${aws_subnet.kops_subc.id}"]
      tags {
        Name = "Gitlab PgSQL"
        KubernetesCluster = "k8s.myzone.net"
      }
    }
    resource "aws_security_group" "gitlab-pgsql" {
      name = "gitlab-pgsql"
      description = "Allow PgSQL inbound traffic"
      vpc_id = "${aws_vpc.kops_vpc.id}"
      ingress {
        from_port = 5432
        to_port = 5432
        protocol = "TCP"
        security_groups =  ["${var.node_sg_id}", "${var.master_sg_id}"]
      }
      egress {
        from_port = 0
        to_port = 0
        protocol = "-1"
        cidr_blocks = ["0.0.0.0/0"]
      }
      tags {
        Name = "gitlab-pgsql"
        KubernetesCluster = "k8s.myzone.net"
      }
    }
    resource "aws_db_instance" "gitlab-pgsql" {
      allocated_storage       = "50"
      engine                  = "postgres"
      engine_version          = "9.3.14"
      identifier              = "gitlab-pgsql"
      instance_class          = "db.t2.medium"
      storage_type            = "gp2"
      name                    = "gitlab_production"
      password                = "yourpassword"
      username                = "gitlab"
      backup_retention_period = "30"
      backup_window           = "04:00-04:30"
      maintenance_window      = "sun:04:30-sun:05:30"
      multi_az                = true # <= important!
      port                    = "5432"
      vpc_security_group_ids  = ["${aws_security_group.gitlab-pgsql.id}"]
      db_subnet_group_name    = "${aws_db_subnet_group.gitlab_pgsql.name}"
      storage_encrypted       = false
      auto_minor_version_upgrade = true
      tags {
        Name        = "gitlab-pgsql"
        KubernetesCluster = "k8s.myzone.net"
      }
    }
    output "PgSQL_endpoint" {
      value = "${aws_db_instance.gitlab-pgsql.endpoint}"
    }

    Après un terraform plan/apply, les endpoints NFS et PostgreSQL devraient être affichés.

    Qu’est-ce que ce code crée ?

    • Une instance RDS/PostgreSQL avec l’option multi-AZ activée,
    • Un filesystem EFS et ces points de montage associés dans chaque AZ,
    • Les différents security groups requis.

    Ressources Kubernetes

    Une fois que nous avons créé tout ce dont nous avons besoin sur AWS, on peut passer à K8s !

    Pour chaque bout de yaml dans cet article, vous pouvez créer les ressources associées en mettant le code dans un fichier.yaml puis en exécutant kubectl apply -f fichier.yaml.

    Persistent Volumes

    Commençons par les objets de type PersistentVolume. Comme nous avons 3 endpoints EFS, nous avons besoin de 3 PersistentVolumes, même si c’est pour accéder aux mêmes données. Remarquez qu’on labélise chaque PV avec l’AZ : nous allons utiliser ces labels pour choisir le PV dans les deployments Gitlab.

    Dans la déclaration des PVs, il faut utiliser les endpoints donnés par votre dernier run Terraform.

    ---
    apiVersion: v1
    kind: PersistentVolume
    metadata:
      name: gitlab.data.efs.a
      labels:
        usage: gitlab-data
        zone: eu-west-1a
    spec:
      capacity:
        storage: 100Gi
      accessModes:
        - ReadWriteMany
      nfs:
        server: eu-west-1a.fs-xxxxxx.efs.eu-west-1.amazonaws.com
        path: "/"
    ---
    apiVersion: v1
    kind: PersistentVolume
    metadata:
      name: gitlab.data.efs.b
      labels:
        usage: gitlab-data
        zone: eu-west-1b
    spec:
      capacity:
        storage: 100Gi
      accessModes:
        - ReadWriteMany
      nfs:
        server: eu-west-1b.fs-xxxxxx.efs.eu-west-1.amazonaws.com
        path: "/"
    ---
    apiVersion: v1
    kind: PersistentVolume
    metadata:
      name: gitlab.data.efs.c
      labels:
        usage: gitlab-data
        zone: eu-west-1c
    spec:
      capacity:
        storage: 100Gi
      accessModes:
        - ReadWriteMany
      nfs:
        server: eu-west-1c.fs-xxxxxx.efs.eu-west-1.amazonaws.com
        path: "/"

    Redis

    Nous n’avons pas besoin d’une installation compliquée pour Redis, allons donc au plus simple :

    ---
    apiVersion: extensions/v1beta1
    kind: Deployment
    metadata:
      name: redis
    spec:
      replicas: 1
      template:
        metadata:
          labels:
            name: redis
        spec:
          containers:
          - name: redis
            image: redis
            ports:
            - containerPort: 6379
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: redis
    spec:
      ports:
      - port: 6379
        protocol: TCP
        targetPort: 6379
      selector:
        name: redis

    Le Deployment fera en sorte qu’il y aura toujours un serveur Redis (replicas: 1) et le Service permettra d’y accéder simplement avec l’adresse redis.

    Gitlab Deployment

    Comme indiqué précédemment, nous avons besoin d’au moins 2 instances Gitlab, réparties sur 2 AZs. Nous allons donc créer un Deployment Gitlab par AZ. Le code présenté ici est pour l’AZ a. Pour les AZs b et c, il faut juste copier/coller et changer le a par b ou c là où il faut.

    Il y deux choses importantes à noter :

    • Nous utilisons des variables d’environnement pour configurer l’image Docker Gitlab, et les mots de passe sont stockés dans un secret Kubernetes. Les secrets sont « chiffrés » en base64, il ne faut donc pas les inclure dans votre dépôt Git.
    • Chaque Deployment doit être restreint à une AZ, nous utilisons donc des filtres pour sélectionner les ressources selon leurs labels.

    Par exemple, le PersistentVolumeClaim va utiliser le sélecteur suivant pour choisir le bon endpoint EFS :

    selector:
      matchLabels:
        usage: gitlab-data
        zone: eu-west-1a

    De manière similaire, nous devons restreindre le déploiement Gitlab à une AZ, et nous devons donc sélectionner sur quel nœud il doit tourner :

    nodeSelector:
      failure-domain.beta.kubernetes.io/zone: eu-west-1a

    Voici le code complet pour une seule AZ :

    ---
    apiVersion: v1
    kind: Secret
    metadata:
      name: gitlab-secrets
    type: Opaque
    data:
      db-key-base: base64-encoded-key
      secret-key-base: base64-encoded-key
      otp-key-base: base64-encoded-key
      db-pass: base64-encoded-password
      root-pass: base64-encoded-password
    ---
    kind: PersistentVolumeClaim
    apiVersion: v1
    metadata:
      name: gitlab.data.efs.a
    spec:
      accessModes:
        - ReadWriteMany
      resources:
        requests:
          storage: 100Gi
      selector:
        matchLabels:
          usage: gitlab-data
          zone: eu-west-1a
    ---
    apiVersion: extensions/v1beta1
    kind: Deployment
    metadata:
      name: gitlab-a
    spec:
      replicas: 1
      template:
        metadata:
          labels:
            name: gitlab-a
            app: gitlab
        spec:
          nodeSelector:
            failure-domain.beta.kubernetes.io/zone: eu-west-1a
          containers:
          - name: gitlab-a
            image: sameersbn/gitlab:8.12.6
            imagePullPolicy: Always
            ports:
            - containerPort: 80
            env:
            - name: TZ
              value: Europe/Paris
            - name: GITLAB_TIMEZONE
              value: Paris
            - name: GITLAB_SECRETS_DB_KEY_BASE
              valueFrom:
                secretKeyRef:
                  name: gitlab-secrets
                  key: db-key-base
            - name: GITLAB_SECRETS_SECRET_KEY_BASE
              valueFrom:
                secretKeyRef:
                  name: gitlab-secrets
                  key: secret-key-base
            - name: GITLAB_SECRETS_OTP_KEY_BASE
              valueFrom:
                secretKeyRef:
                  name: gitlab-secrets
                  key: otp-key-base
                  - name: GITLAB_ROOT_PASSWORD
                    valueFrom:
                      secretKeyRef:
                        name: gitlab-secrets
                        key: root-pass
                  - name: GITLAB_HOST
                    value: git.default.cluster.local
                  - name: GITLAB_PORT
                    value: "80"
                  - name: GITLAB_SSH_PORT
                    value: "22"
                  - name: GITLAB_NOTIFY_ON_BROKEN_BUILDS
                    value: "true"
                  - name: GITLAB_NOTIFY_PUSHER
                    value: "false"
                  - name: DB_TYPE
                    value: postgres
                  - name: DB_HOST
                    # Value given by Terraform
                    value: gitlab-pgsql.xxxxxx.eu-west-1.rds.amazonaws.com
                  - name: DB_PORT
                    value: "5432"
                  - name: DB_USER
                    value: gitlab
                  - name: DB_PASS
                    valueFrom:
                      secretKeyRef:
                        name: gitlab-secrets
                        key: db-pass
                  - name: DB_NAME
                    value: gitlab_production
                  - name: REDIS_HOST
                    value: redis
                  - name: REDIS_PORT
                    value: "6379"
                  ports:
                  - name: http
                    containerPort: 80
                  - name: ssh
                    containerPort: 22
                  volumeMounts:
                  - mountPath: /home/git/data
                  livenessProbe:
                    httpGet:
                      path: /
                      port: 80
                    initialDelaySeconds: 180
                    timeoutSeconds: 5
                  readinessProbe:
                    httpGet:
                      path: /
                      port: 80
                    initialDelaySeconds: 5
                    timeoutSeconds: 1
                volumes:
                - name: data
                  persistentVolumeClaim:
                    claimName: gitlab.data.efs.a

    Service Gitlab

    Presque terminé ! Quand les 3 Deployements Gitlab sont créés, on peut les lier avec un Service :

    apiVersion: v1
    kind: Service
    metadata:
      name: gitlab
    spec:
      ports:
      - name: http
        port: 80
        protocol: TCP
        targetPort: 80
      - name: ssh
        port: 22
        protocol: TCP
        targetPort: 22
      selector:
        app: gitlab
      type: LoadBalancer

    Remarquez le type: LoadBalancer : il va automatiquement créer un ELB pour rendre Gitlab accessible depuis l’extérieur !

    Conclusion

    Bon, c’était beaucoup à la fois ! Mais maintenant vous avez un Gitlab hautement disponible qui va pouvoir facilement scaler si besoin ! Seuls quelques petites choses manquent pour avoir une installation à l’état de l’art : du HTTPS et des runners pour GitlabCI (j’aborderai peut-être le sujet dans un prochain post).

    La chose principale dont il faut se souvenir, c’est qu’il est possible d’héberger des applications stateful sur Kubernetes, pour peu que vous ayez un stockage hautement disponible. Dans ce cas, nous avons utilisé RDS et EFS et nous avons dû trouver des contournements pour certaines propriétés d’EFS, mais cette méthode pourrait être appliquée à de nombreuses applications. Comprendre les principes sous-jacents à cette installation vous permettra de les adapter à votre cas.

    Auteur : Théo Chamley – Consultant Architecte DevOps chez Oxalide.

Newsletter

Inscrivez-vous et tenez vous au courant de l’actualité Oxalide