GCP CDKTF plugin for StackSolo - provides Google Cloud Platform resource definitions using CDK for Terraform
npm install @stacksolo/plugin-gcp-cdktfGoogle Cloud Platform infrastructure for StackSolo using CDKTF (Terraform).
This plugin lets you deploy your apps to Google Cloud. It creates:
- Cloud Functions - Serverless code that runs when someone calls your API
- Load Balancers - Routes traffic to the right function based on URL path
- Static Websites - Host your React/Vue/HTML frontend
- VPC Networks - Private networks for your resources
- VPC Connectors - Let your functions talk to private resources (like databases)
---
Create or edit stacksolo.config.json:
``json
{
"project": {
"name": "my-app",
"gcpProjectId": "my-gcp-project",
"region": "us-central1",
"backend": "cdktf",
"networks": [{
"name": "main",
"functions": [{
"name": "api",
"runtime": "nodejs20",
"entryPoint": "api",
"allowUnauthenticated": true
}],
"loadBalancer": {
"name": "gateway",
"routes": [
{ "path": "/api/*", "functionName": "api" }
]
}
}]
}
}
`
Create a file at functions/api/index.ts:
`typescript
import * as functions from '@google-cloud/functions-framework';
functions.http('api', (req, res) => {
res.json({ message: 'Hello from Cloud Functions!' });
});
`
`bash`
stacksolo deploy
Your API is now live at the load balancer IP!
---
Serverless functions that run your backend code. They scale automatically and you only pay when they run.
When to use: APIs, webhooks, background processing
Config:
`json`
{
"functions": [{
"name": "api",
"runtime": "nodejs20",
"entryPoint": "api",
"memory": "256Mi",
"timeout": 60,
"allowUnauthenticated": true
}]
}
| Field | Required | Default | What it does |
|-------|----------|---------|--------------|
| name | Yes | - | Name of your function |runtime
| | No | nodejs20 | Language runtime (nodejs20, nodejs18, python311, python310, go121, go120) |entryPoint
| | No | api | The exported function name in your code |memory
| | No | 256Mi | Memory for each instance (128Mi, 256Mi, 512Mi, 1Gi, 2Gi, 4Gi) |timeout
| | No | 60 | Max seconds a request can run |minInstances
| | No | 0 | Keep instances warm (costs money but faster cold starts) |maxInstances
| | No | 100 | Max concurrent instances |allowUnauthenticated
| | No | true | Allow public access |vpcConnector
| | No | - | Name of VPC connector (for database access) |env
| | No | - | Environment variables |
Example with all options:
`json`
{
"functions": [{
"name": "api",
"runtime": "nodejs20",
"entryPoint": "api",
"memory": "512Mi",
"timeout": 120,
"minInstances": 1,
"maxInstances": 10,
"allowUnauthenticated": true,
"vpcConnector": "main-connector",
"env": {
"DATABASE_URL": "@database/main.connectionString"
}
}]
}
#### Storage Triggers (Event-Driven Functions)
Functions can be triggered by Cloud Storage events instead of HTTP requests. Perfect for processing uploaded files.
Config:
`json`
{
"functions": [{
"name": "pdf-processor",
"runtime": "nodejs20",
"entryPoint": "handler",
"memory": "1Gi",
"timeout": 300,
"trigger": {
"type": "storage",
"bucket": "uploads",
"event": "finalize"
}
}]
}
| Trigger Field | Required | What it does |
|---------------|----------|--------------|
| type | Yes | Trigger type: http, storage, or pubsub |bucket
| | Yes* | Bucket name to watch (for storage triggers) |event
| | No | Event type: finalize (default), delete, archive, metadataUpdate |
*Required when type is storage.
Storage event types:
| Event | When it fires |
|-------|---------------|
| finalize | New file uploaded or overwritten (default) |delete
| | File deleted |archive
| | File archived (for versioned buckets) |metadataUpdate
| | File metadata changed |
Example function code for storage trigger:
`typescript
// functions/pdf-processor/src/index.ts
import { CloudEvent } from '@google-cloud/functions-framework';
import { Storage } from '@google-cloud/storage';
interface StorageObjectData {
bucket: string;
name: string;
contentType: string;
}
export async function handler(event: CloudEvent
const { bucket, name, contentType } = event.data!;
console.log(Processing file: ${name} from bucket: ${bucket});
// Download and process the file
const storage = new Storage();
const [contents] = await storage.bucket(bucket).file(name).download();
// Your processing logic here...
}
`
---
Routes incoming traffic to the right place based on the URL path. This is how you get one domain that serves your API and frontend.
When to use:
- You have multiple functions and want one URL
- You want to serve your frontend and API from the same domain
- You need a global IP address
Basic config (one function):
`json`
{
"loadBalancer": {
"name": "gateway",
"routes": [
{ "path": "/*", "functionName": "api" }
]
}
}
Multi-function config:
`json`
{
"loadBalancer": {
"name": "gateway",
"routes": [
{ "path": "/api/*", "functionName": "api" },
{ "path": "/auth/*", "functionName": "auth" },
{ "path": "/webhooks/*", "functionName": "webhooks" }
]
}
}
API + Frontend config:
`json
{
"loadBalancer": {
"name": "gateway",
"routes": [
{ "path": "/api/*", "functionName": "api" },
{ "path": "/*", "uiName": "web" }
]
},
"uis": [{
"name": "web",
"sourceDir": "./frontend"
}]
}
`
| Field | Required | What it does |
|-------|----------|--------------|
| name | Yes | Name for the load balancer |routes
| | Yes | List of path-to-backend mappings |routes[].path
| | Yes | URL path pattern (e.g., /api/*) |routes[].functionName
| | * | Function to route to |routes[].uiName
| | * | Static website to route to |domain
| | No | Custom domain for HTTPS |enableHttps
| | No | Enable HTTPS with managed SSL certificate |dns
| | No | Auto-configure DNS (requires Cloudflare plugin) |
*Either functionName or uiName is required for each route.
With custom domain and HTTPS:
`json`
{
"loadBalancer": {
"name": "gateway",
"domain": "app.example.com",
"enableHttps": true,
"redirectHttpToHttps": true,
"routes": [
{ "path": "/api/*", "functionName": "api" },
{ "path": "/*", "uiName": "web" }
]
}
}
With automatic Cloudflare DNS:
`json`
{
"loadBalancer": {
"name": "gateway",
"domain": "app.example.com",
"enableHttps": true,
"dns": {
"provider": "cloudflare",
"proxied": true
},
"routes": [
{ "path": "/*", "functionName": "api" }
]
}
}
When using Cloudflare DNS, you also need to configure cloudflare.zoneId at the project level.
How path matching works:
- /api/* matches /api/users, /api/orders/123, etc./*
- matches everything (use as fallback)
- More specific paths are matched first
---
Hosts your static frontend (React, Vue, plain HTML) on Cloud Storage with CDN.
When to use: Frontend apps, marketing sites, documentation
Config:
`json`
{
"uis": [{
"name": "web",
"sourceDir": "./frontend",
"indexDocument": "index.html",
"errorDocument": "index.html"
}]
}
| Field | Required | Default | What it does |
|-------|----------|---------|--------------|
| name | Yes | - | Name for the website |sourceDir
| | Yes | - | Path to your frontend folder |location
| | No | US | Storage location |indexDocument
| | No | index.html | Main page |errorDocument
| | No | index.html | 404 page (use index.html for SPAs) |enableCdn
| | No | true | Enable Cloud CDN for faster loading |
For single-page apps (React, Vue, etc.):
Set errorDocument to index.html so client-side routing works:
`json`
{
"uis": [{
"name": "web",
"sourceDir": "./frontend",
"errorDocument": "index.html"
}]
}
---
A private network that isolates your resources. Required if you want functions to access databases or other private resources.
When to use:
- Connecting functions to Cloud SQL
- Connecting functions to Redis/Memorystore
- Any private resource access
Config:
`json`
{
"networks": [{
"name": "main",
"autoCreateSubnetworks": true
}]
}
| Field | Required | Default | What it does |
|-------|----------|---------|--------------|
| name | Yes | - | Name for the network |autoCreateSubnetworks
| | No | true | Auto-create subnets in each region |
---
Connects your Cloud Functions to a VPC network. This is how functions access databases.
When to use: Your function needs to connect to a database or cache
Config:
`json`
{
"vpcConnector": {
"name": "main-connector",
"network": "main",
"region": "us-central1",
"ipCidrRange": "10.8.0.0/28"
}
}
| Field | Required | Default | What it does |
|-------|----------|---------|--------------|
| name | Yes | - | Name for the connector |network
| | Yes | - | VPC network name to connect to |region
| | Yes | - | GCP region |ipCidrRange
| | No | 10.8.0.0/28 | IP range for the connector |minThroughput
| | No | 200 | Min throughput in Mbps |maxThroughput
| | No | 300 | Max throughput in Mbps |
---
Cloud Storage buckets for storing files. Can be used for uploads, data processing, or static hosting.
When to use:
- Storing uploaded files
- Triggering functions on file uploads
- Static file hosting behind a load balancer
Basic config:
`json`
{
"storageBuckets": [{
"name": "uploads",
"location": "us-central1"
}]
}
| Field | Required | Default | What it does |
|-------|----------|---------|--------------|
| name | Yes | - | Bucket name |location
| | No | US | Storage location |storageClass
| | No | STANDARD | Storage class (STANDARD, NEARLINE, COLDLINE, ARCHIVE) |versioning
| | No | false | Enable object versioning |uniformBucketLevelAccess
| | No | true | Uniform IAM access |existing
| | No | false | Reference an existing bucket |website
| | No | - | Website configuration (see below) |cors
| | No | - | CORS configuration |
With website configuration (for SPAs):
`json`
{
"storageBuckets": [{
"name": "admin-ui",
"website": {
"mainPageSuffix": "index.html",
"notFoundPage": "index.html"
}
}]
}
The website config makes the bucket work as a static website:mainPageSuffix
- - Page served for directory requests (e.g., /admin/ → /admin/index.html)notFoundPage
- - Page served for 404 errors. Set to index.html for SPA routing.
With CORS (for browser uploads):
`json`
{
"storageBuckets": [{
"name": "uploads",
"cors": [{
"origin": ["https://app.example.com"],
"method": ["GET", "PUT", "POST"],
"responseHeader": ["Content-Type"],
"maxAgeSeconds": 3600
}]
}]
}
Using as a function trigger:
`json`
{
"storageBuckets": [
{ "name": "uploads" },
{ "name": "processed" }
],
"functions": [{
"name": "processor",
"trigger": {
"type": "storage",
"bucket": "uploads",
"event": "finalize"
},
"env": {
"OUTPUT_BUCKET": "processed"
}
}]
}
---
Just an API, no frontend.
`json
{
"project": {
"name": "my-api",
"gcpProjectId": "my-project",
"region": "us-central1",
"backend": "cdktf",
"networks": [{
"name": "main",
"functions": [{
"name": "api",
"runtime": "nodejs20",
"entryPoint": "api"
}],
"loadBalancer": {
"name": "gateway",
"routes": [
{ "path": "/*", "functionName": "api" }
]
}
}]
}
}
`
Result: Your API is available at http://
---
An API backend with a React/Vue frontend.
`json
{
"project": {
"name": "my-app",
"gcpProjectId": "my-project",
"region": "us-central1",
"backend": "cdktf",
"networks": [{
"name": "main",
"functions": [{
"name": "api",
"runtime": "nodejs20",
"entryPoint": "api"
}],
"uis": [{
"name": "web",
"sourceDir": "./frontend",
"errorDocument": "index.html"
}],
"loadBalancer": {
"name": "gateway",
"routes": [
{ "path": "/api/*", "functionName": "api" },
{ "path": "/*", "uiName": "web" }
]
}
}]
}
}
`
Result:
- http:// → API functionhttp://
- → Frontend
---
Split your backend into separate functions.
`json
{
"project": {
"name": "my-app",
"gcpProjectId": "my-project",
"region": "us-central1",
"backend": "cdktf",
"networks": [{
"name": "main",
"functions": [
{
"name": "api-users",
"runtime": "nodejs20",
"entryPoint": "api"
},
{
"name": "api-orders",
"runtime": "nodejs20",
"entryPoint": "api"
},
{
"name": "api-payments",
"runtime": "nodejs20",
"entryPoint": "api",
"memory": "512Mi"
}
],
"loadBalancer": {
"name": "gateway",
"routes": [
{ "path": "/api/users/*", "functionName": "api-users" },
{ "path": "/api/orders/*", "functionName": "api-orders" },
{ "path": "/api/payments/*", "functionName": "api-payments" }
]
}
}]
}
}
`
---
Connect your function to a Cloud SQL database.
`json
{
"project": {
"name": "my-app",
"gcpProjectId": "my-project",
"region": "us-central1",
"backend": "cdktf",
"networks": [{
"name": "main",
"autoCreateSubnetworks": true,
"vpcConnector": {
"name": "db-connector",
"network": "main",
"region": "us-central1"
},
"databases": [{
"name": "main",
"databaseVersion": "POSTGRES_15",
"tier": "db-f1-micro"
}],
"functions": [{
"name": "api",
"runtime": "nodejs20",
"entryPoint": "api",
"vpcConnector": "db-connector",
"env": {
"DATABASE_URL": "@database/main.connectionString"
}
}],
"loadBalancer": {
"name": "gateway",
"routes": [
{ "path": "/*", "functionName": "api" }
]
}
}]
}
}
`
---
`typescript
// functions/api/index.ts
import * as functions from '@google-cloud/functions-framework';
import express from 'express';
const app = express();
app.use(express.json());
app.get('/api/health', (req, res) => {
res.json({ status: 'ok' });
});
app.get('/api/users', (req, res) => {
res.json([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
]);
});
app.post('/api/users', (req, res) => {
const { name } = req.body;
res.json({ id: 3, name });
});
// Export the Express app as a Cloud Function
functions.http('api', app);
`
`typescript
// functions/api/index.ts
import * as functions from '@google-cloud/functions-framework';
import express from 'express';
import { Pool } from 'pg';
const app = express();
app.use(express.json());
// Database connection
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
app.get('/api/users', async (req, res) => {
const result = await pool.query('SELECT * FROM users');
res.json(result.rows);
});
functions.http('api', app);
`
Access environment variables in your code:
`typescript`
const apiKey = process.env.API_KEY;
const databaseUrl = process.env.DATABASE_URL;
Set them in your config:
`json`
{
"functions": [{
"name": "api",
"env": {
"API_KEY": "your-api-key",
"DATABASE_URL": "@database/main.connectionString"
}
}]
}
---
When deploying, StackSolo expects this structure:
``
my-project/
├── stacksolo.config.json
├── functions/
│ ├── api/
│ │ ├── package.json
│ │ ├── index.ts
│ │ └── ... other files
│ └── webhooks/
│ ├── package.json
│ └── index.ts
└── frontend/
├── package.json
├── index.html
└── ... your frontend code
Each function is in its own folder with its own package.json.
---
1. Install Google Cloud CLI:
`bash
# macOS
brew install google-cloud-sdk
# Or download from: https://cloud.google.com/sdk/docs/install
`
2. Login to Google Cloud:
`bash`
gcloud auth login
gcloud auth application-default login
3. Set your project:
`bash`
gcloud config set project YOUR_PROJECT_ID
4. Install Terraform:
`bash
# macOS
brew install terraform
# Or download from: https://developer.hashicorp.com/terraform/downloads
`
`bash`
stacksolo deploy
`bash`
stacksolo status
`bash`
stacksolo destroy
---
| Resource | Estimated Monthly Cost |
|----------|----------------------|
| Cloud Function (256Mi) | ~$0 (free tier covers 2M invocations) |
| Cloud Function (512Mi) | ~$5 |
| Cloud Function (1Gi) | ~$10 |
| Load Balancer | ~$18 |
| Storage Website (1GB) | ~$1 |
| VPC Connector | ~$0 (pay per GB processed) |
| VPC Network | $0 |
Costs vary based on usage. These are rough estimates.
---
Make sure you're logged in:
`bash`
gcloud auth login
gcloud auth application-default login
Check if allowUnauthenticated is set to true:`json`
{
"functions": [{
"allowUnauthenticated": true
}]
}
1. Make sure you have a VPC connector:
`json`
{
"vpcConnector": {
"name": "db-connector",
"network": "main",
"region": "us-central1"
}
}
2. Add the connector to your function:
`json`
{
"functions": [{
"vpcConnector": "db-connector"
}]
}
Check your route order. More specific paths should come first:
`json`
{
"routes": [
{ "path": "/api/*", "functionName": "api" },
{ "path": "/*", "uiName": "web" }
]
}
GCP load balancers forward the full path to backends. This is different from Kubernetes ingress which strips path prefixes.
| Route | Request URL | Backend receives |
|-------|-------------|-----------------|
| /api/* → function | /api/users | /api/users |/admin/*
| → bucket | /admin/app.js | /admin/app.js |
For functions: Your code receives the full path including the prefix:
`typescript
// Route: /api/* → api function
app.get('/api/users', (req, res) => { // ✅ Include /api prefix
res.json({ users: [] });
});
app.get('/users', (req, res) => { // ❌ Won't match
res.json({ users: [] });
});
`
For storage buckets: Upload files at the full path:
`bash`Route: /admin/* → admin-ui bucket
Files must be at bucket/admin/...
gsutil -m cp -r dist/* gs://my-bucket/admin/
This means:
- https://example.com/admin/ → served from gs://my-bucket/admin/index.htmlhttps://example.com/admin/app.js
- → served from gs://my-bucket/admin/app.js
For single-page apps, set errorDocument to index.html:`json`
{
"uis": [{
"errorDocument": "index.html"
}]
}
---
| Runtime | Language |
|---------|----------|
| nodejs20 | Node.js 20 |nodejs18
| | Node.js 18 |python311
| | Python 3.11 |python310
| | Python 3.10 |go121
| | Go 1.21 |go120
| | Go 1.20 |
| Option | Use case |
|--------|----------|
| 128Mi | Very light tasks |256Mi
| | Default, good for most APIs |512Mi
| | APIs with more processing |1Gi
| | Heavy processing, large payloads |2Gi
| | Very heavy processing |4Gi
| | Maximum available |
Use @type/name.property to reference other resources:
| Reference | What it gets |
|-----------|--------------|
| @database/main.connectionString | Database connection string |@database/main.privateIp
| | Database private IP |@secret/api-key
| | Secret value |@bucket/uploads.name
| | Bucket name |@function/api.url` | Function URL |
|
---
| What you want | What to use |
|---------------|-------------|
| Run backend code | Cloud Function |
| One URL for everything | Load Balancer |
| Host frontend | Storage Website |
| Connect function to database | VPC Network + VPC Connector |
| Route to different backends | Load Balancer with routes |