Si quieres leer más sobre builds, releases y el ALM de desarrollo de Dynamics 365 puedes leer mi guía sobre MSDyn365 y Azure DevOps ALM.

Mover datos desde producción a un entorno sandbox es algo que tenemos que hacer regularmente para tener datos reales para testear o debugar. Es un proceso lento que requiere de bastante tiempo y que se puede automatizar como expliqué en el post LCS DB API: automatizando la copia de la DB de Prod a Dev.

Yo pulsando el botón para subir un data package usando Azure DevOps
Yo pulsando el botón para subir un data package usando Azure DevOps

En este post voy a añadir un paso adicional al refresco de la base de datos: restaurar un data package (DP). ¿Por qué? Porque estoy seguro que todos necesitamos cambiar parámetros o endpoints en los entornos de test después de un refresco desde prod.

Puedes leer más sobre la API REST del DMF, que voy a usar, leyendo este post de Fabio Filardi: Dynamics 365 FinOps: Batch import automation with Azure Functions, Business Events and PowerBI.

Puedes aprender más sobre la API REST de LCS leyendo estos posts que escribí hace un tiempo. Te puede interesar leerlos porque voy a dar por explicadas algunas cosas que voy a referenciar en este post:

¿Cómo lo haremos?

La idea es la siguiente:

Me voy a saltar la parte de restaurar la base de datos porque ya lo expliqué aquí, esa es la base para este post.

El flujo será así:

  1. Obtenemos la URL SAS para el data package que hemos guardado en nuestro blob de Azure.
  2. Obtenemos la URL SAS del blob del entorno de destino de Dynamics 365 con la acción de OData GetAzureWriteUrl.
  3. Copiamos nuestro data package del blob de origen al blob de destino.
  4. Ejecutamos la importación con la acción ImportFromPackage.

Crear proyecto de exportación

Este es el primer pasom que tenemos que hacer en el entorno del que queramos sacar los datos. Tenemos que crear un proyecto de exportación que contenga todas las entidades que queremos restaurar en nuestro entorno de destino. Vamos al workspace de Gestión de datos y le damos a la tile Exportar:

Exportar en el workspace de gestión de datos
Exportar en el workspace de gestión de datos

Nos tenemos que asegurar que el check «Generate data package» está marcado en el proyecto de exportación. Después añadimos todas las entidades que queramos importar luego:

Proyecto de exportación DMF con el check "Generate data package" marcado
Proyecto de exportación DMF con el check «Generate data package» marcado

Ejecutamos el trabajo y cuando esté listo descargamos el data package.

Crear un Blob de Azure y subir el Data package

Creamos una cuenta de almacenamiento de Azure con un blob, primero la cuenta de almacenamiento, y dentro de esta el blob, y subimos el data package que hemos descargado.

Crear proyecto de importación

El siguiente paso es crear un proyecto de importación en el entorno de destino para importar nuestro data package y referenciarlo en nuestra pipeline. Seleccionalos el DP que hemos exportado en el primer paso y lo añadimos:

Proyecto de importación: añadir el data package
Proyecto de importación: añadir el data package

Pipeline de Azure DevOps

Y el último paso es crear una nueva pipeline o añadir pasos adicionales a la que ejecuta nuestro refresco de la base de datos de prod.

Actualización: nos vamos a olvidar de esta parte de subir el azcopy.exe y vamos a usar las d365fo.tools.

Lo que he hecho yo para empezar es subir azcopy.exe a mi repositorio de código:

azcopy.exe añadido a TFVC
azcopy.exe añadido a TFVC

¿Por qué? Porque he probado a usar la API REST de Blobs y me ha sido imposible copiar de un blob a otro, así que he ido por el camino sencillo (para mi).

Luego, mi pipeline tiene dos tareas o pasos:

Pipeline
Pipeline

En la sección de Get sources simplemente he mapeado la carpeta Tools, así:

Carpeta Tools mapeada como la raíz de $(build.sourcesDirectory)
Carpeta Tools mapeada como la raíz de $(build.sourcesDirectory)

Y sobre los pasos, ambos son tareas de PowerShell. En la primera simplemente instalo el módulo PowerShell de Azure y las d365fo.tools, y posteriormente instalamos azcopy en c:\temp. Podría hacer esto en un paso, pero lo he hecho así sólo por un tema de organización.

La primera tarea tiene este script:

Install-Module -Name AZ -AllowClobber -Scope CurrentUser -Force -Confirm:$False -SkipPublisherCheck
Install-Module -Name d365fo.tools -AllowClobber -Scope CurrentUser -Force -Confirm:$false
Invoke-D365InstallAzCopy -Path "C:\temp\AzCopy.exe"

Así, cada vez que se ejecuta la pipeline hospedada por Microsoft se va a instalar el módulo de PowerShell de Azure.

Y en el siguiente paso es en el que sucede todo, este es el script completo que luego veremos por partes:

# Getting settings
$file = "YOUR_EXPORTED_DP.zip"
$saname = "YOUR_STORTAGE_ACCOUNT_NAME"
$containername = "CONTAINER_NAME"
$key = "ACCESS_KEY"
$environmentUrl = "Dynamics365FNO_ENVIRONMENT_URL"
# Get source blob URL
$ctx = New-AzStorageContext -StorageAccountName $saname -StorageAccountKey $key
$blob = Get-AzStorageBlob -Blob $file -Container $containername -Context $ctx -ErrorAction Stop
$StartTime = Get-Date
$EndTime = $startTime.AddMinutes(2.0)
$sourceUrl = New-AzStorageBlobSASToken -Container $containername -Blob $file -Permission r -StartTime $StartTime -ExpiryTime $EndTime -FullUri -Context $ctx
# GET BLOB DEST URL
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("Content-Type", "application/x-www-form-urlencoded")
$headers.Add("Accept", "application/json")
$body = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$body.Add("tenant_id", "yourtenant.com")
$body.Add("client_id", "AZURE_APP_ID")
$body.Add("client_secret", "SECRET")
$body.Add("grant_type", "client_credentials")
$body.Add("resource", $environmentUrl)
$response = Invoke-RestMethod 'https://login.microsoftonline.com/yourtenant.com/oauth2/token' -Method 'POST' -Headers $headers -Body $body
$response | ConvertTo-Json
#UPLOAD/copy from source to dest
$headersDest = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headersDest.Add("Content-Type", "application/json")
$tokenAuth = "Bearer " + $response.access_token
$headersDest.Add("Authorization", $tokenAuth)
$currDate = Get-Date -Format "yyyyMMdd_HHmmss"
$uploadId = "restoreDP" + $currDate + ".zip"
$bodyDest = '{"uniqueFileName": "' + $uploadId + '"}'
$getAzureUrl = $environmentUrl + "https://static.ariste.info/data/DataManagementDefinitionGroups/Microsoft.Dynamics.DataEntities.GetAzureWriteUrl"
$responseDest = Invoke-RestMethod $getAzureUrl -Method 'POST' -Headers $headersDest -Body $bodyDest
$responseDest | ConvertTo-Json
$objUrl = $responseDest.value | ConvertFrom-Json
$destinationUrl = $objUrl.BlobUrl
#COPY TO TARGET
$fileNameCopy = "c:\temp\" + $uploadId + ".zip"
c:\temp\azcopy.exe copy $sourceUrl $fileNameCopy --recursive
c:\temp\azcopy.exe copy $fileNameCopy $destinationUrl --recursive
# EXECUTE IMPORT
$headersImport = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headersImport.Add("Content-Type", "application/json")
$headersImport.Add("Authorization", $tokenAuth)
$bodyImport = "{
`n    `"packageUrl`": `"" + $destinationUrl + "`",
`n    `"definitionGroupId`": `"ImportDP`",
`n    `"executionId`": `"`",
`n    `"execute`": true,
`n    `"overwrite`": true,
`n    `"legalEntityId`": `"USMF`"
`n}"
$executeImportUrl = $environmentUrl + "https://static.ariste.info/data/DataManagementDefinitionGroups/Microsoft.Dynamics.DataEntities.ImportFromPackage"
$responseImport = Invoke-RestMethod $executeImportUrl -Method 'POST' -Headers $headersImport -Body $bodyImport

Paso a paso

El primer bloque corresponde con la configuración de tu blob de Azure y tu entorno de Dynamics 365 Finance and Operations:

# Getting settings
$file = "YOUR_EXPORTED_DP.zip"
$saname = "YOUR_STORTAGE_ACCOUNT_NAME"
$containername = "CONTAINER_NAME"
$key = "ACCESS_KEY"
$environmentUrl = "Dynamics365FNO_ENVIRONMENT_URL"

A continuación obtenemos la URS SAS para el blob de origenm usando el módulo de PowerShell de Azure, que es la cuenta que hemos creado antes. Para asegurarnos de que no la liemos con el token SAS vamos a darle únicamente 2 minutos de validez:

# Get source blob URL
$ctx = New-AzStorageContext -StorageAccountName $saname -StorageAccountKey $key
$blob = Get-AzStorageBlob -Blob $file -Container $containername -Context $ctx -ErrorAction Stop
$StartTime = Get-Date
$EndTime = $startTime.AddMinutes(2.0)

$sourceUrl = New-AzStorageBlobSASToken -Container $containername -Blob $file -Permission r -StartTime $StartTime -ExpiryTime $EndTime -FullUri -Context $ctx

Ya tenemos la URL de origen, ahora vamos a obtener la URL del blob de nuestro entorno de MSDyn365FO usando la acción de OData GetAzureWriteUrl. Solicitamos el token de autenticación para hacer la llamada y también generamos un nombre único a partir de la fecha y hora actuales:

# GET BLOB DEST URL
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("Content-Type", "application/x-www-form-urlencoded")
$headers.Add("Accept", "application/json")

$body = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$body.Add("tenant_id", "yourtenant.com")
$body.Add("client_id", "AZURE_APP_ID")
$body.Add("client_secret", "SECRET")
$body.Add("grant_type", "client_credentials")
$body.Add("resource", $environmentUrl)

$response = Invoke-RestMethod 'https://login.microsoftonline.com/yourtenant.com/oauth2/token' -Method 'POST' -Headers $headers -Body $body
$response | ConvertTo-Json

#UPLOAD/copy from source to dest
$headersDest = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headersDest.Add("Content-Type", "application/json")
$tokenAuth = "Bearer " + $response.access_token
$headersDest.Add("Authorization", $tokenAuth)

$currDate = Get-Date -Format "yyyyMMdd_HHmmss"
$uploadId = "restoreDP" + $currDate + ".zip"
$bodyDest = '{"uniqueFileName": "' + $uploadId + '"}'
$getAzureUrl = $environmentUrl + "https://static.ariste.info/data/DataManagementDefinitionGroups/Microsoft.Dynamics.DataEntities.GetAzureWriteUrl"

$responseDest = Invoke-RestMethod $getAzureUrl -Method 'POST' -Headers $headersDest -Body $bodyDest
$responseDest | ConvertTo-Json
$objUrl = $responseDest.value | ConvertFrom-Json
$destinationUrl = $objUrl.BlobUrl

Ahora tenemos tanto las URL SAS de origen como destino, y vamos a usar azcopy para descargar el data package que hemos creado y copiarlo al entorno de destino en el que vamos a restaurarlo:

#COPY TO TARGET
$fileNameCopy = "c:\temp\" + $uploadId + ".zip"

$(build.sourcesDirectory)\azcopy.exe copy $sourceUrl $fileNameCopy --recursive

$(build.sourcesDirectory)\azcopy.exe copy $fileNameCopy $destinationUrl --recursive

Y, finalmente, vamos a lanzar la importación usando la acción ImportFromPackage con los parámetros de nuestro entorno de Dynamics 365 en el cuerpo de la petición:

# EXECUTE IMPORT
$headersImport = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headersImport.Add("Content-Type", "application/json")
$headersImport.Add("Authorization", $tokenAuth)

$bodyImport = "{
`n    `"packageUrl`": `"" + $destinationUrl + "`",
`n    `"definitionGroupId`": `"ImportDP`",
`n    `"executionId`": `"`",
`n    `"execute`": true,
`n    `"overwrite`": true,
`n    `"legalEntityId`": `"USMF`"
`n}"

$executeImportUrl = $environmentUrl + "https://static.ariste.info/data/DataManagementDefinitionGroups/Microsoft.Dynamics.DataEntities.ImportFromPackage"

$responseImport = Invoke-RestMethod $executeImportUrl -Method 'POST' -Headers $headersImport -Body $bodyImport

Cuando termine la última llamada REST podemos ir al workspace de Gestión de datos en Dynamics 365 y ver que el job está ahí:

Trabajo de importación de gestión de datos
Trabajo de importación de gestión de datos

¡Y listo! Ya podemos refrescar un entorno con datos de producción y dejarlo listo con toda la parametrización cambiada o incluso usuarios activados.

Observaciones finales

Como siempre, ha habido mucha prueba y error haciendo esto, y estoy seguro que el script se puede mejorara y hacer algunas cosas de forma distinta y mejor.

También decir que no soy muy habilidoso con PoweShell, y puede que haya algunas best practices como bloques try-catch, controlar salidas y resultados de operaciones que se podrían mejorar. Simplemente he querido mostrar el proceso y que se puede hacer.

¡Suscríbete!

Recibe un correo cuando se publique un nuevo post
Author

Microsoft Dynamics 365 Finance & Operations technical architect and developer. Business Applications MVP since 2020.

Write A Comment

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

ariste.info