Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Terraform API Gateway with Lambda integration

I've started using terraform a few days ago, so I am a beginner in this topic. I want to create an API Gateway and a Lambda with a Layer as a custom runtime to run R script. Here are my Terraform files:

lambda.tf

resource "aws_lambda_function" "testlambda" {
  function_name = "rlambda"

  s3_bucket = "mybucket"
  s3_key    = "rlambda/v1.0.3/rlambda.zip"
  handler = "main.main"
  runtime = "provided"
  memory_size = 512
  timeout = 30
  layers = [aws_lambda_layer_version.r_layer.arn]
  role = aws_iam_role.lambda_role.arn
}

# IAM role for lambda
resource "aws_iam_role" "lambda_role" {
  name = "lambda_role"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
EOF
}

# Lambda Policy
resource "aws_lambda_permission" "apigw" {
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.testlambda.function_name
  principal     = "apigateway.amazonaws.com"

  source_arn = "${aws_api_gateway_rest_api.apigateway.execution_arn}/*/POST/dev"

  depends_on = [
    aws_api_gateway_rest_api.apigateway,
    aws_api_gateway_resource.apiresource,
  ]
  }

# Lambda Layer
resource "aws_lambda_layer_version" "r_layer" {
  layer_name = "rlayer"
  s3_bucket = "mybucket"
  s3_key = "lambdalayer/v1.0.3/rlayer.zip"
}

# Cloudwatch Logging for Lambda
resource "aws_iam_policy" "lambda_logging" {
  name = "lambda_logging"
  path = "/"
  description = "IAM policy for logging from a lambda"

  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:*:*",
      "Effect": "Allow"
    }
  ]
}
EOF
}

resource "aws_iam_role_policy_attachment" "lambda_logs" {
  role = aws_iam_role.lambda_role.name
  policy_arn = aws_iam_policy.lambda_logging.arn
}

api_gateway.tf

resource "aws_api_gateway_rest_api" "apigateway" {
  name        = "ApiGatewayTest"
  description = "Terraform Created Api Gateway"
  binary_media_types = ["multipart/form-data", "application/octet-stream"]
  }

resource "aws_api_gateway_resource" "apiresource" {
  rest_api_id = aws_api_gateway_rest_api.apigateway.id
  parent_id   = aws_api_gateway_rest_api.apigateway.root_resource_id
  path_part   = "dev"
}

# Method
resource "aws_api_gateway_method" "method" {
  rest_api_id   = aws_api_gateway_rest_api.apigateway.id
  resource_id   = aws_api_gateway_resource.apiresource.id
  http_method   = "POST"
  authorization = "NONE"
}

# Integration
resource "aws_api_gateway_integration" "integration" {
  rest_api_id = aws_api_gateway_rest_api.apigateway.id
  resource_id = aws_api_gateway_method.method.resource_id
  http_method = aws_api_gateway_method.method.http_method

  integration_http_method = "POST"
  type                    = "AWS"
  uri                     = aws_lambda_function.testlambda.invoke_arn
  passthrough_behavior = "WHEN_NO_TEMPLATES"
  request_templates = {
    "multipart/form-data" = file("api_gateway_body_mapping.template")
  }
  depends_on = [aws_api_gateway_method.method]
}

# Method Response
resource "aws_api_gateway_method_response" "methodresponse" {
  rest_api_id = aws_api_gateway_rest_api.apigateway.id
  resource_id = aws_api_gateway_resource.apiresource.id
  http_method = aws_api_gateway_method.method.http_method
  status_code = "200"
  response_parameters = {
        "method.response.header.Access-Control-Allow-Origin" = true,
        "method.response.header.Access-Control-Expose-Headers" = true,
        "method.response.header.Content-Disposition" = true,
        "method.response.header.Content-Type" = true
  }
}

# Integration Response
resource "aws_api_gateway_integration_response" "integrationresponse" {
  rest_api_id = aws_api_gateway_rest_api.apigateway.id
  resource_id = aws_api_gateway_resource.apiresource.id
  http_method = aws_api_gateway_method.method.http_method
  status_code = aws_api_gateway_method_response.methodresponse.status_code
  content_handling = "CONVERT_TO_BINARY"
  response_parameters = {
        "method.response.header.Access-Control-Allow-Origin" = "'*'",
        "method.response.header.Access-Control-Expose-Headers" = "'Content-Disposition'",
        "method.response.header.Content-Disposition" = "'attachment'",
        "method.response.header.Content-Type" = "'application/octet-stream'"
  }
  depends_on = [aws_api_gateway_integration.integration]
}

# CORS Method
resource "aws_api_gateway_method" "cors_method" {
    rest_api_id   = aws_api_gateway_rest_api.apigateway.id
    resource_id   = aws_api_gateway_resource.apiresource.id
    http_method   = "OPTIONS"
    authorization = "NONE"
}

# CORS Integration
resource "aws_api_gateway_integration" "cors_integration" {
    rest_api_id   = aws_api_gateway_rest_api.apigateway.id
    resource_id   = aws_api_gateway_resource.apiresource.id
    http_method   = aws_api_gateway_method.cors_method.http_method
    type          = "MOCK"
    request_templates = {
    "application/json" = <<EOF
{"statusCode": 200}
EOF
    }
    depends_on = [aws_api_gateway_method.cors_method]
}

# CORS Method Response
resource "aws_api_gateway_method_response" "cors_methodresponse" {
    rest_api_id   = aws_api_gateway_rest_api.apigateway.id
    resource_id   = aws_api_gateway_resource.apiresource.id
    http_method   = aws_api_gateway_method.cors_method.http_method
    status_code   = "200"
    response_models = {
        "application/json" = "Empty"
    }
    response_parameters = {
        "method.response.header.Access-Control-Allow-Headers" = true,
        "method.response.header.Access-Control-Allow-Methods" = true,
        "method.response.header.Access-Control-Allow-Origin" = true
    }
    depends_on = [aws_api_gateway_method.cors_method]
}

# CORS Integration Response
resource "aws_api_gateway_integration_response" "cors_integrationresponse" {
    rest_api_id   = aws_api_gateway_rest_api.apigateway.id
    resource_id   = aws_api_gateway_resource.apiresource.id
    http_method   = aws_api_gateway_method.cors_method.http_method
    status_code   = aws_api_gateway_method_response.cors_methodresponse.status_code
    response_parameters = {
        "method.response.header.Access-Control-Allow-Headers" = "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'",
        "method.response.header.Access-Control-Allow-Methods" = "'OPTIONS,POST'",
        "method.response.header.Access-Control-Allow-Origin" = "'*'"
    }
    response_templates = {
      "application/json" = <<EOF
EOF
    }
    depends_on = [
      aws_api_gateway_method_response.cors_methodresponse,
      aws_api_gateway_integration.cors_integration,
    ]
}

# Deployment
resource "aws_api_gateway_deployment" "apideployment" {
  depends_on = [
    aws_api_gateway_integration.integration,
    aws_api_gateway_integration.cors_integration
  ]

  rest_api_id = aws_api_gateway_rest_api.apigateway.id
  stage_name  = "dev"
}

output "base_url" {
  value = aws_api_gateway_deployment.apideployment.invoke_url
}

api_gateway_body_mapping.template

{
    "body": "$input.body",
    "headers": {
        #foreach($param in $input.params().header.keySet())
        "$param": "$util.escapeJavaScript($input.params().header.get($param))"
        #if($foreach.hasNext),#end
        #end
    }
}

So terraform init and apply completes, but API Gateway shows Internal Server Error. (Calling it from postman, with this url: https://(randomchars).execute-api.eu-central-1.amazonaws.com/dev/dev)

"Execution failed due to configuration error: Unable to transform request" - This is the error from the api's cloudwatch log.

If I go to the Integration Request of the POST method execution on the AWS console, and I re-add the Function name (rlambda) it adds another policy to the lambda (Exactly the same as it was before, with different Sid of course) and I re-deploy my API, then it works completely. (It does not work if I only do the deploy API part.)

So my questions are:

  • Is there anything happening in the background when I re-add my Function name ? (Except the policy thing)
  • What to change in my code to be able to call api gateway right after terraform apply?
like image 855
Dániel Flach Avatar asked Dec 14 '25 04:12

Dániel Flach


1 Answers

Okay so after 5 days of suffering I realized what is the problem.

On the AWS console you are not able to set the Integration Request's content_handling and it is only an Optional parameter in Terraform as well.

When you are re-assigning your lambda's name on the console, not only the lambda's policy got updated, but also the integration request's content_handling got set to CONVERT_TO_TEXT. I don't know why it is changing and why to this value. Maybe it's based on the lambda - there is no information about this.

So I added this row to the aws_api_gateway_integration:

content_handling = "CONVERT_TO_TEXT"

And it solved my problem. Hope nobody will run into this again.

like image 114
Dániel Flach Avatar answered Dec 16 '25 02:12

Dániel Flach



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!