diff --git a/.changelog/31873.txt b/.changelog/31873.txt new file mode 100644 index 00000000000..d6417821ee8 --- /dev/null +++ b/.changelog/31873.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_vpc_endpoint: Add `private_dns_only_for_inbound_resolver_endpoint` attribute to the `dns_options` configuration block +``` \ No newline at end of file diff --git a/internal/service/ec2/vpc_endpoint.go b/internal/service/ec2/vpc_endpoint.go index a40bb24de9c..a8abe466bb4 100644 --- a/internal/service/ec2/vpc_endpoint.go +++ b/internal/service/ec2/vpc_endpoint.go @@ -87,6 +87,10 @@ func ResourceVPCEndpoint() *schema.Resource { Optional: true, ValidateFunc: validation.StringInSlice(ec2.DnsRecordIpType_Values(), false), }, + "private_dns_only_for_inbound_resolver_endpoint": { + Type: schema.TypeBool, + Optional: true, + }, }, }, }, @@ -209,7 +213,7 @@ func resourceVPCEndpointCreate(ctx context.Context, d *schema.ResourceData, meta policy, err := structure.NormalizeJsonString(v) if err != nil { - return sdkdiag.AppendErrorf(diags, "policy contains invalid JSON: %s", err) + return sdkdiag.AppendFromErr(diags, err) } input.PolicyDocument = aws.String(policy) @@ -244,12 +248,12 @@ func resourceVPCEndpointCreate(ctx context.Context, d *schema.ResourceData, meta if d.Get("auto_accept").(bool) && aws.StringValue(vpce.State) == vpcEndpointStatePendingAcceptance { if err := vpcEndpointAccept(ctx, conn, d.Id(), aws.StringValue(vpce.ServiceName), d.Timeout(schema.TimeoutCreate)); err != nil { - return sdkdiag.AppendErrorf(diags, "creating EC2 VPC Endpoint (%s): %s", serviceName, err) + return sdkdiag.AppendFromErr(diags, err) } } if _, err = WaitVPCEndpointAvailable(ctx, conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { - return sdkdiag.AppendErrorf(diags, "creating EC2 VPC Endpoint (%s): waiting for completion: %s", serviceName, err) + return sdkdiag.AppendErrorf(diags, "waiting for EC2 VPC Endpoint (%s) create: %s", serviceName, err) } // For partitions not supporting tag-on-create, attempt tag after create. @@ -293,7 +297,6 @@ func resourceVPCEndpointRead(ctx context.Context, d *schema.ResourceData, meta i Resource: fmt.Sprintf("vpc-endpoint/%s", d.Id()), }.String() serviceName := aws.StringValue(vpce.ServiceName) - d.Set("arn", arn) if err := d.Set("dns_entry", flattenDNSEntries(vpce.DnsEntries)); err != nil { return sdkdiag.AppendErrorf(diags, "setting dns_entry: %s", err) @@ -337,13 +340,13 @@ func resourceVPCEndpointRead(ctx context.Context, d *schema.ResourceData, meta i policyToSet, err := verify.SecondJSONUnlessEquivalent(d.Get("policy").(string), aws.StringValue(vpce.PolicyDocument)) if err != nil { - return sdkdiag.AppendErrorf(diags, "while setting policy (%s), encountered: %s", policyToSet, err) + return sdkdiag.AppendFromErr(diags, err) } policyToSet, err = structure.NormalizeJsonString(policyToSet) if err != nil { - return sdkdiag.AppendErrorf(diags, "policy (%s) is invalid JSON: %s", policyToSet, err) + return sdkdiag.AppendFromErr(diags, err) } d.Set("policy", policyToSet) @@ -359,18 +362,25 @@ func resourceVPCEndpointUpdate(ctx context.Context, d *schema.ResourceData, meta if d.HasChange("auto_accept") && d.Get("auto_accept").(bool) && d.Get("state").(string) == vpcEndpointStatePendingAcceptance { if err := vpcEndpointAccept(ctx, conn, d.Id(), d.Get("service_name").(string), d.Timeout(schema.TimeoutUpdate)); err != nil { - return sdkdiag.AppendErrorf(diags, "updating EC2 VPC Endpoint (%s): %s", d.Get("service_name").(string), err) + return sdkdiag.AppendFromErr(diags, err) } } if d.HasChanges("dns_options", "ip_address_type", "policy", "private_dns_enabled", "security_group_ids", "route_table_ids", "subnet_ids") { + privateDNSEnabled := d.Get("private_dns_enabled").(bool) input := &ec2.ModifyVpcEndpointInput{ VpcEndpointId: aws.String(d.Id()), } if d.HasChange("dns_options") { if v, ok := d.GetOk("dns_options"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { - input.DnsOptions = expandDNSOptionsSpecification(v.([]interface{})[0].(map[string]interface{})) + tfMap := v.([]interface{})[0].(map[string]interface{}) + apiObject := expandDNSOptionsSpecification(tfMap) + if privateDNSEnabled { + // Always send PrivateDnsOnlyForInboundResolverEndpoint on update. + apiObject.PrivateDnsOnlyForInboundResolverEndpoint = aws.Bool(tfMap["private_dns_only_for_inbound_resolver_endpoint"].(bool)) + } + input.DnsOptions = apiObject } } @@ -379,7 +389,7 @@ func resourceVPCEndpointUpdate(ctx context.Context, d *schema.ResourceData, meta } if d.HasChange("private_dns_enabled") { - input.PrivateDnsEnabled = aws.Bool(d.Get("private_dns_enabled").(bool)) + input.PrivateDnsEnabled = aws.Bool(privateDNSEnabled) } input.AddRouteTableIds, input.RemoveRouteTableIds = flattenAddAndRemoveStringLists(d, "route_table_ids") @@ -393,7 +403,7 @@ func resourceVPCEndpointUpdate(ctx context.Context, d *schema.ResourceData, meta policy, err := structure.NormalizeJsonString(d.Get("policy")) if err != nil { - return sdkdiag.AppendErrorf(diags, "policy contains invalid JSON: %s", err) + return sdkdiag.AppendFromErr(diags, err) } if policy == "" { @@ -482,6 +492,10 @@ func expandDNSOptionsSpecification(tfMap map[string]interface{}) *ec2.DnsOptions apiObject.DnsRecordIpType = aws.String(v) } + if v, ok := tfMap["private_dns_only_for_inbound_resolver_endpoint"].(bool); ok && v { + apiObject.PrivateDnsOnlyForInboundResolverEndpoint = aws.Bool(v) + } + return apiObject } @@ -532,6 +546,10 @@ func flattenDNSOptions(apiObject *ec2.DnsOptions) map[string]interface{} { tfMap["dns_record_ip_type"] = aws.StringValue(v) } + if v := apiObject.PrivateDnsOnlyForInboundResolverEndpoint; v != nil { + tfMap["private_dns_only_for_inbound_resolver_endpoint"] = aws.BoolValue(v) + } + return tfMap } diff --git a/internal/service/ec2/vpc_endpoint_test.go b/internal/service/ec2/vpc_endpoint_test.go index 47e8552a6ee..b29ddb03e95 100644 --- a/internal/service/ec2/vpc_endpoint_test.go +++ b/internal/service/ec2/vpc_endpoint_test.go @@ -80,6 +80,7 @@ func TestAccVPCEndpoint_interfaceBasic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "dns_entry.#", "0"), resource.TestCheckResourceAttr(resourceName, "dns_options.#", "1"), resource.TestCheckResourceAttr(resourceName, "dns_options.0.dns_record_ip_type", "ipv4"), + resource.TestCheckResourceAttr(resourceName, "dns_options.0.private_dns_only_for_inbound_resolver_endpoint", "false"), resource.TestCheckResourceAttr(resourceName, "ip_address_type", "ipv4"), resource.TestCheckResourceAttr(resourceName, "network_interface_ids.#", "0"), acctest.CheckResourceAttrAccountID(resourceName, "owner_id"), @@ -103,6 +104,51 @@ func TestAccVPCEndpoint_interfaceBasic(t *testing.T) { }) } +func TestAccVPCEndpoint_interfacePrivateDNS(t *testing.T) { + ctx := acctest.Context(t) + var endpoint ec2.VpcEndpoint + resourceName := "aws_vpc_endpoint.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckVPCEndpointDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccVPCEndpointConfig_interfacePrivateDNS(rName, true), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckVPCEndpointExists(ctx, resourceName, &endpoint), + acctest.CheckResourceAttrGreaterThanValue(resourceName, "cidr_blocks.#", 0), + resource.TestCheckResourceAttr(resourceName, "dns_entry.#", "0"), + resource.TestCheckResourceAttr(resourceName, "dns_options.#", "1"), + resource.TestCheckResourceAttr(resourceName, "dns_options.0.dns_record_ip_type", "ipv4"), + resource.TestCheckResourceAttr(resourceName, "dns_options.0.private_dns_only_for_inbound_resolver_endpoint", "true"), + resource.TestCheckResourceAttr(resourceName, "private_dns_enabled", "true"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccVPCEndpointConfig_interfacePrivateDNS(rName, false), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckVPCEndpointExists(ctx, resourceName, &endpoint), + acctest.CheckResourceAttrGreaterThanValue(resourceName, "cidr_blocks.#", 0), + resource.TestCheckResourceAttr(resourceName, "dns_entry.#", "0"), + resource.TestCheckResourceAttr(resourceName, "dns_options.#", "1"), + resource.TestCheckResourceAttr(resourceName, "dns_options.0.dns_record_ip_type", "ipv4"), + resource.TestCheckResourceAttr(resourceName, "dns_options.0.private_dns_only_for_inbound_resolver_endpoint", "false"), + resource.TestCheckResourceAttr(resourceName, "private_dns_enabled", "true"), + ), + }, + }, + }) +} + func TestAccVPCEndpoint_disappears(t *testing.T) { ctx := acctest.Context(t) var endpoint ec2.VpcEndpoint @@ -323,6 +369,7 @@ func TestAccVPCEndpoint_ipAddressType(t *testing.T) { testAccCheckVPCEndpointExists(ctx, resourceName, &endpoint), resource.TestCheckResourceAttr(resourceName, "dns_options.#", "1"), resource.TestCheckResourceAttr(resourceName, "dns_options.0.dns_record_ip_type", "ipv4"), + resource.TestCheckResourceAttr(resourceName, "dns_options.0.private_dns_only_for_inbound_resolver_endpoint", "false"), resource.TestCheckResourceAttr(resourceName, "ip_address_type", "ipv4"), ), }, @@ -338,6 +385,7 @@ func TestAccVPCEndpoint_ipAddressType(t *testing.T) { testAccCheckVPCEndpointExists(ctx, resourceName, &endpoint), resource.TestCheckResourceAttr(resourceName, "dns_options.#", "1"), resource.TestCheckResourceAttr(resourceName, "dns_options.0.dns_record_ip_type", "dualstack"), + resource.TestCheckResourceAttr(resourceName, "dns_options.0.private_dns_only_for_inbound_resolver_endpoint", "false"), resource.TestCheckResourceAttr(resourceName, "ip_address_type", "dualstack"), ), }, @@ -694,6 +742,51 @@ resource "aws_vpc_endpoint" "test" { `, rName) } +func testAccVPCEndpointConfig_interfacePrivateDNS(rName string, privateDNSOnlyForInboundResolverEndpoint bool) string { + return fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" + enable_dns_support = true + enable_dns_hostnames = true + + tags = { + Name = %[1]q + } +} + +data "aws_region" "current" {} + +resource "aws_vpc_endpoint" "gateway" { + vpc_id = aws_vpc.test.id + service_name = "com.amazonaws.${data.aws_region.current.name}.s3" + + tags = { + Name = %[1]q + } +} + +resource "aws_vpc_endpoint" "test" { + vpc_id = aws_vpc.test.id + service_name = "com.amazonaws.${data.aws_region.current.name}.s3" + private_dns_enabled = true + vpc_endpoint_type = "Interface" + ip_address_type = "ipv4" + + dns_options { + dns_record_ip_type = "ipv4" + private_dns_only_for_inbound_resolver_endpoint = %[2]t + } + + tags = { + Name = %[1]q + } + + # To set PrivateDnsOnlyForInboundResolverEndpoint to true, the VPC vpc-abcd1234 must have a Gateway endpoint for the service. + depends_on = [aws_vpc_endpoint.gateway] +} +`, rName, privateDNSOnlyForInboundResolverEndpoint) +} + func testAccVPCEndpointConfig_ipAddressType(rName, addressType string) string { return acctest.ConfigCompose(testAccVPCEndpointServiceConfig_baseSupportedIPAddressTypes(rName), fmt.Sprintf(` resource "aws_vpc_endpoint_service" "test" { @@ -717,6 +810,10 @@ resource "aws_vpc_endpoint" "test" { dns_options { dns_record_ip_type = %[2]q } + + tags = { + Name = %[1]q + } } `, rName, addressType)) } diff --git a/website/docs/r/vpc_endpoint.html.markdown b/website/docs/r/vpc_endpoint.html.markdown index b2190cf16eb..4cab5713801 100644 --- a/website/docs/r/vpc_endpoint.html.markdown +++ b/website/docs/r/vpc_endpoint.html.markdown @@ -132,6 +132,7 @@ If no security groups are specified, the VPC's [default security group](https:// ### dns_options * `dns_record_ip_type` - (Optional) The DNS records created for the endpoint. Valid values are `ipv4`, `dualstack`, `service-defined`, and `ipv6`. +* `private_dns_only_for_inbound_resolver_endpoint` - (Optional) Indicates whether to enable private DNS only for inbound endpoints. This option is available only for services that support both gateway and interface endpoints. It routes traffic that originates from the VPC to the gateway endpoint and traffic that originates from on-premises to the interface endpoint. Can only be specified if `private_dns_enabled` is `true`. ## Timeouts