Photo by Amélie Mourichon on Unsplash.
This story is part of the Applied Cloud Stories initiative by Microsoft.
Build and release pipelines - who does not love them? Just push your code and see how a magic dance begins. Various systems that are connected just by some configuration will start processing our code.
There are many ways to properly implement a pipeline that continuously performs builds, essential validations, and tests. We can set up a set of scripts. We can use tools such as Jenkins. We can go to a service provider. In this post I'll go for Azure DevOps to set up a pipeline for continuous deployments of frontend modules also known as microfrontends.
For the microfrontends we'll use Piral.
Using Azure DevOps we get a lot of features right away - and most of them actually for free. One of the cool features of Azure DevOps is called "Azure Artifacts". This provides a registry for most of the well-known package managers such as NuGet, Maven or NPM.
While Azure DevOps is actually cloud agnostic it certainly comes with the best integration of Microsoft Azure. This should not be of any surprise - everyone would do the same. So when I say it's as easy as it can get to deploy anything to Azure using Azure DevOps I fully mean it.
Using YAML we can write build pipelines that are powerful and easy to manage:
trigger: branches: include: - release - dev pool: vmImage: 'ubuntu-latest' steps: - task: Npm@1 displayName: 'npm install' inputs: verbose: false - script: | npm run build displayName: 'npm build for DSP' condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/release')) - task: Npm@1 displayName: 'npm publish (DSP)' condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/release')) inputs: command: publish publishRegistry: useFeed publishFeed: NPM-Feed
Likewise, secrets can be managed in an Azure Key Vault. These secrets can be then used via "libraries" in Azure DevOps pipelines.
The libraries view is shown below.
The secrets can be connected directly to Azure Key Vault. The latest value will be retrieved when a pipeline using this variable group is run.
The configuration file only requires a single addition. Insert this snippet before the
variables: - group: Pilet Config
For instance, it is possible to use these variables in another task that only kicks in if we pushed to the
(Furthermore, we may want to modify the version in the package.json when we publish from the
dev branch, so I've added a small shell script to accomplish that)
steps: - script: | sed -i -e "s/\(\"version\":\\s\+\"\([0-9]\+\.\?\)\+\)/\1-pre.$(Build.BuildNumber)/" package.json displayName: 'Preview version (CI)' condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/dev')) - task: Npm@1 displayName: 'npm publish (CI)' condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/dev')) env: PILET_PUBLISH_KEY: $(PILET_PUBLISH_KEY) PILET_PUBLISH_URL: $(PILET_PUBLISH_URL) inputs: command: custom verbose: false customCommand: 'run publish-pilet'
We will see later on that such a secret management makes sense to be used for deploying securely.
For now we only use the secret as environment variables to directly deploy from our Azure DevOps build pipeline.
Running the pipeline shows us if everything could be run successfully.
The features for our frontend application are packaged in little modules called pilets. A pilet is just an NPM package containing different resources that should be used in our larger frontend application, which follows the microfrontends pattern.
By using Piral we simplify the whole development approach. Essentially, this boils it down to
- decide on a basic set of components ("pattern library")
- provide an app shell with a layout and some shared dependencies
- scaffold, write, and deploy individual pilets for each feature
The whole flow looks like the following diagram.
After the application shell reaches a certain maturity we start the module factory. Beforehand, we may develop one or the other module to see if our application shell has already reached a point where it makes sense to start producing microfrontends.
There are many articles our there how the development of an app shell and its modules is done. A good starting point would be introduction to microfrontends with Piral.
To summarize the development approach quickly: We would start by using the scaffolding capability.
mkdir my-app-shell cd my-app-shell npm init piral-instance
After filling out the survey we can make the necessary adjustments (if any).
Once done we can push the code to some repository and trigger a CI/CD process involving:
This command will produce two artifacts:
dist/releasewe'll find the plain files (html, css, js, ...) to publish our frontend.
dist/developwe'll find a .tgz file that could be pushed to an NPM repository.
The latter allows using the created Piral instance as an emulator for running microfrontends based on this app shell locally.
As an example, if we pushed the package
my-app-shell to a private Azure DevOps NPM registry we could scaffold a new microfrontend just like:
mkdir my-first-pilet cd my-first-pilet npm init pilet
where we would use
my-app-shell as the name of the application shell. Importantly, when the address of the registry is asked we'll need to provide the custom address. You can find it in Azure DevOps in your artifacts when you hit "Connect to Feed" and "NPM".
At this point we could happily develop the pilet, place an
azure-pipelines.yml and go home.
But where does Piral meet Azure DevOps?
Deploying microfrontends in Azure using Piral and Azure DevOps boils down to using the Azure Artifacts. Our build pipeline can take care of releasing the packages into the NPM feed.
Working in distributed teams using Azure DevOps will potentially resolve in certain projects being visible or not. Azure DevOps also distinguishes between:
- the NPM feed of the organization (global feed)
- an NPM feed for a project (local feed, may be public, too)
The idea is to use the organization feed for releasing pilets. A release pipeline picks up changes to the NPM feed and - in case of a matching package - triggers a release.
The release could go directly against the Piral Feed Service.
Using releases within Azure Pipelines makes it possible to define multiple environments ("stages"). Each stage may be passed automatically, manually, or when certain conditions (e.g., approval gates) are met.
Let's see how it looks with four environments (functional acceptance, systems integration, quality assurance, and production).
The definition of a release is quite straight forward. We create an artifact trigger that is sensitive to the organizational NPM feed. This way we provide teams the capability of pushing their own frontend modules.
The image below shows the settings used for the primary artifact trigger. Only this triggers automatically when the artifact changes.
Likewise, we have tooling in place that automatically creates a release pipeline when a certain package naming condition was found.
We also bring in a shell script from an operational repository. This shell script uses the Piral CLI for publishing the package to an environment specific feed using the API key taken from the variable group.
The result is a beautiful pipeline. In our definition we go automatically from FAE to OAE, however, for the other two environments we need either to manually trigger (QSS) and / or to have the approval of the required gate keepers (PROD).
Green is good 🚀!
The whole approach so far is quite close to a deployment of (micro)services using docker containers.
- Initial phase (check ✅):
- Building code for Docker image vs
- Building code for NPM package
- Packaging (check ✅):
- Docker image creation (Dockerfile) vs
- NPM package creation (package.json)
- Providing (check ✅):
- Docker container registry vs
- NPM package registry
- Publish (check ✅):
- Trigger on container tag or version vs
- Trigger on package tag or version
- Run (check ✅):
- Start the container via the entrypoint vs
- Load the pilet via its root module
This way we can push features (i.e., modules) of our frontend the same way as with features (i.e., services) of our backend. Simple, fast, reliable.