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

Когда вы настраиваете новую рабочую область в Terraform Cloud, вы можете выбрать репозиторий, каталог в вашем репозитории, где находится ваша конфигурация Terraform, а также ветку git, из которой рабочая область должна создавать ресурсы. Рабочая область может быть подключена только к одному репозиторию и ветке git. Если вы хотите создать несколько экземпляров своей инфраструктуры, вам необходимо создать несколько рабочих областей в Terraform Cloud. Это все хорошо, но есть ли способ автоматизировать создание новых эфемерных рабочих областей для веток разработки, которые вы поддерживаете только на короткое время? Для этого вам нужно использовать Terraform Cloud API. Интерфейс командной строки отсутствует, что немного удивительно, поскольку у HashiCorp есть множество различных инструментов командной строки для различных продуктов. Компания HashiCorp любезно разработала клиент Go для Terraform Cloud/Enterprise, поэтому мы не вынуждены выполнять всю работу самостоятельно.

Давайте использовать клиент Go для разработки чего-то полезного! Одна вещь, которую я тщетно искал, — это способ работы с Terraform Cloud с использованием GitHub Actions. Я мечтаю иметь возможность автоматически создавать рабочие пространства Terraform Cloud для веток разработки, которые также автоматически удаляются, когда я сливаю свои изменения в основную ветку. Итак, в этом посте я расскажу, как я это сделал!

Это сообщение не представляет Terraform Cloud. Если вы хотите использовать различные действия GitHub, которые я представляю в этом посте, вам потребуется работающая организация Terraform Cloud и вы должны быть знакомы с тем, как создать токен для аутентификации в Terraform Cloud. Если вы хотите узнать больше и начать работу, я рекомендую вам ознакомиться с документами

Написание пользовательских действий в Go

Недавно я прошел сертификацию GitHub Actions и в рамках этого обучения научился создавать собственные настраиваемые действия. Я использовал эти навыки для создания ряда действий, выполняющих небольшие задачи в Terraform Cloud. Все мои действия можно посмотреть здесь:

  • Настройка переменных среды для Terraform Cloud: это действие требует ввода ряда данных, указывающих токен API для Terraform Cloud, название организации, которой вы владеете, название проекта (коллекции рабочих областей) и имя рабочее пространство, которое вы хотите использовать. Затем он делает эти различные настройки доступными в качестве переменных среды для следующих действий.
  • Создать новую рабочую область: это действие создает новую рабочую область. Версия 1 этого действия (текущая версия) очень категорична в отношении того, как должно выглядеть рабочее пространство.
  • Удалить существующую рабочую область: это действие удаляет существующую рабочую область. Сначала он запускает удаление, чтобы удалить всю инфраструктуру, созданную Terraform, а затем приступает к удалению самой рабочей области.
  • Применить набор переменных к рабочему пространству: это действие применяет существующий набор переменных к рабочему пространству. Набор переменных — это набор переменных, которые вы хотите повторно использовать во многих рабочих областях. Хорошие кандидаты для наборов переменных включают учетные данные для облачных провайдеров или любые другие учетные данные для различных платформ.
  • Начать новый запуск в рабочей области: это действие запускает планирование и применение в выбранной рабочей области.

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

Прежде всего, если вы хотите создать пользовательское действие, вы можете выбрать один из трех вариантов:

  • Действие JavaScript: напишите действие на JavaScript.
  • Действие Docker: напишите действие на любом языке или платформе, о которых только можно мечтать, и упакуйте его в виде образа Docker.
  • Составное действие: напишите действие, состоящее из нескольких шагов, выполняющих команды оболочки или сценарии.

Поскольку у HashiCorp есть клиент Go для Terraform Cloud, я естественно решил написать свой код действия на Go, таким образом выбрав тип действия Docker action.

Метаданные для действия

Метаданные для пользовательского действия указывают такие вещи, как его имя, описание, какие входные данные оно принимает, какие выходные данные оно производит, а также свойства того, из чего состоит действие. Эти данные настраиваются в файле с именем action.yaml (или action.yml). Для моего пользовательского действия файл action.yaml выглядит так:

name: Create Terraform Cloud workspace
author: Mattias Fjellström (mattias.fjellstrom [at] gmail.com)
description: Create a new workspace in Terraform Cloud

inputs:
  organization:
    description: Organization name
  project:
    description: Project name
  workspace:
    description: Desired workspace name
  repository:
    description: GitHub repository name
    default: ${{ github.repository }}
  branch:
    description: Git branch name to trigger runs from
  directory:
    description: Repository directory name containing Terraform configuration

runs:
  using: docker
  image: Dockerfile
  args:
    - -organization
    - ${{ inputs.organization }}
    - -project
    - ${{ inputs.project }}
    - -workspace
    - ${{ inputs.workspace }}
    - -repository
    - ${{ inputs.repository }}
    - -working_directory
    - ${{ inputs.directory }}
    - -branch
    - ${{ inputs.branch }}

