feat: pulumi + pipeline
This commit is contained in:
parent
2df5f3d7f9
commit
ac9b6a25bf
11 changed files with 4795 additions and 0 deletions
2
infrastructure/.gitignore
vendored
Normal file
2
infrastructure/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/bin/
|
||||
/node_modules/
|
||||
3
infrastructure/Pulumi.dev.yaml
Normal file
3
infrastructure/Pulumi.dev.yaml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
encryptionsalt: v1:Jf+BVg+eXws=:v1:MScHsvDc/TE20MSb:p+6j/0hGULp3a4VXaAzcd+vti9Mn3g==
|
||||
config:
|
||||
gcp:project: digital-equator-311106
|
||||
10
infrastructure/Pulumi.yaml
Normal file
10
infrastructure/Pulumi.yaml
Normal 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
81
infrastructure/README.md
Normal 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
168
infrastructure/index.ts
Normal 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
4415
infrastructure/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
12
infrastructure/package.json
Normal file
12
infrastructure/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
18
infrastructure/tsconfig.json
Normal file
18
infrastructure/tsconfig.json
Normal 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
36
pipeline/pipeline.yaml
Normal 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
|
||||
18
pipeline/pipeline_templates/account_mappings.json
Normal file
18
pipeline/pipeline_templates/account_mappings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
32
pipeline/pipeline_templates/gcp_auth.yaml
Normal file
32
pipeline/pipeline_templates/gcp_auth.yaml
Normal 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)
|
||||
Loading…
Add table
Reference in a new issue