Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 25 additions & 10 deletions stelvio/aws/acm.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ class AcmValidatedDomainResources:

@final
class AcmValidatedDomain(Component[AcmValidatedDomainResources]):
def __init__(self, name: str, domain_name: str):
def __init__(self, name: str, domain_name: str, customize: dict[str, dict] | None = None):
self.domain_name = domain_name
super().__init__(name)
super().__init__(name, customize=customize)

def _create_resources(self) -> AcmValidatedDomainResources:
dns = context().dns
Expand All @@ -34,26 +34,41 @@ def _create_resources(self) -> AcmValidatedDomainResources:
# 1 - Issue Certificate
certificate = pulumi_aws.acm.Certificate(
context().prefix(f"{self.name}-certificate"),
domain_name=self.domain_name,
validation_method="DNS",
**self._customizer(
"certificate",
{
"domain_name": self.domain_name,
"validation_method": "DNS",
},
),
)

# 2 - Validate Certificate with DNS PROVIDER
first_option = certificate.domain_validation_options.apply(lambda options: options[0])
validation_record = dns.create_caa_record(
resource_name=context().prefix(f"{self.name}-certificate-validation-record"),
name=first_option.apply(lambda opt: opt["resource_record_name"]),
record_type=first_option.apply(lambda opt: opt["resource_record_type"]),
content=first_option.apply(lambda opt: opt["resource_record_value"]),
ttl=1,
**self._customizer(
"validation_record",
{
"record_type": first_option.apply(lambda opt: opt["resource_record_type"]),
"content": first_option.apply(lambda opt: opt["resource_record_value"]),
"ttl": 1,
},
),
)

# 3 - Wait for validation - use the validation record's FQDN to ensure it exists
cert_validation = pulumi_aws.acm.CertificateValidation(
context().prefix(f"{self.name}-certificate-validation"),
certificate_arn=certificate.arn,
# This ensures validation_record exists
validation_record_fqdns=[validation_record.name],
**self._customizer(
"cert_validation",
{
"certificate_arn": certificate.arn,
# This ensures validation_record exists
"validation_record_fqdns": [validation_record.name],
},
),
opts=pulumi.ResourceOptions(
depends_on=[certificate, validation_record.pulumi_resource]
),
Expand Down
7 changes: 5 additions & 2 deletions stelvio/aws/api_gateway/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,15 @@ def __init__(
self,
name: str,
config: ApiConfig | None = None,
customize: dict[str, dict] | None = None,
**opts: Unpack[ApiConfigDict],
) -> None:
self._routes = []
self._authorizers = []
self._default_auth = None
self._config = self._parse_config(config, opts)
self._validate_cors_for_rest_api()
super().__init__(name)
super().__init__(name, customize=customize)

@staticmethod
def _parse_config(config: ApiConfig | ApiConfigDict | None, opts: ApiConfigDict) -> ApiConfig:
Expand Down Expand Up @@ -538,7 +539,9 @@ def _create_resources(self) -> ApiResources:
# c. create base path mapping
endpoint_type = self._config.endpoint_type or DEFAULT_ENDPOINT_TYPE
rest_api = RestApi(
context().prefix(self.name), endpoint_configuration={"types": endpoint_type.upper()}
context().prefix(self.name),
endpoint_configuration={"types": endpoint_type.upper()},
**self._customizer("rest_api", {}),
)

account = _create_api_gateway_account_and_role()
Expand Down
190 changes: 109 additions & 81 deletions stelvio/aws/cloudfront/cloudfront.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,16 @@ class CloudFrontDistributionResources:

@final
class CloudFrontDistribution(Component[CloudFrontDistributionResources]):
def __init__(
def __init__( # noqa: PLR0913
self,
name: str,
bucket: Bucket,
price_class: CloudfrontPriceClass = "PriceClass_100",
custom_domain: str | None = None,
function_associations: list[FunctionAssociation] | None = None,
customize: dict[str, dict] | None = None,
):
super().__init__(name)
super().__init__(name, customize=customize)
self.bucket = bucket
self.custom_domain = custom_domain
self.price_class = price_class
Expand All @@ -63,103 +64,125 @@ def _create_resources(self) -> CloudFrontDistributionResources:
acm_validated_domain = AcmValidatedDomain(
f"{self.name}-acm-validated-domain",
domain_name=self.custom_domain,
customize=self._customize,
)

# Create Origin Access Control for S3
origin_access_control = pulumi_aws.cloudfront.OriginAccessControl(
context().prefix(f"{self.name}-oac"),
description=f"Origin Access Control for {self.name}",
origin_access_control_origin_type="s3",
signing_behavior="always",
signing_protocol="sigv4",
**self._customizer(
"origin_access_control",
{
"origin_access_control_origin_type": "s3",
"signing_behavior": "always",
"signing_protocol": "sigv4",
},
),
)

# Create CloudFront Distribution
distribution = pulumi_aws.cloudfront.Distribution(
context().prefix(self.name),
aliases=[self.custom_domain] if self.custom_domain else None,
origins=[
{
"domain_name": self.bucket.resources.bucket.bucket_regional_domain_name,
"origin_id": f"{self.name}-S3-Origin",
"origin_access_control_id": origin_access_control.id,
}
],
enabled=True,
is_ipv6_enabled=True,
default_root_object="index.html",
default_cache_behavior={
"allowed_methods": ["GET", "HEAD", "OPTIONS"], # Reduced to read-only methods
"cached_methods": ["GET", "HEAD"],
"target_origin_id": f"{self.name}-S3-Origin",
"compress": True,
"viewer_protocol_policy": "redirect-to-https",
"forwarded_values": {
"query_string": False,
"cookies": {"forward": "none"},
"headers": ["If-Modified-Since"], # Forward cache validation headers
},
"min_ttl": 0,
"default_ttl": 300, # Reduce default TTL to 5 minutes for faster updates
"max_ttl": 3600, # Reduce max TTL to 1 hour
"function_associations": self.function_associations,
},
price_class=self.price_class,
restrictions={
"geo_restriction": {
"restriction_type": "none",
}
},
viewer_certificate={
"acm_certificate_arn": acm_validated_domain.resources.certificate.arn,
"ssl_support_method": "sni-only",
"minimum_protocol_version": "TLSv1.2_2021",
}
if self.custom_domain
else {
"cloudfront_default_certificate": True,
},
custom_error_responses=[
{
"error_code": 403,
"response_code": 404,
"response_page_path": "/error.html",
"error_caching_min_ttl": 0, # Don't cache 403 errors
},
**self._customizer(
"distribution",
{
"error_code": 404,
"response_code": 404,
"response_page_path": "/error.html",
"error_caching_min_ttl": 300, # Cache 404s for only 5 minutes
"aliases": [self.custom_domain] if self.custom_domain else None,
"origins": [
{
"domain_name": self.bucket.resources.bucket.bucket_regional_domain_name, # noqa: E501
"origin_id": f"{self.name}-S3-Origin",
"origin_access_control_id": origin_access_control.id,
}
],
"enabled": True,
"is_ipv6_enabled": True,
"default_root_object": "index.html",
"default_cache_behavior": {
"allowed_methods": [
"GET",
"HEAD",
"OPTIONS",
], # Reduced to read-only methods
"cached_methods": ["GET", "HEAD"],
"target_origin_id": f"{self.name}-S3-Origin",
"compress": True,
"viewer_protocol_policy": "redirect-to-https",
"forwarded_values": {
"query_string": False,
"cookies": {"forward": "none"},
"headers": ["If-Modified-Since"], # Forward cache validation headers
},
"min_ttl": 0,
"default_ttl": 300, # Reduce default TTL to 5 minutes for faster updates
"max_ttl": 3600, # Reduce max TTL to 1 hour
"function_associations": self.function_associations,
},
"price_class": self.price_class,
"restrictions": {
"geo_restriction": {
"restriction_type": "none",
}
},
"viewer_certificate": {
"acm_certificate_arn": acm_validated_domain.resources.certificate.arn,
"ssl_support_method": "sni-only",
"minimum_protocol_version": "TLSv1.2_2021",
}
if self.custom_domain
else {
"cloudfront_default_certificate": True,
},
"custom_error_responses": [
{
"error_code": 403,
"response_code": 404,
"response_page_path": "/error.html",
"error_caching_min_ttl": 0, # Don't cache 403 errors
},
{
"error_code": 404,
"response_code": 404,
"response_page_path": "/error.html",
"error_caching_min_ttl": 300, # Cache 404s for only 5 minutes
},
],
},
],
),
)

# Update S3 bucket policy to allow CloudFront access
bucket_policy = pulumi_aws.s3.BucketPolicy(
context().prefix(f"{self.name}-bucket-policy"),
bucket=self.bucket.resources.bucket.id,
policy=pulumi.Output.all(
distribution_arn=distribution.arn,
bucket_arn=self.bucket.arn,
).apply(
lambda args: pulumi.Output.json_dumps(
{
"Version": "2012-10-17",
"Statement": [
**self._customizer(
"bucket_policy",
{
"policy": pulumi.Output.all(
distribution_arn=distribution.arn,
bucket_arn=self.bucket.arn,
).apply(
lambda args: pulumi.Output.json_dumps(
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {"Service": "cloudfront.amazonaws.com"},
"Action": "s3:GetObject",
"Resource": f"{args['bucket_arn']}/*",
"Condition": {
"StringEquals": {"AWS:SourceArn": args["distribution_arn"]}
},
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {"Service": "cloudfront.amazonaws.com"},
"Action": "s3:GetObject",
"Resource": f"{args['bucket_arn']}/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": args["distribution_arn"]
}
},
}
],
}
],
}
)
)
),
},
),
opts=pulumi.ResourceOptions(
depends_on=[distribution]
Expand All @@ -171,9 +194,14 @@ def _create_resources(self) -> CloudFrontDistributionResources:
record = context().dns.create_record(
resource_name=context().prefix(f"{self.name}-cloudfront-record"),
name=self.custom_domain,
record_type="CNAME",
value=distribution.domain_name,
ttl=1,
**self._customizer(
"record",
{
"record_type": "CNAME",
"value": distribution.domain_name,
"ttl": 1,
},
),
)

pulumi.export(f"cloudfront_{self.name}_domain_name", distribution.domain_name)
Expand Down
Loading