Skip to content

Commit

Permalink
Merge pull request #7 from Real-Life-IaC/add-gh-actions
Browse files Browse the repository at this point in the history
Merge add-gh-actions
  • Loading branch information
andresionek91 authored Feb 18, 2024
2 parents b06b7f4 + a346192 commit 931c404
Show file tree
Hide file tree
Showing 14 changed files with 354 additions and 1,204 deletions.
7 changes: 0 additions & 7 deletions app/Dockerfile.dynamo-admin

This file was deleted.

6 changes: 2 additions & 4 deletions app/Dockerfile.local
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ FROM python:3.11

ENV POETRY_VIRTUALENVS_CREATE=false
ENV PATH="/root/.local/bin:$PATH"
WORKDIR /home/app/

# Install python poetry
RUN curl -sSL https://install.python-poetry.org | python3 -
Expand All @@ -12,9 +13,6 @@ COPY pyproject.toml poetry.lock /
# Extract dependencies from poetry.lock and install them with pip
RUN poetry install

# Copy only the relevant function code to the lambda
COPY api api

EXPOSE 5000

CMD [ "poetry", "run", "uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "5001"]
CMD poetry run uvicorn api.main:app --host "0.0.0.0" --port 5001 --reload
43 changes: 31 additions & 12 deletions app/api/db.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os

import boto3
import botocore

from aws_lambda_powertools import Logger
from mypy_boto3_dynamodb import DynamoDBServiceResource
Expand All @@ -12,18 +13,36 @@
def initialize_db() -> DynamoDBServiceResource:
"""Initialize DynamoDB."""

if os.getenv("DYNAMO_ENDPOINT"):
logger.info("Running locally. Initializing DynamoDB locally.")
ddb = boto3.resource(
service_name="dynamodb",
endpoint_url=os.getenv("DYNAMO_ENDPOINT"),
region_name="local", # nosec
aws_access_key_id="local", # nosec
aws_secret_access_key="local", # nosec
)
else:
logger.info("Running on AWS. Initializing DynamoDB on AWS.")
ddb = boto3.resource(service_name="dynamodb")
logger.info("Initializing DynamoDB.")
ddb = boto3.resource(
service_name="dynamodb",
endpoint_url=os.getenv("LOCALSTACK_ENDPOINT"),
)
if os.getenv("LOCALSTACK_ENDPOINT"):
try:
logger.info("Creating table 'downloads' in DynamoDB.")
ddb.create_table(
TableName="downloads",
KeySchema=[
{"AttributeName": "order_id", "KeyType": "HASH"},
{"AttributeName": "email", "KeyType": "RANGE"},
],
AttributeDefinitions=[
{"AttributeName": "order_id", "AttributeType": "S"},
{"AttributeName": "email", "AttributeType": "S"},
],
ProvisionedThroughput={
"ReadCapacityUnits": 2,
"WriteCapacityUnits": 2,
},
)
except botocore.exceptions.ClientError as e:
if e.response["Error"]["Code"] == "ResourceInUseException":
logger.info(
"Table 'downloads' already exists in DynamoDB."
)
else:
raise

logger.info("DynamoDB initialized.")

Expand Down
21 changes: 21 additions & 0 deletions app/api/eventbridge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import os

import boto3

from aws_lambda_powertools import Logger
from mypy_boto3_events import EventBridgeClient


logger = Logger()


def initialize_eventbridge() -> EventBridgeClient:
"""Initialize EventBridge."""

logger.info("Initializing EventBridge.")
event_client: EventBridgeClient = boto3.client(
"events",
endpoint_url=os.getenv("LOCALSTACK_ENDPOINT"),
) # type: ignore

return event_client
4 changes: 3 additions & 1 deletion app/api/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Any

from api.db import initialize_db
from api.eventbridge import initialize_eventbridge
from api.models.downloads import DownloadsDomain
from api.repositories.downloads import DownloadsRepository
from api.routes.downloads import DownloadsRouter
Expand All @@ -22,7 +23,8 @@
)

db = initialize_db()
downloads_repository = DownloadsRepository(db)
events = initialize_eventbridge()
downloads_repository = DownloadsRepository(db=db, events=events)
downloads_domain = DownloadsDomain(downloads_repository)
downloads_router = DownloadsRouter(downloads_domain)

Expand Down
15 changes: 11 additions & 4 deletions app/api/models/downloads.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,17 @@ class DownloadsModel(InputDownloadsModel):
)

created_at: datetime | str = Field(
default_factory=lambda: datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S.%f"),
default_factory=lambda: datetime.utcnow().strftime(
"%Y-%m-%d %H:%M:%S.%f"
),
title="The creation date of the download",
description="The creation date of the download",
)

updated_at: datetime | str = Field(
default_factory=lambda: datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S.%f"),
default_factory=lambda: datetime.utcnow().strftime(
"%Y-%m-%d %H:%M:%S.%f"
),
title="The update date of the download",
description="The update date of the download",
)
Expand Down Expand Up @@ -77,5 +81,8 @@ def get_count(self) -> int:
def create_download(self, data: InputDownloadsModel) -> DownloadsModel:
"""Create a download"""
data_model = DownloadsModel(**data.model_dump())
self.__repository.create_download(data_model.model_dump())
return data_model
response = self.__repository.create_download(
data_model.model_dump()
)
response_model = DownloadsModel(**response)
return response_model
37 changes: 33 additions & 4 deletions app/api/repositories/downloads.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import json
import os

from aws_lambda_powertools import Logger
from aws_lambda_powertools import Tracer
from mypy_boto3_dynamodb import DynamoDBServiceResource
from mypy_boto3_events import EventBridgeClient


TABLE_NAME = os.getenv("TABLE_NAME", "downloads")
Expand All @@ -14,8 +16,11 @@
class DownloadsRepository:
"""Repository for downloads."""

def __init__(self, db: DynamoDBServiceResource) -> None:
def __init__(
self, db: DynamoDBServiceResource, events: EventBridgeClient
) -> None:
self.__db = db
self.__events = events

@tracer.capture_method(capture_response=False)
def get_count(self) -> int:
Expand All @@ -36,7 +41,31 @@ def get_count(self) -> int:
@tracer.capture_method(capture_response=False)
def create_download(self, data: dict) -> dict:
"""Create a download in the database."""
logger.info("Creating download.")
table = self.__db.Table(TABLE_NAME)
response = table.put_item(Item=data)
return response

# Check if the download already exists
logger.info("Checking if download already exists.")
existing_download = table.get_item(
Key={"order_id": data["order_id"], "email": data["email"]},
)

if existing_download.get("Item"):
logger.info("Download already exists.")
return existing_download.get("Item")
else:
logger.info("Download does not exist. Creating download.")
table.put_item(Item=data)

self.__events.put_events(
Entries=[
{
"Source": "restApi",
"EventBusName": os.getenv(
"EVENT_BUS_NAME", "default"
),
"DetailType": "downloadCreated",
"Detail": json.dumps(data),
}
]
)
return data
44 changes: 23 additions & 21 deletions app/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,36 @@
version: '3.8'

services:
db:
image: amazon/dynamodb-local
restart: unless-stopped
localstack:
image: localstack/localstack
ports:
- 8000:8000
- "127.0.0.1:4566:4566" # LocalStack Gateway
- "127.0.0.1:4510-4559:4510-4559" # external services port range
environment:
AWS_DEFAULT_REGION: us-east-2
AWS_ACCESS_KEY_ID: test
AWS_SECRET_ACCESS_KEY: test
DEBUG: ${DEBUG:-1}
DOCKER_HOST: unix:///var/run/docker.sock
LS_LOG: WARN
HOSTNAME: localstack
HOSTNAME_EXTERNAL: localstack
volumes:
- "${TMPDIR:-/tmp}/localstack:/var/lib/localstack"
- "/var/run/docker.sock:/var/run/docker.sock"
app:
container_name: app
build:
context: .
dockerfile: Dockerfile.local
volumes:
- "./:/app"
- ".:/home/app"
ports:
- 5001:5001
environment:
- DYNAMO_ENDPOINT=http://db:8000
depends_on:
- db
admin:
build:
context: .
dockerfile: Dockerfile.dynamo-admin
ports:
- 8002:8002
environment:
- DYNAMO_ENDPOINT=http://db:8000
- AWS_REGION=local
- AWS_ACCESS_KEY_ID=local
- AWS_SECRET_ACCESS_KEY=local
- PORT=8002
- LOCALSTACK_ENDPOINT=http://localstack:4566
- AWS_ACCESS_KEY_ID=test
- AWS_SECRET_ACCESS_KEY=test
- AWS_DEFAULT_REGION=us-east-2
depends_on:
- db
- localstack
492 changes: 34 additions & 458 deletions app/poetry.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion app/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ fastapi = "^0.104.1"
uvicorn = {version = "^0.24.0.post1", extras = ["standard"]}
mangum = "^0.17.0"
boto3 = "^1.33.6"
boto3-stubs = {extras = ["secretsmanager"], version = "^1.33.6"}
pydantic = {extras = ["email"], version = "^2.6.1"}
mypy-boto3-dynamodb = "^1.34.34"
mypy-boto3-events = "^1.34.17"
32 changes: 16 additions & 16 deletions infra/constructs/b1/lambda_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ def __init__(
parameter_name="/platform/stage",
)

event_bus = events.EventBus.from_event_bus_arn(
scope=self,
id="EventBus",
event_bus_arn=ssm.StringParameter.value_for_string_parameter(
scope=self,
parameter_name="/pubsub/event-bus/arn",
),
)

certificate = acm.Certificate.from_certificate_arn(
scope=self,
id="Certificate",
Expand Down Expand Up @@ -111,11 +120,11 @@ def __init__(
id="Downloads",
billing=dynamodb.Billing.on_demand(),
partition_key=dynamodb.Attribute(
name="id",
name="order_id",
type=dynamodb.AttributeType.STRING,
),
sort_key=dynamodb.Attribute(
name="created_at",
name="email",
type=dynamodb.AttributeType.STRING,
),
)
Expand All @@ -141,11 +150,16 @@ def __init__(
environment_encryption=kms_key,
environment={
"TABLE_NAME": self.table.table_name,
"EVENT_BUS_NAME": event_bus.event_bus_name,
},
)

# Allow lambda to read and write to the DynamoDB table
self.table.grant_read_write_data(self.function)

# Allow lambda to publish to the pubsub event bus
event_bus.grant_put_events_to(self.function)

# Trigger lambda every 4 minutes to keep it warm
event_rule = events.Rule(
scope=self,
Expand Down Expand Up @@ -187,20 +201,6 @@ def __init__(
),
)

# Add endpoints
downloads = self.rest_api.root.add_resource("downloads")
downloads.add_method("POST", api_key_required=False)

downloads_count = self.rest_api.root.add_resource(
"downloads:count"
)
downloads_count.add_method("GET", api_key_required=False)

docs = self.rest_api.root.add_resource("docs")
docs.add_method("GET", api_key_required=False)
openapi = self.rest_api.root.add_resource("openapi.json")
openapi.add_method("GET", api_key_required=False)

route53.ARecord(
scope=self,
id="ARecord",
Expand Down
Loading

0 comments on commit 931c404

Please sign in to comment.