Почему ваш 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: Добавляем деплой на сервер
Теперь, когда образ собран, нужно