GitHub Actions: Docker-білд в AWS ECR та деплой Helm-чарту в AWS EKS

Автор |  02/10/2023
 

Отже, маємо розгорнутий кластер Kubernetes – див. серію Terraform: створення EKS, частина 1 – VPC, Subnets та Endpoints.

Маємо GitHub Actions workflow для його деплою – див. GitHub Actions: деплой Dev/Prod оточень з Terraform.

Прийшов час почати деплоїти наш бекенд в Kubernetes.

Тут знов використаємо GitHub Actions – будемо білдити Docker-образ з API-сервісом бекенду, зберігати його в AWS Elastic Container Service, а потім деплоїти Helm-чарт, якому у values передамо новий Docker Tag.

Робоче оточення поки одне, Dev, пізніше додамо ще Staging та Production. Крім того, треба мати можливість задеплоїти feature-environment – на той же Dev EKS, але з кастомними значеннями деяких змінних.

Проте зараз будемо робити в тестовому репозиторії “atlas-test” і з тестовим Helm-чартом.

Release flow planning

Як будемо релізити?

Поки вирішили по такій схемі:

Тобто:

  • девелопер створює бранч, пише код, тестує локально в Docker Compose
  • після завершення роботи над фічею – створює Pull Request з лейблою “deploy
    • workflow Deploy Feature Env
      • тригер: створення  Pull Request з лейблою “deploy
      • білдить Docker-образ і тегає його з git commit sha --short
      • пушить його в ECR
      • створює feature-оточення в GitHub
      • деплоїть в Kubernetes Dev в feature-env namespace, де девелопер може додатково потестити свої зміни в умовах, наближених до реальних
  • після мержу PR в master-гілку:
    • workflow  Deploy Dev:
      • тригер: push to master або вручну
      • білдить Docker-образ і тегає його з git commit sha --short
      • деплой на Dev
      • якщо деплой пройшов (Helm не видав помилок, Pods запустились, тобто перевірки readiness та liveness Probes пройшли) – створюємо Git Tag
      • тегаємо вже існуючий Docker образ з цим тегом
    • workflow  Deploy Stage:
      • тригер: Git Tag created
      • деплоїться існуючий Docker образ з цим тегом
      • запускаються integration tests (mobile, web)
      • якщо тести пройшли – то створюємо GitHub Release – chanelog, etc
    • workflow  Deploy Prod:
      • тригер: Release created
      • деплоїться існуючий образ з тегом цього релізу
      • виконуються тести
  • ручний деплой
    • з будь якого існуючого образу на Dev або Staging

Сьогодні зробимо два workflow – Deploy Dev, і Deploy та Destroy Feature-env.

Setup AWS

Для початку нам треба мати ECR та IAM Role.

ECR -для зберігання образів, які будемо деплоїти, а IAM Role буде використовувати GitHub Action для доступу до ECR та логіну в EKS під час деплою.

Репозиторій в ECR у нас вже є, теж з назвою “atlas-test“, створено поки що руками – пізніше перенесемо менеджмент ECR в Terraform.

А от AWS IAM-ролі для проектів в GitHub, які будуть деплоїтись в Kubernetes можемо зробити відразу на етапі створення EKS-кластеру.

Terraform: створення IAM Role

Для деплою з GitHub в AWS ми використовуємо OpenID Connect, тобто аутентифікований юзер GitHub (або в нашому випадку – GitHub Actions Runner) може прийти в AWS, і там виконати AssumeRole, а потім з політиками цієї ролі пройти авторизацію в AWS – перевірку того, що він там може робити.

Щоб деплоїти з GitHub в EKS нам потрібни політики на:

  • eks:DescribeCluster та eks:ListClusters: щоб авторизуватись в EKS-кластері
  • ecr: push та read образів з ECR-репозиторію

Крім того, для цієї ролі задамо обмеження на те, з якого репозиторію GitHub можна буде виконати AssumeRole.

В проекті EKS додамо змінну github_projects з типом list, в якій будуть всі GitHub-проекти, яким ми будемо дозволяти деплоїти в цей кластер, поки він тут буде один:

...

