Создание конвейера Terraform без ключей доступа

Основная цель этой серии постов – более подробно рассказать о создании трубопровода Terraform, который не требует хранения ключей доступа в ваших трубопроводах GitHub, GitLab или BitBucket.


Предварительные условия

  • Установка Terraform
  • Установите Terraform CLI
  • Создайте и активируйте учетную запись AWS

Первоначальная настройка

Нам потребуется ведро состояния, чтобы ваши репозитории Terraform могли читать и записывать изменения состояния при каждом выполнении, а также пользователь IAM, который может использоваться для локального запуска Terraform, пока не будут реализованы другие методы, такие как SSO или Active Directory.

Итак, первым шагом будет вход в учетную запись AWS, переход к CloudFormation и добавление следующего шаблона.

cloudformation.yml

AWSTemplateFormatVersion: 2010-09-09

Resources:
  TfBucket:
    Type: AWS::S3::Bucket
    Properties:
      VersioningConfiguration:
        Status: Enabled

  TfUser:
    Type: AWS::IAM::User
    Properties:
      Policies:

        - PolicyName: PermissionForOpenIdConnectModule
          PolicyDocument:
            Version: 2012-10-17
            Statement:

              - Effect: Allow
                Action:
                  - iam:*OpenIDConnectProvider
                Resource:
                  - Fn::Sub:
                      - 'arn:aws:iam::${AccountId}:oidc-provider/*'
                      - AccountId: !Ref AWS::AccountId

              - Effect: Allow
                Action:
                  - iam:*Role*
                Resource:
                  - Fn::Sub:
                      - 'arn:aws:iam::${AccountId}:role/identity-provider-github-assume-role'
                      - AccountId: !Ref AWS::AccountId
                  - Fn::Sub:
                      - 'arn:aws:iam::${AccountId}:role/identity-provider-gitlab-assume-role'
                      - AccountId: !Ref AWS::AccountId
                  - Fn::Sub:
                      - 'arn:aws:iam::${AccountId}:role/identity-provider-bitbucket-assume-role'
                      - AccountId: !Ref AWS::AccountId

        - PolicyName: PermissionToBucketState
          PolicyDocument:
            Version: 2012-10-17
            Statement:

              - Effect: Allow
                Action:
                  - s3:ListBucket
                Resource:
                  - !GetAtt TfBucket.Arn

              - Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:PutObject
                  - s3:DeleteObject
                Resource:
                  - Fn::Sub:
                      - "${BucketArn}/*"
                      - BucketArn: !GetAtt TfBucket.Arn

  # Generates the Access Key
  TfAccessKey:
    Type: AWS::IAM::AccessKey
    Properties:
      # Increment serial to rotate key
      Serial: 1
      Status: Active
      UserName: !Ref TfUser

  # Adds the Credentials to Secrets Manager
  TfSecret:
    Type: AWS::SecretsManager::Secret
    Properties:
      Description: Credentials for Terraform.
      Name: TERRAFORM_CREDENTIALS
      SecretString: !Sub
        - '{"AWS_ACCESS_KEY_ID":"${AWS_ACCESS_KEY_ID}","AWS_SECRET_ACCESS_KEY":"${AWS_SECRET_ACCESS_KEY}","AWS_DEFAULT_REGION":"${AWS_DEFAULT_REGION}"}'
        - AWS_ACCESS_KEY_ID: !Ref TfAccessKey
          AWS_SECRET_ACCESS_KEY: !GetAtt TfAccessKey.SecretAccessKey
          AWS_DEFAULT_REGION: !Ref "AWS::Region"

Outputs:
  BucketName:
    Description: The name of the state bucket used for Terraform State.
    Value: !Ref TfBucket

  BucketRegion:
    Description: The region of the state bucket that will be used for Terraform State.
    Value: !Ref AWS::Region

Вход в полноэкранный режим Выйти из полноэкранного режима

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


Структура проекта

Мы создадим структуру проекта, как показано ниже, и начнем с создания модуля.

