New boilerplate for an openapi stack running via AWS Lambda Function

This commit is contained in:
Brian Lee 2024-12-11 15:17:45 -08:00
commit a0c1e85867
28 changed files with 1197 additions and 0 deletions

251
.gitignore vendored Normal file
View File

@ -0,0 +1,251 @@
# Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### OSX ###
*.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### PyCharm ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# CMake
cmake-build-debug/
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Ruby plugin and RubyMine
/.rakeTasks
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
### PyCharm Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
# Sonarlint plugin
.idea/sonarlint
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
.pytest_cache/
nosetests.xml
coverage.xml
*.cover
.hypothesis/
# Translations
*.mo
*.pot
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule.*
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history
### Windows ###
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
# Build folder
*/build/*
# End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode
.aws-sam
*.log
archive/
layer/opt/deno
.tmp_deno_dir
.terraform
*.tfstate*

5
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"recommendations": [
"denoland.vscode-deno"
]
}

8
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"deno.enable": true,
"deno.lint": true,
"editor.formatOnSave": true,
"[typescript]": {
"editor.defaultFormatter": "denoland.vscode-deno"
}
}

127
README.md Normal file
View File

@ -0,0 +1,127 @@
# Hello OpenAPI
This repo contains boilerplate code for building AWS Lambda functions using Docker. See [notes/decisions.md](notes/decisions.md) for stack decisions.
Sample containers are provided:
* [hello-deno](containers/hello-deno/) - simplest example lambda function using Deno
* [hello-openapi](containers/hello-openapi/) - opinionated OpenAPI stack
## IDE Note
In VSCode, import module resolution can be handled by the `denoland.vscode-deno`. They suggest keeping it disabled globally and only enabling it per workspace.
![enable deno extension in workspace](notes/ide-deno-extension.png)
## Build Docker Image
Build and test the docker image locally.
```sh
export CONTAINER_NAME=hello-openapi
docker build -t $CONTAINER_NAME .
```
(Optional) Run the container locally.
```sh
docker run -p 8000:8000 $CONTAINER_NAME:latest
curl http://localhost:8000
docker stop $(docker ps -q)
```
## Deployment
Overview:
1. Create the ECR repository.
2. Push the container image to the ECR repository.
3. Deploy the lambda function.
### Create the ECR repository
You'll first need to authenticate with AWS.
```sh
export AWS_REGION=us-west-1
export AWS_PROFILE=playground
aws configure sso --profile $AWS_PROFILE
```
With Terraform:
```sh
cd terraform
terraform init
terraform apply -target=aws_ecr_repository.hello_docker
export REPOSITORY_URI=$(terraform output -raw ecr_repository_url)
aws ecr get-login-password --region $AWS_REGION --profile $AWS_PROFILE | docker login --username AWS --password-stdin $REPOSITORY_URI
docker tag $CONTAINER_NAME:latest $REPOSITORY_URI:latest
docker push $REPOSITORY_URI:latest
```
With aws-sam-cli:
```sh
cd sam
sam build
sam deploy --guided --profile playground --region us-west-1 --parameter-overrides DeployECROnly=true
export REPOSITORY_URI=$(aws ecr describe-repositories --repository-names $CONTAINER_NAME --region $AWS_REGION --profile $AWS_PROFILE | jq -r '.repositories[0].repositoryUri')
aws ecr get-login-password --region $AWS_REGION --profile $AWS_PROFILE | docker login --username AWS --password-stdin $REPOSITORY_URI
docker tag $CONTAINER_NAME:latest $REPOSITORY_URI:latest
docker push $REPOSITORY_URI:latest
```
In either case, we must build the ECR repository before deploying the lambda function. For sam, the `DeployECROnly` parameter is used to control this behavior for sam. You can set it to `false` in your [samconfig.toml](sam/samconfig.toml) after the first deploy. Thereafter you can use `--no-confirm-changeset` instead of `--guided`. For terraform, simply omit the `-target` parameter to build the full stack.
### Deploy the lambda function
With Terraform:
```sh
terraform apply
```
With aws-sam-cli:
```sh
sam deploy --guided --profile playground --region us-west-1 --parameter-overrides DeployECROnly=false --resolve-image-repos
```
## Tips
Get the function url for the lambda function
```sh
export FUNCTION_NAME=
aws lambda get-function-url-config --function-name $FUNCTION_NAME --query 'FunctionUrl' --output text
```
Check the image uri for a lambda function
```sh
aws lambda get-function --function-name $FUNCTION_NAME --query 'Code.ImageUri' --output text
```
Review the images in an ECR repository
```sh
export REPOSITORY_NAME=hello-world
aws ecr describe-images --repository-name $REPOSITORY_NAME --region $AWS_REGION --profile $AWS_PROFILE
```
## Logs
With Terraform:
```sh
export FUNCTION_NAME=hello_docker
aws logs tail /aws/lambda/$FUNCTION_NAME --since 1h --profile $AWS_PROFILE --region $AWS_REGION
aws logs tail /aws/lambda/$FUNCTION_NAME --follow --profile $AWS_PROFILE --region $AWS_REGION
```
With aws-sam-cli:
```sh
sam logs -n HelloWorldFunction --profile playground --tail
```

View File

@ -0,0 +1,19 @@
# Set up the base image
FROM public.ecr.aws/awsguru/aws-lambda-adapter:0.8.4 AS aws-lambda-adapter
FROM denoland/deno:bin-1.45.2 AS deno_bin
FROM debian:bookworm-20230703-slim AS deno_runtime
COPY --from=aws-lambda-adapter /lambda-adapter /opt/extensions/lambda-adapter
COPY --from=deno_bin /deno /usr/local/bin/deno
ENV PORT=8000
EXPOSE 8000
RUN mkdir /var/deno_dir
ENV DENO_DIR=/var/deno_dir
# Copy the function code
WORKDIR "/var/task"
COPY . /var/task
# Warmup caches
RUN timeout 10s deno run -A main.ts || [ $? -eq 124 ] || exit 1
CMD ["deno", "run", "-A", "main.ts"]

View File

@ -0,0 +1,43 @@
# Guide
## Deploy Lambda Container Image
Create the ECR (Elastic Container Registry) repository and authenticate docker to it.
```sh
export AWS_REGION=us-west-1
export AWS_PROFILE=playground
aws configure sso --profile $AWS_PROFILE
aws ecr create-repository --repository-name hello-world --region $AWS_REGION --profile $AWS_PROFILE
export REPOSITORY_URI=$(aws ecr describe-repositories --repository-names hello-world --region $AWS_REGION --profile $AWS_PROFILE | jq -r '.repositories[0].repositoryUri')
aws ecr get-login-password --region $AWS_REGION --profile $AWS_PROFILE | docker login --username AWS --password-stdin $REPOSITORY_URI
```
Build the docker image and push it to the ECR repository.
```sh
docker build -t hello-world .
docker tag hello-world:latest $REPOSITORY_URI:latest
docker push $REPOSITORY_URI:latest
```
### Testing
Test the container image locally.
```sh
docker run -p 8000:8000 hello-world:latest
```
Check it in another terminal tab.
```sh
curl http://localhost:8000
docker stop $(docker ps -q)
```
Cleanup the repo.
```sh
aws ecr delete-repository --repository-name hello-world --region $AWS_REGION --profile $AWS_PROFILE --force
```

View File

View File

@ -0,0 +1,2 @@
// https://docs.deno.com/examples/aws_lambda_tutorial/
Deno.serve((req) => new Response("Hello World!"));

View File

@ -0,0 +1,27 @@
# Set up the base image
# https://gallery.ecr.aws/awsguru/aws-lambda-adapter
FROM public.ecr.aws/awsguru/aws-lambda-adapter:0.8.4@sha256:e2653f741cd15851ba4f13f3cc47d29f2d14377c7d11737bfa272baa1b569007 AS aws-lambda-adapter
# https://hub.docker.com/r/denoland/deno
# https://github.com/denoland/deno_docker
FROM docker.io/denoland/deno:bin@sha256:49206ee7e411bba0ac80047814ce9d6ceaf4eabe36f8cde7445839b91f8f5df4 AS deno_bin
# https://hub.docker.com/_/debian
FROM docker.io/library/debian:bookworm-slim@sha256:1537a6a1cbc4b4fd401da800ee9480207e7dc1f23560c21259f681db56768f63 AS deno_runtime
COPY --from=aws-lambda-adapter /lambda-adapter /opt/extensions/lambda-adapter
COPY --from=deno_bin /deno /usr/local/bin/deno
ENV PORT=8000
EXPOSE 8000
RUN mkdir /var/deno_dir
ENV DENO_DIR=/var/deno_dir
# Copy the function code
WORKDIR "/var/task"
COPY . /var/task
# Warmup caches
RUN timeout 10s deno run -A main.ts || [ $? -eq 124 ] || exit 1
CMD ["deno", "run", "-A", "main.ts"]

View File

@ -0,0 +1,47 @@
# Guide
Build the docker image and run it locally. Or run the deno server directly:
Test the server locally
```sh
deno task start
```
Then open [http://localhost:8000/reference](http://localhost:8000/reference) in your browser.
Test an authenticated endpoint:
```sh
curl -H 'Authorization: Bearer hunter2' 127.0.0.1:8000/users/123
```
Update the Dockerfile with the latest SHA256 digests.
```sh
./update_dockerfile.sh hello-openapi
```
Build the docker image.
```sh
docker build -t hello-openapi .
```
Test the container image locally.
```sh
docker run -p 8000:8000 hello-openapi:latest
```
Stop it from another terminal tab.
```sh
docker stop $(docker ps -q)
```
Lock the updated image digests in the Dockerfile.
```sh
./lock_dockerfile.sh hello-openapi
```

View File

@ -0,0 +1,5 @@
{
"tasks": {
"start": "deno run --allow-net --watch main.ts"
}
}

View File

@ -0,0 +1,78 @@
{
"version": "4",
"specifiers": {
"npm:@hono/zod-openapi@*": "0.18.3_hono@4.6.13_zod@3.23.8",
"npm:@scalar/hono-api-reference@*": "0.5.163_hono@4.6.13",
"npm:hono@*": "4.6.13"
},
"npm": {
"@asteasolutions/zod-to-openapi@7.3.0_zod@3.23.8": {
"integrity": "sha512-7tE/r1gXwMIvGnXVUdIqUhCU1RevEFC4Jk6Bussa0fk1ecbnnINkZzj1EOAJyE/M3AI25DnHT/zKQL1/FPFi8Q==",
"dependencies": [
"openapi3-ts",
"zod"
]
},
"@hono/zod-openapi@0.18.3_hono@4.6.13_zod@3.23.8": {
"integrity": "sha512-bNlRDODnp7P9Fs13ZPajEOt13G0XwXKfKRHMEFCphQsFiD1Y+twzHaglpNAhNcflzR1DQwHY92ZS06b4LTPbIQ==",
"dependencies": [
"@asteasolutions/zod-to-openapi",
"@hono/zod-validator",
"hono",
"zod"
]
},
"@hono/zod-validator@0.4.1_hono@4.6.13_zod@3.23.8": {
"integrity": "sha512-I8LyfeJfvVmC5hPjZ2Iij7RjexlgSBT7QJudZ4JvNPLxn0JQ3sqclz2zydlwISAnw21D2n4LQ0nfZdoiv9fQQA==",
"dependencies": [
"hono",
"zod"
]
},
"@scalar/hono-api-reference@0.5.163_hono@4.6.13": {
"integrity": "sha512-kIZRMSIBT1Ac0PjWLuoIERZYTk0ZTp+B/zfK13lOjTQ8cgPnau34hsAT8Bydr9hOWENbmEPOumMBFJ1F81+PKw==",
"dependencies": [
"@scalar/types",
"hono"
]
},
"@scalar/openapi-types@0.1.5": {
"integrity": "sha512-6geH9ehvQ/sG/xUyy3e0lyOw3BaY5s6nn22wHjEJhcobdmWyFER0O6m7AU0ZN4QTjle/gYvFJOjj552l/rsNSw=="
},
"@scalar/types@0.0.23": {
"integrity": "sha512-dOvQig4hyeVw1kXIo9MQAnM9tUt9vCOZs3zOe6oSqOUG8xY7+WXioirlRCsc+wcQegMbuNYOlNBXCDugOP1YJA==",
"dependencies": [
"@scalar/openapi-types",
"@unhead/schema"
]
},
"@unhead/schema@1.11.13": {
"integrity": "sha512-fIpQx6GCpl99l4qJXsPqkXxO7suMccuLADbhaMSkeXnVEi4ZIle+l+Ri0z+GHAEpJj17FMaQdO5n9FMSOMUxkw==",
"dependencies": [
"hookable",
"zhead"
]
},
"hono@4.6.13": {
"integrity": "sha512-haV0gaMdSjy9URCRN9hxBPlqHa7fMm/T72kAImIxvw4eQLbNz1rgjN4hHElLJSieDiNuiIAXC//cC6YGz2KCbg=="
},
"hookable@5.5.3": {
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="
},
"openapi3-ts@4.4.0": {
"integrity": "sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==",
"dependencies": [
"yaml"
]
},
"yaml@2.6.1": {
"integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg=="
},
"zhead@2.2.4": {
"integrity": "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="
},
"zod@3.23.8": {
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="
}
}
}

View File

@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Function to get SHA256 digest
get_digest() {
docker pull $1 > /dev/null
digest=$(docker inspect --format='{{index .RepoDigests 0}}' $1 | cut -d'@' -f2)
echo $digest
}
# Array of images to process
declare -A images
images["public.ecr.aws/awsguru/aws-lambda-adapter:0.8.4"]="aws-lambda-adapter"
images["docker.io/denoland/deno:bin"]="deno_bin"
images["docker.io/library/debian:bookworm-slim"]="deno_runtime"
# Process each image
for image in "${!images[@]}"; do
alias=${images[$image]}
digest=$(get_digest $image)
echo "Image: $image"
echo "Alias: $alias"
echo "Digest: $digest"
echo
# Update Dockerfile
sed -i "s|FROM $image AS $alias|FROM $image@$digest AS $alias|g" Dockerfile
done
echo "Dockerfile updated with SHA256 digests."

View File

@ -0,0 +1,107 @@
/* Define our schemas */
// fun fact: the @ means zod-openapi is a scoped package instead of a submodule of hono
import { z } from "npm:@hono/zod-openapi";
import { bearerAuth } from "npm:hono/bearer-auth";
const ParamsSchema = z.object({
id: z
.string()
.min(3)
.openapi({
param: {
name: "id",
in: "path",
},
example: "1212121",
}),
});
const UserSchema = z
.object({
id: z.string().openapi({
example: "123",
}),
name: z.string().openapi({
example: "John Doe",
}),
age: z.number().openapi({
example: 42,
}),
})
.openapi("User");
/* Create a route */
import { createRoute } from "npm:@hono/zod-openapi";
const route = createRoute({
method: "get",
security: [
{
Bearer: [],
},
],
path: "/users/{id}",
request: {
params: ParamsSchema,
},
responses: {
200: {
content: {
"application/json": {
schema: UserSchema,
},
},
description: "Retrieve the user",
},
},
});
/* App setup */
import { OpenAPIHono } from "npm:@hono/zod-openapi";
const app = new OpenAPIHono();
// https://github.com/honojs/middleware/tree/main/packages/zod-openapi#how-to-setup-authorization
// https://hono.dev/docs/middleware/builtin/bearer-auth
const token = "hunter2";
app.use("/users/*", bearerAuth({ token }));
app.openAPIRegistry.registerComponent("securitySchemes", "Bearer", {
type: "http",
scheme: "bearer",
});
app.openapi(route, (c) => {
const { id } = c.req.valid("param");
return c.json({
id,
age: 21,
name: "pleb",
});
});
// OpenAPI Spec
app.doc("/openapi.json", {
openapi: "3.0.0",
info: {
version: "1.0.0",
title: "My API",
},
});
/* OpenAPI Reference */
import { apiReference } from "npm:@scalar/hono-api-reference";
app.get(
"/reference",
apiReference({
pageTitle: "Scalar/Hono API Reference",
theme: "elysiajs",
spec: { url: "/openapi.json" },
}),
);
Deno.serve(app.fetch);

