Рубрики
Uncategorized

Создание простого трубопровода CI/CD для локального тестирования с использованием GO, Docker, Minikube и сценария Bash

Что в этой статье я подробно расскажу о мыслительном процессе и реализации автоматизированного B … Tagged with Go, DevOps, Kubernetes, Bash.

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

Большая часть этого кода написана в Go и Bash. Это очень взломанная реализация, но было очень весело писать и использовать.

Отказ от ответственности: этот пост довольно Код тяжел, но я стараюсь выделить процесс мысли за кодом.

Вы можете найти код для всего этого поста здесь CICDEXAMER

Это быстрый гиф того, что мы собираемся построить:

Обратная связь важна

Самая важная часть для меня при написании какого -либо кода — это то, как быстро я могу получить отзыв.

Ранее в этом году я начал учиться в свободное время, и, когда я учился, я хотел рассказать код приложения (я писал свое первое веб -приложение в Go) и в конце концов разместил его на Heroku.

Так это то, что я делал в течение нескольких дней:

В общей сложности мне потребовалось ~ 10 минут, чтобы проверить новые функции в «производственной» среде. Нет точно плохого времени, когда вы работаете над чем-то для своего дневного занятия, но для Pet Project это было не так весело.

Дополнительным недостатком приведенного выше конвейера является то, что действительно трудно проверить функциональность, когда у меня нет согласованного подключения к Интернету, что происходит довольно часто. Поэтому я начал думать об альтернативах.

Я хотел иметь минимальную задержку между ctrl-s и тестирование изменения — вроде этой диаграммы:

Компромисс по согласованности окружающей среды

Я понял, что тестирование на Heroku не было трудным требованием для моего процесса разработки. Но я действительно хотел сходства с моей «производственной» средой в моей тестовой среде.

Я начал набросать простой трубопровод CI/CD, который мог запустить на своем ноутбуке, который позволил бы мне проверить мое приложение, работающее в Docker на Похоже облачная инфраструктура.

Кроме того, я как бы знал, что, вероятно, напишу что -то, что уже существует, но я также знал, что это будут забавные упражнения и опыт обучения.

Простое начало

Что вам нужно?

  • Go (любая версия сделает для этой статьи)
  • Некоторые знания и терминал Unix
  • Minikube
  • Kubectl
  • Докер

  • IDE или текстовый редактор (я взаимозаменяемо использую VIM и VSCODE на протяжении всей этой статьи)

  • Linux (Ubuntu 18.04 LTS — это дистрибутив, который я использую)

С чего начать?

У меня есть некоторый опыт работы с такими инструментами сборки, как Jenkins, и у меня также есть некоторый опыт работы с автоматическим конвейером сборки, привязанного к инструменту автоматического развертывания.

Таким образом, используя идеи и контекст, которые у меня были из этих инструментов. Я начал просто, я проигнорировал Heroku на данный момент и написал простой сценарий оболочки, который создаст мою базу кода GO, а затем на основе аргумента командной строки, создать и пометить изображения Docker.

Я расширил сценарий оболочки, чтобы включить kubectl командование для развертывания в Minikube после создания изображения Docker.

Я понял, что теперь, когда у меня был этот сценарий оболочки, я, по сути, создал работу и создание работы для моего приложения. Поэтому я начал собирать приложение Go для автоматизации запуска этого сценария на основе триггера.

Как именно вы это сделали?

Чтобы проиллюстрировать, как я сделал это, не усложнив этот пост с чрезмерными техническими деталями, я создал небольшой контрольный список, чтобы следовать, что эта статья расширяет:

а Создайте небольшое приложение GO, которое обслуживает HTML -страницу b. Задачи на это! в Напишите простой сценарий Bash, который создает изображение Docker d. Нажмите изображение Docker в Minikube и посмотрите, как он работает e. Напишите небольшой инструмент командной строки, который автоматически запускает скрипт

Создание тестового приложения

Давайте создадим пример приложения Go, которое запускает простой файловый сервер и обслуживает простую HTTP -страницу (которая использует bulma css , чтобы сделать его немного лучше) на ‘/’

Веб -сервер выглядит так:

package main

import (
    "log"
    "net/http"
    "os"
)

