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.
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:
- API REST de movimiento de base de datos de LCS
- LCS DB API: automatizando la copia de la DB de Prod a Dev
- Llama a la API de Movimiento de DB de LCS en tu pipeline de Azure DevOps
¿Cómo lo haremos?
La idea es la siguiente:
- Vamos a usar la API de LCS para refrescar un entorno sandbox con datos de producción.
- Previamente hemos exportado el data package que queremos importar.
- Necesitaremos una cuenta de almacenamiento de Azure de tipo blob para subir y guardar el data package exportado ahí.
- Usaremos la API REST del Data management para obtener la URL del blob del entorno de Dynamics 365 al que queremos importar el data package.
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í:
- Obtenemos la URL SAS para el data package que hemos guardado en nuestro blob de Azure.
- Obtenemos la URL SAS del blob del entorno de destino de Dynamics 365 con la acción de OData GetAzureWriteUrl.
- Copiamos nuestro data package del blob de origen al blob de destino.
- 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:
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:
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:
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:
¿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:
En la sección de Get sources simplemente he mapeado la carpeta Tools, así:
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í:
¡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.