Its reInvent 2025 in two days. This will be the first time I will be there in person ever ( as an AWS employee or customer ). I should say more of my customer work kept me away from the announcements than an actually distancing myself from it :). But I have been keeping an eye out for some which fell into Terraform support ( especially ones which were in both the providers). Much goes into getting these ready even if they look like a small feature update for an existing service or a completely new thing altogether. So congratulations and thank you for all the contributors who make this possible. For this post, lets take a look at one of those :

  • VPC encryption controls

So what is it ?

VPC encryption control is a feature that allows users to monitor, enforce, and demonstrate encryption for data in transit within and between virtual private clouds (VPCs). It provides visibility into the encryption status of traffic, helps identify resources sending unencrypted traffic, and enables users to enforce encryption to meet compliance standards like HIPAA and PCI DSS.

Sprinkled in those sentences are control modes and what the expectation of the service is.

  • monitor : Think of this as a first step when you introduce a security control or policy. You want to observe how the existing traffic flows between AWS resources in and across VPCs.
  • enforce : Think of this as the step to enforce that encryption requirement within the VPC boundary. There is a catch here though.
    • For existing VPCs : can only be enabled in monitor mode first. The control mode can be updated to enforce once any resources which currently allow unencrypted traffic has been updated to use encryption.
    • For new VPCs : enable either modes. But with our intent to ensure encryption is required, enforce would make more sense.

Lets test these with the configuration using Terraform.

Support via Terraform

The resource equivalent in Terraform is available on both AWS and AWSCC provider. The resource names and links to the documentation are as follows:

monitor control mode

Let’s look at the configuration we are working with:

data "aws_vpc" "existing" {
  id = "vpc-091e289e155590a6f"
}

# AWSCC provider example
resource "awscc_ec2_vpc_encryption_control" "example" {
  vpc_id                                       = data.aws_vpc.existing.id
  mode                                         = "monitor"
  internet_gateway_exclusion_input             = "disable"
  nat_gateway_exclusion_input                  = "disable"
  egress_only_internet_gateway_exclusion_input = "disable"
  virtual_private_gateway_exclusion_input      = "disable"
  vpc_peering_exclusion_input                  = "disable"
  vpc_lattice_exclusion_input                  = "disable"
  elastic_file_system_exclusion_input          = "disable"
  lambda_exclusion_input                       = "disable"

  tags = [{
    key   = "Name"
    value = "vpc-encryption-control-example"
  }]
}

# AWS provider example
resource "aws_vpc_encryption_control" "existing" {
  vpc_id = data.aws_vpc.existing.id
  mode   = "monitor"
}

For the monitor control mode set , lets run terraform init and terraform apply.

data.aws_vpc.existing: Reading...
data.aws_vpc.existing: Read complete after 0s [id=vpc-091e289e155590a6f]

Terraform used the selected providers to generate the following execution plan. Resource actions
are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # awscc_ec2_vpc_encryption_control.example will be created
  + resource "awscc_ec2_vpc_encryption_control" "example" {
      + egress_only_internet_gateway_exclusion_input = "disable"
      + elastic_file_system_exclusion_input          = "disable"
      + id                                           = (known after apply)
      + internet_gateway_exclusion_input             = "disable"
      + lambda_exclusion_input                       = "disable"
      + mode                                         = "monitor"
      + nat_gateway_exclusion_input                  = "disable"
      + resource_exclusions                          = (known after apply)
      + state                                        = (known after apply)
      + state_message                                = (known after apply)
      + tags                                         = [
          + {
              + key   = "Name"
              + value = "vpc-encryption-control-example"
            },
        ]
      + virtual_private_gateway_exclusion_input      = "disable"
      + vpc_encryption_control_id                    = (known after apply)
      + vpc_id                                       = "vpc-091e289e155590a6f"
      + vpc_lattice_exclusion_input                  = "disable"
      + vpc_peering_exclusion_input                  = "disable"
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + resource_exclusions       = (known after apply)
  + vpc_encryption_control_id = (known after apply)
  + vpc_encryption_mode       = "monitor"
  + vpc_encryption_state      = (known after apply)
awscc_ec2_vpc_encryption_control.example: Creating...
awscc_ec2_vpc_encryption_control.example: Still creating... [00m10s elapsed]
awscc_ec2_vpc_encryption_control.example: Still creating... [00m20s elapsed]
...
awscc_ec2_vpc_encryption_control.example: Still creating... [01m20s elapsed]
awscc_ec2_vpc_encryption_control.example: Still creating... [01m30s elapsed]
awscc_ec2_vpc_encryption_control.example: Creation complete after 1m38s [id=vpcec-0e83ea45895d28728]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

vpc_encryption_control_id = "vpcec-0e83ea45895d28728"
vpc_encryption_mode = "monitor"
vpc_encryption_state = "available"

The encryption mode gets set to monitor as expected.

Control mode of enforce on an existing VPC

With the AWS provider

Configuration is modified to set the mode to monitor

# AWS provider example
resource "aws_vpc_encryption_control" "existing" {
  vpc_id = data.aws_vpc.existing.id
  mode   = "monitor"
}

The deployment took ~15 mins owing to the fact that the provider is infact setting the enforce mode to monitor initially and then updating to enforce while setting the encryption control state to `Available.

tfy
data.aws_vpc.existing: Reading...
data.aws_vpc.existing: Read complete after 1s [id=vpc-091e289e155590a6f]

Terraform used the selected providers to generate the following execution plan. Resource actions
are indicated with the following symbols:
 + create

Terraform will perform the following actions:

 # aws_vpc_encryption_control.existing will be created
 + resource "aws_vpc_encryption_control" "existing" {
     + egress_only_internet_gateway_exclusion = "disable"
     + elastic_file_system_exclusion          = "disable"
     + id                                     = (known after apply)
     + internet_gateway_exclusion             = "disable"
     + lambda_exclusion                       = "disable"
     + mode                                   = "enforce"
     + nat_gateway_exclusion                  = "disable"
     + region                                 = "us-east-1"
     + resource_exclusions                    = (known after apply)
     + state                                  = (known after apply)
     + state_message                          = (known after apply)
     + tags_all                               = {}
     + virtual_private_gateway_exclusion      = "disable"
     + vpc_id                                 = "vpc-091e289e155590a6f"
     + vpc_lattice_exclusion                  = "disable"
     + vpc_peering_exclusion                  = "disable"
   }

Plan: 1 to add, 0 to change, 0 to destroy.
aws_vpc_encryption_control.existing: Creating...
aws_vpc_encryption_control.existing: Still creating... [00m10s elapsed]
aws_vpc_encryption_control.existing: Still creating... [00m20s elapsed]
....
aws_vpc_encryption_control.existing: Still creating... [14m50s elapsed]
aws_vpc_encryption_control.existing: Still creating... [15m00s elapsed]
aws_vpc_encryption_control.existing: Still creating... [15m10s elapsed]
aws_vpc_encryption_control.existing: Creation complete after 15m15s [id=vpcec-0c49ba8012a7df0b7]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Subsequent runs on terraform apply show no changes to be applied as the AWS provider based resource provisions or modifies the resource based on the final desired state - A mode of enforce


❯ tfy
data.aws_vpc.existing: Reading...
data.aws_vpc.existing: Read complete after 1s [id=vpc-091e289e155590a6f]
aws_vpc_encryption_control.existing: Refreshing state... [id=vpcec-0c49ba8012a7df0b7]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no
differences, so no changes are needed.

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Cleanup:

tfd
data.aws_vpc.existing: Reading...
data.aws_vpc.existing: Read complete after 1s [id=vpc-091e289e155590a6f]
aws_vpc_encryption_control.existing: Refreshing state... [id=vpcec-0c49ba8012a7df0b7]

Terraform used the selected providers to generate the following execution plan. Resource actions
are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # aws_vpc_encryption_control.existing will be destroyed
  - resource "aws_vpc_encryption_control" "existing" {
      - egress_only_internet_gateway_exclusion = "disable" -> null
      - elastic_file_system_exclusion          = "disable" -> null
      - id                                     = "vpcec-0c49ba8012a7df0b7" -> null
      - internet_gateway_exclusion             = "disable" -> null
      - lambda_exclusion                       = "disable" -> null
      - mode                                   = "enforce" -> null
      - nat_gateway_exclusion                  = "disable" -> null
      - region                                 = "us-east-1" -> null
      - resource_exclusions                    = {
          - egress_only_internet_gateway = {
              - state         = "disabled"
              - state_message = "succeeded"
            }
          - elastic_file_system          = {
              - state         = "disabled"
              - state_message = "succeeded"
            }
          - internet_gateway             = {
              - state         = "disabled"
              - state_message = "succeeded"
            }
          - lambda                       = {
              - state         = "disabled"
              - state_message = "succeeded"
            }
          - nat_gateway                  = {
              - state         = "disabled"
              - state_message = "succeeded"
            }
          - virtual_private_gateway      = {
              - state         = "disabled"
              - state_message = "succeeded"
            }
          - vpc_lattice                  = {
              - state         = "disabled"
              - state_message = "succeeded"
            }
          - vpc_peering                  = {
              - state         = "disabled"
              - state_message = "succeeded"
            }
        } -> null
      - state                                  = "available" -> null
      - state_message                          = "succeeded" -> null
      - tags_all                               = {} -> null
      - virtual_private_gateway_exclusion      = "disable" -> null
      - vpc_id                                 = "vpc-091e289e155590a6f" -> null
      - vpc_lattice_exclusion                  = "disable" -> null
      - vpc_peering_exclusion                  = "disable" -> null
    }

Plan: 0 to add, 0 to change, 1 to destroy.
aws_vpc_encryption_control.existing: Destroying... [id=vpcec-0c49ba8012a7df0b7]
aws_vpc_encryption_control.existing: Still destroying... [id=vpcec-0c49ba8012a7df0b7, 00m10s elapsed]
aws_vpc_encryption_control.existing: Still destroying... [id=vpcec-0c49ba8012a7df0b7, 00m20s elapsed]
aws_vpc_encryption_control.existing: Destruction complete after 24s

Destroy complete! Resources: 1 destroyed.

With AWSCC provider

This is where it gets interesting. Keep in mind that the AWS documentation around the two step approach of setting the encryption control mode on existing VPCs.

With the mode set to enforce on the first apply, AWSCC provider sets the resource to monitor on the console while state file indicates that the mode is enforce. There must be some issue in the way the the CloudControl API is responding here for the get-resource request. Due to this, the actual operation takes only 1 minute or lower.

tfy
data.aws_vpc.existing: Reading...
data.aws_vpc.existing: Read complete after 1s [id=vpc-091e289e155590a6f]

Terraform used the selected providers to generate the following execution plan. Resource actions
are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # awscc_ec2_vpc_encryption_control.example will be created
  + resource "awscc_ec2_vpc_encryption_control" "example" {
      + egress_only_internet_gateway_exclusion_input = "disable"
      + elastic_file_system_exclusion_input          = "disable"
      + id                                           = (known after apply)
      + internet_gateway_exclusion_input             = "disable"
      + lambda_exclusion_input                       = "disable"
      + mode                                         = "enforce"
      + nat_gateway_exclusion_input                  = "disable"
      + resource_exclusions                          = (known after apply)
      + state                                        = (known after apply)
      + state_message                                = (known after apply)
      + tags                                         = [
          + {
              + key   = "Name"
              + value = "vpc-encryption-control-example"
            },
        ]
      + virtual_private_gateway_exclusion_input      = "disable"
      + vpc_encryption_control_id                    = (known after apply)
      + vpc_id                                       = "vpc-091e289e155590a6f"
      + vpc_lattice_exclusion_input                  = "disable"
      + vpc_peering_exclusion_input                  = "disable"
    }

Plan: 1 to add, 0 to change, 0 to destroy.
awscc_ec2_vpc_encryption_control.example: Creating...
awscc_ec2_vpc_encryption_control.example: Still creating... [00m10s elapsed]
awscc_ec2_vpc_encryption_control.example: Still creating... [00m20s elapsed]
awscc_ec2_vpc_encryption_control.example: Still creating... [00m30s elapsed]
awscc_ec2_vpc_encryption_control.example: Still creating... [00m40s elapsed]
awscc_ec2_vpc_encryption_control.example: Still creating... [00m50s elapsed]
awscc_ec2_vpc_encryption_control.example: Creation complete after 54s [id=vpcec-01d293b400b0fbcf8]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

State file shows the control mode is set to enforce which is not accurate.

      "mode": "managed",
      "type": "awscc_ec2_vpc_encryption_control",
      "name": "example",
      "provider": "provider[\"registry.terraform.io/hashicorp/awscc\"]",
      "instances": [
        {
          "schema_version": 1,
          "attributes": {
            "egress_only_internet_gateway_exclusion_input": "disable",
            "elastic_file_system_exclusion_input": "disable",
            "id": "vpcec-01d293b400b0fbcf8",
            "internet_gateway_exclusion_input": "disable",
            "lambda_exclusion_input": "disable",
            "mode": "enforce",
            "nat_gateway_exclusion_input": "disable",
            "state": "available",
            "state_message": "succeeded",

Calling the CloudControl API ( CC API), to see the resource properties it shows monitor.

aws cloudcontrol get-resource --type-name AWS::EC2::VPCEncryptionControl --identifier vpcec-09b701632c8c65e90 | jq -r '.ResourceDescription.Properties | fromjson | .Mode'
monitor

On a subsequent apply, you can see that the provider reconciles the plan with the current status in the update in-place operation. And this operation also takes a little over 15 minutes.

tfy
data.aws_vpc.existing: Reading...
data.aws_vpc.existing: Read complete after 1s [id=vpc-091e289e155590a6f]
awscc_ec2_vpc_encryption_control.example: Refreshing state... [id=vpcec-01d293b400b0fbcf8]

Terraform used the selected providers to generate the following execution plan. Resource actions
are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # awscc_ec2_vpc_encryption_control.example will be updated in-place
  ~ resource "awscc_ec2_vpc_encryption_control" "example" {
        id                                           = "vpcec-01d293b400b0fbcf8"
      ~ mode                                         = "monitor" -> "enforce"
        tags                                         = [
            {
                key   = "Name"
                value = "vpc-encryption-control-example"
            },
        ]
        # (13 unchanged attributes hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.
awscc_ec2_vpc_encryption_control.example: Modifying... [id=vpcec-01d293b400b0fbcf8]
awscc_ec2_vpc_encryption_control.example: Still modifying... [id=vpcec-01d293b400b0fbcf8, 00m10s elapsed]
awscc_ec2_vpc_encryption_control.example: Still modifying... [id=vpcec-01d293b400b0fbcf8, 00m20s elapsed]
...
awscc_ec2_vpc_encryption_control.example: Still modifying... [id=vpcec-01d293b400b0fbcf8, 15m00s elapsed]
awscc_ec2_vpc_encryption_control.example: Still modifying... [id=vpcec-01d293b400b0fbcf8, 15m10s elapsed]
awscc_ec2_vpc_encryption_control.example: Modifications complete after 15m11s [id=vpcec-01d293b400b0fbcf8]

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

Between the 15 minutes, the AWS console shows

After the terraform apply completes, you would notice that the encryption control is set to

Third time is the charm; we have the desired No changes.

tfy
data.aws_vpc.existing: Reading...
data.aws_vpc.existing: Read complete after 1s [id=vpc-091e289e155590a6f]
awscc_ec2_vpc_encryption_control.example: Refreshing state... [id=vpcec-01d293b400b0fbcf8]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes
are needed.

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

So what now ?

From the looks of it, the CloudControl API or CloudFormation process sets up the control mode as monitor initially for an existing vpc irrespective of what we set in the configuration. AWS provider seems to catch the expectation and goes through phases of modifying the state to the desired enforce status.

Note: For the time being, I have opened an issue in the AWSCC provider for the state mismatch.