diff --git a/.bumpversion.cfg b/.bumpversion.cfg index b49e513..ab04341 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.0.0 +current_version = 3.0.0 commit = True message = Bumps version to {new_version} tag = False diff --git a/CHANGELOG.md b/CHANGELOG.md index 55dd2c1..76c9896 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,27 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +### [3.0.0](https://github.com/plus3it/terraform-aws-tardigrade-vpc-flow-log/releases/tag/3.0.0) + +**Released**: 2024.11.20 + +**Summary**: + +* Refactors module to support all attributes of the Flow Log resource +* Supported sources include: + * VPC ID + * Subnet ID + * Transit Gateway ID + * Transit Gateway Attachment ID + * Elastic Network Interface ID +* Supported destination types include: + * CloudWatch Log Group + * S3 Bucket + * Kinesis Data Firehose +* When the destination type is a CloudWatch Log Group, the module supports either + creating the log group, or providing the log group arn via the `log_destination` + input. For other destination types, the `log_destination` is required. + ### 1.0.2 **Released**: 2019.10.28 diff --git a/README.md b/README.md index f45074a..feaf610 100644 --- a/README.md +++ b/README.md @@ -2,65 +2,59 @@ Terraform module to create a VPC Flow Log -## Testing - -Manual testing: - -``` -# Replace "xxx" with an actual AWS profile, then execute the integration tests. -export AWS_PROFILE=xxx -make terraform/pytest PYTEST_ARGS="-v --nomock" -``` - -For automated testing, PYTEST_ARGS is optional and no profile is needed: - -``` -make mockstack/up -make terraform/pytest PYTEST_ARGS="-v" -make mockstack/clean -``` - ## Requirements | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 0.12 | +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 5.68.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | n/a | +| [aws](#provider\_aws) | >= 5.68.0 | ## Resources | Name | Type | |------|------| -| [aws_iam_policy_document.role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | -| [aws_iam_policy_document.trust](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | -| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | +| [aws_caller_identity.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_iam_policy_document.cloudwatch_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.cloudwatch_trust](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_partition.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [iam\_role\_arn](#input\_iam\_role\_arn) | (Optional) ARN for the IAM role to attach to the flow log. If blank, a minimal role will be created | `string` | `null` | no | -| [log\_destination](#input\_log\_destination) | (Optional) The ARN of the logging destination. | `string` | `null` | no | -| [log\_destination\_type](#input\_log\_destination\_type) | Controls whether to create the VPC Flow Log with a `cloud-watch-logs` or `s3` bucket destination | `string` | `null` | no | -| [log\_format](#input\_log\_format) | (Optional) The fields to include in the flow log record, in the order in which they should appear. | `string` | `null` | no | -| [log\_group\_name](#input\_log\_group\_name) | (Optional) Name to assign to the CloudWatch Log Group. If blank, will use `/aws/vpc/flow-log/$${var.vpc_id}` | `string` | `null` | no | -| [tags](#input\_tags) | A map of tags to add to the CloudWatch Log Group for the VPC Flow Log | `map(string)` | `{}` | no | -| [vpc\_id](#input\_vpc\_id) | VPC ID for which the VPC Flow Log will be created | `string` | `null` | no | +| [flow\_log](#input\_flow\_log) | Object of attributes for managing a Flow Log |
object({
name = string
log_destination_type = string

eni_id = optional(string)
subnet_id = optional(string)
transit_gateway_id = optional(string)
transit_gateway_attachment_id = optional(string)
vpc_id = optional(string)

deliver_cross_account_role = optional(string)
iam_role_arn = optional(string)
log_destination = optional(string)
log_format = optional(string)
max_aggregation_interval = optional(number)
tags = optional(map(string), {})
traffic_type = optional(string, "ALL")

destination_options = optional(object({
file_format = optional(string)
hive_compatible_partitions = optional(bool)
per_hour_partition = optional(bool)
}))

cloudwatch_log_group = optional(object({
enable = optional(bool, true)
name = optional(string)
kms_key_id = optional(string)
log_group_class = optional(string, "INFREQUENT_ACCESS")
retention_in_days = optional(number, 30)
skip_destroy = optional(bool, false)
tags = optional(map(string), {})
}), {})
})
| n/a | yes | ## Outputs | Name | Description | |------|-------------| -| [flow\_log\_id](#output\_flow\_log\_id) | The ID of the VPC Flow Log | -| [iam\_role\_arn](#output\_iam\_role\_arn) | ARN of the IAM Role for the VPC Flow Log | -| [iam\_role\_name](#output\_iam\_role\_name) | Name of the IAM Role for the VPC Flow Log | -| [iam\_role\_unique\_id](#output\_iam\_role\_unique\_id) | Unique ID of the IAM Role for the VPC Flow Log | -| [log\_group\_arn](#output\_log\_group\_arn) | ARN of the Log Group for the VPC Flow Log | +| [cloudwatch\_log\_group](#output\_cloudwatch\_log\_group) | Object of attributes for the CloudWatch Log Group | +| [flow\_log](#output\_flow\_log) | Object of attributes for the Flow Log | +| [iam\_role](#output\_iam\_role) | Object of attributes for the IAM Role used by the Flow Log | + +## Testing + +Manual testing: + +``` +# Replace "xxx" with an actual AWS profile, then execute the integration tests. +export AWS_PROFILE=xxx +make terraform/pytest PYTEST_ARGS="-v --nomock" +``` + +For automated testing, PYTEST_ARGS is optional and no profile is needed: + +``` +make mockstack/up +make terraform/pytest PYTEST_ARGS="-v" +make mockstack/clean +``` diff --git a/main.tf b/main.tf index b6eb506..c02a8b4 100644 --- a/main.tf +++ b/main.tf @@ -1,15 +1,109 @@ -locals { - iam_role_name = "flow-log-${format("%v", var.vpc_id)}" - log_group_name = var.log_group_name == null ? "/aws/vpc/flow-log/${format("%v", var.vpc_id)}" : var.log_group_name - iam_role_arn = var.iam_role_arn == null ? join("", aws_iam_role.this.*.arn) : var.iam_role_arn - create_iam_role = var.log_destination_type == "cloud-watch-logs" && var.iam_role_arn == null +resource "aws_flow_log" "this" { + # Source ID -- one of these must be set + eni_id = var.flow_log.eni_id + subnet_id = var.flow_log.subnet_id + transit_gateway_attachment_id = var.flow_log.transit_gateway_attachment_id + transit_gateway_id = var.flow_log.transit_gateway_id + vpc_id = var.flow_log.vpc_id + + # Options + deliver_cross_account_role = var.flow_log.deliver_cross_account_role + iam_role_arn = local.cloudwatch_iam_role_arn + log_destination_type = var.flow_log.log_destination_type + log_destination = local.create_log_group ? one(aws_cloudwatch_log_group.this[*].arn) : var.flow_log.log_destination + log_format = var.flow_log.log_format + max_aggregation_interval = var.flow_log.transit_gateway_attachment_id != null || var.flow_log.transit_gateway_id != null ? 60 : var.flow_log.max_aggregation_interval + traffic_type = var.flow_log.traffic_type + + tags = merge( + { + Name = var.flow_log.name + }, + var.flow_log.tags, + ) + + dynamic "destination_options" { + for_each = var.flow_log.destination_options != null ? [var.flow_log.destination_options] : [] + content { + file_format = destination_options.value.file_format + hive_compatible_partitions = destination_options.value.hive_compatible_partitions + per_hour_partition = destination_options.value.per_hour_partition + } + } } -data "aws_partition" "current" { +resource "aws_cloudwatch_log_group" "this" { + count = local.create_log_group ? 1 : 0 + + name = local.log_group_name + + kms_key_id = var.flow_log.cloudwatch_log_group.kms_key_id + log_group_class = var.flow_log.cloudwatch_log_group.log_group_class + retention_in_days = var.flow_log.cloudwatch_log_group.retention_in_days + skip_destroy = var.flow_log.cloudwatch_log_group.skip_destroy + + tags = merge( + { + Name = local.log_group_name + }, + var.flow_log.cloudwatch_log_group.tags, + ) +} + +resource "aws_iam_role" "cloudwatch" { + count = local.create_cloudwatch_iam_role ? 1 : 0 + + name = local.cloudwatch_iam_role_name + assume_role_policy = data.aws_iam_policy_document.cloudwatch_trust[0].json + + tags = merge( + { + Name = local.cloudwatch_iam_role_name + }, + var.flow_log.tags, + ) +} + +resource "aws_iam_role_policy" "cloudwatch" { + count = local.create_cloudwatch_iam_role ? 1 : 0 + + name = local.cloudwatch_iam_role_name + role = aws_iam_role.cloudwatch[0].id + policy = data.aws_iam_policy_document.cloudwatch_policy[0].json +} + +resource "aws_iam_role_policies_exclusive" "cloudwatch" { + count = local.create_cloudwatch_iam_role ? 1 : 0 + + role_name = aws_iam_role.cloudwatch[0].name + policy_names = [aws_iam_role_policy.cloudwatch[0].name] +} + +locals { + source_id = coalesce( + var.flow_log.eni_id, + var.flow_log.subnet_id, + var.flow_log.transit_gateway_attachment_id, + var.flow_log.transit_gateway_id, + var.flow_log.vpc_id, + ) + + create_cloudwatch_iam_role = var.flow_log.iam_role_arn == null && var.flow_log.log_destination_type == "cloud-watch-logs" + cloudwatch_iam_role_arn = local.create_cloudwatch_iam_role ? coalesce(one(aws_iam_role.cloudwatch[*].arn), aws_iam_role_policy.cloudwatch[0].name) : var.flow_log.iam_role_arn + cloudwatch_iam_role_name = "flow-log-cloudwatch-${format("%v", local.source_id)}" + + create_log_group = var.flow_log.cloudwatch_log_group.enable && var.flow_log.log_destination_type == "cloud-watch-logs" + log_group_name = var.flow_log.cloudwatch_log_group.name == null ? "/aws/vendedlogs/flow-log/${format("%v", local.source_id)}" : var.flow_log.cloudwatch_log_group.name + + account_id = data.aws_caller_identity.this.account_id + partition = data.aws_partition.this.partition } -data "aws_iam_policy_document" "role" { - count = local.create_iam_role ? 1 : 0 +data "aws_caller_identity" "this" {} +data "aws_partition" "this" {} + +data "aws_iam_policy_document" "cloudwatch_policy" { + count = local.create_cloudwatch_iam_role ? 1 : 0 statement { actions = [ @@ -21,14 +115,29 @@ data "aws_iam_policy_document" "role" { ] resources = [ - "arn:${data.aws_partition.current.partition}:logs:*:*:log-group:${local.log_group_name}", - "arn:${data.aws_partition.current.partition}:logs:*:*:log-group:${local.log_group_name}:*", + "arn:${local.partition}:logs:*:*:log-group:${local.log_group_name}", + "arn:${local.partition}:logs:*:*:log-group:${local.log_group_name}:*", ] + + condition { + test = "StringEquals" + variable = "aws:SourceAccount" + values = [local.account_id] + } + + condition { + test = "ArnLike" + variable = "aws:SourceArn" + values = [ + "arn:${local.partition}:logs:*:*:log-group:${local.log_group_name}", + "arn:${local.partition}:logs:*:*:log-group:${local.log_group_name}:*", + ] + } } } -data "aws_iam_policy_document" "trust" { - count = local.create_iam_role ? 1 : 0 +data "aws_iam_policy_document" "cloudwatch_trust" { + count = local.create_cloudwatch_iam_role ? 1 : 0 statement { actions = ["sts:AssumeRole"] @@ -39,37 +148,3 @@ data "aws_iam_policy_document" "trust" { } } } - -resource "aws_flow_log" "this" { - - log_destination_type = var.log_destination_type - log_destination = var.log_destination_type == "s3" ? var.log_destination : join("", aws_cloudwatch_log_group.this.*.arn) - iam_role_arn = local.iam_role_arn - log_format = var.log_format - vpc_id = var.vpc_id - traffic_type = "ALL" -} - -resource "aws_cloudwatch_log_group" "this" { - count = var.log_destination_type == "cloud-watch-logs" ? 1 : 0 - - name = local.log_group_name - tags = var.tags -} - -resource "aws_iam_role" "this" { - count = local.create_iam_role ? 1 : 0 - - name = local.iam_role_name - assume_role_policy = data.aws_iam_policy_document.trust[0].json - tags = var.tags -} - -resource "aws_iam_role_policy" "this" { - count = local.create_iam_role ? 1 : 0 - - name = local.iam_role_name - role = aws_iam_role.this[0].id - policy = data.aws_iam_policy_document.role[0].json -} - diff --git a/outputs.tf b/outputs.tf index d49795a..56a23b4 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,26 +1,15 @@ # VPC Flow Log -output "flow_log_id" { - description = "The ID of the VPC Flow Log" - value = aws_flow_log.this.id +output "flow_log" { + description = "Object of attributes for the Flow Log" + value = aws_flow_log.this } -output "log_group_arn" { - description = "ARN of the Log Group for the VPC Flow Log" - value = join("", aws_cloudwatch_log_group.this.*.arn) +output "cloudwatch_log_group" { + description = "Object of attributes for the CloudWatch Log Group" + value = aws_cloudwatch_log_group.this } -output "iam_role_arn" { - description = "ARN of the IAM Role for the VPC Flow Log" - value = join("", aws_iam_role.this.*.arn) +output "iam_role" { + description = "Object of attributes for the IAM Role used by the Flow Log" + value = aws_iam_role.cloudwatch } - -output "iam_role_unique_id" { - description = "Unique ID of the IAM Role for the VPC Flow Log" - value = join("", aws_iam_role.this.*.unique_id) -} - -output "iam_role_name" { - description = "Name of the IAM Role for the VPC Flow Log" - value = join("", aws_iam_role.this.*.name) -} - diff --git a/tests/baseline_cloudwatch_logs/main.tf b/tests/baseline_cloudwatch_logs/main.tf deleted file mode 100644 index 2aaae79..0000000 --- a/tests/baseline_cloudwatch_logs/main.tf +++ /dev/null @@ -1,20 +0,0 @@ -module "vpc" { - source = "github.com/terraform-aws-modules/terraform-aws-vpc?ref=v5.12.0" - - providers = { - aws = aws - } - - name = "tardigrade-vpc-flow-log-testing" - cidr = "10.0.0.0/16" -} - -module "baseline_s3" { - source = "../../" - providers = { - aws = aws - } - - vpc_id = module.vpc.vpc_id - log_destination_type = "cloud-watch-logs" -} diff --git a/tests/baseline_s3/main.tf b/tests/baseline_s3/main.tf deleted file mode 100644 index cab51ee..0000000 --- a/tests/baseline_s3/main.tf +++ /dev/null @@ -1,30 +0,0 @@ -resource "random_id" "name" { - byte_length = 6 - prefix = "terraform-aws-vpc-flow-log-" -} - -resource "aws_s3_bucket" "this" { - bucket = random_id.name.hex -} - -module "vpc" { - source = "github.com/terraform-aws-modules/terraform-aws-vpc?ref=v5.12.0" - - providers = { - aws = aws - } - - name = "tardigrade-vpc-flow-log-testing" - cidr = "10.0.0.0/16" -} - -module "baseline_s3" { - source = "../../" - providers = { - aws = aws - } - - vpc_id = module.vpc.vpc_id - log_destination_type = "s3" - log_destination = aws_s3_bucket.this.arn -} diff --git a/tests/baseline_s3_log_format/main.tf b/tests/baseline_s3_log_format/main.tf deleted file mode 100644 index 43e3251..0000000 --- a/tests/baseline_s3_log_format/main.tf +++ /dev/null @@ -1,31 +0,0 @@ -resource "random_id" "name" { - byte_length = 6 - prefix = "terraform-aws-vpc-flow-log-" -} - -resource "aws_s3_bucket" "this" { - bucket = random_id.name.hex -} - -module "vpc" { - source = "github.com/terraform-aws-modules/terraform-aws-vpc?ref=v5.12.0" - - providers = { - aws = aws - } - - name = "tardigrade-vpc-flow-log-testing" - cidr = "10.0.0.0/16" -} - -module "baseline_s3_log_format" { - source = "../../" - providers = { - aws = aws - } - - vpc_id = module.vpc.vpc_id - log_destination_type = "s3" - log_destination = aws_s3_bucket.this.arn - log_format = "$${version} $${account-id} $${interface-id} $${srcaddr} $${dstaddr} $${srcport} $${dstport} $${protocol} $${packets} $${bytes} $${start} $${end} $${action} $${log-status} $${vpc-id} $${subnet-id} $${instance-id} $${tcp-flags} $${type} $${pkt-srcaddr} $${pkt-dstaddr} $${region} $${az-id} $${sublocation-type} $${sublocation-id}" -} diff --git a/tests/test_tgw_flow_log/main.tf b/tests/test_tgw_flow_log/main.tf new file mode 100644 index 0000000..cf9fc99 --- /dev/null +++ b/tests/test_tgw_flow_log/main.tf @@ -0,0 +1,45 @@ +module "flow_log_cloudwatch" { + source = "../../" + + flow_log = { + name = "${local.test_name}-cloudwatch-${local.id}" + log_destination_type = "cloud-watch-logs" + transit_gateway_id = aws_ec2_transit_gateway.this.id + } +} + +module "flow_log_s3" { + source = "../../" + + flow_log = { + name = "${local.test_name}-s3-${local.id}" + log_destination_type = "s3" + log_destination = aws_s3_bucket.this.arn + transit_gateway_id = aws_ec2_transit_gateway.this.id + } +} + +resource "aws_ec2_transit_gateway" "this" { + description = "${local.test_name}-tgw-${local.id}" + + tags = { + Name = "${local.test_name}-tgw-${local.id}" + } +} + +resource "aws_s3_bucket" "this" { + bucket = "${local.test_name}-s3-bucket-${local.id}" + force_destroy = true +} + +resource "random_string" "this" { + length = 6 + upper = false + special = false + numeric = false +} + +locals { + test_name = "tardigrade-test-flow-log" + id = random_string.this.result +} diff --git a/tests/test_vpc_flow_log/main.tf b/tests/test_vpc_flow_log/main.tf new file mode 100644 index 0000000..29d6da4 --- /dev/null +++ b/tests/test_vpc_flow_log/main.tf @@ -0,0 +1,45 @@ +module "flow_log_cloudwatch" { + source = "../../" + + flow_log = { + name = "${local.test_name}-cloudwatch-${local.id}" + log_destination_type = "cloud-watch-logs" + vpc_id = module.vpc.vpc_id + } +} + +module "flow_log_s3" { + source = "../../" + + flow_log = { + name = "${local.test_name}-s3-${local.id}" + log_destination_type = "s3" + log_destination = aws_s3_bucket.this.arn + vpc_id = module.vpc.vpc_id + } +} + +module "vpc" { + source = "terraform-aws-modules/vpc/aws" + version = "~> 5.0" + + name = "${local.test_name}-vpc-${local.id}" + cidr = "10.0.0.0/16" +} + +resource "aws_s3_bucket" "this" { + bucket = "${local.test_name}-s3-bucket-${local.id}" + force_destroy = true +} + +resource "random_string" "this" { + length = 6 + upper = false + special = false + numeric = false +} + +locals { + test_name = "tardigrade-test-flow-log" + id = random_string.this.result +} diff --git a/variables.tf b/variables.tf index eb94aa7..4238bb1 100644 --- a/variables.tf +++ b/variables.tf @@ -1,41 +1,37 @@ -variable "log_destination_type" { - description = "Controls whether to create the VPC Flow Log with a `cloud-watch-logs` or `s3` bucket destination" - type = string - default = null -} - -variable "vpc_id" { - description = "VPC ID for which the VPC Flow Log will be created" - type = string - default = null -} - -variable "iam_role_arn" { - description = "(Optional) ARN for the IAM role to attach to the flow log. If blank, a minimal role will be created" - type = string - default = null -} +variable "flow_log" { + description = "Object of attributes for managing a Flow Log" + type = object({ + name = string + log_destination_type = string -variable "log_destination" { - description = "(Optional) The ARN of the logging destination." - type = string - default = null -} + eni_id = optional(string) + subnet_id = optional(string) + transit_gateway_id = optional(string) + transit_gateway_attachment_id = optional(string) + vpc_id = optional(string) -variable "log_format" { - description = "(Optional) The fields to include in the flow log record, in the order in which they should appear." - type = string - default = null -} + deliver_cross_account_role = optional(string) + iam_role_arn = optional(string) + log_destination = optional(string) + log_format = optional(string) + max_aggregation_interval = optional(number) + tags = optional(map(string), {}) + traffic_type = optional(string, "ALL") -variable "log_group_name" { - description = "(Optional) Name to assign to the CloudWatch Log Group. If blank, will use `/aws/vpc/flow-log/$$${var.vpc_id}`" - type = string - default = null -} + destination_options = optional(object({ + file_format = optional(string) + hive_compatible_partitions = optional(bool) + per_hour_partition = optional(bool) + })) -variable "tags" { - description = "A map of tags to add to the CloudWatch Log Group for the VPC Flow Log" - type = map(string) - default = {} + cloudwatch_log_group = optional(object({ + enable = optional(bool, true) + name = optional(string) + kms_key_id = optional(string) + log_group_class = optional(string, "INFREQUENT_ACCESS") + retention_in_days = optional(number, 30) + skip_destroy = optional(bool, false) + tags = optional(map(string), {}) + }), {}) + }) } diff --git a/versions.tf b/versions.tf index ac97c6a..822a17a 100644 --- a/versions.tf +++ b/versions.tf @@ -1,4 +1,11 @@ terraform { - required_version = ">= 0.12" + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.68.0" + } + } }