BuzzerboyAWSLightsail 0.330.1__py3-none-any.whl → 0.332.1__py3-none-any.whl

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.
@@ -0,0 +1,385 @@
1
+ """
2
+ AWS Lightsail Mini Infrastructure Stack
3
+ ======================================
4
+
5
+ This module provides a comprehensive AWS Lightsail infrastructure deployment stack
6
+ using CDKTF (Cloud Development Kit for Terraform) with Python.
7
+
8
+ The stack includes:
9
+ * Lightsail Container Service with automatic custom domain attachment
10
+ * PostgreSQL Database (optional)
11
+ * DNS management with CNAME records
12
+ * SSL certificate management with automatic validation
13
+ * IAM resources for service access
14
+ * S3 bucket for application data
15
+ * Secrets Manager for credential storage
16
+
17
+ :author: Generated with GitHub Copilot
18
+ :version: 1.0.0
19
+ :license: MIT
20
+ """
21
+
22
+
23
+ #region specific imports
24
+
25
+ import os
26
+ import json
27
+ from enum import Enum
28
+ from constructs import Construct
29
+ from cdktf import TerraformOutput
30
+
31
+ # Import the base class
32
+ from .LightsailBaseStandalone import LightsailBaseStandalone, BaseLightsailArchitectureFlags
33
+
34
+ #endregion
35
+
36
+ #region AWS Provider and Resources
37
+ from cdktf_cdktf_provider_aws.provider import AwsProvider
38
+ from cdktf_cdktf_provider_aws import (
39
+ lightsail_container_service,
40
+ lightsail_database,
41
+ cloudfront_distribution,
42
+ s3_bucket,
43
+ )
44
+ #endregion
45
+
46
+ #region Random Provider and Resources
47
+ from cdktf_cdktf_provider_random import password
48
+
49
+ # AWS WAF (currently unused but imported for future use)
50
+ from cdktf_cdktf_provider_aws.wafv2_web_acl import (
51
+ Wafv2WebAcl,
52
+ Wafv2WebAclDefaultAction,
53
+ Wafv2WebAclRule,
54
+ Wafv2WebAclVisibilityConfig,
55
+ Wafv2WebAclDefaultActionAllow,
56
+ Wafv2WebAclRuleOverrideAction,
57
+ Wafv2WebAclRuleOverrideActionNone,
58
+ Wafv2WebAclRuleOverrideActionCount,
59
+ Wafv2WebAclRuleVisibilityConfig,
60
+ )
61
+ from cdktf_cdktf_provider_aws.wafv2_web_acl_association import Wafv2WebAclAssociation
62
+ from cdktf_cdktf_provider_aws.wafv2_rule_group import Wafv2RuleGroupRuleVisibilityConfig
63
+
64
+ #endregion
65
+
66
+
67
+
68
+ #region ArchitectureFlags
69
+ class ArchitectureFlags(Enum):
70
+ """
71
+ Architecture configuration flags for optional components.
72
+
73
+ Includes both base flags and container-specific flags.
74
+
75
+ Base flags:
76
+ :param SKIP_DEFAULT_POST_APPLY_SCRIPTS: Skip default post-apply scripts
77
+ :param PRESERVE_EXISTING_SECRETS: Don't overwrite existing secret versions (smart detection)
78
+ :param IGNORE_SECRET_CHANGES: Ignore all changes to secret after initial creation
79
+
80
+ Container-specific flags:
81
+ :param SKIP_DATABASE: Skip database creation
82
+ :param SKIP_DOMAIN: Skip domain and DNS configuration
83
+ """
84
+
85
+ # Base flags from BaseLightsailArchitectureFlags
86
+ SKIP_DEFAULT_POST_APPLY_SCRIPTS = "skip_default_post_apply_scripts"
87
+ PRESERVE_EXISTING_SECRETS = "preserve_existing_secrets"
88
+ IGNORE_SECRET_CHANGES = "ignore_secret_changes"
89
+
90
+ # Container-specific flags
91
+ SKIP_DATABASE = "skip_database"
92
+ SKIP_DOMAIN = "skip_domain"
93
+
94
+ #endregion
95
+
96
+
97
+ class LightsailContainerStandaloneStack(LightsailBaseStandalone):
98
+ """
99
+ AWS Lightsail Mini Infrastructure Stack.
100
+
101
+ A comprehensive infrastructure stack that deploys:
102
+ * Lightsail Container Service with custom domain support
103
+ * PostgreSQL database (optional)
104
+ * IAM resources and S3 storage
105
+
106
+ :param scope: The construct scope
107
+ :param id: The construct ID
108
+ :param kwargs: Configuration parameters including region, domains, flags, etc.
109
+
110
+ Example:
111
+ >>> stack = LightsailContainerStandaloneStack(
112
+ ... app, "my-stack",
113
+ ... region="ca-central-1",
114
+ ... domains=["app.example.com"],
115
+ ... project_name="my-app",
116
+ ... postApplyScripts=[
117
+ ... "echo 'Deployment completed'",
118
+ ... "curl -X POST https://webhook.example.com/notify"
119
+ ... ]
120
+ ... )
121
+ """
122
+
123
+ @staticmethod
124
+ def get_architecture_flags():
125
+ """
126
+ Get the ArchitectureFlags enum for configuration.
127
+
128
+ :returns: ArchitectureFlags enum class
129
+ :rtype: type[ArchitectureFlags]
130
+ """
131
+ return ArchitectureFlags
132
+
133
+ def __init__(self, scope, id, **kwargs):
134
+ """
135
+ Initialize the AWS Lightsail Mini Infrastructure Stack.
136
+
137
+ :param scope: The construct scope
138
+ :param id: Unique identifier for this stack
139
+ :param kwargs: Configuration parameters
140
+
141
+ **Configuration Parameters:**
142
+
143
+ :param region: AWS region (default: "us-east-1")
144
+ :param environment: Environment name (default: "dev")
145
+ :param project_name: Project identifier (default: "bb-aws-lightsail-mini-v1a-app")
146
+ :param domain_name: Primary domain name
147
+ :param domains: List of custom domains to configure
148
+ :param flags: List of ArchitectureFlags to modify behavior
149
+ :param profile: AWS profile to use (default: "default")
150
+ :param postApplyScripts: List of shell commands to execute after deployment
151
+
152
+ .. warning::
153
+ Lightsail domain operations must use us-east-1 region regardless of
154
+ the main stack region.
155
+ """
156
+ # Set container-specific defaults
157
+ if "project_name" not in kwargs:
158
+ kwargs["project_name"] = "bb-aws-lightsail-mini-v1a-app"
159
+
160
+ # Set database defaults before base initialization
161
+ self.default_db_name = kwargs.get("default_db_name", kwargs["project_name"])
162
+ self.default_db_username = kwargs.get("default_db_username", "dbadmin")
163
+
164
+ # Call parent constructor which handles all the base initialization
165
+ super().__init__(scope, id, **kwargs)
166
+
167
+ # ===== Container-Specific Configuration =====
168
+ self.domains = kwargs.get("domains", []) or []
169
+
170
+ # ===== Database Configuration =====
171
+ self.default_db_name = kwargs.get("default_db_name", self.project_name)
172
+ self.default_db_username = kwargs.get("default_db_username", "dbadmin")
173
+
174
+ def _initialize_providers(self):
175
+ """Initialize all required Terraform providers."""
176
+ # Call parent class to initialize base providers
177
+ super()._initialize_providers()
178
+
179
+ # Add Lightsail-specific provider for domain operations (must be us-east-1)
180
+ self.aws_domain_provider = AwsProvider(
181
+ self, "aws_domain", region="us-east-1", profile=self.profile, alias="domain"
182
+ )
183
+ self.resources["aws_domain"] = self.aws_domain_provider
184
+
185
+ def _set_default_post_apply_scripts(self):
186
+ """
187
+ Set default post-apply scripts specific to container deployments.
188
+ """
189
+ # Call parent method for base scripts
190
+ super()._set_default_post_apply_scripts()
191
+
192
+ # Skip if flag is set
193
+ if BaseLightsailArchitectureFlags.SKIP_DEFAULT_POST_APPLY_SCRIPTS.value in self.flags:
194
+ return
195
+
196
+ # Add container-specific scripts
197
+ container_scripts = [
198
+ f"echo '🚀 Container Service URL: https://{self.project_name}.{self.region}.cs.amazonlightsail.com'",
199
+ ]
200
+
201
+ # Insert container-specific scripts before the final "execution started" message
202
+ if self.post_apply_scripts:
203
+ # Find the index of the last script and insert before it
204
+ insert_index = len(self.post_apply_scripts) - 1
205
+ for script in reversed(container_scripts):
206
+ self.post_apply_scripts.insert(insert_index, script)
207
+
208
+ def create_lightsail_resources(self):
209
+ """
210
+ Create core Lightsail resources.
211
+
212
+ Creates:
213
+ * Lightsail Container Service with nano power and scale of 1
214
+ * Random password for database authentication (if database not skipped)
215
+
216
+ .. note::
217
+ Custom domains are configured separately through DNS records and
218
+ post-deployment automation rather than the public_domain_names parameter
219
+ due to CDKTF type complexity.
220
+ """
221
+ # Lightsail Container Service
222
+ self.container_service = lightsail_container_service.LightsailContainerService(
223
+ self,
224
+ "app_container",
225
+ name=f"{self.project_name}",
226
+ power="nano",
227
+ region=self.region,
228
+ scale=1,
229
+ is_disabled=False,
230
+ # Note: Custom domains are configured separately via DNS records
231
+ # The public_domain_names parameter has complex type requirements
232
+ tags={"Environment": self.environment, "Project": self.project_name, "Stack": self.__class__.__name__},
233
+ )
234
+ self.container_service_url = self.get_lightsail_container_service_domain()
235
+
236
+ # Create Lightsail database if not skipped
237
+ if not self.has_flag(ArchitectureFlags.SKIP_DATABASE.value):
238
+ self.create_lightsail_database()
239
+
240
+ # Create S3 bucket for application data
241
+ self.create_s3_bucket()
242
+
243
+ self.resources["lightsail_container_service"] = self.container_service
244
+
245
+ def create_lightsail_database(self):
246
+ """
247
+ Create Lightsail PostgreSQL database (optional).
248
+
249
+ Creates a micro PostgreSQL 14 database instance if the SKIP_DATABASE flag
250
+ is not set. Also populates the secrets dictionary with database connection
251
+ information for use in Secrets Manager.
252
+
253
+ Database Configuration:
254
+ * Engine: PostgreSQL 14
255
+ * Size: micro_2_0
256
+ * Final snapshot: Disabled (skip_final_snapshot=True)
257
+
258
+ .. note::
259
+ Database creation can be skipped by including ArchitectureFlags.SKIP_DATABASE
260
+ in the flags parameter during stack initialization.
261
+ """
262
+ # Database Password Generation
263
+ self.db_password = password.Password(
264
+ self, "db_password", length=16, special=True, override_special="!#$%&*()-_=+[]{}<>:?"
265
+ )
266
+
267
+ self.database = lightsail_database.LightsailDatabase(
268
+ self,
269
+ "app_database",
270
+ relational_database_name=f"{self.project_name}-db",
271
+ blueprint_id="postgres_14",
272
+ bundle_id="micro_2_0",
273
+ master_database_name=self.clean_hyphens(f"{self.project_name}"),
274
+ master_username=self.default_db_username,
275
+ master_password=self.db_password.result,
276
+ skip_final_snapshot=True,
277
+ tags={"Environment": self.environment, "Project": self.project_name, "Stack": self.__class__.__name__},
278
+ )
279
+
280
+ # Populate secrets for database connection
281
+ self.secrets.update(
282
+ {
283
+ "password": self.db_password.result,
284
+ "username": self.default_db_username,
285
+ "dbname": self.default_db_name,
286
+ "host": self.database.master_endpoint_address,
287
+ "port": self.database.master_endpoint_port,
288
+ }
289
+ )
290
+
291
+ def create_s3_bucket(self, bucket_name=None):
292
+ """
293
+ Create S3 bucket for application data storage.
294
+
295
+ Creates a private S3 bucket with proper tagging for application data storage
296
+ and security configurations:
297
+ - Bucket versioning enabled
298
+ - Server-side encryption with Amazon S3 managed keys (SSE-S3)
299
+ - Bucket key enabled to reduce encryption costs
300
+ - Private ACL
301
+
302
+ The bucket name follows the pattern: {project_name}-s3
303
+
304
+ .. note::
305
+ The ACL parameter is deprecated in favor of aws_s3_bucket_acl resource
306
+ but is retained for backwards compatibility.
307
+ """
308
+ if bucket_name is None:
309
+ bucket_name = self.properize_s3_bucketname(f"{self.project_name}-s3")
310
+
311
+ self.s3_bucket = s3_bucket.S3Bucket(
312
+ self,
313
+ "app_data_bucket",
314
+ bucket=bucket_name,
315
+ acl="private",
316
+ versioning={"enabled": True},
317
+ server_side_encryption_configuration=({
318
+ "rule": ({
319
+ "apply_server_side_encryption_by_default": {
320
+ "sse_algorithm": "AES256"
321
+ },
322
+ "bucket_key_enabled": True
323
+ })
324
+ }),
325
+ tags={"Environment": self.environment, "Project": self.project_name, "Stack": self.__class__.__name__},
326
+ )
327
+
328
+ # Store the S3 bucket in resources registry
329
+ self.resources["s3_bucket"] = self.s3_bucket
330
+ self.bucket_name = bucket_name
331
+
332
+ def get_lightsail_container_service_domain(self):
333
+ """
334
+ Retrieve the actual Lightsail container service domain from AWS.
335
+
336
+ Returns a default format domain since we cannot query AWS at synthesis time.
337
+
338
+ :returns: The public domain URL for the container service
339
+ :rtype: str
340
+ """
341
+ return f"{self.project_name}.{self.region}.cs.amazonlightsail.com"
342
+
343
+ def create_outputs(self):
344
+ """
345
+ Create Terraform outputs for important resource information.
346
+
347
+ Generates outputs for:
348
+ * Container service public URL
349
+ * Database endpoint (if database is enabled)
350
+ * Database password (sensitive, if database is enabled)
351
+ * IAM access keys (sensitive)
352
+
353
+ .. note::
354
+ Sensitive outputs are marked as such and will be hidden in
355
+ Terraform output unless explicitly requested.
356
+ """
357
+ # Container service public URL
358
+ TerraformOutput(
359
+ self,
360
+ "container_service_url",
361
+ value=self.container_service_url,
362
+ description="Public URL of the Lightsail container service",
363
+ )
364
+
365
+ # Database outputs (if database is enabled)
366
+ if not self.has_flag(ArchitectureFlags.SKIP_DATABASE.value) and hasattr(self, 'database'):
367
+ TerraformOutput(
368
+ self,
369
+ "database_endpoint",
370
+ value=f"{self.database.master_endpoint_address}:{self.database.master_endpoint_port}",
371
+ description="Database connection endpoint",
372
+ )
373
+ TerraformOutput(
374
+ self,
375
+ "database_password",
376
+ value=self.database.master_password,
377
+ sensitive=True,
378
+ description="Database master password (sensitive)",
379
+ )
380
+
381
+ # Use the shared IAM output helper
382
+ self.create_iam_outputs()
383
+
384
+ """Placeholder for networking resources creation."""
385
+ pass
@@ -41,22 +41,28 @@ from cdktf_cdktf_provider_random import password
41
41
 
42
42
  #endregion
43
43
 
44
+ #region Null Provider and Resources
45
+ from cdktf_cdktf_provider_null.resource import Resource as NullResource
46
+
47
+ #endregion
48
+
44
49
  #region ArchitectureFlags
45
50
  class ArchitectureFlags(Enum):
46
51
  """
47
52
  Architecture configuration flags for optional components.
48
53
 
49
- Includes both base and database-specific flags.
54
+ Includes both base flags and database-specific flags.
50
55
 
51
56
  Base flags:
52
57
  :param SKIP_DEFAULT_POST_APPLY_SCRIPTS: Skip default post-apply scripts
53
- :param PRESERVE_EXISTING_SECRETS: Don't overwrite existing secret versions
58
+ :param PRESERVE_EXISTING_SECRETS: Don't overwrite existing secret versions (smart detection)
54
59
  :param IGNORE_SECRET_CHANGES: Ignore all changes to secret after initial creation
55
60
 
56
61
  Database-specific flags:
57
62
  :param SKIP_DATABASE_USERS: Skip creating individual database users (use master user only)
58
63
  """
59
- # Base flags (from BaseLightsailArchitectureFlags)
64
+
65
+ # Base flags from BaseLightsailArchitectureFlags
60
66
  SKIP_DEFAULT_POST_APPLY_SCRIPTS = "skip_default_post_apply_scripts"
