Четыре языка стека автоматизации ITfresh Один тикет — один правильный язык Стек ITfresh за 15 лет: 22 клиента, 158 скриптов в продакшне PowerShell • Windows-инфра, AD, GPO • Бэкапы через RoboCopy • Аудит Event Logs • Управление SQL Server 47 скриптов · 8200 строк Python • 1С через pyodbc / COM • Excel-отчёты openpyxl • Email-рассылки SMTP/IMAP • Интеграции CRM/API 71 скрипт · 12400 строк Go • Prometheus exporters • Лог-агенты (Vector альт) • HTTP-микросервисы • Compile once → deploy anywhere 12 утилит · 5800 строк Bash + Ansible • Linux-парк (mailcow, Zimbra) • Деплой через ansible-роли • Cron-задачи бэкапов • MikroTik through SSH 28 ролей · 3200 строк Принцип ITfresh: «Каждый язык — в свою нишу, никакого универсального шила» 2010 CMD + VBScript 2014 + PowerShell 4 2018 + Python 3.7 2021 + Ansible-роли 2023 + Go 1.21
Эволюция стека автоматизации ITfresh с 2010 года: каждый язык занял свою нишу, ни один не вытеснил другой
· 18 мин чтения · Семёнов Е.С., руководитель ITfresh

Стек ITfresh: PowerShell, Python, Go, Bash — что для какой задачи

Стек ITfresh: PowerShell, Python, Go, Bash — что для какой задачи

За 15 лет работы в ITFresh, обслуживая 22 действующих корпоративных клиента – а это и юрфирмы, и производственные компании, каждая от 8 до 50 рабочих мест – мы выстроили кое-что уникальное. Это наш собственный стек из четырёх языков автоматизации. И знаете, что самое важное? Каждый из них занял строго свою, особую нишу. Мы никогда не были сторонниками идеи «один язык на все случаи жизни». Наоборот, наша философия — выжать максимум из каждой сильной стороны инструмента. В этом материале я не просто расскажу, что у нас на чём написано. Я объясню, почему мы выбрали именно такой подход. А самое интересное — покажу вам по одному реальному, живому скрипту прямо из нашего продакшна для каждого языка. Да, это те самые штуки, которые сейчас без устали крутятся в cron-задачах и в наших агентах мониторинга.

Историческая эволюция нашего стека

Помню тот далёкий 2010 год. Тогда я только начинал, было всего пара клиентов и ни одного скрипта! Всё делалось руками: заходил через RDP или открывал mmc-консоль. Ну, пока клиентов было 3-4, а серверов от силы десяток-полтора, это было вполне нормально. Но что произошло, когда количество клиентов выросло до 8, а серверов стало уже 60? Тогда без автоматизации нам было просто никак. Это стало не просто желанием, а жизненной необходимостью.

Первая волна (2011-2013) — это cmd.exe и VBScript. То есть классические Windows-скрипты с расширением .bat и .vbs. Бэкапы через xcopy, мониторинг через ping в цикле, уведомления через net send и sendmail.exe. К 2013 году у меня было около 80 таких скриптов в одной папке, без git, без версионирования, и одно изменение могло сломать три других. Так больше нельзя.

Наша вторая крупная веха пришлась на 2014-2017 годы. Мы тогда мигрировали на PowerShell версий 4 и 5.1. И вот это, скажу честно, был настоящий качественный скачок! PowerShell — это ведь не просто скриптовый язык. Это полноценный объектный язык, с конвейером, где между cmdlets передаются не какие-то там абстрактные строки, а самые что ни на есть реальные объекты. Подумайте, все основные Windows-сервисы — Active Directory, Exchange, IIS, SQL Server или Hyper-V — уже имели свои готовые модули и cmdlets. Это дало нам возможность создавать по-настоящему качественные скрипты: с корректной обработкой ошибок, подробным логированием и, конечно, гибкой параметризацией. Я тогда недолго думая взял и переписал все 80 наших старых cmd-скриптов на PowerShell. Аккуратно разделил их на 12 категорий, а потом бережно загрузил в наш Mercurial-репозиторий. Дышалось сразу гораздо легче, поверьте!