Части name, description и author говорят сами за себя. Раздел inputs определяет шесть параметров:

  • organization и project используются для обозначения того, где в Terraform Cloud следует разместить это рабочее пространство. Рабочие области в Terraform Cloud сгруппированы в проекты, и все проекты являются частью организации. Обычно у вас есть одна организация, несколько проектов и еще больше рабочих пространств.
  • workspace используется для настройки желаемого имени рабочей области, которая будет создана.
  • repository, branch и directory используются для определения того, где находится конфигурация Terraform.

Последний раздел runs. Здесь я настраиваю это действие using: docker и предоставляю путь к моему Dockerfile (который, кстати, просто Dockerfile), и я предоставляю свои входные данные в качестве аргументов для своего образа Docker.

Докеризация моего действия

Dockerfile для моего действия предназначено для простого приложения Go без каких-либо причудливых функций:

FROM golang:1.20.2-alpine
WORKDIR /app
COPY ./ ./
RUN go build -o /bin/app main.go
ENTRYPOINT ["app"]

Код Go

Суть самого действия находится в main.go. Я пройдусь по соответствующим частям кода по частям. Для разбора входных аргументов я использую пакет flag:

var organizationName string
var projectName string
var workspaceName string
var repositoryName string
var workingDirectory string
var branchName string

func init() {
    flag.StringVar(&organizationName, "organization", "", "Organization name")
    flag.StringVar(&projectName, "project", "", "Project name")
    flag.StringVar(&workspaceName, "workspace", "", "Desired workspace name")
    flag.StringVar(&repositoryName, "repository", "", "Git repository")
    flag.StringVar(&workingDirectory, "working_directory", "", "Directory containing Terraform configuration")
    flag.StringVar(&branchName, "branch", "", "Git branch name")
}

func main() {
    flag.Parse()

    // ...
}

Я определяю различные флаги в функции init(), которая запускается перед моей функцией main().

Первым шагом функции main() является анализ флагов для получения любых предоставленных значений (если они есть).

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

Пример названия организации выглядит так:

if organizationName == "" {
    log.Println("No organization name provided, will fall back to environment variable")

    _, ok := os.LookupEnv(ENV_TERRAFORM_CLOUD_ORGANIZATION)
    if !ok {
        log.Fatal("Organization name must be provided as input or as environment variable")
    }

    organizationName = os.Getenv(ENV_TERRAFORM_CLOUD_ORGANIZATION)
    log.Println("Organization name read from environment variable")
}

Фактическое имя переменной окружения хранится как const с именем ENV_TERRAFORM_CLOUD_ORGANIZATION. Для чтения переменных окружения я использую пакет os.

После анализа входных данных я ищу токен Terraform Cloud API, который мне нужно установить в качестве переменной среды, и если я нахожу его, я продолжаю инициализировать клиент Terraform Cloud Go:

token, ok := os.LookupEnv(ENV_TERRAFORM_CLOUD_TOKEN)
if !ok || token == "" {
    log.Fatalf("%s environment variable must be set with a valid token", ENV_TERRAFORM_CLOUD_TOKEN)
}

config := &tfe.Config{
    Token:             token,
    RetryServerErrors: true,
}

client, err := tfe.NewClient(config)
if err != nil {
    log.Fatal(err)
}

Я импортировал клиент Go как import tfe "github.com/hashicorp/go-tfe" и присвоил ему псевдоним tfe.

Далее есть две части, где я ищу разные вещи:

  • Мне нужно найти правильный проект для использования. Я должен получить доступ к идентификатору проекта, а не к его имени. Клиент Go не позволяет мне искать проект по имени, поэтому я должен перечислить все проекты и выбрать тот, у которого совпадает имя. Это немного утомительно, и я не буду показывать код для этого здесь. Подробности смотрите в репозитории GitHub.
  • Мне также нужно найти правильное подключение к GitHub. Это требование для моего действия, должна быть установка приложения GitHub, соединяющая мою организацию Terraform Cloud с моей организацией GitHub. Будущим улучшением этого действия будет разрешение других git-соединений. Однако, как и в случае с проектами, я должен перечислить доступные приложения GitHub и найти то, которое предназначено для той же организации GitHub, которая в данный момент выполняет действие. Я не буду приводить сюда этот код, но подробности доступны в репозитории GitHub.

Со всем этим последняя часть кода создает рабочее пространство:

_, err = client.Workspaces.Create(ctx, organizationName, tfe.WorkspaceCreateOptions{
    Type:             "workspaces",
    Name:             tfe.String(workspaceName),
    AutoApply:        tfe.Bool(true),
    WorkingDirectory: tfe.String(workingDirectory),
    VCSRepo: &tfe.VCSRepoOptions{
        Branch:            tfe.String(branchName),
        Identifier:        tfe.String(repositoryName),
        GHAInstallationID: gitHubApplication.ID,
    },
    Project: project,
})
if err != nil {
    log.Fatal(err)
}