func main() {
    // for heroku since we have to use the assigned port for the app
    port := os.Getenv("PORT")
    if port == "" {
        // if we are running on minikube or just running the bin
        defaultPort := "3000"
        log.Println("no env var set for port, defaulting to " + defaultPort)
        // serve the contents of the static folder on /
        http.Handle("/", http.FileServer(http.Dir("./static")))
        http.ListenAndServe(":" + defaultPort, nil)
    } else {
        http.Handle("/", http.FileServer(http.Dir("./static")))
        log.Println("starting server on port " + port)
        http.ListenAndServe(":" + port, nil)
    }
}

HTML -файл в /статический довольно просто:


    
        
    
    CI/CD Example
    
    
    
        

Example CI/CD Stuff

Changes should make this automatically redeploy to our local test environment

Отлично, теперь, если мы построим ( go build ) Это приложение GO и запустить его, мы должны увидеть следующее в Localhost: 3000

Контейнеризация

Теперь у нас есть приложение, которое действительно круто и делает целую кучу вещей (ничего), но это не так. Мы сосредоточены на том, как заставить этого маленького парня построить себя. Перво -наперво, нам нужно сделать это.

Если у вас нет Docker, проверьте их документы здесь ( https://docs.docker.com/install/ )

Давайте создадим следующий простой 2-этап DockerFile:

# Stage 1
FROM golang:alpine as builder
RUN apk update && apk add --no-cache git
RUN mkdir /build 
ADD . /build/
WORKDIR /build
RUN go get -d -v
RUN go build -o cicdexample .
# Stage 2
FROM alpine
RUN adduser -S -D -H -h /app appuser
USER appuser
COPY --from=builder /build/ /app/
WORKDIR /app
CMD ["./cicdexample"]

Этот DockerFile сначала создает изображение застройщика с всем содержимым локального каталога, скопированного в каталог под названием /строить на рисунке. Затем он получает зависимости для нашего маленького приложения и создает его — создавая двоичный файл под названием CICDEXAMER

Второй этап на самом деле создает изображение, которое мы в конечном итоге запустим. Мы используем базовое альпийское изображение и создаем пользователя под названием Appuser который использует каталог под названием /приложение как это домашний каталог. Затем мы копируем содержимое /build каталог из изображения строителя в /приложение каталог на альпийском изображении, установите рабочий каталог на /приложение И запустите бинарный, который мы только что скопировали. Важно отметить, что мы копируем все /build Содержимое каталога, так как нам нужен каталог статических ресурсов, который не является частью бинарного.

Как только мы создали Dockerfile и сможем успешно запустить Docker Build -t Пример: тест. который производит (выход слегка обрезан):

Sending build context to Docker daemon  7.638MB
...
Successfully built 561ee4597a93
Successfully tagged example:test

Мы готовы двигаться дальше.

Избивая вещи в форму

Далее нам нужен сценарий для автоматизации процесса создания этого изображения Docker для нас. Но мы не хотим просто создать изображение с помощью скрипта, мы хотим создать цикл обратной связи, используя иди строить Так что мы не тратим время на настройку изображения Docker, когда наш код даже не компилируется.

Технически мы не должны отмечать успех из результата иди строить , но скорее сделайте ошибку, перенаправленную на файл, который мы можем затем проверить на любой контент, и сбой сигнала, если есть что -нибудь в Stderr в результате идти строить

Логические шаги для этого скрипта лучше всего представлены графически:

Сценарий выглядит следующим образом (с хорошими временными метками и выводом командной строки):

#!/bin/bash

# Timestamp Function
timestamp() {
    date +"%T"
}

# Temporary file for stderr redirects
tmpfile=$(mktemp)

# Go build
build () {
    echo "⏲️    $(timestamp): started build script..."
    echo "🏗️   $(timestamp): building cicdexample"
    go build 2>tmpfile
    if [ -s tmpfile ]; then
        cat tmpfile
        echo "❌   $(timestamp): compilation error, exiting"
        rm tmpfile
        exit 0
    fi
}

# Deploy to Minikube using kubectl
deploy() {
    echo "🌧️    $(timestamp): deploying to Minikube"
    kubectl apply -f deploy.yml
}

# Orchestrate
echo "🤖  Welcome to the Builder v0.2, written by github.com/cishiv"
if [[ $1 = "build" ]]; then
    if [[ $2 = "docker" ]]; then
        build
        buildDocker
        echo "✔️    $(timestamp): complete."
        echo "👋  $(timestamp): exiting..."
    elif [[ $2 = "bin" ]]; then
        build
        echo "✔️    $(timestamp): complete."
        echo "👋  $(timestamp): exiting..."
    else
        echo "🤔   $(timestamp): missing build argument"
    fi
else
    if [[ $1 = "--help" ]]; then
        echo "build - start a build to produce artifacts"
        echo "  docker - produces docker images"
        echo "  bin - produces executable binaries"
    else
        echo "🤔  $(timestamp): no arguments passed, type --help for a list of arguments"
    fi
fi

Запуск его с различными аргументами дает нам следующее:

./Build Build Bin (сбой компиляции)

🤖    Welcome to the Builder builder v0.2, written by github.com/cishiv
⏲️  16:40:47: started build script...
🏗️ 16:40:47: building cicdexample
# _/home/shiv/Work/dev/go/cicdexample
./main.go:25:1: syntax error: non-declaration statement outside function body
❌ 16:40:47: compilation error, exiting

./Build Build Docker (слегка обрезанный выход)

🤖    Welcome to the Builder builder v0.2, written by github.com/cishiv
⏲️  16:40:05: started build script...
🏗️ 16:40:05: building cicdexample
🐋    16:40:06: building image example:test
Sending build context to Docker daemon  7.639MB
...
Successfully tagged example:test
✔️  16:40:06: complete.
👋    16:40:06: exiting...

Это кажется разумным, мы можем скомпилировать наш код и создать изображение Docker с одной командой.

Прежде чем перейдем к следующему шагу, давайте пока посмотрим на наш контрольный список:

а Создайте небольшое приложение GO, которое обслуживает HTML -страницу ✔ беременный Задачи на это! ✔ в Напишите простой скрипт Bash, который создает изображение Docker ✔ дюймовый Нажмите изображение Docker в Minikube и посмотрите, как он работает e. Напишите небольшой инструмент командной строки, который автоматически запускает скрипт

Minikube-ing

Наконец, у нас есть какая -то автоматизация для нашего конвейера сборки, но это еще не дает нам возможности проверить наше приложение.

Введите Minikube, чтобы сохранить день (своего рода).

Мы хотим, чтобы наш тестовый конвейер был следующим образом:

Если у вас нет установки Minikube, вы можете проверить документы здесь ( https://kubernetes.io/docs/tasks/tools/install-minikube/ ) о том, как его запустить и запустить.

Вам также нужно будет захватить kubectl Анкет

Как только Minikube установлен, он так же просто, как запуск Minikube Start Чтобы запустить один кластер узлов.

Следующим шагом является настройка развертывания Kubernetes для нашего приложения, которое мы можем протолкнуть в кластер Minikube.

Поскольку это не статья о Kubernetes, я буду держать этот шаг коротким. Мы хотим иметь Deploy.yml Файл, где мы можем сказать Kubernetes создать развертывание и услугу для нашего приложения. Было бы лучше иметь отдельные файлы для создания развертывания и сервиса, но для этого примера мы просто воссоздаем их обоих каждый раз, когда хотим перераспределить.

Итак, нам нужен следующий файл.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: example
spec:
  selector:
    matchLabels:
      app: example
      tier: example
      track: stable
  template:
    metadata:
      labels:
        app: example
        tier: example
        track: stable
    spec:
      containers:
        - name: example
          image: "example:test"
          ports:
            - name: http
              containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
  name: example
spec:
  type: NodePort
  selector:
    app: example
    tier: example
  ports:
  - protocol: TCP
    port: 3000
    targetPort: http
    nodePort: 30000

Мы говорим Kubernetes создать услугу под названием Пример Это выставлено на Nodeport 30000 (так что мы можем получить к нему доступ через URL, который не меняется каждый раз, когда мы воссоздаем услугу), с приложением, работающим на порту 3000 в контейнере.

На этом этапе, поскольку мы указали Nodeport в нашем дескрипторе развертывания, мы должны быть в состоянии просто обновить нашу веб -страницу и увидеть наши изменения.

Чтобы развернуть приложение в кластер, запустите следующую команду в терминале:

./Build Build Docker && Kubectl Apply -f deploy.yml

Теперь приложение должно быть живет на кластере Minikube и выставлена на NodePort.

Чтобы получить URL для приложения, запустите эту команду:

Пример обслуживания minikube -url

Вы должны получить URL, похожий на это:

http://10.0.0.101:30000

Пример Имя службы, которое мы указали в нашем Deploy.yml файл.

Если мы перейдем к нашему URL, мы должны увидеть наше приложение.

Теперь мы можем проверить наш трубопровод в первый раз.

Сделайте изменение кода на index.html и беги:

./Build Build Docker && Kubectl Apply -f deploy.yml

Вы можете обнаружить, что это еще не работает. Выходы команды Kubectl:

deployment.apps/example unchanged
service/example unchanged

И правильно, поскольку мы не помечаем наше изображение Docker по -разному каждый раз, когда мы его создаем — Kubernetes на самом деле не признает, что наш код изменился. Быстрый взлом, чтобы исправить это, так как мы тестируем локально, — изменить нашу команду в следующее:

./Build Build Docker && Kubectl Delete Deployment Пример && kubectl Delete Service Пример && kubectl Apply -f deploy.yml

Это позволяет нам чисто воссоздавать услуги и развертывание каждый раз, когда мы заново запускаем сценарий. Важно отметить, что это очень большой взлом, и лучший способ сделать это — это пометить изображения Docker по -разному каждый раз, когда они производятся, и обновлять Deploy.yml Файл с правильным тегом изображения Docker.

Так бег:

./Build Build Docker && Kubectl Delete Deployment Пример && kubectl Delete Service Пример && kubectl Apply -f deploy.yml

Позволит нам увидеть изменение, которое мы внесли в наш HTML.

Это кажется разумным, однако, чтобы немного почистить его, давайте добавим дополнительное kubectl Команды нашему сценарию Bash.

Это достаточно просто сделать, добавив следующую функцию и слегка внести изменения в условную логику, что позволяет получить развернуть Параметр, который должен быть передан в ./Build Build ... Команда:

Функция:

# Deploy to Minikube using kubectl
deploy() {
    echo "🌧️    $(timestamp): deploying to Minikube"
    kubectl delete deployment example
    kubectl delete service example
    kubectl apply -f deploy.yml
}

Условная логика:

if [[ $1 = "build" ]]; then
    if [[ $2 = "docker" ]]; then
        if [[ $3 = "deploy" ]]; then
            build
            buildDocker
            deploy
        else
            build
            buildDocker
        fi
        echo "✔️    $(timestamp): complete."
        echo "👋  $(timestamp): exiting..."

Теперь мы можем запустить: ./Build Build Docker развертывание Чтобы довольно легко привести код, все время от компиляции до развертывания! (с временем поворота ~ 1)

Последний акт: настоящая автоматизация

Наконец, мы хотим обернуть сценарий Bash, который мы создали в специально созданное приложение GO, которое автоматизирует этот процесс.

Мы должны определить триггер для нашего конвейера сборки. На данный момент есть 2 варианта:

  1. Сборки запускаются при сочетании истекающих времени и изменений файлов.
  2. Сборки запускаются при коммитах в репозиторий GIT

Чтобы проиллюстрировать эту концепцию без особых технических накладных расходов, мы пойдем с первым вариантом

По сути, это то, что мы хотим написать:

Я не буду вставлять здесь весь исходный код, однако я буду обсуждать несколько ключевых моментов относительно него (полный код можно найти Здесь )

Нам нужно решить 5 проблем:

  1. Какие хэши мы собираемся использовать для файлов?
  2. Как часто должно быть совершено пересчечность хэша?
  3. Как мы проводим опросы на интервале?
  4. Как запустить наш сценарий в контексте приложения GO?
  5. Какие есть условия гонки?

Первая проблема решается довольно кратко в Go, мы можем рассчитать хэш SHA256 для файла только со следующим фрагментом кода:

func CalculateHash(absoluteFilePath string) string {
    f, err := os.Open(absoluteFilePath)
    HandleError(err)
    defer f.Close()
    h := sha256.New()
    if _, err := io.Copy(h, f); err != nil {
        log.Fatal(err)
    }
    return hex.EncodeToString(h.Sum(nil))
}

Вторая проблема имеет нетривиальный ответ и он специфичен для вашего варианта использования, однако она должна быть достаточно разумной, чтобы пересматривать хэши каждые 15 секунд или около того — это означает, что у нас должно быть автоматическое развертывание, выполняемые каждые 15 секунд, если произошло изменение кода в этом окне.

Я запустил несколько критериев в реальном времени, которое необходимо запустить сборку в приложении GO, чтобы мы могли сделать образованное предположение о том, как часто для опроса для изменений файлов.

Фрагмент, который я использовал для запуска эталона, выглядит следующим образом:

func stopwatch(start time.Time, name string) {
    elapsed := time.Since(start)
    log.Printf("%s took %s", name, elapsed)
}

Просто добавьте Отложить секундомер (время. Теперь (), "тест") В верхней части функции вы хотите сравнить!

После теста я подсчитал, что нам нужно разрешить 7 секунд для завершения прошлой сборки и 8 секунд накладных расходов для неожиданных. Что дает нам в общей сложности 15 секунд.

Задача № 3 решается путем определения следующей функции и используя ее, как проиллюстрировано:

package main

import (
    "fmt"
    "time"
)

func main() {
    go DoEvery(10*time.Second, f, "test")
    for {}
}

func DoEvery(d time.Duration, f func(time.Time, string), action string) {
    for x := range time.Tick(d) {
        f(x, action)
    }
}

func f(t time.Time, action string) {
    fmt.Println(action)
}

Мы просто создаем функцию, которая будет вызываться каждые X секунд.

4 -я проблема также имеет довольно простое и эффективное решение в Go, мы можем определить действие строка и запустить его, используя ОС/EXEC упаковка в Go.

package main

import (
    "bytes"
    "log"
    "os/exec"
)

func main() {
    runAction("./build build docker deploy")
}

func runAction(action string) {
    log.Println("Taking action, running: " + action)
    cmd := exec.Command("/bin/sh", "-c", action)
    var outb, errb bytes.Buffer
    cmd.Stdout = &outb
    cmd.Stderr = &errb
    err := cmd.Run()
    if err != nil {
        log.Printf("error")
    }
    log.Println(outb.String())
    log.Println(errb.String())

Последняя проблема является важной, одно явное условие гонки заключается в том, что если мы отслеживаем хэши для всех файлов в каталоге, то мы, скорее всего, пойдем в какой -то рекурсивный цикл сборки, поскольку мы активно меняем файлы Монитор (путем создания бинарного). Мы можем одолжить концепцию из git Здесь и реализуйте Белый список , то есть список файлов, которые следует игнорировать в нашем хэш -расчете. Что -то на это влияние,

var whiteList []string

func CreateWhiteList() {
    file, err := os.Open("./.ignore")
    if err != nil {
        log.Println("no .ignore file found, race condition will ensue if jobs edit files -- will not create whitelist")

    } else {
        defer file.Close()
        scanner := bufio.NewScanner(file)
        for scanner.Scan() {
            log.Println(scanner.Text())
            whiteList = append(whiteList, scanner.Text())
        }
        if err := scanner.Err(); err != nil {
            log.Fatal(err)
        }
    }
}

Сочетая решения этих проблем, мы можем произвести это)

Строительство строителя с Go Build -o Pipeline приведет к бинарному под названием трубопровод Анкет

Затем мы можем перенести этот бинар в наш рабочий каталог. Нам также нужно создать игнорировать Файл в нашем рабочем каталоге, чтобы игнорировать бинарную, произведенную строителем для нашего приложения.

игнорировать Файл просто:

pipeline
cicdexample

Мы игнорируем .git Программатически, поскольку это никогда не то, что мы хотим включить.

Мы можем наконец запустить наш строитель

./трубопровод

Внесение изменения в index.html Из нашего приложения следует начать автоматизированную сборку и перераспределение, точно так же, как мы намеревались сделать.

Как вы можете видеть из этого вывода:

shiv@shiv-Lenovo-ideapad-310-15IKB:~/Work/dev/go/cicdexample$ sudo ./pipeline
2019/12/24 18:23:42 map[]
2019/12/24 18:23:42 creating whitelist
2019/12/24 18:23:42 pipeline
2019/12/24 18:23:42 cicdexample
2019/12/24 18:23:42 builder
2019/12/24 18:23:42 building registry
2019/12/24 18:23:42 starting directory scan
2019/12/24 18:23:42 pipeline is whitelisted, not adding to registry
2019/12/24 18:23:42 computing hashes & creating map entries
2019/12/24 18:23:57 verifying hashes
2019/12/24 18:24:12 verifying hashes
2019/12/24 18:24:12 ./static/index.html old hashbebc0fe5b73e2217e1e61def2978c4d65b0ffc15ce2d4f36cf6ab6ca1b519c17new hash16af318df74a774939db922bcb4458a695b9a38ecf28f9ea573b91680771eb3achanged detected - updating hash, action required
2019/12/24 18:24:12 Taking action, running: ./build build docker deploy
2019/12/24 18:24:20 🤖    Welcome to the Builder builder v0.2, written by github.com/cishiv
⏲️  18:24:12: started build script...
🏗️ 18:24:12: building cicdexample
🐋    18:24:13: building image example:test
Sending build context to Docker daemon  10.11MB
...
Successfully tagged example:test
🌧️  18:24:19: deploying to Minikube
deployment.apps "example" deleted
service "example" deleted
deployment.apps/example created
service/example created
✔️  18:24:20: complete.
👋    18:24:20: exiting...

2019/12/24 18:24:20 
2019/12/24 18:24:20 --------------------------------------------------------------------------------
2019/12/24 18:24:27 verifying hashes

Вот и все! Были сделаны. Мы успешно написали, полезный, хотя и простой трубопровод CI/CD для улучшения нашего процесса разработки.

Каковы выводы из этого опыта?

Советы

Может быть заманчиво использовать ранее существовавшее решение для такого рода трубопровода, но я рекомендую, что если у вас есть время и энергия, чтобы написать небольшое решение проблемы, с которой вы столкнулись, вы определенно должны. Это заставляет вас думать о том, как ваши приложения (ы) работают в производственных условиях, а также о том, как может быть улучшен ваш процесс разработки. Это также очень весело.

Расширения

Есть много возможных расширений в этом проекте, некоторые из которых я хочу заняться скоро, я перечислил некоторые из самых интересных здесь:

  • Разрешить создание сценариев сборки через приложение Builder
  • Постройте из VCS (то есть GIT клонировать репо и построить его на основе описания работы)
  • Пользовательский интерфейс для этого конвейера сборки
  • Запустите приложение Builder в Docker на самом Minikube
  • Размещайте приложение Builder на облачной платформе, а также настраиваются развертывания и сборки
  • Пусть пользователи создают .json файлы заданий для сборки
  • Многоязычная поддержка

Предостережения

Этот вид быстрой и грязной трубопроводы не лучше, чем ранее существовавшее решение, но его гораздо веселее использовать, так как я могу быстро внести в него изменения в зависимости от потребности, которую я могу иметь.

Я относительно новичок, чтобы идти, Kubernetes и Docker. Таким образом, стиль, который я использовал, может быть не лучшей практикой, но он работает для меня.

Примеры использования

В настоящее время я использую аналогичный конвейер для автоматизации развертывания проекта, над которым я работаю под названием CRTX ( http://crtx.xyz/ ) — в тестовую среду, когда я пишу код. В первую очередь это база кода GO с несколькими приложениями, которые непрерывно развертываются в кластере Minikube. Этот трубопровод значительно облегчает протекание данных тестирования между приложениями.

Заключительные замечания

Это первый раз, когда я написал техническую статью и хотел бы услышать отзывы о подходе, который я принял, а также о фактическом содержании!

Я планирую написать больше об инструментах, которые я использую и строю, поэтому, если вам понравилось читать это, пожалуйста, дайте мне знать!

Вы можете дать мне отзыв о Twitter или прямо здесь, в комментариях ✍ ️

Оригинал: «https://dev.to/cishiv/building-a-simple-ci-cd-pipeline-for-local-testing-using-go-docker-minikube-and-a-bash-script-1647»