Como criar módulos PowerShell reutilizáveis para o time

1. Fundamentos de Módulos PowerShell

Módulos PowerShell são pacotes que contêm funções, variáveis e recursos relacionados, organizados em uma estrutura padronizada. Diferentemente de scripts avulsos, módulos oferecem:

  • Encapsulamento: funções internas ficam isoladas do escopo global
  • Versionamento: controle semântico de versões
  • Distribuição: facilidade para compartilhar com o time

A estrutura básica de um módulo consiste em:

MeuModulo/
├── MeuModulo.psm1      # Script do módulo (código principal)
├── MeuModulo.psd1      # Manifesto (metadados e configurações)
├── pt-BR/              # Pastas de recursos localizados (opcional)
└── en-US/

Para carregar um módulo:

# Importar módulo do diretório atual
Import-Module .\MeuModulo.psd1 -Force

# Verificar módulos carregados
Get-Module

# Remover módulo da sessão
Remove-Module MeuModulo

A diferença fundamental entre scripts e módulos está no escopo: módulos criam um contexto isolado, evitando poluição do namespace global.

2. Criando o Manifesto do Módulo (.psd1)

O manifesto é o coração do módulo. Use New-ModuleManifest para gerar um template:

New-ModuleManifest -Path .\MeuModulo\MeuModulo.psd1 `
    -RootModule MeuModulo.psm1 `
    -ModuleVersion "1.0.0" `
    -Author "Seu Nome" `
    -CompanyName "Sua Empresa" `
    -Description "Módulo para automação de infraestrutura" `
    -Guid "a1b2c3d4-e5f6-7890-abcd-ef1234567890"

Exemplo completo de manifesto editado manualmente:

@{
    RootModule        = 'MeuModulo.psm1'
    ModuleVersion     = '1.0.0'
    GUID              = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
    Author            = 'Time de DevOps'
    CompanyName       = 'Empresa XYZ'
    Copyright         = '(c) 2025 Empresa XYZ. Todos os direitos reservados.'
    Description       = 'Módulo reutilizável para gerenciamento de servidores'

    # Dependências
    RequiredModules   = @(
        @{ModuleName = 'Pester'; ModuleVersion = '5.0.0'}
        @{ModuleName = 'PSWriteHTML'; ModuleVersion = '1.0.0'}
    )

    RequiredAssemblies = @('System.Management.Automation')

    # Exportação controlada
    FunctionsToExport = @('Get-ServerInfo', 'Set-ServerConfig', 'New-Report')
    CmdletsToExport   = @()
    VariablesToExport = @()
    AliasesToExport   = @()

    PrivateData = @{
        PSData = @{
            Tags         = @('infraestrutura', 'monitoramento', 'devops')
            ProjectUri   = 'https://github.com/empresa/meu-modulo'
            LicenseUri   = 'https://opensource.org/licenses/MIT'
        }
    }
}

3. Estruturando Funções Modulares

A nomenclatura deve seguir os verbos aprovados pelo PowerShell (Get-, Set-, New-, Remove-, Test-) com sufixos de domínio específicos.

Exemplo de função bem estruturada:

<#
.SYNOPSIS
    Obtém informações detalhadas de um servidor remoto.

.DESCRIPTION
    Esta função consulta WMI e WinRM para retornar CPU, memória e disco.
    Suporta múltiplos servidores e pipeline.

.PARAMETER ComputerName
    Nome ou IP do servidor. Aceita múltiplos valores via pipeline.

.PARAMETER Credential
    Credencial alternativa para conexão remota.

.EXAMPLE
    Get-ServerInfo -ComputerName "SRV001"

.EXAMPLE
    "SRV001", "SRV002" | Get-ServerInfo -Credential (Get-Credential)
#>
function Get-ServerInfo {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
        [ValidateNotNullOrEmpty()]
        [string[]]$ComputerName,

        [Parameter(Mandatory=$false)]
        [System.Management.Automation.PSCredential]$Credential
    )

    begin {
        Write-Verbose "Iniciando consulta de servidores..."
        $results = [System.Collections.ArrayList]::new()
    }

    process {
        foreach ($computer in $ComputerName) {
            try {
                Write-Verbose "Consultando servidor: $computer"

                $session = New-CimSession -ComputerName $computer -Credential $Credential -ErrorAction Stop

                $cpu = Get-CimInstance -CimSession $session -ClassName Win32_Processor
                $memory = Get-CimInstance -CimSession $session -ClassName Win32_OperatingSystem

                [void]$results.Add([PSCustomObject]@{
                    ComputerName = $computer
                    CPU          = "{0}% - {1}" -f $cpu.LoadPercentage, $cpu.Name
                    MemoryGB     = "{0:N2} GB / {1:N2} GB" -f 
                        (($memory.TotalVisibleMemorySize - $memory.FreePhysicalMemory) / 1MB),
                        ($memory.TotalVisibleMemorySize / 1MB)
                    Timestamp    = Get-Date
                })

                Remove-CimSession -CimSession $session
            }
            catch {
                Write-Error "Falha ao consultar $computer : $_"
            }
        }
    }

    end {
        Write-Verbose "Consulta concluída. Retornando $($results.Count) resultados."
        return $results
    }
}

4. Práticas Avançadas de Reutilização

Organize funções internas (privadas) separadas das públicas:

# Estrutura de pastas do módulo
MeuModulo/
├── Public/               # Funções exportadas
│   ├── Get-ServerInfo.ps1
│   └── New-Report.ps1
├── Private/              # Funções internas
│   ├── Invoke-WmiQuery.ps1
│   └── Format-Output.ps1
└── MeuModulo.psm1        # Carrega tudo dinamicamente

No arquivo .psm1, use carregamento dinâmico:

# Carrega funções privadas primeiro
Get-ChildItem -Path $PSScriptRoot\Private\*.ps1 | ForEach-Object {
    . $_.FullName
}

# Depois carrega funções públicas
Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 | ForEach-Object {
    . $_.FullName
}

# Exporta apenas as funções públicas
Export-ModuleMember -Function (Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 | 
    ForEach-Object { [System.IO.Path]::GetFileNameWithoutExtension($_.Name) })

Implemente logging estruturado:

function Invoke-MaintenanceTask {
    [CmdletBinding()]
    param([string]$TaskName)

    Write-Information "Iniciando tarefa: $TaskName" -InformationAction Continue

    try {
        # Lógica principal
        Write-Verbose "Executando etapa 1..."
        Write-Debug "DEBUG: Variável interna = $internalVar"

        # Resultado
        Write-Information "Tarefa concluída com sucesso" -InformationAction Continue
    }
    catch {
        Write-Warning "Falha na tarefa $TaskName : $_"
        throw
    }
}

5. Gerenciamento de Erros e Tratamento de Exceções

Implemente tratamento robusto com padrões consistentes:

function Set-ServerConfig {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateScript({Test-Connection $_ -Quiet -Count 1})]
        [string]$ComputerName,

        [Parameter(Mandatory=$true)]
        [ValidateSet('Production', 'Staging', 'Development')]
        [string]$Environment
    )

    $ErrorActionPreference = 'Stop'

    try {
        Write-Verbose "Configurando ambiente $Environment em $ComputerName"

        # Operação principal
        Invoke-Command -ComputerName $ComputerName -ScriptBlock {
            param($env)
            # Lógica de configuração
        } -ArgumentList $Environment

        return [PSCustomObject]@{
            ComputerName = $ComputerName
            Environment  = $Environment
            Status       = 'Configured'
            Timestamp    = Get-Date
        }
    }
    catch [System.UnauthorizedAccessException] {
        $errorRecord = [System.Management.Automation.ErrorRecord]::new(
            $_,
            'AccessDenied',
            [System.Management.Automation.ErrorCategory]::PermissionDenied,
            $ComputerName
        )
        Write-Error $errorRecord
    }
    catch {
        Write-Error "Erro inesperado em $ComputerName : $_"
        throw
    }
    finally {
        Write-Verbose "Finalizando configuração de $ComputerName"
    }
}

6. Versionamento e Distribuição para o Time

Adote versionamento semântico no manifesto:

# Exemplo de changelog (CHANGELOG.md)
# 1.1.0 - 2025-03-15
# - Adicionado suporte a Credential
# - Corrigido bug na consulta de memória
# - Melhorado logging com Write-Information

# Script de bootstrap para instalação
$moduleName = "MeuModulo"
$repoUrl = "https://nuget.empresa.com/api/v2"

if (-not (Get-Module -ListAvailable -Name $moduleName)) {
    Register-PSRepository -Name "EmpresaRepo" -SourceLocation $repoUrl -InstallationPolicy Trusted
    Install-Module -Name $moduleName -Repository "EmpresaRepo" -Scope CurrentUser
}

Import-Module $moduleName -Force

Para distribuição via pasta de rede:

# Publicar módulo
$destination = "\\fileserver\modules\$moduleName"
Copy-Item -Path ".\$moduleName" -Destination $destination -Recurse -Force

# Adicionar ao PSModulePath
$env:PSModulePath += ";\\fileserver\modules"

7. Testes e Validação Contínua

Crie testes unitários com Pester:

# Tests\Get-ServerInfo.Tests.ps1
BeforeAll {
    $modulePath = Join-Path $PSScriptRoot '..\MeuModulo'
    Import-Module $modulePath -Force
}

Describe 'Get-ServerInfo' {
    Context 'Parâmetros' {
        It 'Deve aceitar pipeline' {
            { 'SRV001' | Get-ServerInfo } | Should -Not -Throw
        }

        It 'Deve rejeitar ComputerName vazio' {
            { Get-ServerInfo -ComputerName '' } | Should -Throw
        }
    }

    Context 'Funcionalidade' {
        It 'Deve retornar objeto com propriedades corretas' {
            Mock -CommandName New-CimSession -MockWith { [PSCustomObject]@{Id=1} }
            Mock -CommandName Get-CimInstance -MockWith { 
                [PSCustomObject]@{LoadPercentage=25; Name='Intel Xeon'}
            }

            $result = Get-ServerInfo -ComputerName 'localhost'

            $result | Should -HaveProperty 'ComputerName'
            $result | Should -HaveProperty 'CPU'
            $result | Should -HaveProperty 'MemoryGB'
        }
    }
}

Execute testes localmente ou em CI/CD:

# Executar testes
Invoke-Pester -Path .\Tests\ -Output Detailed

# Integração com GitHub Actions
# .github/workflows/test.yml
# name: Test Module
# on: [push, pull_request]
# jobs:
#   test:
#     runs-on: windows-latest
#     steps:
#       - uses: actions/checkout@v2
#       - name: Run Pester tests
#         shell: pwsh
#         run: |
#           Install-Module Pester -Force -SkipPublisherCheck
#           Invoke-Pester -Path .\Tests\ -Output Detailed -PassThru

Referências