refactor: 구조 변경
This commit is contained in:
@@ -0,0 +1 @@
|
||||
terraform
|
||||
@@ -0,0 +1,4 @@
|
||||
# Copy this file to .iac-engine after choosing one engine.
|
||||
# Leave exactly one uncommented value in the copied file:
|
||||
# tofu
|
||||
# terraform
|
||||
@@ -0,0 +1,33 @@
|
||||
# Infrastructure
|
||||
|
||||
클라우드 또는 온프레미스의 네트워크, identity, DNS, registry와 Kubernetes
|
||||
cluster 같은 기반 리소스를 코드로 관리합니다.
|
||||
|
||||
```text
|
||||
components reusable primitive
|
||||
│
|
||||
├──────────────┐
|
||||
▼ ▼
|
||||
stacks live
|
||||
│ ▲
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
- `components`: 작고 재사용 가능한 구현 단위
|
||||
- `stacks`: 반복되는 component 조합(선택)
|
||||
- `live`: 실제 environment root와 state 경계
|
||||
- `tests`: component와 contract 검증
|
||||
|
||||
IaC 엔진은 프로젝트에서 하나를 선택합니다. Terraform/OpenTofu 호환이 필요하면
|
||||
같은 `.tf` 구성을 공유하고 실행 명령만 프로젝트 표준으로 통일합니다. 서로 다른
|
||||
엔진용으로 동일한 인프라 트리를 복제하지 않습니다.
|
||||
|
||||
IaC 파일을 추가할 때 `.iac-engine.example`을 `.iac-engine`으로 복사하고
|
||||
`tofu` 또는 `terraform` 중 하나만 기록합니다. 이 선택 파일은 커밋합니다.
|
||||
선택한 실행 파일과 version을 로컬/CI에 고정하고, 프로젝트별 `init -backend=false`
|
||||
및 semantic validate 단계도 검증 스크립트에 추가합니다. 기본 스켈레톤의 자동
|
||||
검사는 provider를 선택하지 않았기 때문에 IaC format까지만 수행합니다.
|
||||
|
||||
Kubernetes API 안의 platform/app 리소스는 원칙적으로 `gitops`가 소유합니다.
|
||||
클러스터 생성 시 반드시 필요한 최소 bootstrap 출력만 명시적인 contract로
|
||||
전달합니다.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Infrastructure Components
|
||||
|
||||
작고 응집된 재사용 단위를 둡니다.
|
||||
|
||||
예:
|
||||
|
||||
```text
|
||||
components/
|
||||
├── aws/
|
||||
│ ├── network/
|
||||
│ ├── identity/
|
||||
│ └── eks/
|
||||
├── gcp/
|
||||
│ ├── network/
|
||||
│ └── gke/
|
||||
└── shared/
|
||||
└── naming/
|
||||
```
|
||||
|
||||
실제로 사용하는 provider 경로만 만듭니다. component에는 environment backend,
|
||||
실제 credential과 환경 고유 값을 두지 않습니다. 입력, 출력, version constraint,
|
||||
권한 요구 사항과 사용 예를 component README에 기록합니다.
|
||||
|
||||
새 component는 `_template`을 복사해 시작합니다.
|
||||
@@ -0,0 +1,29 @@
|
||||
# __REPLACE_ME_COMPONENT_NAME__
|
||||
|
||||
## 책임
|
||||
|
||||
이 component가 생성하고 소유하는 리소스를 적습니다.
|
||||
|
||||
## 입력
|
||||
|
||||
필수/선택 입력과 민감 정보 여부를 적습니다.
|
||||
|
||||
## 출력
|
||||
|
||||
다른 component 또는 live root에 제공하는 안정적인 contract를 적습니다.
|
||||
|
||||
## 요구 권한
|
||||
|
||||
plan/apply에 필요한 최소 provider 권한을 적습니다.
|
||||
|
||||
## 사용 예
|
||||
|
||||
실제 credential, account ID와 운영 값을 포함하지 않는 호출 예를 적습니다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] backend를 선언하지 않는다.
|
||||
- [ ] provider/version constraint를 명시한다.
|
||||
- [ ] 입력 validation과 민감 output 표시를 추가한다.
|
||||
- [ ] 환경 이름을 내부에 하드코딩하지 않는다.
|
||||
- [ ] README와 테스트를 함께 갱신한다.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Vault Kubernetes roles
|
||||
|
||||
## 책임
|
||||
|
||||
Vault Kubernetes auth role을 입력 map에서 생성하는 backend 없는 재사용
|
||||
Terraform component입니다.
|
||||
|
||||
## 입력과 출력
|
||||
|
||||
- 입력: auth backend 경로와 role별 audience, ServiceAccount, namespace,
|
||||
policy, TTL
|
||||
- 출력: 없음
|
||||
- 민감 payload: 없음
|
||||
|
||||
호출하는 live root가 provider와 state를 소유합니다. 이 component는 환경
|
||||
이름이나 credential을 내부에 저장하지 않습니다.
|
||||
@@ -0,0 +1,11 @@
|
||||
resource "vault_kubernetes_auth_backend_role" "this" {
|
||||
for_each = var.roles
|
||||
|
||||
audience = one(each.value.audiences)
|
||||
backend = var.backend
|
||||
bound_service_account_names = each.value.service_account_names
|
||||
bound_service_account_namespaces = each.value.service_account_namespaces
|
||||
role_name = each.key
|
||||
token_policies = each.value.token_policies
|
||||
token_ttl = each.value.token_ttl
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
variable "backend" {
|
||||
description = "Kubernetes auth backend path."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "roles" {
|
||||
description = "Kubernetes auth roles keyed by Vault role name."
|
||||
type = map(object({
|
||||
audiences = set(string)
|
||||
service_account_names = set(string)
|
||||
service_account_namespaces = set(string)
|
||||
token_policies = set(string)
|
||||
token_ttl = number
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.11.0"
|
||||
|
||||
required_providers {
|
||||
vault = {
|
||||
source = "hashicorp/vault"
|
||||
version = "~> 5.7.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# Vault policy set
|
||||
|
||||
## 책임
|
||||
|
||||
이름과 HCL 문서 map을 Vault ACL policy로 만드는 backend 없는 재사용
|
||||
Terraform component입니다.
|
||||
|
||||
## 입력과 출력
|
||||
|
||||
- 입력: policy 이름별 HCL 문서
|
||||
- 출력: 요청 이름별 생성된 Vault policy 이름
|
||||
- 민감 payload: 없음
|
||||
|
||||
Policy 문서는 이를 소비하는 live root가 소유하며, 이 component는 API 객체
|
||||
생성만 캡슐화합니다.
|
||||
@@ -0,0 +1,11 @@
|
||||
resource "vault_policy" "this" {
|
||||
for_each = var.policies
|
||||
|
||||
name = each.key
|
||||
policy = each.value
|
||||
}
|
||||
|
||||
output "names" {
|
||||
description = "Policy names keyed by their requested names."
|
||||
value = { for name, policy in vault_policy.this : name => policy.name }
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
variable "policies" {
|
||||
description = "Map of Vault policy names to HCL policy documents."
|
||||
type = map(string)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.11.0"
|
||||
|
||||
required_providers {
|
||||
vault = {
|
||||
source = "hashicorp/vault"
|
||||
version = "~> 5.7.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
# Live Infrastructure
|
||||
|
||||
실제로 plan/apply하는 root를 둡니다. leaf 디렉터리 하나가 독립적인 state,
|
||||
locking, 권한과 실패 범위입니다.
|
||||
|
||||
소규모 예:
|
||||
|
||||
```text
|
||||
live/
|
||||
└── dev/
|
||||
├── network/
|
||||
└── cluster/
|
||||
```
|
||||
|
||||
확장 예:
|
||||
|
||||
```text
|
||||
live/
|
||||
└── aws/
|
||||
└── platform-prod/
|
||||
└── ap-northeast-2/
|
||||
└── prod/
|
||||
├── network/
|
||||
├── shared-services/
|
||||
└── cluster-a/
|
||||
```
|
||||
|
||||
경로 깊이보다 leaf의 실행 계약이 중요합니다. 자동화는 특정 depth를 가정하지
|
||||
말고 IaC root marker를 기준으로 대상을 찾습니다.
|
||||
|
||||
## 규칙
|
||||
|
||||
- 각 root는 remote backend와 locking을 사용합니다.
|
||||
- production과 non-production state/credential을 분리합니다.
|
||||
- 민감하지 않은 입력만 커밋하며 secret 입력은 runtime에 주입합니다.
|
||||
- 다른 환경 경로를 상대 import하지 않습니다.
|
||||
- provider, module과 component version을 고정합니다.
|
||||
- output consumer와 파괴 영향 범위를 README에 기록합니다.
|
||||
@@ -0,0 +1,34 @@
|
||||
# __REPLACE_ME_LIVE_ROOT_NAME__
|
||||
|
||||
## 대상
|
||||
|
||||
- Provider/account/project: __REPLACE_ME_SCOPE__
|
||||
- Region: __REPLACE_ME_REGION_OR_GLOBAL__
|
||||
- Environment: __REPLACE_ME_ENVIRONMENT__
|
||||
- Stack: __REPLACE_ME_STACK__
|
||||
- Owner: __REPLACE_ME_OWNER__
|
||||
|
||||
## State
|
||||
|
||||
- Backend: __REPLACE_ME_BACKEND__
|
||||
- Locking:
|
||||
- Encryption:
|
||||
- Recovery runbook:
|
||||
|
||||
## 의존성
|
||||
|
||||
선행 state/output과 사용하는 component/stack version을 적습니다.
|
||||
|
||||
## Output Contract
|
||||
|
||||
downstream bootstrap 또는 system에 전달하는 값을 적습니다. 민감 output은
|
||||
명시적으로 표시하고 로그에 출력하지 않습니다.
|
||||
|
||||
## 실행
|
||||
|
||||
프로젝트가 선택한 IaC 엔진의 init/plan/apply 절차를 적습니다. production은
|
||||
검토된 plan, 승인과 concurrency lock 없이 적용하지 않습니다.
|
||||
|
||||
## 롤백/복구
|
||||
|
||||
되돌릴 수 있는 변경과 state 복구 절차 링크를 적습니다.
|
||||
@@ -0,0 +1,22 @@
|
||||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/hashicorp/vault" {
|
||||
version = "5.7.0"
|
||||
constraints = "~> 5.7.0"
|
||||
hashes = [
|
||||
"h1:Pm0AcUSYmBPZgRahQX/ahiYcjtZODSAEc2rK8r8MQ18=",
|
||||
"zh:1dd9ab6d23f61a5e522efcb462f1fd6f4a210c77b9038c8e12fa5fa663b45d01",
|
||||
"zh:3c98d37ead857c980f7b9285f8c3e1eb7a8fd6d6799275c311c6997973389cc9",
|
||||
"zh:3df895fbaed383e3748ba1b50f5f1046f75503483bc3d783992059f85c85ba31",
|
||||
"zh:3e9faaa0a85c6f03c7fd7f8b7008bb3fbb8777f26c001875947cafa47f91c657",
|
||||
"zh:52a057d0c6cde7cbfd9ceb78a3781dcfc81cf108c533f454530ea6bb87a9bea8",
|
||||
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
||||
"zh:8521c3825254a5f7fbff8f42ca57cabf052366f0420f5f239ebebf8292c03d0e",
|
||||
"zh:953563d429e40087eb34faf22f28e781e50eee27cfc9ac1ad04308ba592a647f",
|
||||
"zh:a52dd76bb7f5b86cb8de7380d2e68b47ec4445782c16ee205e6a013be35a57b6",
|
||||
"zh:bdad38c95a14c8cce1eeadcc539cf9bf74902ce7c662b79105ad993bb48ec073",
|
||||
"zh:d3c676d7d12c15b58518fa3ee7fc398a13893b4057fe9bf4bc1fe635f3fb995a",
|
||||
"zh:f8673b6c06da80e912c9e32dd4853f07bfca386968d5b33c9fceb6f68b519959",
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# dev-k3s Vault database
|
||||
|
||||
## 대상과 State
|
||||
|
||||
- Environment/cluster: `dev-k3s`
|
||||
- Provider: HashiCorp Vault
|
||||
- State: `vault-database`
|
||||
- Backend example: `backend.s3.hcl.example`
|
||||
- Owner: delegated database automation
|
||||
|
||||
Project Auth PostgreSQL connection과 migration dynamic role만 소유합니다.
|
||||
PostgreSQL이 준비되고 연결 입력을 안전하게 주입할 수 있을 때 별도 승인으로
|
||||
실행합니다. Database password는 ephemeral/write-only 입력이며 state나
|
||||
repository에 저장하지 않습니다.
|
||||
@@ -0,0 +1,5 @@
|
||||
bucket = "project-gitops-terraform-state"
|
||||
key = "dev-k3s/vault-database.tfstate"
|
||||
region = "us-east-1"
|
||||
encrypt = true
|
||||
use_lockfile = true
|
||||
@@ -0,0 +1,83 @@
|
||||
terraform {
|
||||
required_version = ">= 1.11.0"
|
||||
|
||||
required_providers {
|
||||
vault = {
|
||||
source = "hashicorp/vault"
|
||||
version = "~> 5.7.0"
|
||||
}
|
||||
}
|
||||
|
||||
backend "s3" {}
|
||||
}
|
||||
|
||||
provider "vault" {
|
||||
address = var.vault_addr
|
||||
skip_child_token = true
|
||||
token = var.vault_token
|
||||
}
|
||||
|
||||
moved {
|
||||
from = vault_database_secret_backend_connection.platform_postgres
|
||||
to = vault_database_secret_backend_connection.auth_system_postgres
|
||||
}
|
||||
|
||||
removed {
|
||||
from = vault_database_secret_backend_role.postgres_operator
|
||||
|
||||
lifecycle {
|
||||
destroy = false
|
||||
}
|
||||
}
|
||||
|
||||
locals {
|
||||
database_config_name = "auth-system-postgres-dev"
|
||||
database_mount_path = "database"
|
||||
migration_role_name = "auth-db-migration-dev"
|
||||
|
||||
creation_statements = [
|
||||
<<-EOT
|
||||
CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';
|
||||
GRANT "${var.auth_db_role}" TO "{{name}}";
|
||||
EOT
|
||||
]
|
||||
|
||||
revocation_statements = [
|
||||
<<-EOT
|
||||
REASSIGN OWNED BY "{{name}}" TO "${var.auth_db_role}";
|
||||
DROP OWNED BY "{{name}}";
|
||||
REVOKE "${var.auth_db_role}" FROM "{{name}}";
|
||||
DROP ROLE IF EXISTS "{{name}}";
|
||||
EOT
|
||||
]
|
||||
}
|
||||
|
||||
resource "vault_database_secret_backend_connection" "auth_system_postgres" {
|
||||
allowed_roles = [local.migration_role_name]
|
||||
backend = local.database_mount_path
|
||||
name = local.database_config_name
|
||||
plugin_name = "postgresql-database-plugin"
|
||||
verify_connection = true
|
||||
|
||||
postgresql {
|
||||
connection_url = "postgresql://{{username}}:{{password}}@${var.postgres_host}:${var.postgres_port}/${var.postgres_database}?sslmode=${var.postgres_sslmode}"
|
||||
password_authentication = "scram-sha-256"
|
||||
password_wo = var.postgres_admin_password
|
||||
password_wo_version = var.postgres_admin_password_version
|
||||
username = var.postgres_admin_username
|
||||
}
|
||||
|
||||
lifecycle {
|
||||
create_before_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "vault_database_secret_backend_role" "auth_db_migration" {
|
||||
backend = local.database_mount_path
|
||||
creation_statements = local.creation_statements
|
||||
db_name = vault_database_secret_backend_connection.auth_system_postgres.name
|
||||
default_ttl = var.auth_db_migration_default_ttl_seconds
|
||||
max_ttl = var.auth_db_migration_max_ttl_seconds
|
||||
name = local.migration_role_name
|
||||
revocation_statements = local.revocation_statements
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
variable "auth_db_migration_default_ttl_seconds" {
|
||||
description = "Default TTL for migration credentials."
|
||||
type = number
|
||||
default = 3600
|
||||
}
|
||||
|
||||
variable "auth_db_migration_max_ttl_seconds" {
|
||||
description = "Maximum TTL for migration credentials."
|
||||
type = number
|
||||
default = 86400
|
||||
}
|
||||
|
||||
variable "auth_db_role" {
|
||||
description = "Stable PostgreSQL owner role used by dynamic users."
|
||||
type = string
|
||||
default = "project_auth"
|
||||
}
|
||||
|
||||
variable "postgres_admin_password" {
|
||||
description = "PostgreSQL admin password passed only through a write-only provider field."
|
||||
type = string
|
||||
sensitive = true
|
||||
ephemeral = true
|
||||
}
|
||||
|
||||
variable "postgres_admin_password_version" {
|
||||
description = "Increment whenever postgres_admin_password is rotated."
|
||||
type = number
|
||||
}
|
||||
|
||||
variable "postgres_admin_username" {
|
||||
description = "Dedicated database administration username."
|
||||
type = string
|
||||
default = "postgres"
|
||||
}
|
||||
|
||||
variable "postgres_database" {
|
||||
description = "Database in which dynamic migration objects are owned and revoked."
|
||||
type = string
|
||||
default = "project_auth"
|
||||
}
|
||||
|
||||
variable "postgres_host" {
|
||||
description = "Auth system PostgreSQL service DNS name."
|
||||
type = string
|
||||
default = "postgres.auth-system-dev.svc.cluster.local"
|
||||
}
|
||||
|
||||
variable "postgres_port" {
|
||||
description = "Auth system PostgreSQL service port."
|
||||
type = number
|
||||
default = 5432
|
||||
}
|
||||
|
||||
variable "postgres_sslmode" {
|
||||
description = "PostgreSQL SSL mode. Dev currently uses disable; production must use verify-full."
|
||||
type = string
|
||||
default = "disable"
|
||||
|
||||
validation {
|
||||
condition = contains(["disable", "require", "verify-ca", "verify-full"], var.postgres_sslmode)
|
||||
error_message = "postgres_sslmode must be disable, require, verify-ca, or verify-full."
|
||||
}
|
||||
}
|
||||
|
||||
variable "vault_addr" {
|
||||
description = "Workload Vault API address."
|
||||
type = string
|
||||
default = "http://127.0.0.1:8200"
|
||||
}
|
||||
|
||||
variable "vault_token" {
|
||||
description = "Short-lived token carrying vault-database-automation-dev."
|
||||
type = string
|
||||
sensitive = true
|
||||
ephemeral = true
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/hashicorp/vault" {
|
||||
version = "5.7.0"
|
||||
constraints = "~> 5.7.0"
|
||||
hashes = [
|
||||
"h1:Pm0AcUSYmBPZgRahQX/ahiYcjtZODSAEc2rK8r8MQ18=",
|
||||
"zh:1dd9ab6d23f61a5e522efcb462f1fd6f4a210c77b9038c8e12fa5fa663b45d01",
|
||||
"zh:3c98d37ead857c980f7b9285f8c3e1eb7a8fd6d6799275c311c6997973389cc9",
|
||||
"zh:3df895fbaed383e3748ba1b50f5f1046f75503483bc3d783992059f85c85ba31",
|
||||
"zh:3e9faaa0a85c6f03c7fd7f8b7008bb3fbb8777f26c001875947cafa47f91c657",
|
||||
"zh:52a057d0c6cde7cbfd9ceb78a3781dcfc81cf108c533f454530ea6bb87a9bea8",
|
||||
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
||||
"zh:8521c3825254a5f7fbff8f42ca57cabf052366f0420f5f239ebebf8292c03d0e",
|
||||
"zh:953563d429e40087eb34faf22f28e781e50eee27cfc9ac1ad04308ba592a647f",
|
||||
"zh:a52dd76bb7f5b86cb8de7380d2e68b47ec4445782c16ee205e6a013be35a57b6",
|
||||
"zh:bdad38c95a14c8cce1eeadcc539cf9bf74902ce7c662b79105ad993bb48ec073",
|
||||
"zh:d3c676d7d12c15b58518fa3ee7fc398a13893b4057fe9bf4bc1fe635f3fb995a",
|
||||
"zh:f8673b6c06da80e912c9e32dd4853f07bfca386968d5b33c9fceb6f68b519959",
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
# dev-k3s Vault foundation
|
||||
|
||||
## 대상과 State
|
||||
|
||||
- Environment/cluster: `dev-k3s`
|
||||
- Provider: HashiCorp Vault
|
||||
- State: `vault-foundation`
|
||||
- Backend example: `backend.s3.hcl.example`
|
||||
- Owner: bootstrap/security administrator
|
||||
|
||||
Vault mount, Kubernetes auth config, delegated automation policy와 선택적 CI JWT
|
||||
role을 소유합니다. Routine automation 대상이 아니며 downstream state가 자기
|
||||
실행 권한을 직접 만들지 않도록 합니다.
|
||||
|
||||
`policies/`는 이 state가 소유하는 정확한 delegated automation ACL입니다.
|
||||
실행과 복구 절차는
|
||||
[`docs/runbooks/dev-bootstrap.md`](../../../../docs/runbooks/dev-bootstrap.md)와
|
||||
[`docs/runbooks/terraform-state-migration.md`](../../../../docs/runbooks/terraform-state-migration.md)를
|
||||
따릅니다.
|
||||
@@ -0,0 +1,5 @@
|
||||
bucket = "project-gitops-terraform-state"
|
||||
key = "dev-k3s/vault-foundation.tfstate"
|
||||
region = "us-east-1"
|
||||
encrypt = true
|
||||
use_lockfile = true
|
||||
@@ -0,0 +1,189 @@
|
||||
terraform {
|
||||
required_version = ">= 1.11.0"
|
||||
|
||||
required_providers {
|
||||
vault = {
|
||||
source = "hashicorp/vault"
|
||||
version = "~> 5.7.0"
|
||||
}
|
||||
}
|
||||
|
||||
backend "s3" {}
|
||||
}
|
||||
|
||||
provider "vault" {
|
||||
address = var.vault_addr
|
||||
token = var.vault_token
|
||||
}
|
||||
|
||||
removed {
|
||||
from = module.workload_policies
|
||||
|
||||
lifecycle {
|
||||
destroy = false
|
||||
}
|
||||
}
|
||||
|
||||
removed {
|
||||
from = module.workload_roles
|
||||
|
||||
lifecycle {
|
||||
destroy = false
|
||||
}
|
||||
}
|
||||
|
||||
removed {
|
||||
from = vault_transit_secret_backend_key.project_auth_jwt
|
||||
|
||||
lifecycle {
|
||||
destroy = false
|
||||
}
|
||||
}
|
||||
|
||||
removed {
|
||||
from = vault_policy.platform_admin
|
||||
|
||||
lifecycle {
|
||||
destroy = false
|
||||
}
|
||||
}
|
||||
|
||||
removed {
|
||||
from = vault_kubernetes_auth_backend_role.operator
|
||||
|
||||
lifecycle {
|
||||
destroy = false
|
||||
}
|
||||
}
|
||||
|
||||
removed {
|
||||
from = vault_jwt_auth_backend_role.ci
|
||||
|
||||
lifecycle {
|
||||
destroy = false
|
||||
}
|
||||
}
|
||||
|
||||
locals {
|
||||
platform_policy_dir = "${path.module}/policies"
|
||||
|
||||
database_automation_policy_name = "vault-database-automation-dev"
|
||||
database_mount_path = "database"
|
||||
ci_database_role_name = "project-gitops-vault-database-dev"
|
||||
ci_jwt_auth_path = "jwt-ci"
|
||||
ci_workloads_role_name = "project-gitops-vault-workloads-dev"
|
||||
kubernetes_auth_path = "kubernetes"
|
||||
kv_mount_path = "kv"
|
||||
transit_mount_path = "transit"
|
||||
workloads_automation_policy_name = "vault-workloads-automation-dev"
|
||||
|
||||
automation_roles = var.ci_jwt_oidc_discovery_url == null ? {} : {
|
||||
workloads = {
|
||||
bound_claims = var.ci_workloads_bound_claims
|
||||
name = local.ci_workloads_role_name
|
||||
policy = vault_policy.workloads_automation.name
|
||||
}
|
||||
database = {
|
||||
bound_claims = var.ci_database_bound_claims
|
||||
name = local.ci_database_role_name
|
||||
policy = vault_policy.database_automation.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "vault_mount" "kv" {
|
||||
path = local.kv_mount_path
|
||||
type = "kv"
|
||||
options = {
|
||||
version = "2"
|
||||
}
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "vault_mount" "database" {
|
||||
path = local.database_mount_path
|
||||
type = "database"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "vault_mount" "transit" {
|
||||
path = local.transit_mount_path
|
||||
type = "transit"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "vault_auth_backend" "kubernetes" {
|
||||
path = local.kubernetes_auth_path
|
||||
type = "kubernetes"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "vault_kubernetes_auth_backend_config" "cluster" {
|
||||
backend = vault_auth_backend.kubernetes.path
|
||||
disable_iss_validation = true
|
||||
disable_local_ca_jwt = false
|
||||
kubernetes_host = var.kubernetes_host
|
||||
}
|
||||
|
||||
resource "vault_policy" "workloads_automation" {
|
||||
name = local.workloads_automation_policy_name
|
||||
policy = file("${local.platform_policy_dir}/vault-workloads-automation-dev.hcl")
|
||||
}
|
||||
|
||||
resource "vault_policy" "database_automation" {
|
||||
name = local.database_automation_policy_name
|
||||
policy = file("${local.platform_policy_dir}/vault-database-automation-dev.hcl")
|
||||
}
|
||||
|
||||
resource "vault_jwt_auth_backend" "ci" {
|
||||
count = var.ci_jwt_oidc_discovery_url == null ? 0 : 1
|
||||
|
||||
bound_issuer = var.ci_jwt_bound_issuer
|
||||
oidc_discovery_url = var.ci_jwt_oidc_discovery_url
|
||||
path = local.ci_jwt_auth_path
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
|
||||
precondition {
|
||||
condition = (
|
||||
var.ci_jwt_bound_issuer != null &&
|
||||
length(var.ci_jwt_bound_audiences) > 0 &&
|
||||
length(var.ci_workloads_bound_claims) > 0 &&
|
||||
length(var.ci_database_bound_claims) > 0 &&
|
||||
length([
|
||||
for claim, value in var.ci_workloads_bound_claims : claim
|
||||
if lookup(var.ci_database_bound_claims, claim, value) != value
|
||||
]) > 0
|
||||
)
|
||||
error_message = "Enabled CI JWT auth requires issuer/audience constraints and workload/database claim maps with at least one shared discriminator key carrying different values."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "vault_jwt_auth_backend_role" "automation" {
|
||||
for_each = local.automation_roles
|
||||
|
||||
backend = vault_jwt_auth_backend.ci[0].path
|
||||
bound_audiences = var.ci_jwt_bound_audiences
|
||||
bound_claims = each.value.bound_claims
|
||||
bound_claims_type = "string"
|
||||
role_name = each.value.name
|
||||
role_type = "jwt"
|
||||
token_explicit_max_ttl = var.ci_token_ttl_seconds
|
||||
token_no_default_policy = true
|
||||
token_policies = [each.value.policy]
|
||||
user_claim = var.ci_jwt_user_claim
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
# Managed by vault-foundation. The database runner may reconcile only the named
|
||||
# PostgreSQL connection, dynamic role, and minimal self-service token endpoints.
|
||||
|
||||
path "database/config/auth-system-postgres-dev" {
|
||||
capabilities = ["create", "read", "update", "delete"]
|
||||
}
|
||||
|
||||
path "database/roles/auth-db-migration-dev" {
|
||||
capabilities = ["create", "read", "update", "delete"]
|
||||
}
|
||||
|
||||
# no-default-policy runner tokens retain only the self-service operations used
|
||||
# for capability checks, identity verification, and explicit revocation.
|
||||
path "sys/capabilities-self" {
|
||||
capabilities = ["update"]
|
||||
}
|
||||
|
||||
path "auth/token/lookup-self" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
|
||||
path "auth/token/revoke-self" {
|
||||
capabilities = ["update"]
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
# Managed by vault-foundation. This trusted security runner may reconcile only
|
||||
# the named workload policies, Kubernetes auth roles, application Transit key,
|
||||
# and the minimal self-service token endpoints declared below.
|
||||
|
||||
path "sys/policies/acl/auth-server-dev" {
|
||||
capabilities = ["create", "read", "update", "delete"]
|
||||
}
|
||||
|
||||
path "sys/policies/acl/auth-db-migration-dev" {
|
||||
capabilities = ["create", "read", "update", "delete"]
|
||||
}
|
||||
|
||||
path "sys/policies/acl/postgres-dev" {
|
||||
capabilities = ["create", "read", "update", "delete"]
|
||||
}
|
||||
|
||||
path "sys/policies/acl/keycloak-dev" {
|
||||
capabilities = ["create", "read", "update", "delete"]
|
||||
}
|
||||
|
||||
path "sys/policies/acl/keycloak-client-sync-dev" {
|
||||
capabilities = ["create", "read", "update", "delete"]
|
||||
}
|
||||
|
||||
path "auth/kubernetes/role/auth-server-dev" {
|
||||
capabilities = ["create", "read", "update", "delete"]
|
||||
}
|
||||
|
||||
path "auth/kubernetes/role/auth-db-migration-dev" {
|
||||
capabilities = ["create", "read", "update", "delete"]
|
||||
}
|
||||
|
||||
path "auth/kubernetes/role/postgres-dev" {
|
||||
capabilities = ["create", "read", "update", "delete"]
|
||||
}
|
||||
|
||||
path "auth/kubernetes/role/keycloak-dev" {
|
||||
capabilities = ["create", "read", "update", "delete"]
|
||||
}
|
||||
|
||||
path "auth/kubernetes/role/keycloak-client-sync-dev" {
|
||||
capabilities = ["create", "read", "update", "delete"]
|
||||
}
|
||||
|
||||
path "transit/keys/project-auth-jwt" {
|
||||
capabilities = ["create", "read", "update"]
|
||||
}
|
||||
|
||||
path "transit/keys/project-auth-jwt/config" {
|
||||
capabilities = ["update"]
|
||||
}
|
||||
|
||||
# no-default-policy runner tokens retain only the self-service operations used
|
||||
# for capability checks, identity verification, and explicit revocation.
|
||||
path "sys/capabilities-self" {
|
||||
capabilities = ["update"]
|
||||
}
|
||||
|
||||
path "auth/token/lookup-self" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
|
||||
path "auth/token/revoke-self" {
|
||||
capabilities = ["update"]
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
variable "ci_database_bound_claims" {
|
||||
description = "Exact repository, protected-ref, and database-job claims for the database role."
|
||||
type = map(string)
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "ci_jwt_bound_audiences" {
|
||||
description = "Exact CI JWT audiences."
|
||||
type = set(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "ci_jwt_bound_issuer" {
|
||||
description = "Expected CI JWT issuer."
|
||||
type = string
|
||||
default = null
|
||||
nullable = true
|
||||
}
|
||||
|
||||
variable "ci_jwt_oidc_discovery_url" {
|
||||
description = "CI OIDC discovery URL. Null leaves external CI authentication disabled."
|
||||
type = string
|
||||
default = null
|
||||
nullable = true
|
||||
}
|
||||
|
||||
variable "ci_jwt_user_claim" {
|
||||
description = "JWT claim used as the Vault identity alias."
|
||||
type = string
|
||||
default = "sub"
|
||||
}
|
||||
|
||||
variable "ci_token_ttl_seconds" {
|
||||
description = "Maximum lifetime for delegated CI tokens."
|
||||
type = number
|
||||
default = 1800
|
||||
|
||||
validation {
|
||||
condition = (
|
||||
var.ci_token_ttl_seconds >= 60 &&
|
||||
var.ci_token_ttl_seconds <= 3600 &&
|
||||
floor(var.ci_token_ttl_seconds) == var.ci_token_ttl_seconds
|
||||
)
|
||||
error_message = "ci_token_ttl_seconds must be a whole number between 60 and 3600."
|
||||
}
|
||||
}
|
||||
|
||||
variable "ci_workloads_bound_claims" {
|
||||
description = "Exact repository, protected-ref, and workloads-job claims for the workloads role."
|
||||
type = map(string)
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "kubernetes_host" {
|
||||
description = "Kubernetes TokenReview API address."
|
||||
type = string
|
||||
default = "https://kubernetes.default.svc.cluster.local:443"
|
||||
}
|
||||
|
||||
variable "vault_addr" {
|
||||
description = "Vault API address reachable by the foundation operator."
|
||||
type = string
|
||||
default = "http://127.0.0.1:8200"
|
||||
}
|
||||
|
||||
variable "vault_token" {
|
||||
description = "Short-lived bootstrap or security-administrator token."
|
||||
type = string
|
||||
sensitive = true
|
||||
ephemeral = true
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/hashicorp/vault" {
|
||||
version = "5.7.0"
|
||||
constraints = "~> 5.7.0"
|
||||
hashes = [
|
||||
"h1:Pm0AcUSYmBPZgRahQX/ahiYcjtZODSAEc2rK8r8MQ18=",
|
||||
"zh:1dd9ab6d23f61a5e522efcb462f1fd6f4a210c77b9038c8e12fa5fa663b45d01",
|
||||
"zh:3c98d37ead857c980f7b9285f8c3e1eb7a8fd6d6799275c311c6997973389cc9",
|
||||
"zh:3df895fbaed383e3748ba1b50f5f1046f75503483bc3d783992059f85c85ba31",
|
||||
"zh:3e9faaa0a85c6f03c7fd7f8b7008bb3fbb8777f26c001875947cafa47f91c657",
|
||||
"zh:52a057d0c6cde7cbfd9ceb78a3781dcfc81cf108c533f454530ea6bb87a9bea8",
|
||||
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
||||
"zh:8521c3825254a5f7fbff8f42ca57cabf052366f0420f5f239ebebf8292c03d0e",
|
||||
"zh:953563d429e40087eb34faf22f28e781e50eee27cfc9ac1ad04308ba592a647f",
|
||||
"zh:a52dd76bb7f5b86cb8de7380d2e68b47ec4445782c16ee205e6a013be35a57b6",
|
||||
"zh:bdad38c95a14c8cce1eeadcc539cf9bf74902ce7c662b79105ad993bb48ec073",
|
||||
"zh:d3c676d7d12c15b58518fa3ee7fc398a13893b4057fe9bf4bc1fe635f3fb995a",
|
||||
"zh:f8673b6c06da80e912c9e32dd4853f07bfca386968d5b33c9fceb6f68b519959",
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
# dev-k3s Vault workloads
|
||||
|
||||
## 대상과 State
|
||||
|
||||
- Environment/cluster: `dev-k3s`
|
||||
- Provider: HashiCorp Vault
|
||||
- State: `vault-workloads`
|
||||
- Backend example: `backend.s3.hcl.example`
|
||||
- Owner: delegated workload automation
|
||||
|
||||
정확한 workload ACL, Kubernetes auth role와 Project Auth Transit key를
|
||||
소유합니다. `policies/`의 ACL은 wildcard 없이 workload가 실제 사용하는
|
||||
경로만 허용합니다.
|
||||
|
||||
Foundation이 만든 short-lived identity로 실행하며, 이 state는 자기 실행
|
||||
policy/login role을 생성하지 않습니다.
|
||||
@@ -0,0 +1,5 @@
|
||||
bucket = "project-gitops-terraform-state"
|
||||
key = "dev-k3s/vault-workloads.tfstate"
|
||||
region = "us-east-1"
|
||||
encrypt = true
|
||||
use_lockfile = true
|
||||
@@ -0,0 +1,95 @@
|
||||
terraform {
|
||||
required_version = ">= 1.11.0"
|
||||
|
||||
required_providers {
|
||||
vault = {
|
||||
source = "hashicorp/vault"
|
||||
version = "~> 5.7.0"
|
||||
}
|
||||
}
|
||||
|
||||
backend "s3" {}
|
||||
}
|
||||
|
||||
provider "vault" {
|
||||
address = var.vault_addr
|
||||
skip_child_token = true
|
||||
token = var.vault_token
|
||||
}
|
||||
|
||||
locals {
|
||||
workload_policy_dir = "${path.module}/policies"
|
||||
|
||||
jwt_transit_key_name = "project-auth-jwt"
|
||||
kubernetes_auth_path = "kubernetes"
|
||||
kubernetes_token_audience = "vault"
|
||||
transit_mount_path = "transit"
|
||||
|
||||
workload_policies = {
|
||||
auth-server-dev = file("${local.workload_policy_dir}/auth-server-dev.hcl")
|
||||
auth-db-migration-dev = file("${local.workload_policy_dir}/auth-db-migration-dev.hcl")
|
||||
postgres-dev = file("${local.workload_policy_dir}/postgres-dev.hcl")
|
||||
keycloak-dev = file("${local.workload_policy_dir}/keycloak-dev.hcl")
|
||||
keycloak-client-sync-dev = file("${local.workload_policy_dir}/keycloak-client-sync-dev.hcl")
|
||||
}
|
||||
}
|
||||
|
||||
module "workload_policies" {
|
||||
source = "../../../components/vault-policy-set"
|
||||
|
||||
policies = local.workload_policies
|
||||
}
|
||||
|
||||
module "workload_roles" {
|
||||
source = "../../../components/vault-kubernetes-roles"
|
||||
|
||||
backend = local.kubernetes_auth_path
|
||||
roles = {
|
||||
auth-server-dev = {
|
||||
audiences = [local.kubernetes_token_audience]
|
||||
service_account_names = ["auth-server"]
|
||||
service_account_namespaces = ["auth-dev"]
|
||||
token_policies = [module.workload_policies.names["auth-server-dev"]]
|
||||
token_ttl = var.kubernetes_role_ttl_seconds
|
||||
}
|
||||
auth-db-migration-dev = {
|
||||
audiences = [local.kubernetes_token_audience]
|
||||
service_account_names = ["auth-db-migration"]
|
||||
service_account_namespaces = ["auth-dev"]
|
||||
token_policies = [module.workload_policies.names["auth-db-migration-dev"]]
|
||||
token_ttl = var.kubernetes_role_ttl_seconds
|
||||
}
|
||||
postgres-dev = {
|
||||
audiences = [local.kubernetes_token_audience]
|
||||
service_account_names = ["postgres"]
|
||||
service_account_namespaces = ["auth-system-dev"]
|
||||
token_policies = [module.workload_policies.names["postgres-dev"]]
|
||||
token_ttl = var.kubernetes_role_ttl_seconds
|
||||
}
|
||||
keycloak-dev = {
|
||||
audiences = [local.kubernetes_token_audience]
|
||||
service_account_names = ["keycloak"]
|
||||
service_account_namespaces = ["auth-system-dev"]
|
||||
token_policies = [module.workload_policies.names["keycloak-dev"]]
|
||||
token_ttl = var.kubernetes_role_ttl_seconds
|
||||
}
|
||||
keycloak-client-sync-dev = {
|
||||
audiences = [local.kubernetes_token_audience]
|
||||
service_account_names = ["keycloak-client-sync"]
|
||||
service_account_namespaces = ["auth-system-dev"]
|
||||
token_policies = [module.workload_policies.names["keycloak-client-sync-dev"]]
|
||||
token_ttl = var.kubernetes_role_ttl_seconds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "vault_transit_secret_backend_key" "project_auth_jwt" {
|
||||
backend = local.transit_mount_path
|
||||
deletion_allowed = false
|
||||
name = local.jwt_transit_key_name
|
||||
type = "rsa-2048"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
path "database/creds/auth-db-migration-dev" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
path "kv/data/dev/systems/auth-system/postgres/auth-server" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
|
||||
path "kv/data/dev/workloads/auth-server/keycloak-client" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
|
||||
path "transit/keys/project-auth-jwt" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
|
||||
path "transit/sign/project-auth-jwt" {
|
||||
capabilities = ["update"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
path "kv/data/dev/systems/auth-system/keycloak/bootstrap-admin" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
|
||||
path "kv/data/dev/workloads/auth-server/keycloak-client" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
path "kv/data/dev/systems/auth-system/postgres/keycloak" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
|
||||
path "kv/data/dev/systems/auth-system/keycloak/bootstrap-admin" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
path "kv/data/dev/systems/auth-system/postgres/superuser" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
|
||||
path "kv/data/dev/systems/auth-system/postgres/auth-server" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
|
||||
path "kv/data/dev/systems/auth-system/postgres/keycloak" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
variable "kubernetes_role_ttl_seconds" {
|
||||
description = "TTL for workload Kubernetes auth tokens."
|
||||
type = number
|
||||
default = 3600
|
||||
}
|
||||
|
||||
variable "vault_addr" {
|
||||
description = "Vault API address reachable by the delegated runner."
|
||||
type = string
|
||||
default = "http://127.0.0.1:8200"
|
||||
}
|
||||
|
||||
variable "vault_token" {
|
||||
description = "Short-lived token carrying only vault-workloads-automation-dev."
|
||||
type = string
|
||||
sensitive = true
|
||||
ephemeral = true
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# Infrastructure Stacks
|
||||
|
||||
여러 component 조합이 두 개 이상의 live root에서 반복될 때 사용하는 선택 계층입니다.
|
||||
|
||||
예:
|
||||
|
||||
- `regional-foundation`: network + shared identity + DNS
|
||||
- `cluster`: Kubernetes cluster + node pools + workload identity
|
||||
- `edge`: CDN + load balancer + certificate
|
||||
|
||||
작은 프로젝트에서는 `live`가 component를 직접 호출하고 이 계층을 생략합니다.
|
||||
stack은 실행 가능한 environment root가 아니므로 backend와 실제 credential을
|
||||
두지 않습니다. stack 중첩을 깊게 만들기보다 live root에서 평평하게 조합합니다.
|
||||
@@ -0,0 +1,19 @@
|
||||
# __REPLACE_ME_STACK_NAME__
|
||||
|
||||
## 목적
|
||||
|
||||
반복해서 함께 배포하는 component 조합을 설명합니다.
|
||||
|
||||
## 포함 Component
|
||||
|
||||
각 component의 version과 책임을 적습니다.
|
||||
|
||||
## 입력과 출력
|
||||
|
||||
live root에 노출하는 최소 interface를 적습니다.
|
||||
|
||||
## 제약
|
||||
|
||||
- backend와 environment credential을 선언하지 않습니다.
|
||||
- 특정 live 경로를 역참조하지 않습니다.
|
||||
- component가 한 번만 필요하면 stack 계층을 만들지 않습니다.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Infrastructure Tests
|
||||
|
||||
IaC를 선택한 뒤 다음 검증을 필요에 따라 추가합니다.
|
||||
|
||||
- formatter와 syntax/validate
|
||||
- component input/output contract test
|
||||
- policy/static analysis
|
||||
- ephemeral account/project integration test
|
||||
- upgrade와 state migration test
|
||||
|
||||
실제 cloud 통합 테스트는 일반 PR 검증과 분리하고, 짧은 수명의 identity와
|
||||
격리된 account/project를 사용합니다. 테스트가 production state를 읽거나
|
||||
변경해서는 안 됩니다.
|
||||
Reference in New Issue
Block a user