Рубрики
Uncategorized

Тестирование в инфраструктуре как код и почему Terraform может быть не лучшим вариантом

Вам не нужен специальный инструмент для автоматического тестирования вашего кода IAC, вы можете использовать любой программирование LANG … Tagged с помощью тестирования, терраформ, облаков, DevOps.

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

Вы должны проверить свою инфраструктуру как код, потому что это код — это, вероятно, очевидно. Скорее всего, вы хотите сделать это автоматически. Здесь я поделюсь нашим подходом к тестированию IAC и о том, как он ездил

Давайте начнем с некоторой теории. Есть старая концепция тестовой пирамиды:

На диаграмме часто бывают разные имена, но концепция:

  • У вас должно быть много быстрых надежных тестов, которые часто являются модульными тестами. Важно, чтобы они выполняют в миллисекундах и не зависят от каких -либо
  • Должно быть умеренное количество интеграционных тестов. Эти тесты часто зависят от внешних служб, но не тестируют всю систему. Они намного медленнее и более хрупкие, чем модульные тесты, вероятно, выполненные в считанные секунды или минуты, и иногда могут терпеть неудачу, например, Из -за проблем сети.
  • У вас должно быть только несколько тестов всей вашей системы. Они действительно медленные и хрупкие. Иногда очень трудно убедить управление и бизнес в этом, но это реальность.

Важно отметить, что ничто не проверяет, как работает код, так же быстро, как и модульные тесты. Вы должны написать их для себя, чтобы проверить свой код. Тем не менее, есть некоторые неправильные представления о том, что такое устройство и как написать хорошие тесты. По моему мнению, вы всегда должны думать о том, чего вы хотите достичь с помощью своего кода и проверить это поведение — иногда вы протестируете одну функцию/метод, иногда что -то большее. Вы не должны вызывать внешние зависимости в ваших модульных тестах и должны писать свои тесты таким образом, чтобы не потребовалось их изменение после рефакторинга. Архитектура портов и адаптеров может помочь с этим. Если у вас есть хорошие модульные тесты для части вашего приложения, вы можете предположить, что это работает хорошо, как вы часто предполагаете, что какая -то внешняя программа работает (например, AWS CLI). Затем вам не нужно проверять каждый вариант в вашей интеграции или системных тестах.

Часто, когда мы говорим об инфраструктуре как о кодных инструментах, Terraform приходит в наши годы. Он отлично подходит для поддержания вашего состояния инфраструктуры и общения с API вашего облачного поставщика, но вы должны написать код в HCL, который не является реальным языком программирования. Это плохо? Иногда да, особенно когда дело доходит до написания модульных тестов. Давайте проверим, как мы можем проверить код Terraform и что мы можем использовать вместо этого, и провести настоящие модульные тесты!

Наш пример -тест на тестируемый код создаст ведро S3, поддерживающее соглашения о именовании:

  • Шаблон имени ведра будет <имя компании>--<Имя приложения>-<Цель ведра> (например, acme-dev-orders-pictures ) Если он не превышает 63 символа (максимум для ведра S3)
  • Если он превышает 63 символа, это будет хэш имени. Мы будем использовать подстроение SHA256.

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

Вам понадобится несколько инструментов:

  • Go (проверено с 1.15.3)
  • Terraform (протестировано с 0,14,3)
  • AWS CDK (протестирован с 1,71,0)
  • Java JDK (протестирован с OpenJDK 11.0.9)
  • Nodejs (требуется для AWS CDK, протестировано с V12.18.3)
  • Docker (протестирован с 18,09,5)
  • cdklocal ( https://github.com/localstack/aws-cdk-local , протестировано с 1.65.2)

Также требуется аккаунт AWS с надлежащими учетными данными. Код, вероятно, будет работать с другими версиями.

Терратест

Мы протестируем этот код Terraform https://github.com/devopsbox-io/example-iac-test/blob/master/terraform/s3/main.tf

terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

locals {
  requested_bucket_name = "${var.company_name}-${var.env_name}-${var.app_name}-${var.bucket_purpose}"
  bucket_name = length(local.requested_bucket_name) > 63 ? substr(sha256(local.requested_bucket_name), 0, 63) : local.requested_bucket_name
}

resource "aws_s3_bucket" "bucket" {
  bucket = local.bucket_name
}

и переменные https://github.com/devopsbox-io/example-iac-test/blob/master/terraform/s3/variables.tf

variable "aws_region" {
  type = string
}

variable "company_name" {
  type = string
}

variable "env_name" {
  type = string
}

variable "app_name" {
  type = string
}

variable "bucket_purpose" {
  type = string
}

Здесь ничего особенного, просто реализация нашего примера в Terraform.

Тесты на терратете довольно легко написать https://github.com/devopsbox-io/example-iac-test/blob/master/test/s3_module_test.go

func TestS3BucketCreated(t *testing.T) {
    t.Parallel()

    envName := strings.ToLower(random.UniqueId())
    awsRegion := "eu-west-1"

    tests := map[string]struct {
        terraformVariables map[string]interface{}
        expectedBucketName string
    }{
        "short name": {
            terraformVariables: map[string]interface{}{
                "aws_region":     awsRegion,
                "company_name":   "acme",
                "env_name":       envName,
                "app_name":       "orders",
                "bucket_purpose": "pictures",
            },
            expectedBucketName: "acme-" + envName + "-orders-pictures",
        },
        "long name": {
            terraformVariables: map[string]interface{}{
                "aws_region":     awsRegion,
                "company_name":   "acme",
                "env_name":       envName,
                "app_name":       "orders",
                "bucket_purpose": "pictures12345678901234567890123456789012345678901234567890",
            },
            expectedBucketName: sha256String("acme-" + envName + "-orders-pictures12345678901234567890123456789012345678901234567890")[:63],
        },
    }

    for name, testCase := range tests {
        // capture range variables
        name := name
        testCase := testCase
        t.Run(name, func(t *testing.T) {
            t.Parallel()

            terraformModuleDir, err := files.CopyTerraformFolderToTemp("../terraform/s3", "terratest-")
            if err != nil {
                t.Fatalf("Error while creating temp dir %v", err)
            }
            defer os.RemoveAll(terraformModuleDir)

            terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
                TerraformDir: terraformModuleDir,
                Vars:         testCase.terraformVariables,
            })

            defer terraform.Destroy(t, terraformOptions)

            terraform.InitAndApply(t, terraformOptions)

            aws.AssertS3BucketExists(t, awsRegion, testCase.expectedBucketName)
        })
    }
}

func sha256String(str string) string {
    sha256Bytes := sha256.Sum256([]byte(str))
    return hex.EncodeToString(sha256Bytes[:])
}

Я использовал паттерн, называемую управляемыми данными (или основанные на таблице) тесты (Подробнее здесь https://dave.cheney.net/2019/05/07/prefer-table-diven-tests ). Тесты выполняются параллельно, что довольно хорошо. Для этого вы должны скопировать свой модуль, используя CopeTerRaFordFortEmpetemp и переменные диапазона переназначения (две строки после переменных диапазона // комментарий).

Выполнить этот код с помощью:

cd test
go test

Здесь есть несколько проблем:

  • Это не модульные тесты. Они должны создать вашу инфраструктуру, чтобы они были медленными и хрупкими.
  • Посмотрите на этот простой алгоритм в HCL и подумайте о чем -то немного более сложном — это может быть очень трудно реализовать, читать и поддерживать.
  • Вы должны использовать Golang — это больше не проблема для нас, но это было раньше.

Вы не можете использовать Terratest утверждения с LocalStack, и есть запрос на открытый притяжение https://github.com/gruntwork-io/terratest/pull/495 Решение этой проблемы. Конечно, вы могли бы написать свои собственные утверждения, и мы сделаем это позже на другом языке, поэтому давайте пропустим это сейчас и пойдем дальше.

Проверьте терраформ с пользовательской «miniframework» на основе Spock

Мы не хотели писать тесты в Голанге, поэтому мы решили проверить, можно ли использовать какой -то другой язык. Одна из моих любимых фреймворков тестирования — Spock — он основан на JVM, и вы пишете свои тесты в Groovy. Мы решили проверить это, и оказывается, что это работает очень хорошо! Вы «только» должны написать код клея, что не так сложно сделать.

Код Terraform почти такой же, но с поддержкой LocalStack нам пришлось изменить конфигурацию поставщика AWS https://github.com/devopsbox-io/example-iac-test/blob/master/terraform/s3/main.tf

provider "aws" {
  region = var.aws_region

  access_key = var.use_localstack ? "fake_access_key" : null
  s3_force_path_style = var.use_localstack
  secret_key = var.use_localstack ? "fake_secret_key" : null
  skip_credentials_validation = var.use_localstack
  skip_metadata_api_check = var.use_localstack
  skip_requesting_account_id = var.use_localstack

  dynamic "endpoints" {
    for_each = var.use_localstack ? [1] : []
    content {
      s3 = "http://localhost:4566"
    }
  }
}

и добавить одну дополнительную переменную:

variable "use_localstack" {
  type = bool
  default = false
}

Тест выглядит так https://github.com/devopsbox-io/example-iac-test/blob/master/src/integtest/grovy/io/devopsbox/infrastructure/test/s3/s3terraformmoduletest.groovy

class S3TerraformModuleTest extends TerraformIntegrationTest {

    @Shared
    S3 s3

    def setupSpec() {
        s3 = new S3(sdkClients)
    }

    @Unroll
    def "should create s3 bucket #testCase"() {
        given:
        def terraformVariables = new S3TerraformModuleVariables(
                useLocalstack: localstack.enabled,
                awsRegion: awsRegion(),
                companyName: "acme",
                envName: environmentName(),
                appName: "orders",
                bucketPurpose: bucketPurpose,
        )

        when:
        deployTerraformModule("terraform/s3", terraformVariables)

        then:
        s3.checkBucketExists(expectedBucketName)

        cleanup:
        destroyTerraformModule("terraform/s3", terraformVariables)

        where:
        testCase     | bucketPurpose                                                | expectedBucketName
        "short name" | "pictures"                                                   | "acme-" + environmentName() + "-orders-pictures"
        "long name"  | "pictures12345678901234567890123456789012345678901234567890" | DigestUtils.sha256Hex("acme-" + environmentName() + "-orders-pictures12345678901234567890123456789012345678901234567890").substring(0, 63)
    }

    class S3TerraformModuleVariables extends TerraformVariables {
        boolean useLocalstack
        String awsRegion
        String companyName
        String envName
        String appName
        String bucketPurpose
    }
}

Код довольно хороший. Обратите внимание на класс s3terraformmodulevariables — не так уж и плох, чтобы передать входные переменные в наш стек Terraform. Одной из проблем является отсутствие поддержки параллелизма (будет поддерживаться в Spock 2.0 http://spockframework.org/spock/docs/2.0-m4/parallel_execution.html#parallel-excution ). У нас уже есть поддержка Localstack здесь — длительные тесты должны быть быстрее, потому что вам не нужно ждать реальной инфраструктуры. Тем не менее, я думаю, что вы также должны запустить свои тесты с реальным облаком — иногда LocalStack может вести себя по -разному или, возможно, не поддерживает какой -то облачный ресурс, который вам нужен.

Чтобы выполнить этот код запуск:

./gradlew integTest --tests *S3TerraformModuleTest

или с LocalStack:

./gradlew integTest --tests *S3TerraformModuleTest -Dlocalstack.enabled=true

Здесь есть проблемы? Да:

  • Все еще нет модульных тестов
  • Все еще не реальный язык программирования

Клейский код «miniframework»

Я упоминал код клея? Есть некоторые, но на самом деле не так много. Я только перечислю все файлы, опишу их, но не вставлю весь код здесь:

Это смесь Groovy и Java. Вы можете скопировать этот код, использовать его в своем проекте или даже переписать его на другой язык. Если вы думаете, что мы должны создать реальную структуру и предоставить банку — пожалуйста, дайте мне знать, мы сделаем все возможное.

Как вы можете видеть — вам не нужен специальный инструмент для проверки вашей инфраструктуры в качестве кода, только ваш любимый язык, модуль -структуру и несколько часов, чтобы написать код клея.

Тест AWS CDK с пользовательской «miniframework» на основе Spock

Мы действительно хотели написать наш код на языке программирования общего назначения и имели возможность для его проверки. После некоторых исследований мы нашли две рамки:

  • AWS CDK
  • Пулуми

Только AWS CDK поддержал Java, так что это был наш выбор, хотя мне нужно написать несколько слов о Pulumi: это действительно круто, я использовал его в одном из моих проектов, используя TypeScript, и я был впечатлен. Возвращаясь к AWS CDK — когда мы начали писать код, CDK находился на ранних этапах, и AWS очень часто менял свой API, но это совершенно другая история …

Вот код AWS CDK, для которого мы будем писать тесты https://github.com/devopsbox-io/example-iac-test/blob/master/src/main/java/io/devopsbox/infrastructure/test/s3/s3construct.java

public class S3Construct extends Construct {

    public S3Construct(Construct scope, String id, S3ConstructProps props) {
        super(scope, id);

        String bucketName = props.getBucketName();
        new Bucket(this, bucketName, BucketProps.builder()
                .removalPolicy(RemovalPolicy.DESTROY)
                .bucketName(bucketName)
                .build());
    }
}

Мы перенесли нашу логику именования ковша в другой класс https://github.com/devopsbox-io/example-iac-test/blob/master/src/main/java/io/devopsbox/infrastructure/test/s3/s3constructprops.java

public class S3ConstructProps extends ConstructProps {
    public static final int BUCKET_NAME_MAX_LENGTH = 63;

    private final String bucketPurpose;

    public S3ConstructProps(String companyName, String envName, String appName, String bucketPurpose) {
        super(companyName, envName, appName);
        this.bucketPurpose = bucketPurpose;
    }

    public String getBucketPurpose() {
        return bucketPurpose;
    }

    public String getBucketName() {
        String bucketName = getCompanyName() + "-" + getEnvName() + "-" + getAppName() + "-" + getBucketPurpose();
        if (bucketName.length() > BUCKET_NAME_MAX_LENGTH) {
            bucketName = DigestUtils.sha256Hex(bucketName).substring(0, BUCKET_NAME_MAX_LENGTH);
        }
        return bucketName;
    }
}

Существует также базовый класс для всех классов свойств конструкции с набором общих свойств https://github.com/devopsbox-io/example-iac-test/blob/master/src/main/java/io/devopsbox/infrastructure/test/constructprops.java

public class ConstructProps implements Serializable {
    private final String companyName;
    private final String envName;
    private final String appName;

    public ConstructProps(String companyName, String envName, String appName) {
        this.companyName = companyName;
        this.envName = envName;
        this.appName = appName;
    }

    public String getCompanyName() {
        return companyName;
    }

    public String getEnvName() {
        return envName;
    }

    public String getAppName() {
        return appName;
    }
}

И два стандартных класса CDK:

Тест похож на тот, который написан для Terraform https://github.com/devopsbox-io/example-iac-test/blob/master/src/integtest/grovy/io/devopsbox/infrastructure/test/s3/s3cdkconstructtest.groovy

class S3CdkConstructTest extends CdkIntegrationTest {

    @Shared
    S3 s3

    def setupSpec() {
        s3 = new S3(sdkClients)
    }

    def "should create s3 bucket"() {
        given:
        def stackId = "S3BucketConstructTest" + environmentName()
        def constructProps = new S3ConstructProps(
                "acme",
                environmentName(),
                "orders",
                "pictures"
        )

        when:
        deployCdkConstruct(stackId, S3Construct, constructProps)

        then:
        s3.checkBucketExists("acme-" + environmentName() + "-orders-pictures")

        cleanup:
        destroyCdkConstruct(stackId, S3Construct, constructProps)
    }
}

Чтобы выполнить этот код запуск:

./gradlew integTest --tests *S3CdkConstructTest

или с LocalStack:

./gradlew integTest --tests *S3CdkConstructTest -Dlocalstack.enabled=true

Мы не тестируем все случаи здесь, просто единый интеграционный тест, потому что мы, наконец, можем написать модульные тесты! Код выглядит так https://github.com/devopsbox-io/example-iac-test/blob/master/src/test/groovy/io/devopsbox/infrastructure/test/s3/s3constructpropstest.groovy

class S3ConstructPropsTest extends Specification {

    @Unroll
    def "should return s3 bucket #testCase"() {
        given:
        def props = new S3ConstructProps(
                "acme",
                "dev",
                "orders",
                bucketPurpose,
        )

        when:
        def bucketName = props.bucketName

        then:
        bucketName == expectedBucketName

        where:
        testCase     | bucketPurpose                                                | expectedBucketName
        "short name" | "pictures"                                                   | "acme-dev-orders-pictures"
        "long name"  | "pictures12345678901234567890123456789012345678901234567890" | DigestUtils.sha256Hex("acme-dev-orders-pictures12345678901234567890123456789012345678901234567890").substring(0, 63)
    }
}

Просто запустите его с ./gradlew test И это заканчивается в миллисекундах!

Наконец, мы можем написать код на реальном языке программирования по нашему выбору и создать модульные тесты. Это идеально? Конечно нет. Наши интеграционные тесты работают в другом процессе, поэтому есть некоторые недостатки. Существует поддержка для работы в том же процессе в Pulumi ( https://github.com/pulumi/pulumi/issues/3901 ), но еще не в AWS CDK ( https://github.com/aws/aws-cdk/weleps/601 ). Мы также можем улучшить наши «miniframework» и запустить тесты с использованием выбранной роли IAM — мы уже делаем это в Devopsbox, но это не включено здесь ради простоты.

Больше «Miniframework» Клейский код…

Мы должны добавить несколько файлов в нашу «miniframework», чтобы поддержать AWS CDK:

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

Возможность писать модульные тесты была одним из ключевых факторов, которые выбирают AWS CDK в качестве нашей инфраструктуры в качестве инструмента кода, и после выбора, мы обнаружили, что очень удобно писать код IAC на языке программирования по нашему выбору, иметь хорошую структуру кода, сделать Рефакторинг, используйте шаблоны дизайна, имеют отличную поддержку IDE, используйте внешние библиотеки и многое другое. Замечательно, что в настоящее время мы можем написать IAC на языке программирования общего назначения, и это все еще декларативно.

Я надеюсь, что проект Terraform-CDK ( https://github.com/hashicorp/terraform-cdk ) будет доступен в ближайшее время и поддерживается в будущем. Тогда мы сможем купить торт и съесть его тоже!

Для получения более подробной информации о платформе Devopsbox, пожалуйста, посетите https://www.devopsbox.io/

Оригинал: «https://dev.to/mraszplewicz/testing-in-infrastructure-as-code-and-why-terraform-may-not-be-the-best-option-3k2i»