Я настраиваю рабочую область, используя входные аргументы и project и gitHubApplication, которые я искал и, надеюсь, нашел. Я установил AutoApply на true, потому что хочу, чтобы инфраструктура создавалась автоматически без ручного утверждения.

Это был краткий обзор кода, полный исходный код доступен на GitHub.

Написание рабочего процесса GitHub, который создает и удаляет рабочие области Terraform Cloud.

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

У меня возникла идея, что я хочу создать рабочий процесс GitHub, в котором, если я создам новый запрос на включение, новое рабочее пространство Terraform Cloud будет создано автоматически, и будет запущен начальный запуск. Затем, когда запрос на вытягивание закрывается, рабочая область и соответствующая инфраструктура должны быть удалены.

Рабочий процесс, который делает именно это, может выглядеть следующим образом:

name: Sample Terraform Cloud administration for pull requests

on:
  # trigger when pull requests are opened or closed
  pull_request:
    types:
      - opened
      - closed

# set some convenience environment variables
env:
  ORGANIZATION: my-terraform-cloud-organization
  PROJECT: my-terraform-cloud-project

jobs:

  # job for creating a workspace for new pull requests
  create-workspace:
    if: ${{ github.event.action == 'opened' }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      # set up environment variables for terraform cloud
      - uses: mattias-fjellstrom/tfc-setup@v1
        with:
          token: ${{ secrets.TERRAFORM_CLOUD_TOKEN }}
          organization: ${{ env.ORGANIZATION }}
          project: ${{ env.PROJECT }}
          workspace: my-application-${{ github.head_ref }}
      # create the workspace (the action I went through above!)
      - uses: mattias-fjellstrom/tfc-create-workspace@v1
        with:
          directory: terraform
          branch: ${{ github.head_ref }}
      # apply a variable set with azure credentials to the workspace
      - uses: mattias-fjellstrom/tfc-apply-variable-set@v1
        with:
          variable_set: azure-credentials
      # trigger an initial run
      - uses: mattias-fjellstrom/tfc-start-run@v1

  # job for deleting a workspace for closed pull requests
  delete-workspace:
    if: ${{ github.event.action == 'closed' }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      # set up environment variables for terraform cloud
      - uses: mattias-fjellstrom/tfc-setup@v1
        with:
          token: ${{ secrets.TERRAFORM_CLOUD_TOKEN }}
          organization: ${{ env.ORGANIZATION }}
          project: ${{ env.PROJECT }}
          workspace: my-application-${{ github.head_ref }}
      # delete infrastructure and terraform cloud workspace
      - uses: mattias-fjellstrom/tfc-delete-workspace@v1

Я добавил несколько комментариев к рабочему процессу, чтобы объяснить, что делают различные действия. Несколько деталей, которые я хочу выделить:

  • Я сохранил токен API для Terraform Cloud в секрете GitHub Actions с именем TERRAFORM_CLOUD_TOKEN. В настоящее время вам необходимо предоставить токен для действия mattias-fjellstrom/tfc-setup@v1, чтобы другие действия работали. Это похоже на то, как работает действие azure/login@v1.
  • Я установил имя рабочей области my-application-${{ github.head_ref }}. Если я создам новую ветку с именем feature-1 и открою запрос на слияние этой ветки с моей основной веткой, я получу новую рабочую область Terraform Cloud с именем my-application-feature-1.
  • В действии mattias-fjellstrom/tfc-create-workspace@v1 указываю directory: terraform. Это потому, что в моем воображаемом репозитории я поместил все свои файлы Terraform (.tf) в каталог с именем terraform. Если бы я не указывал значение для каталога, вместо него использовался бы корневой каталог моего репозитория.
  • В действии mattias-fjellstrom/tfc-apply-variable-set@v1 я указываю, что набор переменных с именем azure-credentials должен применяться к моей рабочей области. Этот набор переменных должен существовать ранее, он не создается автоматически этим действием.
  • Важно помнить, что действие mattias-fjellstrom/tfc-delete-workspace@v1 потенциально может занять некоторое время. В качестве первого шага он начнет уничтожение, удалив всю созданную им инфраструктуру. В зависимости от размера этой инфраструктуры удаление займет соответствующее время.

В целом, я доволен состоянием этих действий прямо сейчас. Если мне нужны дополнительные функции, я могу добавить их позже. Однако больше всего я надеюсь, что HashiCorp сделает одну (или обе) из следующих вещей:

  1. Создание интерфейса командной строки для Terraform Cloud
  2. Создайте официальные действия GitHub для администрирования Terraform Cloud.

Посмотрим, что принесет будущее!

Эта запись изначально была опубликована в моем блоге mattias.engineer