@percepta/create 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -0
- package/dist/chunk-GEVZERMP.js +108 -0
- package/dist/chunk-R4FWPE4A.js +49 -0
- package/dist/chunk-WMJT7CB5.js +57 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +974 -0
- package/dist/init-Z4VGBHAK.js +96 -0
- package/dist/status-MITGDLTT.js +76 -0
- package/dist/sync-J4SFZHDX.js +136 -0
- package/dist/upstream-AQI7P4EU.js +144 -0
- package/package.json +58 -0
- package/template-versions.json +4 -0
- package/templates/library/README.md +30 -0
- package/templates/library/eslint.config.js +10 -0
- package/templates/library/gitignore.template +18 -0
- package/templates/library/package.json.template +29 -0
- package/templates/library/src/index.ts +9 -0
- package/templates/library/tsconfig.json +19 -0
- package/templates/monorepo/README.md +41 -0
- package/templates/monorepo/eslint.config.js +10 -0
- package/templates/monorepo/gitignore.template +31 -0
- package/templates/monorepo/npmrc.template +4 -0
- package/templates/monorepo/package.json.template +25 -0
- package/templates/monorepo/packages/.gitkeep +0 -0
- package/templates/monorepo/pnpm-workspace.yaml +2 -0
- package/templates/monorepo/tsconfig.json +16 -0
- package/templates/webapp/.claude/commands/sync.md +19 -0
- package/templates/webapp/.claude/commands/upstream.md +17 -0
- package/templates/webapp/.dockerignore +59 -0
- package/templates/webapp/.gitattributes +1 -0
- package/templates/webapp/.github/workflows/__APP_NAME__-ryvn-release.yaml +114 -0
- package/templates/webapp/.github/workflows/__APP_NAME__-terraform.yml +28 -0
- package/templates/webapp/.github/workflows/ci.yml +149 -0
- package/templates/webapp/.node-version +2 -0
- package/templates/webapp/.prettierrc.mjs +5 -0
- package/templates/webapp/AGENTS.md +240 -0
- package/templates/webapp/Dockerfile +64 -0
- package/templates/webapp/README.md +200 -0
- package/templates/webapp/agent-skills/database.md +140 -0
- package/templates/webapp/agent-skills/deploy.md +94 -0
- package/templates/webapp/agent-skills/inngest.md +147 -0
- package/templates/webapp/agent-skills/langfuse.md +117 -0
- package/templates/webapp/agent-skills/oneshot.md +216 -0
- package/templates/webapp/agent-skills/ryvn.md +25 -0
- package/templates/webapp/deploy/README.md +39 -0
- package/templates/webapp/deploy/ryvn/__APP_NAME__.service.yaml +11 -0
- package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__.env.percepta-test.serviceinstallation.yaml +121 -0
- package/templates/webapp/docker-compose.yml +19 -0
- package/templates/webapp/drizzle.config.ts +30 -0
- package/templates/webapp/env.example.template +44 -0
- package/templates/webapp/eslint.config.mjs +52 -0
- package/templates/webapp/gitignore.template +53 -0
- package/templates/webapp/next.config.ts +8 -0
- package/templates/webapp/npmrc.template +4 -0
- package/templates/webapp/package.json.template +122 -0
- package/templates/webapp/postcss.config.mjs +5 -0
- package/templates/webapp/scripts/create-user.ts +47 -0
- package/templates/webapp/scripts/migrate.ts +18 -0
- package/templates/webapp/scripts/seed.ts +62 -0
- package/templates/webapp/scripts/setup-database.ts +57 -0
- package/templates/webapp/scripts/setup-readonly-user.ts +193 -0
- package/templates/webapp/scripts/start.sh +52 -0
- package/templates/webapp/src/app/(app)/layout.tsx +21 -0
- package/templates/webapp/src/app/(app)/page.tsx +30 -0
- package/templates/webapp/src/app/(auth)/auth/signin/CredentialsSignInForm.tsx +103 -0
- package/templates/webapp/src/app/(auth)/auth/signin/page.tsx +30 -0
- package/templates/webapp/src/app/(auth)/layout.tsx +15 -0
- package/templates/webapp/src/app/api/auth/[...all]/route.ts +4 -0
- package/templates/webapp/src/app/api/healthz/route.ts +10 -0
- package/templates/webapp/src/app/api/inngest/route.ts +31 -0
- package/templates/webapp/src/app/api/readyz/route.ts +31 -0
- package/templates/webapp/src/app/api/trpc/[trpc]/route.ts +21 -0
- package/templates/webapp/src/app/favicon.ico +0 -0
- package/templates/webapp/src/app/global-error.tsx +27 -0
- package/templates/webapp/src/app/layout.tsx +18 -0
- package/templates/webapp/src/components/FaroProvider.tsx +37 -0
- package/templates/webapp/src/components/Header.tsx +70 -0
- package/templates/webapp/src/components/Providers.tsx +45 -0
- package/templates/webapp/src/components/form/FormItem.tsx +82 -0
- package/templates/webapp/src/config/clientEnvConfig.ts +11 -0
- package/templates/webapp/src/config/getEnvConfig.ts +62 -0
- package/templates/webapp/src/config/isDev.ts +7 -0
- package/templates/webapp/src/drizzle/db.ts +28 -0
- package/templates/webapp/src/drizzle/migrations/0000_eager_grandmaster.sql +57 -0
- package/templates/webapp/src/drizzle/migrations/meta/0000_snapshot.json +376 -0
- package/templates/webapp/src/drizzle/migrations/meta/_journal.json +13 -0
- package/templates/webapp/src/drizzle/schema/auth/accounts.ts +33 -0
- package/templates/webapp/src/drizzle/schema/auth/sessions.ts +25 -0
- package/templates/webapp/src/drizzle/schema/auth/users.ts +38 -0
- package/templates/webapp/src/drizzle/schema/auth/verifications.ts +19 -0
- package/templates/webapp/src/drizzle/schema/index.ts +4 -0
- package/templates/webapp/src/drizzle/schema/utils/jsonbFromZod.ts +25 -0
- package/templates/webapp/src/instrumentation.ts +35 -0
- package/templates/webapp/src/lib/auth/index.ts +85 -0
- package/templates/webapp/src/lib/auth-client.ts +6 -0
- package/templates/webapp/src/lib/trpc.ts +15 -0
- package/templates/webapp/src/server/api/root.ts +5 -0
- package/templates/webapp/src/server/trpc.ts +61 -0
- package/templates/webapp/src/services/AuthContextService.ts +63 -0
- package/templates/webapp/src/services/DatabaseService.ts +54 -0
- package/templates/webapp/src/services/inngest/InngestFunctionCollection.ts +5 -0
- package/templates/webapp/src/services/inngest/InngestService.ts +71 -0
- package/templates/webapp/src/services/inngest/events/AppEvents.ts +34 -0
- package/templates/webapp/src/services/inngest/events/payloads/ExampleEventPayload.ts +14 -0
- package/templates/webapp/src/services/langfuse/LangfuseService.ts +80 -0
- package/templates/webapp/src/services/logger/AppLogger.ts +61 -0
- package/templates/webapp/src/services/logger/withRequestContext.ts +27 -0
- package/templates/webapp/src/services/observability/initFaro.ts +22 -0
- package/templates/webapp/src/startup-checks.ts +32 -0
- package/templates/webapp/src/styles/globals.css +27 -0
- package/templates/webapp/src/utils/__tests__/cn.test.ts +20 -0
- package/templates/webapp/src/utils/cn.ts +6 -0
- package/templates/webapp/src/utils/syncInngestApp.ts +62 -0
- package/templates/webapp/terraform/README.md +147 -0
- package/templates/webapp/terraform/deploy.sh +97 -0
- package/templates/webapp/terraform/main.tf +101 -0
- package/templates/webapp/terraform/modules/cloudtrail/main.tf +27 -0
- package/templates/webapp/terraform/modules/cloudtrail/outputs.tf +10 -0
- package/templates/webapp/terraform/modules/cloudtrail/variables.tf +15 -0
- package/templates/webapp/terraform/modules/networking/main.tf +118 -0
- package/templates/webapp/terraform/modules/networking/outputs.tf +38 -0
- package/templates/webapp/terraform/modules/networking/variables.tf +24 -0
- package/templates/webapp/terraform/modules/rds/main.tf +227 -0
- package/templates/webapp/terraform/modules/rds/outputs.tf +73 -0
- package/templates/webapp/terraform/modules/rds/variables.tf +61 -0
- package/templates/webapp/terraform/modules/s3-logging/main.tf +148 -0
- package/templates/webapp/terraform/modules/s3-logging/outputs.tf +10 -0
- package/templates/webapp/terraform/modules/s3-logging/variables.tf +16 -0
- package/templates/webapp/terraform/modules/secrets/main.tf +39 -0
- package/templates/webapp/terraform/modules/secrets/outputs.tf +9 -0
- package/templates/webapp/terraform/modules/secrets/variables.tf +51 -0
- package/templates/webapp/terraform/outputs.tf +102 -0
- package/templates/webapp/terraform/providers.tf +32 -0
- package/templates/webapp/terraform/terraform.tfvars.example +65 -0
- package/templates/webapp/terraform/variables.tf +129 -0
- package/templates/webapp/tsconfig.json +14 -0
- package/templates/webapp/vitest.config.ts +9 -0
- package/templates/webapp/vitest.setup.ts +5 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
data "aws_region" "current" {}
|
|
2
|
+
|
|
3
|
+
data "http" "rds_ca" {
|
|
4
|
+
url = "https://truststore.pki.rds.amazonaws.com/${data.aws_region.current.name}/${data.aws_region.current.name}-bundle.pem"
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
data "aws_vpc" "vpc" {
|
|
8
|
+
id = var.vpc_id
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
data "aws_subnet" "subnet" {
|
|
12
|
+
count = length(var.subnet_ids)
|
|
13
|
+
id = var.subnet_ids[count.index]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
# Check if existing cluster exists
|
|
17
|
+
data "aws_rds_cluster" "existing" {
|
|
18
|
+
count = var.existing_cluster_name != null ? 1 : 0
|
|
19
|
+
cluster_identifier = var.existing_cluster_name
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
resource "random_pet" "name" {
|
|
23
|
+
count = var.create_new_cluster && var.existing_cluster_name == null ? 1 : 0
|
|
24
|
+
length = 2
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
resource "random_password" "master_password" {
|
|
28
|
+
count = var.create_new_cluster && var.existing_cluster_name == null ? 1 : 0
|
|
29
|
+
length = 16
|
|
30
|
+
special = true
|
|
31
|
+
override_special = "_!%^"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
locals {
|
|
36
|
+
cluster_name = var.existing_cluster_name != null ? var.existing_cluster_name : (var.create_new_cluster ? "${var.name}-${random_pet.name[0].id}" : null)
|
|
37
|
+
|
|
38
|
+
# Use existing cluster details if available, otherwise use new cluster details
|
|
39
|
+
cluster_endpoint = var.existing_cluster_name != null ? data.aws_rds_cluster.existing[0].endpoint : (var.create_new_cluster ? aws_rds_cluster.rds[0].endpoint : null)
|
|
40
|
+
cluster_port = var.existing_cluster_name != null ? data.aws_rds_cluster.existing[0].port : (var.create_new_cluster ? aws_rds_cluster.rds[0].port : null)
|
|
41
|
+
master_username = var.existing_cluster_name != null ? data.aws_rds_cluster.existing[0].master_username : (var.create_new_cluster ? aws_rds_cluster.rds[0].master_username : null)
|
|
42
|
+
master_password = var.existing_cluster_name != null ? null : (var.create_new_cluster ? random_password.master_password[0].result : null)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Security group for new RDS cluster
|
|
46
|
+
resource "aws_security_group" "security_group" {
|
|
47
|
+
count = var.create_new_cluster && var.existing_cluster_name == null ? 1 : 0
|
|
48
|
+
name = "rds-__APP_NAME__-${random_pet.name[0].id}"
|
|
49
|
+
description = "Security group for __APP_NAME_UPPER__ RDS ${random_pet.name[0].id}"
|
|
50
|
+
vpc_id = data.aws_vpc.vpc.id
|
|
51
|
+
|
|
52
|
+
ingress {
|
|
53
|
+
description = "Allow Postgres from VPC"
|
|
54
|
+
from_port = var.port
|
|
55
|
+
to_port = var.port
|
|
56
|
+
protocol = "tcp"
|
|
57
|
+
cidr_blocks = [data.aws_vpc.vpc.cidr_block]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
egress {
|
|
61
|
+
from_port = 0
|
|
62
|
+
to_port = 0
|
|
63
|
+
protocol = "-1"
|
|
64
|
+
cidr_blocks = ["0.0.0.0/0"]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Subnet group for new RDS cluster
|
|
69
|
+
resource "aws_db_subnet_group" "subnet_group" {
|
|
70
|
+
count = var.create_new_cluster && var.existing_cluster_name == null ? 1 : 0
|
|
71
|
+
name = "rds-__APP_NAME__-${random_pet.name[0].id}"
|
|
72
|
+
description = "Subnet group for __APP_NAME_UPPER__ RDS ${random_pet.name[0].id}"
|
|
73
|
+
subnet_ids = var.subnet_ids
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# New RDS cluster
|
|
77
|
+
resource "aws_rds_cluster" "rds" {
|
|
78
|
+
count = var.create_new_cluster && var.existing_cluster_name == null ? 1 : 0
|
|
79
|
+
cluster_identifier = local.cluster_name
|
|
80
|
+
engine = "aurora-postgresql"
|
|
81
|
+
engine_mode = "provisioned"
|
|
82
|
+
engine_version = var.engine_version
|
|
83
|
+
vpc_security_group_ids = [aws_security_group.security_group[0].id]
|
|
84
|
+
db_subnet_group_name = aws_db_subnet_group.subnet_group[0].name
|
|
85
|
+
availability_zones = slice(data.aws_subnet.subnet[*].availability_zone, 0, min(3, length(data.aws_subnet.subnet)))
|
|
86
|
+
database_name = "__DB_NAME__"
|
|
87
|
+
port = var.port
|
|
88
|
+
master_username = "__APP_NAME_SNAKE___admin"
|
|
89
|
+
master_password = random_password.master_password[0].result
|
|
90
|
+
storage_encrypted = true
|
|
91
|
+
backup_retention_period = 5
|
|
92
|
+
preferred_backup_window = "07:00-09:00"
|
|
93
|
+
|
|
94
|
+
skip_final_snapshot = true
|
|
95
|
+
enable_http_endpoint = true
|
|
96
|
+
|
|
97
|
+
serverlessv2_scaling_configuration {
|
|
98
|
+
max_capacity = 1.0
|
|
99
|
+
min_capacity = 0.0
|
|
100
|
+
seconds_until_auto_pause = 3600
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# RDS cluster instance for new cluster
|
|
105
|
+
resource "aws_rds_cluster_instance" "instance" {
|
|
106
|
+
count = var.create_new_cluster && var.existing_cluster_name == null ? 1 : 0
|
|
107
|
+
identifier = "${local.cluster_name}-instance-${count.index}"
|
|
108
|
+
cluster_identifier = aws_rds_cluster.rds[0].id
|
|
109
|
+
instance_class = var.instance_class
|
|
110
|
+
engine = aws_rds_cluster.rds[0].engine
|
|
111
|
+
engine_version = aws_rds_cluster.rds[0].engine_version
|
|
112
|
+
publicly_accessible = false
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# Store master password in AWS Secrets Manager for new cluster
|
|
116
|
+
resource "aws_secretsmanager_secret" "master_password" {
|
|
117
|
+
count = var.create_new_cluster && var.existing_cluster_name == null ? 1 : 0
|
|
118
|
+
name_prefix = "rds-__APP_NAME__-master-password-${random_pet.name[0].id}-"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
resource "aws_secretsmanager_secret_version" "master_password" {
|
|
122
|
+
count = var.create_new_cluster && var.existing_cluster_name == null ? 1 : 0
|
|
123
|
+
secret_id = aws_secretsmanager_secret.master_password[0].id
|
|
124
|
+
secret_string = random_password.master_password[0].result
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
################################################################################
|
|
128
|
+
# EDW Readonly User Resources
|
|
129
|
+
################################################################################
|
|
130
|
+
|
|
131
|
+
# Generate secure password for readonly user
|
|
132
|
+
resource "random_password" "readonly_user_password" {
|
|
133
|
+
length = 32
|
|
134
|
+
special = true
|
|
135
|
+
# Use PostgreSQL-safe special characters
|
|
136
|
+
override_special = "!#$%&*()-_=+[]{}<>:?"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Store readonly user credentials in Secrets Manager
|
|
140
|
+
resource "aws_secretsmanager_secret" "readonly_user_credentials" {
|
|
141
|
+
name_prefix = "rds-__APP_NAME__-readonly-${var.name}-${var.environment}-"
|
|
142
|
+
description = "Readonly database credentials for EDW access to __APP_NAME_UPPER__ database"
|
|
143
|
+
|
|
144
|
+
tags = {
|
|
145
|
+
Name = "__APP_NAME__-readonly-credentials"
|
|
146
|
+
Environment = var.environment
|
|
147
|
+
Purpose = "EDW"
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
resource "aws_secretsmanager_secret_version" "readonly_user_credentials" {
|
|
152
|
+
secret_id = aws_secretsmanager_secret.readonly_user_credentials.id
|
|
153
|
+
secret_string = jsonencode({
|
|
154
|
+
username = "__APP_NAME_SNAKE___readonly"
|
|
155
|
+
password = random_password.readonly_user_password.result
|
|
156
|
+
host = local.cluster_endpoint
|
|
157
|
+
port = local.cluster_port
|
|
158
|
+
database = "__DB_NAME__"
|
|
159
|
+
engine = "postgres"
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# Cross-account IAM role for EDW to assume
|
|
164
|
+
resource "aws_iam_role" "edw_secret_reader_role" {
|
|
165
|
+
count = length(var.edw_allowed_principals) > 0 ? 1 : 0
|
|
166
|
+
name = "__APP_NAME_UPPER__-EDW-SecretReader-${var.name}-${var.environment}"
|
|
167
|
+
|
|
168
|
+
assume_role_policy = jsonencode({
|
|
169
|
+
Version = "2012-10-17"
|
|
170
|
+
Statement = [
|
|
171
|
+
{
|
|
172
|
+
Effect = "Allow"
|
|
173
|
+
Principal = {
|
|
174
|
+
AWS = var.edw_allowed_principals
|
|
175
|
+
}
|
|
176
|
+
Action = "sts:AssumeRole"
|
|
177
|
+
}
|
|
178
|
+
]
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
tags = {
|
|
182
|
+
Name = "__APP_NAME__-edw-secret-reader"
|
|
183
|
+
Environment = var.environment
|
|
184
|
+
Purpose = "EDW"
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# IAM policy to allow reading the readonly credentials secret
|
|
189
|
+
resource "aws_iam_policy" "edw_secret_reader_policy" {
|
|
190
|
+
count = length(var.edw_allowed_principals) > 0 ? 1 : 0
|
|
191
|
+
name = "__APP_NAME_UPPER__-EDW-SecretReader-Policy-${var.name}-${var.environment}"
|
|
192
|
+
description = "Allows EDW to read __APP_NAME_UPPER__ readonly database credentials from Secrets Manager"
|
|
193
|
+
|
|
194
|
+
policy = jsonencode({
|
|
195
|
+
Version = "2012-10-17"
|
|
196
|
+
Statement = [
|
|
197
|
+
{
|
|
198
|
+
Effect = "Allow"
|
|
199
|
+
Action = [
|
|
200
|
+
"secretsmanager:GetSecretValue",
|
|
201
|
+
"secretsmanager:DescribeSecret"
|
|
202
|
+
]
|
|
203
|
+
Resource = aws_secretsmanager_secret.readonly_user_credentials.arn
|
|
204
|
+
}
|
|
205
|
+
]
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
resource "aws_iam_role_policy_attachment" "edw_secret_reader_attach" {
|
|
210
|
+
count = length(var.edw_allowed_principals) > 0 ? 1 : 0
|
|
211
|
+
role = aws_iam_role.edw_secret_reader_role[0].name
|
|
212
|
+
policy_arn = aws_iam_policy.edw_secret_reader_policy[0].arn
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
# Add ingress rule to security group for EDW VPC CIDR blocks
|
|
216
|
+
resource "aws_security_group_rule" "edw_ingress" {
|
|
217
|
+
count = var.create_new_cluster && var.existing_cluster_name == null && length(var.edw_vpc_cidr_blocks) > 0 ? 1 : 0
|
|
218
|
+
type = "ingress"
|
|
219
|
+
description = "Allow Postgres from EDW VPC via VPC peering"
|
|
220
|
+
from_port = var.port
|
|
221
|
+
to_port = var.port
|
|
222
|
+
protocol = "tcp"
|
|
223
|
+
cidr_blocks = var.edw_vpc_cidr_blocks
|
|
224
|
+
security_group_id = aws_security_group.security_group[0].id
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
output "host" {
|
|
2
|
+
description = "RDS cluster endpoint"
|
|
3
|
+
value = local.cluster_endpoint
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
output "port" {
|
|
7
|
+
description = "RDS cluster port"
|
|
8
|
+
value = local.cluster_port
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
output "database_name" {
|
|
12
|
+
description = "Database name"
|
|
13
|
+
value = "__DB_NAME__"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
output "username" {
|
|
17
|
+
description = "Database username (master user)"
|
|
18
|
+
value = local.master_username
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
output "password" {
|
|
22
|
+
description = "Database password (master user)"
|
|
23
|
+
value = local.master_password
|
|
24
|
+
sensitive = true
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
output "ssl_cert" {
|
|
28
|
+
description = "RDS CA certificate"
|
|
29
|
+
value = data.http.rds_ca.response_body
|
|
30
|
+
sensitive = true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
output "connection_url" {
|
|
34
|
+
description = "PostgreSQL connection URL"
|
|
35
|
+
value = "postgresql://${local.master_username}:${local.master_password}@${local.cluster_endpoint}:${local.cluster_port}/__DB_NAME__"
|
|
36
|
+
sensitive = true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
output "cluster_name" {
|
|
40
|
+
description = "RDS cluster name"
|
|
41
|
+
value = local.cluster_name
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
output "master_username" {
|
|
45
|
+
description = "RDS master username (for new clusters only)"
|
|
46
|
+
value = local.master_username
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
output "master_password" {
|
|
50
|
+
description = "RDS master password (for new clusters only)"
|
|
51
|
+
value = local.master_password
|
|
52
|
+
sensitive = true
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
output "readonly_username" {
|
|
56
|
+
description = "Readonly database username for EDW access"
|
|
57
|
+
value = "__APP_NAME_SNAKE___readonly"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
output "readonly_user_secret_arn" {
|
|
61
|
+
description = "ARN of the Secrets Manager secret containing readonly user credentials"
|
|
62
|
+
value = aws_secretsmanager_secret.readonly_user_credentials.arn
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
output "readonly_user_secret_name" {
|
|
66
|
+
description = "Name of the Secrets Manager secret containing readonly user credentials"
|
|
67
|
+
value = aws_secretsmanager_secret.readonly_user_credentials.name
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
output "readonly_user_secret_reader_role_arn" {
|
|
71
|
+
description = "ARN of the IAM role that EDW can assume to read the readonly credentials secret"
|
|
72
|
+
value = length(var.edw_allowed_principals) > 0 ? aws_iam_role.edw_secret_reader_role[0].arn : null
|
|
73
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
variable "name" {
|
|
2
|
+
description = "Base name for resources"
|
|
3
|
+
type = string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
variable "environment" {
|
|
7
|
+
description = "Environment name (e.g. dev, staging, prod)"
|
|
8
|
+
type = string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
variable "existing_cluster_name" {
|
|
12
|
+
description = "Name of existing RDS cluster to use (optional)"
|
|
13
|
+
type = string
|
|
14
|
+
default = null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
variable "create_new_cluster" {
|
|
18
|
+
description = "Whether to create a new RDS cluster if existing_cluster_name is not provided"
|
|
19
|
+
type = bool
|
|
20
|
+
default = true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
variable "vpc_id" {
|
|
24
|
+
description = "VPC ID where resources will be deployed"
|
|
25
|
+
type = string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
variable "subnet_ids" {
|
|
29
|
+
description = "List of subnet IDs for RDS"
|
|
30
|
+
type = list(string)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
variable "engine_version" {
|
|
34
|
+
description = "PostgreSQL engine version"
|
|
35
|
+
type = string
|
|
36
|
+
default = "16.8"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
variable "port" {
|
|
40
|
+
description = "Port for RDS"
|
|
41
|
+
type = number
|
|
42
|
+
default = 5432
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
variable "instance_class" {
|
|
46
|
+
description = "RDS instance class"
|
|
47
|
+
type = string
|
|
48
|
+
default = "db.serverless"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
variable "edw_allowed_principals" {
|
|
52
|
+
description = "List of IAM principal ARNs allowed to assume the EDW secret reader role"
|
|
53
|
+
type = list(string)
|
|
54
|
+
default = []
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
variable "edw_vpc_cidr_blocks" {
|
|
58
|
+
description = "List of CIDR blocks from EDW VPC to allow database access via VPC peering"
|
|
59
|
+
type = list(string)
|
|
60
|
+
default = []
|
|
61
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
data "aws_caller_identity" "current" {}
|
|
2
|
+
|
|
3
|
+
resource "random_string" "suffix" {
|
|
4
|
+
length = 8
|
|
5
|
+
special = false
|
|
6
|
+
upper = false
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
################################################################################
|
|
10
|
+
# S3 Bucket for Access Logs
|
|
11
|
+
################################################################################
|
|
12
|
+
|
|
13
|
+
resource "aws_s3_bucket" "logs" {
|
|
14
|
+
bucket = "${var.name}-logs-${random_string.suffix.result}"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
resource "aws_s3_bucket_public_access_block" "logs" {
|
|
18
|
+
bucket = aws_s3_bucket.logs.id
|
|
19
|
+
block_public_acls = true
|
|
20
|
+
block_public_policy = true
|
|
21
|
+
ignore_public_acls = true
|
|
22
|
+
restrict_public_buckets = true
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
resource "aws_s3_bucket_ownership_controls" "logs" {
|
|
26
|
+
bucket = aws_s3_bucket.logs.id
|
|
27
|
+
rule {
|
|
28
|
+
object_ownership = "BucketOwnerPreferred"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
resource "aws_s3_bucket_versioning" "logs" {
|
|
33
|
+
bucket = aws_s3_bucket.logs.id
|
|
34
|
+
versioning_configuration {
|
|
35
|
+
status = "Enabled"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
resource "aws_s3_bucket_acl" "logs" {
|
|
40
|
+
depends_on = [aws_s3_bucket_ownership_controls.logs]
|
|
41
|
+
bucket = aws_s3_bucket.logs.id
|
|
42
|
+
acl = "private"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
resource "aws_s3_bucket_server_side_encryption_configuration" "logs" {
|
|
46
|
+
bucket = aws_s3_bucket.logs.id
|
|
47
|
+
|
|
48
|
+
rule {
|
|
49
|
+
apply_server_side_encryption_by_default {
|
|
50
|
+
sse_algorithm = "AES256"
|
|
51
|
+
}
|
|
52
|
+
bucket_key_enabled = true
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
resource "aws_s3_bucket_lifecycle_configuration" "logs" {
|
|
57
|
+
count = var.s3_expiration_days != null ? 1 : 0
|
|
58
|
+
bucket = aws_s3_bucket.logs.bucket
|
|
59
|
+
|
|
60
|
+
rule {
|
|
61
|
+
id = "expire-objects"
|
|
62
|
+
status = "Enabled"
|
|
63
|
+
filter {
|
|
64
|
+
prefix = ""
|
|
65
|
+
}
|
|
66
|
+
expiration {
|
|
67
|
+
days = var.s3_expiration_days
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Bucket policy to allow S3 and CloudTrail services to write logs
|
|
73
|
+
resource "aws_s3_bucket_policy" "logs" {
|
|
74
|
+
bucket = aws_s3_bucket.logs.bucket
|
|
75
|
+
policy = jsonencode({
|
|
76
|
+
Version = "2012-10-17"
|
|
77
|
+
Statement = [
|
|
78
|
+
{
|
|
79
|
+
Sid = "AWSCloudTrailAclCheck"
|
|
80
|
+
Effect = "Allow"
|
|
81
|
+
Principal = {
|
|
82
|
+
Service = "cloudtrail.amazonaws.com"
|
|
83
|
+
}
|
|
84
|
+
Action = "s3:GetBucketAcl"
|
|
85
|
+
Resource = aws_s3_bucket.logs.arn
|
|
86
|
+
Condition = {
|
|
87
|
+
StringEquals = {
|
|
88
|
+
"aws:SourceAccount" = data.aws_caller_identity.current.account_id
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
Sid = "AWSCloudTrailWrite"
|
|
94
|
+
Effect = "Allow"
|
|
95
|
+
Principal = {
|
|
96
|
+
Service = "cloudtrail.amazonaws.com"
|
|
97
|
+
}
|
|
98
|
+
Action = "s3:PutObject"
|
|
99
|
+
Resource = "${aws_s3_bucket.logs.arn}/cloudtrail/*"
|
|
100
|
+
Condition = {
|
|
101
|
+
StringEquals = {
|
|
102
|
+
"s3:x-amz-acl" = "bucket-owner-full-control"
|
|
103
|
+
"aws:SourceAccount" = data.aws_caller_identity.current.account_id
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
Sid = "AllowS3LogDeliveryAcl"
|
|
109
|
+
Effect = "Allow"
|
|
110
|
+
Principal = {
|
|
111
|
+
Service = "s3.amazonaws.com"
|
|
112
|
+
}
|
|
113
|
+
Action = [
|
|
114
|
+
"s3:GetBucketAcl",
|
|
115
|
+
"s3:GetBucketLocation"
|
|
116
|
+
]
|
|
117
|
+
Resource = aws_s3_bucket.logs.arn
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
Sid = "AllowS3LogDeliveryPut"
|
|
121
|
+
Effect = "Allow"
|
|
122
|
+
Principal = {
|
|
123
|
+
Service = "s3.amazonaws.com"
|
|
124
|
+
}
|
|
125
|
+
Action = "s3:PutObject"
|
|
126
|
+
Resource = "${aws_s3_bucket.logs.arn}/*"
|
|
127
|
+
Condition = {
|
|
128
|
+
StringEquals = {
|
|
129
|
+
"s3:x-amz-acl" = "bucket-owner-full-control"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
Sid = "DenyUnEncryptedObjectUploads"
|
|
135
|
+
Effect = "Deny"
|
|
136
|
+
Principal = "*"
|
|
137
|
+
Action = "s3:*"
|
|
138
|
+
Resource = "${aws_s3_bucket.logs.arn}/*"
|
|
139
|
+
Condition = {
|
|
140
|
+
Bool = {
|
|
141
|
+
"aws:SecureTransport" = "false"
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
]
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
variable "name" {
|
|
2
|
+
description = "Base name for resources"
|
|
3
|
+
type = string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
variable "environment" {
|
|
7
|
+
description = "Environment name (e.g. dev, staging, prod)"
|
|
8
|
+
type = string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
variable "s3_expiration_days" {
|
|
12
|
+
description = "Number of days after which S3 objects expire (null to disable expiration)"
|
|
13
|
+
type = number
|
|
14
|
+
default = null
|
|
15
|
+
}
|
|
16
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
data "aws_region" "current" {}
|
|
2
|
+
|
|
3
|
+
################################################################################
|
|
4
|
+
# Database Credentials Secret
|
|
5
|
+
################################################################################
|
|
6
|
+
|
|
7
|
+
resource "kubernetes_secret" "database_credentials" {
|
|
8
|
+
metadata {
|
|
9
|
+
name = "__APP_NAME__-database-credentials"
|
|
10
|
+
namespace = var.namespace
|
|
11
|
+
}
|
|
12
|
+
type = "Opaque"
|
|
13
|
+
data = {
|
|
14
|
+
host = var.db_host
|
|
15
|
+
port = tostring(var.db_port)
|
|
16
|
+
database = var.db_name
|
|
17
|
+
username = var.db_username
|
|
18
|
+
password = var.db_password
|
|
19
|
+
ssl_cert = var.db_ssl_cert
|
|
20
|
+
database_url = var.db_url
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# PostgreSQL Secret (Langfuse-style structure)
|
|
25
|
+
resource "kubernetes_secret" "postgresql" {
|
|
26
|
+
metadata {
|
|
27
|
+
name = "__APP_NAME__-postgresql"
|
|
28
|
+
namespace = var.namespace
|
|
29
|
+
}
|
|
30
|
+
type = "Opaque"
|
|
31
|
+
data = {
|
|
32
|
+
postgres-password = var.db_password
|
|
33
|
+
database_url = "postgresql://${var.db_username}:${var.db_password}@${var.db_host}:${var.db_port}/${var.db_name}"
|
|
34
|
+
database_name = var.db_name
|
|
35
|
+
host = var.db_host
|
|
36
|
+
port = tostring(var.db_port)
|
|
37
|
+
username = var.db_username
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
output "database_secret_name" {
|
|
2
|
+
description = "Name of the Kubernetes secret containing database credentials"
|
|
3
|
+
value = kubernetes_secret.database_credentials.metadata[0].name
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
output "postgresql_secret_name" {
|
|
7
|
+
description = "Name of the Kubernetes secret containing PostgreSQL credentials (Langfuse-style)"
|
|
8
|
+
value = kubernetes_secret.postgresql.metadata[0].name
|
|
9
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
variable "namespace" {
|
|
2
|
+
description = "Kubernetes namespace for secrets"
|
|
3
|
+
type = string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
variable "cluster_name" {
|
|
7
|
+
description = "EKS cluster name"
|
|
8
|
+
type = string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
################################################################################
|
|
12
|
+
# Database Variables
|
|
13
|
+
################################################################################
|
|
14
|
+
|
|
15
|
+
variable "db_host" {
|
|
16
|
+
description = "Database host"
|
|
17
|
+
type = string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
variable "db_port" {
|
|
21
|
+
description = "Database port"
|
|
22
|
+
type = number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
variable "db_name" {
|
|
26
|
+
description = "Database name"
|
|
27
|
+
type = string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
variable "db_username" {
|
|
31
|
+
description = "Database username"
|
|
32
|
+
type = string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
variable "db_password" {
|
|
36
|
+
description = "Database password"
|
|
37
|
+
type = string
|
|
38
|
+
sensitive = true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
variable "db_ssl_cert" {
|
|
42
|
+
description = "Database SSL certificate"
|
|
43
|
+
type = string
|
|
44
|
+
sensitive = true
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
variable "db_url" {
|
|
48
|
+
description = "Database connection URL"
|
|
49
|
+
type = string
|
|
50
|
+
sensitive = true
|
|
51
|
+
}
|