commit
64be99308b
53 files changed
+1983
No files matched your search
@@ -0,0 +1,5 @@
|
||||
POSTGRES_PASSWORD=
|
||||
PGADMIN_PASSWORD=
|
||||
SILO_PASSWORD=
|
||||
|
||||
VAULT_TOKEN=
|
||||
@@ -0,0 +1,26 @@
|
||||
name: terraform
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: hashicorp/setup-terraform@v3
|
||||
with:
|
||||
terraform_version: 1.16.5
|
||||
|
||||
- name: fmt
|
||||
run: terraform fmt -check -recursive modules vault storage database
|
||||
|
||||
- name: validate
|
||||
run: |
|
||||
for dir in vault storage database; do
|
||||
terraform -chdir=$dir init -backend=false -input=false
|
||||
terraform -chdir=$dir validate
|
||||
done
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
# some data
|
||||
data/*
|
||||
vault-deploy/local-config/*
|
||||
|
||||
# secrets
|
||||
.env
|
||||
vault_keys.txt
|
||||
|
||||
# generated from config.yaml by mise run render
|
||||
.env.config
|
||||
backend.hcl
|
||||
|
||||
# terraform
|
||||
.terraform/
|
||||
*.tfstate*
|
||||
*.tfvars
|
||||
@@ -0,0 +1,290 @@
|
||||
# infra
|
||||
|
||||
Локальная инфраструктура для проекта: Postgres с pgvector, S3-хранилище Silo, хранилище секретов OpenBao (форк Vault) и pgAdmin. Репозиторий задуман как шаблон: всё, что относится к конкретному проекту, лежит в `config.yaml` и `.env`.
|
||||
|
||||
Роли разделены:
|
||||
|
||||
- **Docker Compose** запускает сервисы.
|
||||
- **Terraform** настраивает то, что внутри них: бакеты и ключи S3, базы и роли Postgres, политики и секреты Vault.
|
||||
- **Vault** хранит сгенерированные секреты. Приложения берут их оттуда, а не из `.env`.
|
||||
|
||||
## Требования
|
||||
|
||||
| Инструмент | Зачем |
|
||||
|---|---|
|
||||
| Docker с Compose 2.20+ | сервисы; нужна поддержка `include` |
|
||||
| Terraform 1.6+ | проверено на 1.16.5 |
|
||||
| [mise](https://mise.jdx.dev) | запуск всех команд и сборка окружения |
|
||||
| `jq`, `curl` | используются задачами |
|
||||
|
||||
## Повседневная работа
|
||||
|
||||
Все команды запускаются из каталога `infra/`. При первом запуске mise попросит доверить ему конфигурацию: `mise trust`.
|
||||
|
||||
| Команда | Что делает |
|
||||
|---|---|
|
||||
| `mise run up` | поднять сервисы |
|
||||
| `mise run down` | остановить сервисы, данные в `data/` остаются |
|
||||
| `mise run plan` | `terraform plan` во всех трёх конфигурациях |
|
||||
| `mise run tf <каталог> <команда>` | любая команда Terraform, например `mise run tf storage apply` |
|
||||
| `mise run fmt` | отформатировать все `.tf` |
|
||||
| `mise run check` | форматирование и `terraform validate`; то же делает CI в `.github/workflows/terraform.yml` |
|
||||
| `mise run vault:app-check [сервис]` | проверить, что сервис получает из Vault всё нужное и ничего лишнего |
|
||||
| `mise run vault:backup` | снапшот Vault в бакет бэкапов |
|
||||
| `mise run vault:bootstrap` | выпустить токен Terraform, нужен `ROOT_TOKEN` |
|
||||
| `mise run example:up [сервис] [static\|dynamic]` | запустить пример Go-сервиса, `example:down` — остановить |
|
||||
|
||||
`mise tasks` показывает полный список. Обычный `docker compose` (`ps`, `logs`, `exec`) работает как всегда.
|
||||
|
||||
Правило: **перед каждым `apply` читать `plan`**, особенно строки `destroy` и `-/+`.
|
||||
|
||||
### После перезапуска контейнера Vault
|
||||
|
||||
Vault запускается запечатанным, и пока его не распечатали, ни Terraform, ни сервисы секретов не получат:
|
||||
|
||||
```bash
|
||||
docker compose exec vault bao operator unseal # спросит unseal-ключ
|
||||
docker compose exec vault bao status # Sealed должно быть false
|
||||
```
|
||||
|
||||
## Где лежат настройки
|
||||
|
||||
| Что | Где | Кто правит |
|
||||
|---|---|---|
|
||||
| Всё несекретное: адреса, порты, образы, логины администраторов, имена бакетов, сервисы | `config.yaml` | вы |
|
||||
| Пароли администраторов и токен Vault для Terraform | `.env` (образец в `.env.example`) | вы |
|
||||
| Секреты сервисов: ключи S3, пароли баз | Vault | Terraform |
|
||||
| `.env.config`, `backend.hcl` | генерируются из `config.yaml` | никто |
|
||||
|
||||
Два сгенерированных файла нужны потому, что Compose и блок `backend` в Terraform читать YAML не умеют. mise пересоздаёт их сам, когда `config.yaml` изменился; в git они не попадают.
|
||||
|
||||
Секреты из `.env` превращаются в переменные, которые ждут инструменты (`TF_VAR_*` для Terraform, `AWS_*` для backend), в секции `[env]` файла `mise.toml`.
|
||||
|
||||
## Как добавить сервис
|
||||
|
||||
1. Описать его в `config.yaml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
orders:
|
||||
postgres:
|
||||
username: orders
|
||||
name: orders
|
||||
pgvector: true # необязательно, по умолчанию false
|
||||
buckets:
|
||||
orders-files:
|
||||
versioning: true # по умолчанию true
|
||||
noncurrent_days: 30 # сколько хранить старые версии
|
||||
```
|
||||
|
||||
Блоки `postgres` и `buckets` необязательны: сервису можно дать только базу или только бакеты.
|
||||
|
||||
2. Применить по очереди, читая `plan` перед каждым `apply`:
|
||||
|
||||
```bash
|
||||
mise run tf vault apply # политика и роль AppRole
|
||||
mise run tf storage apply # бакеты и ключи S3
|
||||
mise run tf database apply # база, роль, пароли
|
||||
```
|
||||
|
||||
3. Проверить: `mise run vault:app-check orders`.
|
||||
|
||||
Удаление сервиса — те же шаги в обратном порядке (`database`, `storage`, `vault`) после удаления блока из `config.yaml`. Бакет должен быть пустым. Переименование бакета или базы Terraform воспринимает как удаление старого и создание нового.
|
||||
|
||||
Имена бакетов должны быть уникальны между сервисами; удобно начинать их с имени сервиса.
|
||||
|
||||
Опечатка в имени необязательного поля (например, `noncurent_days`) ошибки не вызовет: лишнее поле молча отбрасывается, и берётся значение по умолчанию. После правки `config.yaml` смотрите в `plan`, что изменилось именно то, что вы хотели.
|
||||
|
||||
## Что получает сервис
|
||||
|
||||
Сервис входит в Vault через AppRole по паре `role_id` и `secret_id` и получает токен на час. С ним доступно:
|
||||
|
||||
| Путь в Vault | Содержимое |
|
||||
|---|---|
|
||||
| `secrets/<сервис>/s3` | `endpoint`, `buckets`, `access_key`, `secret_key` |
|
||||
| `secrets/<сервис>/postgres` | постоянные `host`, `port`, `database`, `username`, `password`, `sslmode` |
|
||||
| `database/creds/<сервис>` | временные `username` и `password`: Vault создаёт роль в Postgres на час и сам её удаляет |
|
||||
|
||||
Какой пароль базы выбрать: постоянный проще и подходит по умолчанию; временный не нужно ротировать и он не лежит в KV, но сервис обязан продлевать токен Vault, иначе через час потеряет доступ к базе.
|
||||
|
||||
Адреса в секретах указаны для сети Compose (`postgres:5432`, `object-storage:9000`). Чужие пути отдают `403`. В HTTP API движка KV версии 2 после имени mount идёт `data/`: `GET /v1/secrets/data/<сервис>/s3`.
|
||||
|
||||
`role_id` лежит в `mise run tf vault output approle_role_ids`. `secret_id` — это пароль сервиса: он действует 30 дней, после чего сервису нужен новый. Выпускается отдельно:
|
||||
|
||||
```bash
|
||||
curl -X POST -H "X-Vault-Token: $VAULT_TOKEN" \
|
||||
http://127.0.0.1:8200/v1/auth/approle/role/<сервис>/secret-id
|
||||
```
|
||||
|
||||
Готовый пример всех запросов, которые делает сервис, — задача `mise-tasks/vault/app-check`.
|
||||
|
||||
## Пример сервиса на Go
|
||||
|
||||
В `examples/go-service/` лежит небольшой сервис, который показывает всю цепочку: при запуске он знает только адрес Vault, `role_id` и `secret_id`, а ключи S3 и пароль базы получает из Vault.
|
||||
|
||||
```bash
|
||||
mise run example:up # первый сервис из config.yaml, постоянный пароль базы
|
||||
mise run example:up <сервис> dynamic # временная роль в Postgres от Vault
|
||||
curl http://127.0.0.1:8090/ # состояние подключений
|
||||
mise run example:down
|
||||
```
|
||||
|
||||
Задача `example:up` действует как администратор: берёт `role_id`, выпускает одноразовый `secret_id`, собирает образ и запускает контейнер в сети основного Compose. Дальше работает только код из `main.go`:
|
||||
|
||||
1. вход через AppRole клиентом `github.com/hashicorp/vault/api`;
|
||||
2. чтение `secrets/<сервис>/s3` и `secrets/<сервис>/postgres`;
|
||||
3. подключение к Postgres (`pgx`) и S3 (`minio-go`);
|
||||
4. HTTP-обработчик, который показывает, под кем сервис работает в базе и сколько объектов видит в своих бакетах.
|
||||
|
||||
Это учебный пример, и в нём намеренно нет того, что нужно настоящему сервису:
|
||||
|
||||
- **Продления токена.** Токен Vault живёт час. С постоянным паролем сервис продолжит работать и после этого, а с временной ролью потеряет доступ к базе: Vault удаляет роль вместе с токеном. Настоящий сервис продлевает токен (`LifetimeWatcher` в клиенте) или входит заново.
|
||||
- **Повторного входа.** `secret_id` одноразовый, поэтому после перезапуска контейнера сервис в Vault не войдёт; нужен новый `mise run example:up`.
|
||||
- **Отзыва токена при остановке.** Временная роль остаётся в Postgres до истечения срока, до часа.
|
||||
|
||||
## Структура
|
||||
|
||||
```
|
||||
infra/
|
||||
├── config.yaml # несекретные настройки
|
||||
├── .env # секреты, не в git
|
||||
├── mise.toml # окружение и короткие задачи
|
||||
├── mise-env.sh # следит за актуальностью сгенерированных файлов
|
||||
├── mise-tasks/ # длинные задачи: tf, render, vault/*, example/*
|
||||
├── examples/go-service/ # пример сервиса, который берёт секреты из Vault
|
||||
├── docker-compose.yml # подключает compose.services.yml с обоими env-файлами
|
||||
├── compose.services.yml # описание сервисов
|
||||
├── config/vault/vault.hcl # конфигурация OpenBao
|
||||
├── data/ # данные сервисов, не в git
|
||||
├── modules/
|
||||
│ ├── config/ # типы, проверки и значения по умолчанию для config.yaml
|
||||
│ ├── app-storage/ # бакеты, lifecycle, пользователь и ключи одного сервиса
|
||||
│ └── app-database/ # база, роль и права одного сервиса
|
||||
├── vault/ # Terraform: mount, политики, AppRole
|
||||
├── storage/ # Terraform: Silo и запись ключей в Vault
|
||||
└── database/ # Terraform: Postgres, запись паролей и временные роли
|
||||
```
|
||||
|
||||
`vault/`, `storage/` и `database/` — три отдельные конфигурации, каждая со своим state в бакете из `infra.silo.state_bucket`. Объединить их нельзя: провайдеру нужен уже настроенный сервис во время `plan`. Порядок применения всегда `vault` → `storage` → `database`.
|
||||
|
||||
## Vault: что нужно знать
|
||||
|
||||
- **Токен Terraform** лежит в `.env` как `VAULT_TOKEN`. Он живёт, пока его продлевают хотя бы раз в 30 дней: `docker compose exec -e BAO_TOKEN=<токен> vault bao token renew`.
|
||||
- **Его политика** (`vault_policy.terraform` в `vault/main.tf`) перечисляет только реально используемые пути. Новый тип ресурса в Vault сначала требует строки в политике; токен может обновить её сам. Создавать и удалять mount он не может намеренно.
|
||||
- **Root-токен отозван.** Новый выпускается через `bao operator generate-root` с unseal-ключом.
|
||||
- **Unseal-ключ** хранить вне машины и отдельно от снапшотов: без него снапшот не расшифровать, вместе они дают полный доступ.
|
||||
- **Бэкап** делает `mise run vault:backup`: снапшот уходит в бакет из `infra.silo.backup_bucket` и хранится 30 дней. Расписания нет, оно зависит от машины; задачу можно вызывать из cron или таймера systemd.
|
||||
- **Снапшоты лежат на той же машине**, что и Vault, так что от потери диска они не спасают. Восстановление из снапшота не проверялось.
|
||||
|
||||
## Новый проект из шаблона
|
||||
|
||||
Шаги проверены на чистой копии репозитория с пустым каталогом данных.
|
||||
|
||||
0. Скопировать репозиторий без `.env`, `data/`, `vault_keys.txt` и каталогов `.terraform/` (в git их и так нет). В `config.yaml` заменить под свой проект: логины администраторов, порты, если стандартные заняты, имена бакетов для state и бэкапов, раздел `services`. Больше ничего править не нужно.
|
||||
1. Скопировать `.env.example` в `.env` и заполнить три пароля; `VAULT_TOKEN` оставить пустым, его запишет шаг 6. Спецсимволы в паролях ломают адреса подключения, поэтому удобно генерировать их командой `openssl rand -hex 32`. Затем `mise trust` и `mise run up`. Поднимать именно этой задачей: она выдаёт Vault и pgAdmin права на их каталоги данных, без чего на чистой машине они не запускаются.
|
||||
2. Инициализировать Vault, сохранить unseal-ключ и root-токен, распечатать:
|
||||
|
||||
```bash
|
||||
docker compose exec vault bao operator init -key-shares=1 -key-threshold=1
|
||||
docker compose exec vault bao operator unseal
|
||||
```
|
||||
|
||||
3. Создать бакет для state вручную. Terraform хранит в нём свой state, поэтому создать его сам до `init` не может:
|
||||
|
||||
```bash
|
||||
set -a; source .env; source .env.config; set +a
|
||||
docker compose exec -e MC_HOST_local="http://${SILO_USER}:${SILO_PASSWORD}@localhost:9000" \
|
||||
object-storage mc mb local/<state_bucket из config.yaml>
|
||||
```
|
||||
|
||||
4. Временно записать root-токен в `.env` как `VAULT_TOKEN`: первый `apply` создаёт mount и метод входа, а у токена Terraform на это нет прав.
|
||||
5. Инициализировать и применить по порядку. В `storage` бакет из шага 3 сначала принимается под управление:
|
||||
|
||||
```bash
|
||||
mise run tf vault init && mise run tf vault apply
|
||||
mise run tf storage init
|
||||
mise run tf storage import minio_s3_bucket.state_terraform_s3 <state_bucket>
|
||||
mise run tf storage apply
|
||||
mise run tf database init && mise run tf database apply
|
||||
```
|
||||
|
||||
6. Заменить root-токен на токен Terraform и отозвать root:
|
||||
|
||||
```bash
|
||||
ROOT_TOKEN=<root-токен> mise run vault:bootstrap --revoke-root
|
||||
```
|
||||
|
||||
7. Убедиться, что всё на месте: `mise run plan` должен трижды показать `No changes`, `mise run vault:app-check` — пройти.
|
||||
|
||||
Два проекта из одного шаблона могут работать на одной машине одновременно, если у них разные порты и разные имена каталогов: имя каталога становится именем проекта Compose и его сети.
|
||||
|
||||
## Неочевидные места в коде
|
||||
|
||||
В коде нет комментариев, поэтому причины решений, которые не видны из самого кода, собраны здесь.
|
||||
|
||||
- **`depends_on` между грантами в `modules/app-database`.** Гранты `public` и `owner` меняют права одной и той же базы; без явного порядка Terraform выполнял бы их параллельно, и Postgres мог бы вернуть `tuple concurrently updated`.
|
||||
- **Грант `owner`.** После отзыва прав у `public` владелец базы не может подключиться к ней без явного `CONNECT`.
|
||||
- **`ignore_changes = [roles]` у роли `vault` в `database/main.tf`.** Членством управляет `postgresql_grant_role`; без этой строки два ресурса по очереди переписывают список ролей.
|
||||
- **`with_admin_option` там же.** Начиная с Postgres 16 роль с `CREATEROLE` добавляет участников только в те роли, где у неё есть `ADMIN OPTION`.
|
||||
- **`ALTER ROLE ... SET role` в `creation_statements`.** Временная роль работает от имени роли сервиса, поэтому созданные ею объекты принадлежат сервису, а саму её можно удалить без ошибок.
|
||||
- **`delete_all_versions` у секретов.** Без него удалённый сервис оставлял бы в Vault восстановимые версии своих ключей.
|
||||
- **`prevent_destroy` у бакета со state.** В нём лежит state той самой конфигурации, которая им управляет.
|
||||
- **Политика `terraform` в `vault/main.tf`.** Каждый путь взят из записи реальных запросов провайдера (`TF_LOG=DEBUG`) при добавлении и удалении сервиса. Создание и удаление mount, метода входа и самой политики в неё не входят намеренно.
|
||||
- **`host` и `internal_host` в `config.yaml`.** Первый — адрес с хоста, им пользуется Terraform; второй — адрес внутри сети Compose, он попадает в секреты сервисов и в настройки Vault.
|
||||
- **`include` в `docker-compose.yml`.** Только так Compose читает второй env-файл для подстановки переменных; сам он загружает один `.env`.
|
||||
- **`chown` в задаче `up`.** Docker создаёт каталоги данных от имени root, а Vault и pgAdmin работают под своими пользователями.
|
||||
- **`mise-env.sh`.** mise собирает окружение раньше, чем выполняет зависимости задач, поэтому сгенерированные файлы проверяются в момент загрузки окружения, а не отдельной задачей.
|
||||
- **`#MISE` в начале задач** — не комментарии, а директивы mise с описанием задачи.
|
||||
|
||||
## Шпаргалка
|
||||
|
||||
Команды выполняются из `infra/`. Первые две строки загружают в shell те же переменные, что получают задачи mise.
|
||||
|
||||
```bash
|
||||
set -a; source .env; source .env.config; set +a
|
||||
MC="docker compose exec -e MC_HOST_local=http://${SILO_USER}:${SILO_PASSWORD}@localhost:9000 object-storage mc"
|
||||
BAO="docker compose exec -e BAO_TOKEN=$VAULT_TOKEN vault bao"
|
||||
|
||||
# Silo
|
||||
$MC ls local # бакеты
|
||||
$MC version info local/<бакет> # включён ли versioning
|
||||
$MC ls --versions local/<бакет> # объекты со всеми версиями
|
||||
$MC ilm rule ls local/<бакет> # lifecycle-правила
|
||||
$MC admin user ls local # пользователи
|
||||
$MC admin policy ls local # политики
|
||||
|
||||
# Vault
|
||||
docker compose exec vault bao status # запечатан или нет
|
||||
$BAO kv get -mount=secrets <сервис>/s3 # секрет сервиса
|
||||
$BAO policy read terraform # политика токена Terraform
|
||||
$BAO token lookup # срок жизни токена
|
||||
|
||||
# Terraform
|
||||
mise run tf storage state list # чем управляет конфигурация
|
||||
mise run tf storage state show <адрес> # атрибуты одного ресурса
|
||||
mise run tf storage plan -refresh-only # только дрейф, без изменений из кода
|
||||
```
|
||||
|
||||
Как обращаться со state:
|
||||
|
||||
- **Изменение руками** (дрейф) видно в `plan` и откатывается следующим `apply`.
|
||||
- **Объект, созданный руками**, берётся под управление блоком `import { to = ..., id = ... }` рядом с описанием ресурса; после `apply` блок можно удалить.
|
||||
- **Блок `moved`** переименовывает ресурс в state без пересоздания.
|
||||
- **State содержит секреты в открытом виде**, поэтому он лежит в бакете, а не в git.
|
||||
|
||||
## Ссылки
|
||||
|
||||
- Язык Terraform: https://developer.hashicorp.com/terraform/language
|
||||
- Backend S3: https://developer.hashicorp.com/terraform/language/backend/s3
|
||||
- Провайдер MinIO: https://registry.terraform.io/providers/aminueza/minio/latest/docs
|
||||
- Провайдер PostgreSQL: https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs
|
||||
- Провайдер Vault: https://registry.terraform.io/providers/hashicorp/vault/latest/docs
|
||||
- OpenBao: https://openbao.org/docs/
|
||||
- mise: https://mise.jdx.dev/tasks/
|
||||
|
||||
## Ограничения
|
||||
|
||||
- Всё рассчитано на одну локальную машину: TLS выключен, порты привязаны к `127.0.0.1`, у Vault один unseal-ключ.
|
||||
- У state нет блокировки. Два одновременных `apply` в одном каталоге могут его испортить.
|
||||
- Секреты сервисов и пароль роли `vault` в Postgres лежат в state в открытом виде. Доступ к бакету со state равен доступу ко всем секретам.
|
||||
- После смены порта Silo в `config.yaml` нужен `mise run tf <каталог> init -reconfigure` в каждом каталоге: адрес state запоминается при `init`.
|
||||
@@ -0,0 +1,71 @@
|
||||
services:
|
||||
postgres:
|
||||
image: ${POSTGRES_IMAGE}
|
||||
restart: always
|
||||
ports:
|
||||
- "127.0.0.1:${POSTGRES_PORT}:5432"
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
PGDATA: /var/lib/postgresql/data/pgdata
|
||||
volumes:
|
||||
- ./data/postgres:/var/lib/postgresql/data/pgdata
|
||||
networks:
|
||||
- infra
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
pgadmin:
|
||||
image: ${PGADMIN_IMAGE}
|
||||
restart: always
|
||||
ports:
|
||||
- "127.0.0.1:${PGADMIN_PORT}:80"
|
||||
environment:
|
||||
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL}
|
||||
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD}
|
||||
PGADMIN_CONFIG_SERVER_MODE: 'False'
|
||||
volumes:
|
||||
- ./data/pgadmin:/var/lib/pgadmin
|
||||
networks:
|
||||
- infra
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
object-storage:
|
||||
image: ${SILO_IMAGE}
|
||||
restart: always
|
||||
ports:
|
||||
- "127.0.0.1:${SILO_STORAGE_PORT}:9000"
|
||||
- "127.0.0.1:${SILO_WEB_PORT}:9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${SILO_USER}
|
||||
MINIO_ROOT_PASSWORD: ${SILO_PASSWORD}
|
||||
command: server /data --console-address ":9001"
|
||||
volumes:
|
||||
- ./data/s3:/data
|
||||
networks:
|
||||
- infra
|
||||
|
||||
vault:
|
||||
image: ${VAULT_IMAGE}
|
||||
restart: always
|
||||
command: server
|
||||
cap_add:
|
||||
- IPC_LOCK
|
||||
ports:
|
||||
- "127.0.0.1:${VAULT_PORT}:8200"
|
||||
environment:
|
||||
BAO_ADDR: http://127.0.0.1:8200
|
||||
volumes:
|
||||
- ./config/vault:/openbao/config:ro
|
||||
- ./data/vault:/openbao/data
|
||||
networks:
|
||||
- infra
|
||||
|
||||
networks:
|
||||
infra:
|
||||
driver: bridge
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
infra:
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg18
|
||||
admin_user: admin
|
||||
host: localhost
|
||||
port: 5455
|
||||
internal_host: postgres
|
||||
internal_port: 5432
|
||||
pgadmin:
|
||||
image: dpage/pgadmin4:9.18.0
|
||||
port: 5050
|
||||
email: admin@example.com
|
||||
silo:
|
||||
image: pgsty/silo:RELEASE.2026-09-16T00-00-00Z
|
||||
admin_user: admin
|
||||
host: localhost
|
||||
port: 9000
|
||||
console_port: 9001
|
||||
internal_host: object-storage
|
||||
internal_port: 9000
|
||||
state_bucket: terraform-state
|
||||
backup_bucket: infra-backups
|
||||
vault:
|
||||
image: openbao/openbao:2.7
|
||||
host: 127.0.0.1
|
||||
port: 8200
|
||||
mount: secrets
|
||||
database_mount: database
|
||||
|
||||
services:
|
||||
app:
|
||||
postgres:
|
||||
username: app
|
||||
name: app
|
||||
pgvector: true
|
||||
buckets:
|
||||
app-files:
|
||||
versioning: true
|
||||
noncurrent_days: 30
|
||||
worker:
|
||||
buckets:
|
||||
worker-tmp:
|
||||
versioning: false
|
||||
@@ -0,0 +1,14 @@
|
||||
storage "raft" {
|
||||
path = "/openbao/data"
|
||||
node_id = "node1"
|
||||
}
|
||||
|
||||
listener "tcp" {
|
||||
address = "0.0.0.0:8200"
|
||||
tls_disable = true
|
||||
}
|
||||
|
||||
api_addr = "http://127.0.0.1:8200"
|
||||
cluster_addr = "http://127.0.0.1:8201"
|
||||
ui = true
|
||||
disable_mlock = true
|
||||
Generated
+65
@@ -0,0 +1,65 @@
|
||||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/cyrilgdn/postgresql" {
|
||||
version = "1.27.0"
|
||||
constraints = "1.27.0"
|
||||
hashes = [
|
||||
"h1:OLbc15TOdKw2afn+M5H8MjQQRbYbEpiWDSj7yHg6iEw=",
|
||||
"zh:0d6fa6f12393f19b95652932c1a53984096bca2dfe27ce8c50f2f7881bae1b75",
|
||||
"zh:1c962ce9e73d96b368354607fd30dc55313c39f03f7ca5f3dea64e143b4f7c06",
|
||||
"zh:289b4a93b310d698914025bd0990f5c7e1eef21ce1db171ae3bd2970e69ea302",
|
||||
"zh:36960ffa44073d0fd71ed37d0d5e65e981d9ac009e69791c874a8903f06d7cfc",
|
||||
"zh:3936aa067c45bf8c2867bd5753f918c64b4d4214d29c999422343a8848ef25df",
|
||||
"zh:69c15b2dfa6e013fd9317a6edb3e5fa85f5bd32d581174ff8c6ec9ebbb88a31e",
|
||||
"zh:74b990f51f92035a6a5cd5f1ba8a427c5781bd5d2531f5dffb704325364459a2",
|
||||
"zh:91d45dbd0593a1f53c4cf4198a999545961937893cf16e2347b4e64bccbb626a",
|
||||
"zh:a50ed48b61d1745f9c185174a56c849c88413ea1adc7409d07947853468d8663",
|
||||
"zh:bc7cbe107bfcf9b57ba3f2ef5e64b205117d09b50528834250772b8c548fdfe3",
|
||||
"zh:c91ee6aa986bb89f69737354ea422292d39fd18410da92ba951e1203fbd9c3ef",
|
||||
"zh:f8cfdb72240d759d7a20f05cd098edbdd44f3b97a6067d7ea2ce29910356d27a",
|
||||
"zh:fa08178b78d23158945483fbdadee90aab7a741089600de27ec19b457d8c6928",
|
||||
]
|
||||
}
|
||||
|
||||
provider "registry.terraform.io/hashicorp/random" {
|
||||
version = "3.9.1"
|
||||
constraints = "3.9.1"
|
||||
hashes = [
|
||||
"h1:g40qr7yDmIpaur4SsK5BcOda3HSo1RJ6zHVMqN4EJ+0=",
|
||||
"zh:05f4734c1f0be840b711b3eff259ebc5fca436784c728955b1678078466f48d7",
|
||||
"zh:0b91bf19371d012434eba1deeb6aab77158def9b39601dcbd94450b3974a2a26",
|
||||
"zh:0ee6eacd47ec00183d55d726a4b6c4ce951a199f944bf22f1aa58392ebdfa7a2",
|
||||
"zh:19388a4074b76a89a43a6c8328d7ae8ee2e7de3d346af51e80d3e6d3d12925f1",
|
||||
"zh:23e74d48c5e2ac2e823fd527f49fee9db37d32a1990c9e3bf126ead697b843eb",
|
||||
"zh:3cabf7fbd096c520064aae3aba61aba670af83ab91291a71fa1b1332929c2b7f",
|
||||
"zh:5c0a3b8af0be60be4eca12ddee385cfa8babc1ec8e98cdf9de2f2274c73eabfa",
|
||||
"zh:60b4f8a8ef18f52bf8e19215229dae408bee732825964092db7c989fd2de4097",
|
||||
"zh:7359015acfedcbd6366f2329c854cf8d3c8ca5cd0faa89d2d37db358d6eba6c5",
|
||||
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
||||
"zh:7b38758402f0e13a1071162da28994023cd2ac676e54af350c9ffd8dfa73fa7b",
|
||||
"zh:7c7fbb8895eb75bb4de1f933e98553bd99c8d048c89a925ddba490aa5a67f7dc",
|
||||
"zh:8c2b8c6a7ccdec16b73e2fb9f3700ea097f58c592571e4c5de60c93d2301732c",
|
||||
]
|
||||
}
|
||||
|
||||
provider "registry.terraform.io/hashicorp/vault" {
|
||||
version = "5.12.0"
|
||||
constraints = "5.12.0"
|
||||
hashes = [
|
||||
"h1:NXnrOFre4t8nj7+ZQ3MKIECpDohjMBtHWm2aen75nfU=",
|
||||
"zh:0683ab1870c6ffe78463465fc0954ff18fbbfe16d79dc121e20efb27cd2be850",
|
||||
"zh:0ade50207774b544995066be02cf7e108abe828e089eb646da4c04863cbee702",
|
||||
"zh:2b3360bec8d457a7d19df86bf4576711035785928a9fa307affce10cbeae89fd",
|
||||
"zh:5317888b0e3547341b8434f42bbb8df2d30bdd95cc1bde9a4c7a80d266fe9f6b",
|
||||
"zh:6c0271736ceb08774bebedf424735d92c4fe33b8e4db371cc027cbded0439a2e",
|
||||
"zh:70bb990d22d1ce1ceac2209aee025e0f7deab48d95002aa2b8360d9c86343ea8",
|
||||
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
||||
"zh:7f33d7bbf145e15542cfc6b1dbeed639d08204802c239c098770c5f507d1e59c",
|
||||
"zh:8ae8ca31d3c9e4b530c80eed12d8debe6fc46b407acb469b33247227aaafbf51",
|
||||
"zh:c61fcc4d714cefee0696e2f8bf715b5826099e405f66acc859e496de688b6a8e",
|
||||
"zh:ceca8db980ae3bd986142903487d75d143edfc5c7cf467da38f3698f717db046",
|
||||
"zh:e1bba3cb6a728e9bf2951139f1e7bae5bec37a37cd4bfebe282e306212a62c4c",
|
||||
"zh:f9765b4aa90e9e4eae03626ebfdfeda54954ec2552267aedce1fb2643f6eb2de",
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
locals {
|
||||
config = yamldecode(file("${path.root}/../config.yaml"))
|
||||
}
|
||||
|
||||
module "config" {
|
||||
source = "../modules/config"
|
||||
infra = local.config.infra
|
||||
services = local.config.services
|
||||
}
|
||||
|
||||
locals {
|
||||
databases = { for name, svc in module.config.services : name => svc.postgres if svc.postgres != null }
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
module "app_database" {
|
||||
source = "../modules/app-database"
|
||||
for_each = local.databases
|
||||
service = { name = each.key, database = each.value }
|
||||
}
|
||||
|
||||
resource "vault_kv_secret_v2" "postgres" {
|
||||
for_each = local.databases
|
||||
mount = module.config.vault.mount
|
||||
name = "${each.key}/postgres"
|
||||
delete_all_versions = true
|
||||
data_json = jsonencode(merge(module.app_database[each.key].credentials, {
|
||||
host = module.config.postgres.internal_host
|
||||
port = module.config.postgres.internal_port
|
||||
sslmode = each.value.sslmode
|
||||
}))
|
||||
}
|
||||
|
||||
resource "random_password" "vault" {
|
||||
length = 32
|
||||
special = false
|
||||
}
|
||||
|
||||
resource "postgresql_role" "vault" {
|
||||
name = "vault"
|
||||
login = true
|
||||
create_role = true
|
||||
password = random_password.vault.result
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [roles]
|
||||
}
|
||||
}
|
||||
|
||||
resource "postgresql_grant_role" "vault" {
|
||||
for_each = local.databases
|
||||
role = postgresql_role.vault.name
|
||||
grant_role = each.value.username
|
||||
with_admin_option = true
|
||||
|
||||
depends_on = [module.app_database]
|
||||
}
|
||||
|
||||
resource "vault_database_secrets_mount" "postgres" {
|
||||
path = module.config.vault.database_mount
|
||||
|
||||
postgresql {
|
||||
name = "postgres"
|
||||
connection_url = "postgresql://{{username}}:{{password}}@${module.config.postgres.internal_host}:${module.config.postgres.internal_port}/postgres?sslmode=disable"
|
||||
username = postgresql_role.vault.name
|
||||
password = random_password.vault.result
|
||||
allowed_roles = keys(local.databases)
|
||||
}
|
||||
}
|
||||
|
||||
resource "vault_database_secret_backend_role" "service" {
|
||||
for_each = local.databases
|
||||
backend = vault_database_secrets_mount.postgres.path
|
||||
name = each.key
|
||||
db_name = vault_database_secrets_mount.postgres.postgresql[0].name
|
||||
|
||||
creation_statements = [
|
||||
"CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}' IN ROLE \"${each.value.username}\";",
|
||||
"ALTER ROLE \"{{name}}\" SET role = \"${each.value.username}\";",
|
||||
]
|
||||
revocation_statements = [
|
||||
"DROP ROLE IF EXISTS \"{{name}}\";",
|
||||
]
|
||||
default_ttl = 3600
|
||||
max_ttl = 86400
|
||||
|
||||
depends_on = [postgresql_grant_role.vault]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
output "vault_secrets" {
|
||||
value = { for name, secret in vault_kv_secret_v2.postgres : name => secret.path }
|
||||
description = "vault paths of the postgres credentials of the services"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
provider "vault" {
|
||||
address = module.config.vault.address
|
||||
token = var.vault_token
|
||||
}
|
||||
|
||||
provider "postgresql" {
|
||||
host = module.config.postgres.host
|
||||
port = module.config.postgres.port
|
||||
username = module.config.postgres.admin_user
|
||||
password = var.postgres_password
|
||||
sslmode = "disable"
|
||||
connect_timeout = 15
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
variable "postgres_password" {
|
||||
description = "password of the postgres administrator"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "vault_token" {
|
||||
description = "vault token"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
postgresql = {
|
||||
source = "cyrilgdn/postgresql"
|
||||
version = "1.27.0"
|
||||
}
|
||||
random = {
|
||||
source = "hashicorp/random"
|
||||
version = "3.9.1"
|
||||
}
|
||||
vault = {
|
||||
source = "hashicorp/vault"
|
||||
version = "5.12.0"
|
||||
}
|
||||
}
|
||||
backend "s3" {
|
||||
key = "database/terraform.tfstate"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
include:
|
||||
- path: compose.services.yml
|
||||
env_file:
|
||||
- .env
|
||||
- .env.config
|
||||
@@ -0,0 +1,11 @@
|
||||
FROM golang:1.27-alpine AS build
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -o /service .
|
||||
|
||||
FROM alpine:3
|
||||
COPY --from=build /service /service
|
||||
USER nobody
|
||||
ENTRYPOINT ["/service"]
|
||||
@@ -0,0 +1,22 @@
|
||||
name: infra-example
|
||||
|
||||
services:
|
||||
example:
|
||||
build: .
|
||||
environment:
|
||||
VAULT_ADDR: http://vault:8200
|
||||
VAULT_ROLE_ID: ${VAULT_ROLE_ID:-}
|
||||
VAULT_SECRET_ID: ${VAULT_SECRET_ID:-}
|
||||
VAULT_KV_MOUNT: ${VAULT_MOUNT:-secrets}
|
||||
VAULT_DB_MOUNT: ${VAULT_DATABASE_MOUNT:-database}
|
||||
SERVICE_NAME: ${SERVICE_NAME:-}
|
||||
POSTGRES_CREDS: ${POSTGRES_CREDS:-static}
|
||||
ports:
|
||||
- "127.0.0.1:${EXAMPLE_PORT:-8090}:8080"
|
||||
networks:
|
||||
- infra
|
||||
|
||||
networks:
|
||||
infra:
|
||||
external: true
|
||||
name: ${INFRA_NETWORK:-infra_infra}
|
||||
@@ -0,0 +1,50 @@
|
||||
module example/go-service
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/hashicorp/vault/api v1.23.0
|
||||
github.com/hashicorp/vault/api/auth/approle v0.12.0
|
||||
github.com/jackc/pgx/v5 v5.11.0
|
||||
github.com/minio/minio-go/v7 v7.3.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
|
||||
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
|
||||
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/klauspost/compress v1.19.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
|
||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
github.com/zeebo/xxh3 v1.1.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
golang.org/x/crypto v0.55.0 // indirect
|
||||
golang.org/x/net v0.58.0 // indirect
|
||||
golang.org/x/sync v0.22.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.3 // indirect
|
||||
)
|
||||
@@ -0,0 +1,116 @@
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI=
|
||||
github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA=
|
||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0=
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
|
||||
github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=
|
||||
github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=
|
||||
github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I=
|
||||
github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
|
||||
github.com/hashicorp/vault/api v1.23.0 h1:gXgluBsSECfRWTSW9niY2jwg2e9mMJc4WoHNv4g3h6A=
|
||||
github.com/hashicorp/vault/api v1.23.0/go.mod h1:zransKiB9ftp+kgY8ydjnvCU7Wk8i9L0DYWpXeMj9ko=
|
||||
github.com/hashicorp/vault/api/auth/approle v0.12.0 h1:PhF7jrQjydK1DC05EboosXmZg31GDUIKL8bjyilsJ+E=
|
||||
github.com/hashicorp/vault/api/auth/approle v0.12.0/go.mod h1:J7BJLpXeQXhuMAWi31Puunu5QOeCoRAgLh2iDti7OLA=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg=
|
||||
github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
|
||||
github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
|
||||
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
|
||||
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
|
||||
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.3.0 h1:HM4pFCSQq/TK+j0/zmorSh5ddh81iDgRgU0BG0Vz/YU=
|
||||
github.com/minio/minio-go/v7 v7.3.0/go.mod h1:KUPWdecEO1LWyUz+sTGXAuf2jZHrPh5fCsRH86QbPfk=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
|
||||
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
|
||||
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
||||
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.67.3 h1:iM9Lhz5MRSGhHVGGwCuzG9KO8PoirCXj/m/qTmOJJQw=
|
||||
gopkg.in/ini.v1 v1.67.3/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,134 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
vault "github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/api/auth/approle"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
service := mustEnv("SERVICE_NAME")
|
||||
kvMount := mustEnv("VAULT_KV_MOUNT")
|
||||
|
||||
client, err := vault.NewClient(vault.DefaultConfig())
|
||||
check(err, "клиент Vault")
|
||||
auth, err := approle.NewAppRoleAuth(mustEnv("VAULT_ROLE_ID"), &approle.SecretID{FromEnv: "VAULT_SECRET_ID"})
|
||||
check(err, "AppRole")
|
||||
login, err := client.Auth().Login(ctx, auth)
|
||||
check(err, "вход в Vault")
|
||||
log.Printf("vault: вошёл, политики %v, токен на %dс", login.Auth.Policies, login.Auth.LeaseDuration)
|
||||
|
||||
kv := client.KVv2(kvMount)
|
||||
s3Secret, err := kv.Get(ctx, service+"/s3")
|
||||
check(err, "секрет s3")
|
||||
pgSecret, err := kv.Get(ctx, service+"/postgres")
|
||||
check(err, "секрет postgres")
|
||||
|
||||
pgUser, pgPassword := str(pgSecret.Data, "username"), str(pgSecret.Data, "password")
|
||||
if os.Getenv("POSTGRES_CREDS") == "dynamic" {
|
||||
creds, err := client.Logical().ReadWithContext(ctx, mustEnv("VAULT_DB_MOUNT")+"/creds/"+service)
|
||||
check(err, "временные учётные данные")
|
||||
pgUser, pgPassword = str(creds.Data, "username"), str(creds.Data, "password")
|
||||
log.Printf("postgres: временная роль %s на %dс", pgUser, creds.LeaseDuration)
|
||||
}
|
||||
dsn := url.URL{
|
||||
Scheme: "postgres",
|
||||
User: url.UserPassword(pgUser, pgPassword),
|
||||
Host: fmt.Sprintf("%s:%s", str(pgSecret.Data, "host"), str(pgSecret.Data, "port")),
|
||||
Path: str(pgSecret.Data, "database"),
|
||||
RawQuery: "sslmode=" + str(pgSecret.Data, "sslmode"),
|
||||
}
|
||||
db, err := pgxpool.New(ctx, dsn.String())
|
||||
check(err, "пул postgres")
|
||||
defer db.Close()
|
||||
check(db.Ping(ctx), "подключение к postgres")
|
||||
log.Printf("postgres: подключился к %s как %s", dsn.Path, pgUser)
|
||||
|
||||
endpoint, err := url.Parse(str(s3Secret.Data, "endpoint"))
|
||||
check(err, "endpoint s3")
|
||||
s3, err := minio.New(endpoint.Host, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(str(s3Secret.Data, "access_key"), str(s3Secret.Data, "secret_key"), ""),
|
||||
Secure: endpoint.Scheme == "https",
|
||||
})
|
||||
check(err, "клиент s3")
|
||||
buckets := strings.Split(str(s3Secret.Data, "buckets"), ",")
|
||||
log.Printf("s3: бакеты %v на %s", buckets, endpoint.Host)
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
status := map[string]any{"service": service}
|
||||
|
||||
var sessionUser, currentUser, database string
|
||||
err := db.QueryRow(ctx, "select session_user, current_user, current_database()").
|
||||
Scan(&sessionUser, ¤tUser, &database)
|
||||
if err != nil {
|
||||
status["postgres"] = map[string]string{"error": err.Error()}
|
||||
} else {
|
||||
status["postgres"] = map[string]string{
|
||||
"database": database, "session_user": sessionUser, "current_user": currentUser,
|
||||
}
|
||||
}
|
||||
|
||||
objects := map[string]any{}
|
||||
for _, bucket := range buckets {
|
||||
count := 0
|
||||
var listErr error
|
||||
for object := range s3.ListObjects(ctx, bucket, minio.ListObjectsOptions{Recursive: true}) {
|
||||
if object.Err != nil {
|
||||
listErr = object.Err
|
||||
break
|
||||
}
|
||||
count++
|
||||
}
|
||||
if listErr != nil {
|
||||
objects[bucket] = map[string]string{"error": listErr.Error()}
|
||||
} else {
|
||||
objects[bucket] = map[string]int{"objects": count}
|
||||
}
|
||||
}
|
||||
status["s3"] = objects
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(status)
|
||||
})
|
||||
|
||||
log.Print("http: слушаю :8080")
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
|
||||
func mustEnv(name string) string {
|
||||
value := os.Getenv(name)
|
||||
if value == "" {
|
||||
log.Fatalf("не задана переменная окружения %s", name)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func check(err error, what string) {
|
||||
if err != nil {
|
||||
log.Fatalf("%s: %v", what, err)
|
||||
}
|
||||
}
|
||||
|
||||
func str(data map[string]any, key string) string {
|
||||
value, ok := data[key]
|
||||
if !ok {
|
||||
log.Fatalf("в секрете нет поля %s", key)
|
||||
}
|
||||
return fmt.Sprint(value)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
root=$(dirname "${BASH_SOURCE[0]}")
|
||||
|
||||
if [[ ! -f $root/.env.config || ! -f $root/backend.hcl || $root/config.yaml -nt $root/.env.config ]]; then
|
||||
"$root/mise-tasks/render" >&2
|
||||
fi
|
||||
|
||||
set -a
|
||||
. "$root/.env.config"
|
||||
set +a
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Запустить пример Go-сервиса: mise run example:up [сервис] [static|dynamic]"
|
||||
set -euo pipefail
|
||||
|
||||
roles=$(terraform -chdir=vault output -json approle_role_ids)
|
||||
service=${1:-$(jq -r 'keys[0]' <<<"$roles")}
|
||||
creds=${2:-static}
|
||||
compose=(docker compose -f examples/go-service/compose.yml)
|
||||
url=http://127.0.0.1:${EXAMPLE_PORT:-8090}/
|
||||
|
||||
role_id=$(jq -er --arg s "$service" '.[$s]' <<<"$roles") ||
|
||||
{ echo "нет роли AppRole для сервиса $service" >&2; exit 1; }
|
||||
secret_id=$(curl -sS -X POST -H "X-Vault-Token: $VAULT_TOKEN" -d '{"ttl": "5m", "num_uses": 1}' \
|
||||
"$VAULT_ADDR/v1/auth/approle/role/$service/secret-id" | jq -er .data.secret_id)
|
||||
|
||||
export SERVICE_NAME=$service VAULT_ROLE_ID=$role_id VAULT_SECRET_ID=$secret_id POSTGRES_CREDS=$creds
|
||||
INFRA_NETWORK=$(docker compose config --format json | jq -er .networks.infra.name)
|
||||
export INFRA_NETWORK
|
||||
"${compose[@]}" up -d --build --force-recreate --quiet-pull 2>&1 | tail -3
|
||||
|
||||
for _ in $(seq 1 30); do
|
||||
if response=$(curl -fsS "$url" 2>/dev/null); then
|
||||
"${compose[@]}" logs --no-log-prefix example
|
||||
printf '\nGET %s\n' "$url"
|
||||
jq . <<<"$response"
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
"${compose[@]}" logs --no-log-prefix example
|
||||
echo "сервис не ответил на $url" >&2
|
||||
exit 1
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Сгенерировать .env.config и backend.hcl из config.yaml"
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
INFRA=$(echo 'jsonencode(yamldecode(file("config.yaml")).infra)' | terraform console | jq -r .)
|
||||
get() { jq -er "$1" <<<"$INFRA" || { echo "в config.yaml нет infra$1" >&2; exit 1; }; }
|
||||
|
||||
cat >.env.config.tmp <<ENV
|
||||
# Сгенерировано задачей mise run render из config.yaml, не редактировать
|
||||
POSTGRES_IMAGE=$(get .postgres.image)
|
||||
POSTGRES_USER=$(get .postgres.admin_user)
|
||||
POSTGRES_PORT=$(get .postgres.port)
|
||||
PGADMIN_IMAGE=$(get .pgadmin.image)
|
||||
PGADMIN_PORT=$(get .pgadmin.port)
|
||||
PGADMIN_EMAIL=$(get .pgadmin.email)
|
||||
SILO_IMAGE=$(get .silo.image)
|
||||
SILO_USER=$(get .silo.admin_user)
|
||||
SILO_STORAGE_PORT=$(get .silo.port)
|
||||
SILO_WEB_PORT=$(get .silo.console_port)
|
||||
VAULT_IMAGE=$(get .vault.image)
|
||||
VAULT_PORT=$(get .vault.port)
|
||||
VAULT_ADDR=http://$(get .vault.host):$(get .vault.port)
|
||||
VAULT_MOUNT=$(get .vault.mount)
|
||||
VAULT_DATABASE_MOUNT=$(get .vault.database_mount)
|
||||
BACKUP_BUCKET=$(get .silo.backup_bucket)
|
||||
ENV
|
||||
|
||||
cat >backend.hcl.tmp <<HCL
|
||||
# Сгенерировано задачей mise run render из config.yaml, не редактировать
|
||||
bucket = "$(get .silo.state_bucket)"
|
||||
region = "us-east-1"
|
||||
endpoints = {
|
||||
s3 = "http://$(get .silo.host):$(get .silo.port)"
|
||||
}
|
||||
use_path_style = true
|
||||
skip_credentials_validation = true
|
||||
skip_region_validation = true
|
||||
skip_requesting_account_id = true
|
||||
skip_metadata_api_check = true
|
||||
skip_s3_checksum = true
|
||||
HCL
|
||||
|
||||
mv .env.config.tmp .env.config
|
||||
mv backend.hcl.tmp backend.hcl
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Terraform с окружением из .env и config.yaml: mise run tf storage plan"
|
||||
set -euo pipefail
|
||||
|
||||
[[ $# -ge 2 ]] || {
|
||||
echo "использование: mise run tf <vault|storage|database> <команда terraform> [аргументы]" >&2
|
||||
exit 1
|
||||
}
|
||||
dir=$1
|
||||
shift
|
||||
|
||||
if [[ $1 == init ]]; then
|
||||
shift
|
||||
exec terraform -chdir="$dir" init -backend-config=../backend.hcl "$@"
|
||||
fi
|
||||
exec terraform -chdir="$dir" "$@"
|
||||
Executable
+136
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Проверка глазами сервиса: вход по AppRole, секреты из Vault, доступ к S3 и Postgres"
|
||||
set -euo pipefail
|
||||
|
||||
KV=$VAULT_MOUNT
|
||||
DB=$VAULT_DATABASE_MOUNT
|
||||
|
||||
step() { printf '\n== %s\n' "$*"; }
|
||||
ok() { printf ' ok: %s\n' "$*"; }
|
||||
fail() {
|
||||
printf ' ОШИБКА: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
vault_api() {
|
||||
local method=$1 path=$2 token=${3:-} body=${4:-}
|
||||
curl -sS -X "$method" -w '\n%{http_code}' \
|
||||
${token:+-H "X-Vault-Token: $token"} \
|
||||
${body:+-d "$body"} \
|
||||
"$VAULT_ADDR/v1/$path"
|
||||
}
|
||||
code() { tail -n1 <<<"$1"; }
|
||||
body() { sed '$d' <<<"$1"; }
|
||||
|
||||
psql_as() {
|
||||
docker compose exec -T -e PGPASSWORD="$2" postgres \
|
||||
psql -h postgres -U "$1" -d "$3" -Atc "select session_user || ' работает как ' || current_user" 2>&1 | tail -1 || true
|
||||
}
|
||||
|
||||
token=""
|
||||
revoke_token() {
|
||||
[[ -z "$token" ]] || vault_api POST auth/token/revoke-self "$token" >/dev/null 2>&1 || true
|
||||
token=""
|
||||
}
|
||||
trap revoke_token EXIT
|
||||
|
||||
admin_token=$VAULT_TOKEN
|
||||
ROLES=$(terraform -chdir=vault output -json approle_role_ids)
|
||||
mapfile -t ALL < <(jq -r 'keys[]' <<<"$ROLES")
|
||||
if [[ $# -gt 0 ]]; then SERVICES=("$@"); else SERVICES=("${ALL[@]}"); fi
|
||||
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
printf '\n################ %s\n' "$svc"
|
||||
|
||||
role_id=$(jq -r --arg s "$svc" '.[$s] // empty' <<<"$ROLES")
|
||||
[[ -n "$role_id" ]] || fail "нет роли AppRole для $svc"
|
||||
secret_id=$(vault_api POST "auth/approle/role/$svc/secret-id" "$admin_token" '{"ttl": "5m", "num_uses": 1}' |
|
||||
sed '$d' | jq -r .data.secret_id)
|
||||
|
||||
step "1. Вход через AppRole"
|
||||
r=$(vault_api POST auth/approle/login "" "$(jq -n --arg r "$role_id" --arg s "$secret_id" '{role_id: $r, secret_id: $s}')")
|
||||
[[ $(code "$r") == 200 ]] || fail "вход не удался: $(body "$r")"
|
||||
token=$(body "$r" | jq -r .auth.client_token)
|
||||
ok "токен получен, политики: $(body "$r" | jq -r '.auth.policies | join(", ")'), ttl: $(body "$r" | jq -r .auth.lease_duration)s"
|
||||
|
||||
step "2. Ключи S3 из $KV/$svc/s3"
|
||||
r=$(vault_api GET "$KV/data/$svc/s3" "$token")
|
||||
if [[ $(code "$r") == 200 ]]; then
|
||||
s3=$(body "$r" | jq .data.data)
|
||||
ok "поля: $(jq -r 'keys | join(", ")' <<<"$s3")"
|
||||
cfg=/tmp/app-check-$$
|
||||
docker compose exec -T object-storage mc --config-dir "$cfg" alias set app http://localhost:9000 \
|
||||
"$(jq -r .access_key <<<"$s3")" "$(jq -r .secret_key <<<"$s3")" >/dev/null
|
||||
IFS=, read -ra own <<<"$(jq -r .buckets <<<"$s3")"
|
||||
for b in "${own[@]}"; do
|
||||
docker compose exec -T object-storage mc --config-dir "$cfg" ls "app/$b" >/dev/null 2>&1 || fail "свой бакет $b недоступен"
|
||||
ok "бакет $b доступен"
|
||||
done
|
||||
docker compose exec -T object-storage rm -rf "$cfg"
|
||||
else
|
||||
ok "секрета нет (у сервиса нет бакетов), HTTP $(code "$r")"
|
||||
fi
|
||||
|
||||
step "3. Постоянные учётные данные Postgres из $KV/$svc/postgres"
|
||||
r=$(vault_api GET "$KV/data/$svc/postgres" "$token")
|
||||
own_db=""
|
||||
if [[ $(code "$r") == 200 ]]; then
|
||||
pg=$(body "$r" | jq .data.data)
|
||||
own_db=$(jq -r .database <<<"$pg")
|
||||
ok "поля: $(jq -r 'keys | join(", ")' <<<"$pg")"
|
||||
out=$(psql_as "$(jq -r .username <<<"$pg")" "$(jq -r .password <<<"$pg")" "$own_db")
|
||||
[[ "$out" == *"работает как"* ]] || fail "не подключился к $own_db: $out"
|
||||
ok "$own_db: $out"
|
||||
else
|
||||
ok "секрета нет (у сервиса нет базы), HTTP $(code "$r")"
|
||||
fi
|
||||
|
||||
step "4. Временные учётные данные Postgres из $DB/creds/$svc"
|
||||
if [[ -n "$own_db" ]]; then
|
||||
r=$(vault_api GET "$DB/creds/$svc" "$token")
|
||||
[[ $(code "$r") == 200 ]] || fail "Vault не выдал временную роль: $(body "$r")"
|
||||
dyn_user=$(body "$r" | jq -r .data.username)
|
||||
dyn_pass=$(body "$r" | jq -r .data.password)
|
||||
ok "выдана роль $dyn_user на $(body "$r" | jq -r .lease_duration)s"
|
||||
out=$(psql_as "$dyn_user" "$dyn_pass" "$own_db")
|
||||
[[ "$out" == *"работает как"* ]] || fail "временная роль не подключилась к $own_db: $out"
|
||||
ok "$own_db: $out"
|
||||
for other in $(docker compose exec -T postgres psql -U "$POSTGRES_USER" -d postgres -Atc \
|
||||
"select datname from pg_database where datdba <> 10 and datname <> '$own_db'"); do
|
||||
out=$(psql_as "$dyn_user" "$dyn_pass" "$other")
|
||||
[[ "$out" == *"CONNECT privilege"* ]] || fail "временная роль попала в чужую базу $other: $out"
|
||||
ok "$other: отказ, нет права CONNECT"
|
||||
done
|
||||
else
|
||||
ok "пропущено, у сервиса нет базы"
|
||||
fi
|
||||
|
||||
step "5. Чужие секреты"
|
||||
for path in "$KV/data/infra/postgres" "$KV/data/infra/silo"; do
|
||||
[[ $(code "$(vault_api GET "$path" "$token")") == 403 ]] || fail "сервис прочитал $path"
|
||||
ok "$path: 403"
|
||||
done
|
||||
for other in "${ALL[@]}"; do
|
||||
[[ "$other" == "$svc" ]] && continue
|
||||
for path in "$KV/data/$other/s3" "$KV/data/$other/postgres" "$DB/creds/$other"; do
|
||||
[[ $(code "$(vault_api GET "$path" "$token")") == 403 ]] || fail "сервис прочитал $path"
|
||||
ok "$path: 403"
|
||||
done
|
||||
done
|
||||
|
||||
step "6. Отзыв токена"
|
||||
old_token=$token
|
||||
revoke_token
|
||||
[[ $(code "$(vault_api GET "$KV/data/$svc/s3" "$old_token")") == 403 ]] || fail "токен всё ещё работает"
|
||||
ok "токен больше не принимается"
|
||||
if [[ -n "${dyn_user:-}" ]]; then
|
||||
sleep 2
|
||||
left=$(docker compose exec -T postgres psql -U "$POSTGRES_USER" -d postgres -Atc \
|
||||
"select count(*) from pg_roles where rolname = '$dyn_user'")
|
||||
[[ "$left" == 0 ]] || fail "временная роль $dyn_user осталась в Postgres"
|
||||
ok "временная роль $dyn_user удалена вместе с токеном"
|
||||
dyn_user=""
|
||||
fi
|
||||
done
|
||||
|
||||
printf '\nВсе проверки пройдены.\n'
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Снапшот Vault в бакет бэкапов"
|
||||
set -euo pipefail
|
||||
|
||||
BUCKET=$BACKUP_BUCKET
|
||||
|
||||
NAME="vault/$(date -u +%Y-%m-%dT%H-%M-%SZ).snap"
|
||||
TMP=/tmp/vault-backup.snap
|
||||
|
||||
docker compose exec -T -e BAO_TOKEN="$VAULT_TOKEN" vault \
|
||||
bao operator raft snapshot save "$TMP" >/dev/null
|
||||
|
||||
docker compose exec -T vault cat "$TMP" |
|
||||
docker compose exec -T -e MC_HOST_local="http://${SILO_USER}:${SILO_PASSWORD}@localhost:9000" \
|
||||
object-storage mc pipe --quiet "local/$BUCKET/$NAME" >/dev/null
|
||||
docker compose exec -T vault rm -f "$TMP"
|
||||
|
||||
docker compose exec -T -e MC_HOST_local="http://${SILO_USER}:${SILO_PASSWORD}@localhost:9000" \
|
||||
object-storage mc stat "local/$BUCKET/$NAME" | grep -E '^(Name|Size)'
|
||||
Executable
+108
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Токен Terraform для Vault, проверка политик, отзыв root-токена (нужен ROOT_TOKEN)"
|
||||
set -euo pipefail
|
||||
|
||||
MOUNT=$VAULT_MOUNT
|
||||
ENV_FILE=.env
|
||||
KEYS_FILE=vault_keys.txt
|
||||
REVOKE_ROOT=false
|
||||
[[ "${1:-}" == "--revoke-root" ]] && REVOKE_ROOT=true
|
||||
|
||||
bao_as() {
|
||||
local token=$1
|
||||
shift
|
||||
docker compose exec -T -e BAO_TOKEN="$token" vault bao "$@"
|
||||
}
|
||||
|
||||
step() { printf '\n== %s\n' "$*"; }
|
||||
ok() { printf ' ok: %s\n' "$*"; }
|
||||
fail() {
|
||||
printf ' ОШИБКА: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
ROOT_TOKEN=${ROOT_TOKEN:-$(sed -n 's/^Initial Root Token: *//p' "$KEYS_FILE" 2>/dev/null || true)}
|
||||
[[ -n "$ROOT_TOKEN" ]] || fail "нет root-токена: задайте ROOT_TOKEN или положите $KEYS_FILE"
|
||||
bao_as "$ROOT_TOKEN" token lookup >/dev/null 2>&1 || fail "root-токен не принят (отозван или Vault запечатан)"
|
||||
|
||||
step "Шаг 1. Токен для Terraform"
|
||||
TF_TOKEN=${VAULT_TOKEN:-}
|
||||
if [[ -n "$TF_TOKEN" ]] && bao_as "$TF_TOKEN" token lookup -format=json 2>/dev/null |
|
||||
jq -e '.data.policies | index("terraform")' >/dev/null; then
|
||||
ok "в $ENV_FILE уже есть рабочий токен, новый не создаю"
|
||||
else
|
||||
TF_TOKEN=$(bao_as "$ROOT_TOKEN" token create -orphan -policy=terraform -period=720h \
|
||||
-display-name=terraform -format=json | jq -r .auth.client_token)
|
||||
[[ -n "$TF_TOKEN" && "$TF_TOKEN" != null ]] || fail "токен не создан"
|
||||
sed -i '/^VAULT_TOKEN=/d' "$ENV_FILE"
|
||||
printf 'VAULT_TOKEN=%s\n' "$TF_TOKEN" >>"$ENV_FILE"
|
||||
ok "токен создан и записан в $ENV_FILE как VAULT_TOKEN"
|
||||
fi
|
||||
bao_as "$TF_TOKEN" token lookup -format=json |
|
||||
jq -r '.data | " политики: \(.policies | join(", ")); orphan: \(.orphan); period: \(.period)s"'
|
||||
|
||||
step "Шаг 2. terraform plan в vault/ с новым токеном"
|
||||
set +e
|
||||
TF_VAR_vault_token=$TF_TOKEN terraform -chdir=vault plan -input=false -no-color -detailed-exitcode >/tmp/vault-plan.$$ 2>&1
|
||||
PLAN_EXIT=$?
|
||||
set -e
|
||||
case $PLAN_EXIT in
|
||||
0) ok "No changes: политики terraform хватает" ;;
|
||||
2)
|
||||
grep -E '^ # |^Plan:' /tmp/vault-plan.$$ || true
|
||||
rm -f /tmp/vault-plan.$$
|
||||
fail "plan хочет что-то изменить, прочитайте его: terraform -chdir=vault plan"
|
||||
;;
|
||||
*)
|
||||
grep -vE 'Refreshing state' /tmp/vault-plan.$$ | tail -30
|
||||
rm -f /tmp/vault-plan.$$
|
||||
fail "plan упал, скорее всего в политике terraform не хватает пути из ошибки выше"
|
||||
;;
|
||||
esac
|
||||
rm -f /tmp/vault-plan.$$
|
||||
|
||||
step "Шаг 3. Границы сервисов через AppRole"
|
||||
ROLES_JSON=$(terraform -chdir=vault output -json approle_role_ids)
|
||||
mapfile -t SERVICES < <(jq -r 'keys[]' <<<"$ROLES_JSON")
|
||||
|
||||
cleanup() {
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
bao_as "$ROOT_TOKEN" kv metadata delete -mount="$MOUNT" "$svc/bootstrap-check" >/dev/null 2>&1 || true
|
||||
done
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
bao_as "$ROOT_TOKEN" kv put -mount="$MOUNT" "$svc/bootstrap-check" ok=1 >/dev/null
|
||||
done
|
||||
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
role_id=$(jq -r --arg s "$svc" '.[$s]' <<<"$ROLES_JSON")
|
||||
secret_id=$(bao_as "$ROOT_TOKEN" write -f -field=secret_id "auth/approle/role/$svc/secret-id")
|
||||
svc_token=$(bao_as "$ROOT_TOKEN" write -field=token auth/approle/login role_id="$role_id" secret_id="$secret_id")
|
||||
|
||||
for target in "${SERVICES[@]}"; do
|
||||
set +e
|
||||
out=$(bao_as "$svc_token" kv get -mount="$MOUNT" "$target/bootstrap-check" 2>&1)
|
||||
rc=$?
|
||||
set -e
|
||||
if [[ "$target" == "$svc" ]]; then
|
||||
[[ $rc -eq 0 ]] || fail "$svc не читает свой путь $MOUNT/$target: $out"
|
||||
ok "$svc читает $MOUNT/$target/*"
|
||||
else
|
||||
[[ $rc -ne 0 && "$out" == *"permission denied"* ]] || fail "$svc прочитал чужой путь $MOUNT/$target"
|
||||
ok "$svc получает permission denied на $MOUNT/$target/*"
|
||||
fi
|
||||
done
|
||||
bao_as "$svc_token" token revoke -self >/dev/null
|
||||
done
|
||||
|
||||
step "Шаг 4. Root-токен"
|
||||
if $REVOKE_ROOT; then
|
||||
cleanup
|
||||
trap - EXIT
|
||||
bao_as "$ROOT_TOKEN" token revoke -self >/dev/null
|
||||
ok "root-токен отозван; новый выпускается через bao operator generate-root с unseal-ключом"
|
||||
else
|
||||
ok "не отозван (запустите с --revoke-root, когда будете готовы)"
|
||||
fi
|
||||
@@ -0,0 +1,47 @@
|
||||
[env]
|
||||
_.file = ".env"
|
||||
_.source = "mise-env.sh"
|
||||
|
||||
TF_VAR_vault_token = "{{ env.VAULT_TOKEN | default(value='') }}"
|
||||
TF_VAR_s3_password = "{{ env.SILO_PASSWORD | default(value='') }}"
|
||||
TF_VAR_postgres_password = "{{ env.POSTGRES_PASSWORD | default(value='') }}"
|
||||
AWS_ACCESS_KEY_ID = "{{ env.SILO_USER | default(value='') }}"
|
||||
AWS_SECRET_ACCESS_KEY = "{{ env.SILO_PASSWORD | default(value='') }}"
|
||||
|
||||
[tasks.up]
|
||||
description = "Поднять сервисы: Postgres, pgAdmin, Silo, Vault"
|
||||
run = [
|
||||
"mkdir -p data/vault data/pgadmin",
|
||||
"docker run --rm --user root --entrypoint chown -v ./data/vault:/openbao/data $VAULT_IMAGE openbao /openbao/data",
|
||||
"docker run --rm --user root --entrypoint chown -v ./data/pgadmin:/var/lib/pgadmin $PGADMIN_IMAGE pgadmin /var/lib/pgadmin",
|
||||
"docker compose up -d --wait",
|
||||
]
|
||||
|
||||
[tasks.down]
|
||||
description = "Остановить сервисы (данные в data/ остаются, Vault после запуска будет запечатан)"
|
||||
run = "docker compose down"
|
||||
|
||||
[tasks.plan]
|
||||
description = "terraform plan во всех трёх конфигурациях"
|
||||
run = [
|
||||
"terraform -chdir=vault plan",
|
||||
"terraform -chdir=storage plan",
|
||||
"terraform -chdir=database plan",
|
||||
]
|
||||
|
||||
[tasks.fmt]
|
||||
description = "Отформатировать все .tf"
|
||||
run = "terraform fmt -recursive modules vault storage database"
|
||||
|
||||
[tasks."example:down"]
|
||||
description = "Остановить пример Go-сервиса"
|
||||
run = "docker compose -f examples/go-service/compose.yml down"
|
||||
|
||||
[tasks.check]
|
||||
description = "Проверки без обращения к сервисам: форматирование и terraform validate"
|
||||
run = [
|
||||
"terraform fmt -check -recursive modules vault storage database",
|
||||
"terraform -chdir=vault validate",
|
||||
"terraform -chdir=storage validate",
|
||||
"terraform -chdir=database validate",
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
locals {
|
||||
databases = var.service.database == null ? {} : {
|
||||
(var.service.database.name) = var.service.database
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
resource "random_password" "this" {
|
||||
for_each = local.databases
|
||||
length = 32
|
||||
special = false
|
||||
}
|
||||
|
||||
resource "postgresql_role" "this" {
|
||||
for_each = local.databases
|
||||
name = each.value.username
|
||||
login = true
|
||||
password = random_password.this[each.key].result
|
||||
}
|
||||
|
||||
resource "postgresql_database" "this" {
|
||||
for_each = local.databases
|
||||
name = each.key
|
||||
owner = postgresql_role.this[each.key].name
|
||||
}
|
||||
|
||||
resource "postgresql_extension" "vector" {
|
||||
for_each = { for key, value in local.databases : key => value if value.pgvector }
|
||||
name = "vector"
|
||||
database = postgresql_database.this[each.key].name
|
||||
}
|
||||
|
||||
resource "postgresql_grant" "public" {
|
||||
for_each = postgresql_database.this
|
||||
database = each.value.name
|
||||
role = "public"
|
||||
object_type = "database"
|
||||
privileges = []
|
||||
}
|
||||
|
||||
resource "postgresql_grant" "owner" {
|
||||
for_each = postgresql_database.this
|
||||
database = each.value.name
|
||||
role = postgresql_role.this[each.key].name
|
||||
object_type = "database"
|
||||
privileges = ["CONNECT", "CREATE", "TEMPORARY"]
|
||||
|
||||
depends_on = [postgresql_grant.public]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
output "credentials" {
|
||||
value = one([for name, db in postgresql_database.this : {
|
||||
database = db.name
|
||||
username = postgresql_role.this[name].name
|
||||
password = random_password.this[name].result
|
||||
}])
|
||||
description = "credentials of the service database, null if the service has no database"
|
||||
sensitive = true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
variable "service" {
|
||||
type = object({
|
||||
name = string
|
||||
database = optional(object({
|
||||
name = string
|
||||
username = string
|
||||
pgvector = optional(bool, false)
|
||||
}))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
postgresql = {
|
||||
source = "cyrilgdn/postgresql"
|
||||
version = ">= 1.27.0"
|
||||
}
|
||||
random = {
|
||||
source = "hashicorp/random"
|
||||
version = ">= 3.9.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
resource "minio_s3_bucket" "this" {
|
||||
for_each = var.service.buckets
|
||||
bucket = each.key
|
||||
acl = "private"
|
||||
}
|
||||
|
||||
resource "minio_s3_bucket_versioning" "this" {
|
||||
for_each = { for name, b in var.service.buckets : name => b if b.versioning }
|
||||
bucket = minio_s3_bucket.this[each.key].bucket
|
||||
|
||||
versioning_configuration {
|
||||
status = "Enabled"
|
||||
}
|
||||
}
|
||||
|
||||
resource "minio_ilm_policy" "this" {
|
||||
for_each = var.service.buckets
|
||||
bucket = minio_s3_bucket.this[each.key].bucket
|
||||
|
||||
rule {
|
||||
id = "clean-rule-1"
|
||||
status = "Enabled"
|
||||
dynamic "noncurrent_expiration" {
|
||||
for_each = each.value.versioning ? [1] : []
|
||||
content {
|
||||
days = "${each.value.noncurrent_days}d"
|
||||
}
|
||||
}
|
||||
expired_object_delete_marker = each.value.versioning
|
||||
abort_incomplete_multipart_upload {
|
||||
days_after_initiation = "3d"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "minio_iam_user" "this" {
|
||||
name = var.service.name
|
||||
}
|
||||
|
||||
resource "minio_iam_policy" "get_upd_del_policy" {
|
||||
name = var.service.name
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Effect = "Allow",
|
||||
Action = ["s3:PutObject", "s3:GetObject", "s3:DeleteObject"],
|
||||
Resource = [
|
||||
for bucket in minio_s3_bucket.this : "arn:aws:s3:::${bucket.bucket}/*"
|
||||
]
|
||||
},
|
||||
{
|
||||
Effect = "Allow",
|
||||
Action = ["s3:ListBucket"],
|
||||
Resource = [
|
||||
for bucket in minio_s3_bucket.this : "arn:aws:s3:::${bucket.bucket}"
|
||||
]
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
resource "minio_iam_user_policy_attachment" "this" {
|
||||
user_name = minio_iam_user.this.name
|
||||
policy_name = minio_iam_policy.get_upd_del_policy.name
|
||||
}
|
||||
|
||||
resource "minio_iam_service_account" "this" {
|
||||
target_user = minio_iam_user.this.name
|
||||
description = "keys of ${var.service.name}"
|
||||
|
||||
depends_on = [minio_iam_user_policy_attachment.this]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
output "buckets" {
|
||||
value = [for bucket in minio_s3_bucket.this : bucket.bucket]
|
||||
description = "buckets of the service"
|
||||
}
|
||||
|
||||
output "access_key" {
|
||||
value = minio_iam_service_account.this.access_key
|
||||
description = "access key of the service"
|
||||
}
|
||||
|
||||
output "secret_key" {
|
||||
value = minio_iam_service_account.this.secret_key
|
||||
description = "secret key of the service"
|
||||
sensitive = true
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
variable "service" {
|
||||
type = object({
|
||||
name = string
|
||||
buckets = map(object({
|
||||
versioning = optional(bool, true)
|
||||
noncurrent_days = optional(number, 30)
|
||||
}))
|
||||
})
|
||||
validation {
|
||||
condition = length(var.service.buckets) > 0
|
||||
error_message = "У сервиса должен быть хотя бы один бакет: IAM-политика с пустым списком Resource невалидна."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
minio = {
|
||||
source = "aminueza/minio"
|
||||
version = ">= 3.44.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
output "infra" {
|
||||
value = var.infra
|
||||
}
|
||||
|
||||
output "services" {
|
||||
value = var.services
|
||||
}
|
||||
|
||||
output "vault" {
|
||||
value = {
|
||||
address = "http://${var.infra.vault.host}:${var.infra.vault.port}"
|
||||
mount = var.infra.vault.mount
|
||||
database_mount = var.infra.vault.database_mount
|
||||
}
|
||||
}
|
||||
|
||||
output "silo" {
|
||||
value = {
|
||||
server = "${var.infra.silo.host}:${var.infra.silo.port}"
|
||||
internal_endpoint = "http://${var.infra.silo.internal_host}:${var.infra.silo.internal_port}"
|
||||
admin_user = var.infra.silo.admin_user
|
||||
state_bucket = var.infra.silo.state_bucket
|
||||
backup_bucket = var.infra.silo.backup_bucket
|
||||
}
|
||||
}
|
||||
|
||||
output "postgres" {
|
||||
value = var.infra.postgres
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
variable "infra" {
|
||||
type = object({
|
||||
postgres = object({
|
||||
image = string
|
||||
admin_user = string
|
||||
host = string
|
||||
port = number
|
||||
internal_host = string
|
||||
internal_port = number
|
||||
})
|
||||
pgadmin = object({
|
||||
image = string
|
||||
port = number
|
||||
email = string
|
||||
})
|
||||
silo = object({
|
||||
image = string
|
||||
admin_user = string
|
||||
host = string
|
||||
port = number
|
||||
console_port = number
|
||||
internal_host = string
|
||||
internal_port = number
|
||||
state_bucket = string
|
||||
backup_bucket = string
|
||||
})
|
||||
vault = object({
|
||||
image = string
|
||||
host = string
|
||||
port = number
|
||||
mount = string
|
||||
database_mount = string
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
variable "services" {
|
||||
type = map(object({
|
||||
postgres = optional(object({
|
||||
username = string
|
||||
name = string
|
||||
sslmode = optional(string, "disable")
|
||||
pgvector = optional(bool, false)
|
||||
}))
|
||||
buckets = optional(map(object({
|
||||
versioning = optional(bool, true)
|
||||
noncurrent_days = optional(number, 30)
|
||||
})), {})
|
||||
}))
|
||||
|
||||
validation {
|
||||
condition = alltrue(flatten([
|
||||
for svc in values(var.services) : [
|
||||
for name in keys(svc.buckets) :
|
||||
can(regex("^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$", name))
|
||||
]
|
||||
]))
|
||||
error_message = "Имя бакета: 3–63 символа, строчные латинские буквы, цифры, точки и дефисы."
|
||||
}
|
||||
}
|
||||
Generated
+44
@@ -0,0 +1,44 @@
|
||||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/aminueza/minio" {
|
||||
version = "3.44.0"
|
||||
constraints = "~> 3.44.0"
|
||||
hashes = [
|
||||
"h1:kNYbcRnleLjMG5M3D9x5Edzvm4KZgNbItsRV19nP+pY=",
|
||||
"zh:25d3b705f6dfcec6144d0d2e1535eaac06045f4e592311c8b13ccb4644855ca1",
|
||||
"zh:2845cd40624a5fbf35cc71e709b817281887c65eb05c68da77e2e7b60ad2f379",
|
||||
"zh:3d4bf825b616dfb01c4a0a9a48066a1d292dbbaf28bb2943a978b20339ed131c",
|
||||
"zh:4346b7c5194621bed62a7ca66ef7ab1f28332aea45c96b7e66abe6ed74db8148",
|
||||
"zh:452ea3862ff67b9ee6c626ce0bfd81e7af46bdcf7d187358edd8e0f262aa1337",
|
||||
"zh:62649e83fd99cbb3ecbe33ea450fd5993b465fc241b607f865c64e2237f1dec8",
|
||||
"zh:674380f7fc61b2aa96690811b352e4c9e410ad2417380782ead57984d673925f",
|
||||
"zh:6d136207a60567c29d9669aee1784acb6ba7249791b37b015acac5881656c804",
|
||||
"zh:7bc076b707f5222a8397a4fe3673feedf6caa1d32ef59b8ea7726c809ec3ba7f",
|
||||
"zh:c5f003ee66a9698a09d5ee2ec6f9df213f230fc06f5803853b6712f3a995b018",
|
||||
"zh:e46d7ccd969f1e5480aa0a2d7cfb7f49d0575696bcceeb69625fe11b8d704338",
|
||||
"zh:e52a57394f23661a236e3578a28bd0656adddc336c2dafe2e7b573703c2b538e",
|
||||
"zh:f40393f8fd1a7e05f39331c7ad9c63b59a3ccc5151598aecb2ea56eb36bc98c4",
|
||||
]
|
||||
}
|
||||
|
||||
provider "registry.terraform.io/hashicorp/vault" {
|
||||
version = "5.12.0"
|
||||
constraints = "5.12.0"
|
||||
hashes = [
|
||||
"h1:NXnrOFre4t8nj7+ZQ3MKIECpDohjMBtHWm2aen75nfU=",
|
||||
"zh:0683ab1870c6ffe78463465fc0954ff18fbbfe16d79dc121e20efb27cd2be850",
|
||||
"zh:0ade50207774b544995066be02cf7e108abe828e089eb646da4c04863cbee702",
|
||||
"zh:2b3360bec8d457a7d19df86bf4576711035785928a9fa307affce10cbeae89fd",
|
||||
"zh:5317888b0e3547341b8434f42bbb8df2d30bdd95cc1bde9a4c7a80d266fe9f6b",
|
||||
"zh:6c0271736ceb08774bebedf424735d92c4fe33b8e4db371cc027cbded0439a2e",
|
||||
"zh:70bb990d22d1ce1ceac2209aee025e0f7deab48d95002aa2b8360d9c86343ea8",
|
||||
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
||||
"zh:7f33d7bbf145e15542cfc6b1dbeed639d08204802c239c098770c5f507d1e59c",
|
||||
"zh:8ae8ca31d3c9e4b530c80eed12d8debe6fc46b407acb469b33247227aaafbf51",
|
||||
"zh:c61fcc4d714cefee0696e2f8bf715b5826099e405f66acc859e496de688b6a8e",
|
||||
"zh:ceca8db980ae3bd986142903487d75d143edfc5c7cf467da38f3698f717db046",
|
||||
"zh:e1bba3cb6a728e9bf2951139f1e7bae5bec37a37cd4bfebe282e306212a62c4c",
|
||||
"zh:f9765b4aa90e9e4eae03626ebfdfeda54954ec2552267aedce1fb2643f6eb2de",
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
locals {
|
||||
config = yamldecode(file("${path.root}/../config.yaml"))
|
||||
}
|
||||
|
||||
module "config" {
|
||||
source = "../modules/config"
|
||||
infra = local.config.infra
|
||||
services = local.config.services
|
||||
}
|
||||
|
||||
locals {
|
||||
services_with_buckets = { for name, svc in module.config.services : name => svc if length(svc.buckets) > 0 }
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
resource "minio_s3_bucket" "state_terraform_s3" {
|
||||
bucket = module.config.silo.state_bucket
|
||||
acl = "private"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "minio_s3_bucket_versioning" "state_terraform_s3" {
|
||||
bucket = minio_s3_bucket.state_terraform_s3.bucket
|
||||
|
||||
versioning_configuration {
|
||||
status = "Enabled"
|
||||
}
|
||||
}
|
||||
|
||||
resource "minio_ilm_policy" "admin_policy" {
|
||||
bucket = minio_s3_bucket.state_terraform_s3.bucket
|
||||
|
||||
rule {
|
||||
id = "clean-rule-1"
|
||||
status = "Enabled"
|
||||
noncurrent_expiration {
|
||||
days = "90d"
|
||||
}
|
||||
expired_object_delete_marker = true
|
||||
abort_incomplete_multipart_upload {
|
||||
days_after_initiation = "6d"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "minio_s3_bucket" "backups" {
|
||||
bucket = module.config.silo.backup_bucket
|
||||
acl = "private"
|
||||
}
|
||||
|
||||
resource "minio_ilm_policy" "backups" {
|
||||
bucket = minio_s3_bucket.backups.bucket
|
||||
|
||||
rule {
|
||||
id = "expire-old-backups"
|
||||
status = "Enabled"
|
||||
expiration = "30d"
|
||||
}
|
||||
}
|
||||
|
||||
module "app_storage" {
|
||||
source = "../modules/app-storage"
|
||||
for_each = local.services_with_buckets
|
||||
service = { name = each.key, buckets = each.value.buckets }
|
||||
}
|
||||
|
||||
resource "vault_kv_secret_v2" "s3" {
|
||||
for_each = module.app_storage
|
||||
mount = module.config.vault.mount
|
||||
name = "${each.key}/s3"
|
||||
delete_all_versions = true
|
||||
data_json = jsonencode({
|
||||
endpoint = module.config.silo.internal_endpoint
|
||||
buckets = join(",", each.value.buckets)
|
||||
access_key = each.value.access_key
|
||||
secret_key = each.value.secret_key
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
output "vault_secrets" {
|
||||
value = { for name, secret in vault_kv_secret_v2.s3 : name => secret.path }
|
||||
description = "vault paths of the s3 keys of the services"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
provider "vault" {
|
||||
address = module.config.vault.address
|
||||
token = var.vault_token
|
||||
}
|
||||
|
||||
provider "minio" {
|
||||
minio_server = module.config.silo.server
|
||||
minio_user = module.config.silo.admin_user
|
||||
minio_password = var.s3_password
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
variable "s3_password" {
|
||||
description = "password of the silo administrator"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "vault_token" {
|
||||
description = "vault token"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
minio = {
|
||||
source = "aminueza/minio"
|
||||
version = "3.44.0"
|
||||
}
|
||||
vault = {
|
||||
source = "hashicorp/vault"
|
||||
version = "5.12.0"
|
||||
}
|
||||
}
|
||||
backend "s3" {
|
||||
key = "storage/terraform.tfstate"
|
||||
}
|
||||
}
|
||||
Generated
+23
@@ -0,0 +1,23 @@
|
||||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/hashicorp/vault" {
|
||||
version = "5.12.0"
|
||||
constraints = "5.12.0"
|
||||
hashes = [
|
||||
"h1:NXnrOFre4t8nj7+ZQ3MKIECpDohjMBtHWm2aen75nfU=",
|
||||
"zh:0683ab1870c6ffe78463465fc0954ff18fbbfe16d79dc121e20efb27cd2be850",
|
||||
"zh:0ade50207774b544995066be02cf7e108abe828e089eb646da4c04863cbee702",
|
||||
"zh:2b3360bec8d457a7d19df86bf4576711035785928a9fa307affce10cbeae89fd",
|
||||
"zh:5317888b0e3547341b8434f42bbb8df2d30bdd95cc1bde9a4c7a80d266fe9f6b",
|
||||
"zh:6c0271736ceb08774bebedf424735d92c4fe33b8e4db371cc027cbded0439a2e",
|
||||
"zh:70bb990d22d1ce1ceac2209aee025e0f7deab48d95002aa2b8360d9c86343ea8",
|
||||
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
||||
"zh:7f33d7bbf145e15542cfc6b1dbeed639d08204802c239c098770c5f507d1e59c",
|
||||
"zh:8ae8ca31d3c9e4b530c80eed12d8debe6fc46b407acb469b33247227aaafbf51",
|
||||
"zh:c61fcc4d714cefee0696e2f8bf715b5826099e405f66acc859e496de688b6a8e",
|
||||
"zh:ceca8db980ae3bd986142903487d75d143edfc5c7cf467da38f3698f717db046",
|
||||
"zh:e1bba3cb6a728e9bf2951139f1e7bae5bec37a37cd4bfebe282e306212a62c4c",
|
||||
"zh:f9765b4aa90e9e4eae03626ebfdfeda54954ec2552267aedce1fb2643f6eb2de",
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
locals {
|
||||
config = yamldecode(file("${path.root}/../config.yaml"))
|
||||
}
|
||||
|
||||
module "config" {
|
||||
source = "../modules/config"
|
||||
infra = local.config.infra
|
||||
services = local.config.services
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
resource "vault_mount" "secret" {
|
||||
path = module.config.vault.mount
|
||||
type = "kv"
|
||||
options = {
|
||||
version = "2"
|
||||
}
|
||||
}
|
||||
|
||||
resource "vault_policy" "service" {
|
||||
for_each = module.config.services
|
||||
name = "service-${each.key}"
|
||||
policy = jsonencode({
|
||||
path = {
|
||||
"${vault_mount.secret.path}/data/${each.key}/*" = {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
"${module.config.vault.database_mount}/creds/${each.key}" = {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
resource "vault_auth_backend" "approle" {
|
||||
type = "approle"
|
||||
}
|
||||
|
||||
resource "vault_approle_auth_backend_role" "service" {
|
||||
for_each = module.config.services
|
||||
backend = vault_auth_backend.approle.path
|
||||
role_name = each.key
|
||||
token_policies = [vault_policy.service[each.key].name]
|
||||
token_ttl = 3600
|
||||
token_max_ttl = 14400
|
||||
secret_id_ttl = 2592000
|
||||
}
|
||||
|
||||
locals {
|
||||
kv = vault_mount.secret.path
|
||||
db = module.config.vault.database_mount
|
||||
ar = vault_auth_backend.approle.path
|
||||
|
||||
manage = ["create", "read", "update", "delete"]
|
||||
}
|
||||
|
||||
resource "vault_policy" "terraform" {
|
||||
name = "terraform"
|
||||
policy = jsonencode({
|
||||
path = {
|
||||
"auth/token/create" = { capabilities = ["update"] }
|
||||
|
||||
"sys/mounts/${local.kv}" = { capabilities = ["read"] }
|
||||
"sys/mounts/auth/${local.ar}" = { capabilities = ["read"] }
|
||||
"sys/mounts/auth/${local.ar}/tune" = { capabilities = ["read"] }
|
||||
"sys/policies/acl/service-*" = { capabilities = local.manage }
|
||||
"sys/policies/acl/terraform" = { capabilities = ["read", "update"] }
|
||||
"auth/${local.ar}/role/+" = { capabilities = local.manage }
|
||||
"auth/${local.ar}/role/+/role-id" = { capabilities = ["read"] }
|
||||
"auth/${local.ar}/role/+/secret-id" = { capabilities = ["update"] }
|
||||
|
||||
"${local.kv}/data/+/s3" = { capabilities = local.manage }
|
||||
"${local.kv}/data/+/postgres" = { capabilities = local.manage }
|
||||
"${local.kv}/metadata/+/s3" = { capabilities = ["read", "update", "delete"] }
|
||||
"${local.kv}/metadata/+/postgres" = { capabilities = ["read", "update", "delete"] }
|
||||
|
||||
"sys/mounts/${local.db}" = { capabilities = ["read"] }
|
||||
"sys/mounts/${local.db}/tune" = { capabilities = ["update"] }
|
||||
"${local.db}/config" = { capabilities = ["list"] }
|
||||
"${local.db}/config/postgres" = { capabilities = ["read", "update"] }
|
||||
"${local.db}/roles/+" = { capabilities = local.manage }
|
||||
|
||||
"sys/storage/raft/snapshot" = { capabilities = ["read"] }
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
output "approle_role_ids" {
|
||||
value = { for name, role in vault_approle_auth_backend_role.service : name => role.role_id }
|
||||
description = "role_id of the services, secret_id is issued separately"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
provider "vault" {
|
||||
address = module.config.vault.address
|
||||
token = var.vault_token
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
variable "vault_token" {
|
||||
description = "vault token"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
vault = {
|
||||
source = "hashicorp/vault"
|
||||
version = "5.12.0"
|
||||
}
|
||||
}
|
||||
backend "s3" {
|
||||
key = "vault/terraform.tfstate"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user