Управление модулями PowerShell в корпоративной среде: Gallery, приватный NuGet и контроль версий
Меня зовут Семёнов Евгений Сергеевич, директор АйТи Фреш. За 15+ лет мы у себя в команде прошли все стадии отношений с модулями PowerShell: от «качаю всё из Gallery под рутом» до «каждый модуль проверен, подписан и лежит в корпоративном репозитории с контролем версий». В этой статье — как правильно ставить, обновлять, держать в порядке и распространять модули на парк Windows-серверов, чтобы не получить по голове из-за подменённого модуля или конфликта версий в 3 часа ночи.
Что такое PowerShell Gallery
PowerShell Gallery — центральный публичный репозиторий модулей Microsoft на базе NuGet v2. Там лежат и модули самой Microsoft (Az, Microsoft.Graph, PackageManagement), и сообщества (ImportExcel, Pester, PSFramework, dbatools). Установка одной командой:
Find-Module -Name ImportExcel
Install-Module -Name ImportExcel -Scope CurrentUser -Force
Get-InstalledModule
Update-Module -Name ImportExcel
Плюсы: огромный каталог, быстрая установка. Минусы для корпоратива: зависимость от публичного ресурса, отсутствие контроля, риск supply chain атаки (в Gallery пару раз уже появлялись вредоносные пакеты под именами похожих на популярные).
PowerShellGet 2 vs PSResourceGet (v3)
| Особенность | PowerShellGet v2 (Install-Module) | PSResourceGet v3 (Install-PSResource) |
|---|---|---|
| Движок | NuGet v2, OneGet | NuGet v3 нативно |
| Скорость | Базовая | В 2–3 раза быстрее |
| Поддержка Semantic Versioning | Ограниченная | Полная |
| Прокси и аутентификация | Проблемы за корпоративным прокси | Улучшенная поддержка |
| Совместимость | PowerShell 5.1 и 7.x | PowerShell 5.1 и 7.x |
| Имя командлета | Install-Module, Find-Module | Install-PSResource, Find-PSResource |
Я у себя на всех новых машинах сразу ставлю PSResourceGet:
Install-Module Microsoft.PowerShell.PSResourceGet -Scope AllUsers -Force
# И теперь
Find-PSResource Az.Accounts
Install-PSResource Az.Accounts -Scope CurrentUser -Reinstall
Где PowerShell ищет модули
Переменная $env:PSModulePath определяет все пути. По умолчанию на Windows в PowerShell 7:
%USERPROFILE%\Documents\PowerShell\Modules— пользовательский (CurrentUser scope).%ProgramFiles%\PowerShell\Modules— AllUsers scope.%ProgramFiles%\PowerShell\7\Modules— встроенные в PowerShell 7.%ProgramFiles%\WindowsPowerShell\Modules— AllUsers для 5.1.%windir%\system32\WindowsPowerShell\v1.0\Modules— системные.
Для корпоратива я добавляю путь к сетевой шаре \\corp\PSModules в $PSModulePath через машинную переменную окружения:
[Environment]::SetEnvironmentVariable('PSModulePath',
"$env:PSModulePath;\\corp\PSModules", [EnvironmentVariableTarget]::Machine)
Все серверы забирают модули оттуда, я централизованно контролирую версии.
Приватный репозиторий модулей
Три варианта развёртывания приватного NuGet-репозитория:
- Azure Artifacts — SaaS, легко подключается к Azure DevOps, платный.
- ProGet от Inedo — on-premise, бесплатный Community Edition, поддержка NuGet, npm, Chocolatey.
- Nexus Repository OSS — бесплатный, универсальный, знаком многим.
- SMB-шара — самый простой вариант, работает без сервера NuGet.
SMB-вариант (годится для офисов до 100 серверов):
# На файл-сервере создаём шару \\files\PSRepo с правами Read для Domain Computers
# На клиенте
Register-PSResourceRepository -Name 'ITFreshLocal' `
-Uri '\\files\PSRepo' -Trusted
# Публикация своего модуля
Publish-PSResource -Path .\MyModule -Repository ITFreshLocal
# Установка
Find-PSResource -Repository ITFreshLocal
Install-PSResource MyModule -Repository ITFreshLocal
Версионирование и манифест
Каждый модуль имеет файл .psd1 — манифест. Ключевые поля:
@{
RootModule = 'MyModule.psm1'
ModuleVersion = '1.4.2'
CompatiblePSEditions = @('Desktop','Core')
PowerShellVersion = '5.1'
GUID = 'a3b1f7c9-...'
Author = 'ITFresh'
CompanyName = 'АйТи Фреш'
Copyright = '(c) 2026 ITFresh'
Description = 'Корпоративные функции автоматизации'
RequiredModules = @(
@{ ModuleName = 'PSFramework'; ModuleVersion = '1.12.0' }
)
FunctionsToExport = @('Get-CorpUser','Set-CorpLicense')
PrivateData = @{
PSData = @{
Tags = @('Corp','AD')
ProjectUri = 'https://gitlab.itfresh.local/ps/MyModule'
ReleaseNotes = 'Added support for M365 licensing groups'
}
}
}
Я использую Semantic Versioning: MAJOR.MINOR.PATCH. Breaking-изменения — бампаем MAJOR. Новая фича без ломки — MINOR. Баг-фикс — PATCH. Ваши скрипты зависящие от модуля указывают #Requires -Modules @{ModuleName='MyModule';RequiredVersion='1.4.2'} или версия диапазон через манифест.
Side-by-side установки версий
Install-Module -Name Az -RequiredVersion 11.6.0 -Scope AllUsers -AllowClobber
Install-Module -Name Az -RequiredVersion 12.1.0 -Scope AllUsers -AllowClobber
# Обе версии теперь в Program Files/PowerShell/Modules/Az/11.6.0 и /12.1.0
Get-Module -ListAvailable Az | Select-Object Name, Version
# Импорт конкретной версии
Import-Module Az -RequiredVersion 11.6.0
Автозагрузка (implicit loading при первом вызове функции) всегда берёт старшую версию. Если нужна старая — явный Import-Module -RequiredVersion до первого использования. Это важно помнить при работе с Az/Microsoft.Graph, где между мажорами ломаются параметры.
Мини-кейс: хаос с версиями Az на 30 серверах
Апрель 2025, клиент — девелопер недвижимости, 30 Windows Server 2019 и 2022. У них работал скрипт ежедневной выгрузки подписок Azure — в один день упал, причина: Connect-AzAccount в какой-то момент перестал принимать старый параметр, потому что на половине серверов Az обновился до 11.x, а скрипт ожидал 9.x. Классическая боль «pwsh автоматом обновляет модули при вызове Install-Module».
Решение заняло 4 дня:
- Развернули ProGet на виртуалке Dell с Xeon Platinum 8280, 32 ГБ RAM, 500 ГБ SSD, подключённой на 40G Mellanox к бэкапному стораджу. Хостинг в дата-центре МТС.
- Импортировали Az 11.6.0 и все зависимости из публичной Gallery в ProGet.
- Через DSC-конфигурацию на все 30 серверов выкатили ровно эту версию в AllUsers scope.
- Убрали с серверов публичный репозиторий (Unregister-PSResourceRepository PSGallery).
- Скрипт теперь всегда работает только с утверждённой версией.
Через полгода обновили до 12.1 — контролируемо, с тестом и откатом. Стоимость работ 55 тыс. руб., ежегодная экономия — около 30 часов на разбор инцидентов «опять что-то сломалось после обновления».
CI/CD для собственных модулей
Свой модуль разрабатывают как обычный код: репозиторий Git, тесты Pester, пайплайн с публикацией в приватный репозиторий. Упрощённый .gitlab-ci.yml:
test:
stage: test
script:
- pwsh -c "Invoke-Pester -Path ./tests -OutputFormat NUnitXml -OutputFile test.xml"
artifacts: { reports: { junit: test.xml } }
publish:
stage: deploy
only: [tags]
script:
- pwsh -c "Update-ModuleManifest -Path ./MyModule/MyModule.psd1 -ModuleVersion '$CI_COMMIT_TAG'"
- pwsh -c "Publish-PSResource -Path ./MyModule -Repository ITFreshLocal -ApiKey $env:REPO_KEY"
Теги вида v1.4.2 триггерят публикацию. Все участники команды знают: в приватный репо попадает только то, что прошло pipeline, с полной историей.
Безопасность и подписи
Корпоративные модули стоит подписывать. Это решает две задачи: защита от подмены в транзите и возможность использовать ExecutionPolicy AllSigned на рабочих станциях.
$cert = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert | Select-Object -First 1
Get-ChildItem .\MyModule\*.ps1, .\MyModule\*.psm1, .\MyModule\*.psd1 |
ForEach-Object { Set-AuthenticodeSignature -FilePath $_.FullName -Certificate $cert }
# Проверка
Get-AuthenticodeSignature .\MyModule\MyModule.psm1
Сертификат для подписи — выдаётся из корпоративного PKI (AD CS), шаблон Code Signing. Подписанные модули неизменяемы: любая модификация ломает подпись, PowerShell отказывается их грузить.
Чек-лист управления модулями
- PSResourceGet установлен на всех серверах.
- Приватный репозиторий (ProGet/Nexus/SMB) настроен и известен всем админам.
- Публичная Gallery либо отключена, либо помечена как Untrusted.
- Скрипты указывают конкретные версии зависимостей через #Requires или Import-Module -RequiredVersion.
- Свои модули — с манифестом, GUID, семантической версией.
- CI/CD публикует в репозиторий по тегам.
- Модули подписаны, ExecutionPolicy на рабочих станциях минимум RemoteSigned.
- Периодическая чистка старых версий — раз в квартал.
Наведём порядок в ваших модулях PowerShell
Развёрну приватный NuGet-репозиторий, перенесу корпоративные модули под контроль версий, подниму CI/CD на GitLab или Azure DevOps, обучу команду. Опыт 15+ лет с Windows-парками, серверное плечо — Dell с Xeon Platinum 8280 в дата-центре МТС на 40G Mellanox. От 10 рабочих мест до полноценного продакшен-CI/CD.
Телефон: +7 903 729-62-41
Telegram: @ITfresh_Boss
Семёнов Евгений Сергеевич, директор АйТи Фреш
FAQ — частые вопросы по модулям PowerShell
- Где PowerShell ищет модули?
- В путях переменной $env:PSModulePath. Для пользователя — ~/Documents/PowerShell/Modules, для всех — Program Files/PowerShell/Modules, системные — Windows/System32/WindowsPowerShell/v1.0/Modules. Можно добавить свой путь, например корпоративную шару.
- Разные версии одного модуля конфликтуют?
- PowerShell поддерживает side-by-side версии. При Install-Module ставится новая в подпапку с версией, обе живут рядом. Import-Module -RequiredVersion выбирает нужную. Но автозагрузка всегда берёт старшую.
- Зачем приватный репозиторий?
- Для корпоративных модулей, для кэширования публичной Gallery (на случай недоступности или санкционных ограничений), для контроля версий — никто не поставит неутверждённую версию.
- PSGet v2 или v3?
- Для новых проектов — v3 (Microsoft.PowerShell.PSResourceGet). Быстрее, совместим с NuGet v3, лучше работает за прокси. Но часть старых скриптов требует Install-Module — проще держать оба.
- Как задеплоить модуль на 50 серверов?
- Через DSC с PackageManagement-ресурсом, через Ansible с win_psmodule, через push-скрипт с Invoke-Command. Или централизованно установить в Program Files/PowerShell/Modules при развёртывании ОС.