PowerShell Invoke-Command: Una Guía Completa

Los profesionales de TI rara vez trabajan solo en nuestra computadora local. ¡Con el cmdlet Invoke-Command de PowerShell, no tenemos que hacerlo! Este cmdlet nos permite escribir código de manera fluida como si estuviéramos trabajando en nuestra computadora local.

Al utilizar la función de Remoting de PowerShell, el cmdlet Invoke-Command es un cmdlet de PowerShell comúnmente utilizado que permite al usuario ejecutar código dentro de una PSSession. Esta PSSession puede ser una creada previamente con el cmdlet New-PSSession, o también puede crear y destruir rápidamente una sesión temporal.

Relacionado: Remoting de PowerShell: La Guía Definitiva

Piensa en Invoke-Command como el psexec de PowerShell. Aunque están implementados de manera diferente, el concepto es el mismo. Toma un poco de código o comando y ejecútalo “localmente” en la computadora remota.

Sin embargo, para que Invoke-Command funcione, debes tener habilitado y disponible el Remoting de PowerShell en la computadora remota. Por defecto, todas las máquinas con Windows Server 2012 R2 o posterior lo tienen habilitado junto con las excepciones de firewall apropiadas. Si tienes la mala suerte de seguir teniendo máquinas Server 2008, hay varias formas de configurar el Remoting, pero una manera fácil es ejecutar winrm quickconfig o Enable-PSRemoting en la máquina remota.

Para demostrar cómo funciona Invoke-Command con un “comando ad-hoc”, lo que significa que no se requiere crear una nueva PSSession, supongamos que tienes un equipo remoto con Windows Server 2012 R2 o posterior unido a un dominio. Las cosas se complican un poco cuando se trabaja en equipos de grupo de trabajo. Abriré mi consola de PowerShell, escribiré Invoke-Command y presionaré Enter.

PS> Invoke-Command
cmdlet Invoke-Command at command pipeline position 1
Supply values for the following parameters:
ScriptBlock:

I’m immediately asked to provide a scriptblock. The scriptblock is the code that we’re going to run on the remote computer.

Para demostrar que el código dentro del scriptblock se ejecuta en el equipo remoto, simplemente ejecutaremos el comando hostname. Este comando devolverá el nombre del equipo en el que se está ejecutando. Ejecutar hostname en mi equipo local devuelve su nombre.

PS> hostname
MACWINVM

Ahora pasemos un scriptblock con ese mismo código dentro de un scriptblock a Invoke-Command. Antes de hacer eso, olvidamos un parámetro requerido: ComputerName. Tenemos que decirle a Invoke-Command en qué equipo remoto ejecutar este comando.

PS> Invoke-Command -ScriptBlock { hostname } -ComputerName WEBSRV1 WEBSRV1

Observa que la salida de hostname ahora es el nombre del equipo remoto WEBSRV1. Has ejecutado algún código en WEBSRV1. Ejecutar código simple dentro de un scriptblock y pasarlo a una sola máquina remota es la aplicación más sencilla de Invoke-Command, pero puede hacer mucho más.

Pasando variables locales a scriptblocks remotos.

No vas a tener una única referencia Invoke-Command dentro de un script. Tu script probablemente será de varias líneas, con variables definidas en lugares específicos, funciones definidas en módulos, y así sucesivamente. Aunque envolver algún código entre un par de llaves rizadas pueda parecer inocente, estás cambiando todo el ámbito en el que se ejecuta ese código. Después de todo, estás enviando ese código a una computadora remota. Esa computadora remota no tiene idea de todo el código local en tu máquina, excepto lo que está en el bloque de script.

Por ejemplo, tal vez tienes una función con el nombre de la computadora y un parámetro de ruta de archivo. El propósito de esta función es ejecutar algún instalador de software en la computadora remota. Puedes pasar el nombre de la computadora y la ruta de archivo “local” al instalador que ya está ubicado en la computadora remota.

La función a continuación parece razonable, ¿verdad? Vamos a ejecutarla.

