A learning-focused platform and service project built for LIA preparation at Kaerus Software.
The project combines:
- Go REST API
- SQLite, PostgreSQL and CockroachDB
- Docker and Docker Compose
- Terraform on Google Cloud
- Makefile automation
- Integration testing
- GoCD continuous delivery
- GitHub Actions CI and software supply-chain security
flowchart TD
Repo["GitHub repository"] --> Actions["GitHub Actions"]
Repo --> GoCD["Local GoCD"]
Actions --> Checks["Unit tests, integration tests<br/>govulncheck and Trivy"]
Checks --> Image["Build API image<br/>Trivy image scan"]
Image --> SBOM["Generate SPDX SBOM<br/>Cosign sign and verify"]
SBOM --> Artifact["Upload SBOM and signature bundle"]
GoCD --> Verify["Unit tests, integration tests<br/>Trivy repository scan"]
Verify --> LocalImage["Build and scan local API image"]
Repo --> Terraform["Terraform"]
Terraform --> GCP["Google Cloud infrastructure<br/>IAM, remote state and budget"]
GCP --> Registry["Artifact Registry"]
GCP --> CloudRun["Private Cloud Run"]
Manual["Manual cloud image build and push"] --> Registry
Registry --> CloudRun
CloudRun --> API["Go REST API<br/>Health and service endpoints"]
API --> SQLite["SQLite: ephemeral on Cloud Run"]
GitHub Actions tests and scans the project, then generates, signs and verifies an SBOM. GoCD provides a separate local verification and image-build pipeline. Terraform manages the cloud infrastructure; cloud image publishing and deployment are manual. The initial Cloud Run deployment uses ephemeral SQLite storage.
The API currently provides:
GET /healthPOST /servicesGET /services- SQLite support
- PostgreSQL support through pgx
- CockroachDB support through the PostgreSQL protocol
- Automatic database migrations
- Unit and HTTP handler tests
The platform currently provides:
- Multi-stage Docker image for the Go API
- Docker Compose for the API, PostgreSQL and CockroachDB
- Persistent local Docker volumes
- Docker container health checks
- Local GoCD server and custom GoCD build agent
- GoCD agent with Go, Git, Make, Docker CLI and Trivy
- Parallel unit, integration-test and security-scan jobs
- Docker image build and security scan after successful verification
- Pipeline security gates for HIGH and CRITICAL findings
- GitHub Actions CI for pull requests and pushes to
main - Go vulnerability analysis with
govulncheck - SPDX JSON SBOM generation for the API Docker image
- Keyless SBOM signing with Cosign and GitHub Actions OIDC
- Signature verification against the expected workflow identity and issuer
- SBOM and Sigstore signature bundle uploaded as a CI artifact
- Automatic pipeline scheduling for changes on
main - Makefile automation for Go, Docker, Terraform and GoCD
- Terraform-managed Google Cloud infrastructure
- Protected GCS remote Terraform state
- Artifact Registry Docker repository
- Versioned
linux/amd64cloud container image - Dedicated Cloud Run service account
- Private Cloud Run deployment
- Scale-to-zero with a maximum of one instance
- 100 SEK monthly GCP budget with alert thresholds
- Go 1.27 or later
- GNU Make
- Git
- Docker Desktop
- Trivy
- Terraform
- Google Cloud CLI
Go and Make are required for local development. Docker is required for container workflows; Trivy is required for local security scans. Terraform and the Google Cloud CLI are needed only for cloud infrastructure work. CI installs its own Go, Trivy and Cosign tools.
git clone https://github.com/MusdafOmar/platform-service.git
cd platform-service
make test
make integration-test
make runIn a second terminal, check http://localhost:8080/health. Stop the local API with Control + C before starting another API on the same port.
Build the API Docker image:
make docker-buildThe image is created with this name:
platform-service:local
Build and start the containerized API:
make api-upCheck the container status:
make api-statusThe status should eventually show:
healthy
Test the health endpoint:
curl http://localhost:8080/healthExpected response:
{"status":"ok","service":"platform-service"}Follow the API container logs:
make api-logsPress Control + C to stop following the logs. This does not stop the container.
Stop and remove the API container and its network:
make api-downThe named volume below preserves the SQLite database when the container is removed:
platform-service-api_sqlite_data
Do not add -v to the Compose down command unless you intentionally want to delete the stored SQLite data.
The Dockerfile uses a multi-stage build.
The builder stage:
- Uses the Go Alpine image.
- Downloads the Go dependencies.
- Compiles the API into a Linux binary.
- Produces the
platform-serviceexecutable.
The runtime stage:
- Uses a smaller Alpine Linux image.
- Installs CA certificates.
- Creates a non-root application user.
- Copies only the compiled binary.
- Stores SQLite data under
/app/data. - Exposes port
8080.
The application runs as the non-root user:
app
This reduces the privileges available inside the running container.
The .dockerignore file prevents unnecessary or sensitive files from being sent to Docker during a build.
Ignored files include:
- Git history
- Editor settings
- Environment files
- Local database files
- Build output
- Terraform state
- Documentation and deployment files that are not required for compilation
This keeps the build context smaller and reduces accidental inclusion of local secrets. It is not a substitute for reviewing files and scanning for secrets.
SQLite is the default database:
make runThe local database is stored in:
platform-service.db
Stop the API with:
Control + C
Create the private environment file:
cp deployments/docker/.env.example deployments/docker/.envStart PostgreSQL:
make postgres-upCheck its status:
make postgres-statusRun the API with PostgreSQL:
make run-postgresStop the API with Control + C, then stop PostgreSQL:
make postgres-downThe named Docker volume preserves PostgreSQL data when the container is stopped or removed.
Start CockroachDB:
make cockroach-upCreate the application database:
make cockroach-initCheck its status:
make cockroach-statusRun the API with CockroachDB:
make run-cockroachThe CockroachDB administration interface is available at:
http://localhost:8081
Stop the API with Control + C, then stop CockroachDB:
make cockroach-downThe named Docker volume preserves CockroachDB data when the container is stopped or removed.
The local CockroachDB container uses insecure mode for development only. Production environments must use authentication and encrypted connections.
The application selects its database using environment variables:
DB_DRIVER
DB_DSN
Default local configuration:
DB_DRIVER=sqlite
DB_DSN=platform-service.db
The Docker container uses:
DB_DRIVER=sqlite
DB_DSN=/app/data/platform-service.db
PostgreSQL uses:
DB_DRIVER=pgx
The PostgreSQL DSN contains:
- Database username
- Database password
- Database host
- Database port
- Database name
- SSL configuration
CockroachDB also uses:
DB_DRIVER=pgx
The local development DSN is:
postgresql://root@localhost:26257/platform_service?sslmode=disable
CockroachDB can reuse pgx because it supports the PostgreSQL wire protocol.
Database migrations are stored in:
migrations/
The first migration creates the services table:
migrations/001_create_services.sql
The Go application embeds the SQL migration files into the compiled binary.
When the API starts, it automatically applies the migrations before starting the HTTP server.
The same migration currently works with:
- SQLite
- PostgreSQL
- CockroachDB
The API starts at:
http://localhost:8080
Request:
curl -i http://localhost:8080/healthExpected status:
HTTP/1.1 200 OK
Expected response:
{"status":"ok","service":"platform-service"}Request:
curl -i \
-X POST \
-H "Content-Type: application/json" \
-d '{"name":"catalog-api","description":"LIA platform service"}' \
http://localhost:8080/servicesExpected status:
HTTP/1.1 201 Created
Example response:
{
"id": "generated-uuid",
"name": "catalog-api",
"description": "LIA platform service",
"created_at": "generated-timestamp"
}The API:
- Validates the request.
- Generates a UUID.
- Creates a UTC timestamp.
- Inserts the service into the selected database.
- Returns the created record as JSON.
Request:
curl -i http://localhost:8080/servicesExpected status:
HTTP/1.1 200 OK
Example response:
{
"services": [
{
"id": "generated-uuid",
"name": "catalog-api",
"description": "LIA platform service",
"created_at": "generated-timestamp"
}
]
}Run the unit and handler tests:
make testFormat the Go code and run the unit and handler tests:
make checkRun the black-box API integration test:
make integration-testThe integration test performs a fresh run every time. It:
- Builds the real API binary
- Creates a temporary SQLite database
- Selects an available local port
- Starts the API as a separate process
- Waits for
GET /healthto return successfully - Creates a service with
POST /services - Retrieves the service with
GET /services - Stops the API and removes temporary test files automatically
The test suite covers:
- Health endpoint
- Create-service handler
- List-services handler
- SQLite database connection
- Service repository
- Service listing
- Database migrations
- Full API service lifecycle
Build and start the GoCD server and custom build agent:
make gocd-upOpen the GoCD interface:
http://localhost:8153/go
Check the container status:
make gocd-statusFollow the GoCD logs:
make gocd-logsThe pipeline contains two stages:
verify
├── unit-tests
├── integration-tests
└── security-scan
build
└── docker-build
└── security-image-scan
The three verification jobs run independently. The security job scans the source code, Go dependencies, configuration and secrets with Trivy.
When all verification jobs pass, GoCD starts the build stage. It builds platform-service:local and scans the resulting Docker image. HIGH or CRITICAL security findings fail the relevant job.
Changes pushed to the main branch automatically schedule the pipeline.
Stop GoCD:
make gocd-downThe named Docker volumes preserve the GoCD server configuration and agent data.
The workflow is defined in .github/workflows/ci.yaml. It runs on pull requests targeting main, pushes to main, and manual dispatch.
| Job | Checks | Dependency |
|---|---|---|
| Unit tests | Go formatting and make test |
None |
| Integration tests | make integration-test |
None |
| Security scan | govulncheck and make security-scan |
None |
| Build and scan image | Docker build, Trivy image scan, SBOM generation, Cosign signing, signature verification and artifact upload | All three verification jobs must pass |
The first three jobs are independent. The image job starts only after all three succeed. Jobs use ubuntu-24.04 to avoid an automatic major Ubuntu-version change; the hosted runner still receives updates.
Trivy scans Go dependencies, configuration and secrets in the repository, and scans the built image. HIGH or CRITICAL findings fail the relevant Trivy scan. govulncheck adds Go-specific vulnerability analysis. A clean scan means no findings were detected under the configured checks and current vulnerability database; it does not guarantee the application is vulnerability-free.
An SBOM (Software Bill of Materials) is an inventory of the components found in the built image. Trivy generates SPDX 2.3 JSON, and Cosign signs the file using GitHub Actions OIDC without a stored long-lived signing key.
Before uploading, CI verifies the signature against:
- The exact workflow identity derived from
github.workflow_ref - The issuer
https://token.actions.githubusercontent.com
The successful verification log contains Verified OK.
The platform-service-sbom artifact contains:
| File | Purpose |
|---|---|
platform-service.spdx.json |
Component inventory for the built API image |
platform-service.spdx.sigstore.json |
Signature bundle and verification metadata |
To download it, open the repository's Actions tab, select a successful run, and download platform-service-sbom from Artifacts. Extract the download and open the JSON files with VS Code. Artifacts are configured for 30-day retention.
This signs the SBOM file, not the Docker image. A valid signature establishes file integrity and signer identity; it does not prove that the inventory is complete or that the software is safe.
make help
make fmt
make test
make integration-test
make run
make run-postgres
make run-cockroach
make build
make check
make clean
make docker-build
make api-up
make api-down
make api-status
make api-logs
make postgres-up
make postgres-down
make postgres-status
make cockroach-up
make cockroach-init
make cockroach-down
make cockroach-status
make gocd-up
make gocd-down
make gocd-status
make gocd-logs
make security-scan
make security-image-scan| Command | Purpose |
|---|---|
make run |
Run the API locally with SQLite |
make run-postgres |
Run the API locally with PostgreSQL |
make run-cockroach |
Run the API locally with CockroachDB |
make build |
Compile the Go API |
make test |
Run unit and handler tests |
make integration-test |
Build and test the complete API lifecycle |
make check |
Format the code and run all tests |
make clean |
Remove generated build files |
| Command | Purpose |
|---|---|
make docker-build |
Build the API Docker image |
make api-up |
Build and start the containerized API |
make api-down |
Stop and remove the API container |
make api-status |
Show the API container status |
make api-logs |
Follow the API container logs |
| Command | Purpose |
|---|---|
make postgres-up |
Start PostgreSQL |
make postgres-down |
Stop PostgreSQL |
make postgres-status |
Show PostgreSQL status |
| Command | Purpose |
|---|---|
make cockroach-up |
Start CockroachDB |
make cockroach-init |
Create the application database |
make cockroach-down |
Stop CockroachDB |
make cockroach-status |
Show CockroachDB status |
| Command | Purpose |
|---|---|
make gocd-up |
Build and start the GoCD server and agent |
make gocd-down |
Stop and remove the GoCD containers |
make gocd-status |
Show the GoCD container status |
make gocd-logs |
Follow the GoCD logs |
| Command | Purpose |
|---|---|
make security-scan |
Scan source code, dependencies, configuration and secrets |
make security-image-scan |
Build and scan the local API Docker image |
platform-service/
├── .github/
│ └── workflows/
│ └── ci.yaml
├── cmd/
│ └── api/
│ ├── main.go
│ ├── main_test.go
│ ├── services.go
│ ├── services_test.go
│ ├── list_services.go
│ └── list_services_test.go
├── deployments/
│ ├── docker/
│ │ ├── .env.example
│ │ ├── api.compose.yaml
│ │ ├── compose.yaml
│ │ └── cockroach.compose.yaml
│ └── gocd/
│ ├── agent.Dockerfile
│ └── compose.yaml
├── docs/
├── infrastructure/
│ └── terraform/
├── internal/
│ ├── config/
│ ├── database/
│ │ ├── database.go
│ │ └── database_test.go
│ ├── server/
│ └── service/
│ ├── repository.go
│ ├── repository_test.go
│ ├── list.go
│ └── list_test.go
├── migrations/
│ ├── 001_create_services.sql
│ ├── migrations.go
│ └── migrations_test.go
├── scripts/
├── tests/
│ └── integration/
│ └── api_test.go
├── .dockerignore
├── .gitignore
├── Dockerfile
├── go.mod
├── go.sum
├── Makefile
└── README.md
The repository uses numbered SQL parameters:
VALUES ($1, $2, $3, $4)This parameter style works with:
- SQLite
- PostgreSQL
- CockroachDB
This allows the same repository implementation to support all three databases.
Database storage differs by environment:
| Environment | Storage |
|---|---|
| Local SQLite | platform-service.db |
| Dockerized SQLite | platform-service-api_sqlite_data volume |
| PostgreSQL | docker_postgres_data volume |
| CockroachDB | docker_cockroach_data volume |
A container can be removed and recreated while its named volume remains available.
Running Compose down normally preserves the volume:
make api-downDeleting the volume is destructive and removes its stored database data.
The cloud sections describe the initial deployment and configured infrastructure. Current billing and live-service availability have not been revalidated for the final handoff. A GitHub push or successful CI run does not automatically deploy to GCP. Confirm billing and review a Terraform plan before applying changes or pushing cloud images.
The Google Cloud project is:
lia-platform-musdaf-2026
The following foundation work is complete:
- Google Cloud CLI authentication
- Application Default Credentials
- Billing enabled
- Monthly budget of 100 SEK configured
- Budget thresholds at 25%, 50%, 90% and 100%
- Required Google Cloud APIs enabled
- Regional resources configured in
europe-north1
Terraform manages the Google Cloud infrastructure under:
infrastructure/terraform/
The configuration currently manages:
- Required Google Cloud APIs
- Artifact Registry Docker repository
- Dedicated Cloud Run service account
- Private Cloud Run service
- Protected GCS bucket for remote Terraform state
- Terraform outputs for repository and service information
Initialize Terraform:
make terraform-initFormat and validate the configuration:
make terraform-checkPreview infrastructure changes:
make terraform-planApply reviewed infrastructure changes:
make terraform-applyDisplay Terraform outputs:
make terraform-outputTerraform state is stored remotely in:
gs://lia-platform-musdaf-2026-terraform-state/platform-service/development/
The state bucket uses:
- Object versioning
- Uniform bucket-level access
- Public access prevention
- Terraform destroy protection
Private values belong in:
infrastructure/terraform/terraform.tfvars
This file is ignored by Git. Use the tracked example as a template:
infrastructure/terraform/terraform.tfvars.example
The Docker repository is:
europe-north1-docker.pkg.dev/lia-platform-musdaf-2026/platform-service
Authenticate Docker:
make artifact-authBuild a Cloud Run-compatible linux/amd64 image and push it:
make cloud-image-pushThe default image is:
europe-north1-docker.pkg.dev/lia-platform-musdaf-2026/platform-service/platform-service:v0.1.0
A different version can be pushed with:
make cloud-image-push CLOUD_IMAGE_TAG=v0.1.1The Terraform-managed Cloud Run service is:
platform-service
It uses:
- A dedicated service account
- Authenticated access
- Scale-to-zero
- Maximum one running instance
- One vCPU
- 512 MiB memory
- Container port 8080
- Image version
v0.1.0
Display the service URL:
terraform -chdir=infrastructure/terraform \
output -raw cloud_run_service_urlStore the URL temporarily:
LIA_SERVICE_URL="$(
terraform -chdir=infrastructure/terraform \
output -raw cloud_run_service_url
)"Test the authenticated health endpoint:
curl \
-H "Authorization: Bearer $(gcloud auth print-identity-token)" \
"$LIA_SERVICE_URL/health"Expected response:
{"status":"ok","service":"platform-service"}Anonymous requests return 403 Forbidden because the service is private.
The initial Cloud Run deployment uses SQLite at:
/tmp/platform-service.db
Cloud Run container filesystems are ephemeral. Records can disappear when the instance restarts or scales down.
SQLite in Cloud Run is therefore used only to verify that the deployed container works. A durable cloud deployment requires an external database such as PostgreSQL.
- Local development environment — complete
- Google Cloud foundation — complete
- Go service foundation — complete
- SQLite database integration — complete
- PostgreSQL integration — complete
- Database configuration and CockroachDB compatibility — complete
- Dockerize the Go API — complete
- Terraform infrastructure — complete
- Integration testing — complete
- GoCD pipeline — complete
- Trivy security scanning — complete
- Google Cloud deployment — initial Cloud Run deployment complete
- GitHub Actions and signed SBOM - complete
- Final validation and API demonstration - complete; submission handoff documented
Validation results and known limitations: Final validation.
- Local development does not establish production readiness or application-level authentication. Cloud Run's authenticated access protects the cloud service at the platform boundary.
- Local CockroachDB uses insecure mode. Production database connections require appropriate authentication and TLS.
- The automated lifecycle integration test uses SQLite. This is not an automated PostgreSQL or CockroachDB integration-test matrix.
- Cloud Run SQLite is ephemeral, and the current CI does not deploy to GCP.
- GoCD pipeline settings configured through the local server must be preserved or recreated; the Compose setup alone is not proof that the pipeline is reproducible from a fresh clone.
- Repository scans can have incomplete Terraform evaluation when variables such as
project_idare not supplied. - Secrets, private
.envfiles and Terraform state must not be committed. Budget alerts are notifications, not a spending cap. - SBOM signing is implemented; Docker-image signing, durable cloud storage, automated deployment and further CI hardening remain future improvements.
Before marking milestone 14 complete:
- Clone the repository into a new directory and follow the quick-start instructions.
- Run
make test,make integration-test,make security-scanandmake security-image-scanwith the required tools available. - Start the Docker API with
make api-up, confirm its health, and demonstrate creating and listing a service. Stop it withmake api-down. - Check the latest complete GitHub Actions run: all four jobs must pass, signature verification must report
Verified OK, and the artifact must contain both JSON files. - Record cloud availability separately; do not present the historical deployment as currently live without checking it.
- Review the README on GitHub, capture demonstration evidence and confirm
git status --shortis empty after committing and pushing.
For a short demonstration, show the API lifecycle, the three independent verification jobs, the dependent image job, the signed artifact and the signature-verification log. Explain one remediated dependency vulnerability and the ephemeral-cloud-storage limitation.