variable "github_projects" {
  type        = list(string)
  default     = [
    "atlas-test"
  ]
  description = "GitHub repositories to allow access to the cluster"
}

Описуємо сам роль, де в циклі for_each перебираємо всі елементи списку github_projects:

data "aws_caller_identity" "current" {}

resource "aws_iam_role" "eks_github_access_role" {
  for_each = var.github_projects
  name = "${local.env_name}-github-${each.value}-access-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRoleWithWebIdentity"
        Effect = "Allow"
        Sid    = ""
        Principal = {
          Federated : "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/token.actions.githubusercontent.com"
        }
        Condition: {
            StringLike: {
                "token.actions.githubusercontent.com:sub": "repo:GitHubOrgName/${each.value}:*"
            },
            StringEquals: {
                "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
            }
        }
      }
    ]
  })

  inline_policy {
    name = "${local.env_name}-github-${each.value}-access-policy"

    policy = jsonencode({
      Version = "2012-10-17"
      Statement = [
        {
          Action   = [
            "eks:DescribeCluster*",
            "eks:ListClusters"
          ]
          Effect   = "Allow"
          Resource = module.eks.cluster_arn
        },
        {
          Action   = [
            "ecr:GetAuthorizationToken",
            "ecr:BatchGetImage",
            "ecr:BatchCheckLayerAvailability",
            "ecr:CompleteLayerUpload",
            "ecr:GetDownloadUrlForLayer",
            "ecr:InitiateLayerUpload",
            "ecr:PutImage",
            "ecr:UploadLayerPart"
          ]
          Effect   = "Allow"
          Resource = "*"
        },
      ]
    })
  }

  tags = {
    Name = "${local.env_name}-github-${each.value}-access-policy"
  }
}

Тут:

  • в assume_role_policy дозволяємо AssumeRoleWithWebIdentity цієї ролі для token.actions.githubusercontent.com і репозиторію repo:GitHubOrgName:atlas-test
  • в inline_policy:
    • дозволяємо eks:DescribeCluster кластеру, який деплоїться
    • дозволяємо eks:ListClusters всіх кластерів
    • дозволяємо операції в ECR на всі репозиторії

На ECR було б краще обмежити конкретними репозиторіями, але це поки в тесті, і ще не відомо який неймінг репозиторієв буде.

Див. Pushing an image, AWS managed policies for Amazon Elastic Container Registry та Amazon EKS identity-based policy examples.

Далі треба додати ці створені ролі в aws_auth_roles, де зараз вже маємо одну роль:

...
  aws_auth_roles = [
    {
      rolearn  = aws_iam_role.eks_masters_access_role.arn
      username = aws_iam_role.eks_masters_access_role.arn
      groups   = ["system:masters"]
    }
  ]
...

В locals будуємо новий list(map(any))github_roles, а потім в aws_auth_roles за допомогою flatten() створюємо новий list, в який включаємо eks_masters_access_role та ролі із github_roles:

...
locals {
  vpc_out = data.terraform_remote_state.vpc.outputs
  github_roles = [ for role in aws_iam_role.eks_github_access_role : {
      rolearn = role.arn
      username  = role.arn
      groups   = ["system:masters"]
    }]
  aws_auth_roles = flatten([ 
    {
      rolearn = aws_iam_role.eks_masters_access_role.arn
      username  = aws_iam_role.eks_masters_access_role.arn
      groups   = ["system:masters"]
    },
    local.github_roles
  ])
}
...

Поки тут використовуємо system:masters, бо це все ще в розробці, і RBAC поки не налаштовую. Але див. User-defined cluster role binding should not include system:masters group as a subject.

Деплоїмо, та перевіряємо aws-auth ConfigMap, де тепер маємо новий об’єкт в mapRoles:

Workflow: Deploy Dev manually

Тепре, маючи ролі, можемо робити Workflow.

Почнемо з ручного деплою на Dev, бо він самий простий. А потім вже маючи працючий білд і процеси – будемо робити решту.

Triggers

По яких умовах будемо запускати білд?

  • workflow_dispatch:
    • з будь-якого бранча або тега
  • push в master: в репозиторії бекенду master-бранч у нас має обмеження на push тільки з Pull Request, тож інших пушів тут не буде

Ще можна робити додаткову перевірку в джобах, на кшталт:

- name: Build
        if: github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true

Див. Trigger workflow only on pull request MERGE.

Environments та Variables

На рівні репозиторію додаємо змінні – переходимо в Settings > Secrets and variables > Actions:

  • ECR_REPOSITORY
  • AWS_REGION

Створюємо GitHub Environment “dev“, і йому задаємо:

  • AWS_IAM_ROLE: arn беремо з outputs деплою EKS з Terraform
  • AWS_EKS_CLUSTER: ім’я беремо з outputs деплою EKS з Terraform
  • ENVIRONMENT: “dev

Job: Docker Build

Тестити будемо з мінімальним Dockerfile – створюємо його в корні репозиторію:

FROM alpine

Створюємо директорію .github/workflows:

$ mkdir -p .github/workflows

І в ній файл .github/workflows/deploy-dev.yml:

name: Deploy to EKS

on: 
  workflow_dispatch:
  push:
    branches: [ master ]  

permissions:
  id-token: write
  contents: read

jobs:

  build-docker:

    name: Build Docker image
    runs-on: ubuntu-latest
    environment: dev

    steps:

    - name: "Setup: checkout code"
      uses: actions/checkout@v3

    - name: "Setup: Configure AWS credentials"
      uses: aws-actions/configure-aws-credentials@v2
      with:
        role-to-assume: ${{ vars.AWS_IAM_ROLE }}
        role-session-name: github-actions-test
        aws-region: ${{ vars.AWS_REGION }}

    - name: "Setup: Login to Amazon ECR"
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v1
      with:
        mask-password: 'true'

    - name: "Setup: create commit_sha"
      id: set_sha
      run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT

    - name: "Build: create image, set tag, push to Amazon ECR"
      env:
        ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }}
        IMAGE_TAG: ${{ steps.set_sha.outputs.sha_short }}
      run: |
        docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f Dockerfile .
        docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG

В ньому маємо джобу, яка запускається з GitHub Environment == dev, і:

  • з actions/checkout зачекаутить код на GitHub Runner
  • з aws-actions/configure-aws-credentials залогіниться в AWS виконавши AssumeRole ролі, яку ми створили раніше
  • з aws-actions/amazon-ecr-login залогіниться в AWS ECR (мені стало цікаво як жеж він логиниться, але – 54.000 строк коду на JS!)
  • згенерує output sha_short, в який внесе Commit ID
  • і виконає docker build та docker push

Пушимо, мержимо в мастер, і запускаємо білд:

Перевіряємо образ в ECR:

Job: Helm deploy

Наступний шаг – це задеплоїти Helm-чарт в EKS.

Швиденько зробимо тестовий чарт:

$ mkdir -p helm/templates

В директорії helm створюємо файл Chart.yaml:

apiVersion: v2
name: test-chart
description: A Helm chart
type: application
version: 0.1.0
appVersion: "1.16.0"

Файл templates/deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: test-app
  template:
    metadata:
      labels:
        app: test-app
    spec:
      containers:
        - name: test-container
          image: {{ .Values.image.repo}}:{{ .Values.image.tag }}
          imagePullPolicy: Always

Та файл values.yaml:

image:
  repo: 492***148.dkr.ecr.us-east-1.amazonaws.com/atlas-test
  tag: latest

Передача змінних між GitHub Action Jobs

Далі питання по самому Workflow, а саме – як передати Docker tag, який ми створили в Job build-docker?

Можно зробити деплой з Helm в тій самій джобі, і тоді зможемо використати ту ж змінну IMAGE_TAG.

А можемо зробити окремою job, і передати значення тегу між джобами.

Великого сенсу розділяти на дві джоби тут нема, але по-перше це в білді виглядає більш логічно, по-друге – хочеться спробувати всякі штуки в GitHub Actions, тому давайте передамо значення змінної між джобами.

Для цього в першій джобі додаємо outputs:

...
jobs:

  build-docker:

    name: Build Docker image
    runs-on: ubuntu-latest
    environment: dev

    outputs:
      image_tag: ${{ steps.set_sha.outputs.sha_short }}
