Стек ITfresh: PowerShell, Python, Go, Bash — что для какой задачи
Слушайте, за 15 лет работы в ITfresh, обслуживая 22 действующих корпоративных клиента — а это и юрфирмы, и производственные компании, каждая от 8 до 50 РМ — мы выстроили свой уникальный стек из четырёх языков автоматизации. И что самое главное, каждый из них занял свою, строго определённую нишу. Мы не сторонники идеи «один язык на все случаи жизни»; наоборот, стараемся выжимать максимум из сильных сторон каждого инструмента. В этом материале я собираюсь подробно рассказать вам, что именно у нас на чём написано, объясню, почему мы выбрали именно такой подход, и, конечно, покажу по одному реальному, живому скрипту прямо из нашего продакшна для каждого языка. Это те самые штуки, которые крутятся прямо сейчас в cron-задачах и в наших агентах мониторинга.
Историческая эволюция нашего стека
Помню, начинал я в далёком 2010 году всего с парой клиентов и вообще без каких-либо скриптов. Тогда всё делалось вручную, через RDP или mmc-консоль. Это было вполне себе нормально, пока клиентов было, скажем, 3-4, а серверов — от силы 10-15. Но вот когда количество клиентов выросло до 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-сервисы, будь то AD, 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+. Но это уже не какой-то «общий» инструмент для всего подряд, нет. Это скорее такой специальный ключ для особых задач: мы на нём пишем агенты мониторинга, экспортеры для Prometheus, а ещё кросс-платформенные утилиты. Главная суперсила Go — его способность компилироваться в один статический бинарь. Это значит, что такой файл запустится на любой машине без лишних зависимостей. Сейчас у нас уже 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 просто безальтернативен, никуда от него не денешься. Вот какие задачи мы обычно на нём решаем:
- Ежедневные бэкапы файловых шар через RoboCopy в нашу хранилищеную инфраструктуру (Veeam B&R + резерв на ITfresh_FTP в МТС-ДЦ).
- Аудит изменений в Active Directory: новые учётки, изменения групп, заходы под administrator-аккаунтами.
- Управление SQL Server: бэкапы баз 1С, проверки целостности, ребилд индексов.
- Скрипты для GPO-развёртывания приложений и настроек.
- Снятие отчётов о состоянии серверов (RAM, диск, процессы) с отправкой в Grafana.
А вот вам наш реальный продакшн-скрипт. Он каждый день делает бэкап файловой шары 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. Вот какие задачи мы на нём решаем:
- Выгрузка отчётов из 1С 8.3 в Excel и отправка по почте директорам каждый понедельник.
- Синхронизация контактов из 1С в Bitrix24 и AmoCRM раз в час.
- Парсинг писем от банков и формирование уведомлений о платежах.
- Email-рассылки от клиентских доменов (предупреждения о задолженности, статусы заказов).
- Веб-API для внутренних дашбордов (через FastAPI).
Вот, например, реальный скрипт, который каждую неделю выгружает отчёт о продажах из 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+ машинах разных клиентов с разными ОС. Идеология «compile once, run anywhere» Go подходила лучше Python (где надо тащить интерпретатор и venv) и лучше PowerShell (который не везде есть).
Сейчас у нас на Go написаны:
itfresh-agent— основной агент мониторинга, собирающий метрики о CPU, RAM, диске, сетях, активных пользователях, статусе ключевых сервисов. Отправляет в Prometheus через push-gateway.itfresh-logger— лог-агент, читающий локальные журналы и шлющий в наш Loki.itfresh-watchdog— наблюдатель за критичными процессами (например, 1С-сервер, mailcow-контейнеры) с автоперезапуском при падении.itfresh-portcheck— утилита для проверки доступности портов с клиентских машин через TCP/UDP/ICMP/HTTP.itfresh-backup-verifier— проверка целостности бэкапов (что-то типа аналога RestoreLatestPoint).
Вот фрагмент 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-строчный отчёт по почте, писать его на Go будет избыточно. В общем, мы убедились: каждый язык лучше всего проявляет себя в своей нише, и мы стараемся использовать сильные стороны каждого, а не пытаться натянуть одно универсальное решение на все задачи.
Сколько строк кода вы написали за 15 лет и как храните?
У нас в собственном GitLab self-hosted, расположенном прямо у нас, мы поддерживаем 4 репозитория. Это itfresh-powershell (там около 8 200 строк и 47 скриптов), itfresh-python (целых 12 400 строк и 71 скрипт!), itfresh-go (5 800 строк, это 12 агентов и утилит) и itfresh-ansible (3 200 строк, 28 ролей и плейбуков). Всё это, конечно, находится под Git, и каждый коммит проходит через CI-проверки на синтаксис и lint-ровно. Очень важно: скрипты, которые попадают в продакшн к нашим клиентам, обязательно проходят review сразу двух инженеров. Кстати, всю историю изменений мы бережно храним аж с 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, native Go test, bats для Bash). И только после этого — деплой на тестовый стенд. В продакшн код попадает только после ручного approve старшего инженера. У нас работают три GitLab Runner’а: один на Windows (специально для PowerShell-стенда), один на Linux (для всего остального) и ещё один с docker-in-docker для контейнеров. Вся эта инфраструктура обходится нам примерно в 8 000 ₽/мес. Но без такого CI, могу сказать честно, порядка в нашей командной разработке просто не было бы.
Что бы вы посоветовали клиенту, у которого один-два сисадмина — на чём писать им свои скрипты?
Если у вас парк в основном на Windows (например, 1С, Office, AD), то PowerShell — это ваш основной язык, и он будет идеален. Для небольшой команды один язык вместо четырёх — это абсолютно нормально. Базовых знаний хватит, чтобы закрыть 80% задач: это и бэкапы через RoboCopy, и скрипты для AD-учёток, и парсинг логов Event Viewer, и выгрузка отчётов из 1С через COM. Если же ваш парк смешанный, и у вас есть Linux-серверы, то обязательно добавьте Bash для них. Python пригодится, когда понадобится интеграция между разными системами (допустим, выгрузить данные из 1С в PostgreSQL для внешней CRM). А вот Go — это уже для тех, кто строит серьёзные сервисы для нескольких клиентов, и нужны по-настоящему компактные агенты. Для одного админа в одной компании Go, как правило, будет избыточен.
Итог
Итак, за 15 лет мы не просто пришли, а буквально выстрадали и выстроили наш стек из четырёх языков, каждый из которых занял свою уникальную нишу. У нас есть PowerShell для Windows-инфры и AD (это 47 скриптов), Python для работы с данными и интеграций (71 скрипт), Go для агентов и кросс-платформенных утилит (12 утилит) и, конечно, Bash+Ansible для всего нашего Linux-парка (28 ролей). Все эти 158 продакшн-скриптов обслуживают 22 клиента, проходят через жёсткий GitLab CI, обязательно подвергаются code review и версионируются аж с 2014 года. Мы не следуем идеологии «один язык для всего», наш подход — это чисто инженерный принцип: «правильный инструмент для каждой задачи».
Похожая задача в вашей компании?
Расскажите, что у вас сейчас — пришлю план работ и оценку в течение рабочего дня.
Написать в Telegram или +7 903 729-62-41
Семёнов Е.С., руководитель ITfresh