function Install-Stuff {
    param(
    	[Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$ComputerName,
        
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$InstallerFilePath
	)
    Invoke-Command -ComputerName $ComputerName -ScriptBlock { & $InstallerFilePath }
}

PS> Install-Stuff -ComputerName websrv1 -InstallerFilePath 'C:\install.exe'
The expression after '&' in a pipeline element produced an object that was not valid. It must result in a command name, a script block, or a CommandInfo object.
+ CategoryInfo          : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : BadExpression
+ PSComputerName        : websrv1

Falla con un mensaje de error oscuro debido al uso del operador ampersand. El código no estaba mal, pero falló porque $InstallerFilePath estaba vacío a pesar de que pasaste un valor con el parámetro de la función. Podemos probar esto reemplazando el ampersand con Write-Host.

Nueva línea de función: Invoke-Command -ComputerName $ComputerName -ScriptBlock { Write-Host "La ruta del instalador es: $InstallerFilePath" }

PS> Install-Stuff -ComputerName websrv1 -InstallerFilePath 'C:\install.exe'
Installer path is:
PS>

Tenga en cuenta que el valor de $InstallerFilePath es nada. La variable no se ha expandido porque no se pasó a la máquina remota. Para pasar variables definidas localmente al scriptblock remoto, tenemos dos opciones; podemos anteponer el nombre de la variable con $using: dentro del scriptblock o podemos usar el parámetro ArgumentList de Invoke-Command. Veamos ambos.

El Parámetro ArgumentList

Una forma de pasar variables locales a un scriptblock remoto es usar el parámetro ArgumentList de Invoke-Command. Este parámetro le permite pasar variables locales al parámetro y reemplazar las referencias de variables locales en el scriptblock con marcadores de posición.

Pasar las variables locales al parámetro ArgumentList es fácil.

Invoke-Command -ComputerName WEBSRV1 -ScriptBlock { & $InstallerFilePath } -ArgumentList $InstallerFilePath

Lo que confunde a algunas personas es cómo estructurar las variables dentro del scriptblock. En lugar de usar { & $InstallerPath }, necesitamos cambiarlo para que sea { & $args[0] } o {param($foo) & $foo }. Ambos funcionan igual, pero ¿cuál debería usar?

El parámetro ArgumentList es una colección de objetos. Las colecciones de objetos le permiten pasar uno o más objetos a la vez. En este caso, solo estoy pasando uno.

Cuando se ejecuta, el cmdlet Invoke-Command toma esa colección y la inyecta en el bloque de script, transformándolo en un array llamado $args. Recuerda que $args -eq ArgumentList. En este punto, debes referenciar cada índice de la colección como lo harías con un array. En nuestro caso, solo teníamos un elemento en la colección ($InstallerFilePath) que se “traduce” a $args[0], lo que significa el primer índice de esa colección. Sin embargo, si tuvieras más elementos, los referenciarías como $args[1], $args[2], y así sucesivamente.

Además, si prefieres asignar mejores nombres de variables a las variables del bloque de script, también puedes agregar parámetros al bloque de script como si fuera una función. Después de todo, un bloque de script es simplemente una función anónima. Para crear parámetros de bloque de script, crea un bloque param con el nombre del parámetro. Una vez creado, puedes referenciar ese parámetro en el bloque de script como se muestra a continuación.

Invoke-Command -ComputerName WEBSRV1 -ScriptBlock { param($foo) & $foo } -ArgumentList $InstallerFilePath

En este caso, los elementos en la colección ArgumentList se “mapean” a los parámetros definidos en orden. Los nombres de los parámetros no importan; lo importante es el orden. Invoke-Command tomará el primer elemento en la colección ArgumentList, buscará el primer parámetro y mapeará esos valores, hará lo mismo para el segundo, el tercero y así sucesivamente.

La construcción $Using

El constructo $using es otra forma popular de pasar variables locales a un scriptblock remoto. Este constructo te permite reutilizar las variables locales existentes simplemente anteponiendo el nombre de la variable con $using:. No es necesario preocuparse por una colección $args ni agregar un bloque de parámetros.

Invoke-Command -ComputerName WEBSRV1 -ScriptBlock { & $using:InstallerFilePath }

El constructo $using de PowerShell es mucho más simple, pero si alguna vez te adentras en el aprendizaje de Pester, verás que ArgumentList será tu amigo.

Invoke-Command y New-PSSession

Técnicamente, esta publicación trata solo sobre Invoke-Command, pero para demostrar su utilidad, necesitamos tocar brevemente el comando New-PSSession también. Recuerda que mencioné anteriormente que Invoke-Command puede usar comandos “ad-hoc” o utilizar sesiones existentes.

A lo largo de esta publicación, simplemente hemos estado ejecutando comandos “ad-hoc” en computadoras remotas. Hemos estado iniciando una nueva sesión, ejecutando código y cerrándola. Esto está bien para situaciones puntuales, pero no tanto para momentos en los que estás realizando docenas de comandos en la misma computadora. En este caso, es mejor reutilizar una sesión PSSession existente creándola con New-PSSession de antemano.

Antes de ejecutar cualquier comando, primero necesitarás crear una PSSession con New-PSSession. Podemos hacer esto simplemente ejecutando $session = New-PSSession -ComputerName WEBSRV1. Esto crea una sesión remota en el servidor, así como una referencia a esa sesión en mi máquina local. En este punto, puedo reemplazar mis referencias de ComputerName con Session y apuntar Session a mi variable guardada $session.

Invoke-Command -Session $session -ScriptBlock { & $using:InstallerFilePath }

Al ejecutarlo, notarás que el rendimiento es más rápido porque la sesión ya ha sido creada. Sin embargo, cuando hayas terminado, es importante eliminar la sesión abierta con Remove-PSSession.

Resumen

El cmdlet PowerShell Invoke-Command es uno de los cmdlets más comunes y potentes que existen. Es uno que personalmente uso más que casi todos los demás. Su facilidad de uso y capacidad para ejecutar cualquier código en computadoras remotas es extremadamente poderosa y es un comando que recomiendo aprender de arriba abajo.

Source:
https://adamtheautomator.com/invoke-command/