...

А потім його використаємо в новій джобі з Helm, в якій передаємо як values: image.tag:

...

  deploy-helm:

    name: Deploy Helm chart
    runs-on: ubuntu-latest
    environment: dev
    needs: build-docker

    steps:
    
    - name: "Setup: checkout code"
      uses: actions/checkout@v3

    - name: "Setup: Configure AWS credentials"
      uses: aws-actions/configure-aws-credentials@v2
      with:
        role-to-assume: ${{ vars.AWS_IAM_ROLE }}
        role-session-name: github-actions-test
        aws-region: ${{ vars.AWS_REGION }}

    - name: "Setup: Login to Amazon ECR"
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v1
      with:
        mask-password: 'true' 

    - name: Deploy Helm
      uses: bitovi/[email protected]
      with:
        cluster-name: ${{ vars.AWS_EKS_CLUSTER }}
        namespace: ${{ vars.ENVIRONMENT }}-testing-ns
        name: test-release
        # may enable roll-back on fail
        #atomic: true
        values: image.tag=${{ needs.build-docker.outputs.image_tag }}
        timeout: 60s
        helm-extra-args: --debug

Пушимо зміни, і перевіряємо деплой:

Job: створення Git Tag

Наступним кроком треба створити Git Tag. Тут можемо використати Action github-tag-action, який під капотом виконує перевірку заголовку комміту, і в залежності від нього – інкрементить major, minor або patch версію тегу.

Тож давайте спершу поглянемо на Commit Message Format, хоча взагалі-то це тема, яку можна було б винести окремим постом.

Git commit message format

Див. Understanding Semantic Commit Messages Using Git and Angular.

Отже, якщо кратко, то заголовок пишемо в форматі “type(scope): subject“, тобто, наприклад – git commit -m "ci(actions): add new workflow".

При цьому type умовно можна поділити на development та production, тобто – зміни, якві відносяться до розробки/розробників, або зміни, які відносяться до production-оточення та end-юзерів:

  • development:
    • build (раніше chore): зміни, якві відносяться до білду та пакетів (npm build, npm install, etc)
    • ci: зміни CI/CD (workflow-файли, terraform apply, etc)
    • docs: зміни в документації проекту
    • refactor: рефакторінг коду – нові назви змінних, спрощення коду
    • style: зміни коду в відступах, коми, лапки-дужки і т.д.
    • test: зміни в тестах коду – юніт-тести, інтеграційні, etc
  • production:
    • feat: нові фічі, функціональність
    • fix: баг-фікси
    • perf: зміни, які стосуються performance

Тож додаємо нову job:

...

  create_tag:
    name: "Create Git version tag"
    runs-on: ubuntu-latest
    timeout-minutes: 5
    needs: deploy-helm
    permissions:
      contents: write
    outputs:
      new_tag: ${{ steps.tag_version.outputs.new_tag }}

    steps:
      - name: "Checkout"
        uses: actions/checkout@v3

      - name: "Misc: Bump version and push tag"
        id: tag_version
        uses: mathieudutour/[email protected]
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          tag_prefix: eks-

В джобі вказуємо needs, щоб запускати тільки після деплою Helm, додаємо permissions, щоб github-tag-action мав змогу додавати теги в репозиторії, і додаємо tag_prefix, бо в репозіторії бекенду, де все це потім буде працювати, вже є стандартні теги з префіксом v. А токен в secrets.GITHUB_TOKEN є по дефолту в саміх Action.

Пушимо з комітом ga -A && gm "ci(actions): add git tag job" && gp, і маємо новий тег:

Workflow: Deploy Feature Environment

Окей – у нас є білд Docker, є деплой Helm-чарту. Все деплоїться на Dev-оточення, все працює.

Давайте додамо ще один workflow – для деплою на EKS Dev, але вже не як dev-оточення, а у тимчасовий Kubernetes Namespace, щоб девелопери мали змогу потестити свої фічі незалежно від Dev-оточення.

