From 318d0695f719e1d63cca7fc4ba8da2cfa210858d Mon Sep 17 00:00:00 2001 From: Hugo JOUBERT Date: Fri, 28 Nov 2025 13:03:39 +0100 Subject: [PATCH 1/4] feat(vpclink): added vpclinkV2 integration for apigateway REST --- CHANGELOG.md | 1 + internal/service/apigateway/integration.go | 18 + .../service/apigateway/integration_test.go | 366 ++++++++++++++++++ .../r/api_gateway_integration.html.markdown | 63 +++ 4 files changed, 448 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b33b92dffccf..67fef77f0e11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ ENHANCEMENTS: * resource/aws_lambda_function: Add `tenancy_config` argument ([#45170](https://github.com/hashicorp/terraform-provider-aws/issues/45170)) * resource/aws_lambda_invocation: Add `tenant_id` argument ([#45170](https://github.com/hashicorp/terraform-provider-aws/issues/45170)) * resource/aws_vpn_connection: Add `vpn_concentrator_id` argument to support Site-to-Site VPN Concentrator ([#45175](https://github.com/hashicorp/terraform-provider-aws/issues/45175)) +* resource/aws_api_gateway_integration: Add `integration_target` argument to support VPC Link V2 with Application Load Balancer integrations ([#45230](https://github.com/hashicorp/terraform-provider-aws/issues/45230)) ## 6.22.1 (November 21, 2025) diff --git a/internal/service/apigateway/integration.go b/internal/service/apigateway/integration.go index e44885ccf890..63300024e10c 100644 --- a/internal/service/apigateway/integration.go +++ b/internal/service/apigateway/integration.go @@ -25,6 +25,7 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" "github.com/hashicorp/terraform-provider-aws/internal/flex" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/internal/verify" "github.com/hashicorp/terraform-provider-aws/names" ) @@ -68,6 +69,11 @@ func resourceIntegration() *schema.Resource { Type: schema.TypeString, Optional: true, }, + "integration_target": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: verify.ValidARN, + }, "connection_type": { Type: schema.TypeString, Optional: true, @@ -220,6 +226,9 @@ func resourceIntegrationCreate(ctx context.Context, d *schema.ResourceData, meta if v, ok := d.GetOk("tls_config"); ok && len(v.([]any)) > 0 { input.TlsConfig = expandTLSConfig(v.([]any)) } + if v, ok := d.GetOk("integration_target"); ok { + input.IntegrationTarget = aws.String(v.(string)) + } if v, ok := d.GetOk(names.AttrURI); ok { input.Uri = aws.String(v.(string)) @@ -261,6 +270,7 @@ func resourceIntegrationRead(ctx context.Context, d *schema.ResourceData, meta a d.Set("content_handling", integration.ContentHandling) d.Set("credentials", integration.Credentials) d.Set("integration_http_method", integration.HttpMethod) + d.Set("integration_target", integration.IntegrationTarget) d.Set("passthrough_behavior", integration.PassthroughBehavior) d.Set("request_parameters", integration.RequestParameters) // We need to explicitly convert key = nil values into key = "", which aws.ToStringMap() removes @@ -462,6 +472,14 @@ func resourceIntegrationUpdate(ctx context.Context, d *schema.ResourceData, meta } } + if d.HasChange("integration_target") { + operations = append(operations, types.PatchOperation{ + Op: types.OpReplace, + Path: aws.String("/integrationTarget"), + Value: aws.String(d.Get("integration_target").(string)), + }) + } + // Updating, Stage 1: Everything except cache key parameters // Updating is handled in two stages because of the challenges of keeping AWS and state in sync in diff --git a/internal/service/apigateway/integration_test.go b/internal/service/apigateway/integration_test.go index 4fa68447567c..f8a6ca049914 100644 --- a/internal/service/apigateway/integration_test.go +++ b/internal/service/apigateway/integration_test.go @@ -664,6 +664,98 @@ func testAccIntegrationImportStateIdFunc(resourceName string) resource.ImportSta } } +func TestAccAPIGatewayIntegration_vpcLinkV2WithALB(t *testing.T) { + ctx := acctest.Context(t) + var conf apigateway.GetIntegrationOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_api_gateway_integration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.APIGatewayServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckIntegrationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccIntegrationConfig_vpcLinkV2ALB(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckIntegrationExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "connection_type", "VPC_LINK"), + resource.TestCheckResourceAttrPair(resourceName, "connection_id", "aws_apigatewayv2_vpc_link.test", "id"), + resource.TestCheckResourceAttrPair(resourceName, "integration_target", "aws_lb.test", "arn"), + resource.TestCheckResourceAttr(resourceName, "type", "HTTP_PROXY"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccIntegrationImportStateIdFunc(resourceName), + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAPIGatewayIntegration_vpcLinkV2Update(t *testing.T) { + ctx := acctest.Context(t) + var conf apigateway.GetIntegrationOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_api_gateway_integration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.APIGatewayServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckIntegrationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccIntegrationConfig_vpcLinkV2ALB(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckIntegrationExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttrPair(resourceName, "integration_target", "aws_lb.test", "arn"), + ), + }, + { + Config: testAccIntegrationConfig_vpcLinkV2ALBUpdated(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckIntegrationExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttrPair(resourceName, "integration_target", "aws_lb.test2", "arn"), + ), + }, + }, + }) +} + +func TestAccAPIGatewayIntegration_integrationTargetRemoval(t *testing.T) { + ctx := acctest.Context(t) + var conf apigateway.GetIntegrationOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_api_gateway_integration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.APIGatewayServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckIntegrationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccIntegrationConfig_vpcLinkV2ALB(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckIntegrationExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttrPair(resourceName, "integration_target", "aws_lb.test", "arn"), + ), + }, + { + Config: testAccIntegrationConfig_vpcLinkV2NoTarget(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckIntegrationExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "integration_target", ""), + ), + }, + }, + }) +} + func testAccIntegrationConfig_basic(rName string) string { return fmt.Sprintf(` resource "aws_api_gateway_rest_api" "test" { @@ -1269,3 +1361,277 @@ resource "aws_api_gateway_integration" "test" { } `, rName, insecureSkipVerification) } + +func testAccIntegrationConfig_vpcLinkV2ALB(rName string) string { + return acctest.ConfigCompose( + acctest.ConfigVPCWithSubnets(rName, 2), + fmt.Sprintf(` +resource "aws_security_group" "test" { + name = %[1]q + vpc_id = aws_vpc.test.id + + ingress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = [aws_vpc.test.cidr_block] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "aws_apigatewayv2_vpc_link" "test" { + name = %[1]q + security_group_ids = [aws_security_group.test.id] + subnet_ids = aws_subnet.test[*].id +} + +resource "aws_lb" "test" { + name = %[1]q + internal = true + load_balancer_type = "application" + security_groups = [aws_security_group.test.id] + subnets = aws_subnet.test[*].id +} + +resource "aws_lb_listener" "test" { + load_balancer_arn = aws_lb.test.arn + port = "80" + protocol = "HTTP" + + default_action { + type = "fixed-response" + fixed_response { + content_type = "text/plain" + message_body = "OK" + status_code = "200" + } + } +} + +resource "aws_api_gateway_rest_api" "test" { + name = %[1]q +} + +resource "aws_api_gateway_resource" "test" { + rest_api_id = aws_api_gateway_rest_api.test.id + parent_id = aws_api_gateway_rest_api.test.root_resource_id + path_part = "test" +} + +resource "aws_api_gateway_method" "test" { + rest_api_id = aws_api_gateway_rest_api.test.id + resource_id = aws_api_gateway_resource.test.id + http_method = "GET" + authorization = "NONE" +} + +resource "aws_api_gateway_integration" "test" { + rest_api_id = aws_api_gateway_rest_api.test.id + resource_id = aws_api_gateway_resource.test.id + http_method = aws_api_gateway_method.test.http_method + integration_http_method = "GET" + type = "HTTP_PROXY" + connection_type = "VPC_LINK" + connection_id = aws_apigatewayv2_vpc_link.test.id + integration_target = aws_lb.test.arn + uri = "http://example.com" +} +`, rName)) +} + +func testAccIntegrationConfig_vpcLinkV2ALBUpdated(rName string) string { + // Use a shorter name for test2 to avoid 32-character limit + rName2 := fmt.Sprintf("%.27s-alt", rName) + return acctest.ConfigCompose( + acctest.ConfigVPCWithSubnets(rName, 2), + fmt.Sprintf(` +resource "aws_security_group" "test" { + name = %[1]q + vpc_id = aws_vpc.test.id + + ingress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = [aws_vpc.test.cidr_block] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "aws_apigatewayv2_vpc_link" "test" { + name = %[1]q + security_group_ids = [aws_security_group.test.id] + subnet_ids = aws_subnet.test[*].id +} + +resource "aws_lb" "test" { + name = %[1]q + internal = true + load_balancer_type = "application" + security_groups = [aws_security_group.test.id] + subnets = aws_subnet.test[*].id +} + +resource "aws_lb_listener" "test" { + load_balancer_arn = aws_lb.test.arn + port = "80" + protocol = "HTTP" + + default_action { + type = "fixed-response" + fixed_response { + content_type = "text/plain" + message_body = "OK" + status_code = "200" + } + } +} + +resource "aws_lb" "test2" { + name = %[2]q + internal = true + load_balancer_type = "application" + security_groups = [aws_security_group.test.id] + subnets = aws_subnet.test[*].id +} + +resource "aws_lb_listener" "test2" { + load_balancer_arn = aws_lb.test2.arn + port = "80" + protocol = "HTTP" + + default_action { + type = "fixed-response" + fixed_response { + content_type = "text/plain" + message_body = "OK" + status_code = "200" + } + } +} + +resource "aws_api_gateway_rest_api" "test" { + name = %[1]q +} + +resource "aws_api_gateway_resource" "test" { + rest_api_id = aws_api_gateway_rest_api.test.id + parent_id = aws_api_gateway_rest_api.test.root_resource_id + path_part = "test" +} + +resource "aws_api_gateway_method" "test" { + rest_api_id = aws_api_gateway_rest_api.test.id + resource_id = aws_api_gateway_resource.test.id + http_method = "GET" + authorization = "NONE" +} + +resource "aws_api_gateway_integration" "test" { + rest_api_id = aws_api_gateway_rest_api.test.id + resource_id = aws_api_gateway_resource.test.id + http_method = aws_api_gateway_method.test.http_method + integration_http_method = "GET" + type = "HTTP_PROXY" + connection_type = "VPC_LINK" + connection_id = aws_apigatewayv2_vpc_link.test.id + integration_target = aws_lb.test2.arn + uri = "http://example.com" +} +`, rName, rName2)) +} + +func testAccIntegrationConfig_vpcLinkV2NoTarget(rName string) string { + return acctest.ConfigCompose( + acctest.ConfigVPCWithSubnets(rName, 2), + fmt.Sprintf(` +resource "aws_security_group" "test" { + name = %[1]q + vpc_id = aws_vpc.test.id + + ingress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = [aws_vpc.test.cidr_block] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "aws_apigatewayv2_vpc_link" "test" { + name = %[1]q + security_group_ids = [aws_security_group.test.id] + subnet_ids = aws_subnet.test[*].id +} + +resource "aws_lb" "test" { + name = %[1]q + internal = true + load_balancer_type = "application" + security_groups = [aws_security_group.test.id] + subnets = aws_subnet.test[*].id +} + +resource "aws_lb_listener" "test" { + load_balancer_arn = aws_lb.test.arn + port = "80" + protocol = "HTTP" + + default_action { + type = "fixed-response" + fixed_response { + content_type = "text/plain" + message_body = "OK" + status_code = "200" + } + } +} + + +resource "aws_api_gateway_rest_api" "test" { + name = %[1]q +} + +resource "aws_api_gateway_resource" "test" { + rest_api_id = aws_api_gateway_rest_api.test.id + parent_id = aws_api_gateway_rest_api.test.root_resource_id + path_part = "test" +} + +resource "aws_api_gateway_method" "test" { + rest_api_id = aws_api_gateway_rest_api.test.id + resource_id = aws_api_gateway_resource.test.id + http_method = "GET" + authorization = "NONE" +} + +resource "aws_api_gateway_integration" "test" { + rest_api_id = aws_api_gateway_rest_api.test.id + resource_id = aws_api_gateway_resource.test.id + http_method = aws_api_gateway_method.test.http_method + integration_http_method = "GET" + type = "HTTP_PROXY" + connection_type = "VPC_LINK" + connection_id = aws_apigatewayv2_vpc_link.test.id + uri = "http://example.com" +} +`, rName)) +} diff --git a/website/docs/r/api_gateway_integration.html.markdown b/website/docs/r/api_gateway_integration.html.markdown index e9a1a165a352..d36f37eb2656 100644 --- a/website/docs/r/api_gateway_integration.html.markdown +++ b/website/docs/r/api_gateway_integration.html.markdown @@ -196,6 +196,68 @@ resource "aws_api_gateway_integration" "test" { } ``` +## VPC Link V2 with Application Load Balancer + +```terraform +resource "aws_apigatewayv2_vpc_link" "example" { + name = "example" + security_group_ids = [aws_security_group.example.id] + subnet_ids = aws_subnet.example[*].id +} + +resource "aws_lb" "example" { + name = "example-alb" + internal = true + load_balancer_type = "application" + security_groups = [aws_security_group.example.id] + subnets = aws_subnet.example[*].id +} + +resource "aws_lb_listener" "example" { + load_balancer_arn = aws_lb.example.arn + port = "80" + protocol = "HTTP" + + default_action { + type = "fixed-response" + fixed_response { + content_type = "text/plain" + message_body = "OK" + status_code = "200" + } + } +} + +resource "aws_api_gateway_rest_api" "example" { + name = "example" +} + +resource "aws_api_gateway_resource" "example" { + rest_api_id = aws_api_gateway_rest_api.example.id + parent_id = aws_api_gateway_rest_api.example.root_resource_id + path_part = "example" +} + +resource "aws_api_gateway_method" "example" { + rest_api_id = aws_api_gateway_rest_api.example.id + resource_id = aws_api_gateway_resource.example.id + http_method = "GET" + authorization = "NONE" +} + +resource "aws_api_gateway_integration" "example" { + rest_api_id = aws_api_gateway_rest_api.example.id + resource_id = aws_api_gateway_resource.example.id + http_method = aws_api_gateway_method.example.http_method + integration_http_method = "GET" + type = "HTTP_PROXY" + connection_type = "VPC_LINK" + connection_id = aws_apigatewayv2_vpc_link.example.id + integration_target = aws_lb.example.arn + uri = "http://example.com" +} +``` + ## Argument Reference This resource supports the following arguments: @@ -205,6 +267,7 @@ This resource supports the following arguments: * `resource_id` - (Required) API resource ID. * `http_method` - (Required) HTTP method (`GET`, `POST`, `PUT`, `DELETE`, `HEAD`, `OPTION`, `ANY`) when calling the associated resource. +* `integration_target` - (Optional) The ALB or NLB ARN to send the request to. Used for private integrations with VPC Link V2. When using VPC Link V2, this parameter specifies the load balancer ARN, while `uri` is used to set the Host header. * `integration_http_method` - (Optional) Integration HTTP method (`GET`, `POST`, `PUT`, `DELETE`, `HEAD`, `OPTIONs`, `ANY`, `PATCH`) specifying how API Gateway will interact with the back end. **Required** if `type` is `AWS`, `AWS_PROXY`, `HTTP` or `HTTP_PROXY`. From 3564f6f52c130022c7c20b111fbdf7e36e53d5a7 Mon Sep 17 00:00:00 2001 From: Hugo JOUBERT Date: Fri, 28 Nov 2025 14:29:58 +0100 Subject: [PATCH 2/4] changelog modified --- .changelog/45311.txt | 3 +++ CHANGELOG.md | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 .changelog/45311.txt diff --git a/.changelog/45311.txt b/.changelog/45311.txt new file mode 100644 index 000000000000..c234ff9bc495 --- /dev/null +++ b/.changelog/45311.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_api_gateway_integration: Add VPC Link V2 to ApiGateway private backend +``` \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 67fef77f0e11..b33b92dffccf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,6 @@ ENHANCEMENTS: * resource/aws_lambda_function: Add `tenancy_config` argument ([#45170](https://github.com/hashicorp/terraform-provider-aws/issues/45170)) * resource/aws_lambda_invocation: Add `tenant_id` argument ([#45170](https://github.com/hashicorp/terraform-provider-aws/issues/45170)) * resource/aws_vpn_connection: Add `vpn_concentrator_id` argument to support Site-to-Site VPN Concentrator ([#45175](https://github.com/hashicorp/terraform-provider-aws/issues/45175)) -* resource/aws_api_gateway_integration: Add `integration_target` argument to support VPC Link V2 with Application Load Balancer integrations ([#45230](https://github.com/hashicorp/terraform-provider-aws/issues/45230)) ## 6.22.1 (November 21, 2025) From 92a4f4d34badd587d748ed2b7656215416992c96 Mon Sep 17 00:00:00 2001 From: Hugo JOUBERT Date: Fri, 28 Nov 2025 14:49:21 +0100 Subject: [PATCH 3/4] fixed code quality --- internal/service/apigateway/integration_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/service/apigateway/integration_test.go b/internal/service/apigateway/integration_test.go index f8a6ca049914..6c4fac8c1f8b 100644 --- a/internal/service/apigateway/integration_test.go +++ b/internal/service/apigateway/integration_test.go @@ -681,9 +681,9 @@ func TestAccAPIGatewayIntegration_vpcLinkV2WithALB(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckIntegrationExists(ctx, resourceName, &conf), resource.TestCheckResourceAttr(resourceName, "connection_type", "VPC_LINK"), - resource.TestCheckResourceAttrPair(resourceName, "connection_id", "aws_apigatewayv2_vpc_link.test", "id"), - resource.TestCheckResourceAttrPair(resourceName, "integration_target", "aws_lb.test", "arn"), - resource.TestCheckResourceAttr(resourceName, "type", "HTTP_PROXY"), + resource.TestCheckResourceAttrPair(resourceName, names.AttrConnectionID, "aws_apigatewayv2_vpc_link.test", names.AttrID), + resource.TestCheckResourceAttrPair(resourceName, "integration_target", "aws_lb.test", names.AttrARN), + resource.TestCheckResourceAttr(resourceName, names.AttrType, "HTTP_PROXY"), ), }, { @@ -712,14 +712,14 @@ func TestAccAPIGatewayIntegration_vpcLinkV2Update(t *testing.T) { Config: testAccIntegrationConfig_vpcLinkV2ALB(rName), Check: resource.ComposeTestCheckFunc( testAccCheckIntegrationExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttrPair(resourceName, "integration_target", "aws_lb.test", "arn"), + resource.TestCheckResourceAttrPair(resourceName, "integration_target", "aws_lb.test", names.AttrARN), ), }, { Config: testAccIntegrationConfig_vpcLinkV2ALBUpdated(rName), Check: resource.ComposeTestCheckFunc( testAccCheckIntegrationExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttrPair(resourceName, "integration_target", "aws_lb.test2", "arn"), + resource.TestCheckResourceAttrPair(resourceName, "integration_target", "aws_lb.test2", names.AttrARN), ), }, }, From 7197d4c39f19a69d46a5962203997aad11189dcb Mon Sep 17 00:00:00 2001 From: Hugo JOUBERT Date: Fri, 28 Nov 2025 15:03:36 +0100 Subject: [PATCH 4/4] code quality fix --- internal/service/apigateway/integration_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/apigateway/integration_test.go b/internal/service/apigateway/integration_test.go index 6c4fac8c1f8b..6acfc435275a 100644 --- a/internal/service/apigateway/integration_test.go +++ b/internal/service/apigateway/integration_test.go @@ -742,7 +742,7 @@ func TestAccAPIGatewayIntegration_integrationTargetRemoval(t *testing.T) { Config: testAccIntegrationConfig_vpcLinkV2ALB(rName), Check: resource.ComposeTestCheckFunc( testAccCheckIntegrationExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttrPair(resourceName, "integration_target", "aws_lb.test", "arn"), + resource.TestCheckResourceAttrPair(resourceName, "integration_target", "aws_lb.test", names.AttrARN), ), }, {