Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Deploy on LocalStack

on:
push:
paths-ignore:
- '**.md'
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:

jobs:
deploy:
name: Deploy the dog API with Terraform
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
cache: maven

- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_wrapper: false

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22

- name: Install lstk
run: |
npm install -g @localstack/lstk
lstk setup aws --force --non-interactive

- name: Build the Lambda functions
run: make build

- name: Start LocalStack
env:
LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }}
run: lstk start

- name: Deploy the infrastructure
run: make deploy

- name: Run tests
run: make test

- name: Show LocalStack logs
if: failure()
run: |
(cd terraform && lstk tf output) || true
lstk logs | tail -300
11 changes: 10 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,16 @@ stop: ## Stop LocalStack
logs: ## Save the logs in a separate file
@lstk logs > logs.txt

build: ## Build the Lambda functions
cd api-lambdas && mvn -B clean package

deploy: ## Deploy infrastructure using Terraform
cd terraform && lstk tf init && lstk tf apply --auto-approve

.PHONY: usage install start stop logs deploy
init-db: ## Create the dogs table and database users
lstk aws lambda invoke --function-name db-setup --cli-read-timeout 120 output.json && cat output.json

test: ## Initialize the database and test the API end to end
./test.sh

.PHONY: usage install start stop logs build deploy init-db test
28 changes: 18 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ make start
Ensure you have Java 21 and Maven installed. Then, build the Lambda functions:

```sh
cd api-lambdas
mvn clean package
make build
```
*Enjoy some elevator music while the build completes.*

Expand All @@ -52,20 +51,28 @@ Ensure you have Java 21 and Maven installed. Then, build the Lambda functions:

3. **Deploy the infrastructure:**

Navigate to the `terraform` directory and apply the Terraform configuration:
Apply the Terraform configuration in `terraform/`:

```sh
cd terraform
lstk tf init
lstk tf apply --auto-approve
```
make deploy
```

4. **Initialize the database:**

Invoke the `db-setup` Lambda function to create the `dogs` table in the RDS PostgreSQL database:

```sh
lstk aws lambda invoke --function-name db-setup --region us-east-1 output.json
make init-db
```

The function is safe to invoke again.

5. **Test the API:**

`make test` initializes the database, then creates, reads, updates and deletes a dog through the API. It covers both proxies: POST and DELETE use the secret auth proxy, PUT uses an IAM auth token with the IAM proxy, and GET uses the RDS Data API.

```sh
make test
```

## Usage
Expand All @@ -87,12 +94,13 @@ The Lambda functions use the following environment variables:
- `DATABASE_NAME`: Name of the database.
- `DB_CLUSTER_ARN`: ARN of the RDS cluster.
- `HOST`: Endpoint of the RDS proxy.
- `DB_PORT`: Port of the IAM auth RDS proxy, used to sign the IAM auth token.
- `AWS_REGION`: AWS region.

**Note:**
If any of these environment variables are not set, please run `lstk tf apply --auto-approve`.
This can happen when the Lambda functions are deployed before the cluster is ready.
You should see the following, whivh indicates that the environment variables have been set:
You should see the following, which indicates that the environment variables have been set:
```sh
Apply complete! Resources: 0 added, 6 changed, 0 destroyed.
```
Expand All @@ -102,7 +110,7 @@ You should see the following, whivh indicates that the environment variables hav
To clean up the resources created by Terraform, run:

```sh
terraform destroy
cd terraform && lstk tf destroy --auto-approve
```

## Testing endpoints
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ public Map<String, Object> handleRequest(Map<String, Object> input, Context cont
"category VARCHAR(50))";
stmt.executeUpdate(createTableQuery);

// Create user token_user
stmt.executeUpdate("CREATE USER token_user");
// Create user token_user, unless a previous run already did
stmt.executeUpdate("DO $$ BEGIN " +
"IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'token_user') THEN CREATE USER token_user; END IF; " +
"END $$");

// Grant roles to token_user
stmt.executeUpdate("GRANT rds_iam TO token_user");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ public static Connection getConnectionWithIamAuth(String username, String host,

String authToken = generateAuthToken(host, port, region, username);
System.out.println("Auth token: " + authToken);
String jdbcUrl = String.format("jdbc:postgresql://%s/%s?sslmode=require",
host, databaseName);
String jdbcUrl = String.format("jdbc:postgresql://%s:%d/%s?sslmode=require",
host, port, databaseName);

try {
Properties properties = new Properties();
Expand Down
17 changes: 12 additions & 5 deletions terraform/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,8 @@ resource "aws_lambda_function" "get_dog" {
role = aws_iam_role.lambda_role.arn
handler = "cloud.localstack.getdog.GetDogHandler"
filename = "../api-lambdas/get-dog-lambda/target/get-dog-lambda-1.0.0.jar"
timeout = 30
memory_size = 512

vpc_config {
subnet_ids = [
Expand All @@ -430,7 +432,7 @@ resource "aws_lambda_function" "post_dog" {
role = aws_iam_role.lambda_role.arn
handler = "cloud.localstack.postdog.AddDogHandler"
filename = "../api-lambdas/post-dog-lambda/target/post-dog-lambda-1.0.0.jar"
timeout = 15
timeout = 30
memory_size = 512

vpc_config {
Expand All @@ -443,7 +445,7 @@ resource "aws_lambda_function" "post_dog" {

environment {
variables = {
HOST = aws_db_proxy.dogdb_secret_proxy.endpoint
HOST = "${aws_db_proxy.dogdb_secret_proxy.endpoint}:${aws_rds_cluster.dogdb.port}"
DATABASE_NAME = var.db_name
DB_USER = var.db_username
USER_PASSWORD = var.db_password
Expand All @@ -457,7 +459,7 @@ resource "aws_lambda_function" "put_dog" {
role = aws_iam_role.lambda_role.arn
handler = "cloud.localstack.putdog.UpdateDogHandler"
filename = "../api-lambdas/put-dog-lambda/target/put-dog-lambda-1.0.0.jar"
timeout = 15
timeout = 30
memory_size = 512

vpc_config {
Expand All @@ -471,6 +473,7 @@ resource "aws_lambda_function" "put_dog" {
environment {
variables = {
HOST = aws_db_proxy.dogdb_iam_proxy.endpoint
DB_PORT = aws_rds_cluster.dogdb.port
DATABASE_NAME = var.db_name
DB_USER = var.iam_username
AWS_REGION = var.aws_region
Expand All @@ -484,6 +487,8 @@ resource "aws_lambda_function" "delete_dog" {
role = aws_iam_role.lambda_role.arn
handler = "cloud.localstack.deletedog.DeleteDogHandler"
filename = "../api-lambdas/delete-dog-lambda/target/delete-dog-lambda-1.0.0.jar"
timeout = 30
memory_size = 512

vpc_config {
subnet_ids = [
Expand All @@ -495,7 +500,7 @@ resource "aws_lambda_function" "delete_dog" {

environment {
variables = {
HOST = aws_db_proxy.dogdb_secret_proxy.endpoint
HOST = "${aws_db_proxy.dogdb_secret_proxy.endpoint}:${aws_rds_cluster.dogdb.port}"
SECRET_ARN = aws_secretsmanager_secret.super_secret.arn
AWS_REGION = var.aws_region
DATABASE_NAME = var.db_name
Expand Down Expand Up @@ -582,6 +587,8 @@ resource "aws_lambda_function" "db-setup" {
role = aws_iam_role.lambda_role.arn
handler = "cloud.localstack.initdb.InitDBHandler"
filename = "../api-lambdas/db-setup-lambda/target/db-setup-lambda-1.0.0.jar"
timeout = 30
memory_size = 512

vpc_config {
subnet_ids = [
Expand All @@ -595,7 +602,7 @@ resource "aws_lambda_function" "db-setup" {
variables = {
AWS_REGION = var.aws_region
DB_SECRET_ARN = aws_secretsmanager_secret.super_secret.arn
RDS_PROXY_ENDPOINT = aws_db_proxy.dogdb_secret_proxy.endpoint
RDS_PROXY_ENDPOINT = "${aws_db_proxy.dogdb_secret_proxy.endpoint}:${aws_rds_cluster.dogdb.port}"
DB_NAME = var.db_name
}
}
Expand Down
8 changes: 8 additions & 0 deletions terraform/outputs.tf
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
output "api_gateway_url" {
value = aws_apigatewayv2_api.dog_api.api_endpoint
}

output "secret_proxy_endpoint" {
value = aws_db_proxy.dogdb_secret_proxy.endpoint
}

output "iam_proxy_endpoint" {
value = aws_db_proxy.dogdb_iam_proxy.endpoint
}
78 changes: 78 additions & 0 deletions test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/bin/bash
# Initialize the database, then walk a dog through the API:
# POST (secret auth through the RDS proxy), GET (RDS Data API),
# PUT (IAM auth token through the IAM proxy), DELETE (secret from Secrets Manager).
set -euo pipefail

TF_DIR=${TF_DIR:-terraform}
NAME="ci-dog-$(date +%s)"
OUT=$(mktemp)
trap 'rm -f "$OUT"' EXIT

for fn in post-dog put-dog delete-dog db-setup; do
host=$(lstk aws lambda get-function-configuration --function-name "$fn" \
--query 'Environment.Variables.HOST || Environment.Variables.RDS_PROXY_ENDPOINT' --output text)
if [ -z "$host" ] || [ "$host" = "None" ] || [[ "$host" == :* ]]; then
echo "Test failed: $fn has no proxy endpoint set. Run 'lstk tf apply --auto-approve' again."
exit 1
fi
done

lstk aws lambda invoke --function-name db-setup --cli-read-timeout 120 "$OUT" > /dev/null
if ! jq -e '.statusCode == 200' "$OUT" > /dev/null; then
echo "Test failed: db-setup returned $(cat "$OUT")"
exit 1
fi
echo "PASS: db-setup created the dogs table"

API="$(cd "$TF_DIR" && lstk tf output -raw api_gateway_url)/dev/dogs"
echo "API: $API"

# request <expected status> <expected body substring> <curl args...>
request() {
local status=$1 substring=$2 response actual
shift 2
response=$(curl -s -m 60 -w '\n%{http_code}' -H 'Content-Type: application/json' "$@")
actual=${response##*$'\n'}
response=${response%$'\n'*}
if [ "$actual" != "$status" ] || [[ "$response" != *"$substring"* ]]; then
echo "Test failed: $* returned HTTP $actual '$response', expected $status with '$substring'" >&2
return 1
fi
echo "$response"
}

request 200 "Dog added successfully!" -X POST "$API" \
-d "{\"name\": \"$NAME\", \"age\": \"1\", \"category\": \"Pupper\"}" > /dev/null
echo "PASS: POST added $NAME through the secret auth proxy"

CLUSTER_ARN=$(lstk aws rds describe-db-clusters --db-cluster-identifier dogdb-cluster \
--query 'DBClusters[0].DBClusterArn' --output text)
SECRET_ARN=$(lstk aws secretsmanager describe-secret --secret-id super-secret --query ARN --output text)
ID=$(lstk aws rds-data execute-statement --resource-arn "$CLUSTER_ARN" --secret-arn "$SECRET_ARN" \
--database dogs --sql "SELECT id FROM dogs WHERE name = :name" \
--parameters "[{\"name\": \"name\", \"value\": {\"stringValue\": \"$NAME\"}}]" \
--query 'records[0][0].longValue' --output text)
echo "Dog id: $ID"

dog=$(request 200 "$NAME" "$API/$ID")
if ! echo "$dog" | jq -e --arg n "$NAME" '.name == $n and .age == 1 and .category == "Pupper"' > /dev/null; then
echo "Test failed: GET returned $dog"
exit 1
fi
echo "PASS: GET returned the dog through the RDS Data API"

request 200 "Dog updated successfully!" -X PUT "$API" \
-d "{\"id\": \"$ID\", \"name\": \"$NAME\", \"age\": \"2\", \"category\": \"Woofer\"}" > /dev/null
dog=$(request 200 "$NAME" "$API/$ID")
if ! echo "$dog" | jq -e '.age == 2 and .category == "Woofer"' > /dev/null; then
echo "Test failed: GET after PUT returned $dog"
exit 1
fi
echo "PASS: PUT updated the dog through the IAM auth proxy"

request 200 "Dog deleted successfully!" -X DELETE "$API/$ID" > /dev/null
request 404 "Dog not found" "$API/$ID" > /dev/null
echo "PASS: DELETE removed the dog"

echo "Test passed: the dog API works through both RDS proxies"
Loading