Для цього нам потрібно:

  • тригерити workflow при створенні Pull Request з лейблою “deploy”
  • створити custom name для нового Namespace – використаємо Pull Request ID
  • збілдити Docker
  • задеплоїти Helm-чарт у новий Namespace

Створюємо новий файл – create-feature-env-on-pr.yml.

В ньому буде три джоби:

  • Docker build
  • Deploy feature-env
  • Destroy feature-env

Умови запуску Jobs з if та github.event context

Docker build та Deploy мають запускатись, коли Pull Request створено і якщо він має лейблу “deploy”, а Destroy – коли Pull Request з лейблою “deploy” закрито.

Для тригеру workflow задаємо умову on.pull_request – тоді будемо мати PullRequestEvent з набором полів, які можемо перевірити.

Єдине, що в документації чомусь не вказано поле label, про що говорилось ще в 2017 (!) році, але на ділі вона є.

Тут дуже може допомогти додатковий step, в якому можна вивести весь payload.

Створюємо бранч для тестів, і у файлі create-feature-env-on-pr.yml додаємо першу джобу:

name: "Create feature environment on PR"

on:
  pull_request:
    types: [ opened, edited, closed, reopened, labeled, unlabeled, synchronize ]

permissions:
  id-token: write
  contents: read

concurrency:
  group: deploy-${{ github.event.number }}
  cancel-in-progress: false

jobs:

  print-event:    
    name: Print event
    runs-on: ubuntu-latest
    steps:
    - name: Dump GitHub context
      env:
        GITHUB_CONTEXT: ${{ toJson(github.event) }}
      run: |
        echo "$GITHUB_CONTEXT"

Пушимо в репозиторій, створюємо Pull Request, створюємо нову label:

І наш workflow запустився:

І видав нам всі дані, які маємо в event:

Тепер, маючи їх, можемо подумати про те, як будемо перевіряти умови для запуску jobs:

  • Створити Environment, якщо Pull Request: opened, editied, reopened, synchronize (якщо в source-бранч додано новий коміт)
    • але Pull Request може бути без лейбли – тоді нам деплоїти не треба
    • до вже існуючого Pull Request може бути додана лейбла – тоді event буде labeled, деплоїмо
  • Видалити Environment, якщо Pull Request: closed
    • але може бути без лейбли – тоді джобу на видалення запускати не треба
    • але у вже існуючого Pull Request може бути прибрана лейбла “deploy” – тоді event буде unlabeled, видаляємо Environment

Умови запуску Workflow у нас зараз виглядають так:

on:
  pull_request:
    types: [opened, edited, closed, reopened, labeled, unlabeled, synchronize]

Job: Deploy при створенні Pull Request з лейблою “deploy”

Отже, вище вже побачили, що при створенні Pull Request з лейблою “deploy” маємо "action": "opened" та pull_request.labels[].name: "deploy":

Тоді можемо перевірити умову як:

if: contains(github.event.pull_request.labels.*.name, 'deploy')

Див. contains.

Але якщо івент був на закриття Pull Request – то він все одно буде мати “deploy” лейблу, і тригерне нашу джобу.

Тому додаємо ще одну умову:

if: contains(github.event.pull_request.labels.*.name, 'deploy') && github.event.action != 'closed'

Тож тестова джоба, яка буде “деплоїти”, може виглядати так:

...

jobs:

  print-event:    
    name: Print event
    runs-on: ubuntu-latest
    steps:
    - name: Dump GitHub context
      env:
        GITHUB_CONTEXT: ${{ toJson(github.event) }}
      run: |
        echo "$GITHUB_CONTEXT"

  deploy:
    name: Deploy
    if: contains(github.event.pull_request.labels.*.name, 'deploy') && github.event.action != 'closed'
    runs-on: ubuntu-latest
    steps: 
    - name: Run deploy 
      run: echo "This is deploy"

Пушимо, створюмо PR, перевіряємо, і бачимо, що джоба запустилась два рази:

Бо при створенні PR з лейблою маємо два івенти – власне створення PR, тобто івент “opened“, та додавання лейбли – event “labeled“.

Взагалі виглядає трохи криво, як на мене. Але, мабуть, GitHub не зміг обійтися одним івентом в такому випадку.

Тому ми можемо просто прибрати opened з тригерів on.pull_request – і при створенні PR з лейблою джоба затригериться тільки на івент “labeled“.

Ще одна річ, яка виглядає кривувато, хоча тут ми її не використовуємо, але:

  • для перевірки string в contains() ми використовуємо форму contains(github.event.pull_request.labels.*.name, 'deploy') – спочатку вказуємо об’єкт, в якому шукаємо, потім строка, яку шукаємо
  • але щоб перевірити декілька strings – формат буде contains('["labeled","closed"]', github.event.action) – тобто спочатку список строк для перевірки, потім об’єкт, в якому їх шукаємо

Окей, йдемо далі: ми можемо мати ще одну умову для запуску – коли для вже створеного PR без лейбли “deploy” який не тригернув нашу джобу, була додана лейбла “deploy” – і тоді нам треба запустити джобу.

Це можемо перевірити з такою умовою:

github.event.action == 'labeled' && github.event.label.name == 'deploy'

А щоб вибрати одну із умов – першу, чи цю – використовуємо оператор “або” – “||“, тобто наш if буде виглядати так:

if: |
  (contains(github.event.pull_request.labels.*.name, 'deploy') && github.event.action != 'closed') ||
  (github.event.action == 'labeled' && github.event.label.name == 'deploy')'

Після чого для тесту створюємо PR без лейбли – і спрацює тільки джоба “Print event”:

Потім додаємо лейблу “deploy” – і маються стригеритись обидві джоби:

Job: Destroy при закритті Pull Request з лейблою “deploy”

Залишилось зробити job для видалення feature-env – коли Pull Request з лейблою “deploy” закривається.

Умови тут схожі з тими, що ми робили для деплою:

...

  destroy:
    name: Destroy 
    if: |
      (contains(github.event.pull_request.labels.*.name, 'deploy') && github.event.action == 'closed') ||
      (github.event.action == 'unlabeled' && github.event.label.name == 'deploy')
    runs-on: ubuntu-latest
    steps: 
    - name: Run destroy  
      run: echo "This is dstroy"
  • запуск джоби, якщо action == 'closed' і labels.*.name == 'deploy' АБО
  • запуск джоби, якщо action == 'unlabeled' і event.label.name == 'deploy'

Перевіряємо – створюємо PR з лейблою “deploy” – запускається джоба Deploy, мержимо цей PR – і маємо джобу Destroy:

Тож повністю файл зараз виглядає так:

name: "Create feature environment on PR"

on:
  pull_request:
    types: [ opened, edited, closed, reopened, labeled, unlabeled, synchronize ]

permissions:
  id-token: write
  contents: read

concurrency:
  group: deploy-${{ github.event.number }}
  cancel-in-progress: false

jobs:

  print-event:    
    name: Print event
    runs-on: ubuntu-latest
    steps:
    - name: Dump GitHub context
      env:
        GITHUB_CONTEXT: ${{ toJson(github.event) }}
      run: |
        echo "$GITHUB_CONTEXT"

  deploy:
    name: Deploy
    if: |
      (contains(github.event.pull_request.labels.*.name, 'deploy') && github.event.action != 'closed') ||
      (github.event.action == 'labeled' && github.event.label.name == 'deploy')  
    runs-on: ubuntu-latest
    steps: 
    - name: Run deploy 
      run: echo "This is deploy"

  destroy:
    name: Destroy 
    if: |
      (contains(github.event.pull_request.labels.*.name, 'deploy') && github.event.action == 'closed') ||
      (github.event.action == 'unlabeled' && github.event.label.name == 'deploy')
    runs-on: ubuntu-latest
    steps: 
    - name: Run destroy  
      run: echo "This is dstroy"

Добре – наче все працює. Якщо що – то вже по ходу діла поправимо умови, але в цілому ідея іх використання така.

Створення Feature Environment

Тут вже в принципі все нам відомо – використовуємо джоби, які робили для Deploy Dev, тільки міняємо пару параметрів.

В тому ж файлі create-feature-env-on-pr.yml описуємо джоби.