View File

@ -0,0 +1,30 @@
#!/bin/bash
# Function to get latest SHA256 digest
get_latest_digest() {
image_name=$(echo $1 | cut -d'@' -f1)
docker pull $image_name > /dev/null
digest=$(docker inspect --format='{{index .RepoDigests 0}}' $image_name | cut -d'@' -f2)
echo $digest
}
# Update Dockerfile
update_dockerfile() {
old_line=$1
new_digest=$2
new_line=$(echo $old_line | sed "s|@sha256:[a-f0-9]*|@$new_digest|")
sed -i "s|$old_line|$new_line|" Dockerfile
echo "Updated: $new_line"
}
# Process Dockerfile
while IFS= read -r line
do
if [[ $line == FROM* && $line == *@sha256:* ]]; then
image=$(echo $line | awk '{print $2}')
new_digest=$(get_latest_digest $image)
update_dockerfile "$line" "$new_digest"
fi
done < Dockerfile
echo "Dockerfile updated with latest SHA256 digests."

14
notes/decisions.md Normal file
View File

@ -0,0 +1,14 @@
# Architecture Decision Record
A powerful stack for building APIs that is flexible, performant, and easy to reason about.
* Cloud: AWS - status quo for QOS (we're not locked into it)
* CI/CD: Gitlab - multi-cloud, versatile, FOSS (ENA is already using it)
* Deployment: Terraform - declarative, IaC, multi-cloud
* Containers: Docker - consistency between environments, rootless in dev
* TypeScript: type-safety, anyone with JS experience can contribute (ServiceNow uses JS)
* Runtime: [Deno](https://deno.com/) - secure-by-default, URL-based module resolution, dependency locking, built-in toolchain
* API Layer: [hono](https://hono.dev/) - portable, fast, simple, automatic OpenAPI w/ [@hono/zod-openapi](https://hono.dev/examples/zod-openapi)
* OpenAPI UI: [scalar](https://scalar.com/)/[hono-api-reference](https://github.com/scalar/scalar/blob/main/packages/hono-api-reference/README.md) - offline-first, code generation
The strong alternative would be Python with FastAPI, which might be more suitable for smaller projects.

52
notes/docker.md Normal file
View File

@ -0,0 +1,52 @@
# Build Docker Image
Requirements:
* sam, aws-cli, docker
## Docker Container
Push a docker image to the ECR repository.
```sh
export AWS_REGION=us-west-1
export AWS_PROFILE=playground
aws configure sso --profile $AWS_PROFILE
export REPOSITORY_URI=$(aws ecr describe-repositories --repository-names hello-world --region $AWS_REGION --profile $AWS_PROFILE | jq -r '.repositories[0].repositoryUri')
aws ecr get-login-password --region $AWS_REGION --profile $AWS_PROFILE | docker login --username AWS --password-stdin $REPOSITORY_URI
```
Build the docker image and push it to the ECR repository.
```sh
docker build -t hello-world .
docker tag hello-world:latest $REPOSITORY_URI:latest
docker push $REPOSITORY_URI:latest
```
### Testing
Test the container image locally.
```sh
docker run -p 8000:8000 hello-world:latest
```
Check it in another terminal tab.
```sh
curl http://localhost:8000
docker stop $(docker ps -q)
```
Cleanup the repo.
```sh
aws ecr delete-repository --repository-name hello-world --region $AWS_REGION --profile $AWS_PROFILE --force
```
## Resources
* [deno lambda guide](https://docs.deno.com/runtime/tutorials/aws_lambda/)
* (Deprecated) Deno on AWS Lambda: [Manual lambda packaging guide](https://github.com/denoland/deno-lambda/tree/f735a1a4f0a4a57a348f8af88d6cf30ddc0a5f2e)

View File

@ -0,0 +1,7 @@
```
deno task start
```
```
curl 'localhost:8000/posts/9?page=7'
```

View File

@ -0,0 +1,12 @@
{
"imports": {
"hono": "jsr:@hono/hono@^4.6.13"
},
"tasks": {
"start": "deno run --allow-net --watch main.ts"
},
"compilerOptions": {
"jsx": "precompile",
"jsxImportSource": "hono/jsx"
}
}

View File

@ -0,0 +1,16 @@
{
"version": "4",
"specifiers": {
"jsr:@hono/hono@^4.6.13": "4.6.13"
},
"jsr": {
"@hono/hono@4.6.13": {
"integrity": "198970fc6facf71a88ee57aa1ed32143b56bcb0f842fa965ca3ee7d6fdd21a8e"
}
},
"workspace": {
"dependencies": [
"jsr:@hono/hono@^4.6.13"
]
}
}

83
notes/hello-hono/main.ts Normal file
View File

@ -0,0 +1,83 @@
import { Hono } from 'hono'
const app = new Hono()
// return text
app.get('/', (c) => {
return c.text('Hello Hono!')
})
// return JSON
app.get('/json', (c) => {
return c.json({
ok: true,
message: 'Hello Hono!',
})
})
// query params: curl 'localhost:8000/posts/9?page=7'
app.get('/posts/:id', (c) => {
const page = c.req.query('page')
const id = c.req.param('id')
c.header('X-Message', 'Hi!')
return c.text(`You want to see ${page} of ${id}`)
})
// create: curl -X POST localhost:8000/posts
app.post('/posts', (c) => c.text('Created!', 201))
// delete: curl -X DELETE localhost:8000/posts/8
app.delete('/posts/:id', (c) =>
c.text(`${c.req.param('id')} should be deleted!`)
)
// return raw response
app.get('/raw', () => {
return new Response('Hello Hono!')
})
/*
// return HTML using JSX (Deno's linter hates it)
const View = () => {
return (
<html>
<body>
<h1>Hello Hono!</h1>
</body>
</html>
)
}
app.get('/page', (c) => {
return c.html(<View />)
})
*/
// "Middleware can do the hard work for you. For example, add in Basic Authentication."
// curl -u admin:secret http://localhost:8000/admin
import { basicAuth } from 'hono/basic-auth'
app.use(
'/admin/*',
basicAuth({
username: 'admin',
password: 'secret',
})
)
app.get('/admin', (c) => {
return c.text('You are authorized!')
})
// "There are Adapters for platform-dependent functions, e.g., handling static files or WebSocket.
// For example, to handle WebSocket in Cloudflare Workers, import hono/cloudflare-workers."
/*
import { upgradeWebSocket } from 'hono/cloudflare-workers'
app.get(
'/ws',
upgradeWebSocket((c) => {
// ...
})
)
*/
Deno.serve(app.fetch)

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

12
sam/samconfig.toml Normal file
View File

@ -0,0 +1,12 @@
version = 0.1
[default.deploy.parameters]
stack_name = "hello-world"
resolve_s3 = true
s3_prefix = "hello-world"
region = "us-west-1"
profile = "playground"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
parameter_overrides = "DeployECROnly=\"false\""
image_repositories = []
resolve_image_repos = true

96
sam/template.yaml Normal file
View File

@ -0,0 +1,96 @@
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Docker Container Lambda Function
Globals:
Function:
Timeout: 10
Parameters:
DeployECROnly:
Type: String
Default: "false"
AllowedValues: ["true", "false"]
Description: If true, only deploys the ECR repository
Conditions:
DeployFullStack: !Equals [!Ref DeployECROnly, "false"]
Resources:
HelloWorldRepository:
Type: AWS::ECR::Repository
Properties:
RepositoryName: hello-world
ImageScanningConfiguration:
ScanOnPush: true
HelloWorldFunctionRole:
Condition: DeployFullStack
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
Service:
- "lambda.amazonaws.com"
Action:
- "sts:AssumeRole"
Policies:
- PolicyName: HelloWorldFunctionAccess
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "secretsmanager:GetSecretValue"
Resource:
- "arn:aws:secretsmanager:us-west-1:003525187774:secret:*"
ManagedPolicyArns:
# - arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess
- arn:aws:iam::aws:policy/AWSOpsWorksCloudWatchLogs
HelloWorldFunction:
Condition: DeployFullStack
Type: AWS::Serverless::Function
Properties:
PackageType: Image
ImageUri: !Sub "${HelloWorldRepository.RepositoryUri}:latest"
# ImageUri: !Sub "${HelloWorldRepository.RepositoryUri}@sha256:c195f0c7d0bb2a5bdf408f149f4cf558e3376128a4887b5cd0a6fb2196992bf3"
Role: !GetAtt HelloWorldFunctionRole.Arn
Environment:
Variables:
PORT: 8000
Architectures:
- x86_64
FunctionUrlConfig:
AuthType: NONE
Cors:
AllowOrigins:
- '*'
HelloWorldFunctionPermission:
Condition: DeployFullStack
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref HelloWorldFunction
Action: lambda:InvokeFunctionUrl
Principal: '*'
FunctionUrlAuthType: NONE
Outputs:
HelloWorldRepositoryUri:
Description: ECR Repository URI
Value: !GetAtt HelloWorldRepository.RepositoryUri
HelloWorldFunctionName:
Condition: DeployFullStack
Description: Lambda Function Name
Value: !Ref HelloWorldFunction
# Error: Requested attribute FunctionUrl does not exist in schema for AWS::Lambda::Function
# HelloWorldFunctionUrl:
# Condition: DeployFullStack
# Description: "The URL of the Lambda Function"
# Value: !GetAtt HelloWorldFunction.FunctionUrl

View File

@ -0,0 +1,25 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/aws" {
version = "5.80.0"
constraints = "~> 5.0"
hashes = [
"h1:N5Wfsf4xe5DJfSeo0G/ulkIxzyfmUIoSj/hAiZ2DaKU=",
"zh:0b1655e39639d60f2de2860a5df8642f9556ba0ca04529c1b861fde4935cb0df",
"zh:13dc0155e0a11edceee29ce687fc04c5a5a85f3324c67556472713cfd52e5807",
"zh:180f6cb2be44be14cfe329e0649121b774319f083b6e4e8fb749f85090d73121",
"zh:3158d44b74c67465f7f19f22c42b643840c8d18ce833e2ec86e8d93085b06926",
"zh:6351b5bf7cde5dc83e926944891570636069e05ca43341f4d1feda67773469bf",
"zh:6fa9db1532096ba50e842d369b6688979306d2295c7ead49b8a266b0d60962cc",
"zh:85d2fe75def7619ff2cc29102048875039cad088fafb62ecc14c3763e7b1e9d9",
"zh:9028d653f1d7341c6dfe2afe961b6541581e9043a474eac2faf90e6426a24f6d",
"zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
"zh:9c4e248c442bc60f07f9f089e5361f19936833370dc3c04b27916672b765f0e1",
"zh:a710a3979596e3f3938c3ec6bb748e604724d3a4afa96ed2c14f0a245cc41a11",
"zh:c27936bdf447779d0c0833bf52a9ef618985f5ea8e3e243d6266513520ca31c4",
"zh:c7681134a123486e72eaedc3f8d2d75e267dbbfd45fa7de5aea8f757af57f89b",
"zh:ea717ebad3561fd02591f9eecf30f3df5635405556fba2bdbf29fd42691bebac",
"zh:f4e1e8f23c58c3e8f4371f9c3379a723ab4155246e6b6daad8eb99e16666b2cb",
]
}

84
terraform/main.tf Normal file
View File

@ -0,0 +1,84 @@
provider "aws" {
region = "us-west-1"
profile = "playground"
}
# ECR Repository
resource "aws_ecr_repository" "hello_docker" {
name = "hello-docker"
image_scanning_configuration {
scan_on_push = true
}
}
# IAM Role for Lambda
resource "aws_iam_role" "hello_docker_role" {
name = "hello_docker_lambda_role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
}]
})
}
# IAM Policy for Secrets Manager access
resource "aws_iam_role_policy" "hello_docker_policy" {
name = "hello_docker_function_access"
role = aws_iam_role.hello_docker_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["secretsmanager:GetSecretValue"]
Resource = ["arn:aws:secretsmanager:us-west-1:003525187774:secret:*"]
}]
})
}
# Attach CloudWatch Logs policy
resource "aws_iam_role_policy_attachment" "hello_docker_logs" {
role = aws_iam_role.hello_docker_role.name
policy_arn = "arn:aws:iam::aws:policy/AWSOpsWorksCloudWatchLogs"
}
# Lambda Function
resource "aws_lambda_function" "hello_docker" {
function_name = "hello-docker"
role = aws_iam_role.hello_docker_role.arn
package_type = "Image"
image_uri = "${aws_ecr_repository.hello_docker.repository_url}:latest"
architectures = ["x86_64"]
timeout = 10
environment {
variables = {
PORT = "8000"
}
}
}
# Lambda Function URL
resource "aws_lambda_function_url" "hello_docker_url" {
function_name = aws_lambda_function.hello_docker.function_name
authorization_type = "NONE"
cors {
allow_origins = ["*"]
}
}
# Lambda permission for Function URL
resource "aws_lambda_permission" "function_url" {
statement_id = "AllowExecutionFromFunctionURL"
action = "lambda:InvokeFunctionUrl"
function_name = aws_lambda_function.hello_docker.function_name
principal = "*"
function_url_auth_type = "NONE"
}

9
terraform/outputs.tf Normal file
View File

@ -0,0 +1,9 @@
output "function_url" {
description = "Lambda Function URL"
value = aws_lambda_function_url.hello_docker_url.function_url
}
output "ecr_repository_url" {
description = "ECR Repository URI"
value = aws_ecr_repository.hello_docker.repository_url
}

9
terraform/versions.tf Normal file
View File

@ -0,0 +1,9 @@
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
required_version = ">= 1.0"
}