Skip to content

CloudFront: multi-destination access logging with Firehose HTTP endpoint - #121

Open
devin-ai-integration[bot] wants to merge 6 commits into
mainfrom
devin/1787259630-cloudfront-multi-destination-logging
Open

CloudFront: multi-destination access logging with Firehose HTTP endpoint#121
devin-ai-integration[bot] wants to merge 6 commits into
mainfrom
devin/1787259630-cloudfront-multi-destination-logging

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Summary

CloudFront access logging was single-choice (logging_destination = cloudwatch | s3). It's now a set, and a third destination — Amazon Data Firehose to an HTTP endpoint — is added, so a distribution can keep the CloudWatch delivery that powers the Ravion Logs panel and stream logs to an external consumer at the same time. Driving use case: shipping ravion.com access logs to Lightsage agent analytics, which needs edge-level visibility (cache hits and the Mintlify docs origin never reach our ECS origin, so origin-side tracking can't see them).

The Firehose path is standard logging v2, not aws_cloudfront_realtime_log_config — it reuses the existing vended-log delivery chain in cloudwatch_access_logs.tf, which means no CloudFront per-line charge and one shared delivery source per distribution:

aws_cloudwatch_log_delivery_source.access_logs (per distribution, shared)
  ├─ delivery → destination(log group)        # existing, unchanged
  └─ delivery → destination(firehose stream)  # new, record_fields + output_format=json
                                              → HTTP endpoint (GZIP, access key)
                                              → S3 backup bucket (FailedDataOnly)

Destination resolution, with the old input kept as a fallback so existing callers are untouched:

logging_destinations = var.logging_enabled ? (
  var.logging_destinations != null ? toset(var.logging_destinations)  # [] ⇒ no logging resources
                                   : toset([var.logging_destination]) # legacy single value
) : toset([])

