Установка Jenkins с помощью terraform в Kubernetes в Yandex Cloud с letsencypt

от автора

В этой статье будет следующее:

  • Заведение DNS домена на reg.ru.
  • Управление DNS зоной в Yandex DNS c помощью terraform.
  • Создание Kubernetes в Yandex Cloud.
  • Резервируем внешний статический IP адрес.
  • Установка Jenkins c помощью terraform модуля helm_release.
  • Создание ClusterIssue(Issue) для создания letsencypt сертификата.

Быстрая установка Jenkins с помощью terraform в Kubernetes в Yandex Cloud с letsencypt.

Скачать репозиторий:

git clone https://github.com/patsevanton/infrastructure-as-a-code-example.git

Перейти в каталог terraform-helm-release-jenkins:

cd terraform-helm-release-jenkins

Заполнить private.auto.tfvars на базе шаблона private.auto.tfvars.example.

Запустить установку:

k8s_install.sh

Разберем подробнее.

Заводим DNS домен на reg.ru — mycompany.ru или mycompany.org.ru. В настройках домена на вкладке «DNS-серверы и управление зоной» указываем DNS Yandex Cloud:

  • ns1.yandexcloud.net
  • ns2.yandexcloud.net

DNS зона управляется с помощью terraform кода. Документация — https://cloud.yandex.ru/docs/dns/.

resource "yandex_dns_zone" "dns_domain" {   name   = replace(var.dns_domain, ".", "-")   zone   = join("", [var.dns_domain, "."])   public = true }

Так как у zone на конце должна быть точка, то используем метод join для соединения нашего домена и точки.

Создание DNS записи.

В имени DNS записи на конце необходима точка, поэтому используем метод join для соединения DNS записи и точки. Код:

resource "yandex_dns_recordset" "jenkins_dns_domain" {   zone_id = yandex_dns_zone.dns_domain.id   name    = join("", [var.jenkins_dns_name, "."])   type    = "A"   ttl     = 200   data    = [yandex_vpc_address.addr.external_ipv4_address[0].address] }

Поле data — список IP адресов, на которые должны резолвиться эта DNS запись.

*Создание сервисного аккаунта.

Перед созданием Kubernetes кластера необходимо рассказать о создание сервисного аккаунта. Название сервисного аккаунта — sa-k8s-admin.

resource "yandex_iam_service_account" "sa-k8s-admin" {   folder_id = var.yc_folder_id   name      = "sa-k8s-admin" }  resource "yandex_resourcemanager_folder_iam_member" "sa-k8s-admin-permissions" {   folder_id = var.yc_folder_id   role      = "admin"   member    = "serviceAccount:${yandex_iam_service_account.sa-k8s-admin.id}" }

Лучше использовать yandex_resourcemanager_folder_iam_member вместо yandex_resourcemanager_folder_iam_binding, так как не всегда работает корректно. У меня на другом коде вызывает ошибку. Issue — https://github.com/yandex-cloud/terraform-provider-yandex/issues/267 и https://github.com/yandex-cloud/terraform-provider-yandex/issues/283.

"Binding for role "mdb.dataproc.agent" not found in policy"

В role указываем какие права имеет сервисный аккаунт. Более подробно в документации https://cloud.yandex.ru/docs/iam/concepts/access-control/roles.

Создание kubernetes кластера в Yandex Cloud.

Для создание kubernetes кластера в Yandex Cloud используется уже обычный terraform код:

