<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Paweł Kopaniecki&apos;s Blog</title><description>Pawel Kopaniecki Blog</description><link>https://thepapito.com/</link><item><title>Developing AWS reliant services locally</title><link>https://thepapito.com/blog/undefined/</link><guid isPermaLink="true">https://thepapito.com/blog/undefined/</guid><description>Using AWS SES locally with LocalStack</description><pubDate>Sun, 05 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Today I would like to present LocalStack and how it can be used during local development and testing without the need to use real AWS services.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;What is LocalStack?&lt;/h2&gt;
&lt;p&gt;LocalStack is an open-source tool that provides mimicked AWS services. It can be run locally during the development and testing. It allows to use AWS cloud stack locally and eliminates the need for using real AWS services while developing or testing the application. This allow developers to freely experiment with their applications without a worry about additional AWS usage costs. Detailed information about LocalStack can be found &lt;a href=&quot;https://www.localstack.cloud&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Goal of the article&lt;/h2&gt;
&lt;p&gt;The goal of this project is to create simple FastAPI application, which will be responsible for sending e-mails via SES using user input. Full project can be found &lt;a href=&quot;https://github.com/pawelkopaniecki/python-localstack-ses-example&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Technological stack&lt;/h2&gt;
&lt;p&gt;Technological stack used in this project:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;uv&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Python 3.14&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pydantic&lt;/code&gt; - data validation&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boto3&lt;/code&gt; - AWS SDK&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mypy-boto3-ses&lt;/code&gt; - Types for AWS SES client&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FastAPI&lt;/code&gt; - REST framework&lt;/li&gt;
&lt;li&gt;&lt;code&gt;uvicorn&lt;/code&gt; - ASGI web server implementation&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LocalStack 3.8.1&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Cloning the project&lt;/h2&gt;
&lt;p&gt;Project can be cloned from: &lt;a href=&quot;https://github.com/pawelkopaniecki/python-localstack-ses-example&quot;&gt;https://github.com/pawelkopaniecki/python-localstack-ses-example&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Installing the dependencies&lt;/h2&gt;
&lt;p&gt;Firstly, we need to sync the project. We can do that by typing in the terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv sync
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Preparing the service&lt;/h2&gt;
&lt;p&gt;First step is to init AWS SES client. We will create file called &lt;code&gt;aws.py&lt;/code&gt; and inside initialise new SES client. We will create separate variable &lt;code&gt;localstack_url&lt;/code&gt;, which will determine if we are running application inside Docker or if we do it locally, because it makes a difference in LocalStack connection url. This variable will be added to our docker compose later. Let&apos;s create &lt;code&gt;aws.py&lt;/code&gt; file and put code below there.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import boto3
import os

from mypy_boto3_ses.client import SESClient


ENDPOINT_URL = (
    &quot;http://localstack:4566&quot; if os.getenv(&quot;DOCKER&quot;) else &quot;http://127.0.0.1:4566&quot;
)

client: SESClient = boto3.client(
    service_name=&quot;ses&quot;,
    region_name=&quot;eu-west-1&quot;,
    endpoint_url=ENDPOINT_URL,
    aws_access_key_id=&quot;dummy&quot;,
    aws_secret_access_key=&quot;dummy&quot;,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We have to create method that will be responsible for actually sending e-mails using AWS SES. Let&apos;s add this code to &lt;code&gt;aws.py&lt;/code&gt; file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from mypy_boto3_ses.type_defs import SendEmailResponseTypeDef
from .schemas import EmailSchema


def send_email(email: EmailSchema) -&amp;gt; SendEmailResponseTypeDef:
    &quot;&quot;&quot;Send email using AWS SES.

    Args:
        email (EmailSchema): Email to be sent.

    Returns:
        SendEmailResponseTypeDef: AWS SES email response.
    &quot;&quot;&quot;

    return client.send_email(
        Source=&quot;test@test.test&quot;,
        Destination={&quot;ToAddresses&quot;: [email.address]},
        Message={
            &quot;Subject&quot;: {&quot;Data&quot;: email.subject, &quot;Charset&quot;: &quot;string&quot;},
            &quot;Body&quot;: {&quot;Text&quot;: {&quot;Data&quot;: email.message, &quot;Charset&quot;: &quot;string&quot;}},
        },
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We also need the schema for user e-mail parameters input. We will put it in &lt;code&gt;schemas.py&lt;/code&gt; file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pydantic import BaseModel


class EmailSchema(BaseModel):
    &quot;&quot;&quot;Pydantic model schema for email data validation.

    Attributes:
        address (str): The recipient&apos;s email address
        subject (str): The subject line of the email
        message (str): The main body content of the email
    &quot;&quot;&quot;

    address: str
    subject: str
    message: str
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, we have to create endpoint that will be responsible for sending e-mails based on user input. Let&apos;s put it in &lt;code&gt;main.py&lt;/code&gt; file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from fastapi import FastAPI

from .aws import send_email
from .schemas import EmailSchema

app = FastAPI()


@app.get(&quot;/api/v1/ping&quot;, status_code=200)
async def ping():
    &quot;&quot;&quot;Liveness check endpoint.&quot;&quot;&quot;

    return {&quot;message&quot;: &quot;Email service is running&quot;}


@app.post(&quot;/api/v1/email/send&quot;, status_code=201)
async def email_route(email: EmailSchema):
    &quot;&quot;&quot;Send email endpoint.

    Args:
        email (EmailSchema): Email to be sent.
    &quot;&quot;&quot;

    response = send_email(email)

    return {
        &quot;message&quot;: &quot;Email sent successfully&quot;,
        &quot;message_id&quot;: response[&quot;MessageId&quot;],
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now it is possible to run the FastAPI application using command below while being in the root directory.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uvicorn src.main:app --port 80 --reload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next step would be to containerise our application.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM python:3.12.1-slim-bookworm

WORKDIR /app

COPY ./requirements.txt /app/requirements.txt

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

COPY ./src /app/src

CMD [&quot;uvicorn&quot;, &quot;src.main:app&quot;, &quot;--host&quot;, &quot;0.0.0.0&quot;, &quot;--port&quot;, &quot;80&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the first part is done, but our application won&apos;t work without LocalStack. Let&apos;s fix that!&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Spinning up LocalStack&lt;/h2&gt;
&lt;p&gt;As previously mentioned LocalStack exists in a form of Docker container. By default, LocalStack does not create any resources by itself when spinning up. In order to create AWS services that we want, its necessary to do it by hand when service has spun up or to create an init script, that will create required resources when LocalStack will be starting. I will present how to create init script.&lt;/p&gt;
&lt;h3&gt;Create init script&lt;/h3&gt;
&lt;p&gt;Firstly, script file must be created. Name of this file does not matter, only extension of this file must be &lt;code&gt;.sh&lt;/code&gt;. I named this file &lt;code&gt;init.sh&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Next step is to create AWS profile. This profile will be created inside LocalStack and it will be used to create resources. The same profile can be created on the local machine and added to &lt;code&gt;awscli&lt;/code&gt; configuration, which allows sending requests using &lt;code&gt;awscli&lt;/code&gt; from the local machine to the LocalStack container.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;########### Creating profile ###########&quot;

aws configure set aws_access_key_id &quot;dummy&quot; --profile test-profile
aws configure set aws_secret_access_key &quot;dummy&quot; --profile test-profile
aws configure set region &quot;eu-west-1&quot; --profile test-profile
aws configure set output &quot;table&quot; --profile test-profile
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lastly we must verify identity from which e-mails will be sent.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;########### Verifying identity ###########&quot;

aws ses verify-email-identity \
    --endpoint-url=http://localhost:4566 \
    --region eu-west-1 \
    --profile test-profile \
    --email-address test@test.test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Init script is ready, let&apos;s spin everything up!&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Docker compose&lt;/h2&gt;
&lt;p&gt;Script is ready and service is created. Now it is necessary to create Docker Compose file containing setup for LocalStack and the service.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;version: &quot;3.2&quot;

services:
  localstack:
    image: localstack/localstack:2.3.1
    ports:
      - &quot;4566-4597:4566-4597&quot;
    environment:
      - LOCALSTACK_HOST=localstack
      - SERVICES=ses
      - AWS_ACCESS_KEY_ID=dummy
      - AWS_SECRET_ACCESS_KEY=dummy
      - AWS_DEFAULT_REGION=eu-west-1
      - DOCKER_HOST=unix:///var/run/docker.sock
    volumes:
      - &quot;../resources/init.sh:/etc/localstack/init/ready.d/init.sh&quot;
      - &quot;/var/run/docker.sock:/var/run/docker.sock&quot;
    healthcheck:
      test: [&quot;CMD&quot;, &quot;curl&quot;, &quot;-f&quot;, &quot;http://localhost:4566/_localstack/health&quot;]
      interval: 10s
      timeout: 10s
      retries: 5
      start_period: 10s

  ses:
    build:
      context: ../
      dockerfile: docker/Dockerfile
    ports:
      - 80:80
    environment:
      - DOCKER=True
    healthcheck:
      test: [&quot;CMD&quot;, &quot;curl&quot;, &quot;-f&quot;, &quot;http://localhost:80/api/v1/ping&quot;]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 45s
    depends_on:
      localstack:
        condition: service_healthy
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Running the service&lt;/h2&gt;
&lt;p&gt;Now that everything is ready we can try it!&lt;/p&gt;
&lt;p&gt;Firstly we need to run our docker compose. We can do that by typing when being in root directory in terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker compose -f ./docker/compose.yaml up -d --build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When our services spin up, we can check all e-mails sent by our service using &lt;code&gt;http://localhost:4566/_aws_/ses/&lt;/code&gt; url. It should look exactly like shown on image below if we haven&apos;t sent any e-mail yet.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;messages&quot;: []
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can now test it by sending a POST request to &lt;code&gt;http://localhost:80/api/v1/email/send&lt;/code&gt; with request body:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;address&quot;: &quot;me@mydomain.com&quot;,
  &quot;subject&quot;: &quot;Hello&quot;,
  &quot;message&quot;: &quot;How are you?&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Response should be like below with exception that &lt;code&gt;message_id&lt;/code&gt; will be different every time as it is unique identifier of an AWS SES message.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;message&quot;: &quot;Email sent successfully&quot;,
  &quot;message_id&quot;: &quot;klivnjymbiwvtita-spyvyfrj-eofl-qirf-fdlg-fvbhulaxbygt-dzzacx&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can check if our e-mail was successfully sent by visiting again &lt;code&gt;http://localhost:4566/_localstack/ses/&lt;/code&gt;. We should see our sent e-mail and additional details about it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;messages&quot;: [
    {
      &quot;Id&quot;: &quot;klivnjymbiwvtita-spyvyfrj-eofl-qirf-fdlg-fvbhulaxbygt-dzzacx&quot;,
      &quot;Region&quot;: &quot;eu-west-1&quot;,
      &quot;Destination&quot;: {
        &quot;ToAddresses&quot;: [&quot;me@mydomain.com&quot;]
      },
      &quot;Source&quot;: &quot;test@test.test&quot;,
      &quot;Subject&quot;: &quot;Hello&quot;,
      &quot;Body&quot;: {
        &quot;text_part&quot;: &quot;How are you?&quot;,
        &quot;html_part&quot;: null
      },
      &quot;Timestamp&quot;: &quot;2024-01-14T16:56:56&quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;That&apos;s it! We can now use AWS SES locally and write additional code that will make use of it.&lt;/p&gt;
</content:encoded></item></channel></rss>