feat: pulumi + pipeline

This commit is contained in:
gerrystev 2026-04-16 12:38:49 +08:00
parent 2df5f3d7f9
commit ac9b6a25bf
11 changed files with 4795 additions and 0 deletions

2
infrastructure/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/bin/
/node_modules/

View file

@ -0,0 +1,3 @@
encryptionsalt: v1:Jf+BVg+eXws=:v1:MScHsvDc/TE20MSb:p+6j/0hGULp3a4VXaAzcd+vti9Mn3g==
config:
gcp:project: digital-equator-311106

View file

@ -0,0 +1,10 @@
name: picoclaw-cloud-run
description: A minimal Google Cloud TypeScript Pulumi program
runtime:
name: nodejs
options:
packagemanager: npm
config:
pulumi:tags:
value:
pulumi:template: gcp-typescript

81
infrastructure/README.md Normal file
View file

@ -0,0 +1,81 @@
# Pulumi GCP TypeScript Template
A minimal Google Cloud Storage bucket example using Pulumi and TypeScript. This template helps you get started quickly with a basic Pulumi program on GCP.
## Overview
This template provisions a Google Cloud Storage bucket in the `US` region and exports its URL. It demonstrates how to use the Pulumi GCP provider with TypeScript.
## Providers
- `@pulumi/pulumi`
- `@pulumi/gcp`
## Resources Created
- **Storage Bucket** (`gcp.storage.Bucket`)
## Outputs
- `bucketName` The URL of the created Storage Bucket.
## When to Use
Use this template when you:
- Want a quick, minimal example of provisioning GCP resources with Pulumi.
- Are exploring Pulumi and TypeScript on Google Cloud.
- Need a starting point for building more complex GCP infrastructure in TypeScript.
## Prerequisites
- Node.js installed on your machine.
- Pulumi CLI installed.
- A Google Cloud project.
- GCP credentials configured (for example, via `gcloud auth login` or the `GOOGLE_APPLICATION_CREDENTIALS` environment variable).
## Getting Started
Create a new Pulumi project from this template:
```bash
pulumi new gcp-typescript
```
Follow the interactive prompts to set:
- Project name and description.
- `gcp:project` (the target Google Cloud project ID).
## Project Layout
```
.
├── Pulumi.yaml # Pulumi project definition and template metadata
├── index.ts # Entry point for the Pulumi program
├── package.json # Node.js dependencies and metadata
└── tsconfig.json # TypeScript compiler configuration
```
## Configuration
This template recognizes the following configuration values:
- `gcp:project` The Google Cloud project where resources will be deployed.
Set this value in your stack with:
```bash
pulumi config set gcp:project YOUR_PROJECT_ID
```
## Next Steps
- Customize the storage bucket (e.g., change location, storage class, access policies).
- Add more GCP resources such as Compute Engine instances, Pub/Sub topics, or Firestore databases.
- Explore the full Pulumi GCP provider documentation:
https://www.pulumi.com/docs/reference/pkg/gcp/
- Learn more about Pulumi with TypeScript:
https://www.pulumi.com/docs/get-started/typescript/
## Getting Help
If you run into issues or have questions, check out:
- Pulumi Documentation: https://www.pulumi.com/docs/
- Community Slack: https://slack.pulumi.com/
- GitHub Issues: https://github.com/pulumi/pulumi/issues

168
infrastructure/index.ts Normal file
View file

@ -0,0 +1,168 @@
import * as pulumi from "@pulumi/pulumi";
import * as gcp from "@pulumi/gcp";
const config = new pulumi.Config();
const gcpConfig = new pulumi.Config("gcp");
const project = gcpConfig.require("project");
const region = config.get("region") ?? "asia-southeast1";
const imageTag = config.get("imageTag") ?? "latest";
const imageName = config.get("imageName") ?? "picoclaw";
const PICOCLAW_IMAGE = pulumi.interpolate`${region}-docker.pkg.dev/enterprise-automation-352103/container-repo/${imageName}:${imageTag}`;
// ─────────────────────────────────────────────
// Look up pre-existing secrets in Secret Manager
// ─────────────────────────────────────────────
const awsAccessKeySecret = gcp.secretmanager.Secret.get(
"picoclaw-aws-access-key-id",
`projects/${project}/secrets/PICOCLAW_AWS_ACCESS_KEY_ID`,
);
const awsSecretKeySecret = gcp.secretmanager.Secret.get(
"picoclaw-aws-secret-access-key",
`projects/${project}/secrets/PICOCLAW_AWS_SECRET_ACCESS_KEY`,
);
const awsRegionNameSecret = gcp.secretmanager.Secret.get(
"picoclaw-aws-region-name",
`projects/${project}/secrets/PICOCLAW_AWS_REGION_NAME`,
);
const launcherTokenSecret = gcp.secretmanager.Secret.get(
"picoclaw-launcher-token",
`projects/${project}/secrets/PICOCLAW_LAUNCHER_TOKEN`,
);
// ─────────────────────────────────────────────
// Dedicated service account for the Cloud Run service
// ─────────────────────────────────────────────
const gatewayServiceAccount = new gcp.serviceaccount.Account("picoclaw-gateway-sa", {
project,
accountId: "picoclaw-gateway",
displayName: "PicoClaw Gateway Service Account",
});
// Grant the service account secretAccessor at the project level so it can
// read all pre-existing secrets without needing setIamPolicy on each one.
const iamSecretAccessor = new gcp.projects.IAMMember("picoclaw-sa-secret-accessor", {
project,
role: "roles/secretmanager.secretAccessor",
member: pulumi.interpolate`serviceAccount:${gatewayServiceAccount.email}`,
});
// ─────────────────────────────────────────────
// Cloud Run v2 service — picoclaw gateway
// ─────────────────────────────────────────────
const gatewayService = new gcp.cloudrunv2.Service("picoclaw-gateway", {
name: "picoclaw-gateway",
location: region,
project,
ingress: "INGRESS_TRAFFIC_ALL",
template: {
serviceAccount: gatewayServiceAccount.email,
scaling: {
minInstanceCount: 1,
maxInstanceCount: 3,
},
containers: [
{
image: PICOCLAW_IMAGE,
ports: {
containerPort: 18790,
},
envs: [
{ name: "PICOCLAW_GATEWAY_HOST", value: "0.0.0.0" },
{
name: "AWS_ACCESS_KEY_ID",
valueSource: {
secretKeyRef: {
secret: awsAccessKeySecret.secretId,
version: "latest",
},
},
},
{
name: "AWS_SECRET_ACCESS_KEY",
valueSource: {
secretKeyRef: {
secret: awsSecretKeySecret.secretId,
version: "latest",
},
},
},
{
name: "AWS_REGION",
valueSource: {
secretKeyRef: {
secret: awsRegionNameSecret.secretId,
version: "latest",
},
},
},
{
name: "AWS_DEFAULT_REGION",
valueSource: {
secretKeyRef: {
secret: awsRegionNameSecret.secretId,
version: "latest",
},
},
},
{
name: "PICOCLAW_LAUNCHER_TOKEN",
valueSource: {
secretKeyRef: {
secret: launcherTokenSecret.secretId,
version: "latest",
},
},
},
],
resources: {
limits: {
cpu: "1",
memory: "512Mi",
},
cpuIdle: false,
},
startupProbe: {
httpGet: {
path: "/health",
port: 18790,
},
initialDelaySeconds: 5,
periodSeconds: 10,
failureThreshold: 6,
},
livenessProbe: {
httpGet: {
path: "/health",
port: 18790,
},
periodSeconds: 30,
failureThreshold: 3,
},
},
],
},
}, {
dependsOn: [iamSecretAccessor],
});
// Grant invoker access only to authenticated members of the current project
new gcp.cloudrunv2.ServiceIamBinding("picoclaw-gateway-invoker", {
project,
location: region,
name: gatewayService.name,
role: "roles/run.invoker",
members: [
`projectOwner:${project}`,
`projectEditor:${project}`,
`projectViewer:${project}`,
],
});
export const serviceUrl = gatewayService.uri;
export const serviceName = gatewayService.name;
export const serviceAccountEmail = gatewayServiceAccount.email;

4415
infrastructure/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,12 @@
{
"name": "picoclaw-cloud-run",
"main": "index.ts",
"devDependencies": {
"@types/node": "^18",
"typescript": "^5.0.0"
},
"dependencies": {
"@pulumi/gcp": "^9.0.0",
"@pulumi/pulumi": "^3.113.0"
}
}

View file

@ -0,0 +1,18 @@
{
"compilerOptions": {
"strict": true,
"outDir": "bin",
"target": "es2020",
"module": "nodenext",
"moduleResolution": "nodenext",
"sourceMap": true,
"experimentalDecorators": true,
"pretty": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true
},
"files": [
"index.ts"
]
}

36
pipeline/pipeline.yaml Normal file
View file

@ -0,0 +1,36 @@
pr:
branches:
include:
- '*'
trigger:
branches:
include:
- main
variables:
- name: DockerImageName
value: 'enterprise-automation-352103/container-repo/picoclaw'
- name: Registry
value: 'europe-west4-docker.pkg.dev'
pool:
vmImage: ubuntu-latest
stages:
- stage: build
jobs:
- job: buildAndPush
steps:
- template: pipeline_templates/gcp_auth.yaml
parameters:
environment: 'automation'
- script: |
gcloud auth configure-docker $(Registry) --quiet
displayName: Configure Docker for GCR
- script: |
docker build -t $(Registry)/$(DockerImageName):latest -f docker/Dockerfile.launcher --build-arg GO_BUILD_TAGS=goolm,stdjson,bedrock .
displayName: Build docker image
- script: |
docker push $(Registry)/$(DockerImageName):latest
displayName: Push docker image

View file

@ -0,0 +1,18 @@
{
"dev": {
"project": "digital-equator-311106",
"account": "digital-equator-311106@digital-equator-311106.iam.gserviceaccount.com"
},
"uat": {
"project": "intnt-in-house-uat",
"account": "intnt-in-house-uat@intnt-in-house-uat.iam.gserviceaccount.com"
},
"prod": {
"project": "intnt-in-house-prod",
"account": "intnt-in-house-prod@intnt-in-house-prod.iam.gserviceaccount.com"
},
"automation": {
"project": "enterprise-automation-352103",
"account": "application-automation-account@enterprise-automation-352103.iam.gserviceaccount.com"
}
}

View file

@ -0,0 +1,32 @@
parameters:
- name: 'environment' # defaults for any parameters that aren't specified
type: string
steps:
- script: |
ls
pwd
PROJECT=$(jq -r '.${{parameters.environment}}.project' account_mappings.json)
ACCOUNT=$(jq -r '.${{parameters.environment}}.account' account_mappings.json)
echo "##vso[task.setvariable variable=gcpProject]$PROJECT"
echo "##vso[task.setvariable variable=gcpAccount]$ACCOUNT"
displayName: 'Set Environment Variables from JSON'
workingDirectory: scripts/pipeline_templates
env:
ENVIRONMENT: ${{ parameters.environment }}
- task: DownloadSecureFile@1
name: gcpServiceAccountKey_${{ parameters.environment }}
inputs:
secureFile: 'gcp-services-account-key-${{parameters.environment}}.json'
- script: |
echo "Authenticating with Google Cloud..."
echo $(gcpServiceAccountKey_${{ parameters.environment }}.secureFilePath)
echo $(gcpProject)
echo $(gcpAccount)
gcloud auth login --cred-file=$(gcpServiceAccountKey_${{ parameters.environment }}.secureFilePath)
gcloud config set project $(gcpProject)
gcloud config set account $(gcpAccount)
echo "##vso[task.setvariable variable=GOOGLE_APPLICATION_CREDENTIALS]$(gcpServiceAccountKey_${{ parameters.environment }}.secureFilePath)"
displayName: 'authenticate gcp'
env:
GOOGLE_APPLICATION_CREDENTIALS: $(gcpServiceAccountKey_${{ parameters.environment }}.secureFilePath)