
Как развернуть временные рамки на AWS с Terraform (пошаговый гид)
17 июля 2025 г.Временной масштабявляется расширением PostgreSQL для высокопроизводительной аналитики в реальном времени на временных рядах и данных о событиях.
Это в основномобычайПостгрес.
Важный:Этот пост используетТерраформсоздать и уничтожить ресурсы на аккаунте AWS. Убедитесь, что вы уничтожаете ресурсы, как только и до тех пор, пока они вам больше не нужны. В противном случае вас будет взиматься больше, чем вы можете.
Как настроить временные рамки на AWS?
К сожалению, вы не можете использовать на момент написания этой статьи (июль 2025 г.),Управляемый сервис AWS RDSПолем НиAWS RDS Custom ServiceПолем
Подписаться на размещенные услуги временного масштаба
Если вы хотите использовать TimeScaledB, у вас есть некоторые варианты от компаний, которые предлагают его в качестве хостинга.
- Tigerdata Cloud
- Aiven.io
- ScaleGrid
- и еще
Установите его вручную на свою учетную запись AWS
Это требует, чтобы вы были более компетентны с точки зрения DevOps, но это дает вам больше гибкости.
Это вариант, с которым я собираюсь пойти в этом сообщении в блоге.
Вот архитектурная схема для демонстрационного проекта, который я разработаю, чтобы продемонстрировать, как я могу настроить временные рамки на AWS.
- Мне понадобится доступ к машине EC2, чтобы установить сервер TimeScaledB. Вот почему доступ к SSH на порту
22
Полем Это самый простой способ установить программное обеспечение на машине EC2. В последующих сообщениях в блоге я представлю другие способы установления стороннего программного обеспечения, такого как TimeScaledB, не требуя доступа SSH. - Мне понадобится доступ к серверу базы данных через
psql
с удаленной машины. Вот почему мне нужно будет открыть порт5432
Полем Однако, опять же, если бы это была реальная производственная среда, это сделало бы его более безопасным, если бы прямой доступ к базе данных был возможен только из VPC, а не из общедоступного Интернета. Это еще одна тема для другого сообщения в блоге.
TimeScaledB на моей учетной записи AWS
Настройка проекта
Исходный код здесь:
Сторонние инструменты
Это инструменты и их версия на момент написания: (кстати, я используюasdfКак мой сторонний менеджер версий инструментов)
- Терраформ, версия 1.12.2
- Диренв, версия 2.35.0
Git - не обходитесь без
Я не ухожу без git. Я также создаю.gitignore
файл. Изначально у меня есть следующие папки и файлы, игнорируемые:
.env
.env.*
.envrc
*.pem
*.pem.pub
terraform/.terraform
*.tfstate
*.tfstate.backup
Папка для файлов терраформ
Мне нравится создавать папкуterraform
и положить все внутрь
Итак, все файлы, связанные с терраформи, будут внутри этогоterraform
субэкуптор.
Кроме того, когда я расскажу вам об командах Shell, которые я выполняю, какterraform validate
, они будут выполнены изнутри этого подразделения, что я укажу сterraform >
Префикс в начале.
Основная настройка терраформ
Аmain.tf
Файл объявляет версию Terraform и AWS -провайдера, с которой я буду работать.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "6.0.0"
}
}
required_version = "~>1.12.0"
}
Бэкэнд, чтобы спасти штат Terraform
Я оставлю бэкэнд по умолчанию (local
) Однако вы можете использовать другиеБэкэндыв ваших собственных проектах.
Я определяюБэкэнд внутри файлаbackend.tf
terraform {
backend "local" {
path = "terraform.tfstate"
}
}
Инициализировать
Сmain.tf
иbackend.tf
на месте я уволяюterraform init
Полем
terraform > terraform init
AWS Регион
Я буду использовать Европу/Франкфурт, которыйeu-central-1
Полем Я установлю его как значение по умолчанию для переменнойregion
внутриterraform/variables.tf
(ссылка на файл здесь)Полем Но так как это объявлено как входная переменная, вы можете передать то, что вам нужно.
AWS -провайдер
Я настроил конфигурацию поставщика AWS следующим образом (внутри файлаproviders.tf
):
# Without +alias+ this is the default +aws+ provider
#
provider "aws" {
region = var.region
default_tags {
tags = {
project = var.project
terraform = "1"
environment = var.environment
tf_repo = var.repository_url
tf_folder = "terraform/${var.environment}"
}
}
}
Это вводит следующие три входных переменных:
project
environment
repository_url
что я должен объявить внутриvariables.tf
файл. Я также установлю некоторые значения по умолчанию. Установите свои собственные значения в соответствии с вашими настройками:
variable "project" {
description = "The name of the project."
type = string
default = "setting_up_timescaledb_on_aws"
}
variable "environment" {
description = "The environment for the deployment (e.g., development, staging, production)."
type = string
default = "development"
}
variable "repository_url" {
description = "The GitHub repository URL for the project."
type = string
default = "https://github.com/pmatsinopoulos/setting_up_timescaledb_on_aws"
}
VPC
Я буду использовать VPC по умолчанию в выбранном регионе. Всегда есть VPC по умолчанию. Он имеет множество IP -адресов (172,31.0.0/16 - 65 536 адресов).
Поскольку VPC уже существует, и я не буду его создавать, мне просто нужно создать ссылку на источник данных. Я положу это вdata.tf
файл:
data "aws_vpc" "timescaledb_vpc" {
id = var.vpc_id
}
Я использую входную переменную для ссылки на VPC с помощью его идентификатора. Что означает, что мне нужно определение входной переменной внутриvariables.tf
файл:
variable "vpc_id" {
description = "The ID of the VPC where the EC2 instance will be launched in."
type = string
}
VPC - подсеть
VPC по умолчанию в каждом регионе поступает с 3 подсети по умолчанию, каждая из которых создана в одной из зон доступности:
eu-central-1a
eu-central-1b
eu-central-1c
Я буду использовать подсеть, развернутую вeu-central-1a
Полем Я передам его идентификатор через переменную
variable "subnet_id" {
description = "The ID of the subnet where the EC2 instance will be launched in."
type = string
}
Я объявляю источник данных для подсети внутриdata.tf
Файл, в котором я объявляю ресурсы, которые я ссылаюсь, а не создаю.
data "aws_subnet" "timescaledb_subnet" {
id = var.subnet_id
}
SSH Access Group Group
Мне понадобятся две группы безопасности, одна, чтобы разрешить входящий SSH -трафик в порту22
Полем И еще один для Postgres Traffic на порту5432
Полем
Вот как я настроил SSH One. Я пишу его конфигурацию внутри файлаsecurity_groups.tf
Полем
resource "aws_security_group" "ssh" {
name = "${var.project}-${var.environment}-security-group-ssh"
description = "Allow SSH traffic from anywhere to anywhere"
vpc_id = data.aws_vpc.timescaledb_vpc.id
ingress {
description = "SSH from anywhere"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
"Name" = "${var.project}-${var.environment}-security-group-ssh"
}
}
Это открывает входящий трафик с любого IP -адреса по порту22
Полем
Postgres Access Group Group
В том же файле я создаю еще одну группу безопасности, которая позволит мне отправить Postgres Traffer через порт5432
Полем
resource "aws_security_group" "postgres" {
name = "${var.project}-${var.environment}-security-group-postgres"
description = "Allow postgres traffic from anywhere"
vpc_id = data.aws_vpc.timescaledb_vpc.id
ingress {
description = "postgres from anywhere"
from_port = 5432
to_port = 5432
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
"Name" = "${var.project}-${var.environment}-security-group-postgres"
}
}
SSH -ключевой пара
Мне нужно будет создать пару клавиш SSH (Private-Public) и загрузить публичную часть в пары клавиш AWS.
Я используюssh-keygen
Чтобы создать пару ключей. Но вы можете использовать любой описанный методздесьПолем Пожалуйста, обратите внимание, что экземпляр EC2, который я создам для установки сервера TimeScaledB, будет сервером Linux/Ubuntu. Следовательно, я следую инструкциям, чтобы создать пару клавиш SSH, которая совместима с Linux/Ubuntu.
Вот как я генерирую ключ на своем клиенте Linux:
ssh-keygen -m PEM -f timescaledb.pem
Обратите внимание, что когда меня просят провести фразу, я оставляю это пустым.
Это генерирует два файла локально:
timescaledb.pem
timescaledb.pem.pub
Важный:А.gitignore
Файл должен включать запись*.pem
Чтобы убедиться, что частная часть ключа не зарегистрирована.
Важный:Я использую следующую команду, чтобы изменить режим доступа к файлу закрытого ключа. Это убедится, что файл будет только читабелен мной.
terraform > chmod 400 "timescaledb.pem"
Теперь мне нужно объявить ресурс, чтобы открытый ключ загружен в пары клавиш AWS. Я делаю это внутри файлаec2_key_pair.tf
# This imports the public part of an OpenSSH key.
# The key has been generated using `ssh-keygen` in PEM format.
# Example:
#
# ssh-keygen -m PEM
#
# The key has been generated without a passphrase.
#
resource "aws_key_pair" "timescaledb" {
key_name = "${var.project}-${var.environment}-timescaledb"
public_key = file("${path.module}/timescaledb.pem.pub")
tags = {
"Name" = "${var.project}-${var.environment}-timescaledb"
}
}
Экземпляр EC2
Теперь я готов код для ресурса экземпляра EC2, который я буду использовать для запуска моего сервера TimeScaledB.
Я пишу это внутри файлаec2_instance.tf
Полем
data "aws_ami" "timescaledb" {
most_recent = true
filter {
name = "block-device-mapping.delete-on-termination"
values = ["true"]
}
filter {
name = "is-public"
values = ["true"]
}
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server*"]
}
filter {
name = "root-device-type"
values = ["ebs"]
}
owners = ["099720109477"] # Canonical
}
resource "aws_instance" "timescaledb" {
ami = data.aws_ami.timescaledb.id
instance_type = var.timescaledb_server_instance_type
availability_zone = "${var.region}${var.timescaledb_server_availability_zone}"
subnet_id = var.subnet_id
vpc_security_group_ids = [
data.aws_security_group.timescaledb_vpc_default_security_group.id,
aws_security_group.ssh.id,
aws_security_group.postgres.id,
]
key_name = aws_key_pair.timescaledb.key_name
associate_public_ip_address = true
tags = {
"Name" = "${var.project}-${var.environment}-timescaledb"
}
}
output "ec2_timescaledb_public_ip" {
value = aws_instance.timescaledb.public_ip
description = "The public IP address of the EC2 Timescale DB Machine"
}
output "ssh_connect" {
value = "ssh -i ${path.module}/timescaledb.pem -o IdentitiesOnly=yes ubuntu@${aws_instance.timescaledb.public_ip}"
description = "SSH command to connect to the Timescale DB EC2 instance"
}
Аdata “aws_ami” “timescaledb” { … }
Блок используется для выбора AMI, который будет использоваться для запуска экземпляра EC2, описанного в следующемresource “aws_instance” “timescaledb” { … }
Вы можете увидеть, какresource
Блок ссылается наdata
блокировать:
...
ami = data.aws_ami.timescaledb.id
...
Внутриresource
Блок, я устанавливаю самые минимальные свойства, чтобы я мог запустить экземпляр EC2, как я хочу.
Выходы:
Я также удостоверяюсь, что я экспортирую два выхода:
ec2_timescaledb_public_ip
: Публичный IP машины EC2.ssh_connect
: Команда для использования локально, если я хочу SSH для машины EC2.
Ценностиterraform
Входные переменные
Большая частьterraform
Команды, которые мне понадобятся в этом проекте, потребуют, чтобы я имел метод передачи значений в входные переменные, которые у них нет значения по умолчанию.
Есть много способов передать значения в входные переменные терраформПолем Для этой конкретной демонстрации я предпочитаю методпеременные средыПолем
Итак, я уверен, что терминальная оболочка, которую я использую для выполненияterraform
Команды устанавливаются с правильными значениями для переменных среды, которыеterraform
потребности.
Инструмент, который помогает мне сделать это, этоДиренвПолем Я создалterraform/.envrc
Файл со следующим содержанием:
ВидетьTerraform/.envrc.sampleи создайте свой собственныйterraform/.envrc
Полем
export AWS_PROFILE=...<put here the name of the AWS profile you have configured>...
export TF_VAR_subnet_id=...<put here the sunet id from your VPC>...
export TF_VAR_vpc_id=...<put here the id of your VPC>...
Обратите внимание, чтоterraform/.envrc
Игнорируется git и не зарегистрирован. Это должно избежать проверки конфиденциальной секретной информации. У меня есть файлterraform/.envrc.sample
который зарегистрирован и является шаблоном, чтобы рассказать читателю, как фактическоеterraform/.envrc
Файл должен быть структурирован.
AWS_PROFILE
Аaws
Поставщик нужен способ подключиться к моей учетной записи AWS. Один из способов, которым мне очень нравится использовать профили AWS CLI. ИAWS_PROFILE
Удерживает имя профиля, который я настроил на своей локальной машине, и который я хочу использовать для этого проекта.
Если вы хотите использовать этот метод, следуйте инструкциямздесьиздесьПолем
Контрольная точка 1
Теперь я дважды проверю, что до этого момента все работает нормально.
Я бегаю
terraform > terraform validate
Ему нужно распечатать
Успех! Конфигурация действительна.
А потом я действительно создам ресурсы.
Важный!Это тот момент, когда затраты будут понесены в вашей учетной записи AWS.
terraform > terraform apply
Мне представлены план, и я не могу ответить сyes
, если я хочу применить план:
Terraform will perform the following actions:
# aws_instance.timescaledb will be created
+ resource "aws_instance" "timescaledb" {
...
}
# aws_key_pair.timescaledb will be created
+ resource "aws_key_pair" "timescaledb" {
...
}
# aws_security_group.postgres will be created
+ resource "aws_security_group" "postgres" {
...
}
# aws_security_group.ssh will be created
+ resource "aws_security_group" "ssh" {
...
}
Plan: 4 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ ec2_timescaledb_public_ip = (known after apply)
+ ssh_connect = (known after apply)
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value:
Я отвечаюyes
И это займет около 2 минут, чтобы все было готово.
Затем я использую консоль управления AWS, чтобы проверить состояние нового экземпляра EC2. Я жду, когда станетRunning
:
Тогда я используюssh_connect
вывод для подключения к экземпляру с помощьюssh
командование Изterraform
Папка снова, я выполняю команду: (сначала оболочка выполняет командуterraform output ssh_connect
а затем берет свой выход и снова выполняется).
terraform > $(terraform output -raw ssh_connect)
После того, как я отвечу сyes
Чтобы сохранить удаленный ключ к известным хостам, я нахожусь в машине EC2 с оболочкой SSH:
Welcome to Ubuntu 24.04.2 LTS (GNU/Linux 6.8.0-1030-aws x86_64)
...
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.
ubuntu@ip-172-31-25-169:~$
Объем EBS - хранение данных
Все идет нормально.
Но в экземпляре EC2 нет установленного временного масштаба.
Кроме того, у него нет конкретного независимого от экземпляра хранилища для сохранения фактических данных базы данных.
Я хочу:
- Каждый раз, когда экземпляр EC2 создается (или воссоздан), чтобы установить временные рамки и
- Убедитесь, что, если экземпляр EC2 создан (или воссоздан), данные о времени, созданных из предыдущих запуска экземпляров, он выживает и продолжает существовать; то есть я хочу убедиться, что я не теряю свои данные каждый раз, когда экземпляр EC2, по любой причине, он разрушается, а затем создается с самого начала.
Схема шагов
Чтобы удовлетворить эти требования:
- Я создам громкость AWS EBS (используя ресурс
”aws_ebs_volume”
), затем - Я прикреплю его к экземпляру EC2 (используя ресурс
”aws_volume_attachment“
, затем - Я автоматически подключусь к экземпляру EC2 и
- Создайте устройство файловой системы, чтобы представить прикрепленный объем EBS
- Создать
/data
каталог и установите его на созданное устройство - Установите Postgres и настройку расширения времени ScaleDbb
- Настройка Postgres для использования
/data
Папка для хранения данных.
AWS EBS Volume Resource
Это конфигурация ресурса для тома AWS EBS:
файл:Terraform/ec2_volume.tf
resource "aws_ebs_volume" "timescaledb_volume" {
availability_zone = "${var.region}${var.timescaledb_server_availability_zone}"
size = 64
type = "gp3"
encrypted = false
final_snapshot = false
tags = {
Name = "${var.project}-${var.environment}-timescaledb-volume"
}
}
Примечание:final_snapshot
был настроен наfalse
Полем Вы можете повернуть это кtrue
Чтобы позволить AWS создать снимок при уничтожении этого тома, на случай, если вы хотите восстановить старые данные. Но настройка наtrue
понесет расходы.
Это довольно минимально, и многие значения жестко кодируются. Например,size
установлен на64
Гиб. И тип установлен наgp3
Полем Видимо, продолжайте и установите свои собственные ценности.
Если яterraform apply
, Terraform собирается создать объем EBS EBS, который будет стоять в одиночку, не прикрепленный ни к какому экземпляру EC2.
Прикрепить новый том к экземпляру EC2
Вот как я прикрепляю новый том к экземпляру EC2:
файл:Terraform/ec2_volume.tf
resource "aws_volume_attachment" "timescaledb_volume_attachment" {
device_name = "/dev/sdd"
volume_id = aws_ebs_volume.timescaledb_volume.id
instance_id = aws_instance.timescaledb.id
}
Это довольно просто, не так ли.
Но тогда можно спросить, как я придумал ценность”/dev/sdd”
дляdevice_name
?
Объяснять/dev/sdd
Если я посмотрю на детали AMI, который я использую, я получаю эту картинку с точки зрения блокировки устройств, которые с ним связаны:
Есть 3 устройства, которые AMI предлагает для экземпляров EC2, запущенных с этим AMI. Только одно из 3 устройств, с именем/dev/sda1
, используется в качестве корневого устройства 8GIB. Вот откуда экземпляр EC2 загружается. Устройства/dev/sdb
и/dev/sdc
являютсяэфемерныйВиртуальные устройства, которые еще не используются экземпляром EC2.
Это еще одна фотография (сделанная из документации AWS) о том, что происходит с хранением в этом экземпляре EC2:
И если я посмотрю на вкладку «Хранение» в деталях экземпляра, я увижу это:
Итак, экземпляр EC2 использует/dev/sda1
устройство.
Но как мне наконец выбрать имя/dev/sdd
Для нового тома, который я прикрепляю? АДокументация AWS об названии устройстваимеет детали.
Согласно документации и виртуализации AMI, я здесь:
С/dev/sdb
и/dev/sdc
ужевзятый, Я иду с/dev/sdd
Полем
Обратите внимание, что я не заинтересован в монтировании/dev/sdb
и/dev/sdc
эфемерные устройства. Итак, я просто оставляю их неизвестными.
Следовательно,после яterraform apply
новый ресурсtimescaledb_volume_attachment
, вкладка «Хранение» на изображении экземпляра будет:
Я вижу новый том, прикрепленный с использованием имени устройства/dev/sdd
Полем
Подготовьтесь к хранению данных
Но даже если объем EBS прикреплен к машине EC2, это не означает, что он готов писать на нем данные.
Если я перепутаю новый экземпляр EC2 и выполняю следующую команду:
$ lsblk
Я увижу что -то вроде этого:
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
loop0 7:0 0 27.2M 1 loop /snap/amazon-ssm-agent/11320
loop1 7:1 0 73.9M 1 loop /snap/core22/2010
loop2 7:2 0 50.9M 1 loop /snap/snapd/24718
nvme0n1 259:0 0 8G 0 disk
├─nvme0n1p1 259:1 0 7G 0 part /
├─nvme0n1p14 259:2 0 4M 0 part
├─nvme0n1p15 259:3 0 106M 0 part /boot/efi
└─nvme0n1p16 259:4 0 913M 0 part /boot
nvme1n1 259:5 0 64G 0 disk
Последняя записьnvme1n1
размера64G
соответствует недавно прикрепленному тому AWS EBS. Но у него нет разделения (например,nvme0n1
делает, например).
Мне нужно отформатировать и создать логические разделы. Тогда нам придется установить на него каталог.
У меня есть несколько способов сделать это с Terraform. Я решил пойти сterraform_dataресурс.
Я создаю файлprepare_ebs_volume.tf
со следующим контентом:
файл:Terraform/Prepare_ebs_volume.tf
resource "terraform_data" "prepare_ebs_volume_for_writing" {
depends_on = [aws_volume_attachment.timescaledb_volume_attachment]
triggers_replace = {
volume_attachment = aws_volume_attachment.timescaledb_volume_attachment.id
}
connection {
type = "ssh"
user = "ubuntu"
host = aws_instance.timescaledb.public_ip
private_key = file("${path.module}/timescaledb.pem")
}
provisioner "remote-exec" {
inline = concat(
[
"sudo file -s /dev/nvme1n1 | grep -q '/dev/nvme1n1: data$' && sudo mkfs -t xfs /dev/nvme1n1",
"sudo mkdir /data",
"sudo mount /dev/nvme1n1 /data",
"sudo cp /etc/fstab /etc/fstab.bak",
"echo \"UUID=$(sudo blkid -s UUID -o value /dev/nvme1n1) /data xfs defaults,nofail 0 2\" | sudo tee -a /etc/fstab"
]
)
}
}
- а
depends_on
имеет решающее значение, чтобы убедиться, что ресурсы создаются в правильном порядке, но - а
triggers_replace
имеет решающее значение для запуска реконструкций/развлечений, когда ресурс уже существует. И для этого конкретного случая, если объем EBS переосмыслен (см., Например, позже по делу, в котором экземпляр EC2 снова разрушен и воссоздан), мы хотим, чтобы этот предварительный провод снова стрелял. mkfs
Создает систему файлов типаxfs
на новом томе. Обратите внимание, что я звонюmkfs
only if there is not file system already on the volume, чтобы избежать перезагрузки любых существующих данных.mkdir
Создает/data
каталог. Именно здесь Postgres/TimeScaledB будет писать данные. Смотрите позже.mount
Собирает объем в каталог/data
Полем- а
echo….
Обновляет/etc/fstab
файл так, чтобы, когда экземпляр когда -либо перезагрузился, чтобы установить объем/data
автоматически.
Примечание:Если вы хотите прочитать больше об этом процессе, прочитайтеОфициальная документация AWS здесьПолем
Я делаюterraform apply
А потом я подходит к экземпляру EC2.
Командаlsblk
Теперь отпечатки:
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
loop0 7:0 0 27.2M 1 loop /snap/amazon-ssm-agent/11320
loop1 7:1 0 73.9M 1 loop /snap/core22/2010
loop2 7:2 0 50.9M 1 loop /snap/snapd/24718
nvme0n1 259:0 0 8G 0 disk
├─nvme0n1p1 259:1 0 7G 0 part /
├─nvme0n1p14 259:2 0 4M 0 part
├─nvme0n1p15 259:3 0 106M 0 part /boot/efi
└─nvme0n1p16 259:4 0 913M 0 part /boot
nvme1n1 259:5 0 64G 0 disk /data
Посмотри, какnvme1n1
Полем Вместо простоdisk
это показывает/data
Полем
Бинго! Если вы вернетесь и прочитаетеСхема шаговВыше вы увидите, что я уже сделал эти два шага:
- Создайте устройство файловой системы, чтобы представить прикрепленный объем EBS
- Создать
/data
каталог и установите его на созданное устройство
Следующие два шага:
Установите расширение Postgres и TimeScaledB
Теперь добавлю еще дваterraform_data
Блоки, которые будут позаботиться об установке расширения Postgres и TimeScaledB и настройки его для хранения данных в/data
Папка (которая теперь находится в новом томе EBS).
Я называю файлinstall_and_setup_timescaledb.tf
Полем
файл:Terraform/install_and_setup_timescaledb.tfПолем
locals {
db_name = "events_server_${var.environment}"
path_to_postgres_data_dir = "/data/postgresql/${var.postgresql_version}/main"
}
resource "terraform_data" "install_and_setup_timescaledb" {
depends_on = [terraform_data.prepare_ebs_volume_for_writing]
triggers_replace = {
volume_attachment = aws_volume_attachment.timescaledb_volume_attachment.id
postgresql_version = var.postgresql_version
}
connection {
type = "ssh"
user = "ubuntu"
host = aws_instance.timescaledb.public_ip
private_key = file("${path.module}/timescaledb.pem")
}
provisioner "file" {
source = "${path.module}/install_postgres.sh"
destination = "/home/ubuntu/install_postgres.sh"
}
provisioner "file" {
source = "${path.module}/install_timescaledb.sh"
destination = "/home/ubuntu/install_timescaledb.sh"
}
provisioner "remote-exec" {
inline = concat(
[
"sudo chmod u+x /home/ubuntu/install_postgres.sh",
"sudo ./install_postgres.sh ${var.postgresql_version} ${var.timescaledb_server_port} ${local.db_name} ${var.timescaledb_version}",
"sudo chmod u+x /home/ubuntu/install_timescaledb.sh",
"sudo ./install_timescaledb.sh ${var.postgresql_version} ${var.timescaledb_version} ${local.db_name}"
]
)
}
}
resource "terraform_data" "postgres_password" {
depends_on = [terraform_data.install_and_setup_timescaledb]
triggers_replace = {
volume_attachment = aws_volume_attachment.timescaledb_volume_attachment.id
postgresql_version = var.postgresql_version
}
connection {
type = "ssh"
user = "ubuntu"
host = aws_instance.timescaledb.public_ip
private_key = file("${path.module}/timescaledb.pem")
}
provisioner "remote-exec" {
inline = concat(
[
"echo '**************** remote-exec: Setting the postgres user password...'",
"sudo -u postgres psql -c \"ALTER USER postgres WITH PASSWORD '${var.timescaledb_server_postgres_password}';\""
]
)
}
}
output "psql_connect" {
value = "psql -h ${aws_instance.timescaledb.public_ip} -U postgres -d ${local.db_name} -p ${var.timescaledb_server_port}"
description = "Command to connect to the PostgreSQL database using psql."
}
Зависимость:Я положил зависимость первого ресурса к предыдущему, то естьinstall_and_setup_timescaledb
вterraform_data.prepare_ebs_volume_for_writing
Полем Следовательно,remote-exec
Вызовы будут выполнены после выполнения команд этого блока.
Triggers Replace:Однако помимо порядка творения, который указан сdependends_on
Блок, я также используюtriggers_replace
Блок, чтобы указать, что я хочу, чтобы этот блок был воссоздан в этих двух условиях:
- Каждый раз, когда воссоздается прикрепление объема EBS.
- Каждый раз, когда версия сервера Postgres меняется.
Два сценария
Я использую два сценария Bash:
- Bash Script для установки Postgres:
install_postgres.sh
Полем - Bash Script для установки TimeScaledB:
install_timescaledb.sh
Полем
Обратите внимание, что оба сценария также называются при обновлении версии Postgres.
Установка Postgres
Файлinstall_postgres.sh
имеет следующее содержимое:
файлTerraform/install_postgres.shПолем
#!/bin/bash
set -e # e: exit if any command has a non-zero exit status
set -x # x: all executed commands are printed to the terminal
set -u # u: all references to variables that have not been previously defined cause an error
NEW_VERSION=$1
PORT=$2
DB_NAME=$3
TIMESCALEDB_VERSION="${4}*"
# Create the PostgreSQL data directory
sudo mkdir -p /data/postgresql
# Update the package list
sudo DEBIAN_FRONTEND=noninteractive apt update -y
# Install postgres common tools
sudo DEBIAN_FRONTEND=noninteractive apt install -y postgresql-common apt-transport-https net-tools
# Enable the PostgreSQL APT repository
sudo DEBIAN_FRONTEND=noninteractive /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
# Let's now install on top of any existing
CURRENT_CLUSTER=$(sudo pg_lsclusters -h 2>/dev/null | grep "${PORT}" | awk '{print $1"-"$2}' || true)
INSTALL="yes"
OLD_VERSION=""
OLD_NAME="main"
if [ -n "$CURRENT_CLUSTER" ]; then
echo "Found existing PostgreSQL cluster:"
echo "$CURRENT_CLUSTER"
echo
OLD_VERSION=$(echo $CURRENT_CLUSTER | cut -d'-' -f1)
OLD_NAME=$(echo $CURRENT_CLUSTER | cut -d'-' -f2)
# Skip if this is the same version and name we're about to install
if [ "$OLD_VERSION" = "$NEW_VERSION" ] && [ "$OLD_NAME" = "main" ]; then
echo "Skipping cluster $OLD_VERSION/$OLD_NAME as it matches the target version and name"
INSTALL="no"
continue
fi
echo "************************ stopping and disabling $OLD_VERSION/$OLD_NAME ************************"
sudo systemctl stop postgresql@$OLD_VERSION-$OLD_NAME || true
sudo systemctl status postgresql@$OLD_VERSION-$OLD_NAME --no-pager || true
sudo systemctl disable postgresql@$OLD_VERSION-$OLD_NAME || true
sudo pg_ctlcluster stop $OLD_VERSION $OLD_NAME || true
sudo pg_lsclusters -h
else
echo "No existing PostgreSQL clusters found on port ${PORT}. Proceeding with installation."
echo
INSTALL="yes"
fi
echo "****************** INSTALL: $INSTALL"
if [ "$INSTALL" = "yes" ]; then
sudo DEBIAN_FRONTEND=noninteractive apt install -y postgresql-$NEW_VERSION postgresql-client-$NEW_VERSION postgresql-contrib-$NEW_VERSION
sudo DEBIAN_FRONTEND=noninteractive apt install -y postgresql-server-dev-$NEW_VERSION
# When I install postgresql for the first time, the cluster is already created.
# But when I install a new version while another already exists, the cluster is not created.
if sudo pg_lsclusters -h 2>/dev/null | grep -q "^${NEW_VERSION}[[:space:]]\+main[[:space:]]"; then
echo "Cluster $NEW_VERSION/main already exists"
else
echo "Creating cluster $NEW_VERSION/main"
sudo pg_createcluster $NEW_VERSION main
sudo pg_ctlcluster start $NEW_VERSION main
sudo pg_lsclusters -h
fi
sudo systemctl start postgresql@${NEW_VERSION}-main
sudo systemctl enable postgresql@$NEW_VERSION-main
else
echo "Skipping installation of PostgreSQL ${NEW_VERSION} as it is already installed."
fi
# Show final status
echo "Current PostgreSQL clusters:"
sudo pg_lsclusters -h
sudo systemctl status postgresql@${NEW_VERSION}-main --no-pager
if [ -n "$OLD_VERSION" ]; then
echo "Stopping and disabling old PostgreSQL cluster $OLD_VERSION/$OLD_NAME"
sudo pg_ctlcluster stop $OLD_VERSION $OLD_NAME || true
else
echo "No old PostgreSQL cluster to stop."
fi
# Stop postgres from running
sudo systemctl stop postgresql@${NEW_VERSION}-main
# Change where PostgreSQL stores its data
ORIGINAL_DATA_DIR="/var/lib/postgresql/${NEW_VERSION}/main"
if [ -d "${ORIGINAL_DATA_DIR}" ];then
if [ -d "/var/lib/postgresql/${NEW_VERSION}/main.bak" ]; then
rm -f -R /var/lib/postgresql/${NEW_VERSION}/main.bak
fi
sudo mv ${ORIGINAL_DATA_DIR} /var/lib/postgresql/${NEW_VERSION}/main.bak
fi
NEW_PATH_TO_POSTGRES_DATA_DIR="/data/postgresql/${NEW_VERSION}/main"
sudo mkdir -p ${NEW_PATH_TO_POSTGRES_DATA_DIR}
sudo chown -R postgres:postgres /data/postgresql
if [ -n "$(sudo ls -A ${NEW_PATH_TO_POSTGRES_DATA_DIR} 2>/dev/null)" ]; then
echo "The new PostgreSQL data directory is not empty. We will not initialize it."
else
echo "Initializing new PostgreSQL data directory at ${NEW_PATH_TO_POSTGRES_DATA_DIR}"
sudo -u postgres /usr/lib/postgresql/${NEW_VERSION}/bin/initdb -D ${NEW_PATH_TO_POSTGRES_DATA_DIR}
fi
sudo sed -i "s|data_directory = '${ORIGINAL_DATA_DIR}'|data_directory = '${NEW_PATH_TO_POSTGRES_DATA_DIR}'|g" /etc/postgresql/${NEW_VERSION}/main/postgresql.conf
# Set the port to whatever we specify as port in the terraform variables
sudo sed -i "s|port = .*|port = ${PORT}|g" /etc/postgresql/${NEW_VERSION}/main/postgresql.conf
# Allow remote connections
sudo sed -i "s|#listen_addresses = 'localhost'|listen_addresses = '*'|g" /etc/postgresql/${NEW_VERSION}/main/postgresql.conf
sudo sed -i "s|host all all 127.0.0.1/32 scram-sha-256|host all all 0.0.0.0/0 scram-sha-256|g" /etc/postgresql/${NEW_VERSION}/main/pg_hba.conf
# upgrade from previous version if needed
LAST_CLUSTER=$(echo "$CURRENT_CLUSTER" | tail -n 1)
if [ -n "$LAST_CLUSTER" ]; then
OLD_VERSION=$(echo $LAST_CLUSTER | cut -d'-' -f1)
OLD_NAME=$(echo $LAST_CLUSTER | cut -d'-' -f2)
if [ "$OLD_VERSION" = "$NEW_VERSION" ] && [ "$OLD_NAME" = "main" ]; then
echo "...no need to upgrade data, we are on the same cluster version and name"
else
echo "We need to upgrade the data of the last cluster ${OLD_VERSION}-${OLD_NAME}"
# We will need to install timescale db for the new version, otherwise the pg_upgrade will fail
# ---------------------------------------------------------------------------------------------
sudo apt install timescaledb-2-postgresql-${NEW_VERSION}="${TIMESCALEDB_VERSION}" timescaledb-2-loader-postgresql-${NEW_VERSION}="${TIMESCALEDB_VERSION}" timescaledb-toolkit-postgresql-${NEW_VERSION} -y
sudo sed -i "s|#shared_preload_libraries = ''|shared_preload_libraries = 'timescaledb'|g" /data/postgresql/${NEW_VERSION}/main/postgresql.conf
# Tune TimescaleDB
sudo timescaledb-tune --yes
# --------------- end of installing timescale db for the new version -----------------------------
(cd /tmp && sudo -u postgres /usr/lib/postgresql/${NEW_VERSION}/bin/pg_upgrade \
--old-datadir=/data/postgresql/$OLD_VERSION/$OLD_NAME \
--new-datadir=/data/postgresql/$NEW_VERSION/main \
--old-bindir=/usr/lib/postgresql/$OLD_VERSION/bin \
--new-bindir=/usr/lib/postgresql/$NEW_VERSION/bin)
fi
fi
# Restart PostgreSQL to apply changes
sudo systemctl restart postgresql@${NEW_VERSION}-main
# Wait for PostgreSQL to start
sleep 10
# Create the database
sudo -u postgres psql -c "create database ${DB_NAME};" || echo "Database ${DB_NAME} already exists, skipping creation."
Я считаю, что комментарии между строками заявлений сценариев достаточно, чтобы понять, что происходит.
Обратите внимание, что сценарий гарантирует, что:
- Его можно использовать для обновления от одной версии Postgres в другую.ПонижениеНе работай, хотя.
- Это гарантирует, что это тоже обновляет данные.
Установка временного масштаба
Файлinstall_timescaledb.sh
имеет следующее содержимое:
файл:Terraform/install_timescaledb.shПолем
#!/bin/bash
set -e # e: exit if any command has a non-zero exit status
set -x # x: all executed commands are printed to the terminal
set -u # u: all references to variables that have not been previously defined cause an error
POSTGRESQL_VERSION=$1
TIMESCALEDB_VERSION="${2}*"
DB_NAME=$3
# Set TimescaleDB repository at system repositories so that we can install TimescaleDB
echo "deb https://packagecloud.io/timescale/timescaledb/ubuntu/ $(lsb_release -c -s) main" | sudo tee /etc/apt/sources.list.d/timescaledb.list
wget --quiet -O - https://packagecloud.io/timescale/timescaledb/gpgkey | sudo gpg --dearmor --yes -o /etc/apt/trusted.gpg.d/timescaledb.gpg
sudo DEBIAN_FRONTEND=noninteractive apt update -y
sudo systemctl stop postgresql@${POSTGRESQL_VERSION}-main || true
# Install TimescaleDB and TimescaleDB Toolkit all TimescaleDB postgres related packages
sudo apt install timescaledb-2-postgresql-${POSTGRESQL_VERSION}="${TIMESCALEDB_VERSION}" timescaledb-2-loader-postgresql-${POSTGRESQL_VERSION}="${TIMESCALEDB_VERSION}" timescaledb-toolkit-postgresql-${POSTGRESQL_VERSION} -y
sudo sed -i "s|#shared_preload_libraries = ''|shared_preload_libraries = 'timescaledb'|g" /data/postgresql/${POSTGRESQL_VERSION}/main/postgresql.conf
# Tune TimescaleDB
sudo timescaledb-tune --yes
sudo systemctl restart postgresql@${POSTGRESQL_VERSION}-main
# Create the extensions for TimescaleDB and TimescaleDB Toolkit
sudo -u postgres psql -d ${DB_NAME} -c "CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;"
sudo -u postgres psql -d ${DB_NAME} -c "CREATE EXTENSION IF NOT EXISTS timescaledb_toolkit CASCADE;"
Опять же, этот сценарий идентификатор и позволяет обновлять время DB Timesscale.
Он устанавливает расширение временного масштаба и инструментарий TimeScaledB.
Кроме того, он устанавливает некоторые общие значения по умолчанию для конфигурации временного масштаба.
Установка пароля
Второйterraform_data
ресурс - это настройка пароля дляpostgres
пользователь. У меня есть это в отдельном блоке, потому что он использует конфиденциальные данные, и если я помесчу их в первый блок, Terraform скрывает все выводы от этого блока (команды установки Postgres и настройки), которые я бы не хотел.
Важный!Эти сценарии представляют больше переменных. Мойvariables.tf
Нужно тоже иметь это:
variable "postgresql_version" {
description = "The PostgreSQL version to install on the TimescaleDB server."
type = string
default = "16"
}
variable "timescaledb_version" {
description = "The TimescaleDB version to install on the TimescaleDB server."
type = string
default = "2.19.3"
}
variable "timescaledb_server_port" {
description = "Port for the TimescaleDB server."
type = number
default = 5432
}
variable "timescaledb_server_postgres_password" {
description = "The password for the PostgreSQL user on the TimescaleDB server."
type = string
sensitive = true
}
Примечание:На момент написания этой статьи последняя версия Postgres была17
, но в приведенном выше файле я выберу для установки версии16
, чтобы начать с. Позже в посте ниже вы увидите, как я решил обновить до17
, чтобы продемонстрировать процесс обновления Postgres.
Важный!Смотрите также на.envrc
Файл (или другой файл, который вы сохраняете переменные среды, которые устанавливают значения в входные переменные Terraform), вам необходимо добавитьTF_VAR_timescaledb_server_postgres_password=…
...
export TF_VAR_timescaledb_server_postgres_password='...<put your postgres password here>...'
Если я это сделаюterraform apply
, в конце, у меня готов к срокам.
Я использую это для подключения (отterraform
папка):
$ (terraform output -raw psql_connect)
А потом я ключу вpostgres
пароль пользователя и я наpsql
Терминал.
Посмотрите на вывод некоторых команд.
Убедитесь, что расширения установлены
Я подключаюсь кevents_server_development
База данных и я выпускаю команду, чтобы установить расширения:
postgres=# \c events_server_development
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
You are now connected to database "events_server_development" as user "postgres".
events_server_development=# select * from pg_extension;
oid | extname | extowner | extnamespace | extrelocatable | extversion | extconfig | extcondition
-------+---------------------+----------+--------------+----------------+------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------
13610 | plpgsql | 10 | 11 | f | 1.0 | |
17140 | timescaledb_toolkit | 10 | 2200 | f | 1.21.0 | |
16385 | timescaledb | 10 | 2200 | f | 2.19.3 | {16406,16407,16429,16443,16442,16462,16461,16477,16476,16502,16518,16519,16536,16535,16555,16556,16611,16624,16651,16664,16674,16684,16688,16704,16715,16731,16740,16739} | {"","WHERE id >= 1","","","","","","","","","","","","","","WHERE id >= 1000"," WHERE key <> 'uuid' ","","","","","","","","","","",""}
(3 rows)
Создание таблицы и ряда
Я создаю таблицу и вставляю ряд.
postgres=# \c events_server_development
psql (16.9 (Ubuntu 16.9-0ubuntu0.24.04.1), server 17.5 (Ubuntu 17.5-1.pgdg24.04+1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
You are now connected to database "events_server_development" as user "postgres".
events_server_development=# create table events (id bigint not null, type varchar not null, created_at timestamp default curren
t_timestamp not null, primary key(id));
CREATE TABLE
Time: 37,602 ms
events_server_development=# insert into events (id, type) values (1, 'product_sold');
INSERT 0 1
Time: 31,945 ms
events_server_development=# select * from e vents;
id | type | created_at
----+--------------+----------------------------
1 | product_sold | 2025-07-06 12:37:03.418051
(1 row)
Time: 31,053 ms
Итак, я создал таблицуevents
И я добавил в него 1 ряд.
Давайте теперь увидим некоторые сценарии, в которых мы хотели бы сохранить наши данные нетронутыми.
Что если экземпляр EC2 обновлен?
Текущий тип экземпляра EC2t3.xlarge
Полем Что если я хочу масштабировать доt3.2xlarge
например.
Я обновляю значение по умолчанию переменнойtimescaledb_server_instance_type
кt3.2xlarge
и я делаюterraform apply
Полем
Представленный план очень прост:
# aws_instance.timescaledb will be updated in-place
~ resource "aws_instance" "timescaledb" {
id = "i-069be2f31daf52118"
~ instance_type = "t3.xlarge" -> "t3.2xlarge"
~ public_dns = "ec2-63-178-242-62.eu-central-1.compute.amazonaws.com" -> (known after apply)
~ public_ip = "63.178.242.62" -> (known after apply)
tags = {
"Name" = "setting_up_timescaledb_on_aws-development-timescaledb"
}
# (35 unchanged attributes hidden)
# (9 unchanged blocks hidden)
}
Plan: 0 to add, 1 to change, 0 to destroy.
Только аизменятьтипа экземпляра.
яприменятьА потом я подключаюсь к временным масштабам сpsql
Полем Оно работает. Мои данные все еще там.
Что если экземпляр EC2 заменен?
Я делаюterraform taint aws_instance.timescaledb
Чтобы отметить экземпляр EC2 как нечто, что нужно уничтожить и создать снова Terraform.
Тогда я делаюterraform apply
Полем Я вижу, что мне представлены план
- уничтожить и создать экземпляр EC2
- Уничтожить и воссоздайте приложение объема EBS.
- уничтожить и воссоздать
terraform_data
prepare_ebs_volume_for_writing
Полем - уничтожить и воссоздать
terraform_data
install_and_setup_timescaledb
Полем
Кажется, это именно то, что я хочу. Новый экземпляр EC2 и объем EBS будут отделены от старого и прикрепленного к новому. Кроме того, установка Postgres на новой машине.
Я продолжаю.
Затем я проверяю, с чем я могу связатьсяpsql
И что я могу найти свои данные на месте.
Бум! Данные там! Все хорошо!
Что если объем EBS увеличивается?
Я иду вec2_volume.tf
Файл, и я увеличиваю размер хранилища из64
к128
Полем
Тогда я делаюterraform apply
Полем
Мне представлен простой план, что объем EBS будет обновлен на месте.
Затем я подключаюсь к базе данных, и я все еще вижу свои данные нетронутыми.
Что, если версия Postgres обновлена?
Я меняю значение по умолчаниюpostgresql_version
переменная от16
к17
Полем Спасибоtriggers_replace
ресурсовinstall_and_setup_timescaledb
иpostgres_password
, эти ресурсы будут воссозданы/заменены.
После того, как я это сделаюterraform apply
Затем я подключаюсь к серверу Postgres. Я все еще вижу свойevents
стол иevents
записывать.
Что, если версия времена обновляется?
Этотне поддерживаетсяпо текущему сценарию. Обновление минорной и/или основной версии TimeScaledB Minate и/или основной версии - это довольно сложный процесс, который здесь описан:Обновление временного масштабаПолем
Что можно улучшить?
Эта установка хороша для начала, но у нее есть много возможностей для улучшения с точки зрения готовности к производству
- Можно реализовать чтение-реплику основной базы данных. Read-Replica может использоваться для масштабирования сервера горизонтально, обслуживая запросы на чтение.
- Автоматизированные резервные копии и быстрое восстановление из резервного копирования должны быть настроены.
- Тревоги CloudWatch должны быть реализованы на
- Процессор
- Память
- Бесплатное хранение
- Разрешить прямой доступ только из VPC.
- Реализуйте развертывание и автоматическое переключение с несколькими AZ от Master на Sand-By Server в другой зоне.
- Использование AWS Systems Manager для установки PostGRES и TimeScaledB и управления обновлениями.
- Иметь поставщика Terraform, который позволил бы настройку специфических параметров временного масштаба.
- Прикрепите тот же упругой IP, если кто -то захочет разоблачить тот же публичный IP.
Заключительные мысли
Выполнить свой собственный, самостоятельный временной шкал на AWS вполне возможно. Вышеуказанная работа демонстрирует это. Тем не менее, это нужно больше работы, если это должно было стать реальной производственной средой.
Гм! Я - Panos Matsinopoulos, скромный читатель программного обеспечения, писатель, классическая музыка и любитель двигателей V8. Я работаю наПротокол талантов, где мы заставляем строителей получить признание, которого они заслуживают.
Оригинал