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
62 changes: 62 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Deploy on LocalStack

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

permissions:
contents: read

jobs:
example:
name: ${{ matrix.example }}
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
example:
- s3
- cf-s3
- lambda
- api-gateway
- dynamodb
steps:
- name: Checkout
uses: actions/checkout@v4

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

- name: Install lstk and the CDK CLI
run: |
npm install -g @localstack/lstk aws-cdk
lstk setup aws --force --non-interactive

- name: Install dependencies
working-directory: ${{ matrix.example }}
run: npm ci

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

- name: Deploy the example
run: make deploy EXAMPLE=${{ matrix.example }}

- name: Run tests
run: make test EXAMPLE=${{ matrix.example }}

- name: Show LocalStack logs
if: failure()
run: lstk logs | tail -300
15 changes: 12 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
EXAMPLE ?= s3

usage: ## Shows usage for this Makefile
@cat Makefile | grep -E '^[a-zA-Z_-]+:.*?## .*$$' | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}'

install: ## Install dependencies
which lstk || npm install -g @localstack/lstk aws-cdk
which lstk || npm install -g @localstack/lstk
which cdk || npm install -g aws-cdk
cd api-gateway && npm install
cd cf-s3 && npm install
cd dynamodb && npm install
Expand Down Expand Up @@ -30,8 +33,14 @@ deploy-api-gateway: ## Deploy the app to LocalStack
lstk cdk deploy --require-approval=never

deploy-dynamodb: ## Deploy the app to LocalStack
cd api-gateway && \
cd dynamodb && \
lstk cdk bootstrap && \
lstk cdk deploy --require-approval=never

.PHONY: usage install deploy-s3 deploy-cf-s3 deploy-lambda deploy-api-gateway deploy-dynamodb
deploy: ## Deploy one example to LocalStack, e.g. make deploy EXAMPLE=lambda
$(MAKE) deploy-$(EXAMPLE)

test: ## Test a deployed example, e.g. make test EXAMPLE=lambda
./test.sh $(EXAMPLE)

.PHONY: usage install deploy deploy-s3 deploy-cf-s3 deploy-lambda deploy-api-gateway deploy-dynamodb test
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ After the deploy is completed, you will see some outputs in the terminal. For ex
* If you get an error during the bootstrap, this can commonly be caused by either LocalStack or Docker not running. Be sure that you are successfully started LocalStack and Docker before bootstrapping.
* If your deployment fails to load the assets required to run the site (i.e. into S3 and Lambda), be sure that you have successfully added your auth token. When you start up LocalStack in the terminal, you should see a message reading something like `Successfully activated cached license`.

### Testing an Example

With LocalStack running, you can deploy and test any example from the repository root:

```bash
make deploy EXAMPLE=dynamodb
make test EXAMPLE=dynamodb
```

`EXAMPLE` is one of `s3`, `cf-s3`, `lambda`, `api-gateway` or `dynamodb`. The test checks the site is served and, for the examples with a backend, that the backend returns the message the site displays.

### Deploying Remotely to AWS

You will need to have the [AWS CLI](https://github.com/aws/aws-cli/tree/v2?tab=readme-ov-file#installation) installed and configured for your account (you will likely need to set up an IAM user and obtain an access key). Finally, you will need to install the [AWS CDK CLI](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html#getting_started_install).
Expand Down
119 changes: 119 additions & 0 deletions test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/bin/bash
# Check a deployed example serves the demo site and, for the examples with a
# backend, that the backend URL in config.json returns the expected message.
# Usage: ./test.sh s3|cf-s3|lambda|api-gateway|dynamodb
set -euo pipefail

EXAMPLE=${1:?usage: ./test.sh s3|cf-s3|lambda|api-gateway|dynamodb}
case "$EXAMPLE" in
s3) STACK=S3Stack ;;
cf-s3) STACK=CFS3Stack ;;
lambda) STACK=LambdaStack ;;
api-gateway) STACK=APIGatewayStack ;;
dynamodb) STACK=DynamoDBStack ;;
*) echo "Unknown example $EXAMPLE"; exit 1 ;;
esac

