Containers

AWS App Runner adds support for monorepos

Introduction

AWS App Runner is a fully managed container application service that lets you build, deploy, and run containerized web applications and API services without prior infrastructure or container experience. Starting today, AWS App Runner supports deploying services from source code repositories that follow a monorepo structure. Now you can tell AWS App Runner the source directory you’d like to deploy from a monorepo that hosts source code for multiple services.

Monorepo is a software development strategy in which code for multiple distinct projects is stored in a single repository with well-defined relationships. Cloud computing customers are adopting monorepo development strategies to increase collaboration, avoid code duplication, and improve visibility. This is especially beneficial for developing modern applications that follow a micro-services based architecture.

Customers use AWS App Runner build-from-source capability to deploy services directly from source code, thereby offloading the build and deployment workflow management to AWS App Runner. Previously, AWS App Runner only supported the root directory of the repository to run your build and start commands. Starting today, you don’t need to manage separate build and deployment pipelines for your applications. You can define the source directory in your AWS App Runner service configuration that it uses to build and deploy your application.

You can also enable automatic deployment for your services. With automatic deployment enabled, AWS App Runner rebuilds and deploys a service when there is any update in the source directory or dependency of your service. AWS App Runner won’t redundantly rebuild and deploy your service if other applications or folders outside of the source directory are updated.

This means customers can benefit from the AWS App Runner build-from-source capability to deploy services directly from a source-code repository that follows a monorepo structure. Customers pay the standard build fee for source-code based services. Customers can benefit from the flexibility of their monorepo applications with the simplicity of running them on AWS App Runner.

Solution overview

To showcase monorepo support in AWS App Runner, we’ll walk you through a demonstration. We’ll deploy a sample application with two services — one frontend and one backend — from a single source-code repository.

The sample application is backed by a monorepo of two microservices that runs a website for a fictional hotel. Both the frontend and backend are NodeJS applications served by the Express web framework. We’ll use AWS App Runner to create two services to host the application.

As part of the infrastructure to support the application, we’ll create an Amazon Relational Database Service (Amazon RDS) database and the Amazon Virtual Private Cloud (Amazon VPC) networking components we need to communicate between our two services and the database. Public internet traffic will be able to reach the frontend service and make requests to manage the hotel rooms.

These requests travel securely through the VPC Connector and VpcIngressConnection to the Backend Service that serves the requests by querying the Amazon RDS database. Finally, the request is served and the requestor receives a response. To learn more about private networking with AWS App Runner, refer to our deep dive posts on VPC Networking and Private Services.

When we create the AWS App Runner services, we’ll configure their respective SourceDirectory with AutoDeployments enabled so that commits made to each SourceDirectory will only deploy to their respective services.

Figure 1: Architecture diagram showing two Monorepo AWS App Runner Services connected within the AWS Cloud.

Figure 1: Architecture diagram showing two Monorepo AWS App Runner Services connected within the AWS Cloud.

Prerequisites

You’ll need these setup to complete the walkthrough:

Walkthrough

AWS App Runner connection

For code-based Services, AWS App Runner requires a connection to deploy code from your repository. We’ve created a GitHub Monorepo of the hotel application for you to fork for the purposes of this walkthrough.

First, access this GitHub repository and create a fork of the monorepo branch in your personal GitHub account. Store your repository URL as an environment variable:

REPOSITORY_URL=https://<<YOUR_FORKED_REPOSITORY_URL>>

Next, access the AWS App Runner Console in us-east-2 to create an AWS App Runner Connection with the name GitHubConnection connecting to your personal GitHub application. Once authenticated, you’ll be able to create AWS App Runner services from your GitHub repositories. See our Developer Guide for more details on how this handshake works.

Store the ConnectionArn locally for future use:

export MRO_AWS_REGION=us-east-2

LIST_CONNECTIONS_RESPONSE=$(aws apprunner --region $MRO_AWS_REGION \
    list-connections \
    --connection-name "GitHubConnection")
    
APP_RUNNER_CONNECTION_ARN=$(jq -n ${LIST_CONNECTIONS_RESPONSE} | jq -r '.ConnectionSummaryList[0].ConnectionArn')

Networking and database infrastructure

Clone the sample repository (or your newly forked repository) which contains the code for our solution:

export MRO_STACK_NAME=apprunner-monorepo
git clone --branch monorepo https://github.com/aws-samples/apprunner-hotel-app.git
cd ./apprunner-hotel-app/

Let’s build the infrastructure by deploying the AWS CloudFormation template infrastructure/base-infrastructure.yaml to provision a VPC, VPC Endpoint, VPC Connector, Amazon RDS instance, and a Secrets Store for the database credentials:

aws cloudformation deploy \
    --region ${MRO_AWS_REGION} \
    --template-file ./infrastructure/base-infra.yaml \
    --stack-name ${MRO_STACK_NAME} \
    --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM

Wait for the above AWS CloudFormation stack to complete. Once the stack has been provisioned, run the below commands to get output values from the stack:

#Hotel Name
HOTEL_NAME=$(aws cloudformation describe-stacks \
    --region ${MRO_AWS_REGION} \
    --stack-name ${MRO_STACK_NAME} \
    --query 'Stacks[0].Outputs[?OutputKey==`HotelName`].OutputValue' \
    --output text)
    
#RDS Secret
SECRET_ARN=$(aws cloudformation describe-stacks \
    --region ${MRO_AWS_REGION} \
    --stack-name ${MRO_STACK_NAME} \
    --query 'Stacks[0].Outputs[?OutputKey==`DBSecret`].OutputValue' \
    --output text)

#VPC ID
VPC_ID=$(aws cloudformation describe-stacks \
    --region ${MRO_AWS_REGION} \
    --stack-name ${MRO_STACK_NAME} \
    --query 'Stacks[0].Outputs[?OutputKey==`VPCID`].OutputValue' \
    --output text)

#App Runner VPC Endpoint
VPC_ENDPOINT=$(aws cloudformation describe-stacks \
    --region ${MRO_AWS_REGION} \
    --stack-name ${MRO_STACK_NAME} \
    --query 'Stacks[0].Outputs[?OutputKey==`AppRunnerVPCEndpoint`].OutputValue' \
    --output text)
    
#Backend App Runner VPC Connector
VPC_BE_CONNECTOR_ARN=$(aws cloudformation describe-stacks \
    --region ${MRO_AWS_REGION} \
    --stack-name ${MRO_STACK_NAME} \
    --query 'Stacks[0].Outputs[?OutputKey==`AppRunnerBEVPCConnector`].OutputValue' \
    --output text)
    
#Frontend App Runner VPC Connector
VPC_FE_CONNECTOR_ARN=$(aws cloudformation describe-stacks \
    --region ${MRO_AWS_REGION} \
    --stack-name ${MRO_STACK_NAME} \
    --query 'Stacks[0].Outputs[?OutputKey==`AppRunnerFEVPCConnector`].OutputValue' \
    --output text)

#App Runner IAM Instance Role
INSTANCE_ROLE_ARN=$(aws cloudformation describe-stacks \
    --region ${MRO_AWS_REGION} \
    --stack-name ${MRO_STACK_NAME} \
    --query 'Stacks[0].Outputs[?OutputKey==`AppRunnerInstanceRole`].OutputValue' \
    --output text)

Creating the Monorepo services

We now have all of infrastructure necessary to support our backend and frontend AWS App Runner services.

Backend service

Let’s now create the backend service. First, run the below command to create a local file create_backend_service.json with the configuration for your service. Note that the SourceDirectory is specified as backend as seen in the git repository.

cat > create_backend_service.json << EOF
{
    "ServiceName": "hotel-backend",
    "SourceConfiguration": {
       "CodeRepository": {
          "RepositoryUrl": "${REPOSITORY_URL}",
          "SourceCodeVersion": {
             "Type": "BRANCH",
             "Value": "monorepo"
          },
          "CodeConfiguration": {
             "ConfigurationSource": "API",
             "CodeConfigurationValues": {
               "BuildCommand": "npm install",
               "Port": "8080",
               "Runtime": "NODEJS_16",
               "RuntimeEnvironmentSecrets": {
                  "HOTEL_NAME" : "${HOTEL_NAME}",
                  "MYSQL_SECRET": "${SECRET_ARN}"
               },
               "StartCommand": "npm start"
             }
          },
          "SourceDirectory": "backend"
       },
       "AutoDeploymentsEnabled": true,
       "AuthenticationConfiguration": {
          "ConnectionArn": "${APP_RUNNER_CONNECTION_ARN}"
       }
    },
    "InstanceConfiguration": {
        "Cpu": "1 vCPU",
        "Memory": "3 GB",
        "InstanceRoleArn": "${INSTANCE_ROLE_ARN}"
    },
    "NetworkConfiguration": {
        "EgressConfiguration": {
            "EgressType": "VPC",
            "VpcConnectorArn": "${VPC_BE_CONNECTOR_ARN}"
        },
        "IngressConfiguration": {
            "IsPubliclyAccessible": false
        }
    }
}
EOF

Create the service:

BACKEND_CREATE_SERVICE_RESPONSE=$(aws apprunner --region $MRO_AWS_REGION \
    create-service \
    --cli-input-json file://create_backend_service.json)

Store the backend ServiceArn from the response:

BACKEND_SERVICE_ARN=$(jq -n ${BACKEND_CREATE_SERVICE_RESPONSE} | jq -r '.Service.ServiceArn')

VpcIngressConnection

By default, AWS App Runner services are accessible publicly over the internet. But the backend service isn’t meant to be exposed publicly. We have to ensure that it is only accessible within our VPC.

To restrict network access to the backend service, we’ll create an AWS App Runner VPC Ingress Connection resource or a VpcIngressConnection. VPC Ingress Connection establishes a connection between a VPC interface endpoint and an AWS App Runner service, making your App Runner service accessible from only within an Amazon VPC.

Create a VpcIngressConnection to allow private access to your backend service:

INGRESS_CREATE_RESPONSE=$(aws apprunner --region ${MRO_AWS_REGION} \
    create-vpc-ingress-connection \
    --service-arn ${BACKEND_SERVICE_ARN} \
    --vpc-ingress-connection-name "Private-Connection-To-Backend" \
    --ingress-vpc-configuration VpcId=${VPC_ID},VpcEndpointId=${VPC_ENDPOINT})

Store the VpcIngressConnectionArn and DomainName from the response:

INGRESS_ARN=$(jq -n ${INGRESS_CREATE_RESPONSE} | jq -r '.VpcIngressConnection.VpcIngressConnectionArn')
BACKEND_URL="https://$(jq -n ${INGRESS_CREATE_RESPONSE} | jq -r '.VpcIngressConnection.DomainName')/"

Frontend service

It’s now time to deploy the frontend service. Run the below command to create a local file create_frontend_service.json with the configuration for your service. Note that the SourceDirectory is specified as frontend as seen in the git repository.

cat > create_frontend_service.json << EOF
{
    "ServiceName": "hotel-frontend",
    "SourceConfiguration": {
       "CodeRepository": {
          "RepositoryUrl": "${REPOSITORY_URL}",
          "SourceCodeVersion": {
             "Type": "BRANCH",
             "Value": "monorepo"
          },
          "CodeConfiguration": {
             "ConfigurationSource": "API",
             "CodeConfigurationValues": {
               "BuildCommand": "npm install",
               "Port": "8080",
               "Runtime": "NODEJS_16",
               "RuntimeEnvironmentSecrets": {
                  "HOTEL_NAME" : "${HOTEL_NAME}"
               },
               "RuntimeEnvironmentVariables": {
                  "BACKEND_URL": "${BACKEND_URL}"
               },
               "StartCommand": "npm start"
             }
          },
          "SourceDirectory": "frontend"
       },
       "AutoDeploymentsEnabled": true,
       "AuthenticationConfiguration": {
          "ConnectionArn": "${APP_RUNNER_CONNECTION_ARN}"
       }
    },
    "InstanceConfiguration": {
        "Cpu": "1 vCPU",
        "Memory": "3 GB",
        "InstanceRoleArn": "${INSTANCE_ROLE_ARN}"
    },
    "NetworkConfiguration": {
        "EgressConfiguration": {
            "EgressType": "VPC",
            "VpcConnectorArn": "${VPC_FE_CONNECTOR_ARN}"
        },
        "IngressConfiguration": {
            "IsPubliclyAccessible": true
        }
    }
}
EOF

Create the frontend service:

FRONTEND_CREATE_SERVICE_RESPONSE=$(aws apprunner --region $MRO_AWS_REGION \
    create-service \
    --cli-input-json file://create_frontend_service.json)

Store the frontend ServiceArn and ServiceUrl from the response:

FRONTEND_SERVICE_ARN=$(jq -n ${FRONTEND_CREATE_SERVICE_RESPONSE} | jq -r '.Service.ServiceArn')
FRONTEND_URL="https://$(jq -n ${FRONTEND_CREATE_SERVICE_RESPONSE} | jq -r '.Service.ServiceUrl')"

Check the status of the creation while you wait for it to complete:

aws apprunner --region $MRO_AWS_REGION \
    describe-service \
    --service-arn ${FRONTEND_SERVICE_ARN}

Once the service is available, access your application through the FRONTEND_URL.

echo ${FRONTEND_URL}

Figure 2: Running Hotel application built with Monorepo frontend and backend services.

Figure 2: Running Hotel application built with Monorepo frontend and backend services.

From here you can choose the Create button to create the database schema, and then add and view your hotel rooms. With AutoDeployments enabled, any changes committed to the frontend/ directory in your GitHub repository launches a deployment to the frontend service, but not the backend service. Give it a try!

Cleaning up

AWS resources created during this walkthrough will incur cost. After finishing the demonstration, be sure to delete the infrastructure you created.

First, delete the AWS App Runner VpcIngressConnection:

INGRESS_DELETE_RESPONSE=$(aws apprunner --region ${MRO_AWS_REGION} \
    delete-vpc-ingress-connection \
    --vpc-ingress-connection-arn ${INGRESS_ARN})

Next, delete the frontend and backend services:

FRONTEND_DELETE_SERVICE_RESPONSE=$(aws apprunner --region $MRO_AWS_REGION \
    delete-service \
    --service-arn ${FRONTEND_SERVICE_ARN})

BACKEND_DELETE_SERVICE_RESPONSE=$(aws apprunner --region $MRO_AWS_REGION \
    delete-service \
    --service-arn ${BACKEND_SERVICE_ARN})

Finally, delete the AWS CloudFormation stack:

DELETE_STACK_RESPONSE=$(aws cloudformation --region $MRO_AWS_REGION \
    delete-stack \
    --stack-name ${MRO_STACK_NAME})

Conclusion

In this post, we showed you how you can deploy services from a monorepo using AWS App Runner. We used App Runner’s monorepo support to deploy a sample application, which has both a frontend and backend tier in a single source-code repository. We would encourage you to try out this new feature for your production web applications with AWS App Runner. Please learn more about AWS App Runner from our documentation and developer guide.