"
+
+ environment = "prod"
+ service = "bcss"
+ name = "vpc"
+
+ vpc_cidr = "10.0.0.0/16"
+
+ flow_log_destination_type = "s3"
+ flow_log_destination_arn = module.logs_bucket.bucket_arn
}
```
@@ -88,8 +117,10 @@ module "database_vpc" {
# Adjust subnet prefix for /24 VPC (must be larger than /24, e.g., /26, /27, /28)
intra_subnet_prefix = 26
- enable_flow_log = true
- flow_log_retention_in_days = 30
+ enable_flow_log = true
+ flow_log_destination_type = "cloud-watch-logs"
+ flow_log_destination_arn = aws_cloudwatch_log_group.database_vpc_flow_logs.arn
+ flow_log_cloudwatch_iam_role_arn = aws_iam_role.database_vpc_flow_logs.arn
}
```
@@ -155,11 +186,21 @@ module "vpc_public" {
| `availability_zones` | Explicit AZs for subnet placement; defaults to the first three available AZs | `null` |
| `single_nat_gateway` | Use one shared NAT instead of per-AZ | `false` |
| `enable_flow_log` | Enable VPC flow logs | `true` |
-| `flow_log_retention_in_days` | CloudWatch log retention | `365` |
+| `flow_log_destination_type` | Flow log destination type (`cloud-watch-logs` or `s3`) | `cloud-watch-logs` |
+| `flow_log_destination_arn` | Consumer-managed destination ARN | `null` |
+| `flow_log_cloudwatch_iam_role_arn` | Consumer-managed CloudWatch delivery role ARN (CloudWatch destination only) | `null` |
| `flow_log_traffic_type` | ACCEPT, REJECT, or ALL | `ALL` |
-| `flow_log_kms_key_id` | KMS key ARN for log encryption | `null` |
| `map_public_ip_on_launch` | Auto-assign public IPs in public subnets | `false` |
+## Validation
+
+Cross-variable checks in `validations.tf` enforce flow-log destination requirements:
+
+- `flow_log_destination_type` must be `cloud-watch-logs` or `s3`
+- For `cloud-watch-logs`, `flow_log_destination_arn` is required
+- For `cloud-watch-logs`, `flow_log_cloudwatch_iam_role_arn` is required
+- For `s3`, `flow_log_destination_arn` is required and `flow_log_cloudwatch_iam_role_arn` must be unset
+
## Key outputs
| Output | Description |
@@ -220,7 +261,6 @@ module "vpc_public" {
| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no |
| [availability\_zones](#input\_availability\_zones) | Availability zones to use for the VPC. Leave null to use the first three available AZs in the current region. | `list(string)` | `null` | no |
| [aws\_region](#input\_aws\_region) | The AWS region | `string` | `"eu-west-2"` | no |
-| [cloudwatch\_log\_group\_tags](#input\_cloudwatch\_log\_group\_tags) | Additional tags for the CloudWatch log group. | `map(string)` | `{}` | no |
| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` | {
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"project": null,
"regex_replace_chars": null,
"region": null,
"service": null,
"stack": null,
"tags": {},
"terraform_source": null,
"workspace": null
} | no |
| [create\_firewall\_subnets](#input\_create\_firewall\_subnets) | Whether to create firewall subnets (required for Network Firewall routing mode). | `bool` | `true` | no |
| [create\_intra\_subnets](#input\_create\_intra\_subnets) | Whether to create intra subnets (no internet access). | `bool` | `true` | no |
@@ -237,7 +277,7 @@ module "vpc_public" {
| [enable\_dhcp\_options](#input\_enable\_dhcp\_options) | Create a custom DHCP option set and associate it with the VPC. | `bool` | `false` | no |
| [enable\_dns\_hostnames](#input\_enable\_dns\_hostnames) | Enable DNS hostnames in the VPC. | `bool` | `true` | no |
| [enable\_dns\_support](#input\_enable\_dns\_support) | Enable DNS support in the VPC. | `bool` | `true` | no |
-| [enable\_flow\_log](#input\_enable\_flow\_log) | Enable VPC flow logs to CloudWatch Logs. | `bool` | `true` | no |
+| [enable\_flow\_log](#input\_enable\_flow\_log) | Enable VPC flow logs. Destination is controlled by flow\_log\_destination\_type. | `bool` | `true` | no |
| [enable\_nat\_gateway](#input\_enable\_nat\_gateway) | Provision NAT Gateway(s) for private subnet internet egress. Not applicable if private subnets are disabled. Set to false for database-only VPCs with no internet-routed workloads. | `bool` | `true` | no |
| [enable\_network\_firewall](#input\_enable\_network\_firewall) | When true, the VPC module creates firewall subnets, takes over
IGW management from the community module, and reconfigures
routing for AWS Network Firewall inspection:
- Firewall subnets created as standalone resources
- IGW created as a standalone resource (community module's create\_igw = false)
- Firewall subnets get a default route (0.0.0.0/0) to the IGW
- Public subnet default route is NOT created (callers must
inject 0.0.0.0/0 → firewall VPCE at the stack level)
When false (default), no firewall subnets are created, the
community module creates the IGW and public → IGW route as
normal — no Network Firewall in the path. | `bool` | `false` | no |
| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no |
@@ -245,12 +285,12 @@ module "vpc_public" {
| [firewall\_subnet\_prefix](#input\_firewall\_subnet\_prefix) | Prefix length for firewall subnets (e.g. 28 = /28, 16 IPs each). AWS allows /16 to /28. Must be more specific (larger numerically) than vpc\_cidr when auto-calculating. Used only when firewall\_subnets list is empty; when explicit firewall\_subnets are provided, this value is ignored. Highly recommended: /28 to minimize wasted IPs. | `number` | `28` | no |
| [firewall\_subnet\_tags](#input\_firewall\_subnet\_tags) | Additional tags for the firewall subnets. | `map(string)` | `{}` | no |
| [firewall\_subnets](#input\_firewall\_subnets) | Explicit CIDR blocks for firewall subnets (one per AZ). Leave empty to auto-calculate. | `list(string)` | `[]` | no |
-| [flow\_log\_kms\_key\_id](#input\_flow\_log\_kms\_key\_id) | ARN of a KMS key to encrypt the CloudWatch log group. Leave null for no encryption. | `string` | `null` | no |
+| [flow\_log\_cloudwatch\_iam\_role\_arn](#input\_flow\_log\_cloudwatch\_iam\_role\_arn) | Existing IAM role ARN used by VPC Flow Logs when destination type is cloud-watch-logs. | `string` | `null` | no |
+| [flow\_log\_destination\_arn](#input\_flow\_log\_destination\_arn) | ARN of the flow log destination. Required when flow logs are enabled. For cloud-watch-logs, set to a CloudWatch log group ARN. For s3, set to an S3 bucket ARN. | `string` | `null` | no |
+| [flow\_log\_destination\_type](#input\_flow\_log\_destination\_type) | Destination type for VPC flow logs. Supported values in this module: cloud-watch-logs, s3. | `string` | `"cloud-watch-logs"` | no |
| [flow\_log\_max\_aggregation\_interval](#input\_flow\_log\_max\_aggregation\_interval) | The maximum interval of time (seconds) during which a flow of packets is captured. Valid values: 60 (1 min) or 600 (10 min). | `number` | `600` | no |
-| [flow\_log\_retention\_in\_days](#input\_flow\_log\_retention\_in\_days) | Number of days to retain VPC flow logs in CloudWatch. | `number` | `365` | no |
| [flow\_log\_tags](#input\_flow\_log\_tags) | Additional tags for the VPC flow log. | `map(string)` | `{}` | no |
| [flow\_log\_traffic\_type](#input\_flow\_log\_traffic\_type) | The type of traffic to capture. Valid values: ACCEPT, REJECT, ALL. | `string` | `"ALL"` | no |
-| [iam\_role\_tags](#input\_iam\_role\_tags) | Additional tags for the IAM role used by the VPC flow log. | `map(string)` | `{}` | no |
| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no |
| [intra\_subnet\_prefix](#input\_intra\_subnet\_prefix) | Prefix length for intra subnets with no internet route (e.g. 23 = /23, 512 IPs each). AWS allows /16 to /28. Must be more specific (larger numerically) than vpc\_cidr when auto-calculating. Used only when intra\_subnets list is empty; when explicit intra\_subnets are provided, this value is ignored. | `number` | `23` | no |
| [intra\_subnet\_tags](#input\_intra\_subnet\_tags) | Additional tags for the intra (no-internet) subnets. | `map(string)` | `{}` | no |
@@ -298,8 +338,8 @@ module "vpc_public" {
| [firewall\_subnet\_ids](#output\_firewall\_subnet\_ids) | List of IDs of the firewall subnets. |
| [firewall\_subnets\_cidr\_blocks](#output\_firewall\_subnets\_cidr\_blocks) | List of CIDR blocks of the firewall subnets. |
| [flow\_log\_arn](#output\_flow\_log\_arn) | The ARN of the VPC Flow Log. |
-| [flow\_log\_cloudwatch\_log\_group\_arn](#output\_flow\_log\_cloudwatch\_log\_group\_arn) | The ARN of the CloudWatch Log Group for VPC flow logs. |
-| [flow\_log\_iam\_role\_arn](#output\_flow\_log\_iam\_role\_arn) | The ARN of the IAM role used by VPC flow logs. |
+| [flow\_log\_cloudwatch\_log\_group\_arn](#output\_flow\_log\_cloudwatch\_log\_group\_arn) | The CloudWatch log group ARN returned by the flow-log submodule. May be null for s3 destinations or when external resources are used. |
+| [flow\_log\_iam\_role\_arn](#output\_flow\_log\_iam\_role\_arn) | The IAM role ARN returned by the flow-log submodule. May be null for s3 destinations or when external resources are used. |
| [flow\_log\_id](#output\_flow\_log\_id) | The ID of the VPC Flow Log. |
| [igw\_arn](#output\_igw\_arn) | The ARN of the Internet Gateway. |
| [igw\_id](#output\_igw\_id) | The ID of the Internet Gateway. |
diff --git a/infrastructure/modules/vpc/main.tf b/infrastructure/modules/vpc/main.tf
index db9d5ecc..9f3e1aa8 100644
--- a/infrastructure/modules/vpc/main.tf
+++ b/infrastructure/modules/vpc/main.tf
@@ -191,10 +191,10 @@ resource "aws_route_table_association" "edge" {
# terraform-aws-modules/vpc/aws (the root module's built-in
# flow log support is deprecated in v6.x, removed in v7.0.0).
#
-# The submodule creates:
-# - CloudWatch Log Group
-# - IAM Role with scoped trust policy
-# - VPC Flow Log resource
+# Flow log destination and IAM role are managed by the consumer.
+# Supported destination types in this wrapper:
+# - cloud-watch-logs
+# - s3
################################################################
module "flow_log" {
@@ -206,24 +206,17 @@ module "flow_log" {
name = "${module.this.id}-flow-log"
vpc_id = module.vpc.vpc_id
- # CloudWatch destination
- log_destination_type = "cloud-watch-logs"
- cloudwatch_log_group_name = "/vpc/${module.this.id}/flow-logs"
- cloudwatch_log_group_use_name_prefix = false
- cloudwatch_log_group_retention_in_days = var.flow_log_retention_in_days
- cloudwatch_log_group_kms_key_id = var.flow_log_kms_key_id
+ # Destination configuration
+ log_destination_type = var.flow_log_destination_type
+ log_destination = var.flow_log_destination_arn
- # IAM role (created by the submodule with scoped trust policy)
- create_iam_role = true
- iam_role_name = "${module.this.id}-flow-logs"
- iam_role_use_name_prefix = false
+ # CloudWatch destinations require a consumer-managed IAM role.
+ iam_role_arn = var.flow_log_cloudwatch_iam_role_arn
traffic_type = var.flow_log_traffic_type
max_aggregation_interval = var.flow_log_max_aggregation_interval
- cloudwatch_log_group_tags = var.cloudwatch_log_group_tags
- flow_log_tags = var.flow_log_tags
- iam_role_tags = var.iam_role_tags
+ flow_log_tags = var.flow_log_tags
tags = module.this.tags
}
diff --git a/infrastructure/modules/vpc/outputs.tf b/infrastructure/modules/vpc/outputs.tf
index f8407110..caaff8ce 100644
--- a/infrastructure/modules/vpc/outputs.tf
+++ b/infrastructure/modules/vpc/outputs.tf
@@ -154,12 +154,12 @@ output "flow_log_arn" {
}
output "flow_log_cloudwatch_log_group_arn" {
- description = "The ARN of the CloudWatch Log Group for VPC flow logs."
+ description = "The CloudWatch log group ARN returned by the flow-log submodule. May be null for s3 destinations or when external resources are used."
value = module.flow_log.cloudwatch_log_group_arn
}
output "flow_log_iam_role_arn" {
- description = "The ARN of the IAM role used by VPC flow logs."
+ description = "The IAM role ARN returned by the flow-log submodule. May be null for s3 destinations or when external resources are used."
value = module.flow_log.iam_role_arn
}
diff --git a/infrastructure/modules/vpc/validations.tf b/infrastructure/modules/vpc/validations.tf
index df93ece3..3519b253 100644
--- a/infrastructure/modules/vpc/validations.tf
+++ b/infrastructure/modules/vpc/validations.tf
@@ -11,6 +11,7 @@
# * enable_network_firewall requires firewall subnets to be created
# * single_nat_gateway requires private subnets to be created
# * Explicit subnet CIDR lists must have correct length (equal to az_count)
+# * Flow log destination-specific inputs are set correctly
#
################################################################
@@ -99,5 +100,42 @@ resource "terraform_data" "validations" {
condition = length(var.intra_subnets) == 0 || length(var.intra_subnets) == local.az_count
error_message = "intra_subnets must be empty or have exactly ${local.az_count} entries (one per AZ); found ${length(var.intra_subnets)}."
}
+
+ precondition {
+ condition = !var.enable_flow_log || contains(["cloud-watch-logs", "s3"], var.flow_log_destination_type)
+ error_message = "When enable_flow_log is true, flow_log_destination_type must be cloud-watch-logs or s3."
+ }
+
+ precondition {
+ condition = !(
+ var.enable_flow_log &&
+ var.flow_log_destination_type == "cloud-watch-logs" &&
+ (var.flow_log_destination_arn == null || trim(var.flow_log_destination_arn) == "")
+ )
+ error_message = "flow_log_destination_arn must be set when flow_log_destination_type is cloud-watch-logs."
+ }
+
+ precondition {
+ condition = !(
+ var.enable_flow_log &&
+ var.flow_log_destination_type == "cloud-watch-logs" &&
+ (var.flow_log_cloudwatch_iam_role_arn == null || trim(var.flow_log_cloudwatch_iam_role_arn) == "")
+ )
+ error_message = "flow_log_cloudwatch_iam_role_arn must be set when flow_log_destination_type is cloud-watch-logs."
+ }
+
+ precondition {
+ condition = !(
+ var.enable_flow_log &&
+ var.flow_log_destination_type == "s3" &&
+ (var.flow_log_destination_arn == null || trim(var.flow_log_destination_arn) == "")
+ )
+ error_message = "flow_log_destination_arn must be set when flow_log_destination_type is s3."
+ }
+
+ precondition {
+ condition = !(var.enable_flow_log && var.flow_log_destination_type == "s3" && var.flow_log_cloudwatch_iam_role_arn != null && trim(var.flow_log_cloudwatch_iam_role_arn) != "")
+ error_message = "flow_log_cloudwatch_iam_role_arn must be null when flow_log_destination_type is s3."
+ }
}
}
diff --git a/infrastructure/modules/vpc/variables.tf b/infrastructure/modules/vpc/variables.tf
index 936ca933..18a01387 100644
--- a/infrastructure/modules/vpc/variables.tf
+++ b/infrastructure/modules/vpc/variables.tf
@@ -304,15 +304,32 @@ variable "firewall_subnet_tags" {
################################################################
variable "enable_flow_log" {
- description = "Enable VPC flow logs to CloudWatch Logs."
+ description = "Enable VPC flow logs. Destination is controlled by flow_log_destination_type."
type = bool
default = true
}
-variable "flow_log_retention_in_days" {
- description = "Number of days to retain VPC flow logs in CloudWatch."
- type = number
- default = 365
+variable "flow_log_destination_type" {
+ description = "Destination type for VPC flow logs. Supported values in this module: cloud-watch-logs, s3."
+ type = string
+ default = "cloud-watch-logs"
+
+ validation {
+ condition = contains(["cloud-watch-logs", "s3"], var.flow_log_destination_type)
+ error_message = "flow_log_destination_type must be one of cloud-watch-logs or s3."
+ }
+}
+
+variable "flow_log_destination_arn" {
+ description = "ARN of the flow log destination. Required when flow logs are enabled. For cloud-watch-logs, set to a CloudWatch log group ARN. For s3, set to an S3 bucket ARN."
+ type = string
+ default = null
+}
+
+variable "flow_log_cloudwatch_iam_role_arn" {
+ description = "Existing IAM role ARN used by VPC Flow Logs when destination type is cloud-watch-logs."
+ type = string
+ default = null
}
variable "flow_log_traffic_type" {
@@ -326,12 +343,6 @@ variable "flow_log_traffic_type" {
}
}
-variable "flow_log_kms_key_id" {
- description = "ARN of a KMS key to encrypt the CloudWatch log group. Leave null for no encryption."
- type = string
- default = null
-}
-
variable "flow_log_max_aggregation_interval" {
description = "The maximum interval of time (seconds) during which a flow of packets is captured. Valid values: 60 (1 min) or 600 (10 min)."
type = number
@@ -343,23 +354,11 @@ variable "flow_log_max_aggregation_interval" {
}
}
-variable "cloudwatch_log_group_tags" {
- description = "Additional tags for the CloudWatch log group."
- type = map(string)
- default = {}
-}
-
variable "flow_log_tags" {
description = "Additional tags for the VPC flow log."
type = map(string)
default = {}
}
-variable "iam_role_tags" {
- description = "Additional tags for the IAM role used by the VPC flow log."
- type = map(string)
- default = {}
-}
-
################################################################
# VPC Endpoints