3

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?
1
  • I'm using aws_apigatewayv2_api resource and I found out that the issue is simply that I didn't have the target argument on it. target = aws_lambda_function.lambda.arn is the LIFE SAVER! Source here. Commented Nov 16, 2021 at 11:51

1 Answer 1

4

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.