9. Cloud Resume Challenge: Creating an API Gateway and API Resources/Methods

9. Cloud Resume Challenge: Creating an API Gateway and API Resources/Methods

In this module, you will be creating your API Gateway and a couple of resources that will interact with your Lambda Functions from the previous module. You will create API resources that will utilize POST HTTP Methods. Once your API Gateway is deployed, I will walk you through testing your Lambda functions through the API Gateway to ensure proper functionality.


Terraform API Gateway REST API Referrence


Create a new branch in GitHub

  • Login to GitHub.

  • Select the Issues tab > Select New Issue.

  • Add the a title, e.g. Creating API Gateway and API Resources

  • Select Submit new issue.

  • On the right pane, under Development, Select Create a branch.

  • Leave the defaults > Select Create branch.

  • Open your IDE Terminal.

  • Input the following:

git fetch origin
git checkout YOUR_BRANCH_NAME

Creating API Gateway

In this section you will be modifying the main, variables and outputs Terraform files located in ./infra/modules/aws/api_gateway/.

main.tf

  • Input the following to create the API Gateway:

      resource "aws_api_gateway_rest_api" "api" {
        name        = var.name
        description = var.description
    
         endpoint_configuration {
          types = var.types
        }
      }
    

    This will create a REST API Gateway that will have resources and methods associated with it

  • Save the file

variables.tf

  • Input the following to define the three variables:

      variable "name" {
        description = "(Required) Name of the REST API. If importing an OpenAPI specification via the body argument, this corresponds to the info.title field. If the argument value is different than the OpenAPI value, the argument value will override the OpenAPI value."
        type = string
      }
    
      variable "description" {
        description = "(Optional) Description of the REST API. If importing an OpenAPI specification via the body argument, this corresponds to the info.description field. If the argument value is provided and is different than the OpenAPI value, the argument value will override the OpenAPI value."
        type = string
      }
    
      variable "types" {
        description = "(Required) List of endpoint types. This resource currently only supports managing a single value. Valid values: EDGE, REGIONAL or PRIVATE. If unspecified, defaults to EDGE. If set to PRIVATE recommend to set put_rest_api_mode = merge to not cause the endpoints and associated Route53 records to be deleted. Refer to the documentation for more information on the difference between edge-optimized and regional APIs."
        type = list(string)
      }
    

  • Save the file

outputs.tf

  • Input the following to define the return variables for the API Gateway module

      output "arn" {
        description = "The ARN of the REST API"
        value = aws_api_gateway_rest_api.api.arn
      }
    
      output "created_date" {
        description = "Creation date of the REST API"
        value = aws_api_gateway_rest_api.api.created_date
      }
    
      output "execution_arn" {
        description = "Execution ARN part to be used in lambda_permission's source_arn when allowing API Gateway to invoke a Lambda function, e.g., arn:aws:execute-api:eu-west-2:123456789012:z4675bid1j, which can be concatenated with allowed stage, method and resource path."
        value = aws_api_gateway_rest_api.api.execution_arn
      }
    
      output "id" {
        description = "ID of the REST API"
        value = aws_api_gateway_rest_api.api.id
      }
    
      output "root_resource_id" {
        description = "Resource ID of the REST API's root"
        value = aws_api_gateway_rest_api.api.root_resource_id
      }
    
      output "tags_all" {
        description = "Map of tags assigned to the resource, including those inherited from the provider default_tags configuration block."
        value = aws_api_gateway_rest_api.api.tags_all
      }
    

    The id and root_resource_id in particular will be required to create the resource and methods and associate it with the API gateway that will be created.

  • Save the file


Creating API Gateway Resource

In this section you will be modifying the main, variables and outputs Terraform files located in ./infra/modules/aws/api_gateway/api_gateway_resource.

main.tf

  • Input the following to create the API Gateway resource

      resource "aws_api_gateway_resource" "resource" {
        rest_api_id = var.api_id
        parent_id   = var.parent_id
        path_part   = var.path_part
      }
    

This will create a resource associated with the API Gateway.

  • Save the file

