Showing posts with label powershell. Show all posts
Showing posts with label powershell. Show all posts

Monday, 13 March 2023

Something went wrong. See SPE logs for more details.

 A nice quick one since I didn't find much useful info around on this particular issue (maybe nobody else has made this stupid mistake).

While working on a client implementation I came across the following error:

7952 03:31:46 INFO  Script item set to master:\system\Modules\PowerShell\Script Library\JSS SXA\Scaffolding\Content Editor\Insert Item\JSS Site in ScriptSession $scriptSession$|gmnwpaqx1sn0u3gz2hwu5bkr|fef42854-5de9-4f59-ab6a-13edd7d2862e.
ManagedPoolThread #4 03:31:47 ERROR Cannot bind argument to parameter 'TenantTemplatesRoot' because it is null.
7444 03:36:59 WARN  Session state elevated for 'ItemSave' by user: sitecore\admin

Digging into the powershell script which was being called (/sitecore/system/Modules/PowerShell/Script Library/JSS SXA/Scaffolding/Functions/New-JSSSite) you can quickly find the relevant line: 

$tenantTemplatesRootID = $tenant.Fields['Templates'].Value

Looking at the fields in my Tenant, I noticed they were all blank! Seems that while transferring items between environments someone forgot to package some of the necessary items.

Long story short, when packaging up your Tenant don't forget to include:

  1. /sitecore/templates/Project/YourTenant
  2. /sitecore/media library/Project/YourTenant
  3. /sitecore/media library/Project/YourTenant/shared
  4. /sitecore/layout/Renderings/Project/YourTenant
  5. /sitecore/layout/Placeholder Settings/Project/YourTenant

Tuesday, 5 December 2017

Enhancing Sitecore ARM templates to be production-ready

The Sitecore ARM templates are great for most environments, and better yet they are extensible, however in a production environment you're probably going to need a few more things in your environment; things like staging slots, your own hostname(s), and maybe some storage / networking / traffic manager.

As I mentioned, Sitecore has made the their templates in a fairly modular fashion: you have your base deployment which takes all parameters, and runs a sub-deployment for '-infrastructure' (your Azure resources), '-application' (Sitecore package installation), and a sub-deployment for each Sitecore module.
This gives us the option of just running a separate template after running the base Sitecore templates, or we can integrate our additional deployment as another sub-deployment in the base template in the same way.  Obviously we want to keep our enhancements as separate as possible so that when there is an update to Sitecore or their templates it doesn't mean a massive and painful re-write of our enhancements.

I've uploaded an ARM template as a gist, and included the extra Powershell code below. They do the following:
  • Change your CM + CD hosting plan to Standard
  • Add staging slots to CM + CD
  • Copy files from your CM + CD to the staging slot
  • Add custom hostnames
  • Add an SSL certificate and enable HTTPS for all your hostnames
  • Add connection strings to your Rep / Prc web apps so you can swap them
  • Add custom firewall rules
See the gist of the ARM template which you can run on its own, or as a sub-deployment, in which case you should place this in the "nested" folder, along with the infrastructure.json file. You can then include this into your primary ARM template by duplicating the "-infrastructure" (deployment) resource and changing the name to "-infrastructure-prod" and the reference to this new json file.

You can upload your SSL cert through the ARM template, by providing the binary like in the provided gist; alternatively you can upload the certificate to a key vault (which also creates a secret in the vault). If you choose to use the vault, you must give the ARM deployment service access to your key vault by running the following Powershell command (the service principal ID is an Azure Guid, so the same for everyone):
Set-AzureRmKeyVaultAccessPolicy -VaultName your-keyvault-name -ServicePrincipalName abfa0a7c-a6b6-4736-8310-5855508787cd -PermissionsToSecrets get

You can then substitute the certificate section in the gist with the following, which uses the key vault ID and secret name:
{
  "apiVersion": "[variables('certificateApiVersion')]",
  "location": "[parameters('location')]",
  "name": "[variables('sslCertificateNameTidy')]",
  "type": "Microsoft.Web/certificates",
  "properties": {
    "keyVaultId": "[parameters('keyVaultId')]",
    "keyVaultSecretName": "[parameters('sslKeyVaultCertificateName')]"
  } 
}

In your Powershell script, after the part which does the deployment, add the following to copy the app settings and files to your staging slots:

function copyAppSettings($rg, $webApp, $slot) {
 $props = (Invoke-AzureRmResourceAction -ResourceGroupName $rg `
    -ResourceType Microsoft.Web/sites/Config -Name $webApp/appsettings `
    -Action list -ApiVersion 2015-08-01 -Force).Properties

 $hash = @{}
 $props | Get-Member -MemberType NoteProperty | % { $hash[$_.Name] = $props.($_.Name) }

 Set-AzureRMWebAppSlot -ResourceGroupName $rg -Name $webApp -Slot $slot -AppSettings $hash
}

# Copy app settings to staging slot
Write-Host "Copying app settings to staging slots";
copyAppSettings $ResourceGroupName "$($DeploymentId)-cd" "cd-staging"
copyAppSettings $ResourceGroupName "$($DeploymentId)-cm" "cm-staging"
Write-Host "Done copying app settings";
  
# Copy files to staging slot
Write-Host "Copying files to staging CD";
..\sync_slots -SubscriptionId $SubscriptionId -ResourceGroupName $ResourceGroupName -WebAppName "$($DeploymentId)-cd" -SlotName "cd-staging"
Write-Host "Copying files to staging CM";
..\sync_slots -SubscriptionId $SubscriptionId -ResourceGroupName $ResourceGroupName -WebAppName "$($DeploymentId)-cm" -SlotName "cm-staging"
Write-Host "Done copying files to staging";

Wednesday, 22 November 2017

Copy Azure web app files to slot

The majority of the out-of-the-box Sitecore ARM template is great for anything from a development to testing environment, but in production you're very likely to be using slots to test in staging and have zero-downtime releases (if you're not using slots, I'd highly recommend it).  I'll be doing a later post on some updates we can make to the Sitecore ARM templates to actually add a staging slot (amongst other enhancements), but once you have your slot you still need the base Sitecore files in there when you kick off your deployment (using CI/CD of course).

