In the first part of this blog I gave a high level overview of continuous integration and deployment.
We saw that being able to '1 click deploy' an entire application along with its infrastructure allows teams to respond rapidly to change and act upon feedback, shortening the loop between developers and end users and ultimately delivering a better product.
This time we are going to show how to set up automatic web application deployment using SAFE Stack, Farmer and Azure Pipelines.
In the interests of brevity, I will work on the assumption that you have already
- Set up an Azure DevOps account
- Created a project in Azure DevOps
- Created a repository (can be in Az DevOps / GitHub / Bitbucket etc)
- Created a new SAFE app in the repository using the dotnet template
Prepare the build script
Open up your SAFE app and navigate to the Build.fs
module in the Build
project.
Somewhere near the top, add the line
let artifactDir = Environment.environVarOrDefault "artifact-dir" "deploy"
This will allow us to pass in a custom bundle path from the pipeline.
Next, find the Azure
target function and change
let web = webApp {
name "SAFEPipelines" // Must be unique across all of Azure
zip_deploy "deploy"
}
to
let web = webApp {
name "SAFEPipelines"
zip_deploy artifactDir // Using the environment var we just loaded
}
Finally, at the bottom of the file, change
"Clean"
==> "InstallClient"
==> "Bundle"
==> "Azure"
to
"Clean"
==> "InstallClient"
==> "Bundle"
This prevents the project from being re-bundled when you run the Azure deploy later.
Install the .NET Test SDK and Expecto adapter
In the root of the solution run the commands
dotnet tool restore
dotnet paket add Microsoft.NET.Test.Sdk -p Server.Tests
dotnet paket add YoloDev.Expecto.TestSdk -p Server.Tests
Open up your tests/Server.Tests/Server.Tests.fsproj
file and add the element <GenerateProgramFile>false</GenerateProgramFile>
to the first PropertyGroup
. This prevents the Test SDK throwing an error related to project entry points.
Don't forget to push these changes up to your repository.
Connect your DevOps project to Azure
Open your DevOps project and select Project settings
from the bottom of the left flyout menu.
From the Project settings menu, select Service connections
and click New service connection
.
From the popout menu, select Azure Resource Manager
and then click next
to select Service principal (automatic)
.
On the following screen, choose your Azure sub from the dropdown and give the service connection a name.
You don't need to select a Resource Group, this is used if you want to restrict access for the connection.
Tick Grant access for all pipelines
and hit Save.
Create an Environment
Visit the Pipelines
- Environments
section of DevOps and click Create environment
.
Name it CD
, leave everything else at the defaults and click Create
.
Create a pipeline
Open your project in Azure DevOps and select the Pipelines
- Pipelines
section.
If this is the first pipeline you have create in your organisation, you may be required to request access from Microsoft which can take a short while to be granted. This check was introduced in 2021 to combat crypto mining bots.
You will be prompted to connect your repository, and then given a choice of pipeline template. For this project, select 'Starter Pipeline'.
You will be presented with a YAML (.yml) file which is set up to run a 'Hello World' script.
There are many, many ways to configure pipelines depending on your use case and personal preferences, this is just one example. Feel free to experiment!
Replace it with the following:
pool:
vmImage: 'windows-latest' # Use a Windows build host
variables:
isMain: $[eq(variables['Build.SourceBranch'], 'refs/heads/main')] # Set a variable to detect if we are on the main branch
appArtifact: "app" # Set a variable which is used to name the published app
stages:
- stage: CI # This stage just builds and tests the app. It runs on every push to every branch.
displayName: CI
jobs:
- job: Build
displayName: Build and publish artifact
steps:
- script: dotnet tool restore
displayName: Restore dotnet local tools
- script: dotnet paket restore
displayName: Download nuget packages
- script: dotnet restore MyApp.sln # Set your solution name here
displayName: Restore nuget packages
- script: npm install
displayName: Restore npm packages
- task: MSBuild@1
displayName: 'Build Server.Tests project'
inputs:
solution: 'tests/Server/*.fsproj'
msbuildArchitecture: 'x64'
clean: true
restoreNugetPackages: false
- task: VSTest@2 # Requires that the project has the .net test adapter installed (see earlier in the article)
displayName: Run Server tests
inputs:
testSelector: 'testAssemblies'
testAssemblyVer2: 'tests\Server\bin\Debug\net5.0\Server.Tests.dll'
- script: dotnet run Bundle # Create the app bundle
displayName: Bundle app
- publish: deploy # Publish the bundle as a pipeline artifact
artifact: $(appArtifact)
- stage: CD # This stage picks up the artifact from the CI stage and publishes it to Azure, along with the infrastructure
dependsOn: CI
condition: and(succeeded(), eq(variables.isMain, true)) # Only run if CI stage succeeded *and* we are on the main branch
displayName: CD
jobs:
- deployment: ContinuousDeploy
displayName: Deploy to dev test environment
environment: CD # The environment we created earlier
strategy:
runOnce:
deploy:
steps:
- download: current # Download the artifact from the previous stage.
artifact: $(appArtifact)
- checkout: self # This stage also needs access to the repo to run the deploy script
- task: AzureCLI@2 # The Azure CLI task allows publishing resources to Azure
inputs:
azureSubscription: 'My Subscription' # Replace with your sub name
scriptType: 'batch'
scriptLocation: 'inlineScript'
inlineScript: | # Run the deploy script, passing the pipeline artifact directory as our environment var
set artifact-dir=$(Pipeline.Workspace)/app
dotnet tool restore
dotnet run Azure
workingDirectory: '$(Build.SourcesDirectory)'
Save and run
Once you save and run the pipeline, a build / deploy cycle will be started.
The .yml
file will be saved in the root of your repository. It can be edited at any time in a text editor and pushed up using Git.
You can visit the Pipelines
section of DevOps, select your pipeline and then select the active run to monitor its progress.
The first time you get to the CD stage, you will be required to click a couple of buttons to allow access to the Azure subscription and the CD environment.
If your build or deploy fails, you can click around to find out which stage it failed at and why.
Setting up a pipeline like this, especially in more complex projects, can require a lot of patience as you slowly work through one issue at a time. However, once you get it all running you rarely need to touch it again, so persevere!
Conclusion
I hope this has been a useful whistle-stop tour of CI / CD with Azure Devops. The principles applied here should also generalise to most similar services.
As I mentioned earlier, this has been a very limited view at what is possible, and you can pretty much configure any process that you might imagine - for example, we often set up multiple environments for testing and production and publish to them consecutively.
If you aren't already embracing automation, perhaps you may now have a clearer understanding of the benefits and feel empowered to give it a shot. It is a great investment in terms of software quality, time saving and therefore ultimately developer and client happiness.
There is a full example solution on Github (although you will need to tweak it to use your own Azure sub etc of course).