Search code examples
amazon-web-servicesterraformaws-api-gatewayamazon-route53

Terraformed AWS API Gateway Custom Domain Names throws 403 Forbidden


I am trying to expose all the stages of my Regional API Gateway through a regional Custom Domain.

Problem

If I curl directly my API Gateway (ie. https://xx.execute-api.eu-west-3.amazonaws.com/default/users), it works, but I get a 403 if I curl de domain name (ie. https://api.acme.com/default/users).

Configuration

My Terraform files looks like that:

data "aws_route53_zone" "acme" {
  name         = "acme.com."
}

resource "aws_api_gateway_rest_api" "backend" {
  name        = "acme-backend-api"
  description = "Backend API"
  body        = "SOMETHING"

  endpoint_configuration {
    types = ["REGIONAL"]
  }
}

resource "aws_api_gateway_deployment" "backend" {
  rest_api_id = aws_api_gateway_rest_api.backend.id
  stage_name  = "default"

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_api_gateway_domain_name" "backend" {
  domain_name              = "api.acme.com"
  regional_certificate_arn = "arn:aws:acm:xx:certificate/xx"

  endpoint_configuration {
    types = ["REGIONAL"]
  }
}

resource "aws_route53_record" "backend" {
  name    = aws_api_gateway_domain_name.backend.domain_name
  type    = "A"
  zone_id = data.aws_route53_zone.acme.id

  alias {
    evaluate_target_health = true
    name                   = aws_api_gateway_domain_name.backend.regional_domain_name
    zone_id                = aws_api_gateway_domain_name.backend.regional_zone_id
  }
}

resource "aws_api_gateway_base_path_mapping" "backend" {
  api_id      = aws_api_gateway_rest_api.backend.id
  domain_name = aws_api_gateway_domain_name.backend.domain_name
  # No stage_name: expose all stages
}

According to the Terraform api_gateway_domain_name and api_gateway_base_path_mapping examples, it should be ok.

I have also followed many howtos, and I have these elements:

  1. The certificate
  2. The A record to the API custom domain
  3. The mapping to the deployed stage (which works if you call it directly)

What do I miss/do wrong?


Solution

  • This is v2 example working for me as off today, this "aws_apigatewayv2_api_mapping" is key to avoid port 80: Connection refused or {"message":"Forbidden"} errors which I see you have but I did struggle with.

    // ACM
    
    resource "aws_acm_certificate" "cert_api" {
      domain_name       = var.api_domain
      validation_method = "DNS"
    
      tags = {
        Name = var.api_domain
      }
    }
    
    resource "aws_acm_certificate_validation" "cert_api" {
      certificate_arn = aws_acm_certificate.cert_api.arn
    }
    
    
    // API Gateway V2
    
    resource "aws_apigatewayv2_api" "lambda" {
      name          = "serverless_lambda_gw"
      protocol_type = "HTTP"
    }
    
    resource "aws_apigatewayv2_stage" "lambda" {
      api_id = aws_apigatewayv2_api.lambda.id
    
      name        = "serverless_lambda_stage"
      auto_deploy = true
    
      access_log_settings {
        destination_arn = aws_cloudwatch_log_group.api_gw.arn
    
        format = jsonencode({
          requestId               = "$context.requestId"
          sourceIp                = "$context.identity.sourceIp"
          requestTime             = "$context.requestTime"
          protocol                = "$context.protocol"
          httpMethod              = "$context.httpMethod"
          resourcePath            = "$context.resourcePath"
          routeKey                = "$context.routeKey"
          status                  = "$context.status"
          responseLength          = "$context.responseLength"
          integrationErrorMessage = "$context.integrationErrorMessage"
          }
        )
      }
    }
    
    resource "aws_apigatewayv2_integration" "testimonials" {
      api_id = aws_apigatewayv2_api.lambda.id
    
      integration_uri    = aws_lambda_function.testimonials.invoke_arn
      integration_type   = "AWS_PROXY"
      integration_method = "POST"
    }
    
    resource "aws_apigatewayv2_route" "testimonials" {
      api_id = aws_apigatewayv2_api.lambda.id
    
      route_key = "GET /testimonials"
      target    = "integrations/${aws_apigatewayv2_integration.testimonials.id}"
    }
    
    resource "aws_cloudwatch_log_group" "api_gw" {
      name = "/aws/api_gw/${aws_apigatewayv2_api.lambda.name}"
    
      retention_in_days = 30
    }
    
    resource "aws_lambda_permission" "api_gw" {
      statement_id  = "AllowExecutionFromAPIGateway"
      action        = "lambda:InvokeFunction"
      function_name = aws_lambda_function.testimonials.function_name
      principal     = "apigateway.amazonaws.com"
    
      source_arn = "${aws_apigatewayv2_api.lambda.execution_arn}/*/*"
    }
    
    resource "aws_apigatewayv2_domain_name" "api" {
      domain_name = var.api_domain
    
      domain_name_configuration {
        certificate_arn = aws_acm_certificate.cert_api.arn
        endpoint_type   = "REGIONAL"
        security_policy = "TLS_1_2"
      }
    }
    
    resource "aws_apigatewayv2_api_mapping" "api" {
      api_id      = aws_apigatewayv2_api.lambda.id
      domain_name = aws_apigatewayv2_domain_name.api.id
      stage       = aws_apigatewayv2_stage.lambda.id
    }
    
    
    
    // Route53
    
    resource "aws_route53_zone" "api" {
      name = var.api_domain
    }
    
    resource "aws_route53_record" "cert_api_validations" {
      allow_overwrite = true
      count           = length(aws_acm_certificate.cert_api.domain_validation_options)
    
      zone_id = aws_route53_zone.api.zone_id
      name    = element(aws_acm_certificate.cert_api.domain_validation_options.*.resource_record_name, count.index)
      type    = element(aws_acm_certificate.cert_api.domain_validation_options.*.resource_record_type, count.index)
      records = [element(aws_acm_certificate.cert_api.domain_validation_options.*.resource_record_value, count.index)]
      ttl     = 60
    }
    
    resource "aws_route53_record" "api-a" {
      name    = aws_apigatewayv2_domain_name.api.domain_name
      type    = "A"
      zone_id = aws_route53_zone.api.zone_id
    
      alias {
        name                   = aws_apigatewayv2_domain_name.api.domain_name_configuration[0].target_domain_name
        zone_id                = aws_apigatewayv2_domain_name.api.domain_name_configuration[0].hosted_zone_id
        evaluate_target_health = false
      }
    }