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.
- BuzzerboyAWSLightsailStack/ArchitectureMaker.py +229 -0
- BuzzerboyAWSLightsailStack/LightsailBaseStandalone.py +318 -0
- BuzzerboyAWSLightsailStack/LightsailContainer.py +5 -4
- BuzzerboyAWSLightsailStack/LightsailContainerStandalone.py +385 -0
- BuzzerboyAWSLightsailStack/LightsailDatabase.py +105 -37
- BuzzerboyAWSLightsailStack/LightsailDatabaseStandalone.py +484 -0
- BuzzerboyAWSLightsailStack/__init__.py +1 -0
- {buzzerboyawslightsail-0.330.1.dist-info → buzzerboyawslightsail-0.332.1.dist-info}/METADATA +91 -1
- buzzerboyawslightsail-0.332.1.dist-info/RECORD +15 -0
- {buzzerboyawslightsail-0.330.1.dist-info → buzzerboyawslightsail-0.332.1.dist-info}/licenses/LICENSE +0 -0
- buzzerboyawslightsail-0.330.1.dist-info/RECORD +0 -11
- {buzzerboyawslightsail-0.330.1.dist-info → buzzerboyawslightsail-0.332.1.dist-info}/WHEEL +0 -0
- {buzzerboyawslightsail-0.330.1.dist-info → buzzerboyawslightsail-0.332.1.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
This method
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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
|
-
#
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
"""
|