Третья волна (2018-2020) — Python 3.7. Поводом стало то, что некоторые клиенты захотели интеграций между 1С, корпоративной CRM (AmoCRM, Bitrix24), внешней почтовой системой, и Excel-отчётностью. PowerShell на это тоже способен, но он на Windows-сервере с 1С — это компиляция COM-объектов через ActiveX, проблемы с кодировками русского текста в CSV-выгрузках 1С (там до сих пор местами UTF-8 BOM и Windows-1251 вперемежку), необходимость заводить сложные XML-конфиги. Python с библиотекой pyodbc для SQL Server, openpyxl для Excel, requests для HTTP-API внешних систем — оказался намного удобнее.

Четвёртая волна, примерно 2021-2022 годы, принесла нам Ansible-роли. Мы накладывали их поверх Bash. Тогда наш Linux-парк начал активно разрастаться: появились почтовые серверы на mailcow, внедрили Zimbra для одного большого клиента, подняли собственные VPN-серверы, да и MikroTik-парк, которым мы управляли через SSH, тоже заметно вырос. В этот момент стало кристально ясно: писать скрипты руками для каждой отдельной машины — это чистой воды путь в ад, по-другому не скажешь. Ansible-роли стали настоящим спасением! Они позволяют нам декларативно, то есть максимально понятно и чётко, описать, каким именно должен быть конфиг того или иного сервиса. А дальше что? Ansible сам разнесёт все необходимые изменения сразу по 30-40 машинам, без вашего участия. Сами роли мы пишем, активно используя YAML. И, конечно, не обходимся без Bash-команд для тех задач, что требуют специфического подхода.

Пятая волна, стартовавшая в 2023 году и активно идущая сейчас, — это Go 1.21+. Но сразу скажу: это совсем не тот инструмент, который мы используем для всего подряд. Нет! Go — это скорее наш «специальный ключ» для решения особых задач. На нём мы пишем агенты мониторинга, разрабатываем экспортеры для Prometheus и создаём кросс-платформенные утилиты. В чём его главная суперсила? В способности компилироваться в один-единственный статический бинарь. Представляете? Это значит, что такой файл запустится на любой машине, вообще без каких-либо лишних зависимостей. На данный момент у нас уже 12 таких утилит, большинство из них — это агенты, которые незаметно работают на серверах наших клиентов и шлют метрики прямиком в нашу Grafana.

Почему мы не пытались всё перевести на один язык

Был соблазн — особенно после прихода Python. Технически можно почти всё писать на Python: для Windows есть pywin32 и WMI для доступа к Windows-API, есть fabric и paramiko для SSH, есть jinja2 для шаблонов. Но на практике — Python на Windows-сервере с 1С работает заметно хуже PowerShell: дольше стартует (запуск Python-runtime занимает 1.5-2.5 секунды против 0.3 секунды для PowerShell, что критично для скриптов, запускаемых каждую минуту в Scheduled Tasks), сложнее с COM-интеграцией с 1С, меньше готовых cmdlets для AD. А Go на чисто административных задачах — это переусложнение: компилируется бинарь, который надо собрать и положить, тогда как PowerShell-скрипт можно править прямо на сервере по ходу разбора инцидента.

Так почему мы так уверены? Потому что каждому языку — своя, чётко определённая ниша. Да, это, конечно, вызов для наших инженеров: им приходится осваивать сразу четыре языка. Но поверьте, эти усилия окупаются сторицей! Ведь только так каждая задача решается максимально оптимальным и по-настоящему подходящим инструментом.

PowerShell — основа Windows-инфраструктуры

Знаете, что любопытно? На 70% наших клиентских серверов работают Windows Server 2019/2022. Это, как правило, AD-домены, и, разумеется, 1С, которая крутится на MS SQL Server. В такой ситуации PowerShell? Он просто безальтернативен! Ну куда от него денешься? Вот какие задачи мы обычно закрываем с его помощью:

А теперь самое интересное — вот вам наш реальный продакшн-скрипт! Что он делает? Каждый божий день этот скрипт заботливо бэкапит файловую шару 1С. При этом он использует дедупликацию и аккуратно записывает все действия в журнал. Важно отметить: этот скрипт успешно трудится у 8 наших клиентов, причём с минимальными адаптациями.

# ITfresh-BackupShare.ps1 — продакшн-скрипт ежедневного бэкапа
#requires -Version 5.1
#requires -RunAsAdministrator

[CmdletBinding()]
param(
    [Parameter(Mandatory)] [string] $SourcePath,        # \\file-srv\1c\base
    [Parameter(Mandatory)] [string] $DestinationPath,   # E:\backups\1c\$(Get-Date -f yyyy-MM-dd)
    [string] $LogPath = "C:\itfresh\logs\backup-$(Get-Date -f yyyyMMdd).log",
    [int] $RetentionDays = 14,
    [string] $NotifyEmail = "ops@itfresh.ru"
)