SITE_MARKER="AWS for Frontend Developers Demo Site"

output() {
lstk aws cloudformation describe-stacks --stack-name "$STACK" \
--query "Stacks[0].Outputs[?OutputKey=='$1'].OutputValue" --output text
}

# Print the body, or fail with the status and body when the status differs
expect_status() {
local expected=$1 body status
shift
body=$(curl -s -m 30 -w '\n%{http_code}' "$@")
status=${body##*$'\n'}
body=${body%$'\n'*}
if [ "$status" != "$expected" ]; then
echo "Test failed: $* returned HTTP $status, expected $expected: $body" >&2
return 1
fi
echo "$body"
}

if [ "$EXAMPLE" = s3 ]; then
bucket=$(lstk aws cloudformation describe-stack-resources --stack-name "$STACK" \
--query "StackResources[?ResourceType=='AWS::S3::Bucket'].PhysicalResourceId" --output text)
SITE="http://$bucket.s3-website.localhost.localstack.cloud:4566"
else
SITE="http://$(output CloudFrontDistributionURL):4566"
fi
echo "Site: $SITE"

if [[ "$(expect_status 200 "$SITE/")" != *"$SITE_MARKER"* ]]; then
echo "Test failed: $SITE/ did not serve index.html"
exit 1
fi
echo "PASS: the site serves index.html"

if [ "$EXAMPLE" = s3 ]; then
if [[ "$(expect_status 404 "$SITE/missing-page")" != *"Something didn't work"* ]]; then
echo "Test failed: a missing page did not serve error.html"
exit 1
fi
echo "PASS: a missing page returns 404 with error.html"
fi

case "$EXAMPLE" in
s3|cf-s3)
echo "Test passed: $EXAMPLE serves the demo site"
exit 0
;;
lambda)
URL=$(output LambdaFunctionURL)
CONFIG_KEY=function_url
EXPECTED="You successfully called the Lambda from a function URL!"
;;
api-gateway)
URL="$(output APIGatewayURL)hello"
CONFIG_KEY=api_url
EXPECTED="You successfully called the Lambda from API Gateway!"
;;
dynamodb)
URL="$(output APIGatewayURL)hello"
CONFIG_KEY=api_url
EXPECTED="You successfully received data from a DynamoDB table!"
;;
esac

config=$(expect_status 200 "$SITE/config.json")
configured=$(echo "$config" | jq -r ".$CONFIG_KEY")
if [ "$configured" = null ] || [ -z "$configured" ]; then
echo "Test failed: config.json has no $CONFIG_KEY: $config"
exit 1
fi
echo "PASS: config.json has $CONFIG_KEY $configured"

# check_backend <url>: the URL returns the expected message
check_backend() {
local response
response=$(expect_status 200 "$1") || return 1
if ! echo "$response" | jq -e --arg m "$EXPECTED" '.message == $m' > /dev/null 2>&1; then
echo "Test failed: $1 returned '$response', expected message '$EXPECTED'" >&2
return 1
fi
}

check_backend "$URL"
echo "PASS: the stack output URL $URL returned '$EXPECTED'"

# The site calls the configured URL, and for API Gateway falls back to the URL
# without the region segment (see site_assets/main.js)
site_url=$configured
[ "$CONFIG_KEY" = api_url ] && site_url="${configured}hello"
if check_backend "$site_url" 2> /dev/null; then
echo "PASS: the URL the site loads from config.json returned the message"
elif [ "$CONFIG_KEY" = api_url ]; then
echo "Note: $site_url did not respond, trying the site's fallback URL"
fallback="$(echo "$configured" | awk -F. 'BEGIN {OFS="."} {$3=""; sub(/\.\./, "."); print}')hello"
check_backend "$fallback"
echo "PASS: the site's fallback URL $fallback returned the message"
else
check_backend "$site_url"
fi

echo "Test passed: $EXAMPLE serves the demo site and its backend responds"
Loading