Terraform: инфраструктура на уровне кода — страница 22 из 65

Рассмотрим пример. Представьте, что вашему кластеру веб-серверов необходимо взаимодействовать с базой данных MySQL. Обслуживание масштабируемой, безопасной, устойчивой и высокодоступной БД требует много усилий. Вы можете позволить Amazon позаботиться об этом с помощью сервиса RDS (Relational Database Service), как показано на рис. 3.9. RDS поддерживает разнообразные базы данных, включая MySQL, PostgreSQL, SQL Server и Oracle.

Рис. 3.9. Кластер веб-серверов взаимодействует с базой данных MySQL, развернутой поверх Amazon RDS

Базу данных MySQL лучше не объявлять в том же наборе конфигурационных файлов, что и кластер веб-серверов, потому что последний обновляется значительно чаще и вам вряд ли захочется рисковать при каждом таком обновлении. Первое, что вы должны сделать, — это создать новую папку stage/data-stores/mysql и поместить в нее три основных файла Terraform (main.tf, variables.tf, outputs.tf), как показано на рис. 3.10.

Рис. 3.10. Код базы данных в папке stage/data-stores

Вслед за этим создайте ресурс базы данных в файле stage/data-stores/mysql/main.tf:

provider "aws" {

  region = "us-east-2"

}

resource "aws_db_instance" "example" {

  identifier_prefix   = "terraform-up-and-running"

  engine              = "mysql"

  allocated_storage   = 10

  instance_class      = "db.t2.micro"

  name                = "example_database"

  username            = "admin"

  # Как нам задать пароль?

  password            = "???"

}

Сразу под стандартным ресурсом provider в верхней части файла находится новый: aws_db_instance. Он создает базу данных в RDS. Параметры в этом коде настраива­ют RDS для запуска MySQL с хранилищем размером 10 Гбайт на сервере db.t2.mi­­­cro, который имеет один виртуальный процессор, 1 Гбайт памяти и входит в бесплатный тариф AWS.

Обратите внимание, что одним из параметров, которые вы должны передать ресурсу aws_db_instance, является главный пароль к базе данных. Поскольку он конфиденциа­лен, его нельзя прописывать прямо в коде в виде обычного текста! Вместо этого можно воспользоваться одним из двух способов передачи конфиденциальных данных в ресурсы Terraform.

Первый способ для работы с конфиденциальными данными заключается в применении источника Terraform, который считывает их из секретного хранилища. Например, вы можете размещать такую информацию, как пароли к базе данных, в управляемом сервисе AWS Secrets Ma­na­ger, предназначенном специально для хранения чувствительных данных. Вы можете воспользоваться его графическим интерфейсом, чтобы сохранить свой пароль, и затем прочитать его в своем коде Terraform с помощью источника данных aws_secretsmanager_secret_version:

provider "aws" {

  region = "us-east-2"

}

resource "aws_db_instance" "example" {

  identifier_prefix = "terraform-up-and-running"

  engine            = "mysql"

  allocated_storage = 10

  instance_class    = "db.t2.micro"

  name              = "example_database"

  username          = "admin"

  password =

    data.aws_secretsmanager_secret_version.db_password.secret_string

}

data "aws_secretsmanager_secret_version" "db_password" {

  secret_id = "mysql-master-password-stage"

}

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

• AWS Secrets Manager и источник данных aws_secretsmanager_secret_version (предыдущий листинг).

• AWS Systems Manager Parameter Store и источник данных aws_ssm_parameter.

• AWS Key Management Service (AWS KMS) и источник данных aws_kms_secrets.

• Google Cloud KMS и источник данных google_kms_secret.

• Azure Key Vault и источник данных azurerm_key_vault_secret.

• HashiCorp Vault и источник данных vault_generic_secret.

Второй способ работы с конфиденциальными данными — полный вынос управления ими за пределы Terraform (например, вы можете делегировать это таким диспетчерам паролей, как 1Password, LastPass или OS X Keychain) и передача их в систему в виде переменных среды. Для этого нужно объявить переменную под названием db_password в файле stage/data-stores/mysql/variables.tf:

variable "db_password" {

  description = "The password for the database"

  type        = string

}

Обратите внимание на то, что у нее нет параметра default. Это сделано намеренно. Вы не должны хранить свой пароль к базе данных или любую чувствительную информацию в открытом виде. Вместо этого значение следует брать из переменной среды.

Напоминаю, что каждой входной переменной, определенной в конфигурации Terraform (как foo), можно предоставить значение, которое берется из переменной среды (вроде TF_VAR_foo). Для входной переменной db_password нужно установить переменную среды TF_VAR_db_password. Вот как это делается в системах Linux/Unix/OS X:

