CI/CD для Go: пишем пайплайн с нуля за 20 минут

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему ваш Go-проект просит CI/CD, как собака просит гулять

Вы написали крутой микросервис на Go. Он летает, тесты проходят, код красивый. Но каждое утро вы вручную запускаете go build, заливаете бинарник на сервер по SSH и молитесь, чтобы ничего не упало. Знакомо? Если да — у меня для вас новость: вы тратите 15% своей жизни на то, что может делать робот.

CI/CD (Continuous Integration / Continuous Deployment) — это не просто модное слово из вакансий DevOps. Это ваш личный ассистент, который:

  • запускает тесты при каждом пуше в Git;
  • собирает бинарник под Linux/amd64, даже если у вас Mac;
  • шлёт уведомления в Telegram, если сборка упала;
  • автоматически деплоит на сервер, когда вы сливаете PR в main.

В этой статье мы настроим CI/CD pipeline для Go с нуля. Без воды, без "поставьте Jenkins на 20 серверов". Только GitHub Actions, Docker и немного магии. Поехали!

Что нам понадобится

Для настройки нам потребуется:

  • Репозиторий на GitHub (можно создать тестовый прямо сейчас);
  • Go-проект с модулями (мы напишем простой HTTP-сервер);
  • Аккаунт на Docker Hub (или любом другом registry);
  • 5 минут внимания и желание автоматизировать рутину.

Весь код, который мы будем использовать, лежит в открытом доступе. Вы можете скопировать его и адаптировать под свой проект. Главное — не забудьте заменить токены и названия репозиториев на свои.

Шаг 1: Пишем простое Go-приложение

Для демонстрации возьмём минимальный HTTP-сервер. Создайте файл main.go:

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello from Go CI/CD pipeline!")
    })

    http.ListenAndServe(":8080", nil)
}

И файл main_test.go для проверки работоспособности:

package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestHelloHandler(t *testing.T) {
    req := httptest.NewRequest("GET", "/", nil)
    w := httptest.NewRecorder()

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello from Go CI/CD pipeline!")
    })

    http.DefaultServeMux.ServeHTTP(w, req)

    if w.Code != http.StatusOK {
        t.Errorf("expected status 200, got %d", w.Code)
    }
}

Инициализируем модуль:

go mod init github.com/ваш-username/go-cicd-example
go mod tidy

Проверяем, что всё работает:

go test ./... -v
go run main.go

Если видите ok и приветствие на localhost:8080 — вы великолепны. Идём дальше.

Шаг 2: Создаём Dockerfile для Go-приложения

Контейнеризация — обязательный этап современного деплоя. Мы соберём статический бинарник (без CGO) и упакуем его в минимальный образ на основе scratch или alpine. Это даст нам образ размером около 10–15 МБ вместо 300+ МБ.

Создайте файл Dockerfile в корне проекта:

FROM golang:1.22-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]

Обратите внимание: мы используем multi-stage сборку. Первый этап — компиляция в полном окружении Go. Второй — минимальный образ Alpine, куда копируется только бинарник. Это стандарт индустрии для Go-приложений.

Соберём образ локально и проверим:

docker build -t go-cicd-example .
docker run -p 8080:8080 go-cicd-example

Если всё ок — переходим к автоматизации.

Шаг 3: Настраиваем GitHub Actions — сердце нашего CI/CD

GitHub Actions — встроенный инструмент для автоматизации. Он бесплатен для публичных репозиториев и даёт 2000 минут в месяц для приватных. Нам хватит с головой.

Создайте в корне репозитория папку .github/workflows/ и внутри неё файл ci-cd.yml. Это и будет наш CI/CD pipeline для Go.

name: Go CI/CD Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    name: Run Tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.22'

      - name: Cache Go modules
        uses: actions/cache@v3
        with:
          path: ~/go/pkg/mod
          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
          restore-keys: |
            ${{ runner.os }}-go-

      - name: Run tests
        run: go test ./... -v -race -coverprofile=coverage.out

      - name: Upload coverage
        uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage.out

  build-and-push:
    name: Build and Push Docker Image
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    steps:
      - uses: actions/checkout@v4

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ secrets.DOCKER_USERNAME }}/go-cicd-example:latest

Что здесь происходит:

  • test — запускает тесты на каждый push и PR. Кэшируем модули, чтобы не качать их каждый раз;
  • build-and-push — собирает Docker-образ и пушит его в Docker Hub, но только если тесты прошли и изменения попали в main.

Важный момент: секреты DOCKER_USERNAME и DOCKER_PASSWORD нужно добавить в настройках репозитория (Settings → Secrets and variables → Actions). Никогда не храните пароли в коде!

Шаг 4: Добавляем деплой на сервер

Теперь, когда образ собран, нужно

Блоги

Рекомендаций по книгам