В першій, Docker build, нічого не міняється – тільки додаємо if:

...

  build-docker:

    name: Build Docker image
    if: |
      (contains(github.event.pull_request.labels.*.name, 'deploy') && github.event.action != 'closed') ||
      (github.event.action == 'labeled' && github.event.label.name == 'deploy')
    runs-on: ubuntu-latest
    timeout-minutes: 30
    environment: dev

    outputs:
      image_tag: ${{ steps.set_sha.outputs.sha_short }}

    steps:

    - name: "Setup: checkout code"
      uses: actions/checkout@v3

    - name: "Setup: Configure AWS credentials"
      uses: aws-actions/configure-aws-credentials@v2
      with:
        role-to-assume: ${{ vars.AWS_IAM_ROLE }}
        role-session-name: github-actions-test
        aws-region: ${{ vars.AWS_REGION }}

    - name: "Setup: Login to Amazon ECR"
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v1
      with:
        mask-password: 'true'

    - name: "Setup: create commit_sha"
      id: set_sha
      run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT

    - name: "Build: create image, set tag, push to Amazon ECR"
      env:
        ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }}
        IMAGE_TAG: ${{ steps.set_sha.outputs.sha_short }}
      run: |
        docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f Dockerfile .
        docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG

А в джобу Helm-деплой теж додамо if та новий step – “Misc: Set ENVIRONMENT variable“, який із значення github.event.number буде створювати нове значення для змінної ENVIRONMENT, яку ми передаємо в ім’я неймспейсу, в який буде деплоїтись чарт:

...

  deploy-helm:

    name: Deploy Helm chart
    if: |
      (contains(github.event.pull_request.labels.*.name, 'deploy') && github.event.action != 'closed') ||
      (github.event.action == 'labeled' && github.event.label.name == 'deploy')    
    runs-on: ubuntu-latest
    timeout-minutes: 15
    environment: dev
    needs: build-docker

    steps:
    
    - name: "Setup: checkout code"
      uses: actions/checkout@v3

    - name: "Setup: Configure AWS credentials"
      uses: aws-actions/configure-aws-credentials@v2
      with:
        role-to-assume: ${{ vars.AWS_IAM_ROLE }}
        role-session-name: github-actions-test
        aws-region: ${{ vars.AWS_REGION }}

    - name: "Setup: Login to Amazon ECR"
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v1
      with:
        mask-password: 'true'

    - name: "Misc: Set ENVIRONMENT variable"
      id: set_stage
      run: echo "ENVIRONMENT=pr-${{ github.event.number }}" >> $GITHUB_ENV

    - name: "Deploy: Helm"
      uses: bitovi/[email protected]
      with:
        cluster-name: ${{ vars.AWS_EKS_CLUSTER }}
        namespace: ${{ env.ENVIRONMENT }}-testing-ns
        name: test-release
        #atomic: true
        values: image.tag=${{ needs.build-docker.outputs.image_tag }}
        timeout: 60s
        helm-extra-args: --debug

І останнім додаємо нову джобу, яка буде видаляти Helm-реліз, та окремий step на видалення неймспейсу, бо сам Helm при uninstall такого не вміє, див. Add option to delete the namespace created during install. Для цього використовуємо ianbelcher/eks-kubectl-action:

...

  destroy-helm:

    name: Uninstall Helm chart
    if: |
      (contains(github.event.pull_request.labels.*.name, 'deploy') && github.event.action == 'closed') ||
      (github.event.action == 'unlabeled' && github.event.label.name == 'deploy')
    runs-on: ubuntu-latest
    timeout-minutes: 15
    environment: dev

    steps:
    
    - name: "Setup: checkout code"
      uses: actions/checkout@v3

    - name: "Setup: Configure AWS credentials"
      uses: aws-actions/configure-aws-credentials@v2
      with:
        role-to-assume: ${{ vars.AWS_IAM_ROLE }}
        role-session-name: github-actions-test
        aws-region: ${{ vars.AWS_REGION }}

    - name: "Misc: Set ENVIRONMENT variable"
      id: set_stage
      run: echo "ENVIRONMENT=pr-${{ github.event.number }}" >> $GITHUB_ENV

    - name: "Destroy: Helm"
      uses: bitovi/[email protected]
      with:
        cluster-name: ${{ vars.AWS_EKS_CLUSTER }}
        namespace: ${{ env.ENVIRONMENT }}-testing-ns
        action: uninstall
        name: test-release
        #atomic: true
        timeout: 60s
        helm-extra-args: --debug

    - name: "Destroy: namespace"
      uses: ianbelcher/eks-kubectl-action@master
      with:
        cluster_name: ${{ vars.AWS_EKS_CLUSTER }}
        args: delete ns ${{ env.ENVIRONMENT }}-testing-ns

