Ship your first service: Define, Build, Deploy¶
In this lesson you take a service from nothing to running in a cloud environment with Alis Build DBD, from the CLI: you write a protobuf contract, publish it, implement it in Go, build a version from a pushed commit and deploy that version with Terraform. It is for a builder who has the CLI installed and is signed in.
You will end with a gRPC service called hello running on Cloud Run in your product's development environment, reachable by your team and visible in the console.
Before you start¶
- The CLI is installed and you are signed in. If not, follow Install the CLI first.
- You are a builder on a product. This lesson uses the build environment
acmeand the productsh; replace them with your own ids. If you have no product yet, see Create a product. - Go 1.26 or later and git are installed.
1. Clone the product's repositories¶
Every product has two repositories: the build environment's definitions repo (protobuf contracts) and the product's build repo (implementations). One command clones both and configures git credentials.
The command reports where each repository landed. You now have:
2. Create the service¶
A service is addressed by its package id: <build environment>.<product>.<service>.<version>.
{
"packageId": "acme.sh.hello.v1",
"name": "organisations/acme/products/sh/neurons/hello-v1",
"organisation": "acme",
"product": "sh",
"serviceId": "hello-v1",
"created": true,
"buildFolder": "/Users/you/alis.build/acme/build/sh/hello/v1",
"defineFolder": "/Users/you/alis.build/acme/define/acme/sh/hello/v1",
"next": ["..."]
}
The service is registered on the platform and two empty folders are created: hello/v1 (with an infra/ subfolder) in the build repo, and acme/sh/hello/v1 in the definitions repo. No files are generated.
3. Write the contract¶
Create hello.proto in the define folder.
syntax = "proto3";
package acme.sh.hello.v1;
// HelloService greets people.
service HelloService {
// Returns a greeting for the given name.
rpc SayHello(SayHelloRequest) returns (SayHelloResponse) {}
}
message SayHelloRequest {
// The name to greet.
string name = 1;
}
message SayHelloResponse {
// The greeting.
string message = 1;
}
The package line must match the service's folder: acme.sh.hello.v1 for acme/sh/hello/v1.
4. Check that it compiles¶
A dry run compiles your local files, uncommitted edits included, with the same compiler a real define uses. Nothing is published.
{
"package": "acme.sh.hello.v1",
"folder": "/Users/you/alis.build/acme/define/acme/sh/hello/v1",
"files": ["hello.proto"],
"valid": true,
"compiled": true,
"diagnostics": [],
"git": {
"uncommittedFiles": ["hello.proto"],
"unpushedCommits": 0,
"defineCommit": "..."
},
"next": "alis define acme.sh.hello.v1 --json"
}
valid is true and diagnostics is empty. If a diagnostic appears, it names the file, line and column to fix.
5. Commit and push the contract¶
A define always compiles a pushed commit, so push first.
cd ~/alis.build/acme/define
git add acme/sh/hello/v1/hello.proto
git commit -m "feat(sh.hello.v1): add HelloService"
git push
6. Write the server¶
Move to the build folder and create a Go module named after the package id, then the server. The generated Go package for your contract is imported as alis.build/acme/sh/hello.
package main
import (
"context"
"log"
"net"
"os"
pb "alis.build/acme/sh/hello"
"google.golang.org/grpc"
)
type server struct {
pb.UnimplementedHelloServiceServer
}
func (s *server) SayHello(ctx context.Context, req *pb.SayHelloRequest) (*pb.SayHelloResponse, error) {
return &pb.SayHelloResponse{Message: "Hello, " + req.GetName()}, nil
}
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
listener, err := net.Listen("tcp", ":"+port)
if err != nil {
log.Fatalf("listen: %v", err)
}
srv := grpc.NewServer()
pb.RegisterHelloServiceServer(srv, &server{})
log.Printf("hello-v1 listening on :%s", port)
if err := srv.Serve(listener); err != nil {
log.Fatal(err)
}
}
7. Define the contract and install the generated package¶
This publishes version 1.0.0 of the contract, generates language packages, and then runs the package install for Go so go.mod picks up alis.build/acme/sh/hello and gRPC.
Progress streams on stderr. The final result on stdout is the operation:
{
"schemaVersion": 1,
"name": "operations/...",
"done": true,
"status": "succeeded",
"version": "1.0.0",
"notes": "..."
}
go.mod and go.sum now list the generated package. Do not run go get for it yourself; the CLI refreshes the private registry credentials.
8. Write the Dockerfile¶
Build detects a Dockerfile in the service folder and builds it into the image neurons/hello-v1. The Go build inside the container must reach your product's private Go registry, so it authenticates first. Replace <region> with your product's default region and <product-project-id> with the projectId that alis product view acme.sh --json prints.
FROM golang:1.26 AS builder
WORKDIR /app
# Authenticate Go module downloads against the product's private registry.
ENV GOPROXY=proxy.golang.org
RUN go run github.com/GoogleCloudPlatform/artifact-registry-go-tools/cmd/auth@v0.4.0 add-locations --locations=<region>
COPY . ./
RUN go run github.com/GoogleCloudPlatform/artifact-registry-go-tools/cmd/auth@v0.4.0 refresh
ENV GOPROXY=https://<region>-go.pkg.dev/<product-project-id>/define-go,https://proxy.golang.org,direct
ENV GONOSUMDB=alis.build
RUN CGO_ENABLED=0 GOOS=linux go build -mod=readonly -o /server .
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
9. Write the infrastructure¶
Deploy runs the Terraform in infra/. The platform injects ALIS_* variables with the deployment environment's project, region and the commit that was built; you declare the ones you use.
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = ">= 6.0.0"
}
}
backend "gcs" {
bucket = "provided_at_runtime_by_alis"
prefix = "provided_at_runtime_by_alis"
}
}
provider "google" {
project = var.ALIS_OS_PROJECT
}
# Injected by Alis Build on every deploy.
variable "ALIS_OS_PROJECT" {}
variable "ALIS_OS_PRODUCT_PROJECT" {}
variable "ALIS_OS_NEURON" {}
variable "ALIS_OS_NEURON_VERSION_COMMIT_SHA" {}
variable "ALIS_REGION" {}
variable "ALIS_PRODUCT_REGION" {}
resource "google_project_service" "run" {
service = "run.googleapis.com"
disable_on_destroy = false
}
resource "google_cloud_run_v2_service" "hello" {
name = var.ALIS_OS_NEURON
location = var.ALIS_REGION
deletion_protection = false
template {
containers {
image = "${var.ALIS_PRODUCT_REGION}-docker.pkg.dev/${var.ALIS_OS_PRODUCT_PROJECT}/neurons/${var.ALIS_OS_NEURON}:${var.ALIS_OS_NEURON_VERSION_COMMIT_SHA}"
# gRPC needs end-to-end HTTP/2.
ports {
name = "h2c"
container_port = 8080
}
}
service_account = "alis-build@${var.ALIS_OS_PROJECT}.iam.gserviceaccount.com"
}
}
10. Commit and push the implementation¶
cd ~/alis.build/acme/build/sh
git add hello/v1
git commit -m "feat(sh.hello.v1): first server"
git push
11. Find the deployment environment id¶
Deploys target a deployment environment by id. Read it from the resolved context rather than guessing.
{
"organisation": "acme",
"product": "sh",
"packageId": "acme.sh.hello.v1",
"serviceFolder": "/Users/you/alis.build/acme/build/sh/hello/v1",
"defineFolder": "/Users/you/alis.build/acme/define/acme/sh/hello/v1",
"environments": [
{
"id": "<env-id>",
"displayName": "Development",
"status": "ACTIVE",
"production": false,
"projectId": "..."
}
]
}
Copy the id of the Development environment.
12. Build and deploy¶
Start the build with a deploy attached, then wait for the operation. Inside the service folder the package id can be omitted.
{
"name": "operations/...",
"done": false,
"status": "running",
"next": "alis operations wait operations/... --json"
}
Run the next command to stream progress until the build and deploy finish:
{
"schemaVersion": 1,
"name": "operations/...",
"done": true,
"status": "succeeded",
"version": "1.0.1",
"images": [{ "path": "." }],
"deployments": [
{
"name": "organisations/acme/products/sh/environments/<env-id>/deployments/hello-v1",
"state": "RUNNING",
"logsUri": "..."
}
],
"logsUri": "..."
}
status is succeeded, the version was built from your pushed commit, and the deployment is RUNNING.
13. Verify¶
Read the service's Cloud Run logs in the deployment environment:
You see the startup line from your server, hello-v1 listening on :8080. The console shows the same result: open Environments, select acme, then the product sh, then Services. The hello-v1 row shows version 1.0.1 deployed to Development.
What you did¶
You defined a contract and published it as a versioned package, implemented it against the generated code, built an immutable version from a pushed commit and deployed that version with Terraform into a real Google Cloud project. Every later change follows the same loop.
Where next¶
- Define, Build, Deploy explains why the loop has three steps and what each one guarantees.
- Organisations, products and services explains the hierarchy you worked in and why the contract lives apart from the implementation.
- Deployment environments and versions explains deployment environments, designated branches and the production gate.
- The Build how-to guides cover each step in depth.