Skip to content

Commit 48894cd

Browse files
committed
Initial commit.
0 parents  commit 48894cd

21 files changed

Lines changed: 1907 additions & 0 deletions

.dockerignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.git
2+
.github
3+
*.md
4+
Dockerfile
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: Create release
2+
on:
3+
pull_request_target:
4+
branches:
5+
- main
6+
types:
7+
- closed
8+
jobs:
9+
release:
10+
if: github.event.pull_request.merged == true && !contains(github.event.pull_request.title, 'skip-release')
11+
uses: libops/actions/.github/workflows/bump-release.yaml@main
12+
with:
13+
workflow_file: goreleaser.yml
14+
permissions:
15+
contents: write
16+
actions: write
17+
secrets: inherit

.github/workflows/goreleaser.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: goreleaser
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
tags:
7+
- "*"
8+
9+
permissions:
10+
contents: write
11+
12+
jobs:
13+
goreleaser:
14+
runs-on: ubuntu-latest
15+
steps:
16+
-
17+
name: Checkout
18+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
19+
with:
20+
fetch-depth: 0
21+
22+
- name: Set up Go
23+
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5
24+
with:
25+
go-version: '>=1.22.2'
26+
27+
- name: Run GoReleaser
28+
uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6
29+
with:
30+
distribution: goreleaser
31+
version: latest
32+
args: release --clean
33+
env:
34+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: lint-test-build-push
2+
on:
3+
push:
4+
paths-ignore:
5+
- "**/*.md"
6+
- "renovate.json5"
7+
branches:
8+
- "**"
9+
tags:
10+
- "*"
11+
12+
concurrency:
13+
group: ${{ github.workflow }}-${{ github.ref }}
14+
cancel-in-progress: true
15+
16+
permissions:
17+
contents: read
18+
jobs:
19+
lint-test:
20+
runs-on: ubuntu-24.04
21+
steps:
22+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
23+
24+
- name: check valid yml
25+
run: yq . *.yml
26+
27+
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5
28+
with:
29+
go-version: ">=1.25.0"
30+
31+
- name: golangci-lint
32+
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8
33+
with:
34+
version: latest
35+
36+
- name: Install dependencies
37+
run: go get .
38+
39+
- name: Build
40+
run: go build -v ./...
41+
42+
- name: Test with the Go CLI
43+
run: go test -v -race ./...
44+
45+
build-push:
46+
needs: [lint-test]
47+
uses: libops/actions/.github/workflows/build-push.yaml@main
48+
permissions:
49+
contents: read
50+
packages: write
51+
secrets: inherit

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ppb
2+
ppb.yaml

.golangci.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
version: "2"
2+
linters:
3+
exclusions:
4+
generated: lax
5+
presets:
6+
- comments
7+
- common-false-positives
8+
- legacy
9+
- std-error-handling
10+
paths:
11+
- ../../go
12+
- ../../../../opt
13+
- ./vendor
14+
- third_party$
15+
- builtin$
16+
- examples$
17+
formatters:
18+
enable:
19+
- gofmt
20+
exclusions:
21+
generated: lax
22+
paths:
23+
- ../../go
24+
- ../../../../opt
25+
- ./vendor
26+
- third_party$
27+
- builtin$
28+
- examples$

.goreleaser.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
before:
2+
hooks:
3+
- go mod tidy
4+
builds:
5+
- binary: scyllaridae
6+
env:
7+
- CGO_ENABLED=0
8+
goos:
9+
- linux
10+
- windows
11+
- darwin
12+
13+
archives:
14+
- format: tar.gz
15+
# this name template makes the OS and Arch compatible with the results of uname.
16+
name_template: >-
17+
{{ .ProjectName }}_
18+
{{- title .Os }}_
19+
{{- if eq .Arch "amd64" }}x86_64
20+
{{- else if eq .Arch "386" }}i386
21+
{{- else }}{{ .Arch }}{{ end }}
22+
{{- if .Arm }}v{{ .Arm }}{{ end }}
23+
# use zip for windows archives
24+
format_overrides:
25+
- goos: windows
26+
format: zip
27+
checksum:
28+
name_template: 'checksums.txt'
29+
snapshot:
30+
name_template: "{{ incpatch .Version }}-next"
31+
changelog:
32+
sort: asc
33+
filters:
34+
exclude:
35+
- '^docs:'
36+
- '^test:'

Dockerfile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FROM golang:1.24-alpine3.21@sha256:ba0e119509b381457e9bb434319f2ef7996ad7ca7ec11a66e4ecdd8da49b8ddf
2+
3+
WORKDIR /app
4+
5+
COPY . ./
6+
7+
RUN go mod download \
8+
&& go build -o /app/ppb \
9+
&& go clean -cache -modcache
10+
11+
ENTRYPOINT ["/app/ppb"]

README.md

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# proxy power button
2+
3+
A secure reverse proxy that automatically powers on Google Compute Engine instances when traffic arrives.
4+
5+
This service is designed to run on **Google Cloud Run as an ingress layer** in front of your full application stack running on Google Compute Engine. When traffic hits your Cloud Run endpoint, PPB automatically powers on the target GCE VM (if needed) and proxies the request through. This architecture allows you to run complete application stacks on cost-effective VMs while only paying for Cloud Run when traffic actually arrives.
6+
7+
## Architecture
8+
9+
```
10+
Internet → Cloud Run (PPB) → Google Compute Engine (Full App Stack)
11+
```
12+
13+
- **Cloud Run**: Runs PPB as serverless ingress, scales to zero when no traffic
14+
- **GCE VM**: Runs your complete application (web server, database, etc.), can power off when idle
15+
- **PPB**: Powers on the VM when requests arrive, proxies traffic through with IP authorization
16+
17+
## Cloud Run Behavior
18+
19+
PPB runs as a reverse proxy with intelligent rate limiting:
20+
21+
1. **Request handling**: Each authorized request attempts to power on the target machine if needed
22+
2. **Rate limiting**: Power-on attempts are rate limited with a configurable cooldown period (default 30s)
23+
3. **API protection**: Prevents hammering Google Cloud APIs during high traffic periods
24+
4. **IP Security**: Only requests from allowed IP ranges can trigger machine power-on attempts
25+
26+
This approach balances responsiveness with API efficiency - machines get powered on when traffic arrives, but multiple requests don't spam the Google Cloud APIs.
27+
28+
## Use Case Examples
29+
30+
- **Full Stack Applications**: Run complete LAMP/MEAN/Django stacks on GCE VMs with PPB as Cloud Run ingress
31+
- **Development Environments**: Keep expensive GPU/high-memory dev instances off until developers access them
32+
- **Legacy Applications**: Modernize access to monolithic applications that need full VM environments
33+
- **Cost Optimization**: Run databases, processing servers, or complete application environments that can idle
34+
- **Multi-service Backends**: Power on VMs running docker-compose stacks, Kubernetes clusters, or complex service meshes
35+
36+
## Config
37+
38+
Currently, only backend services running on Google Compute Engine are supported.
39+
40+
```yaml
41+
type: google_compute_engine
42+
port: 80
43+
scheme: http
44+
allowedIps:
45+
- 127.0.0.1/32 # localhost
46+
- 10.0.0.0/8 # private networks
47+
- 172.16.0.0/12
48+
- 192.168.0.0/16
49+
ipForwardedHeader: X-Forwarded-For # header to check for real client IP
50+
ipDepth: 0 # depth in X-Forwarded-For chain
51+
powerOnCooldown: 30 # seconds between power-on attempts (default: 30)
52+
# Optional proxy timeout configuration (defaults shown)
53+
proxyTimeouts:
54+
dialTimeout: 120 # Connection dial timeout in seconds
55+
keepAlive: 120 # TCP keep-alive timeout in seconds
56+
idleConnTimeout: 90 # Idle connection timeout in seconds
57+
tlsHandshakeTimeout: 10 # TLS handshake timeout in seconds
58+
expectContinueTimeout: 1 # Expect: 100-continue timeout in seconds
59+
maxIdleConns: 100 # Maximum idle connections
60+
machineMetadata:
61+
project_id: foo
62+
zone: us-central1-a
63+
name: my-compute-instance-name
64+
usePrivateIp: false # true for VPC-native setups
65+
```
66+
67+
### Configuration Reference
68+
69+
| Field | Type | Required | Default | Description |
70+
|-------|------|----------|---------|-------------|
71+
| `type` | string | ✅ | - | Backend type, currently only `google_compute_engine` |
72+
| `port` | int | ✅ | - | Port on target machine to proxy to |
73+
| `scheme` | string | ✅ | - | Protocol scheme (`http` or `https`) |
74+
| `allowedIps` | []string | ✅ | - | CIDR ranges of IPs allowed to access the proxy |
75+
| `ipForwardedHeader` | string | ❌ | `""` | Header to check for real client IP (e.g., `X-Forwarded-For`) |
76+
| `ipDepth` | int | ❌ | `0` | Depth in forwarded header chain (0 = rightmost IP) |
77+
| `powerOnCooldown` | int | ❌ | `30` | Seconds between power-on attempts (rate limiting) |
78+
| `proxyTimeouts.dialTimeout` | int | ❌ | `120` | Connection dial timeout in seconds |
79+
| `proxyTimeouts.keepAlive` | int | ❌ | `120` | TCP keep-alive timeout in seconds |
80+
| `proxyTimeouts.idleConnTimeout` | int | ❌ | `90` | Idle connection timeout in seconds |
81+
| `proxyTimeouts.tlsHandshakeTimeout` | int | ❌ | `10` | TLS handshake timeout in seconds |
82+
| `proxyTimeouts.expectContinueTimeout` | int | ❌ | `1` | Expect: 100-continue timeout in seconds |
83+
| `proxyTimeouts.maxIdleConns` | int | ❌ | `100` | Maximum number of idle connections |
84+
| `machineMetadata.project_id` | string | ✅ | - | Google Cloud project ID |
85+
| `machineMetadata.zone` | string | ✅ | - | GCE zone (e.g., `us-central1-a`) |
86+
| `machineMetadata.name` | string | ✅ | - | GCE instance name |
87+
| `machineMetadata.usePrivateIp` | bool | ❌ | `false` | Use private IP for VPC-native setups |
88+
89+
Deploy this service on **Google Cloud Run** as the public endpoint for your application. Configure the `machineMetadata` to point to your GCE VM running the actual application stack. Only requests from allowed IPs will power on the VM and be proxied through.
90+
91+
### Environment Variables
92+
93+
PPB also supports these environment variables for runtime configuration:
94+
95+
| Variable | Description | Default |
96+
|----------|-------------|---------|
97+
| `PPB_CONFIG_PATH` | Path to YAML configuration file | Required |
98+
| `LOG_LEVEL` | Log level (`DEBUG`, `INFO`, `WARN`, `ERROR`) | `INFO` |
99+
| `GOOGLE_APPLICATION_CREDENTIALS` | Path to service account JSON file | Uses default credentials |
100+
101+
## IAM Permissions
102+
103+
The Google Service Account (GSA) used by Cloud Run needs a custom IAM role with minimal permissions to control the target compute instance.
104+
105+
### Custom Role Definition
106+
107+
Create a custom role with only the required permissions:
108+
109+
```hcl
110+
resource "google_project_iam_custom_role" "compute-start" {
111+
project = var.project
112+
113+
role_id = "startVM"
114+
title = "Start Compute Instance"
115+
description = "Minimal permissions for PPB to control compute instances"
116+
permissions = [
117+
"compute.instances.start",
118+
"compute.instances.resume",
119+
"compute.instances.get"
120+
]
121+
}
122+
```
123+
124+
### Assign Role to Service Account
125+
126+
```hcl
127+
resource "google_project_iam_member" "ppb_service_account" {
128+
project = var.project
129+
role = google_project_iam_custom_role.compute-start.name
130+
member = "serviceAccount:${var.service_account_email}"
131+
}
132+
```
133+
134+
### CLI Alternative
135+
136+
```bash
137+
gcloud iam roles create startVM \
138+
--project=PROJECT_ID \
139+
--title="Start Compute Instance" \
140+
--description="Minimal permissions for PPB to control compute instances" \
141+
--permissions="compute.instances.start,compute.instances.resume,compute.instances.get"
142+
143+
gcloud projects add-iam-policy-binding PROJECT_ID \
144+
--member="serviceAccount:SERVICE_ACCOUNT_EMAIL" \
145+
--role="projects/PROJECT_ID/roles/startVM"
146+
```
147+
148+
## Usage
149+
150+
### Local development
151+
152+
```bash
153+
docker build -t ghcr.io/libops/ppb:development .
154+
docker run \
155+
-p 8080:8080 \
156+
-v $GOOGLE_APPLICATION_CREDENTIALS:/app/svc.json:ro \
157+
-v ./ppb.yaml:/app/ppb.yaml:ro \
158+
--env PPB_CONFIG_PATH=/app/ppb.yaml \
159+
--env GOOGLE_APPLICATION_CREDENTIALS=/app/svc.json \
160+
--env PRIVATE_IP=false \
161+
--env LOG_LEVEL=DEBUG \
162+
ghcr.io/libops/ppb:development
163+
```
164+
165+
PPB will act as a local ingress proxy to your GCE VM. Visit http://localhost:8080 and PPB will power on your target VM (if needed) then proxy your requests through to the application running on the VM.
166+
167+
### Google Cloud Run Deployment
168+
169+
Deploy PPB to Cloud Run and configure it as your application's public endpoint:
170+
171+
```bash
172+
gcloud run deploy ppb \
173+
--image=ghcr.io/libops/ppb:latest \
174+
--set-env-vars="PPB_CONFIG_PATH=/app/ppb.yaml" \
175+
--service-account=your-ppb-service-account@project.iam.gserviceaccount.com \
176+
--allow-unauthenticated \
177+
--port=8080 \
178+
--region=us-central1
179+
```
180+
181+
Your users will access `https://ppb-xxx-uc.a.run.app` which automatically powers on your GCE VM and proxies traffic through.

0 commit comments

Comments
 (0)