From fd943ed2702f3519df9db41cb7a41e613b15a8c1 Mon Sep 17 00:00:00 2001 From: Harsh Mishra Date: Tue, 13 Oct 2026 11:22:58 +0530 Subject: [PATCH 1/2] Add CI that tests the dog API through both RDS proxies Set DB_PORT for put-dog, raise Lambda timeouts for Java cold starts, make db-setup re-runnable, and add make build/init-db/test. Claude-Session: https://claude.ai/code/session_01DZfDmzUBPqNAu3yFKyfCNM --- .github/workflows/main.yml | 63 +++++++++++++++ Makefile | 11 ++- README.md | 28 ++++--- .../localstack/initdb/InitDBHandler.java | 6 +- terraform/main.tf | 11 ++- terraform/outputs.tf | 8 ++ test.sh | 78 +++++++++++++++++++ 7 files changed, 190 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/main.yml create mode 100755 test.sh diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..83a5d15 --- /dev/null +++ b/.github/workflows/main.yml @@ -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 diff --git a/Makefile b/Makefile index 6c7aeb5..740a11b 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index 96df1ae..f2f8f39 100644 --- a/README.md +++ b/README.md @@ -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.* @@ -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 @@ -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. ``` @@ -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 diff --git a/api-lambdas/db-setup-lambda/src/main/java/cloud/localstack/initdb/InitDBHandler.java b/api-lambdas/db-setup-lambda/src/main/java/cloud/localstack/initdb/InitDBHandler.java index cf434bc..f7cc69b 100644 --- a/api-lambdas/db-setup-lambda/src/main/java/cloud/localstack/initdb/InitDBHandler.java +++ b/api-lambdas/db-setup-lambda/src/main/java/cloud/localstack/initdb/InitDBHandler.java @@ -36,8 +36,10 @@ public Map handleRequest(Map 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"); diff --git a/terraform/main.tf b/terraform/main.tf index b5df0e2..6f84d6c 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -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 = [ @@ -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 { @@ -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 { @@ -471,6 +473,7 @@ resource "aws_lambda_function" "put_dog" { environment { variables = { HOST = aws_db_proxy.dogdb_iam_proxy.endpoint + DB_PORT = try(regex(":([0-9]+)$", aws_db_proxy.dogdb_iam_proxy.endpoint)[0], "5432") DATABASE_NAME = var.db_name DB_USER = var.iam_username AWS_REGION = var.aws_region @@ -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 = [ @@ -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 = [ diff --git a/terraform/outputs.tf b/terraform/outputs.tf index f858394..bd1ce7b 100644 --- a/terraform/outputs.tf +++ b/terraform/outputs.tf @@ -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 +} diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..61c594a --- /dev/null +++ b/test.sh @@ -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" ]; 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 +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" From 8f1553f4862f106280a5e7b22f3fb1744c20dc3b Mon Sep 17 00:00:00 2001 From: Harsh Mishra Date: Tue, 13 Oct 2026 11:28:30 +0530 Subject: [PATCH 2/2] Connect to the proxy on the database port LocalStack returns the proxy endpoint without a port, so JDBC fell back to 5432. Pass the cluster port, which matches the proxy port on AWS, and use it in the put-dog JDBC URL. Claude-Session: https://claude.ai/code/session_01DZfDmzUBPqNAu3yFKyfCNM --- .../main/java/cloud/localstack/putdog/DatabaseUtil.java | 4 ++-- terraform/main.tf | 8 ++++---- test.sh | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/api-lambdas/put-dog-lambda/src/main/java/cloud/localstack/putdog/DatabaseUtil.java b/api-lambdas/put-dog-lambda/src/main/java/cloud/localstack/putdog/DatabaseUtil.java index a6ed30a..563a1db 100644 --- a/api-lambdas/put-dog-lambda/src/main/java/cloud/localstack/putdog/DatabaseUtil.java +++ b/api-lambdas/put-dog-lambda/src/main/java/cloud/localstack/putdog/DatabaseUtil.java @@ -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(); diff --git a/terraform/main.tf b/terraform/main.tf index 6f84d6c..0cd4421 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -445,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 @@ -473,7 +473,7 @@ resource "aws_lambda_function" "put_dog" { environment { variables = { HOST = aws_db_proxy.dogdb_iam_proxy.endpoint - DB_PORT = try(regex(":([0-9]+)$", aws_db_proxy.dogdb_iam_proxy.endpoint)[0], "5432") + DB_PORT = aws_rds_cluster.dogdb.port DATABASE_NAME = var.db_name DB_USER = var.iam_username AWS_REGION = var.aws_region @@ -500,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 @@ -602,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 } } diff --git a/test.sh b/test.sh index 61c594a..7cfc8bb 100755 --- a/test.sh +++ b/test.sh @@ -12,7 +12,7 @@ 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" ]; then + 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