Стек ITfresh: PowerShell, Python, Go, Bash — что для какой задачи
У нас в ITfresh за 15 лет обслуживания 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-строчный отчёт по почте — избыточно. Каждый язык лучше всего работает в своей нише, и мы используем сильные стороны каждого, не пытаясь натянуть универсальное решение.
Сколько строк кода вы написали за 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