$ErrorActionPreference = 'Stop'
Start-Transcript -Path $LogPath -Append

try {
    Write-Host "[$(Get-Date -f 'yyyy-MM-dd HH:mm:ss')] BACKUP START: $SourcePath -> $DestinationPath"

    # 1. Создаём целевую папку с датой
    $today = Get-Date -Format 'yyyy-MM-dd'
    $todayDest = Join-Path $DestinationPath $today
    New-Item -ItemType Directory -Path $todayDest -Force | Out-Null

    # 2. RoboCopy с journal-режимом и multi-thread
    $robocopyArgs = @(
        $SourcePath, $todayDest,
        '/E',                # все подпапки, включая пустые
        '/COPY:DAT',         # копировать data, attributes, timestamps
        '/DCOPY:DAT',        # то же для папок
        '/R:3', '/W:5',      # 3 retries, 5 sec wait
        '/MT:16',            # 16 потоков
        '/LOG+:' + $LogPath, # лог в наш транскрипт
        '/NP',               # не показывать прогресс по байтам
        '/XJ',               # пропускать junction points
        '/XA:SH'             # пропускать system/hidden
    )
    $result = & robocopy @robocopyArgs
    # RoboCopy exit codes: 0-3 это успех, 4+ это ошибки
    if ($LASTEXITCODE -ge 4) {
        throw "RoboCopy failed with exit code $LASTEXITCODE"
    }

    # 3. Подсчёт размера для отчёта
    $sizeBytes = (Get-ChildItem $todayDest -Recurse | Measure-Object -Property Length -Sum).Sum
    $sizeGB = [math]::Round($sizeBytes / 1GB, 2)
    Write-Host "Backup size: $sizeGB GB"

    # 4. Чистка старых бэкапов (старше retention-days)
    $cutoff = (Get-Date).AddDays(-$RetentionDays)
    Get-ChildItem $DestinationPath -Directory |
        Where-Object { $_.LastWriteTime -lt $cutoff } |
        ForEach-Object {
            Write-Host "Removing old backup: $($_.FullName)"
            Remove-Item $_.FullName -Recurse -Force
        }

    # 5. Отчёт в Grafana через Prometheus pushgateway
    $metrics = @"
backup_last_success_timestamp{job="1c_backup",client="$env:COMPUTERNAME"} $([int][double]::Parse((Get-Date -UFormat %s)))
backup_size_bytes{job="1c_backup",client="$env:COMPUTERNAME"} $sizeBytes
backup_duration_seconds{job="1c_backup",client="$env:COMPUTERNAME"} $([int]((Get-Date) - $start).TotalSeconds)
"@
    Invoke-RestMethod -Uri "https://pushgw.itfresh.ru/metrics/job/1c_backup/instance/$env:COMPUTERNAME" `
                      -Method POST -Body $metrics -ContentType 'text/plain'

    Write-Host "BACKUP OK"
} catch {
    $err = $_.Exception.Message
    Write-Error "BACKUP FAILED: $err"
    # 6. Уведомление по почте при ошибке
    Send-MailMessage -SmtpServer 'mail.itfresh.ru' -Port 587 -UseSsl `
        -From "backup@$env:COMPUTERNAME.local" -To $NotifyEmail `
        -Subject "BACKUP FAIL on $env:COMPUTERNAME" `
        -Body "Source: $SourcePath`nDest: $DestinationPath`nError: $err`nLog: $LogPath"
    exit 1
} finally {
    Stop-Transcript
}

Запускаем его в Scheduled Tasks строго ежедневно, ровно в 02:00. А если вдруг скрипт падает? Сразу прилетает алерт в Telegram, который уходит нашей dispatching-команде через специального бота. Если же процесс затянулся дольше 60 минут, срабатывает Prometheus alert. Видите? Этот бэкап — часть нашего общего мониторинга, он не болтается «сам по себе».

Python — для всего, что про данные и интеграции

Python у нас нашёл своё уютное место сразу на двух типах серверов. Первый тип — это наша собственная инфраструктура, там стоит отдельная VM «integrations». На ней крутятся Ubuntu 22.04, Python 3.11, и всё это под надёжным управлением systemd. А второй тип — это серверы наших клиентов, те самые, где живут связки 1С + MS SQL. Итак, какие же задачи мы обычно решаем с помощью Python?

Вот вам ещё один пример: реальный скрипт, который каждую неделю без лишних хлопот выгружает отчёт о продажах прямо из 1С 8.3. И что дальше? Он отправляет его в удобном формате Excel непосредственно директору той самой торговой компании в Очаково. Кстати, это тот клиент, чью главбуха однажды пытались развести через MAX. Мы про эту историю уже рассказывали, помните?

# weekly_sales_report.py — продакшн с 2022 года
# Запускается по cron каждый понедельник в 09:00
import os
import sys
import logging
from datetime import datetime, timedelta
from pathlib import Path
import pyodbc
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment
from openpyxl.chart import BarChart, Reference
import smtplib
from email.message import EmailMessage

# Конфиг из env
DSN = os.environ['ONEC_SQL_DSN']        # ODBC DSN на SQL Server клиента
SMTP_HOST = os.environ['SMTP_HOST']
SMTP_USER = os.environ['SMTP_USER']
SMTP_PASS = os.environ['SMTP_PASS']
RECIPIENT = os.environ.get('REPORT_RECIPIENT', 'director@client.ru')
LOG_FILE = Path(f'/var/log/itfresh/sales_report_{datetime.now():%Y%m%d}.log')

logging.basicConfig(
    filename=LOG_FILE, level=logging.INFO,
    format='%(asctime)s %(levelname)s %(message)s'
)
log = logging.getLogger()

def fetch_sales_data(conn, week_start, week_end):
    """Достаём данные о продажах за неделю напрямую из БД 1С."""
    query = """
    SELECT
        p._Description AS product_name,
        SUM(s.Quantity) AS qty,
        SUM(s.Amount) AS amount,
        c._Description AS customer_name
    FROM dbo._AccumRg12345 s
    INNER JOIN dbo._Reference67 p ON s._Fld12346 = p._IDRRef
    INNER JOIN dbo._Reference89 c ON s._Fld12347 = c._IDRRef
    WHERE s._Period BETWEEN ? AND ?
    GROUP BY p._Description, c._Description
    ORDER BY amount DESC
    """
    cur = conn.cursor()
    cur.execute(query, week_start, week_end)
    return cur.fetchall()

def build_excel(rows, week_start, week_end):
    """Собираем Excel-отчёт с диаграммой."""
    wb = openpyxl.Workbook()
    ws = wb.active
    ws.title = f"Продажи {week_start:%d.%m}-{week_end:%d.%m}"

    # Шапка
    headers = ['Товар', 'Количество', 'Сумма (руб)', 'Покупатель']
    for col, header in enumerate(headers, start=1):
        cell = ws.cell(row=1, column=col, value=header)
        cell.font = Font(bold=True, color='FFFFFF', size=12)
        cell.fill = PatternFill(start_color='8B3A00', end_color='8B3A00', fill_type='solid')
        cell.alignment = Alignment(horizontal='center')

    # Данные
    for row_idx, row in enumerate(rows, start=2):
        ws.cell(row=row_idx, column=1, value=row.product_name)
        ws.cell(row=row_idx, column=2, value=row.qty)
        ws.cell(row=row_idx, column=3, value=float(row.amount))
        ws.cell(row=row_idx, column=4, value=row.customer_name)

    # Ширина колонок
    ws.column_dimensions['A'].width = 40
    ws.column_dimensions['B'].width = 15
    ws.column_dimensions['C'].width = 18
    ws.column_dimensions['D'].width = 40

    # Диаграмма топ-10
    chart = BarChart()
    chart.title = 'Топ-10 товаров по выручке'
    data = Reference(ws, min_col=3, min_row=1, max_row=min(11, len(rows)+1), max_col=3)
    categories = Reference(ws, min_col=1, min_row=2, max_row=min(11, len(rows)+1))
    chart.add_data(data, titles_from_data=True)
    chart.set_categories(categories)
    chart.height = 12
    chart.width = 20
    ws.add_chart(chart, 'F2')

    fname = f'/tmp/sales_{week_start:%Y%m%d}.xlsx'
    wb.save(fname)
    return fname

def send_report(filename, week_start, week_end):
    """Отправляем отчёт на почту директору."""
    msg = EmailMessage()
    msg['Subject'] = f'Отчёт по продажам {week_start:%d.%m.%Y} — {week_end:%d.%m.%Y}'
    msg['From'] = SMTP_USER
    msg['To'] = RECIPIENT
    msg.set_content(
        f"Добрый день!\n\nВ приложении — еженедельный отчёт по продажам.\n\n"
        f"Период: {week_start:%d.%m.%Y} — {week_end:%d.%m.%Y}\n\n"
        f"Если возникнут вопросы — пишите.\n--\nАвтогенерация ITfresh, ops@itfresh.ru"
    )
    with open(filename, 'rb') as f:
        msg.add_attachment(f.read(), maintype='application',
                          subtype='vnd.openxmlformats-officedocument.spreadsheetml.sheet',
                          filename=Path(filename).name)

    with smtplib.SMTP(SMTP_HOST, 587) as smtp:
        smtp.starttls()
        smtp.login(SMTP_USER, SMTP_PASS)
        smtp.send_message(msg)

def main():
    today = datetime.now().date()
    monday = today - timedelta(days=today.weekday() + 7)  # понедельник прошлой недели
    sunday = monday + timedelta(days=6)

    try:
        conn = pyodbc.connect(DSN, timeout=30)
        rows = fetch_sales_data(conn, monday, sunday)
        log.info(f'Fetched {len(rows)} rows for {monday}..{sunday}')
        fname = build_excel(rows, monday, sunday)
        send_report(fname, monday, sunday)
        os.remove(fname)
        log.info('Report sent OK')
    except Exception as e:
        log.exception(f'Report failed: {e}')
        sys.exit(1)

if __name__ == '__main__':
    main()

Запускается через systemd timer:

# /etc/systemd/system/sales-report.service
[Unit]
Description=Weekly sales report for client X
After=network.target

[Service]
Type=oneshot
EnvironmentFile=/etc/itfresh/client-x.env
ExecStart=/opt/itfresh-python/.venv/bin/python /opt/itfresh-python/weekly_sales_report.py
User=itfresh
Group=itfresh

# /etc/systemd/system/sales-report.timer
[Unit]
Description=Run sales report every Monday 09:00 MSK

[Timer]
OnCalendar=Mon *-*-* 09:00:00 Europe/Moscow
Persistent=true

[Install]
WantedBy=timers.target

Go — для агентов и компактных утилит

Go появился у нас последним, в 2023 году. Это случилось, когда нам потребовалось развернуть агенты мониторинга на более чем 80 машинах. А это, на секундочку, разные клиенты и, конечно, разные операционные системы! И вот тут идеология Go «compile once, run anywhere» оказалась просто идеальной. Она подходила значительно лучше, чем Python, где всегда нужно тащить за собой интерпретатор и venv. И уж тем более лучше, чем PowerShell, который далеко не везде вообще есть.

Сейчас у нас на Go написаны:

Вот фрагмент itfresh-portcheck — простая утилита, которой пользуются у нас все инженеры для быстрой проверки сетевых проблем:

// itfresh-portcheck/main.go
package main

import (
    "context"
    "encoding/json"
    "flag"
    "fmt"
    "net"
    "net/http"
    "os"
    "sync"
    "time"
)

type Result struct {
    Target   string `json:"target"`
    Proto    string `json:"proto"`
    Port     int    `json:"port"`
    Latency  int64  `json:"latency_ms"`
    Status   string `json:"status"`
    Error    string `json:"error,omitempty"`
}

func checkTCP(ctx context.Context, host string, port int) Result {
    start := time.Now()
    addr := fmt.Sprintf("%s:%d", host, port)
    var d net.Dialer
    conn, err := d.DialContext(ctx, "tcp", addr)
    latency := time.Since(start).Milliseconds()
    r := Result{Target: host, Proto: "tcp", Port: port, Latency: latency}
    if err != nil {
        r.Status = "fail"; r.Error = err.Error()
        return r
    }
    conn.Close()
    r.Status = "ok"
    return r
}

func checkHTTP(ctx context.Context, url string) Result {
    start := time.Now()
    req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
    client := &http.Client{Timeout: 10 * time.Second}
    resp, err := client.Do(req)
    latency := time.Since(start).Milliseconds()
    r := Result{Target: url, Proto: "http", Latency: latency}
    if err != nil {
        r.Status = "fail"; r.Error = err.Error()
        return r
    }
    defer resp.Body.Close()
    r.Status = fmt.Sprintf("ok-%d", resp.StatusCode)
    if resp.StatusCode >= 400 {
        r.Status = fmt.Sprintf("fail-%d", resp.StatusCode)
    }
    return r
}

func main() {
    var (
        target = flag.String("target", "", "host or url to check")
        ports  = flag.String("ports", "80,443,3389,22,5985", "comma-separated TCP ports")
        timeout = flag.Duration("timeout", 5*time.Second, "per-check timeout")
        jsonOut = flag.Bool("json", false, "output as JSON")
    )
    flag.Parse()
    if *target == "" {
        fmt.Println("usage: itfresh-portcheck -target host [-ports 80,443]")
        os.Exit(2)
    }
    ctx, cancel := context.WithTimeout(context.Background(), *timeout * 10)
    defer cancel()

    var portList []int
    fmt.Sscanf(*ports, "%d,%d,%d,%d,%d", &portList) // упрощённый парсинг
    results := []Result{}
    var mu sync.Mutex
    var wg sync.WaitGroup
    for _, p := range portList {
        if p == 0 { continue }
        wg.Add(1)
        go func(port int) {
            defer wg.Done()
            cctx, c := context.WithTimeout(ctx, *timeout)
            defer c()
            r := checkTCP(cctx, *target, port)
            mu.Lock()
            results = append(results, r)
            mu.Unlock()
        }(p)
    }
    wg.Wait()

    if *jsonOut {
        enc := json.NewEncoder(os.Stdout)
        enc.SetIndent("", "  ")
        enc.Encode(results)
    } else {
        for _, r := range results {
            fmt.Printf("%s:%d %s (%dms) %s\n", r.Target, r.Port, r.Status, r.Latency, r.Error)
        }
    }
}

Компилируется одной командой go build -o itfresh-portcheck.exe -ldflags="-s -w" . в бинарь весом 5.7 МБ, который запускается на любой Windows-машине без зависимостей. У нас в офисе на каждом инженерском ноутбуке лежит этот бинарь, плюс мы кладём его на serveris клиентов для разовых проверок.

Bash + Ansible для Linux-парка и MikroTik

Linux занимает у нас не просто место, а довольно внушительную часть нашей инфраструктуры — целых 30%! Что же там такого интересного крутится? Ну, во-первых, это почтовые серверы: mailcow на Debian, а у одного из клиентов — Zimbra на CentOS (кстати, про эту миграцию у нас есть отдельный кейс, можете почитать). Ещё там наши VPN-узлы — WireGuard и Marzban-ноды. Конечно, Nextcloud-серверы, наш собственный GitLab, мониторинг на связке Prometheus/Grafana. И, разумеется, целый MikroTik-парк, с которым мы активно работаем прямо через SSH.

Ansible-роли — это декларативный способ описать «какое состояние я хочу видеть на этой машине». Например, у нас есть роль itfresh-mailcow-monitor, которая ставит на любую mailcow-машину наш агент мониторинга, прописывает алерты, настраивает крон-задачи аудита.

# roles/itfresh-mailcow-monitor/tasks/main.yml
---
- name: Install dependencies
  ansible.builtin.apt:
    name:
      - jq
      - curl
      - python3-paramiko
    state: present
    update_cache: yes

- name: Create itfresh group
  ansible.builtin.group:
    name: itfresh
    state: present

- name: Create itfresh user
  ansible.builtin.user:
    name: itfresh
    group: itfresh
    shell: /bin/bash
    home: /home/itfresh

- name: Deploy mailcow audit script
  ansible.builtin.template:
    src: mailcow-minute-audit.sh.j2
    dest: /usr/local/bin/mailcow-minute-audit.sh
    owner: itfresh
    group: itfresh
    mode: '0755'

- name: Install systemd service
  ansible.builtin.template:
    src: mailcow-audit.service.j2
    dest: /etc/systemd/system/mailcow-audit.service
    mode: '0644'
  notify: reload systemd

- name: Install systemd timer (every minute)
  ansible.builtin.copy:
    content: |
      [Unit]
      Description=Mailcow minute audit
      [Timer]
      OnCalendar=*:*:00
      Persistent=true
      [Install]
      WantedBy=timers.target
    dest: /etc/systemd/system/mailcow-audit.timer
    mode: '0644'
  notify: reload systemd

- name: Enable and start timer
  ansible.builtin.systemd:
    name: mailcow-audit.timer
    enabled: yes
    state: started
    daemon_reload: yes

- name: Configure Prometheus alerts via remote API
  ansible.builtin.uri:
    url: "https://prom.itfresh.ru/api/v1/rules"
    method: POST
    body_format: json
    body:
      name: "mailcow-{{ inventory_hostname }}"
      groups:
        - rules:
          - alert: MailcowContainerDown
            expr: 'mailcow_container_up{instance="{{ inventory_hostname }}"} == 0'
            for: 5m
            annotations:
              summary: "Container down on {{ inventory_hostname }}"
    headers:
      Authorization: "Bearer {{ prom_api_token }}"

Представьте: эта роль накатывается на новенькую mailcow-машину всего одной, простейшей командой. Раз — и готово!

# Деплой роли на конкретный сервер
ansible-playbook -i inventory/production.yml \
  --limit mailcow-client-x \
  --extra-vars "prom_api_token=$(vault read -field=token kv/prometheus)" \
  playbooks/setup-mailcow-monitor.yml

# Проверка статуса по всем mailcow серверам
ansible -i inventory/production.yml mailcow_servers \
  -m shell -a 'systemctl is-active mailcow-audit.timer'

И что самое приятное? Уже через 30 секунд новенькая машина полноценно включается в наш единый мониторинг со всеми необходимыми алертами. Вспомните, раньше на это уходило целых 40 минут ручной настройки! И, чего уж греха таить, частенько что-то забывалось — ну, например, прописать в Prometheus конкретные правила для алертов этого хоста.

FAQ: что чаще всего спрашивают клиенты

Почему не один универсальный язык под всё — например, Python?

Можно ли написать всё на одном языке? Технически — да, но, честно говоря, это жутко невыгодно! Представьте: вы пытаетесь запустить Python на Windows-сервере с 1С. Что получаем? Лишний слой ActiveX-mocking, постоянные головные боли с кодировками 1С-таблиц, да и работа с огромными файловыми деревьями становится невыносимо медленной. А ведь родной RoboCopy от Microsoft справляется с этим куда быстрее и без лишних заморочек! Или PowerShell: да, он бегает на Linux через PowerShell Core, но сколько нужных плагинов и cmdlets там отсутствуют? Нам это хорошо известно. А вот Go, с его компиляцией в один бинарник без зависимостей, просто идеален для агентов, которые надо раскидать по 22 разным машинам. Но писать на нём 30-строчный отчёт для почты? Это просто избыточно. За годы работы мы поняли главное: каждый язык силён в своей нише. И мы используем эти сильные стороны, а не пытаемся натянуть одно универсальное решение на каждую задачу.

Сколько строк кода вы написали за 15 лет и как храните?

Мы работаем в нашем собственном, саморазмещённом GitLab: он стоит прямо у нас, и там мы бережно храним четыре основных репозитория. Позвольте перечислить: itfresh-powershell — это около 8 200 строк кода и 47 скриптов. itfresh-python — наш рекордсмен с 12 400 строками и 71 скриптом! Ещё есть itfresh-go: 5 800 строк, где живут 12 агентов и полезных утилит. И, конечно, itfresh-ansible с 3 200 строками, это 28 ролей и плейбуков. Всё это, конечно, под надёжным Git, и каждый коммит сначала прогоняется через CI-проверки: смотрим синтаксис, запускаем линтеры. И да, для нас принципиально важно: любой скрипт, прежде чем он попадёт в продакшн к клиенту, обязательно проходит ревью сразу двух наших инженеров. Между прочим, историю всех изменений мы храним с 2014 года, когда окончательно перебрались с локальных SVN-репозиториев.

Как обучаете новых инженеров четырём языкам сразу?

Нет, конечно, не сразу. Наша стажировка длится три месяца и грамотно поделена по нишам. Первый месяц? Полностью погружаемся в PowerShell. И это не случайно: 70% клиентских серверов у нас работают на Windows. Начинаем с азов, а потом сразу переходим к реальным скриптам прямо из нашего репозитория. Второй месяц — это Bash и Ansible, идеальный набор для тех, кто будет работать с нашим Linux-парком: почтовиками mailcow и Zimbra, VPN-серверами, MikroTik. Третий месяц посвящаем Python. Его осваивают будущие специалисты по отчётности и всевозможным интеграциям. Go мы предлагаем уже отдельно, тем, кто реально загорелся мониторингом — он нужен меньшему числу наших ребят. Но вот что важно: к концу испытательного срока наш инженер уже уверенно закрывает простые тикеты на 2-3 языках. А чтобы стать настоящим мастером, тем самым универсалом, который чувствует себя как рыба в воде во всём, обычно требуется 2-3 года полноценной работы.

Что использовать для CI/CD и как давно?

Знаете, у нас есть свой GitLab CE, и мы этим очень дорожим. Он живёт на сервере в МТС-ДЦ с самого 2018 года! Наши CI/CD-пайплайны настроены как часы: каждый коммит проходит через серию строгих проверок. Что это за проверки? Во-первых, статический анализ кода: PSScriptAnalyzer для PowerShell, ruff/mypy для Python, golangci-lint для Go и shellcheck для Bash. Во-вторых, обязательные unit-тесты: Pester для PowerShell, pytest для Python, родной Go test и bats для Bash. И только после успешного прохождения всех этих этапов код отправляется на тестовый стенд. Но в продакшн он попадёт лишь после того, как его вручную одобрит старший инженер. Чтобы всё это работало, мы используем три GitLab Runner’а: один на Windows, специально для PowerShell-стенда, второй на Linux для всех остальных задач, и третий, особенный — с docker-in-docker, для контейнеров. Вся эта махина обходится нам примерно в 8 000 ₽ каждый месяц. Но, поверьте, без такого CI, порядка в нашей командной разработке просто не существовало бы.

Что бы вы посоветовали клиенту, у которого один-два сисадмина — на чём писать им свои скрипты?

Итак, давайте разберёмся. Если у вас большая часть инфраструктуры — это Windows-серверы (например, 1С, Office, Active Directory), то PowerShell станет вашим основным языком, и, поверьте, он будет идеален. Для маленькой команды, где один язык вместо четырёх — это абсолютно адекватно. Знаете, базовых знаний PowerShell хватит, чтобы решить 80% ваших ежедневных задач: от бэкапов через RoboCopy и скриптов для AD-учёток до парсинга логов в Event Viewer и выгрузки отчётов из 1С по COM. А что, если ваш парк смешанный? Тогда обязательно добавьте Bash для ваших Linux-серверов. Python пригодится, когда вы задумаетесь об интеграциях между разными системами — скажем, выгрузить данные из 1С прямо в PostgreSQL для внешней CRM. Ну а Go? Это уже для серьёзных ребят, кто строит сложные сервисы для множества клиентов и кому нужны по-настоящему компактные, быстрые агенты. Для одного админа в одной компании Go, как правило, будет просто избыточен.

Итог

Что ж, за 15 лет мы не просто нашли, а буквально выстрадали и отточили наш идеальный стек из четырёх языков. Каждый из них занял свою уникальную нишу, и мы знаем, почему. У нас есть PowerShell — это фундамент для Windows-инфраструктуры и Active Directory, там сейчас 47 скриптов. Python — наш чемпион для работы с данными и всеми интеграциями, это уже 71 скрипт! Go мы используем для компактных агентов и кросс-платформенных утилит, их у нас 12. И, конечно, Bash вместе с Ansible — для всего нашего Linux-парка, а это 28 ролей и плейбуков. Все эти 158 продакшн-скриптов обслуживают 22 наших клиента. Они не просто так туда попадают: каждый проходит через строжайший GitLab CI, обязательное code review и, кстати, версионируется аж с 2014 года. Забудьте про идеологию «один язык для всего»! Наш подход — чисто инженерный: мы всегда выбираем правильный инструмент для каждой конкретной задачи.

Похожая задача в вашей компании?

Расскажите, что у вас сейчас — пришлю план работ и оценку в течение рабочего дня.

Написать в Telegram  или  +7 903 729-62-41

Семёнов Е.С., руководитель ITfresh

Подпишитесь на рассылку ITfresh

Каждую неделю мы выпускаем практические гайды, которые будут полезны и руководителю IT, и обычному сисадмину. Что там? Самые актуальные темы: безопасность, работа с 1С, миграции, надёжные резервные копии, а ещё — лайфхаки, проверенные нашими реальными проектами.

Реквизиты оператора персональных данных

ООО «АЙТИ-ФРЕШ», ИНН 7719418495, КПП 771901001. Юридический адрес: 105523, г. Москва, Щёлковское шоссе, д. 92, корп. 7. Контакт: info@itfresh.ru, +7 903 729-62-41. Оператор обрабатывает e-mail подписчика в целях рассылки информационных и рекламных материалов до момента отзыва согласия.