$ export TF_VAR_db_password="(YOUR_DB_PASSWORD)"

$ terraform apply

(...)

Стоит отметить, что пробел перед командой export указан не случайно. Он нужен, чтобы ваши конфиденциальные данные не были сохранены на диск в истории bash40. Но есть лучший способ предотвратить случайную запись конфиденциальных данных на диск в открытом виде: хранить их в секретном хранилище, совместимом с командной строкой, таком как pass (https://www.passwordstore.org/), и безопасно считывать их оттуда в переменные среды с помощью дочерней командной оболочки:

$ export TF_VAR_db_password=$(pass database-password)

$ terraform apply

(...)


Конфиденциальные данные всегда хранятся в состоянии Terraform

Считывание конфиденциальных данных из секретного хранилища или переменных среды — хороший способ предотвратить их хранение в открытом виде внутри вашего кода. Но помните: вне зависимости от того, как вы их читаете, если они передаются в качестве аргумента в ресурс, такой как aws_db_instance, они автоматически сохраняются в состоянии Terraform в виде обычного текста.

Это известное слабое место Terraform, у которого нет эффективных решений. Поэтому будьте максимально бдительны с тем, как вы храните файлы состояния (например, всегда включайте шифрование) и кто имеет к ним доступ (скажем, ограничивайте доступ к своему бакету S3 с помощью привилегий IAM)!

Вслед за конфигурацией пароля нужно сделать так, чтобы модуль хранил свое состояние в бакете S3, который вы создали ранее в файле stage/data-stores/mysql/terraform.tfstate:

terraform {

  backend "s3" {

    # Поменяйте это на имя своего бакета!

    bucket         = "terraform-up-and-running-state"

    key            = "stage/data-stores/mysql/terraform.tfstate"

    region         = "us-east-2"

    # Замените это именем своей таблицы DynamoDB!

    dynamodb_table = "terraform-up-and-running-locks"

    encrypt        = true

  }

}

Выполните команды terraforminit и terraformapply, чтобы создать базу данных. Нужно учитывать, что на инициализацию даже небольшой базы данных в Amazon RDS может уйти около десяти минут, поэтому будьте терпеливы.

Итак, вы создали базу данных. Но как передать ее адрес и порт вашему кластеру веб-серверов? Для начала нужно добавить две выходные переменные в файл stage/data-stores/mysql/outputs.tf:

output "address" {

  value       = aws_db_instance.example.address

  description = "Connect to the database at this endpoint"

}

output "port" {

  value       = aws_db_instance.example.port

  description = "The port the database is listening on"

}

Выполните terraformapply еще раз, и в терминале должны появиться ваши выходные переменные:

$ terraform apply

(...)

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

address = tf-2016111123.cowu6mts6srx.us-east-2.rds.amazonaws.com

port = 3306

Теперь они хранятся и в состоянии Terraform для базы данных, которое находится в вашем бакете S3 в файле stage/data-stores/mysql/terraform.tfstate. Чтобы код вашего кластера веб-серверов прочитал содержимое этого файла состояния, добавьте в файл stage/services/webserver-cluster/main.tf источник данных terraform_remote_state:

data "terraform_remote_state" "db" {

  backend = "s3"

  config = {

    bucket = "(YOUR_BUCKET_NAME)"

    key    = "stage/data-stores/mysql/terraform.tfstate"

    region = "us-east-2"

  }

}

Благодаря этому источнику данных код кластера веб-серверов считывает файл состояния из тех же бакета S3 и папки, где свое состояние хранит база данных (рис. 3.11).

Рис. 3.11. База данных записывает свое состояние в бакет S3 (вверху), а кластер веб-серверов считывает его из того же бакета (внизу)

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

Все выходные переменные БД хранятся в файле состояния, и вы можете считывать их из источника данных terraform_remote_state, используя ссылку на атрибут следующего вида:

data.terraform_remote_state..outputs.

Например, вот как можно обновить пользовательские данные веб-серверов кластера, чтобы они извлекали из источника terraform_remote_state адрес и порт базы данных и возвращали их в виде HTTP-ответа:

user_data = <

#!/bin/bash

echo "Hello, World" >> index.html

echo "${data.terraform_remote_state.db.outputs.address}" >> index.html

echo "${data.terraform_remote_state.db.outputs.port}" >> index.html

nohup busybox httpd -f -p ${var.server_port} &

EOF

Чем длиннее становится скрипт в параметре user_data, тем более неряшливым выглядит ваш код. Встраивание одного языка программирования (bash) в другой (Terraform) усложняет поддержку обоих, поэтому давайте на секунду остановимся и вынесем bash-скрипт в отдельный файл. Для этого можно использовать встроенную функцию file и источник данных template_file. Рассмотрим их по отдельности.