Рассказываем, как использовать Jenkins CI/CD Pipeline для создания инфраструктуры AWS с помощью Terraform и Ansible. Мы не будем вдаваться в подробности, как настраивать Terraform или тестировать код по мере создания инфраструктуры, так как эти шаги считаются стандартными. Конечный результат — код Terraform, создающий среду AWS с общедоступными подсетями и инстансами EC2 с Ansible Playbook. Когда код помещается в репозиторий GitHub, GitHub Webhook запускает Jenkins CI/CD Pipeline, действия которого зависят от того, куда мы отправляем код — в ветку разработки или основную.

Вам понадобится

  • аккаунт GitHub;

  • AWS CLI;

  • Terraform;

  • аккаунт AWS;

  • пользователь AWS с правами администратора;

  • AWS Cloud9 (вы можете использовать другую IDE, но некоторые шаги будут отличаться);

  • аккаунт Terraform Cloud;

  • Git.

Настройте среду

Создайте среду Amazon Cloud9. 

Используйте все значения по умолчанию, кроме раздела «Platform section». Выберите Ubuntu Server 18.04 LTS.

Назначьте Elastic IP address 

Чтобы инстанс Cloud9 не менял свой общедоступный IP-адрес каждый раз, когда он выключается и снова включается, назначьте ему динамический IP-адрес. Позже это позволит назначить общедоступный IP-адрес переменной Terraform для групп безопасности. 

1. В консоли AWS перейдите к EC2.

2. Выберите «Elastic IPs» в разделе «Network & Security». 

3. Нажмите «Allocate Elastic IP address».

4. Нажмите «Allocate». 

5. В раскрывающемся списке «Action» выберите «Associate Elastic IP Address»:

6. Выберите «Cloud9 instance» > «Private IP address» > «Cloud9 private IP address». Нажмите «Associate» (связать). 

Измените размер инстанса Cloud9

1. Создайте в Cloud9 файл с именем resize.sh.

2. Скопируйте код из репозитория в файл resize.sh.

3. Запустите chmod +x resize.sh

4. Запустите ./resize.sh

Создайте SSH-ключ

1. В терминале запустите ssh-keygen -t rsa

2. Введите файл, в котором будет сохранён ключ: /home/ubuntu/.ssh/[имя ключа]

3. Без passphrase.

4. Проверьте, что ключ создан, запустив ls ~/.ssh

Установите jq

Запустите sudo apt install jq

Fork Repo

Здесь вы можете найти код, если захотите сослаться на него.  

Terraform Cloud

Terraform Cloud позволяет хранить Terraform State (состояние Terraform) удалённо, а не локально. Это повышает безопасность и улучшает совместную работу команды.

Прим., переводчика: также Terraform State можно хранить в s3 хранилище — неплохой продакшн-вариант.

1. Создайте новое рабочее пространство.

2. Выберите CLI-driven workflow:

3. Дайте название рабочему пространству, затем нажмите «Create workspace».

4. Скопируйте предоставленный выше пример кода и добавьте его в файл backends.tf, чтобы заменить текущего бэкенда.

5. Установите режим выполнения («Execution Mode»), нажав «Remote» и выбрав «Local»:

Создайте Terraform Cloud Token

1. Нажмите на иконку профиля в правом верхнем углу браузера и выберите «User settings», а затем «Tokens».

2. Введите описание и нажмите «Create an API token».

3. Скопируйте предоставленный токен и сохраните его в надёжном месте. 

4. Перейдите к терминалу Cloud9 и запустите terraform login.

5. Введите «Yes», затем вставьте токен Terraform Cloud.

6. Запустите terraform init

Это свяжет код Terraform с рабочей областью Terraform Cloud, заменив локальный сервер для Terraform Cloud. В будущем это даст Jenkins доступ к Terraform Cloud при запуске пайплайна.

Terraform

Это инструмент с открытым исходным кодом, в котором используется собственный язык HashiCorp (HCL). HCL Terraform позволяет разработчику выучить один язык для работы с несколькими облачными предложениями и локальными поставщиками вместо того, чтобы изучать новый сервис и язык для каждого из них. HCL — это декларативный язык, ориентированный на конечное состояние, в отличие от процедурного языка, где все команды выполняются в том порядке, в котором они написаны. 

Обновите переменные.tf

1. Измените переменную access_ip на общедоступный IP-адрес CIDR.

2. Измените переменную cloud9_ip на IP-адрес Cloud9 CIDR.

Обновите файлы tfvars

1. Обновите переменную key_name в файлах main.tfvars и dev.tfvars, указав имя вашего ключа.

2. Обновите переменную public_key_path в файлах main.tfvars и dev.tfvars, указав имя вашего ключа.

Terraform Code

Код Terraform вы найдёте в репозитории, поэтому не будем всё подробно описывать здесь. Однако выделим несколько моментов, которые помогут сделать файл более универсальным.

Data Source 

Используйте Data Source для AWS Availability Zones, чтобы определить текущие AZs, доступные в Region, где запущены ресурсы. Используйте Local Value для установки имён для AZs, а затем — функцию Length для определения длины на основе числа AZs.  Это нужно для индексации cidr_block и availability_zones, а на для создания нового блока. 

locals {
  azs = data.aws_availability_zones.available.names
}

data "aws_availability_zones" "available" {}

resource "aws_subnet" "public_subnet" {
  count                   = length(local.azs)
  vpc_id                  = aws_vpc.vpc.id
  cidr_block              = var.public_cidr[count.index]
  map_public_ip_on_launch = true
  availability_zone       = local.azs[count.index]


  tags = {
    Name = "docker-public"
  }
}
view raw

Data Source также используется для идентификации AWS AMI IDs в запущенном Region. Это устраняет необходимость жёсткого кодирования AMI ID или маппинга AMIs и Regions. Обратите внимание, как ami обращается к источнику данных, чтобы определить идентификатор Ubuntu AMI.

data "aws_ami" "ubuntu" {
  most_recent = true

  owners = ["099720109477"]

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }
}

resource "aws_instance" "web" {
  count                  = var.instance_count
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = "t2.micro"
  vpc_security_group_ids = [aws_security_group.sg.id]
  subnet_id              = aws_subnet.public_subnet[count.index].id
  key_name               = aws_key_pair.docker_auth.id

  tags = {
    Name = "docker-instance"
  }

Ansible

То, что обычно требует автоматизации сложного сценария, теперь можно сделать с помощью Ansible в несколько строк кода с помощью Ansible Playbook. В Ansible Playbooks используется YAML, который легко читается по сравнению с большинством языков. С Ansible у вас есть возможность управлять текстовым файлом инвентаризации хостов, которые вы хотите отслеживать или изменять. Эти хосты можно сгруппировать вместе или по отдельности под разными заголовками. 

«Ansible: Infrastructure as Code»

Установите Ansible

Выполните команду в инстансе Cloud9, чтобы установить Ansible:

sudo apt update &&
sudo apt install software-properties-common &&
sudo add-apt-repository --yes --update ppa:ansible/ansible &&
sudo apt install ansible

Настройте Ansible Hosts

1. Запустите sudo vim /etc/ansible/hosts

2. Вставьте в начало файла:

[hosts]
localhost
[hosts:vars]
ansible_connection=local
ansible_python_interpreter=/usr/bin/python3

3. Сохраните изменения и выйдите. 

Конфигурация

Чтобы не запрашивать отпечаток ключа ECDSA, настройте файл ansible.cfg.

1. Запустите sudo vim /etc/ansible/ansible.cfg

2. Удалите «#» для host_key_checking = False

3. Сохраните изменения и выйдите. 

Ansible Playbook: docker.yml

Чтобы не запускать команды для установки Docker вручную, используйте docker.yml Ansible Playbook. Playbook разбит на несколько задач, каждая из которых имеет название, описывающее предпринятое действие. Обратите внимание, что для управления пакетами используется ansible.builtin.apt или apt. А также на то, что хост ссылается на main, а не на local.

---
- name: Install Docker
  hosts: main
  become: yes
  
  tasks:
    - name: Update apt cache
      apt: update_cache=yes cache_valid_time=3600

    - name: Upgrade all apt packages
      apt: upgrade=dist

    - name: Install dependencies
      apt:
        name: "{{ packages }}"
        state: present
        update_cache: yes
      vars:
        packages:
        - apt-transport-https
        - ca-certificates
        - curl
        - software-properties-common
        - gnupg-agent
      
    - name: Add an apt signing key for Docker
      apt_key:
        url: https://download.docker.com/linux/ubuntu/gpg
        state: present

    - name: Add apt repository for stable version
      apt_repository:
        repo: deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable
        state: present

    - name: Install Docker
      apt:
        name: "{{ packages }}"
        state: present
        update_cache: yes
      vars:
        packages:
        - docker-ce
        - docker-ce-cli 
        - containerd.io

Jenkins 

Jenkins Pipeline реализует конвейеры непрерывной доставки в Jenkins с помощью подключаемых модулей и Jenkinsfile. Jenkinsfile может быть декларативным или скриптовым. Он содержит список шагов, которым должен следовать пайплайн.

Используйте Ansible для установки Jenkins

Вы можете вручную установить Jenkins на инстанс Cloud9, но чтобы получить больше практики используйте Ansible.

1. Запустите ansible-playbook playbooks/jenkins.yml

Откройте порт 8080

1. Перейдите к Cloud9 Security Group в консоли AWS.

2. Откройте порт 8080 на 0.0.0.0/0. Это необходимо, чтобы разрешить доступ к серверу Jenkins, а также решить проблему, с которой мы столкнемся позже с GitHub Webhook. 

3. Перейдите к общедоступному P-адресу Cloud9 (Cloud9 Public IP). 8080 в браузере для проверки.

Настройте Jenkins

1. Следуйте инструкциям на экране, чтобы получить пароль администратора, запустив sudo cat /var/lib/jenkins/secrets/initialAdminPassword.

2. Скопируйте и вставьте результат в поле «Unlock Jenkins» и нажмите «Continue».

3. Нажмите «Install suggested plugins» (установить предлагаемые плагины).

4. Введите необходимую информацию. Сохраните и перейдите к Jenkins.

5. Нажмите «Manage Jenkins» > «Manage Plugins».

6. Выберите «Available».

7. Найдите «Ansible» и установите без перезагрузки.

8. Нажмите «Manage Plugins» > «Available» > «Pipeline: AWS Steps» и установите без перезагрузки.

Управление учётными данными Jenkins

GitHub App

1. В GitHub щёлкните на значок профиля в правом верхнем углу браузера и выберите «Settings».

2. Нажмите «Developer settings» внизу левой панели навигации.

3. Нажмите «GitHub Apps», а затем — «New GitHub App».

4. Следуйте указаниям раздела «Creating a GitHub App» в документации GitHub.

5. Как только закрытый ключ будет загружен локально, откройте его в папке загрузки, а затем перетащите на верхний уровень инстанса Cloud9 (это гарантирует, что вы случайно не закоммитите этот файл позже).

6. На верхнем уровне терминала запустите:

openssl pkcs8 -topk8 -inform PEM -outform PEM -in [key-in-your-downloads-folder-name].pem -out converted-github-app.pem -nocrypt

7. Это создаст файл с именем convert-github-app.pem, совместимый с Jenkins.

8. Перейдите к Jenkins — нажмите «Manage Jenkins»> «Manage Credentials».

9. Нажмите «Jenkins»:

10. Нажмите «Global credentials» (без ограничений).

11. Нажмите «Add Credentials»:

  • Kind = приложения GitHub;

  • ID = [имя];

  • Description = учётные данные приложения GitHub

  • App ID (идентификатор приложения) = можно найти в приложении GitHub: Settings > Developer settings > GitHub Apps > [название вашего приложения]; 

  • Key = скопируйте и вставьте содержимое файла преобразованного ранее ключа и нажмите «ОК».

12. Вернитесь к своему GitHub App и установите приложение:

13. Вернитесь в Jenkins и кликните на учётные данные вашего приложения GitHub. Нажмите «Update» (обновить), а затем «Test Connection» (проверить соединение), чтобы убедиться, что всё работает.

Учётные данные Terraform Cloud 

1. Чтобы получить учётные данные Terraform Cloud, в терминале запустите cat /home/ubuntu/.terraform.d/credentials.tfrc.json

2. Скопируйте выходные данные. Создайте локальный текстовый файл, вставьте выходные данные и сохраните.

3. Вернитесь к Jenkins и добавьте новые учетные данные:

  • Kind = Secret file;

  • File = выберите файл, который вы только что сохранили;

  • ID = tf-creds

  • Description = учётные данные Terraform Cloud.

SSH Key

1. В терминале запустите cat /home/ubuntu/.ssh/[key name]

2. Скопируйте выходные данные.

3. Вернитесь к Jenkins и добавьте новые учётные данные:

  • Kind = SSH Username with private key (имя пользователя SSH с закрытым ключом);

  • ID = ec2-ssh-key;

  • Description = ssh key for ec2 instances;

  • Username = ubuntu;

  • Private Key = выберите «Enter directly», скопируйте и вставьте выходные данные.

4. Нажмите «OK».

GitHub Webhook

1. Перейдите к репозиторию GitHub проекта и нажмите «Settings»:

2. Кликните «Webhooks»;

3. Кликните «Add webhook»:

 

4. Payload URL = [jenkins url]/github-webhook/

Примечание: не забудьте поставить / в конце.

5. Тип контента = application/json.

6. Which events would you like to trigger this webhook (какие события вы хотите активировать с помощью этого веб-перехватчика)? = push-событие.

Создайте Jenkins пайплайн

1. Перейдите к панели мониторинга (Jenkins Dashboard) и нажмите «New Item».

2. Назовите пайплайн и выберите «Multi-Branch Pipeline». Затем нажмите «OK»:

3. Настройте пайплайн:

  • Display Name = [название пайплайна];

  • Branches Sources (источник веток) = GitHub;

  • GitHub Credentials = выберите учётные данные приложения GitHub; 

  • Repository HTTPS URL = [имя репозитория].git

Примечание: после сохранения вы попадёте на экран журнала сканирования репозитория — Scan Repository Log. Он выглядит так, как будто выполняется, даже если он завершен. Как только увидите SUCCESS в нижней части, можете возвращаться к дашборду.

Jenkinsfile

Jenkins нужен Jenkinsfile в корне кода для создания Jenkins Pipeline. Ниже приведен Jenkinsfile, использующийся в этом проекте:

pipeline {
  agent any
  environment {
    TF_IN_AUTOMATION = 'true'
    TF_CLI_CONFIG_FILE = credentials('tf-creds')
    AWS_SHARED_CREDENTIALS_FILE='/home/ubuntu/.aws/credentials'
  }
  stages {
    stage('Init') {
      steps {
        sh 'ls'
        sh 'cat $BRANCH_NAME.tfvars'
        sh 'terraform init -no-color'
      }
    }
    stage('Plan') {
      steps {
        sh 'terraform plan -no-color -var-file="$BRANCH_NAME.tfvars"'
      }
    }
    stage('Validate Apply') {
      when {
        beforeInput true
        branch "dev"
      }
      input {
        message "Do you want to apply this plan?"
        ok "Apply plan"
      }
    steps {
        echo 'Apply Accepted'
      }
    }
    stage('Apply') {
      steps {
        sh 'terraform apply -auto-approve -no-color -var-file="$BRANCH_NAME.tfvars"'
      }
    }
    stage('Inventory') {
      steps {
        sh '''printf \\
          "\\n$(terraform output -json instance_ips | jq -r \'.[]\')" \\
          >> aws_hosts'''
      }
    }
    stage('EC2 Wait') {
      steps {
        sh '''aws ec2 wait instance-status-ok \\
          --instance-ids $(terraform output -json instance_ids | jq -r \'.[]\') \\
          --region us-east-1'''
      }
    }
    stage('Validate Ansible') {
      when {
        beforeInput true
        branch "dev"
      }
      input {
        message "Do you want to run Ansible?"
        ok "Run Ansible"
      }
      steps {
        echo 'Ansible Approved'
          }
        }
    stage('Ansible') {
      steps {
        ansiblePlaybook(credentialsId: 'ec2-ssh-key', inventory: 'aws_hosts', playbook: 'playbooks/docker.yml')
      }
    }
    stage('Validate Destroy') {
      input {
        message "Do you want to destroy?"
        ok "Destroy"
        }
      steps {
        echo 'Destroy Approved'
      }
    }
    stage('Destroy') {
      steps {
        sh 'terraform destroy -auto-approve -no-color -var-file="$BRANCH_NAME.tfvars"'
      }
    }
  }
  post {
    success {
      echo 'Success!'
    }
    failure {
      sh 'terraform destroy -auto-approve -no-color -var-file="$BRANCH_NAME.tfvars"'
    }
    aborted {
      sh 'terraform destroy -auto-approve -no-color -var-file="$BRANCH_NAME.tfvars"'
    }
  }
}
view raw

Не будем рассказывать обо всем, что происходит с этим файлом, но выделим несколько интересных моментов.

Environment $BRANCH_NAME variable

Использование переменной среды позволяет различать ветки при выполнении Jenkinsfile. Приведенный ниже пример выполняет скрипт оболочки и выводит ветку, которая инициировала GitHub Webhook. На этапе планирования это позволяет вызвать либо файл main.tfvars, либо файл dev.tfvars, который перезапишет наш файл variable.tf по умолчанию.

Conditional & Input

Условие when используется для определения того, следует ли запускать input, если ветвь равна dev. Если ветвь не равна dev, то input пропускается.

Input используется для приостановки конвейера и ожидания ручного выбора Apply (одобрить) план или Abort (прервать). 

EC2 Wait

На этапах Inventory и EC2 Wait используется скрипт оболочки для получения выходных данных Terraform и jq для получения instance ids и instance ips. Этап Inventory заключается в передаче instance ips, созданных из кода Terraform, и добавлении их в файл aws_hosts, который используется Ansible.

Собираем всё вместе

Ветка разработки

1. Создайте ветку разработки, запустив git checkout -b dev

2. Запустите git add.

3. Закоммитьте файлы, запустив git commit -m "initial commit"

4. Отправьте свой код в ветку разработки с помощью команды git push -u origin dev и авторизуйтесь в GitHub.

5. Перейдите к панели инструментов Jenkins.

6. Нажмите на работающий пайплайн в разделе «Build Executor Status», расположенном в левом нижнем углу браузера:

7. Наведите курсор мыши на «Validate Apply step», нажмите «Apply plan»:

8. Пайплайн применит код Terraform, запустит этап инвентаризации и дождется инициализации инстанса EC2, чтобы Ansible не выдал ошибку при попытке доступа к инстансу.

9. Как только он достигнет шага «Validate Ansible», ещё раз наведите курсор мыши на шаг и выберите «Run Ansible».

10. Во время ожидания щелкните зеленую галочку рядом с историей сборки, чтобы увидеть результат. Вы увидите, что Ansible работает, но вы также можете прокрутить вверх и просмотреть все выходные данные всего пайплайна.

11. Пока пайплайн ожидает на шаге «Validate Destroy», давайте проверим инстанс, чтобы убедиться, что Docker установлен правильно. Проскрольте вверх, найдите instance_ips и скопируйте общедоступный IP-адрес инстанса.

12. Вернитесь к терминалу Cloud9 и войдите в инстанс по SSH, используя свой SSH-ключ. ssh -i /home/ubuntu/.ssh/[имя ключа] ubuntu@[IP-адрес инстанса]

13. Для проверки запустите docker –version

14. Вернитесь к пайплайну Jenkins и выберите «Destroy».

Основная ветка

Поскольку ветку разработки мы проверили, кож можно отправить в основную ветку.

1. Переключитесь на основную ветку (main branch), запустив git checkout main

2. Смержите ветку разработки (dev branch), запустив git merge dev

3. Отправьте код на главную, запустив git push -u origin main и авторизовавшись на GitHub.

4. Вернитесь к Jenkins и проверьте пайплайн. На этот раз вы должны увидеть другую ветку для main:

5. Кликните на «main», чтобы увидеть пайплайн в действии. Поскольку код уже был протестирован в пайплайне разработки, Jenkinsfile пропускает этапы проверки на основе заданных условий. Пайплайн должен пройти весь путь до этапа Validate Destroy.

6. Убедитесь, что Docker установлен. После этого возвращайтесь к пайплайну, чтобы выполнить действие «Destroy».

Дополнительное тестирование

Перейдите к файлам dev.tfvars и main.tfvars, измените количество инстансов на два или три. Убедитесь, что пайплайн по-прежнему работает и создано несколько инстансов.

Траблшутинг

Если вы отклонитесь от описанных выше шагов, можете получить другие результаты. Например, если будете использовать IDE поверх Cloud9 или возьмёте Amazon Linux вместо Ubuntu. Если же вы строго следовали инструкциям, но проблемы возникли всё равно, нужно вернуться к своим шагам и проверить, какие вы указывали переменные и создавали учётные данные. Также при наличии ошибок рекомендуем проверить логи.

«DevOps Tools для разработчиков»

Комментарии (6)


  1. Aquahawk
    18.01.2023 15:15
    +2

    А после того как вы храните структуру облака в облаке, вы можете говорить что вы владеете ситуацией? Или у вас теперь есть только пароль от облачного сервиса хранения паролей?


    1. stackjava
      19.01.2023 09:58

      Думаю ответа вы не дождетесь... Данный пользователь, судя по количеству статей в год, не автор, а тот кто просто публикует статьи.


  1. skymal4ik
    18.01.2023 18:00

    Спасибо за статью, было интересно почитать. Хоть и с оговорками, я предпочитаю по возможности хранить своё барахло у себя на машине и серверах. Но это личный принцип :)

    Terraform Cloud позволяет хранить Terraform State (состояние Terraform) удалённо, а не локально. Это повышает безопасность

    Спасибо, поржал. :)))


    1. mc2
      20.01.2023 02:58

      Документация:

      https://developer.hashicorp.com/terraform/language/state/sensitive-data


  1. stackjava
    18.01.2023 20:27
    +1

    Катерина, просто удивляет меня... 63 статьи за год... Шок


  1. KozB
    20.01.2023 11:15

    отличная статья. я добавлю пару нюансов.

    1. если мы не привязываемся к определенному AMI нужно его добавить в игнорирование при изменении иначе неожиданность вас ждёт когда latest поменяться.

    2. для безопасности лучшие не хранить ключи в файле, а использовать hashicorp vault или платные как cyberark vault .