Пушимо, створюємо Pull Request, і маємо деплой:

Перевіряємо Namespace:

$ kk get ns pr-5-testing-ns
NAME              STATUS   AGE
pr-5-testing-ns   Active   95s

$ kk -n pr-5-testing-ns get pod
NAME                               READY   STATUS             RESTARTS      AGE
test-deployment-77f6dcbd95-cg2gf   0/1     CrashLoopBackOff   3 (35s ago)   78s

Мержимо Pull Request – і видаляємо Helm release та Namespace:

Перевіряємо:

$ kk get ns pr-5-testing-ns
Error from server (NotFound): namespaces "pr-5-testing-ns" not found

Bonus: GitHub Deployments

У GitHub є нова фіча – Deployments, яка ще в Beta, але вже можна користуватись:

Ідея Deployments в тому, що для кожного GitHub Environment можна побачити список всіх деплоїв до ньго – зі статусами та комітами:

Для роботи з Deployments використаємо Action bobheadxi/deployments, який приймає input з іменем env. Якщо в env передається не існуючий в репозиторії Environment – то він буде створений.

Крім того, тут маємо набір stepsstart, finish, deactivate-env та delete-env.

Під час деплою нам треба викликати start і передати ім’я оточення, по завершенні деплою – викликати finish, щоб передати статус деплою, а при видаленні оточення – викликати delete-env.

До джоби deploy-helm у воркфлоу create-feature-env-on-pr.yml додаємо permissions і нові степи.

Степ “Misc: Create a GitHub deployment” – перед викликом “Deploy: Helm“, а степ “Misc: Update the GitHub deployment status” – після виконаня Helm install:

...
permissions:
  id-token: write
  contents: read
  deployments: write 

...
  deploy-helm:

    name: Deploy Helm chart
    if: |
      (contains(github.event.pull_request.labels.*.name, 'deploy') && github.event.action != 'closed') ||
      (github.event.action == 'labeled' && github.event.label.name == 'deploy')    
    runs-on: ubuntu-latest
    timeout-minutes: 15
    needs: build-docker
    ...

    - name: "Misc: Set ENVIRONMENT variable"
      id: set_stage
      run: echo "ENVIRONMENT=pr-${{ github.event.number }}" >> $GITHUB_ENV

    - name: "Misc: Create a GitHub deployment"
      uses: bobheadxi/deployments@v1
      id: deployment
      with:
        step: start
        token: ${{ secrets.GITHUB_TOKEN }}
        env: ${{ env.ENVIRONMENT }}

    - name: "Deploy: Helm"
      uses: bitovi/[email protected]
      with:
        cluster-name: ${{ vars.AWS_EKS_CLUSTER }}
        namespace: ${{ env.ENVIRONMENT }}-testing-ns
        name: test-release
        #atomic: true
        values: image.tag=${{ needs.build-docker.outputs.image_tag }}
        timeout: 60s
        helm-extra-args: --debug

    - name: "Misc: Update the GitHub deployment status"
      uses: bobheadxi/deployments@v1
      if: always()
      with:
        step: finish
        token: ${{ secrets.GITHUB_TOKEN }}
        status: ${{ job.status }}
        env: ${{ steps.deployment.outputs.env }}
        deployment_id: ${{ steps.deployment.outputs.deployment_id }} 
...

Пушимо, і маємо нове оточення “pr-7“:

Ну і на цьому поки все.

Наче все працює – можна додавати нові воркфлоу в репозиторій нашого бекенду.