Deploying WAR based Java applications with CI/CD (GitHub Actions, Azure DevOps) on App Service Linux
In this blog post we’ll cover some examples of how to deploy war based applications using Azure DevOps and GitHub Actions.
Overview
Source code for these GitHub Action workflows and Azure DevOps pipelines can be found here.
This section will cover CI/CD deployment for war-based applications - this is for Blessed Tomcat images, which will act as our Web Container for our war. With this image, you still have the option to choose your Java major version, as well as Apache Tomcat major and minor version - but the premise is that we’re deploying a war file into a Tomcat container, which Tomcat itself will run.
Below is a configuration reference from the portal:
This is not the same as running a Java SE “Blessed” Image which requires this to be an executable jar with an embedded Web Server.
This post will also include deployment differences for Maven and Gradle.
Local Development
NOTE: We’ll be using Tomcat 9 and below since Tomcat 10 and above requires additional changes to run with Spring Boot
For simplicity, we’ll be using Spring Boot (that can be packaged as a war) to create the application we’ll be deploying. If you have an application that uses JSP’s, that can be used as well since ultimately we’ll be producing a war to deploy either way.
Configuring for Maven
- Go to Spring Initializr and create the application with the following properties:
- Project: Maven
- Language: Java
- Spring Boot: 2.7.6
- Project Metadata: Fill this as fits your needs
- Packaging: War
- Java: 17
For Dependencies, go to Add Dependencies and choose Spring Web. Click Generate after this, which will download a zip which we’ll extract into a project workspace.
- After downloading the zip, extract it on your local machine and cd into the folder with the source code.
- In a terminal, run either of the following:
- If Maven is on $PATH, you can run
mvn spring-boot:run
relative to thepom.xml
. - If Maven is not on $PATH or not installed locally, run
./mvnw spring-boot:run
relative to thepom.xml
- If Maven is on $PATH, you can run
NOTE: This assumes you have Java 17 locally. Maven needs to point to a Java 17 installation as well. If you’re unsure to what Maven is using, use
mvn -v
.
After running the above command, you should see some output like the below in your terminal:
2022-12-15T18:55:00.699-05:00 INFO 22012 --- [ main] com.devops.azure.AzureApplication : No active profile set, falling back to 1 default profile: "default"
2022-12-15T18:55:03.249-05:00 INFO 22012 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2022-12-15T18:55:03.272-05:00 INFO 22012 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2022-12-15T18:55:03.272-05:00 INFO 22012 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.1]
2022-12-15T18:55:03.453-05:00 INFO 22012 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2022-12-15T18:55:03.455-05:00 INFO 22012 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 2575 ms
2022-12-15T18:55:04.090-05:00 INFO 22012 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2022-12-15T18:55:04.104-05:00 INFO 22012 --- [ main] com.devops.azure.AzureApplication : Started AzureApplication in 5.152 seconds (process running for 6.599)
Locally, we can use Maven to help run our Spring Boot application with an Embedded Tomcat server, even though we specified the packaging as a war - and even if you don’t have a local Tomcat instance running. The war packaging of this only matters for when we move to deploying this to Azure, otherwise you can run this locally as any other application with an embedded server.
- Browsing to localhost:8080 should show a Whitelabel Error Page, which is expected, since we have no Controllers serving our root path.
- Let’s add a Controller to show some type of content when hitting the root path. Under your project src, relative to your entrypoint
.java
file, create a controller. Let’s name is HomeController.java. The project structure should look like this:
| - src
| | - main
| | - java
| | - com
| | - some
| | - reversedns
| | - name
| | Name.java
| | HomeController.java
Add the following code to HomeController.java:
package com.some.package;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HomeController {
String message = "This is a war file from Azure DevOps pipelines!";
@GetMapping("/")
public String index() {
return message;
}
}
- Restart the application. Refresh the browser, we should now see the below:
- Push this code to a repository of your choosing to use later on for the DevOps section. To use with GitHub Actions, it’s recommended to push this to a GitHub repository.
Configuring for Gradle
- Follow the steps under the Local Development - Maven section above to create a Spring Boot application with Gradle. Choose either Gradle - Kotlin or Gradle - Groovy for the Project field on Spring Initializr, the rest of the properties can remain the same.
- Continue to follow all other Local Development section steps to create a Controller, as we did earlier.
- On your local machine, in your terminal, run
./gradlew bootRun
to start the Spring Boot application. You should see the same output above as discussed in th Maven section.
DevOps
Maven
Prerequisites:
- If not done so already, create an Azure DevOps Organization.
- Next, create a Azure DevOps Project to host our pipeline after this.
Creating the pipeline:
In your Azure DevOps project go to:
- Pipelines -> Pipelines -> Create Pipeline
- Select the repository that is hosting the code
- Select the Maven package Java project Web App to Linux on Azure template:
- Select your subscription in the right-hand navbar, when prompted.
- Select the Web App you’re deploying to. Then select Validate and Configure:
- This will generate a
.yaml
that looks closely to the below. In the.yaml
below, we add some changes to account for Java 17 with Maven - the below is now our updated .yaml:
trigger:
- main
variables:
# Azure Resource Manager connection created during pipeline creation
azureSubscription: '00000000-0000-0000-0000-000000000000'
# Web app name
webAppName: 'myapp'
# Environment name
environmentName: 'myapp'
# Agent VM image name
vmImageName: 'ubuntu-latest'
stages:
- stage: Build
displayName: Build stage
jobs:
- job: MavenPackageAndPublishArtifacts
displayName: Maven Package and Publish Artifacts
pool:
vmImage: $(vmImageName)
steps:
- task: Maven@4
displayName: 'Maven Package'
inputs:
mavenPomFile: 'pom.xml'
# We add jdkVersionOption to point to Java 17 for Maven
jdkVersionOption: 1.17
- task: CopyFiles@2
displayName: 'Copy Files to artifact staging directory'
inputs:
SourceFolder: '$(System.DefaultWorkingDirectory)'
Contents: '**/target/*.?(war|jar)'
TargetFolder: $(Build.ArtifactStagingDirectory)
- upload: $(Build.ArtifactStagingDirectory)
artifact: drop
- stage: Deploy
displayName: Deploy stage
dependsOn: Build
condition: succeeded()
jobs:
- deployment: DeployLinuxWebApp
displayName: Deploy Linux Web App
environment: $(environmentName)
pool:
vmImage: $(vmImageName)
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
displayName: 'Azure Web App Deploy: myapp'
inputs:
azureSubscription: $(azureSubscription)
appType: webAppLinux
appName: $(webAppName)
package: '$(Pipeline.Workspace)/drop/**/target/*.?(war|jar)'
# IMPORTANT: If you don't add this it will deploy to a context named after your WAR
# ex. yoursite.azurewebsites.net/azure-0.0.1-SNAPSHOT/
customDeployFolder: 'ROOT'
Note the Glob Pattern for the Contents
and package
properties in these tasks: **/target/*.?(war|jar)
. Ideally, this would work fine for both jar and war based deployments.
You can change this to **/target/yourwar.war
if needed.
However, since we’re using Java 17 we needed to update the Maven task to the below, JAVA_HOME
(at the time of writing this) points to /usr/lib/jvm/temurin-11-jdk-amd64
. We need to point this to a Java 17 JDK. If you don’t do this we’ll get an Fatal error compiling: error: invalid target release: 17
- task: Maven@4
displayName: 'Maven Package'
inputs:
mavenPomFile: 'pom.xml'
jdkVersionOption: 1.17
- Make sure to Authorize the pipeline for deployment. Click into the pipeline to view and permit this. This should be a one time operation.
- At this point after deployment, and ensuring your
.yaml
is updated, you should have a successful deployment.
Azure DevOps - Why am I getting a 404 after deployment?
If you’re following along with the above, you should be able to get a pipeline quickly spun up with a succesful deployment. However, if you are expecting your site content to show up on the site’s root path (“/”) but did not add the customDeployFolder property (seen above), you may see this 404 after deployment:
This is because of the deployment API that the built-in Azure DevOps tasks currently use, which is the War Deploy API. This is called via the /api/wardeploy
URI and can be seen through the DevOps deployment task if we enabled DevOps debug logging:
##[debug][POST]https://$yoursitename:***@yoursitename.scm.azurewebsites.net/api/wardeploy?isAsync=true&name=azure-0.0.1-SNAPSHOT&message=....
You’ll see that when this deployment API is used it creates a webapps
folder under wwwroot
containing our exploded war:
root@4969de8b8855:/# ls /home/site/wwwroot/webapps/
azure-0.0.1-SNAPSHOT
This is because, by default, the API’s used in this deployment task pass the name of our war to the name
parameter in the War Deploy URI being called. Therefore, if your war isn’t named ROOT
, it will always deploy to a different context named after your war file.
This is opposed to the OneDeploy API being used on deployment methods such as the Azure CLI or the Maven Plugin which, under the hood, rename our war (or jar) to app.war
and deploy directly to wwwroot
instead of wwwroot/webapps
. This in turn is mapped directly to the root context (“/”).
Another quick way to solve this, aside from using the customDeployFolder
property is to add the <finalName></finalName>
element to the <build></build>
section of your pom.xml
. Such as:
<finalName>ROOT</finalName>
This will package the war with the name defined here and is what will be passed to the name
parameter described above.
Azure DevOps - What are other ways I can target a root site context?
Aside from the ways mentioned above (customDeployFolder with the AzureWebApp@1
task or <finalName>
in your pom.xml
) you can try to implement the below deployment approach which uses the OneDeploy API.
Azure CLI with az webapp deploy
:
You can replace the Deployment Task in the above .yaml
with this script. Ensure if using authentication such as Service Principals, that it’s appropriately added and scoped. The below is a drop-in replacement for the AzureWebApp@1
task:
- task: AzureCLI@2
displayName: 'Azure Web App Deploy: myapp'
inputs:
azureSubscription: 'Mysub (00000000-0000-0000-0000-000000000000)'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: 'az webapp deploy --resource-group my-rg --name $(webAppName) --src-path "$(Pipeline.Workspace)/drop/target/YOURWAR.war" --type war --async true'
Another example using a command we can put in inlineScript
is with --target-path
, for instance:
az webapp deploy --resource-group my-rg --name $(webAppName) --src-path "$(Pipeline.Workspace)/drop/target/YOURWAR.war" --target-path webapps/test --type war --async true
NOTE:
--target-path
should be relative. An absolute may fail with an internal server error.
You can further confirm OneDeploy is being used from the JSON output after the CLI command completes:
...
"complete": true,
"deployer": "OneDeploy",
"end_time": "2022-12-19T23:26:21.8900147Z",
"id": "00000000-0000-00000-0000-000000000000",
"is_readonly": true,
"is_temp": false,
"last_success_end_time": "2022-12-19T23:26:21.8900147Z",
"log_url": "https://yoursite.scm.azurewebsites.net/api/deployments/latest/log",
"message": "OneDeploy",
"progress": "",
...
You’ll see this deploys directly to wwwroot
:
root@a7285bf25867:/# ls /home/site/wwwroot/
app.war hostingstart.html
NOTE: To avoid unintended issues, only either deploy your war directly to
wwwroot
or only use thewebapps
folder. Do not do both.
Renaming the war to ROOT.war in the pipeline:
Another simple way is to just simply use a script in the pipeline to rename the war to ROOT.war
. Such as below:
- script: |
mv azure-0.0.1-SNAPSHOT.war ROOT.war
displayName: 'Rename war to ROOT.war'
Gradle
In your Azure DevOps project go to:
- Pipelines -> Pipelines -> Create Pipeline
- Select the repository that is hosting the code
- Select Show more, do not choose the Gradle one that appears by default.
- For simplicity, we’ll choose the Maven package Java project Web App to Linux on Azure template, like we did in the above section, but instead change out the Maven task with a Gradle one.
Below is the .yaml
file we’ll use for our Gradle deployment. This is generated from the Maven template but with three notable changes below - that of explicitly setting our Java version to 17, switching our Maven task for our Gradle one and changing our CopyArtifacts task to reflect where Grade outputs our built war to.
NOTE: At the time of writing this, the
Gradle@3
task only has up to JDK 11 support in the Task Assistant. If needed, this can manually be configured to point to a different JDK installation with the Path option.
Below is the .yaml
with the recommended changes. Replace war_name with the name of your war file.
trigger:
- main
variables:
# Azure Resource Manager connection created during pipeline creation
azureSubscription: '00000000-0000-0000-0000-000000000000'
# Web app name
webAppName: 'yourapp'
# Environment name
environmentName: 'yourapp'
# Agent VM image name
vmImageName: 'ubuntu-latest'
stages:
- stage: Build
displayName: Build stage
jobs:
- job: MavenPackageAndPublishArtifacts
displayName: Maven Package and Publish Artifacts
pool:
vmImage: $(vmImageName)
steps:
# We add this to set Java 17 for our pipeline environment
- task: JavaToolInstaller@0
inputs:
versionSpec: '17'
jdkArchitectureOption: 'x64'
jdkSourceOption: 'PreInstalled'
# We add this Gradle task to build with Gradle
- task: Gradle@3
inputs:
gradleWrapperFile: 'gradlew'
tasks: 'build'
javaHomeOption: 'JDKVersion'
- task: CopyFiles@2
displayName: 'Copy Files to artifact staging directory'
inputs:
SourceFolder: '$(System.DefaultWorkingDirectory)'
Contents: '**/build/libs/your_war.war'
TargetFolder: $(Build.ArtifactStagingDirectory)
- upload: $(Build.ArtifactStagingDirectory)
artifact: drop
- stage: Deploy
displayName: Deploy stage
dependsOn: Build
condition: succeeded()
jobs:
- deployment: DeployLinuxWebApp
displayName: Deploy Linux Web App
environment: $(environmentName)
pool:
vmImage: $(vmImageName)
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
displayName: 'Azure Web App Deploy: yourapp'
inputs:
azureSubscription: $(azureSubscription)
appType: webAppLinux
appName: $(webAppName)
package: '$(Pipeline.Workspace)/drop/**/build/libs/your_war.war'
# IMPORTANT: If you don't add this it will deploy to a context named after your WAR
# ex. yoursite.azurewebsites.net/azure-0.0.1-SNAPSHOT/
customDeployFolder: 'ROOT'
Deploying this to a new Java App Service on Linux should show updated site content, for example:
To view other configuration that can be used with Gradle for the Gradle@3
task, view the documentation here.
Why am I getting a 404 after deployment?
The same applies to what was covered in the Maven section above.
What are other ways I can target a root site context?
The same applies to what was covered in the Maven section above.
Troubleshooting
Java tests failing during build
If tests are failing and are able to be excluded, a Maven Options property can be added to the task, like the below:
- task: Maven@4
displayName: 'Maven Package'
inputs:
mavenPomFile: 'pom.xml'
options: '-DskipTests=true'
For Gradle, we can pass the -x
flag for exclusions, like the below:
- task: Gradle@3
inputs:
gradleWrapperFile: 'gradlew'
tasks: 'build'
javaHomeOption: 'JDKVersion'
options: '-x test'
Since Gradle outputs the tasks its running, you can confirm that Task :test
is removed from this in the Azure DevOps task logs:
(Before adding -x test
)
...other tasks
> Task :testClasses
> Task :test
> Task :check
> Task :build
(After adding -x test
)
> Task :compileJava
> Task :processResources
> Task :classes
> Task :resolveMainClassName
> Task :bootJar
> Task :jar
> Task :assemble
> Task :check
> Task :build
Maven or Gradle is pointing to a different Java version
Maven and Gradle are configured to point to a specific Java version. What is the default set to JAVA_HOME
may not be what is the target release goal for Maven or source compatability for Gradle. This can be changed through the jdkVersionOption
property for both Maven and Gradle tasks.
You can alternatively use the JavaToolInstaller@0 task to point to a specific JDK version, this will be set to $PATH
, which Maven and Gradle will pick up. You can alternatively use the javaHomeOption
option to point JAVA_HOME
to the JDK that’s installed and discovered (i.e through the JavaToolInstaller) or to a specific path on the agent.
For example (using JavaToolInstaller):
- task: JavaToolInstaller@0
inputs:
versionSpec: '17'
jdkArchitectureOption: 'x64'
jdkSourceOption: 'PreInstalled'
If there is a version mismatch, you’ll see Gradle exit with a message like this in the task logs:
...
Incompatible because this component declares an API of a component compatible with Java 17 and the consumer needed a runtime of a component compatible with Java 11
...
Maven will look like this if targeting a mismatched version from what it’s pointing towards:
Fatal error compiling: error: invalid target release: <versionNumber>
Application Error : ( is shown at runtime
This is a very generic message, but this means your application/container is either timing out or crashing. A crucial step is to ensure you have App Service Logs enabled. Review here on how to do so.
You can now view your Application Logging (stdout/err) in any of these following areas, or more:
- Diagnose and Solve Problems -> Application Logs detector
- Diagnose and Solve Problems -> Container Crash or Container Issues detector
- Log Stream
- Browsing the Kudu site directly or through FTP
- Using the AZ CLI, and others
You can view additional Tomcat specific logging in troubleshooting scenarios, which gets generated in the form of the below files - these will be found on the Kudu site under /home/LogFiles/Application
and can be accessed through FTP or browsing the Kudu site directly:
- catalina.
.yyyy-mm-dd.log - host-manager.
.yyyy-mm-dd.log - manager.
.yyyy-mm-dd.log - localhost.
.yyyy-mm-dd.log
You can also review Application and Docker specific stdout/err logging under /home/LogFiles
with default_docker.log
and docker.log
files.
Two general reasons this would happen after a new deployment is:
- Container Crash - Due to an unhandled exception/fatal error on start up. Examples such as: making a dependency call but this application wasn’t whitelisted to be allowed to access the resource, missing files (doing some type of i/o operation on a non-existent file from an application level), missing environment variables, bad syntax, forgetting to change your Spring Profile (or other localhost based values) to that of remote resources, etc.
- Container Timeout - Due to the application/container starting, but timing out at 240 (s) (the default). Common scenarios are trying to bind to
localhost
in some fashion or application logic never returning an HTTP response. The Tomcat container listens on port 80, with Tomcat itself listening over 80 which is set by the default value of-Dport.http
.
Default “parking page” is showing
This typically means your war is not under wwwroot (or wwwroot/webapps - depending on the deployment method). If not using the approach above, ensure the zip or single war being passed between stages and deployed actually contains the desired content. You can view the Artifact being passed between stages, if using this method, and review specifically what was produced in the Azure DevOps UI (this can also be downloaded):
It is advised to review the file content under /home/site/wwwroot
(or wwwroot/webapps
) when these scenarios occur (such as with FTP), to check that you’re not accidentally deploying a nested zip. If this is the case we fallback to showing the default hosting page
Pipeline is failing on the build or deploy stage
This can fail for various reasons, and is usually a product of the tasks being misconfigured here - troubleshooting this needs to be on the Azure DevOps or GitHub Actions side, especially if this is failing only in the Build stage. This alone would mean this is not an App Service issue, but rather a pipeline issue.
You can review Azure DevOps task logs by clicking into each specific task in your pipeline UI - and can additionally enable debug logs.
The same be done from the GitHub Actions Actions tab in GitHub.
One common scenario is Error: No package found with specified pattern: /some/path/zip.zip
. This may is normally related to the CopyFiles task or when trying to download said artifact into the Deploy stage. Review the CopyFiles task to ensure the file(s) or folders it’s transferring to the next stage actually actually matches whats specified under the Contents
property. Additionally, ensure your SourceFolder
property is not set to a path that does not contain the Contents
you’re trying to transfer. You may need to investigate the SourceFolder
, TargetFolder
and Contents
property locations all together.
Another is on the Deployment task - such as "Error: No such file or directory or directory, stat /path/to/file/or/folder"
when the Zip Deploy is initiated. This can happen due to the above, if deploying an incorrect file (non war, or non-zip), or the application source is missing a file from the build stage.
This is a direct product of how the pipeline is configured.
DevOps - Tomcat Configuration for runtime
One popular configuration for Tomcat applications on App Service Linux is using CATALINA_OPTS to pass various arguments to the application and JVM at runtime. We can do all of this from our pipeline, in our our Deployment task, without having to manage it from the Azure Portal. Such as below:
- task: AzureWebApp@1
displayName: 'Azure Web App Deploy: myapp'
inputs:
azureSubscription: $(azureSubscription)
appType: webAppLinux
appName: $(webAppName)
package: '$(Pipeline.Workspace)/drop/**/build/libs/somewar.war'
appSettings: -CATALINA_OPTS "-Dfoo=bar"
Use the -CATALINA_OPTS "-Dsome=value -Dfoo=bar"
syntax
NOTE: Tomcat applications use CATALINA_OPTS, where as Java SE (Embedded Tomcat, .jar applications) use JAVA_OPTS - see here
GitHub Actions
GitHub Actions (
azure/webapps-deploy@v3
) does not use WarDeploy. This uses OneDeploy. Onlyazure/webapps-deploy@v2
uses War Deploy
Maven
To get started with GitHub Actions, create a Java on App Service Linux application. In this case, we’ll still use a Java 17 runtime with Tomcat 9. After creating the application, do the following:
- Go to Deployment Center in the Azure Portal on the App Service and select GitHub as the source
- Select Organization, Repository and Branch - then click Save
This will now generate the following .yml
which will be commited and created under .github/<branch>_<appname>.yml
- Choose whether “User Assigned Identity” or “Publish Profile” authentication will be used. Publish Profile authentication requires “Basic Authentication” to be enabled on the App Service.
- For User Assigned Identity authentication this will generate the following
.yml
which will be committed and created under.github/<branch>_<appname>.yml
name: Build and deploy WAR app to Azure Web App - myapp
on:
push:
branches:
- main
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Java version
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'microsoft'
- name: Build with Maven
run: mvn clean install
- name: Upload artifact for deployment job
uses: actions/upload-artifact@v4
with:
name: java-app
path: '$/target/*.war'
deploy:
runs-on: ubuntu-latest
needs: build
environment:
name: 'Production'
url: $
permissions:
id-token: write #This is required for requesting the JWT
steps:
- name: Download artifact from build job
uses: actions/download-artifact@v4
with:
name: java-app
- name: Login to Azure
uses: azure/login@v2
with:
client-id: $
tenant-id: $
subscription-id: $
- name: Deploy to Azure Web App
id: deploy-to-webapp
uses: azure/webapps-deploy@v3
with:
app-name: 'someapp'
slot-name: 'Production'
package: '*.war'
- For publish profile authentication this will generate the following
.yml
which will be committed and created under.github/<branch>_<appname>.yml
# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
# More GitHub Actions for Azure: https://github.com/Azure/actions
name: Build and deploy JAR app to Azure Web App - someapp
on:
push:
branches:
- main
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Java version
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'microsoft'
- name: Build with Maven
run: mvn clean install
- name: Upload artifact for deployment job
uses: actions/upload-artifact@v4
with:
name: java-app
path: '$/target/*.jar'
deploy:
runs-on: ubuntu-latest
needs: build
environment:
name: 'Production'
url: $
steps:
- name: Download artifact from build job
uses: actions/download-artifact@v4
with:
name: java-app
- name: Deploy to Azure Web App
id: deploy-to-webapp
uses: azure/webapps-deploy@v3
with:
app-name: 'someapp'
slot-name: 'Production'
package: '*.war'
publish-profile: $
Since the Maven executable being used here is a typical CLI approach, you pass additional parameters as needed. For example:
mvn clean install -DskipTests=true && mvn -v
Using this generated Actions Workflow should be all we need to get a successful deployment started. However, if your war is not named ROOT
and you’re seeing HTTP 404’s after deployment, review the section below.
GitHub Actions - Why am I getting a 404 after deployment?
Update: azure/webapps-deploy@v3
now uses OneDeploy, which will always, by default, deploy to a ROOT
context with Tomcat - therefor doing manual configuration to deploy to a ROOT
context does not need to be done. azure/webapps-deploy@v2
however uses the older “War Deploy” API - which does not inheritly deploy to a ROOT
context. If using azure/webapps-deploy@v2
, the below will still apply.
The same applies to what was covered in the Maven section above.
GitHub Actions uses the War Deploy API under the hood. If we turn on debug logging, we can see what exactly is being called on the deployment task:
...
Package deployment using WAR Deploy initiated.
##[debug][POST] https://mysite.scm.azurewebsites.net:443/api/wardeploy?isAsync=true&name=azure-0.0.1-SNAPSHOT&message=...
...
NOTE: If you want to avoid this behavior and use the recommended OneDeploy API, consider using
azure/webapps-deploy@v3
Just as discussed in the Azure DevOps section, the name of our war is passed as a value to the name
parameter, which deploys this to a Tomcat Context of the same name. Therefor, if your war is NOT named ROOT
, it will be accessed under the name of your war (ex., https://sitename.azurewebsites.net/azure-0.0.1-SNAPSHOT)
GitHub Actions - What are other ways I can target a root site context?
NOTE: If using
azure/webapps-deploy@v3
, this will by default deploy to a root context. Consider using this instead. Otherwise, follow the below.
As opposed to the Azure DevOps deployment task we used, the azure/webapps-deploy@v2
deployment task does not have a property for setting the value of the name
parameter in the War Deploy API.
However, we can still deploy to root, below is an example using the AZ CLI in the deployment task:
deploy:
runs-on: ubuntu-latest
needs: build
environment:
name: 'Production'
url: $
steps:
- name: Download artifact from build job
uses: actions/download-artifact@v4
with:
name: java-app
- name: Azure Login
uses: azure/login@v2
with:
creds: $
- name: Deploy to Web App with Azure CLI
uses: azure/CLI@v2
with:
azcliversion: latest
inlineScript: |
az webapp deploy --resource-group "myrg" --name "myapp" --src-path azure-0.0.1-SNAPSHOT.war --type war --async true
In this example, we use the help of the azure/login@v1 and azure/cli@v1 task. As discussed in the Maven - Azure DevOps section, the Azure CLI uses the OneDeploy deployment API, which will automatically rename our war to app.war
and place this directly under wwwroot - which maps to the root context with Tomcat.
As seen in the above Azure DevOps section, you can alternatively target a non-root path with the --target-path
flag passed to az webapp deploy
. The path should be relative (eg., webapps/test
)
If you don’t want to alter the workflow file - or can’t, you can add <finalName>ROOT</finalName>
to the <build></build>
section of your pom.xml
. This will name your war to ROOT.war
and always be deployed in a root context.
You can review Mavens reference on this here.
Renaming the war to ROOT.war in the pipeline:
Another simple way is to just simply use a script in the pipeline to rename the war to ROOT.war
. Below is the syntax for GitHub Actions:
- name: Rename war to ROOT
run: mv azure-0.0.1-SNAPSHOT.war ROOT.war
Gradle
We’ll set up GitHub Actions with our Spring Boot project and Gradle using the same approach as seen under the GitHub Actions - Maven section.
NOTE: When setting this up from the Azure Portal as a first time project, it will default to using Maven, so your initial build may fail. We’ll need to change the generated .yml
file to the below:
name: Build and deploy WAR app to Azure Web App - myapp
on:
push:
branches:
- main
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Java version
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'microsoft'
# This is the important change we need to make to switch between Maven to Gradle
# Gradle is available on these runners through typical [CLI commands](https://docs.gradle.org/current/userguide/command_line_interface.html)
- name: Build with Gradle
run: gradle build
- name: Upload artifact for deployment job
uses: actions/upload-artifact@v4
with:
name: java-app
path: '$/build/libs/azure-0.0.1-SNAPSHOT.war'
deploy:
runs-on: ubuntu-latest
needs: build
environment:
name: 'Production'
url: $
permissions:
id-token: write #This is required for requesting the JWT
steps:
- name: Download artifact from build job
uses: actions/download-artifact@v4
with:
name: java-app
- name: Login to Azure
uses: azure/login@v2
with:
client-id: $
tenant-id: $
subscription-id: $
- name: Deploy to Azure Web App
id: deploy-to-webapp
uses: azure/webapps-deploy@v3
with:
app-name: 'someapp'
slot-name: 'Production'
package: '*.war'
GitHub Actions - Tomcat Configuration for runtime
As with Azure DevOps deployment tasks, you can use the azure/appservice-settings@v1
and azure/login@v1
tasks to configure runtime settings for your Tomcat application. The approach between the two differs - to set up proper credentials and authentication to add these App Settings, follow the documentation for appservice-settings
here.
Below is an example of using CATALINA_OPTS
in our deploy
stage:
deploy:
runs-on: ubuntu-latest
needs: build
environment:
name: 'Production'
url: $
steps:
- name: Download artifact from build job
uses: actions/download-artifact@v2
with:
name: java-app
- uses: azure/login@v2
with:
creds: '$'
- uses: azure/appservice-settings@v1
with:
app-name: 'myapp'
app-settings-json: '[{ "name": "CATALINA_OPTS", "value": "-Dfoo=bar" }]'
- name: Deploy to Azure Web App
id: deploy-to-webapp
uses: azure/webapps-deploy@v3
with:
app-name: 'someapp'
slot-name: 'Production'
package: '*.war'
- run: |
az logout
Troubleshooting
Error: More than one package matched with specified pattern: *.war. Please restrain the search pattern.
This may happen in the default generated template where a glob pattern is specified for any file matching a .war
extension. As with Maven and Gradle both, they package two wars on the build output, which means, unless otherwise configured - per build you will be outputting two (2) .war files to the /build (Gradle) or /target (Maven) directory.
This same issue can happen in any of the pipelines using a glob pattern for a wild-card or general search pattern.
To resolve this, either tighten down the glob pattern being used, or, if using a specific or easily identified naming scheme for your war file, replace the path
and package
properties in the above .yml
with the name of the war specifically - example: azure-0.0.1-SNAPSHOT.war
. This same approach can be used in any of the CI/CD examples in this post.
Other troubleshooting
Most other troubleshooting can follow whats listed under the DevOps troubleshooting section, respective of task syntax differences.