From cba23b31dd95364ba8b86a37ada746ced0aa8cb5 Mon Sep 17 00:00:00 2001
From: Pavel Mikhalkevich <pavel@mikhalkevich.com>
Date: Mon, 30 Dec 2024 16:49:12 +0400
Subject: [PATCH] Add API Gateway Local setup

---
 CONTRIBUTING.md              | 47 +++++++++++++++++++++++++
 server/.gitignore            |  1 +
 server/run-dynamodb-local.sh | 23 ++++++++++---
 server/samconfig.toml        | 40 +++++++++++++++++++++
 server/src/main.rs           | 33 +++++++++++++-----
 server/template.yaml         | 67 ++++++++++++++++++++++++++++++++++++
 6 files changed, 199 insertions(+), 12 deletions(-)
 create mode 100644 server/samconfig.toml
 create mode 100644 server/template.yaml

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ff634de..60ef94a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,6 +2,8 @@ Hello there!
 
 So, you want to help improve the site — great!
 
+### Setup
+
 Local setup is fairly straightforward:
 
 1. Run the server (you'll need [Rust](https://www.rust-lang.org/)):
@@ -31,6 +33,8 @@ It will also auto-generate user votes over time for the questions there.
 If you're curious about the technologies used in the server and client,
 see their respective `README.md` files.
 
+### DynamoDB Local
+
 To run tests against a DynamoDB instance running [locally](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html), make sure
 you got [`docker`](https://docs.docker.com/engine/install/) and
 [`AWS CLI`](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html#getting-started-install-instructions) installed, then hit:
@@ -55,3 +59,46 @@ your local DynamoDB instance, hit:
 ```sh
 USE_DYNAMODB=local cargo run
 ```
+
+### API Gateway Local
+
+Prerequisites:
+
+- [Cargo Lambda](https://www.cargo-lambda.info/guide/installation.html#binary-releases)
+- [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html)
+- DynamoDB Local [container](#backend-with-dynamodb-local)
+
+To build and launch the application as a `Lambda` function behind `API Gateway` locally, `cd` to the server
+directory, and hit:
+
+```sh
+sam build
+sam local start-api
+```
+
+Once you make changes to the back-end code, open a separate terminal window and rebuild the app with:
+
+```sh
+sam build
+```
+
+The `sam local` process we've lauched previously will then pick up the new binary from `./server/.aws-sam` directory.
+
+Here is how our `API Gateway Local` plus `DynamoDB Local` setup look like:
+
+```sh
+ ______________________________                                  _______________________________________________
+|           Browser            |                                |       Docker Network: wewerewondering         |
+|   _______________________    |     _______________________    |     __________________________________        |
+|  |                       |   |    | API Gateway Proxy     |   |    | WeWereWondering Server Container |       |
+|  | WeWereWodering Client |-- |--> | http://localhost:3000 | --|--> | ports: SAM assigns dynamically   | --|   |
+|  | http://localhost:5173 |   |    |_______________________|   |    |__________________________________|   |   |
+|  |_______________________|   |                                |                                           |   |
+|   _______________________    |                                |                                           |   |
+|  |                       |   |                                |     _____________________________         |   |
+|  | DynamoDB Admin Client |---|--------------------------------|--> | DynamoDB Local Container    |        |   |
+|  | http://localhost:8001 |   |                                |    | ports: 127.0.0.1:8000:8000  |        |   |
+|  |_______________________|   |                                |    | host: dynamodb-local        | <------|   |
+|                              |                                |    |_____________________________|            |
+|______________________________|                                |_______________________________________________|
+```
diff --git a/server/.gitignore b/server/.gitignore
index a0bcd69..7d104a1 100644
--- a/server/.gitignore
+++ b/server/.gitignore
@@ -3,3 +3,4 @@
 /dynamodb-local
 dynamodb_local_latest.tar.gz*
 Makefile
+.aws-sam
\ No newline at end of file
diff --git a/server/run-dynamodb-local.sh b/server/run-dynamodb-local.sh
index feeca1d..7313f98 100755
--- a/server/run-dynamodb-local.sh
+++ b/server/run-dynamodb-local.sh
@@ -21,10 +21,13 @@ export AWS_ACCESS_KEY_ID=lorem
 export AWS_SECRET_ACCESS_KEY=ipsum
 export AWS_DEFAULT_REGION=us-east-1
 
+DYNAMODB_NETWORK_NAME=wewerewondering
 DYNAMODB_CONTAINER_NAME=dynamodb-local
 DYNAMODB_ADMIN_CONTAINER_NAME=dynamodb-admin
 DYNAMODB_HOST=127.0.0.1
 DYNAMODB_PORT=8000
+DYNAMODB_ADMIN_HOST=127.0.0.1
+DYNAMODB_ADMIN_PORT=8001
 ENDPOINT_URL=http://${DYNAMODB_HOST}:${DYNAMODB_PORT}
 
 docker ps | grep ${DYNAMODB_CONTAINER_NAME} >/dev/null &&
@@ -34,11 +37,17 @@ echo "🖴 Preparing volumes for DynamoDB..."
 rm -rf dynamodb-data
 mkdir dynamodb-data
 
+if docker network inspect ${DYNAMODB_NETWORK_NAME} 2>&1 >/dev/null; then
+    echo "🚫 Network ${DYNAMODB_NETWORK_NAME} already exists, re-using..."
+else
+    docker network create ${DYNAMODB_NETWORK_NAME}
+fi
+
 echo "🚀 Spinning up a container with DynamoDB..."
 (
     docker run --rm -d -v ./dynamodb-data:/home/dynamodblocal/data -p ${DYNAMODB_HOST}:${DYNAMODB_PORT}:8000 \
-        -w /home/dynamodblocal --name ${DYNAMODB_CONTAINER_NAME} amazon/dynamodb-local:latest \
-        -jar DynamoDBLocal.jar -sharedDb -dbPath ./data
+        -w /home/dynamodblocal --name ${DYNAMODB_CONTAINER_NAME} --network ${DYNAMODB_NETWORK_NAME} \
+        amazon/dynamodb-local:latest -jar DynamoDBLocal.jar -sharedDb -dbPath ./data
 ) >/dev/null
 
 while ! (aws dynamodb list-tables --endpoint-url ${ENDPOINT_URL} >/dev/null); do
@@ -54,7 +63,13 @@ docker ps | grep ${DYNAMODB_ADMIN_CONTAINER_NAME} >/dev/null &&
     exit 0
 
 echo "🚀 Spinning up a container with DynamoDB Admin..."
-(docker run -d --rm --net host --name ${DYNAMODB_ADMIN_CONTAINER_NAME} aaronshaf/dynamodb-admin) >/dev/null
-echo "🔎 DynamoDB Admin is available at http://localhost:8001"
+(
+    docker run -d --rm -p ${DYNAMODB_ADMIN_HOST}:${DYNAMODB_ADMIN_PORT}:8001 \
+        --name ${DYNAMODB_ADMIN_CONTAINER_NAME} \
+        --network ${DYNAMODB_NETWORK_NAME} \
+        -e DYNAMO_ENDPOINT=http://${DYNAMODB_CONTAINER_NAME}:8000 \
+        aaronshaf/dynamodb-admin
+) >/dev/null
+echo "🔎 DynamoDB Admin is available at http://${DYNAMODB_ADMIN_HOST}:${DYNAMODB_ADMIN_PORT}"
 
 echo "✅ Done!"
diff --git a/server/samconfig.toml b/server/samconfig.toml
new file mode 100644
index 0000000..895d2d5
--- /dev/null
+++ b/server/samconfig.toml
@@ -0,0 +1,40 @@
+# NB! We are only using SAM to run the application as a Lambda function behind
+# the API Gateway locally. We are actually using Terraform to describe and deploy
+# the infrastructure (see `infra` directory in the project's root with IaC files).
+
+# More information about the configuration file can be found here:
+# https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html
+version = 0.1
+stack_name = "wewerewondering"
+
+[default.build.parameters]
+# Cargo Lambda is supported as a beta feature in SAM:
+# https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/building-rust.html
+beta_features = true
+cached = true
+parallel = true
+
+[default.validate.parameters]
+lint = true
+
+[default.deploy.parameters]
+capabilities = "CAPABILITY_IAM"
+confirm_changeset = true
+resolve_s3 = true
+
+[default.package.parameters]
+resolve_s3 = true
+
+[default.sync.parameters]
+beta_features = true
+watch = true
+
+[default.local_start_api.parameters]
+warm_containers = "EAGER"
+# The command `sam local start-api` will be launch Lambda Runtime in a docker
+# container and so we need to make sure the DynamoDB Local is in the same network
+# (see `DYNAMODB_NETWORK_NAME` in `./run-dynamodb-local.sh`)
+docker_network = "wewerewondering"
+
+[default.local_start_lambda.parameters]
+warm_containers = "EAGER"
diff --git a/server/src/main.rs b/server/src/main.rs
index d8ac3ca..202c387 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -40,9 +40,10 @@ impl Backend {
 
     /// Instantiate a DynamoDB backend.
     ///  
-    /// If `USE_DYNAMODB` is set to "local", the `AWS_ENDPOINT_URL` will be set
-    /// to "http://localhost:8000", the `AWS_DEFAULT_REGION` will be "us-east-1",
-    /// and the [test credentials](https://docs.rs/aws-config/latest/aws_config/struct.ConfigLoader.html#method.test_credentials)
+    /// If `USE_DYNAMODB` is set to "local", the `AWS_ENDPOINT_URL` will be taken
+    /// from the environment with the "http://localhost:8000" fallback , the `AWS_DEFAULT_REGION`
+    /// will be pulled from the environment as well and will default to "us-east-1",
+    /// as for the credentials - the [test credentials](https://docs.rs/aws-config/latest/aws_config/struct.ConfigLoader.html#method.test_credentials)
     /// will be used to sign requests.
     ///
     /// This spares setting those environment variables (including `AWS_ACCESS_KEY_ID`
@@ -55,18 +56,34 @@ impl Backend {
     /// ```sh
     /// USE_DYNAMODB=local cargo t -- --include-ignored
     /// ```
+    /// 
+    /// This also allows us to use the local instance of DynamoDB which is running
+    /// in a container on the same network, in which case the database will be accessible
+    /// under `http://<dynamodb_container_name>:<port>`. This facilitates the setup of
+    /// local API Gateway with SAM, since the `sam local start-api` command will launch our
+    /// back-end app in a docker container.
     ///
-    /// If customization is needed, set `USE_DYNAMODB` to e.g. "custom", and
-    /// set the evironment variables to whatever values you need or let them be
-    /// picked up from your `~/.aws` files (see [`aws_config::load_from_env`](https://docs.rs/aws-config/latest/aws_config/fn.load_from_env.html))
+    /// If more customization is needed (say, you want to set some specific credentials
+    /// rather than rely on those test creds generated by the `aws_config` crate),
+    /// set `USE_DYNAMODB` to e.g. "custom", and set the environment variables to whatever
+    /// values you need or let them be picked up from your `~/.aws` files
+    /// (see [`aws_config::load_from_env`](https://docs.rs/aws-config/latest/aws_config/fn.load_from_env.html))
     async fn dynamo() -> Self {
         let config = if std::env::var("USE_DYNAMODB")
             .ok()
             .is_some_and(|v| v == "local")
         {
             aws_config::from_env()
-                .endpoint_url("http://localhost:8000")
-                .region("us-east-1")
+                .endpoint_url(
+                    std::env::var("AWS_ENDPOINT_URL")
+                        .ok()
+                        .unwrap_or("http://localhost:8000".into()),
+                )
+                .region(aws_config::Region::new(
+                    std::env::var("AWS_DEFAULT_REGION")
+                        .ok()
+                        .unwrap_or("us-east-1".into()),
+                ))
                 .test_credentials()
                 .load()
                 .await
diff --git a/server/template.yaml b/server/template.yaml
new file mode 100644
index 0000000..4c70bdd
--- /dev/null
+++ b/server/template.yaml
@@ -0,0 +1,67 @@
+# NB! We are only using SAM to run the application as a Lambda function behind
+# the API Gateway locally. We are actually using Terraform to describe and deploy
+# the infrastructure (see `infra` directory in the project's root with IaC files, 
+# specifically `infra/apigw.tf` and `infra/lambda.tf`).
+
+AWSTemplateFormatVersion: '2010-09-09'
+Transform: AWS::Serverless-2016-10-31
+Description: Sample SAM Template for running and tesing WeWereWondering locally
+Resources:
+  WeWereWonderingApi:
+    Type: AWS::Serverless::Function
+    Metadata:
+      BuildMethod: rust-cargolambda 
+    Properties:
+      CodeUri: .
+      Handler: bootstrap
+      Runtime: provided.al2
+      Architectures:
+      - x86_64
+      Timeout: 29
+      MemorySize: 512
+      Events:
+        FetchEvent:
+          Type: HttpApi
+          Properties:
+            Path: /api/event/{eid}
+            Method: get
+        CreateEvent:
+          Type: HttpApi
+          Properties:
+            Path: /api/event
+            Method: post
+        AskQuestion:
+          Type: HttpApi
+          Properties:
+            Path: /api/event/{eid}
+            Method: post
+        FetchAllUnhiddenQuestionsForEvent:
+          Type: HttpApi
+          Properties:
+            Path: /api/event/{eid}/questions
+            Method: get
+        FetchFetchAllQuestionsForEventAllQuestionsForEvent:
+          Type: HttpApi
+          Properties:
+            Path: /api/event/{eid}/questions/{secret}
+            Method: get
+        ToggleQuestionProperty:
+          Type: HttpApi
+          Properties:
+            Path: /api/event/{eid}/questions/{secret}/{qid}/toggle/{property}
+            Method: post
+        UpvoteDownvoteQuestion:
+          Type: HttpApi
+          Properties:
+            Path: /api/vote/{qid}/{updown}
+            Method: post
+        FetchQuestions:
+          Type: HttpApi
+          Properties:
+            Path: /api/questions/{qids}
+            Method: get
+      Environment:
+        Variables:
+          RUST_LOG: debug
+          USE_DYNAMODB: local
+          AWS_ENDPOINT_URL: http://dynamodb-local:8000