61
67
  PRESERVE_EXISTING_SECRETS = "preserve_existing_secrets"
62
68
  IGNORE_SECRET_CHANGES = "ignore_secret_changes"
@@ -73,8 +79,8 @@ class LightsailDatabaseStack(LightsailBase):
73
79
 
74
80
  A comprehensive database stack that deploys:
75
81
  * Lightsail Database instance with PostgreSQL
76
- * Multiple databases within the instance
77
- * Individual database users with scoped permissions
82
+ * Multiple databases within the instance (automated creation)
83
+ * Individual database users with scoped permissions (automated creation)
78
84
  * Secrets Manager for storing all database credentials
79
85
  * IAM resources for programmatic access
80
86
 
@@ -126,6 +132,7 @@ class LightsailDatabaseStack(LightsailBase):
126
132
  :param db_instance_size: Database instance size (default: "micro_2_0")
127
133
  :param db_engine: Database engine version (default: "postgres_14")
128
134
  :param master_username: Master database username (default: "dbmasteruser")
135
+ :param db_publicly_accessible: Enable public access to database (default: True, required for automated provisioning)
129
136
  """
130
137
  # Set database-specific defaults
131
138
  if "project_name" not in kwargs:
@@ -135,7 +142,7 @@ class LightsailDatabaseStack(LightsailBase):
135
142
  environment = kwargs.get("environment", "dev")
136
143
  kwargs["secret_name"] = f"{project_name}/{environment}/database-credentials"
137
144
 
138
- # ===== Database-Specific Configuration (set before parent init) =====
145
+ # ===== Database-Specific Configuration (MUST be set before super().__init__) =====
139
146
  self.databases = kwargs.get("databases", [])
140
147
 
141
148
  # Validate required parameters
@@ -146,12 +153,13 @@ class LightsailDatabaseStack(LightsailBase):
146
153
  self.master_username = kwargs.get("master_username", "dbmasteruser")
147
154
  self.db_instance_size = kwargs.get("db_instance_size", "micro_2_0")
148
155
  self.db_engine = kwargs.get("db_engine", "postgres_14")
156
+ self.db_publicly_accessible = kwargs.get("db_publicly_accessible", True)
149
157
 
150
158
  # ===== Internal State =====
151
159
  self.database_users = {}
152
160
  self.database_passwords = {}
153
161
 
154
- # Call parent constructor
162
+ # Call parent constructor (this will call _set_default_post_apply_scripts)
155
163
  super().__init__(scope, id, **kwargs)
156
164
 
157
165
  def _set_default_post_apply_scripts(self):
@@ -162,7 +170,7 @@ class LightsailDatabaseStack(LightsailBase):
162
170
  super()._set_default_post_apply_scripts()
163
171
 
164
172
  # Skip if flag is set
165
- if ArchitectureFlags.SKIP_DEFAULT_POST_APPLY_SCRIPTS.value in self.flags:
173
+ if BaseLightsailArchitectureFlags.SKIP_DEFAULT_POST_APPLY_SCRIPTS.value in self.flags:
166
174
  return
167
175
 
168
176
  # Add database-specific scripts before the final message
@@ -191,8 +199,9 @@ class LightsailDatabaseStack(LightsailBase):
191
199
 
192
200
  Creates:
193
201
  * Database passwords for master and individual users
194
- * Lightsail PostgreSQL database instance
195
- * Individual database user credentials (for later manual creation)
202
+ * Lightsail PostgreSQL database instance (with public access enabled)
203
+ * Individual databases within the instance (automated via SQL)
204
+ * Individual database users with scoped permissions (automated via SQL)
196
205
  """
