Skip to content

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 acme and the product sh; 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.

alis clone acme.sh

The command reports where each repository landed. You now have:

~/alis.build/acme/define        the definitions repo
~/alis.build/acme/build/sh      the product's build repo

2. Create the service

A service is addressed by its package id: <build environment>.<product>.<service>.<version>.

alis service new acme.sh.hello.v1 --json
{
  "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.

~/alis.build/acme/define/acme/sh/hello/v1/hello.proto
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.

alis define acme.sh.hello.v1 --dry-run --json
{
  "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.

Shell
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.

Shell
cd ~/alis.build/acme/build/sh/hello/v1
go mod init acme.sh.hello.v1
~/alis.build/acme/build/sh/hello/v1/main.go
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.

alis define acme.sh.hello.v1 --install --install-language go --json

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.

~/alis.build/acme/build/sh/hello/v1/Dockerfile
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.

infra/main.tf
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
}
infra/variables.tf
# 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" {}
infra/cloudrun.tf
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

Shell
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.

alis --cwd ~/alis.build/acme/build/sh/hello/v1 context view --json
{
  "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.

Shell
cd ~/alis.build/acme/build/sh/hello/v1
alis build --deploy -e <env-id> --async --json
{
  "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:

alis operations wait operations/... --json
{
  "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:

alis logs runtime -e <env-id> --since 30m

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