· 15 мин чтения

Управление модулями 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, OneGetNuGet v3 нативно
СкоростьБазоваяВ 2–3 раза быстрее
Поддержка Semantic VersioningОграниченнаяПолная
Прокси и аутентификацияПроблемы за корпоративным проксиУлучшенная поддержка
СовместимостьPowerShell 5.1 и 7.xPowerShell 5.1 и 7.x
Имя командлетаInstall-Module, Find-ModuleInstall-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:

  1. %USERPROFILE%\Documents\PowerShell\Modules — пользовательский (CurrentUser scope).
  2. %ProgramFiles%\PowerShell\Modules — AllUsers scope.
  3. %ProgramFiles%\PowerShell\7\Modules — встроенные в PowerShell 7.
  4. %ProgramFiles%\WindowsPowerShell\Modules — AllUsers для 5.1.
  5. %windir%\system32\WindowsPowerShell\v1.0\Modules — системные.

Для корпоратива я добавляю путь к сетевой шаре \\corp\PSModules в $PSModulePath через машинную переменную окружения:

[Environment]::SetEnvironmentVariable('PSModulePath',
    "$env:PSModulePath;\\corp\PSModules", [EnvironmentVariableTarget]::Machine)

Все серверы забирают модули оттуда, я централизованно контролирую версии.

Приватный репозиторий модулей

Три варианта развёртывания приватного 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 дня:

Через полгода обновили до 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 отказывается их грузить.

Чек-лист управления модулями

Наведём порядок в ваших модулях 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 при развёртывании ОС.

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

Раз в неделю — практические гайды для руководителя IT и сисадмина: безопасность, 1С, миграции, резервные копии, лайфхаки из реальных проектов.

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

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