Notes worth flagging:

  • logging_firehose_record_fields defaults to the nine fields agent-analytics consumers ask for. Standard logging v2 uses the parenthesized spellings cs(User-Agent) / cs(Referer) (the hyphenated cs-user-agent form is real-time-logs only).
  • The Firehose stream carries the LogDeliveryEnabled = "true" tag required for it to be a vended-log destination.
  • The endpoint access key is expected as a Secrets Manager ARN (logging_firehose_access_key_secret_arn, consumed via Firehose's secrets_manager_configuration); a sensitive plaintext variable exists as a fallback but is deliberately not exposed in the module definition form.
  • Every pre-existing resource address is unchanged, so re-applying with the same effective destinations is a no-op plan.
  • Definition goes to 2.0.0: logging_destination (single select) is replaced by logging_destinations (multi-select, moved_from: [logging_destination]). moved_from only exempts the immutable-input check per module-schema.json — it does not promise value conversion — so the release notes and docs tell upgraders to confirm their selection, since an S3-only 1.x instance landing on the ["cloudwatch"] default would lose S3 delivery.

Follow-up (not in this PR): point website-cdn in ravionhq/flightcontrol's ravion.yaml at the released version and provision the Lightsage key.

Testing

tofu fmt -check -recursive, tofu init -backend=false, tofu validate, tofu test -filter=tests/basic.tftest.hcl (77 passed), definition validation, make readme, and tools/ravion-modules unit tests (83 passed). New tests cover cloudwatch+firehose together, invalid destination values, firehose missing its endpoint URL, firehose missing an access key, empty destination set, and the legacy logging_destination fallback.

Link to Devin session: https://app.devin.ai/sessions/0fdbe26b973c4c8699234df3b2956bec
Requested by: @flybayer

Greptile Summary

The PR expands CloudFront logging from one selected destination to concurrent CloudWatch, S3, and Firehose delivery.

  • Adds an HTTP-endpoint Firehose stream, failed-delivery S3 bucket, IAM role, and standard logging v2 delivery.
  • Introduces multi-destination Terraform inputs, outputs, validation, module-definition fields, documentation, and plan tests.
  • Preserves the legacy single-destination Terraform input as a fallback.

Confidence Score: 3/5

The PR should not merge until Firehose resource-name collisions and customer-managed KMS secret access are addressed.

Distinct valid module names can resolve to identical globally scoped backup-bucket and IAM-role names, and the generated Firehose role cannot retrieve endpoint credentials encrypted under a customer-managed KMS key.

Files Needing Attention: cdn/cloudfront/firehose_access_logs.tf

Important Files Changed

Filename Overview
cdn/cloudfront/firehose_access_logs.tf Adds the Firehose delivery path, but generated resource names can collide and the execution role cannot decrypt customer-key-encrypted endpoint secrets.
cdn/cloudfront/cloudwatch_access_logs.tf Broadens the shared standard-logging source to serve CloudWatch and Firehose destinations.
cdn/cloudfront/variables.tf Adds multi-destination and Firehose configuration inputs with endpoint and credential validation.
cdn/cloudfront/rvn-cloudfront-definition.yml Exposes destination selection and Firehose configuration through the Ravion definition while documenting the major-version migration.
cdn/cloudfront/tests/basic.tftest.hcl Adds plan coverage for combined destinations, disabled logging, legacy fallback, and required Firehose inputs.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    CF[CloudFront distribution] --> SRC[Standard logging v2 source]
    SRC --> CW[CloudWatch Logs destination]
    SRC --> FH[Firehose delivery stream]
    FH --> HTTP[HTTP endpoint]
    FH -->|FailedDataOnly| S3[S3 backup bucket]
    CF -->|Legacy logging| LS3[S3 logging destination]
Loading
Prompt To Fix All With AI
### Issue 1
cdn/cloudfront/firehose_access_logs.tf:17
**Truncated resource names collide**

If two module instances in the same AWS account have names sharing the first 28 characters, they derive the same globally scoped backup-bucket name, causing the second stack to fail during creation; names sharing the first 45 characters similarly collide on the IAM role.

### Issue 2
cdn/cloudfront/firehose_access_logs.tf:123-124
**KMS decryption permission missing**

If the endpoint secret uses a customer-managed KMS key, this role grants only `secretsmanager:GetSecretValue` and cannot decrypt the value, causing Firehose stream creation or HTTP delivery to fail with a KMS access-denied error.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "Document CloudFront logging upgrade sele..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

devin-ai-integration Bot and others added 3 commits August 20, 2026 21:08
Co-Authored-By: brandon <brandon@flightcontrol.dev>
Co-Authored-By: brandon <brandon@flightcontrol.dev>
Co-Authored-By: brandon <brandon@flightcontrol.dev>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown

Ravion Module Publish Plan

Dry run only. No Ravion API mutations were made.

Module Current Version New Version Description
rvn-cloudfront 1.3.0 2.0.0 Add multi-destination CloudFront access logging; confirm destinations when upgrading from 1.x.

Diffs

rvn-cloudfront 1.3.0 -> 2.0.0

--- remote
+++ compiled
     label: Access logging
     type: section
   - default: true
-    description: Enable CloudFront access logging. Logs are delivered to CloudWatch Logs by default and can be viewed in Ravion.
+    description: Enable CloudFront access logging. Select one or more destinations below; CloudWatch Logs can be viewed in Ravion.
     id: logging_enabled
     label: CloudFront access logging
     type: boolean
-  - default: cloudwatch
-    description: Where CloudFront delivers access logs.
-    id: logging_destination
-    label: Logging destination
+  - default:
+      - cloudwatch
+    description: Select one or more destinations for CloudFront access logs. CloudWatch Logs keeps the Ravion Logs panel available; Firehose sends JSON records to an HTTP endpoint; S3 uses legacy standard logging.
+    id: logging_destinations
+    label: Logging destinations
+    moved_from:
+      - logging_destination
+    required: true
     show_when:
       logging_enabled: true
-    type: string
+    type: string_array
     values:
       - description: View access logs directly in Ravion. Ingestion costs more at very high traffic.
         label: CloudWatch Logs
@@
       - description: Deliver compressed log files to an automatically created S3 bucket. Cheapest for high traffic.
         label: S3 bucket
         value: s3
+      - description: Stream selected JSON access-log fields to an HTTP endpoint through Amazon Data Firehose.
+        label: Firehose
+        value: firehose
+  - description: HTTPS URL for the Firehose HTTP endpoint. Applies when Firehose is selected in Logging destinations.
+    id: logging_firehose_endpoint_url
+    label: Firehose endpoint URL
+    placeholder: https://example.com/firehose
+    type: string
+  - description: Optional display name for the Firehose HTTP endpoint. Applies when Firehose is selected in Logging destinations.
+    id: logging_firehose_endpoint_name
+    label: Firehose endpoint name
+    type: string
+  - description: Secrets Manager ARN containing the Firehose HTTP endpoint access key. Required when Firehose is selected unless an access key is supplied through Advanced Terraform variables.
+    id: logging_firehose_access_key_secret_arn
+    label: Firehose access-key secret
+    type: string
+  - description: Optional KMS key ARN used to encrypt the Firehose HTTP endpoint access-key secret. Applies when Firehose is selected and the secret uses a customer-managed KMS key.
+    id: logging_firehose_access_key_secret_kms_key_arn
+    label: Firehose access-key secret KMS key
+    type: string
   - default: 90
     description: Days to retain CloudFront access logs in CloudWatch Logs or the automatically created S3 logging bucket.
     id: logging_bucket_retention_days
@@
 
   The default configuration is safe for dynamic apps: responses are cached at the edge only when your app returns Cache-Control headers, and the full viewer request, including the Host header, cookies, and query strings, is forwarded to the origin. You get TLS termination close to users, HTTP/2 and HTTP/3, connection reuse to the origin, optional WAF, and per-path caching for static assets.
 
-  Terraform source: [ravionhq/modules/cdn/cloudfront](https://github.com/ravionhq/modules/tree/rvn-cloudfront@1.3.0/cdn/cloudfront)
+  Terraform source: [ravionhq/modules/cdn/cloudfront](https://github.com/ravionhq/modules/tree/rvn-cloudfront@2.0.0/cdn/cloudfront)
 
   ## Use cases
 
@@
 
   ## Access logging
 
-  CloudFront access logging is on by default, delivered to CloudWatch Logs and viewable in Ravion. Choose where logs are delivered:
+  CloudFront access logging is on by default. Select any combination of destinations:
 
   | Destination | How it works |
   | --- | --- |
   | CloudWatch Logs (default) | CloudFront standard logging v2 delivers access logs to a module-managed CloudWatch Logs group, and the module UI shows them in the Logs panel. Log ingestion costs more than S3 at very high traffic. |
   | S3 bucket | Legacy standard logging delivers compressed log files to an automatically created S3 bucket. Cheapest for high-traffic sites; not viewable in Ravion. |
+  | Firehose | CloudFront standard logging v2 delivers selected fields as JSON through an Amazon Data Firehose HTTP endpoint. Firehose keeps failed deliveries in a module-managed S3 backup bucket. |
 
-  Logging retention days defaults to 90 and controls the CloudWatch log group retention or the S3 bucket lifecycle expiry, depending on the destination. CloudWatch retention must be one of the standard CloudWatch Logs retention values (1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, and larger).
+  Firehose requires an HTTPS endpoint URL and a Secrets Manager ARN containing the endpoint access key. On upgrade from a 1.x definition, confirm the Logging destinations selection; a prior S3-only configuration must be selected as S3 again. Logging retention days defaults to 90 and controls the CloudWatch log group and module-managed S3 bucket lifecycle expiry. CloudWatch retention must be one of the standard CloudWatch Logs retention values (1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, and larger).
 
   ## Configuration
 
@@
   | Custom cache behaviors | No | [] | Path-based caching and routing before the default behavior |
   | Price class | No | All edge locations | Edge location coverage |
   | Additional metrics | No | false | Enables all 8 CloudFront additional metrics and their flat CloudWatch cost |
-  | CloudFront access logging | No | true | Delivers logs to CloudWatch Logs or S3 |
-  | Logging destination | No | CloudWatch Logs | Visible when logging is enabled; CloudWatch shows logs in Ravion |
+  | CloudFront access logging | No | true | Enables access logging |
+  | Logging destinations | No | CloudWatch Logs | Select any combination of CloudWatch Logs, S3, and Firehose |
+  | Firehose endpoint URL | When Firehose is selected | None | HTTPS URL receiving JSON access-log records |
+  | Firehose endpoint name | No | None | Optional Firehose HTTP endpoint display name |
+  | Firehose access-key secret | When Firehose is selected | None | Secrets Manager ARN containing the HTTP endpoint access key |
   | Tags | No | Standard Ravion tags | Additional tags merged with Ravion ownership tags |
   | Advanced Terraform variables | No | {} | Raw lower-level overrides for exceptional cases |
 
@@
         base_path: cdn/cloudfront
         branch: main
         execution_environment_id: << module.input.execution_environment_id >>
-        ref: rvn-cloudfront@1.3.0
+        ref: rvn-cloudfront@2.0.0
         repo: https://github.com/ravionhq/modules
         stack_id: <<stack.id>>
         terraform_variables:
@@
           geo_restriction_locations: << module.input.geo_restriction_locations >>
           geo_restriction_type: << module.input.geo_restriction_type >>
           http_version: << module.input.http_version >>
-          logging_bucket_creation_enabled: << module.input.logging_enabled && module.input.logging_destination == "s3" >>
+          logging_bucket_creation_enabled: << module.input.logging_enabled && len(module.input.logging_destinations || []) > 0 && (module.input.logging_destinations[0] == "s3" || (len(module.input.logging_destinations || []) > 1 && module.input.logging_destinations[1] == "s3") || (len(module.input.logging_destinations || []) > 2 && module.input.logging_destinations[2] == "s3")) >>
           logging_bucket_domain_name: null
           logging_bucket_retention_days: << module.input.logging_bucket_retention_days >>
-          logging_destination: << module.input.logging_destination >>
+          logging_destinations: << module.input.logging_destinations || [] >>
           logging_enabled: << module.input.logging_enabled >>
+          logging_firehose_access_key_secret_arn: << module.input.logging_firehose_access_key_secret_arn || nil >>
+          logging_firehose_access_key_secret_kms_key_arn: << module.input.logging_firehose_access_key_secret_kms_key_arn || nil >>
+          logging_firehose_endpoint_name: << module.input.logging_firehose_endpoint_name || nil >>
+          logging_firehose_endpoint_url: << module.input.logging_firehose_endpoint_url || nil >>
           logging_prefix: ""
           name: << module.input.name >>
           ordered_cache_behaviors: '<< module.input.cache_behaviors != nil ? map(module.input.cache_behaviors, {"path_pattern": #.path_pattern, "target_origin_id": #.target_origin_id, "viewer_protocol_policy": #.viewer_protocol_policy, "allowed_methods": (#.allowed_methods == "all" ? ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"] : #.allowed_methods == "read_options" ? ["GET", "HEAD", "OPTIONS"] : ["GET", "HEAD"]), "cache_policy_id": #.cache_policy_id, "origin_request_policy_id": (#.origin_request_policy_id || nil), "trusted_key_groups": (#.signed_urls_enabled ? #.trusted_key_group_ids : [])}) : [] >>'
@@
     >>
   logs: |-
     <<
-    module.input.logging_enabled && module.input.logging_destination == "cloudwatch" ?
+    module.input.logging_enabled && len(module.input.logging_destinations || []) > 0 &&
+    (module.input.logging_destinations[0] == "cloudwatch" ||
+    (len(module.input.logging_destinations || []) > 1 && module.input.logging_destinations[1] == "cloudwatch") ||
+    (len(module.input.logging_destinations || []) > 2 && module.input.logging_destinations[2] == "cloudwatch")) ?
     [{"id": "cloudfront_access_logs", "name": "CloudFront access logs", "source": {"type": "cloudwatch", "aws_account_id": module.input.aws_account_id, "region": "us-east-1", "log_group": stack.output.access_log_group_name}}] :
     []
     >>

Co-Authored-By: brandon <brandon@flightcontrol.dev>
Comment thread cdn/cloudfront/firehose_access_logs.tf Outdated
Comment thread cdn/cloudfront/firehose_access_logs.tf
devin-ai-integration Bot and others added 2 commits August 20, 2026 21:25
Co-Authored-By: brandon <brandon@flightcontrol.dev>
Co-Authored-By: brandon <brandon@flightcontrol.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant