Automate Node.js API Deployment to AWS App Runner Using Terraform

Automate Node.js API Deployment to AWS App Runner Using Terraform

Ever run into a situation where you manually deployed your API to AWS via the management console (one with lots of enviroment variables too!), only to realise you forgot to change the region of deployment?

It may not be the exact scenario but forgetting to switch regions for deployments can be a common occurrence; and in my case, I had previously deployed to us-east-1 but now needed the deployment to be eu-west-2. I tried to search for any console features that could allow me “copy” the service into the new region (or even update the current region), but that did not seem to be available, so I was left with repeating the entire process. Using Infrastructure-as-code to automate provisioning of the AWS resources would have saved me from this situation, hence this blogpost.

In this post, I’ll be sharing the Terraform scripts I wrote to automate the deployment of a Node.js (Express) API with environment variables including a database deployed on AWS RDS with MySQL.

Prerequisites

Setup

This was my directory structure:

Note: As you will see below, I created shell scripts to run the necessary Terraform commands due to the number of environment variables I needed to add to the API service.

  1. variables.tf
variable "api_name" {
  type = string
  default = # name you want to give to your api service
}

variable "github_repo" {
  type = string
  default = # your GitHub repo url
}

variable "apprunner_connection_arn" {
  type = string
}
variable "vpc_connector_arn" {
    type = string
}

variable "env_prod_db_username" {
  type = string
}
variable "env_prod_db_password" {
  type = string
}
variable "env_prod_fe_url" {
  type = string
}
  1. provider.tf
terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = "5.67.0"
    }
  }
}

provider "aws" {
  region = "eu-west-2"
}
  1. output.tf
output "api_service_url" {
  value = aws_apprunner_service.express-api-service.service_url
}

output "api_service_arn" {
  value = aws_apprunner_service.express-api-service.arn
}

output "api_service_status" {
  value = aws_apprunner_service.express-api-service.status
}
  1. main.tf

Prerequisites:

1) You need to have a connection to your repository provider (i.e GitHub or BitBucket) to enable access to that repo for App Runner; this can be done manually.

The ARN for this connection is what you need for var.apprunner_connection_arn.

2) You also need to create a VPC connector; this can be done with Terraform in this script. However, I had an existing VPC connector for the region, so the provisioning is not included in my script.

You can use the official documentation to set it up.


resource "aws_apprunner_service" "express-api-service" {
  service_name = var.api_name

  source_configuration {
    auto_deployments_enabled = true
    authentication_configuration {
      connection_arn = var.apprunner_connection_arn
    }

    code_repository {
      repository_url = var.github_repo
      source_code_version {
        type  = "BRANCH"
        value = "master" # or whichever branch you are deploying
      }
      code_configuration {
        configuration_source = "API"
        code_configuration_values {
          build_command = "npm install"
          port          = "8000" # or the port your api runs on
          runtime       = "NODEJS_16"
          start_command = "npm run start"
          runtime_environment_variables = {
            PROD_DB_PORT          = "3306"
            NODE_ENV              = "production"
            PROD_DB_PASSWORD      = var.env_prod_db_password
            PROD_DB_USERNAME      = var.env_prod_db_username
            PROD_FRONTEND_URL     = var.env_prod_fe_url
            # your other env variables
          }
        }
      }
    }
  }

  network_configuration {
    ingress_configuration {
      is_publicly_accessible = true
    }
    egress_configuration {
      egress_type       = "VPC"
      vpc_connector_arn = var.vpc_connector_arn
    }
  }

  instance_configuration {
    cpu    = "2048"  #2vCPU
    memory = "4096"  #4GB
  }

  health_check_configuration {
    interval = 10
    timeout = 5
  }

  tags = {
    DEPLOYED = "api-via-terraform"
  }
}

And that’s it! Run the necessary terraform commands with the scripts:

  • terraform init

  • sh plan.sh for terraform plan and review if necessary

  • sh deploy.sh for terraform apply

Note: Before running these scripts, ensure to have the .env file with all the necessary environment variables.

plan.sh

#!/bin/bash

export $(grep -v '^#' .env | xargs)

terraform plan \
  -var="apprunner_connection_arn=$APPRUNNER_CONNECTION_ARN" \
  -var="vpc_connector_arn=$VPC_CONNECTOR_ARN" \
  -var="env_prod_db_username=$PROD_DB_USERNAME" \
  -var="env_prod_db_password=$PROD_DB_PASSWORD" \
  -var="env_prod_fe_url=$PROD_FRONTEND_URL" \
  # any other environment variables your API needs

deploy.sh

#!/bin/bash

export $(grep -v '^#' .env | xargs)

terraform apply \
  -var="apprunner_connection_arn=$APPRUNNER_CONNECTION_ARN" \
  -var="vpc_connector_arn=$VPC_CONNECTOR_ARN" \
  -var="env_prod_db_username=$PROD_DB_USERNAME" \
  -var="env_prod_db_password=$PROD_DB_PASSWORD" \
  -var="env_prod_fe_url=$PROD_FRONTEND_URL" \
  # any other environment variables your API needs

After successful deployment, you should see a success message in the terminal with the following outputs:

api_service_url = “«value»“
api_service_arn = “«value»”
api_service_status = “«value»“

[Optional] Service Update

For updates to the service, you can use sh update.sh:

#!/bin/bash

export $(grep -v '^#' ../.env | xargs)

API_SERVICE_ARN=$(terraform output -raw api_service_arn)

# Updates the App Runner service with the configuration in the input.json file
aws apprunner update-service --service-arn $API_SERVICE_ARN \
    --cli-input-json file://input.json

echo "App Runner service has been updated"

However, the input.json file needed for the script needs to contain the JSON version of the entire service configuration (which seems like a pain tbh).

Is there a way to use a script similar to the above to update the service with the configuration updates only?🤔

Possible Issues You Might Encounter

  • If you have existing app runner connections and VPC connectors, ensure they are in the new region you are deploying to; these resources need to be located in the same region (your terraform script will throw an error otherwise).

  • After successful deployment, I ran into a database connection timeout error. However, this was related to the error shared in my previous post and I resolved it by allowing inbound access to the database via the security group of the app runner service.


Thanks for reading, and I hope you find it useful!

If you have any suggestions on updating the service without rewriting the entire setup configurations, please let me know in the comments.