Перейти к основному содержимому

Запуск Serverless Container по триггеру

· 3 мин. чтения

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

Тут нужно уточнить детали реализации взаимодействия serverless контейнера и cron-триггера, который будет его запускать. Триггер запускает контейнер просто отправляя ему http-запрос в корень (path /) и ожидая в конце выполнения ответа c HTTP кодом200.

Если же вы в контейнере не реализуете HTTP-сервер, то триггер не может узнать о том, корректно ли завершилось выполнение контейнера. Это будет воспринято как ошибка и триггер попробует запустить контейнер снова. В итоге вы получите много запусков вашего кода, которых вы не ожидали, и большой счёт за их выполнение.

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

Сервер на Go

Давайте напишем простой сервер на go, и добавим его в наш контейнер.

package main

import (
"fmt"
"log"
"net/http"
"os"
"os/exec"
"syscall"
"time"
)

func getRoot(w http.ResponseWriter, r *http.Request) {
start := time.Now()
fmt.Printf("got / request\n")
cmd := exec.Command("sh", "./command.sh")
// Pipe script output to server outputs
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

if err := cmd.Start(); err != nil {
log.Fatalf("cmd.Start: %v", err)
}

if err := cmd.Wait(); err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
// The program has exited with an exit code != 0
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
log.Printf("Exit Status: %d", status.ExitStatus())
// Response to trigger with 500 error status, so it knows that it should retry
w.WriteHeader(500)
}
} else {
log.Fatalf("cmd.Wait: %v", err)
}
}
t := time.Now()
elapsed := t.Sub(start)
fmt.Printf("handled task in %f seconds\n", elapsed.Seconds())
}

func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", getRoot)

port := os.Getenv("PORT")

err := http.ListenAndServe(":"+port, mux)
if err != nil {
os.Exit(1)
}
}

При обработке запроса от триггера мы просто запускаем скрипт command.sh в котором можно описать произвольные команды.

Dockerfile

Теперь надо упаковать это все в докер-контейнер.

FROM golang:1.18 AS builder
WORKDIR /go/src
COPY server.go ./
COPY go.mod ./
RUN GOOS=linux go build -o server .

FROM ubuntu:22.04
WORKDIR /root/
COPY --from=builder /go/src/server ./
# Add here your steps
COPY command.sh ./
CMD ["./server"]

Здесь мы используем подход многофазного построения контейнера. На первом шаге мы собираем наш сервер. Назовем этот контейнер build.

На втором шаге просто копируем сервер в чистый образ ubuntu:22.04. Туда же вы можете добавить все необходимое для запуска вашей задачи. А сам процесс запуска описать в command.sh.

Таким образом вы сможете легко корректно обрабатывать вызовы триггера и не получать в логах контейнера сообщений типа:

{"errorMessage":"user container finished with error: exit status 1","errorType":"UserCodeError"}

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