Skip to content

Commit

Permalink
Merge pull request #323 from lorengordon/feat/support-tgw
Browse files Browse the repository at this point in the history
Refactors module to support all attributes of the Flow Log resource
  • Loading branch information
lorengordon authored Nov 21, 2024
2 parents beda650 + 7faffc8 commit 3cf2880
Show file tree
Hide file tree
Showing 12 changed files with 311 additions and 220 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -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
Expand Down
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 29 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

<!-- BEGIN TFDOCS -->
## Requirements

| Name | Version |
|------|---------|
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 0.12 |
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.0.0 |
| <a name="requirement_aws"></a> [aws](#requirement\_aws) | >= 5.68.0 |

## Providers

| Name | Version |
|------|---------|
| <a name="provider_aws"></a> [aws](#provider\_aws) | n/a |
| <a name="provider_aws"></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 |
|------|-------------|------|---------|:--------:|
| <a name="input_iam_role_arn"></a> [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 |
| <a name="input_log_destination"></a> [log\_destination](#input\_log\_destination) | (Optional) The ARN of the logging destination. | `string` | `null` | no |
| <a name="input_log_destination_type"></a> [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 |
| <a name="input_log_format"></a> [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 |
| <a name="input_log_group_name"></a> [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 |
| <a name="input_tags"></a> [tags](#input\_tags) | A map of tags to add to the CloudWatch Log Group for the VPC Flow Log | `map(string)` | `{}` | no |
| <a name="input_vpc_id"></a> [vpc\_id](#input\_vpc\_id) | VPC ID for which the VPC Flow Log will be created | `string` | `null` | no |
| <a name="input_flow_log"></a> [flow\_log](#input\_flow\_log) | Object of attributes for managing a Flow Log | <pre>object({<br> name = string<br> log_destination_type = string<br><br> eni_id = optional(string)<br> subnet_id = optional(string)<br> transit_gateway_id = optional(string)<br> transit_gateway_attachment_id = optional(string)<br> vpc_id = optional(string)<br><br> deliver_cross_account_role = optional(string)<br> iam_role_arn = optional(string)<br> log_destination = optional(string)<br> log_format = optional(string)<br> max_aggregation_interval = optional(number)<br> tags = optional(map(string), {})<br> traffic_type = optional(string, "ALL")<br><br> destination_options = optional(object({<br> file_format = optional(string)<br> hive_compatible_partitions = optional(bool)<br> per_hour_partition = optional(bool)<br> }))<br><br> cloudwatch_log_group = optional(object({<br> enable = optional(bool, true)<br> name = optional(string)<br> kms_key_id = optional(string)<br> log_group_class = optional(string, "INFREQUENT_ACCESS")<br> retention_in_days = optional(number, 30)<br> skip_destroy = optional(bool, false)<br> tags = optional(map(string), {})<br> }), {})<br> })</pre> | n/a | yes |

## Outputs

| Name | Description |
|------|-------------|
| <a name="output_flow_log_id"></a> [flow\_log\_id](#output\_flow\_log\_id) | The ID of the VPC Flow Log |
| <a name="output_iam_role_arn"></a> [iam\_role\_arn](#output\_iam\_role\_arn) | ARN of the IAM Role for the VPC Flow Log |
| <a name="output_iam_role_name"></a> [iam\_role\_name](#output\_iam\_role\_name) | Name of the IAM Role for the VPC Flow Log |
| <a name="output_iam_role_unique_id"></a> [iam\_role\_unique\_id](#output\_iam\_role\_unique\_id) | Unique ID of the IAM Role for the VPC Flow Log |
| <a name="output_log_group_arn"></a> [log\_group\_arn](#output\_log\_group\_arn) | ARN of the Log Group for the VPC Flow Log |
| <a name="output_cloudwatch_log_group"></a> [cloudwatch\_log\_group](#output\_cloudwatch\_log\_group) | Object of attributes for the CloudWatch Log Group |
| <a name="output_flow_log"></a> [flow\_log](#output\_flow\_log) | Object of attributes for the Flow Log |
| <a name="output_iam_role"></a> [iam\_role](#output\_iam\_role) | Object of attributes for the IAM Role used by the Flow Log |

<!-- END TFDOCS -->

## 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
```
167 changes: 121 additions & 46 deletions main.tf
Original file line number Diff line number Diff line change
@@ -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 = [
Expand All @@ -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"]
Expand All @@ -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
}

29 changes: 9 additions & 20 deletions outputs.tf
Original file line number Diff line number Diff line change
@@ -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)
}

20 changes: 0 additions & 20 deletions tests/baseline_cloudwatch_logs/main.tf

This file was deleted.

Loading

0 comments on commit 3cf2880

Please sign in to comment.