Github: обзор Github Actions и деплой с ArgoCD

Автор: | 06/05/2021
 

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

По сути, Github Actions весьма схож с TravisCI, но более тесно интегрирован в сам Github, вплоть до того, что интерфейс Actions доступен прямо из Github:

Кратко рассмотрим основные возможности, а в следующих частях задеплоим self-hosted runners в Kubernetes кластер и построим CI/CD с использованием Github Actions и ArgoCD.

Стоимость Github Actions

См. документацию тут>>>.

Использование GitHub Actions бесплатно как для free-репозиториев, так и для paid-планов, но в рамках определённых ограничений:

Product Storage Minutes (per month)
GitHub Free 500 MB 2,000
GitHub Pro 1 GB 3,000
GitHub Free for organizations 500 MB 2,000
GitHub Team 2 GB 3,000
GitHub Enterprise Cloud 50 GB 50,000

У нас GitHub Team, значит на месяц мы получаем 2 гигабайта хранилища и 3000 минут бесплатно.

При этом стоимость билдов на Linux, macOS или упаси боже Windows тоже разная:

Operating system Minute multiplier
Linux 1
macOS 10
Windows 2

Т.е. на macOS мы могли бы использовать всего 300 минут билдов.

А каждая дополнительная минута стоит дополнительных денег:

OS Resources Price per extra minute
Linux 2 cores, 7 GB $ 0,008
Windows 2 cores, 7 GB $0,016
MacOS 2 cores, 7 GB $ 0,08
Self-hosted free

Также, Github Actions могут работать как в Github Cloud, так и в виде self-hosted runners, что может решить вопрос с доступами к Kubernetes-кластерам, так как постоянного пула IP у Github нет, соответственно появляется проблема с настройкой SecurityGroup/firewalls.

Сам Github предлагает лепить костыли периодически загружать json-файл со списком обновлённых сетей (btw, работают Github Actions в Microsoft Azure), и городить лишнюю автоматизацию для этого банально лень.

Github Actions: обзор

В Actions билд происходит следующим образом (см. Introduction to GitHub Actions):

  1. event (например, пул-реквест или коммит в репозиторий, см. список тут>>>) триггерит запуск workflow, а workflow содержит jobs
  2. job содержит список steps, а каждый step содержит один или более actions
  3. actions выполняются на одном и том же runner, и несколько таких actions могут выполняться в рамках одного workflow одновременно

Основные компоненты:

  • runner: сервер самого Github или self-hosted, на котором выполняется job
  • workflow: описанная процедура, включающая в себя одну или более job, которая триггеррится неким event
  • jobs: набор steps, которые выполняются на одном и том же runner. Если в одном workflow несколько jobs – по-умолчанию они запускаются параллельно, но могут быть настроены на выполенние по очереди и зависеть от результатов выполнения друг друга.
  • steps: задача, выполняющая команды или actions. Так как steps в одной job выполняются на одном runner – они могут обмениваться данными между собой.
  • actions: основные “исполняемые блоки” – могут представлять собой набор уже готовых задач, или выполнять обычные команды

Структура workflow файла

Кратко рассмотрим структуру файла для workflow:

  • name: имя флоу
  • on: event, при котором будет запущен workflow
  • jobs: список задач этого workflow
    • <JOB_NAME>
      • runs-on: ранrunnerнер, на котором задача будет выполняться
      • steps: шаги в рамках этой job, используя uses или run
        • uses: имя action для запуска
        • run: обычная команда для запуска

Добавление workflow файла

Напишем простой файл.

В корне репозитория создаём каталог .github/workflows/ – тут будут храниться все файлы флоу, которые могут потом триггерится разными events:

[simterm]

$ mkdir -p .github/workflows

[/simterm]

В нём создаём файл, например .github/workflows/actions-test.yaml:

name: actions-test
on: [push]
jobs:
  print-hello:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Hello, world"

Сохраняем и пушим в репозиторий:

[simterm]

$ git add .github/workflows/actions-test.yaml && git commit -m "Test flow" && git push

[/simterm]

В репозитории переходим в Actions, и видим его выполнение:

Events

В Events можно описывать достаточно сложные условия, по которым будет принимать решение о выполнении workflow.

Такими условиями может быть коммит или пул-реквест в репозитории, расписание или определённое событие вне Github, которое выполняет webhook к репозиторию, при этом можно задать несколько событий, при наступлении которых будет запущен workflow.

Например, можно для разных event указать разные бранчи:

name: actions-test
on: 
  push:
    branches:
      - master
  pull_request:
    branches:
      - test-branch
...

Или задать выполнение по крону, см. Scheduled events:

name: actions-test
on: 
  schedule:
    - cron: '* * * *'

Ручной запуск – workflow_dispatch

Можно настроить воркфлоу на ручной запуск, добавив в on условие workflow_dispatch:

name: actions-test
on: 
   workflow_dispatch
jobs:
  print-hello:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Hello, world"

После чего в Actions появится кнопка для запуска с выбором бранча:

Кроме того, сюда же можно добавить набор вводных данных, используя inputs, которые на время выполнения будут сохранены в контексте github.event и доступны в виде переменных:

name: actions-test
on: 
   workflow_dispatch:
     inputs:
       userName:
         description: "Username"
         required: true
         default: "Diablo"
jobs:
  print-hello:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Username: ${{ github.event.inputs.username }}"
      - run: echo "Actor's username: ${{ github.actor }}"

Тут в ${{ github.event.inputs.username }} мы берём значение из workflow_dispatch.inputs.userName, а в github.actor получаем метаданные Github Actions:

Например, таким образом можно будет передавать Docker image tag для деплоя с ArgoCD.

Webhooks: create

См. полный список событий в Webhook events.

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

Например, что бы запускать флоу при создании нового бранча или тега – используем create:

name: actions-test
on: 
  create
jobs:
  print-hello:
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo "Event name: ${{ github.event_name }}"
          echo "Actor's username: ${{ github.actor }}"

Тут ещё используем ${{ github.event_name }}, что бы отобразить имя триггера.

Создаём бранч и пушим его:

[simterm]

$ git checkout -b a-new-branch
Switched to a new branch 'a-new-branch'

$ git push -u origin a-new-branch

[/simterm]

Проверяем:

Переменные окружения

Также, Github Actions поддерживают работу с переменными окружения.

Имеется набор дефолтных переменных, см. Default environment variables, плюс можно задать свои на уровне всего workflow в jobs, на уровне определённой job или step.

При этом надо учитывать “особенности” обращения к переменным, см. About environment variables:

  • context variable${{ env.VARNAME }}: подстановка значения выполняется на уровне процессинга workflow-файла до отправки на runner, используем везде кроме run, например при проверке условий с if (см. ниже)
  • environment variable$VARNAME: подстановка значения выполняется на runner во время выполнения run
  • для создания переменной окружения во время выполнения job – используем файл, заданный в $GITHUB_ENV – значения из него будут доступны всем step далее

Пример:

name: vars-test

on:
  push

env:
  VAR_NAME: "Global value"

jobs:
  print-vars:
    runs-on: ubuntu-latest
    steps:

      # using own varibales
      - name: "Test global var as $VAR_NAME"
        run: echo "Test value $VAR_NAME"

      - name: "Test global var as ${{ env.VAR_NAME }}"
        run: echo "Test value ${{ env.VAR_NAME }}"

      # using default variables
      - name: "Test job var as $GITHUB_REPOSITORY"
        run: echo "Test value $GITHUB_REPOSITORY"

      # this will be empty, as default variables are not in the context
      - name: "Test job var as ${{ env.GITHUB_REPOSITORY }}"
        run: echo "Test value ${{ env.GITHUB_REPOSITORY }}"

      # using 'dynamic' variables
      - name: "Set local var"
        run: echo "local_var=local value" >> $GITHUB_ENV

      - name: "Print local var as $local_var"
        run: echo "$local_var"

      - name: "Print local var as ${{ env.local_var }}"
        run: echo "${{ env.local_var }}"

И результат:

Secrets

Ну и куда же без секретов?

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

Добавляются секреты в настройках репозитория > Secrets:

Добавляем использование, причём можно как создать переменную, которая будет хранить секрет, так и обращаться к секрету напрямую:

name: actions-test

on: 
  push

env:
  TEST_ENV: ${{ secrets.TEST_SECRET }}

jobs:
  print-hello:
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo "Test secret: ${{ secrets.TEST_SECRET }}"
          echo "Test secret: ${{ env.TEST_ENV }}"

Запускаем:

Условия и if

Actions поддерживает проверку условий для jobs, используя оператор if за которым следует выражение, см. About contexts and expressions.

Например:

name: actions-test

on:
  push
  
jobs:
  print-hello:
    runs-on: ubuntu-latest
    steps:

      - id: 'zero'
        run: echo "${{ github.actor }}"

      - id: 'one'
        run: echo "Running because of 'github.actor' contains a 'setevoy' string"
        if: "contains(github.actor, 'setevoy')"

      # this will not run
      - id: 'two'
        run: echo "Skipping because of 'github.actor' contains a 'setevoy' string"
        if: "!contains(github.actor, 'setevoy')"

      - id: 'three'
        run: echo "Running because of Step Two was skipped"
        if: steps.two.conclusion == 'skipped'

      - id: 'four'
        run: echo "Running because of commit message was '${{ github.event.commits[0].message }}'"
        if: contains(github.event.commits[0].message, 'if set')

      - id: 'five'
        run: echo "Running because of previous Step was successful and the trigger event was 'push'"
        if: success() && github.event_name == 'push'

Тут мы используем github context, функцию contains(), операторы != и &&, steps context для проверки условий.

Результат:

needs – зависимости job

Кроме использования if: success() в steps, можно прописать зависимости самих jobs используя needs:

name: actions-test

on: 
  push

jobs:

  init:
    runs-on: ubuntu-latest
    steps:
      - run: echo "An init job"

  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "A build job" && exit 1
    needs: 'init'

  deploy:
    runs-on: ubuntu-latest
    steps:
      - run: echo "A deploy job"
    if: always()
    needs: ['init', 'build']

Тут в build job ожидаем выполнения джобы init, а в джобе deploy – ждём и init, и build, а используя if: always() запускаем её независимо от результатов выполнения предыдущих двух задач:

Actions

И последним рассмотрим “основной” компонент Github Actions – собственно, сами Actions.

Actions позволяют в job и step использовать уже созданные скрипты и утилиты из Github Actions Marketplace или готовые образы с Docker Hub, см. Finding and customizing actions.

В примере ниже используем actions/checkout@v2 для клонирования репозитория на runner-агент и omegion/argocd-app-actions для синхронизации ArgoCD приложения (см. ArgoCD: обзор, запуск, настройка SSL, деплой приложения).

Приложение ArgoCD

Подготовим тестовое приложение:

Обновляем argocd-cm ConfigMap, т.к. у admin по-умолчанию нет прав на использование токенов:

...
data:
  accounts.admin: apiKey,login
...

Логинимся:

[simterm]

$ argocd login dev-1-18.argocd.example.com
Username: admin
Password: 
'admin' logged in successfully
Context 'dev-1-18.argocd.example.com' updated

[/simterm]

Генерируем токен для юзера admin:

[simterm]

$ argocd account generate-token
eyJ***3Pc

[/simterm]

Github Actions workflow для ArgoCD

Добавляем секрет с токеном:

Создаём флоу:

name: "ArgoCD sync"
on: "push"
jobs:
  build:
    runs-on: ubuntu-latest
    steps:

      - name: "Clone reposiory"
        uses: actions/checkout@v2
        with:
          repository: "argoproj/argocd-example-apps.git"
          ref: "master"

      - name: "Sync ArgoCD Application"
        uses: omegion/argocd-app-actions@master
        with:
          address: "dev-1-18.argocd.example.com"
          token: ${{ secrets.ARGOCD_TOKEN }}
          appName: "guestbook"

Пушим в репозиторий, и проверяем:

И приложение в ArgoCD задеплоено:

Готово.