197
206
  # Generate passwords first
198
207
  self.create_database_passwords()
@@ -241,7 +250,13 @@ class LightsailDatabaseStack(LightsailBase):
241
250
  * Engine: PostgreSQL (version specified by db_engine)
242
251
  * Size: Configurable (default: micro_2_0)
243
252
  * Master database: Uses first database name from the list
253
+ * Public Access: Configurable (default: True for automated provisioning)
244
254
  * Final snapshot: Disabled (skip_final_snapshot=True)
255
+
256
+ .. note::
257
+ Public access is enabled by default to allow automated database creation
258
+ via local-exec provisioners. This can be disabled by setting
259
+ db_publicly_accessible=False, but will require manual database setup.
245
260
  """
246
261
  # Use the first database name as the master database name
247
262
  master_db_name = self.clean_hyphens(self.databases[0])
@@ -255,6 +270,7 @@ class LightsailDatabaseStack(LightsailBase):
255
270
  master_database_name=master_db_name,
256
271
  master_username=self.master_username,
257
272
  master_password=self.master_password.result,
273
+ publicly_accessible=self.db_publicly_accessible,
258
274
  skip_final_snapshot=True,
259
275
  tags={
260
276
  "Environment": self.environment,
@@ -280,28 +296,35 @@ class LightsailDatabaseStack(LightsailBase):
280
296
 
281
297
  def create_database_users(self):
282
298
  """
