diff --git a/copier.yml b/copier.yml index 9369caa..fa53e42 100644 --- a/copier.yml +++ b/copier.yml @@ -73,8 +73,8 @@ copier__auth: copier__dynamo_db: type: bool - help: "Do you want to enable DynamoDB for your project? (y/n) [default: No]" - default: false + help: "Do you want to enable DynamoDB for your project? (y/n) [default: Yes]" + default: true copier__aws_region: type: str diff --git a/tasks.py b/tasks.py index 9025a0b..2e52bd7 100644 --- a/tasks.py +++ b/tasks.py @@ -20,7 +20,7 @@ def run_setup(): print("Error: AWS SAM CLI is not installed. Please install it and try again.") exit(1) - print("Running AWS SAM build and validate...") + print("Running AWS SAM validate and build...") subprocess.run(shlex.split("make validate")) subprocess.run(shlex.split("make build")) print("AWS Lambda template build and setup complete.") @@ -52,10 +52,11 @@ def log_next_steps(): HINT + "Next steps:" + "\n1. Run 'cd {{ copier__project_name }}' to enter the project directory" - + "\n2. Update the .envrc file with your AWS credentials" - + "\n3. Run 'make setup' to run dynamodb locally and create the database" - + "\n4. Then run 'make build' to build the lambda function" - + "\n5. Then run 'make deploy' to deploy the lambda function" + + "\n2. Run 'make setup' to run dynamodb locally and create the database" + + "\n3. Then run 'make build' to build the lambda function" + + "\n4. To run lambda function locally, run 'make local' to invoke the lambda function" + + "\n5. To deploy, run 'make deploy' to deploy the lambda function" + + "\n6. To remove the local setup, run 'make down'" + "\nRefer to README.md for more details." + TERMINATOR ) diff --git a/{{copier__project_name}}/.envrc b/{{copier__project_name}}/.envrc deleted file mode 100644 index 5b607f6..0000000 --- a/{{copier__project_name}}/.envrc +++ /dev/null @@ -1,3 +0,0 @@ -export AWS_ACCESS_KEY_ID=dummyAccessKeyId -export AWS_SECRET_ACCESS_KEY=dummySecretAccessKey -export AWS_REGION={{copier__aws_region}} diff --git a/{{copier__project_name}}/Makefile b/{{copier__project_name}}/Makefile index 4e5a403..3d4d068 100644 --- a/{{copier__project_name}}/Makefile +++ b/{{copier__project_name}}/Makefile @@ -1,60 +1,61 @@ -.PHONY: build deploy local clean test setup{% if copier__dynamo_db %} dynamo-up dynamo-down create-table{% endif %} +.PHONY: build deploy local clean test setup{% if copier__dynamo_db %} dynamo-up dynamo-down wait-for-dynamodb create-table{% endif %} # Configuration STACK_NAME ?= {{ copier__stack_name }} REGION ?= {{copier__aws_region}} TEMPLATE_FILE ?= template.yaml BUILD_DIR ?= .aws-sam/build +AWS_PROFILE ?= {{copier__aws_profile}} {% if copier__dynamo_db %} DYNAMO_ENDPOINT ?= http://localhost:8000 TABLE_NAME ?= RequestsTable-{{copier__stack_name}} +CONTAINER_NAME ?= {{copier__stack_name}}-dynamodb-local +NETWORK_NAME ?= {{copier__stack_name}}-network {% endif %} -# Default target -all: build +## Build & Deploy -build: - sam build {% if copier__package_type != "image" %}--use-container{% endif %} > /dev/null 2>&1 +build: ## Build the AWS SAM project + @sam build > /dev/null 2>build_error.log && echo "Build succeeded!" || (cat build_error.log && exit 1) + @rm -f build_error.log -validate: +validate: ## Validate the SAM template with linting sam validate --lint -deploy: - sam deploy --guided --profile {{copier__aws_profile}} +deploy: ## Deploy the stack using interactive guided mode + sam deploy --guided --profile $(AWS_PROFILE) \ + {% if copier__package_type == "image" %}--resolve-image-repos{% endif %} -deploy-sandbox: - sam deploy --stack-name $(STACK_NAME)-sandbox \ - --capabilities CAPABILITY_IAM \ - --parameter-overrides StageName=sandbox \ - {% if copier__package_type != "image" %}--resolve-image-repos{% endif %} +## Local Development -local: - sam local start-api {% if copier__dynamo_db %}--env-vars env_example.json --docker-network lambda-local{% endif %} +local: ## Start the local API using SAM + sam local start-api {% if copier__dynamo_db %}--env-vars env_example.json --docker-network $(NETWORK_NAME){% endif %} -invoke: - sam local invoke HelloWorldFunction --event events/event.json{% if copier__dynamo_db %} --env-vars env_example.json --docker-network lambda-local{% endif %} +invoke: ## Invoke a Lambda function locally using an event payload + sam local invoke HelloWorldFunction --event events/event.json{% if copier__dynamo_db %} --env-vars env_example.json --docker-network $(NETWORK_NAME){% endif %} {% if copier__dynamo_db %} -dynamo-up: - @if [ -z "$$(docker network ls --filter name=^lambda-local$$ -q)" ]; then \ - echo "Creating Docker network 'lambda-local'..."; \ - docker network create lambda-local; \ +## DynamoDB (Local) Setup + +dynamo-up: ## Start a local DynamoDB instance in a Docker container + @if [ -z "$$(docker network ls --filter name=^$(NETWORK_NAME)$$ -q)" ]; then \ + echo "Creating Docker network '$(NETWORK_NAME)'..."; \ + docker network create $(NETWORK_NAME); \ else \ - echo "Docker network 'lambda-local' already exists."; \ + echo "Docker network '$(NETWORK_NAME)' already exists."; \ fi; \ - if [ -f .dynamodb-container-id ] && [ -n "$$(docker ps -q -f id=$$(cat .dynamodb-container-id 2>/dev/null))" ]; then \ - echo "DynamoDB local container is already running with ID: $$(cat .dynamodb-container-id)"; \ + if [ -n "$$(docker ps -q -f name=^/$(CONTAINER_NAME)$$)" ]; then \ + echo "DynamoDB local container '$(CONTAINER_NAME)' is already running."; \ else \ echo "Starting DynamoDB local container..."; \ - rm -f .dynamodb-container-id 2>/dev/null || true; \ - docker run -d --rm --network lambda-local -p 8000:8000 --name {{copier__stack_name}}-dynamodb-local amazon/dynamodb-local:latest -jar DynamoDBLocal.jar -sharedDb > .dynamodb-container-id; \ - echo "DynamoDB local container started with ID: $$(cat .dynamodb-container-id)"; \ + docker run -d --rm --network $(NETWORK_NAME) --name $(CONTAINER_NAME) amazon/dynamodb-local:latest -jar DynamoDBLocal.jar -sharedDb; \ + echo "DynamoDB local container '$(CONTAINER_NAME)' started."; \ fi -wait-for-dynamodb: - @echo "Waiting for DynamoDB Local to be ready..." +wait-for-dynamodb: ## Wait until DynamoDB Local is ready inside the container + @echo "Waiting for DynamoDB Local to be ready (inside container)..." @attempts=0; \ - until curl -s http://localhost:8000 >/dev/null; do \ + until docker exec $(CONTAINER_NAME) curl -s http://localhost:8000 >/dev/null 2>&1; do \ if [ $$attempts -ge 10 ]; then \ echo "DynamoDB Local did not become ready in time"; \ exit 1; \ @@ -65,9 +66,13 @@ wait-for-dynamodb: done; \ echo "DynamoDB Local is ready." -create-table: - @# Store the AWS command output in a variable - $(eval AWS_OUTPUT := $(shell aws dynamodb create-table \ +create-table: ## Create the DynamoDB table in the local container + @echo "Creating DynamoDB table '$(TABLE_NAME)' in container '$(CONTAINER_NAME)'..." + @docker run --rm --network $(NETWORK_NAME) \ + -e AWS_ACCESS_KEY_ID=dummy \ + -e AWS_SECRET_ACCESS_KEY=dummy \ + amazon/aws-cli \ + dynamodb create-table \ --table-name $(TABLE_NAME) \ --attribute-definitions \ AttributeName=ip_address,AttributeType=S \ @@ -76,25 +81,70 @@ create-table: AttributeName=ip_address,KeyType=HASH \ AttributeName=timestamp,KeyType=RANGE \ --billing-mode PAY_PER_REQUEST \ - --endpoint-url $(DYNAMO_ENDPOINT))) + --endpoint-url http://$(CONTAINER_NAME):8000 \ + --region $(REGION) + +list-records: ## List all records in the DynamoDB table + @echo "Listing records from DynamoDB table '$(TABLE_NAME)'..." + @docker run --rm --network $(NETWORK_NAME) \ + -e AWS_ACCESS_KEY_ID=dummy \ + -e AWS_SECRET_ACCESS_KEY=dummy \ + amazon/aws-cli \ + dynamodb scan \ + --table-name $(TABLE_NAME) \ + --endpoint-url http://$(CONTAINER_NAME):8000 \ + --region $(REGION) + +count-records: ## Count the number of records in the DynamoDB table + @echo "Counting records in DynamoDB table '$(TABLE_NAME)'..." + @docker run --rm --network $(NETWORK_NAME) \ + -e AWS_ACCESS_KEY_ID=dummy \ + -e AWS_SECRET_ACCESS_KEY=dummy \ + amazon/aws-cli \ + dynamodb scan \ + --table-name $(TABLE_NAME) \ + --select "COUNT" \ + --endpoint-url http://$(CONTAINER_NAME):8000 \ + --region $(REGION) - @# Check if jq exists and use it if available - @if command -v jq > /dev/null 2>&1; then \ - echo '$(AWS_OUTPUT)' | jq; \ - else \ - echo '$(AWS_OUTPUT)'; \ - echo "\033[33mNote: Install jq for colorized JSON output\033[0m"; \ - fi +setup: dynamo-up wait-for-dynamodb create-table ## Set up local DynamoDB environment{% else %} +setup: ## Install Python dependencies + pip3 install -r src/requirements.txt{% endif %} -setup: dynamo-up wait-for-dynamodb create-table{% else %} -setup: - pip install -r requirements.txt{% endif %} +## Testing -test: +test: ## Run Python unit tests using uv and pytest AWS_SAM_STACK_NAME=$(STACK_NAME) PYTHONPATH=. uv run --with pytest,boto3,requests,pytest-mock pytest -v --disable-warnings --tb=short tests -delete-sandbox: - sam delete --stack-name $(STACK_NAME)-sandbox +## Cleanup -delete: +down: ## Stop and remove local DynamoDB container and network + @if [ -n "$$(docker ps -q -f name=^/$(CONTAINER_NAME)$$)" ]; then \ + echo "Stopping DynamoDB local container..."; \ + docker stop $(CONTAINER_NAME); \ + echo "DynamoDB local container '$(CONTAINER_NAME)' stopped."; \ + else \ + echo "DynamoDB local container '$(CONTAINER_NAME)' is not running."; \ + fi; \ + if [ -n "$$(docker network ls --filter name=^$(NETWORK_NAME)$$ -q)" ]; then \ + echo "Removing Docker network '$(NETWORK_NAME)'..."; \ + docker network rm $(NETWORK_NAME); \ + echo "Docker network '$(NETWORK_NAME)' removed."; \ + else \ + echo "Docker network '$(NETWORK_NAME)' does not exist."; \ + fi + +delete: ## Delete the deployed SAM stack sam delete --stack-name $(STACK_NAME) + +## Help + +help: ## Show the list of all the commands and their help text + @awk 'BEGIN { FS = ":.*##"; target="";printf "\nUsage:\n make \033[36m\033[33m\n\nTargets:\033[0m\n" } \ + /^[a-zA-Z_-]+:.*?##/ { if(target=="")print ""; target=$$1; printf " \033[36m%-10s\033[0m %s\n\n", $$1, $$2 } \ + /^([a-zA-Z_-]+):/ {if(target=="")print "";match($$0, "(.*):"); target=substr($$0,RSTART,RLENGTH) } \ + /^\t## (.*)/ { match($$0, "[^\t#:\\\\]+"); txt=substr($$0,RSTART,RLENGTH);printf " \033[36m%-10s\033[0m", target; printf " %s\n", txt ; target=""} \ + /^## .*/ {match($$0, "## (.+)$$"); txt=substr($$0,4,RLENGTH);printf "\n\033[33m%s\033[0m\n", txt ; target=""} \ + ' $(MAKEFILE_LIST) + +.DEFAULT_GOAL := help diff --git a/{{copier__project_name}}/README.md b/{{copier__project_name}}/README.md index 3ff6130..d6bfe99 100644 --- a/{{copier__project_name}}/README.md +++ b/{{copier__project_name}}/README.md @@ -110,6 +110,12 @@ make test ## Cleanup +To delete the local setup (i.e container and local network) run the following command: + +```bash +make down +``` + To delete the sample application that you created, use the AWS CLI. Assuming you used your project name for the stack name, you can run the following: ```bash diff --git a/{{copier__project_name}}/template.yaml b/{{copier__project_name}}/template.yaml index d2f4960..d9d0b4a 100644 --- a/{{copier__project_name}}/template.yaml +++ b/{{copier__project_name}}/template.yaml @@ -60,7 +60,7 @@ Resources: Runtime: python{{ copier__runtime }} {% endif %} Architectures: - - {{copier__architectures}} + - "{{copier__architectures}}" {% if copier__dynamo_db %} Environment: Variables: