Рассмотрим пример. Представьте, что вашему кластеру веб-серверов необходимо взаимодействовать с базой данных 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.micro, который имеет один виртуальный процессор, 1 Гбайт памяти и входит в бесплатный тариф AWS.
Обратите внимание, что одним из параметров, которые вы должны передать ресурсу aws_db_instance, является главный пароль к базе данных. Поскольку он конфиденциален, его нельзя прописывать прямо в коде в виде обычного текста! Вместо этого можно воспользоваться одним из двух способов передачи конфиденциальных данных в ресурсы Terraform.
Первый способ для работы с конфиденциальными данными заключается в применении источника Terraform, который считывает их из секретного хранилища. Например, вы можете размещать такую информацию, как пароли к базе данных, в управляемом сервисе AWS Secrets Manager, предназначенном специально для хранения чувствительных данных. Вы можете воспользоваться его графическим интерфейсом, чтобы сохранить свой пароль, и затем прочитать его в своем коде 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.
Например, вот как можно обновить пользовательские данные веб-серверов кластера, чтобы они извлекали из источника 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. Рассмотрим их по отдельности.