├── backend.tf
├── main.tf
├── modules
│   └── openid_connect
│       ├── experiments.tf
│       ├── locals.tf
│       ├── main.tf
│       └── variables.tf
├── providers.tf
└── terraform.tfplan
Вход в полноэкранный режим Выход из полноэкранного режима

Создание модуля

Я абстрагировал это в модуль под названием openid_connect, это создаст разделение между остальным кодом и улучшит читаемость.

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

openid_connect/variables.tf

variable "github" {
  type = object({
    enabled        = bool
    workspace_name = string
    repositories   = optional(string)
    permission_statements = optional(list(
      object({
        Effect   = string
        Action   = list(string)
        Resource = string
      })
    ))
  })

  default = {
    enabled        = false
    workspace_name = ""
    permission_statements = []
  }
}

variable "gitlab" {
  type = object({
    enabled   = bool
    group_url = string
    permission_statements = optional(list(
      object({
        Effect   = string
        Action   = list(string)
        Resource = string
      })
    ))
  })

  default = {
    enabled   = false
    group_url = ""
  }
}

variable "bitbucket" {
  type = object({
    enabled          = bool
    workspace_name   = string
    workspace_uuid   = string
    repository_uuids = optional(string)
    permission_statements = optional(list(
      object({
        Effect   = string
        Action   = list(string)
        Resource = string
      })
    ))
  })

  default = {
    enabled        = false
    workspace_name = ""
    workspace_uuid = ""
  }

  validation {
    condition = (
      var.bitbucket.enabled ? (
        length(var.bitbucket.workspace_name) > 0 &&
        length(var.bitbucket.workspace_uuid) > 0
      ) : true
    )
    error_message = "Workspace name and uuid is required. These can be found from OpenId Connect under the pipeline settings."
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

openid_connect/experiments.tf

terraform {
  experiments = [module_variable_optional_attrs]
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Locals будет использоваться для обхода карты провайдеров и выбора только включенных провайдеров.

Кроме того, переменные по умолчанию будут перезаписаны пользовательскими значениями, которые вы передадите с помощью функции merge.

openid_connect/locals.tf

locals {
  providers = {
    github    = var.github,
    gitlab    = var.gitlab,
    bitbucket = var.bitbucket
  }

  default = {
    github = {
      identity_provider_url = "token.actions.githubusercontent.com"
      audience              = "sts.amazonaws.com"
      repositories          = ["*"]
      permission_statements = [{
        Effect : "Allow",
        Resource : "*",
        Action : [
          "*"
        ]
      }]
    }

    gitlab = {
      identity_provider_url = "gitlab.com"
      audience              = "https://gitlab.com"
      project_slug          = "*"
      permission_statements = [{
        Effect : "Allow",
        Resource : "*",
        Action : [
          "*"
        ]
      }]
    }

    bitbucket = {
      identity_provider_url = format("api.bitbucket.org/2.0/workspaces/%s/pipelines-config/identity/oidc", var.bitbucket.workspace_name)
      audience              = format("ari:cloud:bitbucket::workspace/%s", replace(var.bitbucket.workspace_uuid, "/[{}]/", ""))
      repository_uuids      = ["*"]
      permission_statements = [{
        Effect : "Allow",
        Resource : "*",
        Action : [
          "*"
        ]
      }]
    }
  }

  enabled_providers = { for provider_key, provider_value in local.providers : provider_key => merge(
    # Add default settings
    local.default[provider_key],

    # Override with module input options
    { for item_key, item_value in provider_value : item_key => item_value if item_value != null }
  ) if provider_value.enabled }
}
Вход в полноэкранный режим Выход из полноэкранного режима

В файле main.tf мы будем генерировать провайдеров OpenID Connect на основе хэша, возвращаемого через tls_certification к URL провайдера.

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

openid_connect/main.tf

data "tls_certificate" "oid_provider" {
  for_each = local.enabled_providers

  url = format("https://%s", each.value.identity_provider_url)
}

resource "aws_iam_openid_connect_provider" "oid_provider" {
  for_each = local.enabled_providers

  url = format("https://%s", each.value.identity_provider_url)

  client_id_list = [
    each.value.audience
  ]

  thumbprint_list = [
  data.tls_certificate.oid_provider[each.key].certificates.0.sha1_fingerprint
  ]
}

data "aws_iam_policy_document" "assuming_role" {
  for_each = local.enabled_providers


  statement {
    actions = ["sts:AssumeRoleWithWebIdentity"]

    // Allow federated access with the oid provider's arn
    principals {
      type = "Federated"
      identifiers = [
        aws_iam_openid_connect_provider.oid_provider[each.key].arn
      ]
    }

    // Allow access when audience is matching the audience value
    condition {
      test     = "StringLike"
      variable = format("%s:aud", each.value.identity_provider_url)

      values = [
        each.value.audience
      ]
    }


    condition {
      test     = "StringLike"
      variable = format("%s:sub", each.value.identity_provider_url)

      values = concat(
        # Github
        each.key == "github" ? formatlist("repo:%s/%s:*", each.value.workspace_name, each.value.repositories) : [],

        # Bitbucket
        each.key == "bitbucket" ? formatlist("%s:*", each.value.repository_uuids) : [],

        compact([
          # Gitlab
          each.key == "gitlab" ? format(
            "*:%s:*:*:*:*",
            join("/",
              slice(
                split("/", replace(each.value.group_url, "https://", "")),
                1,
                length(
                  split("/", replace(each.value.group_url, "https://", ""))
                )
              )
            )
          ) : null,
        ])
      )

    }
  }
}

resource "aws_iam_role" "assuming_role" {
  for_each = local.enabled_providers

  name               = format("identity-provider-%s-assume-role", each.key)
  assume_role_policy = data.aws_iam_policy_document.assuming_role[each.key].json

  inline_policy {
    name = "identity-provider-permissions"

    policy = jsonencode({
      Version   = "2012-10-17",
      Statement = each.value.permission_statements
    })
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима


Добавление модуля

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

backend.tf

terraform {
  backend "s3" {
    region  = "eu-west-1"
    bucket  = "test-tfbucket-bgbo1k17nnkd"
    key     = "local.tfstate"
  }
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Провайдер определяет, какого провайдера использовать (например, AWS, GCP и т.д.) и применяет теги по умолчанию для всех ресурсов.

provider.tf

provider "aws" {
  region = "eu-west-1"

  default_tags {
    tags = {
      test-pipeline = true
    }
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь нам нужно создать модуль для импорта модуля, а также разрешения, которыми будет обладать роль, поскольку мы используем terraform, нам понадобятся разрешения s3, однако это можно ограничить с помощью утверждения и передачи bucket arn в качестве ресурса.

main.tf

locals {
  permission_statements = [
    {
      Effect : "Allow",
      Action : [
        "sns:*",
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      Resource : "*"
    }
  ]
}

module "openid_connect" {
  source = "./modules/openid_connect"

  github = {
    enabled               = true
    workspace_name        = "lewisstevens1"
    permission_statements = local.permission_statements
  }

  gitlab = {
    enabled               = true
    group_url             = "https://www.gitlab.com/lewisstevens1"
    permission_statements = local.permission_statements
  }

  bitbucket = {
    enabled               = true
    workspace_name        = "lewisstevens1"
    workspace_uuid        = "{fg5125adw3-ab7e-49528-af15-c61ed55112f}"
    permission_statements = local.permission_statements
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима


Применение Terraform

Чтобы применить Terraform, нам сначала нужно экспортировать учетные данные.

Для этого вы можете перейти в менеджер секретов и получить секретное значение для секрета “TERRAFORM_CREDENTIALS”.

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


Настройка конвейеров

Теперь, когда в учетной записи AWS настроены провайдеры OpenId Connect, нам нужно настроить трубопроводы.

Они имеют небольшие различия в каждом провайдере, и для каждого из этих шаблонов требуется переменная AWS_DEFAULT_REGION и ACCOUNT_ID.

Токен BitBucket – BITBUCKET_STEP_OIDC_TOKEN и требует, чтобы oidc был установлен в true для экспорта этой переменной, GitLab же автоматически экспортирует CI_JOB_JWT_V2.

Bitbucket

bitbucket-pipelines.yml

# This is a basic image with just terraform and aws cli installed onto it.
image: lewisstevens1/amazon-linux-terraform

aws-login: &aws-login |-
  STS=($( 
    aws sts assume-role-with-web-identity 
      --role-session-name terraform-execution 
      --role-arn arn:aws:iam::$ACCOUNT_ID:role/identity_provider_bitbucket_assume_role 
      --web-identity-token $BITBUCKET_STEP_OIDC_TOKEN 
      --query "Credentials.[AccessKeyId,SecretAccessKey,SessionToken]" 
      --output text 
  ));

  export AWS_ACCESS_KEY_ID=${STS[0]};
  export AWS_SECRET_ACCESS_KEY=${STS[1]};
  export AWS_SESSION_TOKEN=${STS[2]};
  export AWS_DEFAULT_REGION=$AWS_DEFAULT_REGION;

pipelines:
  branches:
    master:
      - step:
          name: plan-terraform
          oidc: true
          script:
            - *aws-login
            - terraform init && terraform plan

      - step:
          name: apply-terraform
          trigger: 'manual'
          oidc: true
          script:
            - *aws-login
            - terraform init && terraform plan -out terraform.tfplan
            - terraform apply terraform.tfplan
Вход в полноэкранный режим Выйти из полноэкранного режима

Github

.github/workflows/ci.yml

name: Terraform Pipeline

permissions:
  id-token: write
  contents: read

on:
  push:
    branches: [ master ]

jobs:
  github-pipeline:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          role-to-assume: arn:aws:iam::${{ secrets.ACCOUNT_ID }}:role/identity_provider_github_assume_role
          aws-region: $AWS_DEFAULT_REGION

      - name: plan-terraform
        working-directory: ./
        run: terraform init && terraform plan -out terraform.tfplan

      - name: apply-terraform
        working-directory: ./
        run: terraform apply terraform.tfplan
Войти в полноэкранный режим Выйти из полноэкранного режима

Gitlab

.gitlab-ci.yml

# This is a basic image with just terraform and aws cli installed onto it.
image: lewisstevens1/amazon-linux-terraform

stages:
  - test
  - deploy

.aws-login: &aws-login
  - STS=($(
      aws sts assume-role-with-web-identity
      --role-session-name terraform-execution
      --role-arn arn:aws:iam::$ACCOUNT_ID:role/identity_provider_gitlab_assume_role
      --web-identity-token $CI_JOB_JWT_V2
      --query "Credentials.[AccessKeyId,SecretAccessKey,SessionToken]"
      --output text
    ));

  - |
    export AWS_ACCESS_KEY_ID=${STS[0]};
    export AWS_SECRET_ACCESS_KEY=${STS[1]};
    export AWS_SESSION_TOKEN=${STS[2]};
    export AWS_DEFAULT_REGION=$AWS_DEFAULT_REGION;

plan-terraform:
  stage: test
  script:
    - *aws-login
    - terraform init && terraform plan

apply-terraform:
  stage: deploy
  when: manual
  script:
    - *aws-login
    - terraform init && terraform plan -out terraform.tfplan
    - terraform apply terraform.tfplan
Войти в полноэкранный режим Выход из полноэкранного режима


Заключение

Теперь у вас остался трубопровод, в который вы можете добавить свои трубопроводы Terraform, и каждый коммит на master будет вызывать Plan и Apply Terraform.

В следующей статье мы обсудим способы улучшения настройки конвейера.

Репозиторий:
https://github.com/lewisstevens1/terraform_openid_connect

Оцените статью
Procodings.ru
Добавить комментарий