variables.tf

  • Input the following to define the input variables for this module

      variable "api_id" {
        description = "(Required) ID of the associated REST API"
        type = string
      }
    
      variable "parent_id" {
        description = "(Required) ID of the parent API resource"
        type = string
      }
    
      variable "path_part" {
        description = "(Required) Last path segment of this API resource."
        type = string
      }
    

  • Save the file

outputs.tf

  • Input the following to define the return variables for this module

      output "resource_id" {
        description = "Output API Gateway Resource ID for use for API Gateway Deployment"
        value = aws_api_gateway_resource.resource.id
      }
    
      output "path" {
        description = "Path value for API Gateway Resource"
        value = aws_api_gateway_resource.resource.path
      }
    

  • Save the file


Creating API Gateway Resource Methods

In this section you will be modifying the main, variables and outputs Terraform files located in ./infra/modules/aws/api_gateway/api_gateway_resource/api_gateway_method.

Four resources will be defined for the Resource Method (POST)

  • Method Request

  • Integration Request

  • Integration Response

  • Method Response

main.tf

method request:

  • Input the following to create the method request

      resource "aws_api_gateway_method" "method" {
        rest_api_id    = var.api_id
        resource_id    = var.resource_id
        http_method    = var.http_method
        authorization  = var.authorization
      }
    

integration request

  • Input the following to create the integration request

      resource "aws_api_gateway_integration" "integration" {
        rest_api_id             = var.api_id
        resource_id             = var.resource_id
        http_method             = aws_api_gateway_method.method.http_method  
        integration_http_method = var.integration_http_method
        type                    = var.type
        uri                     = var.uri
    
        request_templates = var.response_templates
      }
    

Integration response will define what type of service this will be associated with. You will later define AWS for the AWS Lambda service

integration response

  • Input the following to create the integration response

      resource "aws_api_gateway_integration_response" "IntegrationResponse" {
        rest_api_id = var.api_id
        resource_id = var.resource_id
        http_method = aws_api_gateway_method.method.http_method
        status_code = aws_api_gateway_method_response.response_200.status_code
    
        response_parameters = var.integration_response_parameters
    
        depends_on = [ 
          aws_api_gateway_integration.integration,
          aws_api_gateway_method.method
        ]
      }
    

    Note: The depends_on argument will force the IntegrationResponse resource to wait until the previous methods are created. Without the depends_on argument, the integration response will be configured improperly and the API gateway will not function as intended.

method response

  • Input the following to create the method response

      resource "aws_api_gateway_method_response" "response_200" {
        rest_api_id = var.api_id
        resource_id = var.resource_id
        http_method = aws_api_gateway_method.method.http_method
        status_code = 200
    
        response_parameters = var.method_response_parameters 
    
        response_models = var.response_models
      }
    

  • Save the file

variables.tf

  • Input the following to define the variables for this module:

      variable "api_id" {
        description = "(Required) ID of the associated REST API"
        type = string
        nullable = false
      }
    
      # aws_api_gateway_mathod.method
      variable "http_method" {
        description = "(Required) HTTP Method (GET, POST, PUT, DELETE, HEAD, OPTIONS, ANY)"
        type = string
        default = "POST"
      }
    
      variable "authorization" {
        description = "(Required) Type of authorization used for the method (NONE, CUSTOM, AWS_IAM, COGNITO_USER_POOLS)"
        type = string
        default = "NONE"
      }
    
      # aws_api_gateway_integration.lambda_integration
      variable "type" {
        description = "(Required) Integration input's type. Valid values are HTTP (for HTTP backends), MOCK (not calling any real backend), AWS (for AWS services), AWS_PROXY (for Lambda proxy integration) and HTTP_PROXY (for HTTP proxy integration). An HTTP or HTTP_PROXY integration with a connection_type of VPC_LINK is referred to as a private integration and uses a VpcLink to connect API Gateway to a network load balancer of a VPC."
        type = string
        default = "AWS"
      }
    
      variable "integration_http_method" {
        description = "(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. Not all methods are compatible with all AWS integrations. e.g., Lambda function can only be invoked via POST."
        type = string
        nullable = true
        default = null
      }
    
      variable "uri" {
        description = "(Optional) Input's URI. Required if type is AWS, AWS_PROXY, HTTP or HTTP_PROXY. For HTTP integrations, the URI must be a fully formed, encoded HTTP(S) URL according to the RFC-3986 specification . For AWS integrations, the URI should be of the form arn:aws:apigateway:{region}:{subdomain.service|service}:{path|action}/{service_api}. region, subdomain and service are used to determine the right endpoint. e.g., arn:aws:apigateway:eu-west-1:lambda:path/2015-03-31/functions/arn:aws:lambda:eu-west-1:012345678901:function:my-func/invocations. For private integrations, the URI parameter is not used for routing requests to your endpoint, but is used for setting the Host header and for certificate validation."
        type = string
        nullable = true
        default = null
      }
    
      # aws_api_gateway_method_response.response_200
    
      variable "method_response_parameters" {
        description = "(Optional) A map specifying required or optional response parameters that API Gateway can send back to the caller. A key defines a method response header name and the associated value is a boolean flag indicating whether the method response parameter is required. The method response header names must match the pattern of method.response.header.{name}, where name is a valid and unique header name."
        type = map(bool)
        default = {
          "method.response.header.Access-Control-Allow-Origin" = true
        }
      }
    
      variable "response_models" {
        description = "(Optional) A map specifying the model resources used for the response's content type. Response models are represented as a key/value map, with a content type as the key and a Model name as the value."
        type = map(string)
        default = {
          "application/json" = "Empty"
        }
      }
    
      # aws_api_gateway_integration_response.IntegrationResponse
      variable "integration_response_parameters" {
        description = "(Optional) Map of response parameters that can be read from the backend response. For example: response_parameters = %%{ \"method.response.header.X-Some-Header\" = \"integration.response.header.X-Some-Other-Header\" }."
        type = map(string)
        default = {
          "method.response.header.Access-Control-Allow-Origin" = "'*'"
        }
      }
    
      variable "response_templates" {
        description = "(Optional) Map of templates used to transform the integration response body."
        type = map(string)
        default = {
          "application/json" = ""
        }
      }
    
      variable "resource_id" {
        description = "ID for API Gateway Resource"
        type = string
      }
    

    Here you define some default variable inputs for the POST method using default values.

  • Save the file

outputs.tf

  • Input the following to define the return values for this module:

      output "method" {
        description = "Output API Gateway Resource Method for use for API Gateway Deployment"
        value = aws_api_gateway_method.method.id
      }
    
      output "integration" {
        description = "Output API Gateway Resource Integration Method for use for API Gateway Deployment"
        value = aws_api_gateway_integration.integration.id
      }
    
      output "integration_response" {
        description = "ID for API Gateway integration response"
        value = aws_api_gateway_integration_response.IntegrationResponse.id
      }
    

  • Save the file


Creating API Resource with CORS

In this section you will be modifying the main, variables and outputs Terraform files located in ./infra/modules/API_resource_with_CORS. In order for the REST API to be accessible, CORS will need to be enabled with an OPTIONS method. The API Resource with CORS module will create the respective API Gateway resource, it's Method and an OPTIONS method.

main.tf

  • Input the following to create the resource, method and OPTIONS method for this module:

      module "api_resource" {
        source = "../aws/api_gateway/api_gateway_resource"
    
        api_id                  = var.api_id
        parent_id               = var.parent_id
        path_part               = var.path_part
      }
    
      module "api_methods" {
        source = "../aws/api_gateway/api_gateway_resource/api_gateway_method"
    
        api_id                  = var.api_id
        resource_id             = module.api_resource.resource_id
        integration_http_method = var.http_method
        uri                     = var.lambda_function
      }
    
      module "api_methods_cors" {
        source = "../aws/api_gateway/api_gateway_resource/api_gateway_method"
    
        api_id                  = var.api_id
        resource_id             = module.api_resource.resource_id
        http_method             = "OPTIONS"
        type                    = "MOCK"
    
        response_templates = {
          "application/json" = "{\"statusCode\" : 200}"
        }
    
        method_response_parameters = {
          "method.response.header.Access-Control-Allow-Origin" = true,
          "method.response.header.Access-Control-Allow-Methods" = true,
          "method.response.header.Access-Control-Allow-Headers" = true
        }
    
        integration_response_parameters = {
          "method.response.header.Access-Control-Allow-Origin"  = "'*'", 
          "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'"
        }
    
        depends_on = [ module.api_resource ]
      }
    

  • Save the file

variables.tf

  • Input the following to define the input variables for this module

      variable "api_id" {
        description = "(Required) ID of the associated REST API"
        type = string
        nullable = false
      }
    
      variable "parent_id" {
        description = "(Required) ID of the parent API resource"
        type = string
        nullable = false
      }
    
      variable "path_part" {
        description = "(Required) Last path segment of this API resource."
        type = string
        nullable = false
      }
    
      variable "http_method" {
        description = "(Required) HTTP Method (GET, POST, PUT, DELETE, HEAD, OPTIONS, ANY)"
        type = string
        default = "POST"
      }
    
      variable "lambda_function" {
        description = "Invoke ARN of Lambda Function for Integration with API Gateway"
        type = string
      }
    

  • Save the file

outputs.tf

  • Input the following to define the return values for this module:

      output "api_resource" {
        description = "Output API Gateway Resource ID for use for API Gateway Deployment"
        value = module.api_resource.resource_id
      }
    
      output "method" {
        description = "Output API Gateway Resource Method for use for API Gateway Deployment"
        value = module.api_methods.method
      }
    
      output "integration" {
        description = "Output API Gateway Resource Integration Method for use for API Gateway Deployment"
        value = module.api_methods.integration
      }
    
      output "integration_response" {
        description = "ID for API Gateway integration response"
        value = module.api_methods.integration_response
      }
    
      output "path" {
        description = "Path value for API Gateway Resource"
        value = module.api_resource.path
      }
    
  • Save the file


Modifying lambda files

For the API Gateway to interact with your respective Lambda functions, you will need to provide the API Gateway with the InvokeFunction permissions. Locate your lambda folder in ./infra/modules/aws/.

main.tf

  • Input the following to assign permissions to the Lambda function

      resource "aws_lambda_permission" "permission" {
        statement_id  = "AllowExecutionFromAPIGateway"
        action        = "lambda:InvokeFunction"
        function_name = var.function_name
        principal     = "apigateway.amazonaws.com"
        source_arn = var.api_gateway_arn
      }
    

  • Save the file

variables.tf

  • Include the following input variable for the lambda module

      variable "api_gateway_arn" {
        description = "Source ARN for API Gateway"
        type = string
      }
    

  • Save the file


Modify lambda_function files

In this section, you will include the argument for the api_gateway_arn.

main.tf

  • Modify your main.tf to include the api_gateway_arn. Input the following:

      api_gateway_arn = var.api_gateway_arn
    

  • Save the file

variables.tf

  • Input the following to define the api_gateway_arn variable

      variable "api_gateway_arn" {
        description = "Source ARN for API Gateway"
        type = string
      }
    

  • Save the file


Modify my_portfolio files

In this section, you will update my_portfolio module to include the creation of the API Gateway, API Gateway Resources, and API Gateway Methods.

main.tf

  • Input the following to create the API Gateway, and DynamoDB and SNS Resource/Methods:

      module "api" {
        source = "../aws/api_gateway"
    
        name        = var.api_name
        description = var.api_description
        types       = var.api_types
      }
    
      module "api_resource_dynamodb" {
        source = "../api_resource_with_CORS"
    
        api_id                    = module.api.id
        parent_id                 = module.api.root_resource_id
        path_part                 = var.dynamodb_path_part
        http_method               = var.http_method
        lambda_function           = module.lambda_dynamodb.invoke_arn
      }
    
      module "api_resource_sns" {
        source = "../api_resource_with_CORS"
    
        api_id                    = module.api.id
        parent_id                 = module.api.root_resource_id
        path_part                 = var.sns_path_part
        http_method               = var.http_method
        lambda_function           = module.lambda_sns.invoke_arn
      }
    

  • Modify your entry for module.lambda_dynamodb and module.lambda_sns to include the api_gateway_arn argument

dynamodb:

"${module.api.execution_arn}/*/POST${module.api_resource_dynamodb.path}"

sns:

"${module.api.execution_arn}/*/POST${module.api_resource_sns.path}"

  • Save the file

variables.tf

  • Input the following to define the input variables for the API Gateway resources

      variable "api_name" {
        description = "(Required) Name of the REST API. If importing an OpenAPI specification via the body argument, this corresponds to the info.title field. If the argument value is different than the OpenAPI value, the argument value will override the OpenAPI value."
        type = string
      }
    
      variable "api_description" {
        default = "(Optional) Description of the REST API. If importing an OpenAPI specification via the body argument, this corresponds to the info.description field. If the argument value is provided and is different than the OpenAPI value, the argument value will override the OpenAPI value."
        type = string
      }
    
      variable "api_types" {
        description = "(Required) List of endpoint types. This resource currently only supports managing a single value. Valid values: EDGE, REGIONAL or PRIVATE. If unspecified, defaults to EDGE. If set to PRIVATE recommend to set put_rest_api_mode = merge to not cause the endpoints and associated Route53 records to be deleted. Refer to the documentation for more information on the difference between edge-optimized and regional APIs."
        type = list(string)
      }
    
      variable "dynamodb_path_part" {
        description = "(Required) Last path segment of the DynamoDB API resource."
        type = string
      }
    
      variable "sns_path_part" {
        description = "(Required) Last path segment of the SNS API resource."
        type = string
      }
    
      variable "http_method" {
        description = "(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. Not all methods are compatible with all AWS integrations. e.g., Lambda function can only be invoked via POST."
        type = string
      }
    

  • Save the file

  • There is no requirement for any outputs for the API Gateway.


Modify ./infra/main.tf

In this section, you will define the input variables to pass to the my_portfolio module.

main.tf

  • Input the following to include the creation for the API Gateway resources for the DynamoDB and SNS Lambda functions

      # API Gateway
        api_name = "My_REST_API"
        api_description = "This is a REST API for my portfolio"
        api_types = ["REGIONAL"]
        dynamodb_path_part = "viewers"
        sns_path_part = "emailme"
        http_method = "POST"
    

  • Save the file

Pushing to GitHub

  • Ensure your files are saved.

  • In your IDE Terminal, type the following:

git add .

Add all files that were changed.

git commit -m "Updated lambda permissions for API Gateway execution. Created API Gateway, resources and methods for 
SNS and DynamoDB"

Commit the changes with a comment.

git push

Push to GitHub.


Create Pull Request

  • Login to GitHub.

  • You should see the push on your repository.

  • Select Compare and pull request.

  • Validate the changes that were made to be pushed to main

  • Select Create pull request.

Your Terraform Plan should run before you can merge to main.

If you are using the same site files from the original template, the plan to add 21 should match.

  • Select Merge pull request > Confirm merge.

  • Delete branch.

  • In your IDE Terminal, type the following:

git checkout main
git pull

Validation:

  • AWS Management Console:

  • In the Search bar, search for "API Gateway" > Select APIs

  • You should see your API listed

  • Select your API

  • Under the resources, you should see your defined resources emailme and viewers. Both resources should have a POST method and OPTIONS method listed


Testing API Gateway with Lambda

In this section, you will test your API Gateway resources and the Lambda functions associated with them.

viewers

  • Select the /viewers POST method

  • Select the Test tab

  • Leave the fields blank, and select the Test button at the bottom

  • You should receive output like below once completed

    Note the views value from DynamoDB has incremented by 1 since the last test.

  • This shows a successful integration for this resource using the Lambda function associated with updating the DynamoDB table

emailme

  • Select the /emailme POST method

  • Select the Test tab

  • In the Request body field, input the following:

{
    "name": "King T'challa",
    "email": "blackpanther@wakanda.com",
    "phone": 11111111,
    "subject": "Wakanda Forever!",
    "body": "For Honor, For Legacy, For Wakanda!"
}

  • Select the Test button at the bottom

  • You should receive output like below once completed

  • Check your email. If successfully implemented, you should receive an email like below:


You have successfully implemented your API Gateway to interact with your Lambda functions. In the next module, you will modify your HTML and JavaScript files so that they will integrate the API Gateway resources. Due to CloudFront having cached files of your Static S3 Bucket website, you will need to invalidate the CloudFront Distribution. To do this, you will also modify your GitHub Actions to include invalidating CloudFront whenever your Public Folder files are modified.