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/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..6acfc435275a 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, 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"), + ), + }, + { + 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", names.AttrARN), + ), + }, + { + Config: testAccIntegrationConfig_vpcLinkV2ALBUpdated(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckIntegrationExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttrPair(resourceName, "integration_target", "aws_lb.test2", names.AttrARN), + ), + }, + }, + }) +} + +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", names.AttrARN), + ), + }, + { + 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`.