Azure DevOps and Terraform for Spring Boot
This article will teach you how to automate your Spring Boot app deployment with Azure DevOps and Terraform. In the previous article in this series, we created a simple Spring Boot RESTful app. Then we integrated it with the popular Azure services like Cosmos DB or App Configuration using the Spring Cloud Azure project. We also leveraged the Azure Spring Apps service to deploy, run, and manage our app on the Azure cloud. All the required steps have been performed with the az
CLI and Azure Portal.
Today, we are going to design the CI/CD process for building and deploying the app created in the previous article on Azure. In order to configure required services like Azure Spring Apps or Cosmos DB automatically we will use Terraform. We will use Azure DevOps and Azure Pipelines to build and deploy the app.
Preparation
For the purpose of that exercise, we need to provision an account on Azure and another one on the Azure DevOps platform. Once you install the az
CLI and log in to Azure you can execute the following command for verification:
az account show
ShellSessionIn the next step, you should set up the account on the Azure DevOps platform. In order to do it, go to the following site and click the “Start free” button or “Sign in” if you already have an account there. After that, you should see the Azure DevOps main page. Before we start, we need to create the organization and project. I’m using the “pminkows” name in both cases.
Source Code
If you would like to try it by yourself, you may always take a look at my source code. In order to do that you need to clone my GitHub repository. The Spring Boot app used in the article is located in the microservices/account-service
directory. You will also find the Terraform manifest inside the microservice/terraform
directory and the azure-pipelines.yml
file in the repository root directory. After you go to that directory you should just follow my further instructions.
Create Azure Resources with Terraform
Terraform is a great tool for defining resources according to the “Infrastructure as a code” approach. An official Terraform provider is allowing to configure infrastructure on Azure with the Azure Resource Manager API’s. In order to use it, we need to include the azurerm
provider in the Terraform manifest. We will put all the required objects into the spring-group
resource group.
terraform {
required_version = ">= 1.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">=3.3.0"
}
}
}
provider "azurerm" {
features {}
}
resource "azurerm_resource_group" "spring-group" {
location = "eastus"
name = "spring-apps"
}
HCLConfigure the Azure Cosmos DB Service
In the first step, we are going to configure the Cosmos DB instance required by our sample Spring Boot app. It requires a new database account (1). The name of my account is sample-pminkows-cosmosdb
. It is placed inside our sample-spring-cloud
resource group. We also need to define a default consistency level (consistency_policy
) and a replication policy (geo_location
). Once we enable a database account we can create a database instance (2). The name of our database is sampledb
. Of course, it has to be placed in the previously created sample-pminkows-cosmosdb
Cosmos DB account. Finally, we need to create a container inside our database (3). The name of the container should be the same as the value of the containerName
field declared in the model class. We also have to set the partition key path. It corresponds to the name of the field inside the model class annotated with @PartitionKey
.
# (1)
resource "azurerm_cosmosdb_account" "sample-db-account" {
name = "sample-pminkows-cosmosdb"
location = azurerm_resource_group.spring-group.location
resource_group_name = azurerm_resource_group.spring-group.name
offer_type = "Standard"
consistency_policy {
consistency_level = "Session"
}
geo_location {
failover_priority = 0
location = "eastus"
}
}
# (2)
resource "azurerm_cosmosdb_sql_database" "sample-db" {
name = "sampledb"
resource_group_name = azurerm_cosmosdb_account.sample-db-account.resource_group_name
account_name = azurerm_cosmosdb_account.sample-db-account.name
}
# (3)
resource "azurerm_cosmosdb_sql_container" "sample-db-container" {
name = "accounts"
resource_group_name = azurerm_cosmosdb_account.sample-db-account.resource_group_name
account_name = azurerm_cosmosdb_account.sample-db-account.name
database_name = azurerm_cosmosdb_sql_database.sample-db.name
partition_key_paths = ["/customerId"]
partition_key_version = 1
throughput = 400
}
HCLJust for the record, here’s a model Java class inside our sample Spring Boot app:
@Container(containerName = "accounts")
public class Account {
@Id
@GeneratedValue
private String id;
private String number;
@PartitionKey
private String customerId;
// GETTERS AND SETTERS ...
}
JavaInstall the Azure App Configuration Service
In the next step, we need to enable the Azure App Configuration service and put some properties into the store (1). The name of that instance is sample-spring-cloud-config
. Our sample Spring Boot app uses the Spring Cloud Azure project to interact with the App Configuration service. In order to take advantage of that integration, we need to give proper names to all the configuration keys. They should be prefixed with the /application
. They should also contain the name of the property automatically recognized by Spring Cloud. In our case, these are the properties used for establishing a connection with the Cosmos DB instance. We need to define three Spring Cloud properties: spring.cloud.azure.cosmos.key
(2), spring.cloud.azure.cosmos.database
(3), and spring.cloud.azure.cosmos.endpoint
(4). We can retrieve the value of the Comso DB instance primary key or endpoint URL from the previously created azurerm_cosmosdb_account
resource.
# (1)
resource "azurerm_app_configuration" "sample-config" {
name = "sample-spring-cloud-config"
resource_group_name = azurerm_resource_group.spring-group.name
location = azurerm_resource_group.spring-group.location
}
# (2)
resource "azurerm_app_configuration_key" "cosmosdb-key" {
configuration_store_id = azurerm_app_configuration.sample-config.id
key = "/application/spring.cloud.azure.cosmos.key"
value = azurerm_cosmosdb_account.sample-db-account.primary_key
}
# (3)
resource "azurerm_app_configuration_key" "cosmosdb-key" {
configuration_store_id = azurerm_app_configuration.sample-config.id
key = "/application/spring.cloud.azure.cosmos.database"
value = "sampledb"
}
# (4)
resource "azurerm_app_configuration_key" "cosmosdb-key" {
configuration_store_id = azurerm_app_configuration.sample-config.id
key = "/application/spring.cloud.azure.cosmos.endpoint"
value = azurerm_cosmosdb_account.sample-db-account.endpoint
}
HCLTitle
The App Configuration service requires some additional permissions. First of all, you may need to install the provider with the ‘az provider register –namespace Microsoft.AppConfiguration’ command. Also, the Terraform script in the sample Git repository assigns the additional role ‘App Configuration Data Owner’ to the client.
Create the Azure Spring Apps Instance
In the last step, we need to configure the Azure Spring Apps service instance used for running Spring Boot apps. The name of our instance is sample-spring-cloud-apps
(1). We will enable tracing for the Spring Azure Apps instance with the Application Insights service (2). After that, we will create a single app inside sample-spring-cloud-apps
with the account-service
name (3). This app requires a basic configuration containing an amount of requested resources, a version of Java runtime, or some environment variables including the address of the Azure App Configuration service instance. All those things should be set inside the deployment object represented by the azurerm_spring_cloud_java_deployment
resource (4).
resource "azurerm_application_insights" "spring-insights" {
name = "spring-insights"
location = azurerm_resource_group.spring-group.location
resource_group_name = azurerm_resource_group.spring-group.name
application_type = "web"
}
# (1)
resource "azurerm_spring_cloud_service" "spring-cloud-apps" {
name = "sample-spring-cloud-apps"
location = azurerm_resource_group.spring-group.location
resource_group_name = azurerm_resource_group.spring-group.name
sku_name = "S0"
# (2)
trace {
connection_string = azurerm_application_insights.spring-insights.connection_string
sample_rate = 10.0
}
tags = {
Env = "Staging"
}
}
# (3)
resource "azurerm_spring_cloud_app" "account-service" {
name = "account-service"
resource_group_name = azurerm_resource_group.spring-group.name
service_name = azurerm_spring_cloud_service.spring-cloud-apps.name
identity {
type = "SystemAssigned"
}
}
# (4)
resource "azurerm_spring_cloud_java_deployment" "slot-staging" {
name = "dep1"
spring_cloud_app_id = azurerm_spring_cloud_app.account-service.id
instance_count = 1
jvm_options = "-XX:+PrintGC"
runtime_version = "Java_17"
quota {
cpu = "500m"
memory = "1Gi"
}
environment_variables = {
"Env" : "Staging",
"APP_CONFIGURATION_CONNECTION_STRING": azurerm_app_configuration.sample-config.primary_read_key[0].connection_string
}
}
resource "azurerm_spring_cloud_active_deployment" "dep-staging" {
spring_cloud_app_id = azurerm_spring_cloud_app.account-service.id
deployment_name = azurerm_spring_cloud_java_deployment.slot-staging.name
}
HCLApply the Terraform Manifest to Azure
Our Terraform configuration is ready. Finally, we can apply it to the target Azure account. Go to the microservices/terraform
directory and then run the following commands:
$ terraform init
$ terraform apply -auto-approve
ShellSessionIt can take several minutes until the command finishes. In the end, you should have a similar result. Terraform created 15 resources on Azure successfully.
We can switch to the Azure Portal for a moment. Let’s take a look at a list of resources inside our spring-apps
resource group. As you see, all the required resources including Cosmos DB, App Configuration, and Azure Spring Apps are ready.
Build And Deploy the App with Azure Pipelines
After preparing the required infrastructure on Azure, we may proceed to the creation of a CI/CD pipeline for the app. Assuming you have already logged in to the Azure DevOps portal, you should find the “Pipelines” item on the left-side menu. Once you expand it, you should see several options.
Create Environment
Let’s start with the “Environments”. We will prepare just a single staging
environment as shown below. We don’t need to choose any resources now (the “None” option).
Thanks to environments we can add approval checks for our pipelines. In order to do it, you should go to your environment details and switch to the “Approvals and checks” tab. There are several different available options. Let’s choose a simple approval, which requires someone to manually approve running the particular stage of the pipeline.
After clicking the “Next” button, you will be redirected to the next page containing a list of approvers. We can set a single person responsible for it or a whole group. For me, it doesn’t matter since I have only one user in the project. After defining a list of approvers click the “Create” button.
Define the Azure Pipeline
Now, let’s switch to the “Pipelines” view. We can create a pipeline manually with a GUI editor or just provide the azure-pipelines.yml
file in the repository root directory. Of course, a GUI editor is also creating and committing the YAML manifest with a pipeline definition to the Git repository.
Let’s analyze our pipeline step by step. It is triggered by the commit into the master
branch in the Git repository (1). We choose a standard agent pool (2). Our pipeline consists of two stages: Build_Test
and Deploy_Stage
(3). In the Build_Test
stage we are building the app with Maven (4) and publishing the JAR file to the Azure Artifacts Feeds (5). Thanks to that we will be able to use that artifact in the next stage.
The next Deploy_Stage stage (6) waits until the previous stage is finished successfully (7). However, it won’t continue until we do a review and approve the pipeline. In order to do that, the job must refer to the previously defined staging environment (8) that contains the approval check. Once we approve the pipeline it proceeds to the step responsible for downloading artifacts from the Azure Artifacts Feeds (9). After that, it starts a deployment process (10). We need to use the AzureSpringCloud
task responsible for deploying to the Azure Spring Apps service.
The deployment task requires several inputs. We need to set the Azure subscription ID (11), the ID of the Azure Spring Apps instance (12), the name of the app inside the Azure Spring Apps (13), and the name of a target deployment slot (14). Finally, we are setting the path to the JAR file downloaded in the previous step of the whole job (15). The pipeline reads the values of the Azure subscription ID and Azure Spring Apps instance ID from the input variables: subscription
and serviceName
.
# (1)
trigger:
- master
# (2)
pool:
vmImage: ubuntu-latest
# (3)
stages:
- stage: Build_Test
jobs:
- job: Maven_Package
steps:
- task: MavenAuthenticate@0
inputs:
artifactsFeeds: 'pminkows'
mavenServiceConnections: 'pminkows'
displayName: 'Maven Authenticate'
# (4)
- task: Maven@3
inputs:
mavenPomFile: 'microservices/account-service/pom.xml'
mavenOptions: '-Xmx3072m'
javaHomeOption: 'JDKVersion'
jdkVersionOption: '1.17'
jdkArchitectureOption: 'x64'
publishJUnitResults: true
testResultsFiles: '**/surefire-reports/TEST-*.xml'
goals: 'deploy'
mavenAuthenticateFeed: true # (5)
displayName: 'Build'
# (6)
- stage: Deploy_Stage
dependsOn: Build_Test
condition: succeeded() # (7)
jobs:
- deployment: Deployment_Staging
environment:
name: staging # (8)
strategy:
runOnce:
deploy:
steps:
# (9)
- task: DownloadPackage@1
inputs:
packageType: 'maven'
feed: 'pminkows'
view: 'Local'
definition: 'pl.piomin:account-service'
version: '1.0'
downloadPath: '$(System.ArtifactsDirectory)'
- script: 'ls -la $(System.ArtifactsDirectory)'
# (10)
- task: AzureSpringCloud@0
inputs:
azureSubscription: $(subscription) # (11)
Action: 'Deploy'
AzureSpringCloud: $(serviceName) # (12)
AppName: 'account-service' # (13)
DeploymentName: dep1 # (14)
Package: '$(System.ArtifactsDirectory)/account-service-1.0.jar' # (15)
YAMLRun the Azure Pipeline
Let’s import our pipeline into the Azure DevOps platform. Azure DevOps provides a simple wizard for that. We need to choose the Git repository containing the pipeline definition.
After selecting the repository we will see the review page. We can change the definition of our pipeline taken from the azure-pipelines.yml
file. If there is no need for any changes, we may add some variables or run (and save) the pipeline.
However, before running the pipeline we should define the required variables. The serviceName
variable needs to contain the fully qualified ID of the Azure Spring Apps resource, e.g. /subscriptions/d4cde383-3611-4557-b2b1-b64b50378c9d/resourceGroups/spring-apps/providers/Microsoft.AppPlatform/Spring/sample-spring-cloud-apps
.
We also need to create the Azure Artifact Feed. The pipeline uses it to cache and store artifacts during the Maven build. We should go to the “Artifacts” section. Then click the “Create Feed” button. The name of my feed is pminkows
.
Once we run the pipeline, it will publish the app artifact to the target feed. The Maven group ID and its name determine the artifact’s name. The current version number is 1.0
.
Let’s run the pipeline. It is starting from the build phase.
After finishing the build phase successfully, it proceeds to the deployment phase. However, the pipeline requires us to perform a review and approve the movement to the next step.
We need to click the Deploy_Stage
tile. After that, you should see a similar approval screen as shown below. You can approve or reject the changes.
After approval, the pipeline starts the deployment phase. After around one minute it should deploy our app into the target Azure Spring Apps instance. Here’s the successfully finished run of the pipeline.
We can switch to the Azure Portal once again. Go to the sample-spring-cloud-apps
Azure Spring Apps instance, then choose “Apps” and “account-service”. Finally, go to the “Deployments” section and choose the dep1
. It is the deployment slot used by our pipeline. As you see, our app is running in the staging environment.
Title
Before running the pipeline you should set the ‘dep1’ as the default staging deployment (option ‘Set as staging’)
Final Thoughts
This article shows the holistic approach to app deployment on Azure. We can use Terraform to define all the resources and services required by the app. After that, we can define the CI/CD pipeline with Azure DevOps. As a result, we have a fully automated way of managing all the aspects related to our Spring Boot app running in the Azure cloud.
Leave a Reply