Skip to content

Instantly share code, notes, and snippets.

@malthunayan
Created October 25, 2021 15:29
Show Gist options
  • Save malthunayan/dd9923e88bf1b4d29c78d5f4d0141518 to your computer and use it in GitHub Desktop.
Save malthunayan/dd9923e88bf1b4d29c78d5f4d0141518 to your computer and use it in GitHub Desktop.
Deploy FastAPI to AWS Elastic Beanstalk

Deploy FastAPI to AWS Elastic Beanstalk (Zero to Hero)

This guide will go over how to deploy a FastAPI app, add a postgres database, and attach a SSL certificate (assuming you have purchased a custom domain).

Prerequisites

This tutorial will be using pipx to install the Elastic Beanstalk CLI. Follow the instructions here to install AWS's CLI if you do not already have that installed. Then configure it following this guide.

FastAPI project

Assuming that you are using poetry to manage your dependencies, run the following commands in your terminal:

$ poetry new deployment-example
$ cd deployment-example
$ poetry add fastapi "uvicorn[standard]" tortoise-orm pydantic

Our folder structure will look something like this:

.
├── README.rst
├── deployment_example
│   └── __init__.py
├── poetry.lock
├── pyproject.toml
└── tests
    ├── __init__.py
    └── test_deployment_example.py

Note that we are using tortoise-orm as our ORM here, but feel free to use anything else (e.g., sqlmodel, sqlalchemy, etc...). Notice that we installed the standard version of uvicorn that will help us later on during deployment. Create a main.py file inside of deployment_example to start coding our API:

from typing import Any, Optional

from fastapi import Body, FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import (
    AnyHttpUrl,
    BaseSettings as PydanticBaseSettings,
    Field,
    PostgresDsn,
    ValidationError,
)
from tortoise import fields, models
from tortoise.contrib.fastapi import register_tortoise
from tortoise.contrib.pydantic import pydantic_model_creator
from tortoise.exceptions import DoesNotExist


class User(models.Model):
    id = fields.IntField(pk=True)
    name = fields.CharField(max_length=30)


UserIn = pydantic_model_creator(User, exclude_readonly=True)
UserOut = pydantic_model_creator(User)


class BaseSettings(PydanticBaseSettings):
    class Config:
        case_sensitive = True
        env_file = ".env"


class RdsSettings(BaseSettings):
    # These environment variables will be available when deploying our
    # elastic beanstalk
    username: str = Field(..., env="RDS_USERNAME")
    password: str = Field(..., env="RDS_PASSWORD")
    host: str = Field(..., env="RDS_HOSTNAME")
    port: str = Field(..., env="RDS_PORT")
    name: str = Field(..., env="RDS_DB_NAME")

    @property
    def database_uri(self) -> str:
        username, password = self.username, self.password
        host, port = self.host, self.port
        name = self.name
        return f"postgres://{username}:{password}@{host}:{port}/{name}"


class Settings(BaseSettings):
    allow_origins: tuple[AnyHttpUrl, ...] = Field((), env="ALLOW_CORS_ORIGINS")

    debug: bool = Field(False, env=["FASTAPI_DEBUG", "DEBUG"])
    database_uri: Optional[PostgresDsn] = Field(..., env="DATABASE_URI")

    @property
    def orm_config(self) -> dict[str, Any]:
        return dict(
            connections=dict(default=self.database_uri),
            apps=dict(models=dict(models=self.models)),
        )


try:
    rds_settings = RdsSettings()
except ValidationError:
    settings = Settings()
else:
    settings = Settings(database_uri=rds_settings.database_uri)


app = FastAPI()
app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.allow_origins,
    allow_credentials=True,
    allow_methods="*",
    allow_headers="*",
)
register_tortoise(
    app,
    config=settings.orm_config,
    generate_schemas=settings.debug,
    add_exception_handlers=settings.debug,
)


@app.post("/", response_model=UserOut)
async def create_user(user: UserIn) -> UserOut:
    user_db = await User.create(**user.dict())
    return await UserOut.from_tortoise_orm(user_db)


@app.get("/", response_model=list[UserOut])
async def get_users() -> list[UserOut]:
    return await UserOut.from_queryset(User.all())


@app.put("/{user_id}", response_model=UserOut)
async def update_user(
    user_id: int, name: str = Body(..., embed=True)
) -> UserOut:
    try:
        user_db = await User.get(id=user_id)
    except DoesNotExist:
        raise HTTPException(detail="Could not find user", status=404)

    user_db.name = name
    await user_db.save()
    return await UserOut.from_tortoise_orm(user_db)


@app.get("/{user_id}", response_model=UserOut)
async def get_user(user_id: int) -> UserOut:
    try:
        return await UserOut.from_queryset_single(User.get(id=user_id))
    except DoesNotExist:
        raise HTTPException(detail="Could not find user", status=404)

Our Dockerfile would look something like this:

FROM python:3.9


WORKDIR /tmp

RUN pip install poetry

COPY ./pyproject.toml ./poetry.lock* /tmp/

RUN poetry export --without-hashes -f requirements.txt -o requirements.txt


FROM python:3.9


WORKDIR /code

COPY --from=0 /tmp/requirements.txt /code/requirements.txt

RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

COPY . /code

EXPOSE 80

CMD ["uvicorn", "deployment_example.main:app", "--host", "0.0.0.0", "--port", "80"]

Note that with the platform we will be using below named stages are not supported at the time of writing, which is why we have --from=0 and no named stages. We will also need a Procfile to tell AWS where our ASGI is located, which will contain the following line of code: web: uvicorn --host 0.0.0.0 --port 80 deployment_example.main:app. After everything is done, our app structure will look something like this:

.
├── Dockerfile
├── Procfile
├── README.rst
├── deployment_example
│   ├── __init__.py
│   └── main.py
├── poetry.lock
├── pyproject.toml
└── tests
    ├── __init__.py
    └── test_deployment_example.py

Install Elastic Beanstalk

To install the command line, it is pretty straightforward using pipx, just run the following command and pipx will take care of the rest:

$ pipx install awsebcli
Installing...

⚠️ If pipx is not an option and you are in a hurry you could use plain pip and run the following command:

$ pip install -U --user awsebcli
Installing...

Read the issue with installing it that way here: https://github.com/aws/aws-elastic-beanstalk-cli-setup.

Setup Beanstalk

Set up some variables here in your bash shell to ease subsequent commands:

$ profile='default'
$ platform='Docker running on 64bit Amazon Linux 2'
$ region='ap-south-1'
$ secret_key=$(eval openssl rand -hex 32)
$ db_password=$(eval openssl rand -hex 12)
$ echo "DATABASE PASSWORD: $db_password"
DATABASE PASSWORD: XXXXXXXXXXXXXXXXXXXXXXXX

Make sure to copy the database password and keep it safe. Also, note again that we are using a Docker environment, this will make sure our environment runs locally and on production in a reproducible manner. Initialize the beanstalk by running the following command:

$ eb init --profile $profile -r $region -p "$platform" <Application-Name>
Initializing...

Make sure to replace <Application-Name> with your actual application name. You could re-run the command above safely later on by adding an -i flag. To create the beanstalk environment run the following command:

$ eb create \
    --profile $profile \
    -r $region \
    -c <CNAME> \
    -ix 1 \
    --database.engine postgres \
    --database.username postgres \
    --database.password $db_password \
    --database.instance db.t3.micro \
    <environment-name>
Creating...

Let us breakdown the command above. The -r and --profile options are for using the correct region and profile, set initially. The -c flag is for using a custom subdomain when creating this environment, so you will end up with a beanstalk URL that looks something like this: http://<CNAME>.<REGION>.elasticbeanstalk.com, instead of random characters in the <CNAME> section. The -ix 1 option will limit the load balancer to a single instance. We will need a load balancer to simplify setting up SSL with our custom domain. Then we have the database options to customize our database setup. Make sure to replace <environment-name> with your actual environment name. If you have a lot of environment variables it might be easier to set them up in the command line than using the console. For example, that would look something like this:

$ eb setenv \
    foo=bar \
    spam=egg \
    secret=secure
Setting environment variables...

Setup Amazon Route 53

Make sure to set up your custom domain in Amazon Route 53. If the domain was purchased in Amazon Route 53 then skip the following section.

Set up Domain Purchased in 3rd Party Registrar

Go to Amazon Route 53's console in your region, and click on Create hosted zone: Create hosted zone

Then proceed to add your domain name (e.g., example.com), and create the hosted zone. Afterwards, you should two records already created. One of them will be SOA and the other will be NS (a.k.a. nameserver). Copy the values from the nameserver record, and go to your registrar (e.g., GoDaddy, BlueHost, etc...), and paste those nameserver values one-by-one. You might receive a warning that changing your nameserver will cause some down time. If this domain is being used already on production, you want to be very careful about migrating. Otherwise if that is not the case and this is a newly purchased domain then there is nothing to worry about.

Point custom domain to Elastic Beanstalk Environment

Follow the steps below to have your custom domain serve your beanstalk app:

  1. Inside the hosted zone, create a record.
  2. Add a subdomain if you would like.
  3. Select Simple Routing Policy as the routing policy.
  4. Select Routes traffic to an IPv4 address... as the type.
  5. Toggle on Alias.
  6. Choose the endpoint as an Alias to Elastic Environment.
  7. Select your beanstalk's region.
  8. Select your elastic beanstalk environment.

Create alias record

Obtaining SSL Certificate

Go to AWS Certificate Manager and Request a certificate:

  1. Request certificate
  2. Create request
  3. After creating the request, go to the certificate.
  4. Under the domains table click on Create records in Route 53, and wait for the validation to complete.

Activate HTTPs on Elastic Beanstalk

  1. Go to your Elastic Beanstalk Environment.
  2. Go to the Configurations page.
  3. Click on Edit on the Load Balancer section.
  4. Add a Listener.
  5. Make sure the Listener Port is 443, the Listener Protocol is HTTPS, the Instance Port is 80, and the Instance Protocol is 80 (notice that the instance and listener protocols are different).
  6. Add the certificate you just created.
  7. Click on Apply on the bottom of the page once done.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment