How to Deploy an AI FastAPI Application to App Runner with Pulumi

Table of contents
- Introduction
- Prerequisites
- Building and Containerizing the Application
- Dockerfile Explanation:
- Building the Docker Image:
- Pushing the Docker Image to Amazon ECR
- Creating an ECR Repository (if not already done in Pulumi):
- Authenticating Docker to ECR:
- Defining Infrastructure as Code with Pulumi
- Deploying to AWS App Runner
- Accessing the Deployed Application
- Conclusion
Introduction
In the previous article, "Building a Story Generation App with FastAPI, AWS Bedrock, and Langchain," you created a web application that generates creative stories based on user input. You used FastAPI for the backend, Langchain for interacting with large language models, and AWS Bedrock as your LLM provider. Now, it's time to deploy your application to the cloud so that users can access it.
In this article, you'll use AWS App Runner, a fully managed service that makes it easy to build, deploy, and scale containerized applications quickly. You'll also leverage Pulumi, an Infrastructure as Code (IaC) tool, to define and manage our AWS infrastructure in a programmatic and repeatable way.
Prerequisites
Before you begin, make sure you have the following:
An AWS account with appropriate permissions to create resources (IAM roles, ECR repositories, App Runner services).
A Pulumi account and the Pulumi CLI installed and configured.
Docker installed and configured on your local machine.
The AWS CLI installed and configured.
Familiarity with basic Docker and AWS concepts.
Completed the steps outlined in the "Building a Story Generation App with FastAPI, AWS Bedrock, and Langchain" article.
A repository containing the code for the Story Generation App.
The
requirements.txt
file.
Building and Containerizing the Application
Your Story Generation App uses FastAPI as its backend framework and leverages Langchain to interact with AWS Bedrock for story generation. To deploy this application, you first need to package it into a Docker container.
Dockerfile Explanation:
The Dockerfile
defines how your application's container image will be built. Let's break down each instruction:
Dockerfile
FROM python:3.12-slim
LABEL version="1.0.2"
WORKDIR /code
COPY requirements.txt /code/requirements.txt
RUN python3 -m venv venv && \
. venv/bin/activate && \
pip install --no-cache-dir -r /code/requirements.txt
COPY . /code/app
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
AWS_ACCESS_KEY_ID=your-aws-access-key-id \
AWS_SECRET_ACCESS_KEY=your-secret-access-key \
AWS_REGION_NAME=your-aws-region-name \
CMD ["venv/bin/uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
FROM python:3.12-slim
: You start with a slim Python 3.12 base image to keep the image size small.LABEL version="1.0.2"
: This adds metadata to our image, specifying the version.WORKDIR /code
: You set the working directory inside the container to/code
.COPY requirements.txt /code/requirements.txt
: You copy therequirements.txt
file into the container.RUN python3 -m venv venv ...
: This creates a virtual environment, activates it, and installs the dependencies listed inrequirements.txt
.COPY . /code/app
: You copy the entire application code into theapp
folder.ENV PYTHONDONTWRITEBYTECODE=1 ...
: These environment variables are set for optimization. Important: TheAWS_ACCESS_KEY_ID, AWS_SECRET_KEY, and AWS_REGION_NAME
should be replaced with appropriate AWS credentials or environment variables when deploying to production.CMD ["venv/bin/uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
: This specifies the command to run when the container starts, launching the FastAPI application using Uvicorn on port 8000.
Building the Docker Image:
To build the Docker image, navigate to the directory containing your Dockerfile
in your terminal and run the following command:
Bash
docker build -t story-generator .
This command builds the image and tags it as story-generator
.
Pushing the Docker Image to Amazon ECR
Amazon Elastic Container Registry (ECR) is a fully managed Docker container registry that makes it easy to store, manage, and deploy Docker container images. You'll use ECR to store your Story Generation App's image.
Creating an ECR Repository (if not already done in Pulumi):
While your Pulumi code will handle creating the ECR repository, you can also do this manually through the AWS console or CLI. If you prefer to use the console, navigate to the ECR service, click "Create repository," and follow the instructions.
Authenticating Docker to ECR:
To push images to ECR, you need to authenticate Docker. Use the following AWS CLI command to get the login password and pipe it to docker login
:
Bash
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin <account-id>.dkr.ecr.<region>.amazonaws.com
Replace <account-id>
with your AWS account ID and <region>
with your desired AWS region (e.g., us-east-1
).
Tagging the Image:
Before pushing, you need to tag the image with the ECR repository URI:
Bash
docker tag story-generator:latest <account-id>.dkr.ecr.us-east-1.amazonaws.com/web-projects:latest
Pushing the Image:
Finally, push the image to ECR:
Bash
docker push <account-id>.dkr.ecr.us-east-1.amazonaws.com/web-projects:latest
Defining Infrastructure as Code with Pulumi
Pulumi allows you to define our infrastructure using familiar programming languages. In this case, you are using Python. Your Pulumi project consists of several files:
__main__.py
: The main entry point for our Pulumi program.apprunner.py
: Defines resources and logic related to AWS App Runner.ecr_
instance.py
: Defines resources and logic related to Amazon ECR.Pulumi.dev
.yaml
: Configuration file for the development stack.Pulumi.yaml
: Project metadata file.
Let's break down the key parts of the code:
apprunner.py
Deep Dive:
Python
import functools
from enum import Enum
from typing import Mapping
import pulumi
from pulumi_aws.apprunner import Service, ServiceSourceConfigurationArgs,ServiceInstanceConfigurationArgs,
from pulumi_aws.apprunner.outputs import ServiceSourceConfigurationCodeRepository
config = pulumi.Config()
def manage_instance_configuration(instance_configuration: ServiceInstanceConfigurationArgs):
def decorated(func):
@functools.wraps(func)
def wrapped(*args, **kwargs):
kwargs.update(instance_configuration=instance_configuration)
result = func(*args, **kwargs)
return result
return wrapped
return decorated
def set_service_source_config(access_role_arn: str, auto_deployments_enabled: bool,
image_identifier: str, image_repository_type: str,
start_command: str = None, port: str = None,
runtime_environment_variables: Mapping[str, str] = None,
connection_arn: str = None):
# ... (function body) ...
source_config = set_service_source_config(access_role_arn=ACCESS_ROLE_ARN,
auto_deployments_enabled=True,
image_identifier=IMAGE_IDENTIFIER,
image_repository_type=str(ImageRepositoryType.ECR.value),
port=PORT,
runtime_environment_variables=RUNTIME_ENVIRONMENT)
# ... (other functions and classes) ...
@manage_instance_configuration(instance_configuration=service_instance_configuration)
def create_service(resource_name_: str, service_name_: str, source_configuration_: ServiceSourceConfigurationArgs,
**kwargs):
# ... (function body) ...
set_service_source_config
: This function creates theServiceSourceConfigurationArgs
object, which defines the source of our application code (in this case, an image in ECR). It takes various parameters likeaccess_role_arn
,auto_deployments_enabled
,image_identifier
,image_repository_type
,port
, andruntime_environment_variables
.source_config
: This variable holds the source configuration created usingset_service_source_config
, using values likeACCESS_ROLE_ARN
,IMAGE_IDENTIFIER
,PORT
, andRUNTIME_ENVIRONMENT
defined earlier in the file. It tells App Runner where to find our application code, how to build it, and how to run it.- Inside this function the role of
ServiceSourceConfigurationImageRepositoryArgs
andServiceSourceConfigurationImageRepositoryImageConfigurationArgs
are to configure the image repository and image configuration settings, respectively, for an AWS App Runner service's source configuration. They specify the location of the container image, how App Runner should access it, and any runtime configurations for the image.
- Inside this function the role of
service_instance_configuration
: This variable defines the instance configuration for our App Runner service, including CPU and memory.manage_instance_configuration
: This is a decorator that adds instance configuration settings to thecreate_service
function. The decoratormanage_observability_configuration
add observability configuration to thecreate_service
function.create_service
: This function creates the AWS App Runner service usingpulumi_aws.apprunner.Service
.
ecr_
instance.py
Deep Dive:
Python
import functools
from enum import Enum
from typing import Sequence, Mapping
from pulumi_aws.ecr import Repository
def create_repository(resource_name_: str, name_: str, tags: Mapping[str, str] = None, force_delete: bool = False,
**kwargs):
repository = Repository(resource_name=resource_name_, name=name_, force_delete=force_delete, tags=tags, **kwargs)
return repository
create_repository
: This function creates an ECR repository. The decorators are used to provide default values. If these values are not provided during the function call, the decorators will add them in.
__main__.py
Deep Dive:
Python
import pulumi
import pulumi_aws
from .ecr_instance import create_repository
from .app_runner_instance import create_service, source_config
config = pulumi.Config()
story_generator_project_availability_zone = pulumi_aws.config.region
ecr_result = create_repository(resource_name_='web-projects', name_='web-projects')
app_runner_result = create_service(resource_name_='story-generator', service_name_='story-generatory-dev',
source_configuration_=source_config)
pulumi.export('ecr', ecr_result.repository_url)
pulumi.export('ecr_registry', ecr_result.registry_id)
create_repository(...)
: Calls thecreate_repository
function to create our ECR repository.create_service(...)
: Calls thecreate_service
function to create our App Runner service, passing in thesource_config
.pulumi.export(...)
: These lines export important values from our stack, such as the RDS instance address and the ECR repository URL.config.require_secret
: These lines are used to fetch secret values from the Pulumi configuration. Secrets like database passwords should be stored securely and not hardcoded.
Deploying to AWS App Runner
Now that you have your Pulumi code defined, let's deploy your application to AWS App Runner:
Initializing the Pulumi Stack (if new):
If you haven't already initialized a Pulumi stack, run:
Bash
pulumi stack init dev
Setting AWS Region:
Set the AWS region where you want to deploy your resources:
Bash
pulumi config set aws:region us-east-1
Setting Configuration Values:
Set the required configuration secrets using
pulumi config set --secret
:Bash
pulumi config set --secret CONNECTION_ARN <your_connection_arn> pulumi config set --secret SECRET_KEY <your_secret_key>
Replace the placeholders with your actual values.
Previewing the Deployment:
Preview the changes that will be made to your infrastructure:
Bash
pulumi preview
Deploying the Stack:
Deploy your application and infrastructure:
Bash
pulumi up
Confirm the deployment by typing
yes
when prompted.
Accessing the Deployed Application
Once the deployment is complete, you can find the App Runner service URL in the AWS console:
Go to the AWS App Runner service.
Click on your service name (
story-generator-dev
in this case).You'll find the "Default domain" on the service details page.
Alternatively, you can retrieve it from the Pulumi output if you exported it in your __main__.py
:
pulumi stack output <output_variable_name> # Replace with the name of the output variable if you used pulumi.export
Conclusion
In this article, you successfully deployed your Story Generation App to AWS App Runner using Pulumi to manage our infrastructure as code. By containerizing your application with Docker and using App Runner, you can easily scale and manage your application in the cloud. Pulumi's Infrastructure as Code approach allows you to define your infrastructure in a repeatable and version-controlled manner, making it easier to manage and update your deployments.
Subscribe to my newsletter
Read articles from Iyanuoluwa Ajao directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