283
- Prepare database user credentials for individual databases.
284
-
285
- This method creates credentials for individual database users that would be
286
- created manually or via external scripts after deployment. For each database
287
- in the databases list, it will:
288
- 1. Generate a password for the database user
289
- 2. Store credentials in the secrets dictionary
290
- 3. Create user information for reference
291
-
292
- **Manual Database Setup Required:**
293
- After deployment, you'll need to manually create databases and users:
299
+ Create individual databases and users within the Lightsail PostgreSQL instance.
300
+
301
+ This method automates the creation of databases and users using SQL commands
302
+ executed via null_resource provisioners. For each database in the databases list:
303
+ 1. Generates a password for the database user
304
+ 2. Stores credentials in the secrets dictionary
305
+ 3. Creates the database (if not the first one - master database)
306
+ 4. Creates a dedicated user with the generated password
307
+ 5. Grants all privileges on the database to the user
308
+
309
+ **Automated Database Setup:**
310
+ The following operations are performed automatically for each database:
294
311
  * CREATE DATABASE {db_name};
295
312
  * CREATE USER "{db_name}-dbuser" WITH PASSWORD '{password}';
296
313
  * GRANT ALL PRIVILEGES ON DATABASE {db_name} TO "{db_name}-dbuser";
314
+ * GRANT ALL ON SCHEMA public TO "{db_name}-dbuser";
297
315
 
298
316
  .. note::
299
- Database and user creation happens manually after deployment since
300
- Lightsail doesn't provide Terraform resources for individual databases.
317
+ The first database in the list is created as the master database during
318
+ instance creation, so it's skipped in this automated provisioning process.
319
+
320
+ .. note::
321
+ Requires publicly_accessible=True on the database instance for the
322
+ provisioner to connect from the local machine running Terraform.
301
323
  """
302
324
  if ArchitectureFlags.SKIP_DATABASE_USERS.value in self.flags:
303
325
  return
304
326
 
327
+ # Store credentials for all databases
305
328
  for db_name in self.databases:
306
329
  clean_db_name = self.clean_hyphens(db_name)
307
330
  username = f"{clean_db_name}-dbuser"
@@ -319,22 +342,67 @@ class LightsailDatabaseStack(LightsailBase):
319
342
  "database": clean_db_name
320
343
  }
321
344
 
322
- # Add manual setup instructions to post-terraform messages
323
- if self.databases and not self.has_flag(ArchitectureFlags.SKIP_DATABASE_USERS.value):
324
- setup_commands = []
325
- for db_name in self.databases:
326
- clean_db_name = self.clean_hyphens(db_name)
327
- username = f"{clean_db_name}-dbuser"
328
- setup_commands.extend([
329
- f"CREATE DATABASE IF NOT EXISTS \"{clean_db_name}\";",
330
- f"CREATE USER \"{username}\" WITH PASSWORD '<password_from_secrets>';",
331
- f"GRANT ALL PRIVILEGES ON DATABASE \"{clean_db_name}\" TO \"{username}\";"
332
- ])
345
+ # Skip the first database as it's already created as the master database
346
+ databases_to_create = self.databases[1:] if len(self.databases) > 1 else []
347
+
348
+ # Create additional databases and users using null_resource
349
+ for db_name in databases_to_create:
350
+ clean_db_name = self.clean_hyphens(db_name)
351
+ username = f"{clean_db_name}-dbuser"
352
+ password_ref = self.database_passwords[db_name].result
333
353
 
334
- self.post_terraform_messages.append(
335
- f"Manual database setup required. Connect to the database instance and run:\n" +
336
- "\n".join(setup_commands)
354
+ # SQL commands to create database and user
355
+ # Using environment variables to avoid Terraform interpolation issues
356
+ sql_commands = f"""#!/bin/bash
357
+ set -e
358
+
359
+ echo "Creating database: {clean_db_name}"
360
+
361
+ # Wait for database to be ready (add retry logic)
362
+ for i in {{1..30}}; do
363
+ if PGPASSWORD="$MASTER_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -c "SELECT 1" > /dev/null 2>&1; then
364
+ echo "Database is ready"
365
+ break
366
+ fi
367
+ echo "Waiting for database to be ready... ($i/30)"
368
+ sleep 10
369
+ done
370
+
371
+ # Create database
372
+ PGPASSWORD="$MASTER_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -c "CREATE DATABASE \\"{clean_db_name}\\";" || echo "Database {clean_db_name} may already exist"
373
+
374
+ # Create user
375
+ PGPASSWORD="$MASTER_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -c "CREATE USER \\"{username}\\" WITH PASSWORD '$USER_PASSWORD';" || echo "User {username} may already exist"
376
+
377
+ # Grant database privileges
378
+ PGPASSWORD="$MASTER_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -c "GRANT ALL PRIVILEGES ON DATABASE \\"{clean_db_name}\\" TO \\"{username}\\";"
379
+
380
+ # Grant schema privileges
381
+ PGPASSWORD="$MASTER_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d {clean_db_name} -c "GRANT ALL ON SCHEMA public TO \\"{username}\\";"
382
+
383
+ echo "Successfully created database: {clean_db_name} with user: {username}"
384
+ """
385
+
386
+ # Create null_resource to execute SQL commands
387
+ db_resource = NullResource(
388
+ self,
389
+ f"create_database_{clean_db_name}",
390
+ depends_on=[self.database]
337
391
  )
392
+
393
+ # Add provisioner using override
394
+ db_resource.add_override("provisioner", [{
395
+ "local-exec": {
396
+ "command": sql_commands,
397
+ "environment": {
398
+ "DB_HOST": self.database.master_endpoint_address,
399
+ "DB_PORT": self.database.master_endpoint_port,
400
+ "DB_USER": self.master_username,
401
+ "MASTER_PASSWORD": self.master_password.result,
402
+ "USER_PASSWORD": password_ref,
403
+ }
404
+ }
405
+ }])
338
406
 
339
407
  def create_outputs(self):
340
408
  """