resource "yandex_kubernetes_cluster" "zonal_k8s_cluster" {   name        = "my-cluster"   description = "my-cluster description"   network_id  = yandex_vpc_network.k8s-network.id    master {     version = "1.21"     zonal {       zone      = yandex_vpc_subnet.k8s-subnet.zone       subnet_id = yandex_vpc_subnet.k8s-subnet.id     }     public_ip = true   }    service_account_id      = yandex_iam_service_account.sa-k8s-admin.id   node_service_account_id = yandex_iam_service_account.sa-k8s-admin.id   release_channel         = "STABLE"   // to keep permissions of service account on destroy   // until cluster will be destroyed   depends_on = [yandex_resourcemanager_folder_iam_member.sa-k8s-admin-permissions] }  # yandex_kubernetes_node_group  resource "yandex_kubernetes_node_group" "k8s_node_group" {   cluster_id  = yandex_kubernetes_cluster.zonal_k8s_cluster.id   name        = "name"   description = "description"   version     = "1.21"    labels = {     "key" = "value"   }    instance_template {     platform_id = "standard-v3"      network_interface {       nat        = true       subnet_ids = [yandex_vpc_subnet.k8s-subnet.id]     }      resources {       cores         = 2       memory        = 4       core_fraction = 50     }      boot_disk {       type = "network-hdd"       size = 32     }      scheduling_policy {       preemptible = true     }      metadata = {       ssh-keys = "ubuntu:${file("~/.ssh/id_rsa.pub")}"     }    }    scale_policy {     fixed_scale {       size = 1     }   }    allocation_policy {     location {       zone = "ru-central1-b"     }   }    maintenance_policy {     auto_upgrade = true     auto_repair  = true      maintenance_window {       day        = "monday"       start_time = "15:00"       duration   = "3h"     }      maintenance_window {       day        = "friday"       start_time = "10:00"       duration   = "4h30m"     }   } }  locals {   kubeconfig = <<KUBECONFIG apiVersion: v1 clusters: - cluster:     server: ${yandex_kubernetes_cluster.zonal_k8s_cluster.master[0].external_v4_endpoint}     certificate-authority-data: ${base64encode(yandex_kubernetes_cluster.zonal_k8s_cluster.master[0].cluster_ca_certificate)}   name: kubernetes contexts: - context:     cluster: kubernetes     user: yc   name: ycmk8s current-context: ycmk8s users: - name: yc   user:     exec:       apiVersion: client.authentication.k8s.io/v1beta1       command: yc       args:       - k8s       - create-token KUBECONFIG }  output "kubeconfig" {   value = local.kubeconfig }

Если кластер тестовый, то можно снизить стоимость используя 50% ядра и использовать прерываемые виртуальные машины.

    resources {       ...       core_fraction = 50     }      scheduling_policy {       preemptible = true     }

Сеть и внешний статический IP адрес.

Указываем сеть:

resource "yandex_vpc_network" "k8s-network" {   name = "k8s-network" }

Указываем подсеть:

resource "yandex_vpc_subnet" "k8s-subnet" {   zone           = "ru-central1-b"   network_id     = yandex_vpc_network.k8s-network.id   v4_cidr_blocks = ["10.5.0.0/24"]   depends_on = [     yandex_vpc_network.k8s-network,   ] }

Указываем внешний статический IP адрес:

resource "yandex_vpc_address" "addr" {   name = "static-ip"   external_ipv4_address {     zone_id = "ru-central1-b"   } }

Jenkins установливаем c помощью terraform модуля helm_release.

Рассмотрим helm_release.tf:

provider "helm" описывает настройки подключения к kubernetes для helm.

provider "helm" {   kubernetes {     host                   = yandex_kubernetes_cluster.zonal_k8s_cluster.master[0].external_v4_endpoint     cluster_ca_certificate = yandex_kubernetes_cluster.zonal_k8s_cluster.master[0].cluster_ca_certificate     exec {       api_version = "client.authentication.k8s.io/v1beta1"       args        = ["k8s", "create-token"]       command     = "yc"     }   } }

Создаем ingress-nginx.

resource "helm_release" "ingress_nginx" {   name       = "ingress-nginx"   repository = "https://kubernetes.github.io/ingress-nginx"   chart      = "ingress-nginx"   version    = "4.2.1"   wait       = true   depends_on = [     yandex_kubernetes_node_group.k8s_node_group   ]   set {     name  = "controller.service.loadBalancerIP"     value = yandex_vpc_address.addr.external_ipv4_address[0].address   } }

В terraform опции --set key=value передаются так:

  set {     name  = "controller.service.loadBalancerIP"     value = yandex_vpc_address.addr.external_ipv4_address[0].address   }

В данном случае для controller.service.loadBalancerIP указываем внешний статический IP адрес.

Устанавливаем cert-manager по аналогии с ingress-nginx.

resource "helm_release" "cert-manager" {   namespace        = "cert-manager"   create_namespace = true   name             = "jetstack"   repository       = "https://charts.jetstack.io"   chart            = "cert-manager"   version          = "v1.9.1"   wait             = true   depends_on = [     yandex_kubernetes_node_group.k8s_node_group   ]   set {     name  = "installCRDs"     value = true   } }

Устанавливаем jenkins.

resource "helm_release" "jenkins" {   namespace        = "jenkins"   create_namespace = true   name             = "jenkins"   repository       = "https://charts.jenkins.io"   chart            = "jenkins"   wait             = true   version          = "4.1.17"   depends_on = [     yandex_kubernetes_node_group.k8s_node_group   ]   values = [     #file("${path.module}/jenkins-values-google-login.yaml")     yamlencode(local.jenkins_values_google_login)   ] }

Jenkins helm чарту на вход необходимо указать value.yaml с нашими настройками.

Есть 2 способа указать value.yaml.

Первый способ использовать готовый и настроеный value.yaml в текущей директории:

  values = [     file("${path.module}/jenkins-values-google-login.yaml")   ]

Второй способ использовать yamlencode для перевода terraform кода в yaml код.

  values = [     yamlencode(local.jenkins_values_google_login)   ]

Как получить terraform код из yaml кода?

Подготавливаете и настраиваете value.yaml, а затем используя вот такую команду переводите в terraform код:

echo 'yamldecode(file("value.yaml"))' | terraform console 

В моем случае value.yaml имеет другое название:

echo 'yamldecode(file("jenkins-values-google-login.yaml"))' | terraform console 

Я использую yamlencode, потому что внутри yamlencode кода ссылаюсь на terraform переменные, а так же потому что job лежат отдельно.

Полученный код вставляете в local.jenkins_values_google_login

locals {   jenkins_values_google_login = {   сюда   } }

Затем вместо ваших данных:

"hostName"         = jenkins.mycompany.ru

используйте terraform переменные

"hostName"         = "${var.jenkins_dns_name}"

Рассмотрим local.jenkins_values_google_login.

В корне стоит controller от которого идут все настройки.

Для настройка jenkins из кода используются jcasc, job-dsl. Примеры jcasc https://github.com/jenkinsci/configuration-as-code-plugin

      "JCasC" = {         "authorizationStrategy" = <<-EOT         loggedInUsersCanDoAnything:           allowAnonymousRead: false         EOT

Более подробно — https://github.com/jenkinsci/configuration-as-code-plugin/blob/master/demos/global-matrix-auth/README.md

В configScripts каждый блок EOT это отдельный файл в контейнере jenkins. systemMessage системное сообщение.

        "configScripts" = {           "jenkins-configuration" = <<-EOT           jenkins:             systemMessage: This Jenkins is configured and managed 'as code' by Managed Cloud team.           EOT

Указываем файл job:

          "job-config" = yamlencode({             jobs = [               { script = file("${path.module}/job1.groovy") },               { script = file("${path.module}/job2.groovy") }             ]           })

Указываем список view:

          jenkins:             views:               - all:                   name: "all"               - list:                   columns:                   - "status"                   - "weather"                   - "jobName"                   - "lastSuccess"                   - "lastFailure"                   - "lastDuration"                   - "buildButton"                   jobNames:                   - "job1"                   name: "stage"               - list:                   columns:                   - "status"                   - "weather"                   - "jobName"                   - "lastSuccess"                   - "lastFailure"                   - "lastDuration"                   - "buildButton"                   jobNames:                   - "job2"                   name: "test"             viewsTabBar: "standard"

Указываем как мы будет входить в jenkins:

        "securityRealm" = <<-EOT         googleOAuth2:           clientId: "${var.clientId}"           clientSecret: "${var.clientSecret}"           domain: "${var.google_domain}"         EOT

Я использую googleOAuth2. Можно еще использовать local, ldap и другие.

Дополнительные плагины jenkins:

      "additionalPlugins" = [         "google-login:1.6",         "job-dsl:1.81",         "allure-jenkins-plugin:2.30.2",         "ws-cleanup:0.42",         "build-timeout:1.21",         "timestamper:1.18",         "google-storage-plugin:1.5.6",         "permissive-script-security:0.7",         "ansicolor:1.0.2",         "google-oauth-plugin:1.0.6",       ]

Настройка Ingress:

      "ingress" = {         "annotations" = {           "cert-manager.io/cluster-issuer" = "letsencrypt-prod"         }         "apiVersion"       = "networking.k8s.io/v1"         "enabled"          = true         "hostName"         = "${var.jenkins_dns_name}"         "ingressClassName" = "nginx"         "tls" = [           {             "hosts" = [               "${var.jenkins_dns_name}",             ]             "secretName" = "jenkins-tls"           },         ]       }

Если у вас нет letsencrypt, то вы удаляете cert-manager.io/cluster-issuer.

Указываем javaOpts чтобы запускать Job-DSL скрипты:

javaOpts: '-Dpermissive-script-security.enabled=true'

Сравнение настроенный yaml и yamlencode из terraform кода.

Чем плох настроенный yaml в качестве value.yaml для helm чарта относительно yamlencode ?

  • Он длинный.
  • В yaml коде нельзя вынести кусок кода (например job) в отдельный файл.
  • Для формирования заполненого настроенного value.yaml необходимо использовать templatefile. Мне кажется это лишнее.

В JCasC.configScripts каждый блок до вертикальной черты (|) будет сохранен как отдельный файл в контейнере Jenkins.

В Yaml формате все job нужно расписывать. Если сравнить:

      job-config: |         jobs:           - script: >               pipelineJob('job1') {                 logRotator(120, -1, 1, -1)                 authenticationToken('secret')                 definition {                   cps {                     script("""\                       pipeline {                         agent any                         parameters {                             string(name: 'Variable', defaultValue: '', description: 'Variable', trim: true)                         }                         options {                           timestamps()                           ansiColor('xterm')                             timeout(time: 10, unit: 'MINUTES')                         }                         stages {                           stage ('build') {                             steps {                               cleanWs()                               echo "hello job1"                             }                           }                         }                       }""".stripIndent())                     sandbox()                   }                 }               }

и чтение файла job, то вынос файла лучшее красивее и читабельнее:

          "job-config" = yamlencode({             jobs = [               { script = file("${path.module}/job1.groovy") },               { script = file("${path.module}/job2.groovy") }             ]           })

Добавление kind: ClusterIssuer в Kubernetes.

Если мы будем добавлять kind: ClusterIssuer как resource "kubernetes_manifest" и добавим как подключатся к Kubernetes используя provider "kubernetes", то получим ошибку:

cannot create REST client: no client config

Мы не можем развернуть resource "kubernetes_manifest" в котором ссылаемся на другой ресрус — https://github.com/hashicorp/terraform-provider-kubernetes/issues/1380

Поэтому создадим файл ClusterIssuer.yaml.tpl и будем формировать его через templatefile передав всего 1 переменную email_letsencrypt.

Вот мы с вами и прошли то, что напланировали в самом начале.


ссылка на оригинал статьи https://habr.com/ru/post/683844/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *