diff --git a/README.md b/README.md index ded4547..7ead77c 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Table of Contents: | Example | Runtime | Deployment | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ---------------------- | +|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------|------------------------| | **[Badge PHP](functions/badge-php/README.md)**
A PHP function to generate repository badges. | php82 | [Serverless Framework] | | **[CORS Go](functions/cors-go/README.md)**
A Go function which allows CORS requests. | go122 | [Serverless Framework] | | **[CORS Node](functions/cors-node/README.md)**
A Node function which allows CORS requests. | node18 | [Serverless Framework] | @@ -66,27 +66,28 @@ Table of Contents: ### 📦 Containers -| Example | Language | Deployment | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ---------------------- | -| **[Container Bash Script](containers/bash-scheduled-job/README.md)**
A Bash script runnning on a schedule using serverless containers. | Bash | [Serverless Framework] | -| **[Function Handler Java](containers/function-handler-java/README.md)**
A Java function handler deployed on CaaS. | Java | [Serverless Framework] | -| **[NGINX CORS Private](containers/nginx-cors-private-python/README.md)**
An NGINX proxy to allow CORS requests to a private container. | Python Flask | [Terraform] | -| **[NGINX hello world](containers/nginx-hello-world/README.md)**
A minimal example running the base NGINX image in a serverless container. | N/A | [Serverless Framework] | -| **[Python hello world](containers/python-hello-world/README.md)**
A minimal example running a Flask HTTP server in a serverless container. | Python | [Serverless Framework] | -| **[Python S3 upload](containers/python-s3-upload/README.md)**
A Python + Flask HTTP server that receives file uploads and writes them to S3. | Python | [Terraform] | -| **[Terraform NGINX hello world](containers/terraform-nginx-hello-world/README.md)**
A minimal example running the base NGINX image in a serverless container deployed with Terraform. | N/A | [Terraform] | -| **[Triggers with Terraform](containers/terraform-triggers/README.md)**
Configuring two SQS triggers, used to trigger two containers, one public, one private. | N/A | [Terraform] | -| **[gRPC HTTP2 in Go](containers/grpc-http2-go/README.md)**
A Go gRPC Container using http2 | Go/Protobuf | [CLI] | -| **[Rust hello world](containers/rust-hello-world)**
A simple Rust hello world Container | Rust | [CLI] | -| **[.NET C#](containers/csharp-hello-world)**
A .NET C# Container hello world | C# .NET | [CLI] | -| **[Ruby Hello World](containers/ruby-hello-world/)**
A simple Ruby Hello world Container | Ruby | [CLI] | -| **[Deploy Memos app](containers/memos-terraform/)**
A journaling application with its database deployed with Terraform | Terraform | [Terraform] | -| **[Metabase on VPC](containers/vpc-metabase/README.md)**
A Metabase instance running in a private network with a PostgreSQL database. | N/A | [Terraform] | +| Example | Language | Deployment | +|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------|------------------------| +| **[Container Bash Script](containers/bash-scheduled-job/README.md)**
A Bash script runnning on a schedule using serverless containers. | Bash | [Serverless Framework] | +| **[Function Handler Java](containers/function-handler-java/README.md)**
A Java function handler deployed on CaaS. | Java | [Serverless Framework] | +| **[NGINX CORS Private](containers/nginx-cors-private-python/README.md)**
An NGINX proxy to allow CORS requests to a private container. | Python Flask | [Terraform] | +| **[NGINX hello world](containers/nginx-hello-world/README.md)**
A minimal example running the base NGINX image in a serverless container. | N/A | [Serverless Framework] | +| **[Python hello world](containers/python-hello-world/README.md)**
A minimal example running a Flask HTTP server in a serverless container. | Python | [Serverless Framework] | +| **[Python S3 upload](containers/python-s3-upload/README.md)**
A Python + Flask HTTP server that receives file uploads and writes them to S3. | Python | [Terraform] | +| **[Terraform NGINX hello world](containers/terraform-nginx-hello-world/README.md)**
A minimal example running the base NGINX image in a serverless container deployed with Terraform. | N/A | [Terraform] | +| **[Triggers with Terraform](containers/terraform-triggers/README.md)**
Configuring two SQS triggers, used to trigger two containers, one public, one private. | N/A | [Terraform] | +| **[gRPC HTTP2 in Go](containers/grpc-http2-go/README.md)**
A Go gRPC Container using http2 | Go/Protobuf | [CLI] | +| **[Rust hello world](containers/rust-hello-world)**
A simple Rust hello world Container | Rust | [CLI] | +| **[.NET C#](containers/csharp-hello-world)**
A .NET C# Container hello world | C# .NET | [CLI] | +| **[Ruby Hello World](containers/ruby-hello-world/)**
A simple Ruby Hello world Container | Ruby | [CLI] | +| **[Deploy Memos app](containers/memos-terraform/)**
A journaling application with its database deployed with Terraform | Terraform | [Terraform] | +| **[Metabase on VPC](containers/vpc-metabase/README.md)**
A Metabase instance running in a private network with a PostgreSQL database. | N/A | [Terraform] | +| **[MongoDB® on VPC](containers/vpc-mongodb/README.md)**
A MongoDB® instance running in a private network with a sample application connecting to it. | TypeScript on Deno | [Terraform] | ### ⚙️ Jobs | Example | Language | Deployment | -| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | --------------------------- | +|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-----------------------------| | **[Serverless Jobs Hello World](jobs/terraform-hello-world/README.md)**
An example of building a container image and running it as a Serverless Job using Terraform. | N/A | [Terraform]-[Console] | | **[Serverless MLOps](jobs/ml-ops/README.md)**
An example of running a Serverless Machine Leaning workflow. | Python | [Terraform]-[Console]-[CLI] | | **[Auto Snapshot Instances](jobs/instances-snapshot/README.md)**
Use Serverless Jobs to create snapshots of your instances | Go | [Console] | @@ -97,7 +98,7 @@ Table of Contents: ### 💬 Messaging and Queueing | Example | Services | Language | Deployment | -| ------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------- | -------- | ----------- | +|--------------------------------------------------------------------------------------------------------------------------------------------------------|------------------|----------|-------------| | **[Manage large message](mnq/large-messages/README.md)**
An example of infrastructure to manage large messages. | PaaS & S3 | Python | [Terraform] | | **[Serverless scraping](mnq/serverless-scraping/README.md)**
An example of infrastructure to scrape the hackernews website. | PaaS & RDB | Python | [Terraform] | | **[SNS Instances Notification System](mnq/sns-instances-notification-system/README.md)**
An example of infrastructure to use SNS with Instances. | PaaS & Instances | Golang | [Terraform] | @@ -105,7 +106,7 @@ Table of Contents: ### 💜 Projects | Example | Services | Language | Deployment | -| ----------------------------------------------------------------------------------------------------------------------------------------- | ----------- | -------- | ---------------------- | +|-------------------------------------------------------------------------------------------------------------------------------------------|-------------|----------|------------------------| | **[Kong API Gateway](projects/kong-api-gateway/README.md)**
Deploying a Kong Gateway on containers to provide routing to functions. | CaaS & FaaS | Python | [Serverless Framework] | | **[Serverless Gateway](https://github.com/scaleway/serverless-gateway)**
Our serverless gateway for functions and containers. | API Gateway | Python | [Python API Framework] | | **[Monitoring Glaciers](projects/blogpost-glacier/README.md)**
A project to monitor glaciers and the impact of global warming. | S3 & RDB | Golang | [Serverless Framework] | diff --git a/containers/vpc-mongodb/README.md b/containers/vpc-mongodb/README.md new file mode 100644 index 0000000..8a5627e --- /dev/null +++ b/containers/vpc-mongodb/README.md @@ -0,0 +1,47 @@ +# Serverless Containers with Scaleway Managed MongoDB® + +A simple Deno application demonstrating how to connect to a Scaleway Managed MongoDB® instance using Mongoose. It relies on Scaleway's Private Networks for secure communication. + +## Deploying + +### Prerequisites + +This Terraform pushes a Docker image to a Scaleway Container Registry before deploying the Serverless Container. + +Make sure to log in to the Scaleway Container Registry beforehand: + +```bash +scw registry login +``` + +### Deploy with Terraform + +```bash +terraform init +terraform apply +``` + +That's it! You should be able to access the deployed Serverless Container's endpoint from the Scaleway Console. + +## Using the Application + +Once deployed, you can interact with the application using HTTP requests. + +```bash +CONTAINER_URL="https://your-container-url" +# Check MongoDB® connection +curl $CONTAINER_URL/check_connection +# Add some people +curl $CONTAINER_URL/person/george +curl $CONTAINER_URL/person/alice +# List all people +curl $CONTAINER_URL/people +``` + +## Cleaning Up + +Make sure to destroy the Terraform-managed infrastructure when you're done: + +```bash +terraform destroy +``` diff --git a/containers/vpc-mongodb/app/Dockerfile b/containers/vpc-mongodb/app/Dockerfile new file mode 100644 index 0000000..7815e53 --- /dev/null +++ b/containers/vpc-mongodb/app/Dockerfile @@ -0,0 +1,11 @@ +# Build stage +FROM denoland/deno:latest AS builder +WORKDIR /app +COPY . . +RUN deno cache main.ts + +# Production stage +FROM denoland/deno:latest +WORKDIR /app +COPY --from=builder /app . +CMD ["deno", "run", "--allow-net", "--allow-env", "--allow-read", "--allow-write", "--allow-sys", "main.ts"] diff --git a/containers/vpc-mongodb/app/deno.json b/containers/vpc-mongodb/app/deno.json new file mode 100644 index 0000000..cba210d --- /dev/null +++ b/containers/vpc-mongodb/app/deno.json @@ -0,0 +1,10 @@ +{ + "tasks": { + "dev": "deno run --watch main.ts" + }, + "imports": { + "@std/assert": "jsr:@std/assert@1", + "@std/random": "jsr:@std/random@^0.1.4", + "mongoose": "npm:mongoose@^8.20.1" + } +} diff --git a/containers/vpc-mongodb/app/deno.lock b/containers/vpc-mongodb/app/deno.lock new file mode 100644 index 0000000..1812653 --- /dev/null +++ b/containers/vpc-mongodb/app/deno.lock @@ -0,0 +1,129 @@ +{ + "version": "5", + "specifiers": { + "jsr:@std/assert@1": "1.0.16", + "jsr:@std/internal@^1.0.12": "1.0.12", + "jsr:@std/random@~0.1.4": "0.1.4", + "npm:mongoose@^8.20.1": "8.20.1" + }, + "jsr": { + "@std/assert@1.0.16": { + "integrity": "6a7272ed1eaa77defe76e5ff63ca705d9c495077e2d5fd0126d2b53fc5bd6532", + "dependencies": [ + "jsr:@std/internal" + ] + }, + "@std/internal@1.0.12": { + "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" + }, + "@std/random@0.1.4": { + "integrity": "91407b5c30d63b6570e5d7be8b341bf5941c55d5c6d6b9569a220b63ae03e2d9" + } + }, + "npm": { + "@mongodb-js/saslprep@1.3.2": { + "integrity": "sha512-QgA5AySqB27cGTXBFmnpifAi7HxoGUeezwo6p9dI03MuDB6Pp33zgclqVb6oVK3j6I9Vesg0+oojW2XxB59SGg==", + "dependencies": [ + "sparse-bitfield" + ] + }, + "@types/webidl-conversions@7.0.3": { + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + }, + "@types/whatwg-url@11.0.5": { + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "dependencies": [ + "@types/webidl-conversions" + ] + }, + "bson@6.10.4": { + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==" + }, + "debug@4.4.3": { + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": [ + "ms" + ] + }, + "kareem@2.6.3": { + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==" + }, + "memory-pager@1.5.0": { + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" + }, + "mongodb-connection-string-url@3.0.2": { + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "dependencies": [ + "@types/whatwg-url", + "whatwg-url" + ] + }, + "mongodb@6.20.0": { + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", + "dependencies": [ + "@mongodb-js/saslprep", + "bson", + "mongodb-connection-string-url" + ] + }, + "mongoose@8.20.1": { + "integrity": "sha512-G+n3maddlqkQrP1nXxsI0q20144OSo+pe+HzRRGqaC4yK3FLYKqejqB9cbIi+SX7eoRsnG23LHGYNp8n7mWL2Q==", + "dependencies": [ + "bson", + "kareem", + "mongodb", + "mpath", + "mquery", + "ms", + "sift" + ] + }, + "mpath@0.9.0": { + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==" + }, + "mquery@5.0.0": { + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "dependencies": [ + "debug" + ] + }, + "ms@2.1.3": { + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "punycode@2.3.1": { + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" + }, + "sift@17.1.3": { + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==" + }, + "sparse-bitfield@3.0.3": { + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dependencies": [ + "memory-pager" + ] + }, + "tr46@5.1.1": { + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dependencies": [ + "punycode" + ] + }, + "webidl-conversions@7.0.0": { + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" + }, + "whatwg-url@14.2.0": { + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dependencies": [ + "tr46", + "webidl-conversions" + ] + } + }, + "workspace": { + "dependencies": [ + "jsr:@std/assert@1", + "jsr:@std/random@~0.1.4", + "npm:mongoose@^8.20.1" + ] + } +} \ No newline at end of file diff --git a/containers/vpc-mongodb/app/main.ts b/containers/vpc-mongodb/app/main.ts new file mode 100644 index 0000000..a91b94b --- /dev/null +++ b/containers/vpc-mongodb/app/main.ts @@ -0,0 +1,86 @@ +import mongoose from "mongoose"; +import { randomIntegerBetween } from "@std/random"; + +const MONGODB_CERT_PATH = "/tmp/mongodb_cert.pem"; + +const MONGODB_URI = Deno.env.get("MONGODB_URI") +const MONGODB_CERT = Deno.env.get("MONGODB_CERT") + +let mongooseConnected = false; + +async function getMongooseConnection() { + if (!mongooseConnected) { + await mongoose.connect(MONGODB_URI || "", { + tls: true, + tlsCAFile: MONGODB_CERT_PATH, + } as mongoose.ConnectOptions); + + mongooseConnected = true; + } +} + +const CHECK_CONNECTION = new URLPattern({ pathname: "/check_connection" }); +const CREATE_PERSON = new URLPattern({ pathname: "/person/:name" }); +const LIST_PEOPLE = new URLPattern({ pathname: "/people" }); + +async function handler(req: Request): Promise { + const url = new URL(req.url); + + if (CHECK_CONNECTION.test(url)) { + return await check_connection(req); + } else if (CREATE_PERSON.test(url)) { + return await create_person(req); + } else if (LIST_PEOPLE.test(url)) { + return await list_people(req); + } else { + return new Response("Not Found", { status: 404 }); + } +} + +const peopleSchema = new mongoose.Schema({ name: String, age: Number, }); +const ExamplePeople = mongoose.model('Example', peopleSchema); + +async function create_person(req: Request): Promise { + await getMongooseConnection(); + + const url = new URL(req.url) + const name = url.pathname.split("/person/")[1]; + + const examplePerson = new ExamplePeople({ + name: name, + age: randomIntegerBetween(1, 100), + }); + await examplePerson.save(); + + return new Response(`Created person: ${examplePerson.name}`, { status: 201 }); +} + +async function list_people(_req: Request): Promise { + await getMongooseConnection(); + + const people = await ExamplePeople.find().exec(); + const peopleNames = people.map(person => person.name).join(", "); + + return new Response(`People: ${peopleNames}`, { status: 200 }); +} + +async function check_connection(_req: Request): Promise { + try { + await getMongooseConnection(); + + const connectionState = mongoose.connection.readyState; + const body = `Mongoose connection state: ${connectionState}`; + + return new Response(body, { status: 200 }); + } catch (error) { + return new Response(`MongoDB connection failed: ${error}`, { status: 500 }); + } +} + +if (import.meta.main) { + if (MONGODB_CERT) { + await Deno.writeTextFile(MONGODB_CERT_PATH, MONGODB_CERT); + } + + Deno.serve({ port: 8080, hostname: "0.0.0.0" }, handler); +} diff --git a/containers/vpc-mongodb/main.tf b/containers/vpc-mongodb/main.tf new file mode 100644 index 0000000..719fb3e --- /dev/null +++ b/containers/vpc-mongodb/main.tf @@ -0,0 +1,132 @@ +locals { + name_prefix = "mongodb-example" + tags = ["serverless-examples", "mongodb", "terraform"] +} + +resource "scaleway_account_project" "main" { + name = "${local.name_prefix}-project" +} + +resource "scaleway_vpc" "main" { + project_id = scaleway_account_project.main.id + + name = "${local.name_prefix}-vpc" + tags = local.tags +} + +resource "scaleway_vpc_private_network" "main" { + project_id = scaleway_account_project.main.id + vpc_id = scaleway_vpc.main.id + + name = "${local.name_prefix}-private-network" + tags = local.tags +} + +resource "scaleway_mongodb_instance" "main" { + project_id = scaleway_account_project.main.id + + name = "${local.name_prefix}-instance" + tags = local.tags + + version = "7.0.12" + + node_type = "MGDB-PLAY2-NANO" + node_number = 1 + volume_size_in_gb = 10 + + public_network {} + + private_network { + pn_id = scaleway_vpc_private_network.main.id + } + + user_name = var.mongodb_admin_username + password = var.mongodb_admin_password + + is_snapshot_schedule_enabled = false +} + +resource "scaleway_mongodb_user" "app_user" { + instance_id = scaleway_mongodb_instance.main.id + name = var.mongodb_username + password = var.mongodb_password + + roles { + role = "read_write" + any_database = true + } +} + +# Scaleway registry namespace names are unique per-region, +# so we add a random suffix to avoid name collisions. +resource "random_string" "unique_namespace_suffix" { + length = 4 + lower = true + special = false +} + +resource "scaleway_registry_namespace" "main" { + project_id = scaleway_account_project.main.id + + name = "${local.name_prefix}-registry-${random_string.unique_namespace_suffix.result}" +} + +resource "scaleway_container_namespace" "main" { + project_id = scaleway_account_project.main.id + + name = "${local.name_prefix}-namespace" + description = "Namespace for MongoDB client" + tags = local.tags +} + +locals { + mongodb_pn = scaleway_mongodb_instance.main.private_network[0] + mongodb_private_endpoint = local.mongodb_pn.dns_records[0] + mongodb_private_port = local.mongodb_pn.port +} + +// Note: this is somewhat hacky, in a more real world scenario you'd likely want to +// build and push the Docker image outside of Terraform, e.g. in a CI/CD pipeline. +resource "null_resource" "build_and_push_docker_image" { + depends_on = [scaleway_registry_namespace.main] + + provisioner "local-exec" { + command = <