Sitecore by default is only installed to the production slot (ie. the web app itself), and installing it again in the slot will mean either pointing the installation at a second DB (which you could do), restoring the dacpacs to the live DB a second time (which you don't want to do), or creating a custom Sitecore package without the dacpac files (painful when Sitecore upgrades or changes need to be made).

I was tempted to create some DB-less Sitecore packages, but I knew there would have to be a better way.  There looks to be some options if you upgrade to Premium, but for those of us in Standard I figured there should be a way to copy the files from slot to slot without downloading them locally and uploading them again, via FTP.  After a lot of hunting and a promising upcoming solution from Microsoft, I stumbled across this azure-clone-webapps repo in Github.  This was almost exactly what I was after (massive thanks to the author), I just needed to convert it to Powershell so that I could run it as part of my ARM template deployment script.

I've included my final Powershell script here and uploaded it as a Gist, feel free to use it as-is or tweak it to suit your needs.  Since our client's Sitecore host is Rackspace they've got NewRelic installed, and I've included a skip statement to ignore the newrelic folder inside the website.  Other than that it will copy all the site files from your given web app to the given slot.  Enjoy!

Gist of SyncFilesToSlot.ps1
<#
 .SYNOPSIS
    Copies all of a web app's files to a given slot

 .DESCRIPTION
    Copies all of a web app's files to a given slot. Skips "newrelic" folder as the files are in use.
    
 .PARAMETER SubscriptionId
    The subscription id where the resources reside.

 .PARAMETER ResourceGroupName
    The resource group where the resources reside.

 .PARAMETER WebAppName
    Name of the web app containing files for the slot.
    
 .PARAMETER SlotName
    Name of the slot to fill with files from web app.
#>

param(
    [string]
    $SubscriptionId,

    [Parameter(Mandatory = $True)]
    [string]
    $ResourceGroupName,

    [Parameter(Mandatory = $false)]
    [string]
    $WebAppName,

    [Parameter(Mandatory = $True)]
    [string]
    $SlotName
)

[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Web.Deployment")

function Get-AzureRmWebAppPublishingCredentials($ResourceGroupName, $WebAppName, $SlotName = $null){
  if ([string]::IsNullOrWhiteSpace($SlotName)) {
    $resourceType = "Microsoft.Web/sites/config";
    $resourceName = "$WebAppName/publishingcredentials";
  }
  else {
    $resourceType = "Microsoft.Web/sites/slots/config";
    $resourceName = "$WebAppName/$SlotName/publishingcredentials";
  }
  $publishingCredentials = Invoke-AzureRmResourceAction -ResourceGroupName $ResourceGroupName -ResourceType $resourceType -ResourceName $resourceName -Action list -ApiVersion 2016-08-01 -Force;
  return $publishingCredentials;
}

function GetScmUrl($ResourceGroupName, $WebAppName, $SlotName) {
    # revert to this when MS fixes https://social.msdn.microsoft.com/Forums/expression/en-US/938e59f6-6a83-4640-a423-26fe91d66cf3/scm-uri-for-web-app-deployment-slots
    #$scmUrl = $publishingCredentials.properties.scmUri
    #$scmUrlNoCreds = $scmUrl.Replace($scmUrl.Substring($scmUrl.IndexOf('$'), ($scmUrl.IndexOf('@')-$scmUrl.IndexOf('$')+1)), '') # ugh this version of substring sucks sooo much :'(
    #$apiUrl = "$scmUrl/api/command"
    # revert below
    if($SlotName) {
        $slot = Get-AzureRmWebAppSlot -ResourceGroupname $ResourceGroupName -Name $WebAppName -Slot $SlotName;
        $scmUrl = $slot.EnabledHostNames | where { $_.Contains('.scm.') };
    } else {
        $scmUrl = "$WebAppName.scm.azurewebsites.net";
    }
    # revert above
    return "https://$scmUrl";
}

function SyncWebApps($srcUrl, $srcCredentials, $destUrl, $destCredentials) {
    $syncOptions = New-Object Microsoft.Web.Deployment.DeploymentSyncOptions;
    #$syncOptions.DoNotDelete = $true;
    $appOfflineRule = $null;
    $availableRules = [Microsoft.Web.Deployment.DeploymentSyncOptions]::GetAvailableRules();
    if (!$availableRules.TryGetValue('AppOffline', [ref]$appOfflineRule)) {
        Write-Host "Failed to find AppOffline Rule";
    } else {
        $syncOptions.Rules.Add($appOfflineRule);
        Write-Host "Enabled AppOffline Rule";
    }
    
    $skipNewRelic = New-Object Microsoft.Web.Deployment.DeploymentSkipDirective -ArgumentList @("skipNewRelic", 'objectName=dirPath,absolutePath=.*\\newrelic', $true);

    $sourceBaseOptions = New-Object Microsoft.Web.Deployment.DeploymentBaseOptions;
    $sourceBaseOptions.ComputerName = $srcUrl + "/msdeploy.axd";
    $sourceBaseOptions.UserName = $srcCredentials.properties.PublishingUserName;
    $sourceBaseOptions.Password = $srcCredentials.properties.PublishingPassword;
    $sourceBaseOptions.AuthenticationType = "basic";
    $sourceBaseOptions.SkipDirectives.Add($skipNewRelic);

    $destBaseOptions = New-Object Microsoft.Web.Deployment.DeploymentBaseOptions;
    $destBaseOptions.ComputerName = $destUrl + "/msdeploy.axd";
    $destBaseOptions.UserName = $destCredentials.properties.PublishingUserName;
    $destBaseOptions.Password = $destCredentials.properties.PublishingPassword;
    $destBaseOptions.AuthenticationType = "basic";
    $destBaseOptions.SkipDirectives.Add($skipNewRelic);

    $destProviderOptions = New-Object Microsoft.Web.Deployment.DeploymentProviderOptions -ArgumentList @("contentPath");
    $destProviderOptions.Path = "/site";
    $sourceObj = [Microsoft.Web.Deployment.DeploymentManager]::CreateObject("contentPath", "/site", $sourceBaseOptions);
    $sourceObj.SyncTo($destProviderOptions, $destBaseOptions, $syncOptions); 
}

if($SubscriptionId) {
    try {
        Set-AzureRmContext -SubscriptionID $SubscriptionId;
    } catch {
     Login-AzureRmAccount;
     Set-AzureRmContext -SubscriptionID $SubscriptionId;
    }
}

$srcCreds = Get-AzureRmWebAppPublishingCredentials $ResourceGroupName $WebAppName;
$srcUrl = GetScmUrl $ResourceGroupName $WebAppName;
$destCreds = Get-AzureRmWebAppPublishingCredentials $ResourceGroupName $WebAppName $SlotName;
$destUrl = GetScmUrl $ResourceGroupName $WebAppName $SlotName;
SyncWebApps $srcUrl $srcCreds $destUrl $destCreds;

Thursday, 26 October 2017

Sitecore PaaS conditional deployments

For our current project our client has a production Sitecore environment and DR environment which are almost identical.  Production is always up and running, however DR is spun-up on demand, in a secondary region, when the production region goes down for any reason.  The only difference between the two environments is the data: the SQL servers are using Azure's active geo-replication and failover groups so that the secondary region's data is always up and ready to go, and to fails over automatically.  This is more costly, but enables us to meet the client's RPO and RTO (as opposed to the backup and restore method), and specify the grace period with data loss if the RTO isn't going to be met.
As a side-note, the secondary SQL servers, DBs, and failover groups are located in the primary resource group, not the DR resource group. This is so that the DR resource group can simply be deleted when the primary region comes back on-line.  Don't forget point 1 of resource groups: the resources inside should share the same lifecycle.

But moving on to the topic of the title: the DR environment is identical to prod minus the SQL servers and databases.  There's no point scripting up two ARM templates when things are this similar, and fortunately Azure has us covered with ARM template conditions.  This is the best use for them that I've found so far, but you could also use it for any other environments/deployments which are quite similar.

First, in the Powershell script (we're using a modified version of the Sitecore Azure toolkit's Sitecore.Cloud.Cmdlets.psm1 Start-SitecoreAzureDeployment function), we dynamically set the template property based on a flag:
if($IsDR) {
  $paramJson | Add-Member -NotePropertyName "Environment" -NotePropertyValue @{ "value" = "DR" } -Force
}

Then in the template we add our Environment property:
"Environment": {
    "type": "string",
    "allowedValues": [
        "Prod",
        "DR"
    ],
    "defaultValue": "Prod",
    "metadata": {
        "description": "Select whether this environment is prod (requires SQL) or DR (no SQL required)."
    }
}

Finally, in the resource itself we add the condition to only create SQL servers if it's not DR:
{
  "condition": "[not(equals(parameters('Environment'), 'DR'))]",
  "type": "Microsoft.Sql/servers",
  "name": "[variables('dbServerNameTidy')]",
  ... etc.
}

You can also use the conditions in your properties, which is required in the DR deployment to get the old FQDN of the original prod SQL servers.
"sqlServerFqdn": "[reference(if(equals(parameters('Environment'), 'DR'), resourceId(parameters('prodResourceGroup'), 'Microsoft.Sql/servers', variables('oldDbServerNameTidy')), resourceId('Microsoft.Sql/servers', variables('dbServerNameTidy'))), variables('dbApiVersion')).fullyQualifiedDomainName]",

Easy as that! Now you have one ARM template which can be used for both environments, just by passing the -IsDr parameter to your powershell deployment script.
You could easily modify the Environment parameter to contain more values for different environments if you have more which are similar.  However if they're more than a little different it's probably worth having a different nested template, or entire set of templates, for each environment.