DEV Community

Cover image for Building, Securing, and Deploying a Go App with GitLab CI/CD EP 5.1: Container Image Scanning(GAR) with Trivy in GitLab CI/CD
Booranasak Kanthong
Booranasak Kanthong

Posted on

Building, Securing, and Deploying a Go App with GitLab CI/CD EP 5.1: Container Image Scanning(GAR) with Trivy in GitLab CI/CD

Introduction

Welcome back! Last time, you learned how to build and push your Docker image to Google Artifact Registry (GAR) using GitLab CI/CD. But before we ship our app to production, let’s make sure our Docker image is safe by scanning it for vulnerabilities using a tool called Trivy.

In this episode, you’ll learn:

  • What Trivy is and why you need image scanning
  • How to add Trivy to your GitLab CI/CD pipeline
  • How to read and act on your scan results

1. What is Trivy and Why Scan Your Images?

Trivy is an easy-to-use, open-source tool that scans Docker images for security vulnerabilities.
Just like you wouldn’t install software from an unknown source, you shouldn’t run containers with hidden risks.

Why scan?

  • Containers often include software from many sources
  • Vulnerabilities can sneak into your images even if your code is safe
  • Scanning helps you find and fix risks before they hit production


2. How Trivy Fits in Your Pipeline

Here’s how your pipeline flow looks now:

[GitLab Pipeline]
      |
      v
[Build Docker Image]
      |
      v
[Push Image to GAR]
      |
      v
[Scan Image with Trivy]
      |
      v
[Ready for Deploy!]
Enter fullscreen mode Exit fullscreen mode

3. Adding Trivy to Your .gitlab-ci.yml

Here’s a simple Trivy job based on your previous setup:

image_scan:
  stage: sca_image
  image: google/cloud-sdk:latest
  variables: 
    TRIVY_VERSION: "0.54.1"
  before_script:
    - echo "Install Docker"
    - apt-get update && apt-get install -y docker.io curl ca-certificates tar gzip
    - echo "Setting up Google Cloud authentication..."
    - echo "$GCP_SA_KEY" | base64 -d > $CI_PROJECT_DIR/gcp-key.json
    - gcloud auth activate-service-account --key-file=$CI_PROJECT_DIR/gcp-key.json
    - gcloud config set project YOUR_PROJECT_ID
    - gcloud auth configure-docker asia-southeast1-docker.pkg.dev --quiet
    - curl -fsSL https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz | tar -xz -C /usr/local/bin trivy
    - trivy --version
  script:
    - echo "Scanning Docker image for vulnerabilities..."
    - trivy image $IMAGE_TAG_GOOGLE | tee trivy-report.txt
  allow_failure: true
  artifacts:
    name: "trivy-report"
    paths:
      - trivy-report.txt
    expire_in: 1 week
Enter fullscreen mode Exit fullscreen mode

What’s happening here?

  • Installs Docker and Trivy
  • Authenticates with Google Cloud so the image can be pulled
  • Runs Trivy to scan your image in GAR
  • Saves the results as trivy-report.txt
  • The job is allowed to fail (allow_failure: true) so a vulnerability won’t block your learning or experimenting

4. Merge scripts together

It will look like this

image: docker:latest

services:
  - docker:dind

variables:
  DOCKER_HOST: tcp://docker:2375/
  DOCKER_TLS_CERTDIR: ""
  GO_VERSION: "1.24.3"
  SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
  IMAGE_NAME_GOOGLE: "booranasak-bank-api-golang"
  IMAGE_VERSION_TAG: "1.0.0"
  IMAGE_TAG_GOOGLE: "asia-southeast1-docker.pkg.dev/escian/booranasak-artifact-registry-docker/$IMAGE_NAME_GOOGLE:$IMAGE_VERSION_TAG"  
  IMAGE_TAR: todo-api.tar
  GIT_DEPTH: "0"

stages:
  - lint
  - test
  - sast
  - build
  - push
  - sca_image

.go-job-template: &go-job-template
  image: debian:bullseye
  before_script:
    - apt update && apt install -y curl git tar gzip
    - curl -LO https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz
    - rm -rf /usr/local/go && tar -C /usr/local -xzf go${GO_VERSION}.linux-amd64.tar.gz
    - export PATH="/usr/local/go/bin:$PATH"
    - go version

lint_golint:
  stage: lint
  <<: *go-job-template
  script:
    - export PATH="/usr/local/go/bin:$PATH"
    - go install golang.org/x/lint/golint@latest
    - export PATH="$PATH:$(go env GOPATH)/bin"
    - echo "Linting files:"
    - find . -name '*.go'
    - golint ./... | tee lint-report.txt
    - echo "--- Lint report preview ---"
    - cat lint-report.txt || echo "lint-report.txt is empty"
  allow_failure: true
  artifacts:
    name: "golint-report"
    paths:
      - lint-report.txt
    expire_in: 1 week

unit_test_and_coverage:
  stage: test
  <<: *go-job-template
  script:
    - export PATH="/usr/local/go/bin:$PATH"
    - go mod tidy
    - go test -v -cover ./...
    - go test -v -coverprofile=coverage.out ./...
  artifacts:
    paths:
      - coverage.out
    expire_in: 1 hour

sonarqube-check:
  stage: sast
  image:
    name: sonarsource/sonar-scanner-cli:11
    entrypoint: [""]
  dependencies:
    - unit_test_and_coverage
  script: 
    - sonar-scanner -Dsonar.host.url="${SONAR_HOST_URL}" -Dsonar.go.coverage.reportPaths=coverage.out -Dsonar.exclusions=**/*_test.go
  allow_failure: true
  rules:
    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
    - if: $CI_COMMIT_BRANCH == 'master'
    - if: $CI_COMMIT_BRANCH == 'main'
    - if: $CI_COMMIT_BRANCH == 'develop'

sonarqube-vulnerability-report:
  stage: sast
  image:
    name: sonarsource/sonar-scanner-cli:11
    entrypoint: [""]
  script:
    - 'curl -u "${SONAR_TOKEN}:" "${SONAR_HOST_URL}/api/issues/gitlab_sast_export?projectKey=booranasak-golang-sast&branch=${CI_COMMIT_BRANCH}&pullRequest=${CI_MERGE_REQUEST_IID}" -o gl-sast-sonar-report.json'
  allow_failure: true
  rules:
    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
    - if: $CI_COMMIT_BRANCH == 'master'
    - if: $CI_COMMIT_BRANCH == 'main'
    - if: $CI_COMMIT_BRANCH == 'develop'
  artifacts:
    expire_in: 1 day
    reports:
      sast: gl-sast-sonar-report.json

build_image:
  stage: build
  image: docker:latest
  script:
    - docker build -t $IMAGE_TAG_GOOGLE .
    - docker save $IMAGE_TAG_GOOGLE -o $IMAGE_TAR
  artifacts:
    name: "docker-image-tar"
    paths:
      - $IMAGE_TAR
    expire_in: 1 hour

push_image_to_registry:
  stage: push
  image: google/cloud-sdk:latest
  dependencies:
    - build_image
  before_script:
    - echo "Install Docker"
    - apt-get update && apt-get install -y docker.io
    - echo "Setting up Google Cloud authentication..."
    - echo "$GCP_SA_KEY" | base64 -d > $CI_PROJECT_DIR/gcp-key.json
    - gcloud auth activate-service-account --key-file=$CI_PROJECT_DIR/gcp-key.json
    - gcloud config set project escian
    - gcloud auth configure-docker asia-southeast1-docker.pkg.dev --quiet
  script:
    - docker load -i $IMAGE_TAR
    - docker push $IMAGE_TAG_GOOGLE

image_scan:
  stage: sca_image
  image: google/cloud-sdk:latest
  variables: 
    TRIVY_VERSION: "0.54.1"
  before_script:
    - echo "Install Docker"
    - apt-get update && apt-get install -y docker.io curl ca-certificates tar gzip
    - echo "Setting up Google Cloud authentication..."
    - echo "$GCP_SA_KEY" | base64 -d > $CI_PROJECT_DIR/gcp-key.json
    - gcloud auth activate-service-account --key-file=$CI_PROJECT_DIR/gcp-key.json
    - gcloud config set project escian
    - gcloud auth configure-docker asia-southeast1-docker.pkg.dev --quiet
    - curl -fsSL https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz | tar -xz -C /usr/local/bin trivy
    - trivy --version
  script:
    - echo "Scanning Docker image for vulnerabilities..."
    - trivy image $IMAGE_TAG_GOOGLE
  allow_failure: true
Enter fullscreen mode Exit fullscreen mode

5. Viewing and Understanding Your Trivy Scan Results

  • Go to your pipeline in GitLab.

  • Click at your recent run pipeline

  • Click on the image_scan job.

*You can view the Trivy report directly from the output logs, or configure your pipeline to save the Trivy report as an artifact. This allows you to download or preview the trivy-report.txt artifact.

Output logs(this from other pipeline)

What do the results mean?

  • Trivy will list all detected vulnerabilities, their severity, and where they were found (like OS packages or app dependencies).
  • Focus on “HIGH” or “CRITICAL” vulnerabilities—those should be fixed first.
  • If the Status is fixed, you’re already protected and don’t need to take action.
  • But if the status is affected, will_not_fix, or fix_deferred, you should pay close attention—these vulnerabilities are not yet resolved and may still put your project at risk. Review them carefully and patch or mitigate as soon as possible.

In our Code and Image that being use there are no vulnerabilities


6. What If I Find Vulnerabilities?

  • Read the Trivy output carefully—it tells you which package (and version) is affected.
  • Many issues can be fixed by updating your Dockerfile or base image.
  • Sometimes, vulnerabilities are in dependencies you don’t use directly. Research if an update or patch is available.
  • If you’re not sure, search the CVE ID online for advice.

7. Tips and Best Practices

  • Scan every new build: Don’t skip scanning for speed; automate it!
  • Keep Trivy updated: Vulnerabilities are found all the time.
  • Don’t ignore “HIGH”/“CRITICAL” issues: If you must, at least understand the risk.
  • Automate alerting: Advanced: send scan failures to Slack, email, etc.

8. What’s Next?

Awesome! Your pipeline now checks for vulnerabilities in every image.
Next, we’ll move on to deploying your image and running live API security scans with OWASP ZAP—so your app is not only safe inside, but also from the outside.


Summary

  • Added Trivy to your pipeline for automatic container security checks
  • Learned how to read and act on Trivy results
  • Set your project up for safer, more professional deployments

Ready for the next step? See you in Episode 6 for deploy and API security scanning with ZAP!


Top comments (0)