Имеются два окружения staging и production(так же будут называться и namespace-ы в Kubernetes-кластере)
Настраиваем автоматический deploy в Kubernetes кластер при коммите в одну из веток
При коммите в ветку develop – деплой приложений в namespace staging
При коммите в ветку staging – деплой приложений в namespace production
С помощью GitlabCI выполняется деплой,в котором используется Helm-чарт
Будет представлен только этап деплоя(подразумевается, что этапы тестов, сборки Docker-образа и загрузки образа в Docker-репозитарий были выполнены до стадии деплоя)
В Kubernetes-кластере с помощью Helm будут запущены redis, elasticsearch, кастомное/пользовательское myapp-приложение
Предположим, myapp-приложению требуется доступ к redis, elasticsearch и внешней MongoDB-базе данных( база данных не запускается в Kubernetes-кластере, а используется внешнее сторонее решение)
Буде создан Helm-чарт для удобства деплоя вышеуказанных приложений
Алгоритм действий состоит из следующих шагов
1. Настройка интеграции Gitlab с Kubernetes
2. Создание необходимых переменных в Gitlab
3. Создание GitlabCI/CD
4. Создание Helm-чарта
5. Создание необходимых namespace в Kubernetes-кластере
6. Проверка корректной работы авто-деплоя при коммите в репозитарий
1.Настройка интеграции Gitlab с Kubernetes
https://gitlab.com/help/user/project/clusters/index#adding-an-existing-kubernetes-cluster
Создание ServiceAccount, ClusterRoleBinding, получение значений необходимых параметров: URL для API-сервера, токена, ca-crt-файла
Узнаем URL для API-сервера
1 |
# kubectl cluster-info | grep 'Kubernetes master' | awk '/http/ {print $NF}' |
1 |
https://35.241.153.120 |
Создаем serviceaccount с именем gitlab в namespace default и предоставляем этому аккаунту роль cluster-admin
1 |
# nano gitlab-kubernetes-integration.yaml |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# Create a gitlab service account in the default namespace apiVersion: v1 kind: ServiceAccount metadata: name: gitlab namespace: default --- # Create a cluster role binding to give the gitlab service account cluster-admin privileges: apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: gitlab-cluster-admin subjects: - kind: ServiceAccount name: gitlab namespace: default roleRef: kind: ClusterRole name: cluster-admin apiGroup: rbac.authorization.k8s.io |
1 |
# kubectl apply -f gitlab-kubernetes-integration.yaml |
Провереяем наличие созданных ресурсов serviceaccount и clusterrolebinding
1 |
# kubectl get clusterrolebinding -n default | grep gitlab-cluster-admin |
1 |
gitlab-cluster-admin 2m39s |
1 |
# kubectl get serviceaccount -n default | grep gitlab |
1 |
gitlab 1 2m |
После выполнения вышеуказанной команды (kubectl apply -f gitlab-kubernetes-integration.yaml) создается новый секрет gitlab-token-…..
Просмотр существующих секретов
1 |
# kubectl get secret |
1 2 3 |
NAME TYPE DATA AGE default-token-47wf6 kubernetes.io/service-account-token 3 3h gitlab-token-fjmzp kubernetes.io/service-account-token 3 4m |
Получение токена из имени секрета
1 |
# kubectl get secret gitlab-token-fjmzp -o jsonpath="{['data']['token']}" | base64 --decode |
1 |
eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrd...rmaRfiA4JXK1idhEQ |
Получение ca.crt- файла из имени секрета
1 |
# kubectl get secret gitlab-token-fjmzp -o jsonpath="{['data']['ca\.crt']}" | base64 --decode |
1 2 3 4 5 6 |
-----BEGIN CERTIFICATE----- MIIDCzCCAfOgAwIBAgIQNld8FiPLakN2brhLG7FXPjANBgkqhkiG9w0BAQsFADAv MS0wKwYD … Vqyj+0tOOEfdwHmq7F0Z -----END CERTIFICATE----- |
Настройка подключения Gitlab к Kubernetes в Gitlab WEB-интерфейсе
1 |
Gitlab→Operation->Kubernetes→имя Kubernetes кластера |
После чего в WEB-интерфейсе Gitlab устанавливем
Helm Tiller и Gitlab Runner
1 |
Gitlab→Operation->Kubernetes→имя Kubernetes кластера |
После успешной установки проверяем наличие namespace gitlab-managed-apps
1 |
# kubectl get namespace | grep gitlab-managed-apps |
1 |
gitlab-managed-apps Active 5h |
И наличие двух подов,запущенных в этом namespace(gitlab-managed-apps)
1 |
# kubectl get pod -n gitlab-managed-apps |
1 2 3 |
NAME READY STATUS RESTARTS AGE runner-gitlab-runner-7c5cfc9d94-gqpn5 1/1 Running 0 2h tiller-deploy-5d96b489cc-cpqfp 1/1 Running 0 5h |
2.Создание необходимых переменных в Gitlab
Переменные KUBE_CONFIG, CI_MONGO_URL_STAGING, CI_MONGO_URL_PRODUCTION добавляем в переменные CI/CD в Gitlab
1 |
Gitlab→Settings→CI/CD |
В переменных CI_MONGO_URL_STAGING и CI_MONGO_URL_PROD также экранируем запятые(если они там присутствуют) с помощью символа обратного слеша
Значение переменной KUBE_CONFIG получается из команды
1 |
# cat ~/.kube/config-password | base64 |
т.е. в значение переменной помещаем base64-код
Содержимое файла ~/.kube/config-password
1 |
# cat ~/.kube/config-password |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
apiVersion: v1 clusters: - cluster: certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURDekNDQWZPZ0F3SUJBZ0lRTmxkOE...UtLS0tLQo= server: https://35.241.153.120 name: gke_mydemoproject-123456_europe-west1-b_persistent-disk-tutorial contexts: - context: cluster: gke_mydemoproject-123456_europe-west1-b_persistent-disk-tutorial user: gke_mydemoproject-123456_europe-west1-b_persistent-disk-tutorial name: gke_mydemoproject-123456_europe-west1-b_persistent-disk-tutorial current-context: gke_mydemoproject-123456_europe-west1-b_persistent-disk-tutorial kind: Config preferences: {} users: - name: gke_mydemoproject-123456_europe-west1-b_persistent-disk-tutorial user: password: mypassword username: admin |
Важно!!!! Логин и пароль смотрим в Google WEB Interface
1 |
Google Console→Kubernetes Engine→Clusters→Show credentials |
3. Создание GitlabCI/CD
Создаем CI/CD с помощью файла gitlab-ci.yml
1 |
# cat .gitlab-ci.yml |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
variables: KUBECONFIG: /etc/deploy/config staging_myapp_deploy: stage: deploy image: lwolf/helm-kubectl-docker:v1.10.13-v2.12.3 only: - develop before_script: - mkdir -p /etc/deploy - echo ${KUBE_CONFIG} | base64 -d > ${KUBECONFIG} - helm init --client-only script: - helm upgrade myapp-staging myapp-helm --install --set namespace=${NAMESPACE} --set myapp.env.mongo_url=${CI_MONGO_URL_STAGING} variables: NAMESPACE: 'staging' production_myapp_deploy: stage: deploy image: lwolf/helm-kubectl-docker:v1.10.13-v2.12.3 only: - master before_script: - mkdir -p /etc/deploy - echo ${KUBE_CONFIG} | base64 -d > ${KUBECONFIG} - helm init --client-only script: - helm upgrade myapp-production myapp-helm --install --set namespace=${NAMESPACE} --set myapp.env.mongo_url=${CI_MONGO_URL_PROD} variables: NAMESPACE: 'production' |
Gitlab-runner запускает контейнер из образа
1 |
lwolf/helm-kubectl-docker:v1.10.13-v2.12.3 |
, в котором уже предусановлен helm,kubectl,docker
Значение глобальной переменной KUBECONFIG, которая определяет/содержит конфигурационный файл для kubectl(для возможности корректной авторизации в Kubernetes-кластере) наполняется из переменной Gitlab с именем KUBE_CONFIG,которую мы задали на предыдущем этапе
Значение переменной NAMESPACE будет подставляться из локальных переменных окружения,указанных в параметре “variables:” в файле .gitlab-ci.yml в зависимости от ветки, в которую был сделан коммит
— при коммите в ветку develop namespace будет иметь значение staging
— при коммите в ветку production namespace будет иметь значение production
Значения переменных CI_MONGO_URL_STAGING и CI_MONGO_URL_PROD будут браться из переменных Gitlab, которые мы задали на предыдущем этапе
Команда helm…. установит Helm-чарт,если он не существовал или обновит его,если он уже существует
4.Создание Helm-чарта
Структура HELM-чарта имеет вид
1 |
# tree myapp-helm/ |
1 2 3 4 5 6 7 8 9 |
myapp-helm/ ├── charts ├── Chart.yaml ├── templates │ ├── elasticsearch.yml │ ├── myapp.yaml │ ├── redis.yml │ └── tests └── values.yaml |
Содержимое файлов имеет вид
Файл с описанием Helm-чарта Chart.yaml
1 |
# cat Chart.yaml |
1 2 3 4 5 |
apiVersion: v1 appVersion: "1.0" description: A Helm chart for Kubernetes name: myapp-helm version: 0.1.0 |
Файл с дефолтными значениями values.yaml
1 |
# cat values.yaml |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# Default values for poly. # This is a YAML-formatted file. # Declare variables to be passed into your templates. namespace: myapp: repository: gcr.io/mydemoproject-123456/myapp tag: latest port: 3000 env: mongo_url: elasticsearch: http://elasticsearch:9200 redis: redis://redis:6379 api: name: myapp-api redis: name: redis port: 6379 repository: k8s.gcr.io/redis tag: e2e elasticsearch: name: elasticsearch http_port: 9200 transport_port: 9300 repository: docker.elastic.co/elasticsearch/elasticsearch tag: 6.6.0 |
Шаблон для redis
1 |
# cat templates/redis.yml |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
apiVersion: v1 kind: Service metadata: name: {{ .Values.redis.name }} namespace: {{ .Values.namespace }} labels: app: {{ .Values.redis.name }} spec: type: ClusterIP ports: - port: {{ .Values.redis.port }} targetPort: {{ .Values.redis.port }} protocol: TCP selector: app: {{ .Values.redis.name }} --- apiVersion: apps/v1 kind: StatefulSet metadata: name: {{ .Values.redis.name }} namespace: {{ .Values.namespace }} spec: selector: matchLabels: app: {{ .Values.redis.name }} # Label selector that determines which Pods belong to the StatefulSet # Must match spec: template: metadata: labels serviceName: {{ .Values.redis.name }} replicas: 1 updateStrategy: type: RollingUpdate template: metadata: labels: app: {{ .Values.redis.name }} spec: volumes: - name: host-sys hostPath: path: /sys initContainers: # - name: disable-thp # image: busybox # imagePullPolicy: IfNotPresent # volumeMounts: # - name: host-sys # mountPath: /host-sys # command: ["sh", "-c", "echo never > /host-sys/kernel/mm/transparent_hugepage/enabled"] - name: init-sysctl image: busybox imagePullPolicy: IfNotPresent securityContext: privileged: true command: ["sysctl", "-w", "net.core.somaxconn=512"] containers: - name: {{ .Values.redis.name }} image: "{{ .Values.redis.repository }}:{{ .Values.redis.tag }}" #image: gcr.io/mydemoproject-231415/redis:e2e imagePullPolicy: IfNotPresent resources: requests: #cpu: 100m memory: 200Mi limits: memory: 300Mi ports: - containerPort: {{ .Values.redis.port }} name: {{ .Values.redis.name }} protocol: TCP volumeMounts: - name: redis-persistent-storage mountPath: /data volumeClaimTemplates: - metadata: name: redis-persistent-storage spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 1Gi |
Шаблон для elasticsearch
1 |
# cat templates/elasticsearch.yml |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
apiVersion: v1 kind: Service metadata: name: {{ .Values.elasticsearch.name }} namespace: {{ .Values.namespace }} labels: app: {{ .Values.elasticsearch.name }} spec: type: ClusterIP ports: - name: transport port: {{ .Values.elasticsearch.transport_port }} targetPort: {{ .Values.elasticsearch.transport_port }} protocol: TCP - name: http port: {{ .Values.elasticsearch.http_port }} targetPort: {{ .Values.elasticsearch.http_port }} protocol: TCP selector: app: {{ .Values.elasticsearch.name }} --- apiVersion: v1 kind: ConfigMap metadata: name: {{ .Values.elasticsearch.name }}-config namespace: {{ .Values.namespace }} data: elasticsearch.yml: | cluster.name: poly-elastic network.host: "0.0.0.0" bootstrap.memory_lock: false discovery.zen.ping.unicast.hosts: 127.0.0.1 discovery.zen.minimum_master_nodes: 1 xpack.security.enabled: false xpack.monitoring.enabled: false action.auto_create_index: ".watches,.triggered_watches,.watcher-history-*" ES_JAVA_OPTS: -Xms1024m -Xmx1024m --- apiVersion: apps/v1beta1 kind: StatefulSet metadata: name: {{ .Values.elasticsearch.name }} namespace: {{ .Values.namespace }} spec: serviceName: {{ .Values.elasticsearch.name }} replicas: 1 updateStrategy: type: RollingUpdate #type: OnDelete template: metadata: labels: app: {{ .Values.elasticsearch.name }} spec: securityContext: fsGroup: 1000 initContainers: - name: init-sysctl image: busybox imagePullPolicy: IfNotPresent securityContext: privileged: true command: ["sysctl", "-w", "vm.max_map_count=262144"] containers: - name: {{ .Values.elasticsearch.name }} resources: requests: memory: 2Gi #cpu: 1 limits: memory: 2Gi #cpu: 2 securityContext: privileged: true runAsUser: 1000 capabilities: add: - IPC_LOCK - SYS_RESOURCE image: "{{ .Values.elasticsearch.repository }}:{{ .Values.elasticsearch.tag }}" env: - name: ES_JAVA_OPTS valueFrom: configMapKeyRef: name: {{ .Values.elasticsearch.name }}-config key: ES_JAVA_OPTS ports: - containerPort: {{ .Values.elasticsearch.http_port }} name: es-http - containerPort: {{ .Values.elasticsearch.transport_port }} name: es-transport livenessProbe: tcpSocket: port: es-transport initialDelaySeconds: 20 periodSeconds: 10 readinessProbe: httpGet: scheme: HTTP path: /_cluster/health?local=true port: es-http initialDelaySeconds: 15 periodSeconds: 5 volumeMounts: - name: elasticsearch-data mountPath: /usr/share/elasticsearch/data - name: elasticsearch-config mountPath: /usr/share/elasticsearch/config/elasticsearch.yml subPath: elasticsearch.yml volumes: - name: elasticsearch-config configMap: name: elasticsearch-config items: - key: elasticsearch.yml path: elasticsearch.yml volumeClaimTemplates: - metadata: name: elasticsearch-data spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 10Gi |
Шаблон для приложения myapp
1 |
# cat templates/myapp.yaml |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
apiVersion: v1 kind: Service metadata: name: {{ .Values.myapp.api.name }} namespace: {{ .Values.namespace }} labels: app: {{ .Values.myapp.api.name }} spec: ports: - protocol: TCP port: {{ .Values.myapp.port }} targetPort: {{ .Values.myapp.port }} selector: app: {{ .Values.myapp.api.name }} --- apiVersion: apps/v1beta1 kind: Deployment metadata: name: {{ .Values.myapp.api.name }} namespace: {{ .Values.namespace }} labels: app: {{ .Values.myapp.api.name }} spec: replicas: 1 selector: matchLabels: app: {{ .Values.myapp.api.name }} template: metadata: labels: app: {{ .Values.myapp.api.name }} spec: containers: - name: {{ .Values.myapp.api.name }} image: "{{ .Values.myapp.repository }}:{{ .Values.myapp.tag }}" imagePullPolicy: IfNotPresent ports: - containerPort: {{ .Values.myapp.port }} resources: requests: cpu: 100m memory: 100Mi env: - name: MONGO_URL value: {{ .Values.myapp.env.mongo_url | quote }} - name: ELASTICSEARCH_URL value: {{ .Values.myapp.env.elasticsearch }} - name: REDIS_URL value: {{ .Values.myapp.env.redis }} |
Структра репозитария имеет вид
1 |
# ls -al |
1 2 3 4 5 6 |
drwxr-xr-x 8 kamaok kamaok 4096 Mar 5 16:16 .git -rw-r--r-- 1 kamaok kamaok 189 Feb 7 15:59 .gitignore -rw-r--r-- 1 kamaok kamaok 901 Mar 5 16:13 .gitlab-ci.yml drwxr-xr-x 4 kamaok kamaok 4096 Feb 25 17:47 myapp-helm -rw-r--r-- 1 kamaok kamaok 490 Feb 7 15:59 README.md ………… |
5.Создание необходимых namespace в Kubernetes-кластере
Создаем необходимые namespace – staging и production
1 |
# kubectl create namespace staging |
1 |
# kubectl create namespace production |
6.Проверка корректной работы авто-деплоя при коммите
Коммитим код и проверяем CI/CD в Gitlab WEB-интерфейсе
1 |
Gitlab→CI/CD->Pipeline |
1 |
# git add . && git commit -m "Base setup" && git push |
В командной строке проверяем
Просмотр достпных Helm-чартов
1 |
# helm list |
1 2 |
NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE myapp-staging 1 Tue Mar 5 15:31:54 2019 DEPLOYED myapp-helm-0.1.0 1.0 default |
Просмотр статуса/состояния указанного Helm-чарта
1 |
# helm status myapp-staging |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
LAST DEPLOYED: Tue Mar 5 16:16:42 2019 NAMESPACE: default STATUS: DEPLOYED RESOURCES: ==> v1/ConfigMap NAME DATA AGE elasticsearch-config 2 140m ==> v1/Service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE elasticsearch ClusterIP 10.43.249.150 <none> 9300/TCP,9200/TCP 140m myapp-api ClusterIP 10.43.247.169 <none> 3000/TCP 140m redis ClusterIP 10.43.255.145 <none> 6379/TCP 140m ==> v1beta1/Deployment NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE myapp-api 1 1 1 0 140m ==> v1beta1/StatefulSet NAME DESIRED CURRENT AGE elasticsearch 1 1 140m ==> v1/StatefulSet NAME DESIRED CURRENT AGE redis 1 1 140m ==> v1/Pod(related) NAME READY STATUS RESTARTS AGE myapp-api-7fd5f8cdf8-vdxtr 1/1 Running 0 140m elasticsearch-0 1/1 Running 0 140m redis-0 1/1 Running 0 140m |
Источник:
https://docs.gitlab.com/ee/user/project/clusters
https://about.gitlab.com/2017/09/21/how-to-create-ci-cd-pipeline-with-autodeploy-to-kubernetes-using-gitlab-and-helm/