Build a CI/CD Pipeline with Azure DevOps
Automate building and deploying a Node.js app with Azure DevOps pipelines.
Introduction
30 Second Summary
Imagine if every time a team wanted to update their website, someone had to manually copy files to a server. One typo could break everything. Professional teams avoid this by automating the entire process from code change to live deployment.
In this project, you will build an automated CI/CD pipeline using Azure DevOps that builds and deploys a Node.js web application every time you push code.
What You'll Build
An end-to-end CI/CD pipeline that takes a Node.js Express application from code commit to live deployment on Azure App Service.
By the end of this project, you'll have:
- A Node.js Express web application hosted in Azure Repos with Git version control.
- A multi-stage YAML pipeline with Build and Deploy stages that triggers automatically on every push to your repository.
- A live web app running on Azure App Service, deployed entirely through your pipeline with no manual steps.
- Secret Mission: Add a staging environment with a manual approval gate before production deployment.
Are there any prerequisites?
Basic familiarity with Git (clone, commit, push) is helpful. No prior Azure or DevOps experience is required.
Before We Start
Before We Start...
Set Up Your Accounts and Tools
Before you can build and deploy anything, you need the right accounts and tools in place. Azure DevOps is the platform that will run your CI/CD pipeline, and Azure is where your app will live once deployed.
In this step, you'll create the accounts that connect these services together, install the command-line tools you'll need locally, and set up your first Azure DevOps project.
In this step, get ready to:
- Create an Azure account and Azure DevOps organization.
- Install Git and Node.js on your machine.
- Create an Azure DevOps project for your pipeline.
Create an Azure account and Azure DevOps organization
You need two accounts to wire everything up: an Azure account (for hosting your app) and an Azure DevOps organization (for running your pipeline).
- Open your web browser and go to portal.azure.com.
- Click Start free to create a free Azure account.
- Complete the sign-up process with your email, phone verification, and identity confirmation.
- After sign-up completes, you'll land on the Azure Portal dashboard.
What is the free Azure account?
Azure's free account gives you $200 in credit for 30 days. You won't be charged unless you explicitly upgrade to a pay-as-you-go plan later.
Now create your Azure DevOps organization. This is the workspace where your pipeline will live.
- Open a new browser tab and go to dev.azure.com.
- Click Start free and sign in with the same Microsoft account you used for Azure.
- When prompted, select Create new organization.
- Accept the Terms of Service and select Continue.
- Enter a name for your organization (e.g., your name or a short identifier).
- Select a hosting region close to you.
- Select your Azure subscription for billing, then click Continue.
Why link an Azure subscription?
New Azure DevOps organizations require an active Azure subscription. This links billing to your free account but does not charge you. Azure DevOps itself is free for up to 5 users with 1,800 minutes per month of pipeline time.
After your organization is created, check that you have pipeline capacity.
- In your Azure DevOps organization, click Organization settings (gear icon at the bottom-left).
- Select Pipelines, then Parallel jobs.
- Check that Microsoft-hosted shows at least 1 parallel job available.
Seeing 0 parallel jobs?
New organizations may not automatically receive the free Microsoft-hosted parallel job grant. If you see 0 jobs, you need to request the grant.
Submit a request at https://aka.ms/azpipelines-parallelism-request. Processing takes several business days. You can continue the project setup while you wait, but you won't be able to run a pipeline until the grant is approved.
Install Git and Node.js
Git tracks your code changes and pushes them to Azure Repos. Node.js is the JavaScript runtime that lets you run your Express app locally.
Open a terminal to check if these tools are already installed.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
Check your Git installation by running this command:
git --version
✔️ I see a version number
Git is ready to go. Move on to checking Node.js below.
ⓧ Command not found
You need to install Git. Follow these steps:
- Go to https://git-scm.com in your browser.
- Download the installer for your operating system.
- Run the installer and accept the default settings.
- Close and reopen your terminal, then run git --version again to confirm.
Now check your Node.js installation by running:
node --version
✔️ I see version 20 or higher
Node.js is ready. You're all set to continue.
ⓧ I see a version lower than 20
Your Node.js version is too old for this project. The pipeline targets Node.js 20.x, so you need version 20 or higher locally too.
- Go to https://nodejs.org in your browser.
- Download the LTS installer (the button labelled "Recommended For Most Users").
- Run the installer and accept the default settings. This will upgrade your existing version.
- Close and reopen your terminal, then run node --version again to confirm you see v20 or higher.
ⓧ Command not found
Node.js is not installed. Follow these steps to install it:
- Go to https://nodejs.org in your browser.
- Download the LTS installer (the button labelled "Recommended For Most Users").
- Run the installer and accept the default settings.
- Close and reopen your terminal, then run node --version again to confirm you see v20 or higher.
Still having trouble installing?
Make sure you closed and reopened your terminal after installation. Installers update your system PATH, but existing terminal sessions won't pick up the change.
help me troubleshoot my Git or Node.js installation
Create an Azure DevOps project
A project in Azure DevOps is a container that holds your code repository, pipelines, and work items. You'll create one to house everything for this CI/CD pipeline.
- Go back to your Azure DevOps organization at dev.azure.com.
- Click New project in the top-right corner.
- Enter my-cicd-app as the project name.
- Under Version control, select Git.
- Click Create.
Azure DevOps creates the project and redirects you to the project dashboard. You should see my-cicd-app in the top-left breadcrumb.
What is an Azure DevOps project?
A project groups together a Git repository, CI/CD pipelines, boards, and artifacts under one roof. It's the workspace where your team (or just you) builds, tests, and deploys code.
Your accounts are ready and your tools are installed. Next up, you'll create a Node.js app and push it into your Azure DevOps repository.
Create and Push Your Node.js Application
A CI/CD pipeline needs source code to work with. Without an application in your repository, the pipeline has nothing to build or deploy.
In this step, you'll scaffold a Node.js web application, customize it with a deployment message, and push it to your Azure Repos repository. This gives your pipeline a real app to automate in the next steps.
In this step, get ready to:
- Scaffold and customize a Node.js Express web application.
- Test the app locally to confirm it works.
- Push the code to your Azure Repos repository.
Scaffold a Node.js Express app locally
Express is a lightweight web framework for Node.js that makes it easy to build web applications. The express-generator tool scaffolds a complete app structure for you in seconds.
- Move to your Desktop by running this command:
cd ~/Desktop
- Scaffold a new Express app by running this command:
npx express-generator my-cicd-app --view ejs
What does this command do?
npx express-generator downloads and runs the Express scaffolding tool without installing it globally.
my-cicd-app is the folder name for your new project. --view ejs tells the generator to use EJS as the template engine for rendering HTML pages.
You should see a list of files being created, starting with create : my-cicd-app/ followed by the app structure.
Seeing 'command not found' for npx?
This means Node.js is not installed correctly or not in your system path. Go back to Step 1 and verify your Node.js installation by running node --version.
Help me fix my Node.js installation so I can use npx.
- Move into your new app folder and install its dependencies by running these commands:
cd my-cicd-app
npm install
What does npm install do?
npm install reads the package.json file and downloads all the packages your app needs to run. These get saved in a node_modules folder.
You should see a progress bar followed by a summary of installed packages.
Now let's customize the app with your own content before testing it.
- Open the routes/index.js file in a text editor of your choice.
- Find this line:
res.render('index', { title: 'Express' });
- Replace 'Express' with 'My CI/CD App', so the line looks like this:
res.render('index', { title: 'My CI/CD App' });
- Save routes/index.js.
- Now open the views/index.ejs file.
- Find this line:
<p>Welcome to <%= title %></p>
- Add this new line directly below it:
<p>This app was deployed automatically using Azure DevOps CI/CD!</p>
Why add this message?
This custom text makes it easy to confirm your pipeline deployed the correct version of your app. When you see this message on your live site later, you'll know the deployment worked end to end.
- Save views/index.ejs.
- Test that your app runs locally by starting it:
npm start
- Open your browser and visit http://localhost:3000.
You should see a page with the heading My CI/CD App and the message about automatic deployment using Azure DevOps CI/CD.
- Go back to your terminal and press Ctrl+C to stop the server.
Not seeing your app at localhost:3000?
Make sure you ran npm start from inside the my-cicd-app folder. If you see a syntax error, double check that your edits to routes/index.js and views/index.ejs have matching quotes and brackets.
Help me debug why my Express app isn't loading at localhost:3000.
✔️ Awesome, I've got everything!
Great. Double check you've saved both files before moving on.
ⓧ I'd like to double check the full code
Here's what your routes/index.js should look like:
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'My CI/CD App' });
});
module.exports = router;
Here's what your views/index.ejs should look like:
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<h1><%= title %></h1>
<p>Welcome to <%= title %></p>
<p>This app was deployed automatically using Azure DevOps CI/CD!</p>
</body>
</html>
Initialize Git and push to Azure Repos
Your app works locally. Now you need to push it to Azure Repos so your pipeline can access the source code.
- In your browser, return to your Azure DevOps project at dev.azure.com.
- Click Repos in the left sidebar.
Since this is an empty repository, Azure DevOps shows setup instructions on the page. You'll see a section titled Push an existing repository from command line with a clone URL.
- Copy the HTTPS clone URL shown on the page.
Where is the clone URL?
The URL looks like https://dev.azure.com/your-org/my-cicd-app/_git/my-cicd-app. You can also find it by clicking the Clone button in the top right of the Repos page.
- Back in your terminal, make sure you're inside the my-cicd-app folder on your Desktop.
- Initialize a Git repository and push your code by running these commands:
git init
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin [[REPO_URL="your Azure Repos HTTPS clone URL"]]
git push -u origin main
What do these commands do?
- git init creates a new Git repository in your app folder.
- git add . stages all your files for the first commit.
- git commit -m "Initial commit" saves a snapshot of your code.
- git branch -M main ensures your branch is named main (matching what Azure Repos expects).
- git remote add origin connects your local repo to Azure Repos.
- git push -u origin main uploads your code to the remote repository.
Azure DevOps may prompt you to authenticate. If a browser window opens asking you to sign in, use the same Microsoft account you used to create your Azure DevOps organization.
- Refresh the Repos page in Azure DevOps.
You should see all your app files listed in the repository, including routes/index.js, views/index.ejs, and package.json.
Push failed or authentication error?
If you see a permission error, make sure you're signed in with the same account that owns the Azure DevOps project. If you see remote origin already exists, run git remote remove origin first, then try the git remote add command again.
Help me push my code to Azure Repos.
Your app is now in Azure Repos, ready for a pipeline to pick it up. Next up, you'll build the YAML pipeline that automatically builds this code every time you push a change.
Build the YAML Pipeline
Your Node.js app is pushed to Azure Repos and ready to be built automatically.
Right now, every time you make a change to your code, you'd need to manually package and deploy it. A CI pipeline solves this by automatically building your app every time you push code. In this step, you'll write a YAML pipeline that installs dependencies, archives your app, and produces a deployable artifact.
In this step, get ready to:
- Create a YAML pipeline file in Azure DevOps that triggers on pushes to the main branch.
- Run the pipeline and verify a build artifact is published successfully.
Create the pipeline file
Azure DevOps uses a file called azure-pipelines.yml stored in your repository to define what happens during a build. You'll create this file through the Azure DevOps web editor.
- In your Azure DevOps project, click Pipelines in the left sidebar.
- Click Create Pipeline.
- Select Azure Repos Git as your code source.
- Select your repository (it should match your project name).
- On the Configure your pipeline page, select Starter pipeline.
Azure DevOps opens a YAML editor with some starter content. You'll replace all of it with your own pipeline definition.
- Select all the starter content in the editor and delete it.
- Paste the following YAML pipeline code:
trigger:
- main
stages:
- stage: Build
displayName: 'Build stage'
jobs:
- job: Build
displayName: 'Build job'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
inputs:
versionSpec: '20.x'
displayName: 'Install Node.js'
- script: |
npm install
displayName: 'Install dependencies'
- task: ArchiveFiles@2
inputs:
rootFolderOrFile: '$(System.DefaultWorkingDirectory)'
includeRootFolder: false
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip'
replaceExistingArchive: true
displayName: 'Archive files'
- publish: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
artifact: drop
displayName: 'Publish artifact'
What does this pipeline do?
- trigger: - main tells Azure DevOps to run this pipeline every time code is pushed to the main branch.
- NodeTool@0 installs Node.js 20.x on the build agent so your app's dependencies can be installed.
- npm install downloads all the packages your Express app needs.
- ArchiveFiles@2 zips your entire project into a single file for deployment.
- The publish keyword makes the zip file available as an artifact named drop that later stages can use.
What is a build artifact?
A build artifact is the output of your pipeline's build process. Think of it as a ready-to-deploy package. Other stages (like a deployment stage) can pick up this artifact and deploy it without rebuilding the code.
Save and run the build
Now you'll commit the pipeline file to your repository and trigger the first build.
- Click Save and run in the top right of the YAML editor.
- A dialog appears asking for a commit message. Leave the default message or enter your own.
- Click Save and run again to confirm.
What just happened?
Azure DevOps committed the azure-pipelines.yml file to your repository's main branch. Because the pipeline triggers on main, this commit immediately starts a build run.
- Watch the pipeline run page. You'll see the Build stage appear with a progress indicator.
- Click on the Build job to see each step execute in real time.
- Wait for all steps to show green checkmarks, indicating success.
- Verify the artifact was published by clicking 1 artifact produced (or similar text) in the pipeline run summary. You should see an artifact named drop.
Pipeline stuck or not starting?
New Azure DevOps organizations may not have a free hosted parallel job grant yet. Check under Organization settings > Pipelines > Parallel jobs. If you see 0 parallel jobs, you need to request the free grant at https://aka.ms/azpipelines-parallelism-request. This can take 2-3 business days to process.
If the build fails on the npm install step, make sure your repository has a package.json file in the root. This file was created when you ran npx express-generator in the previous step.
Help me troubleshoot my Azure DevOps pipeline build failure. My pipeline is either not starting or failing on one of the steps.
✔️ Awesome, I've got everything!
Great. Double check you see the drop artifact in your pipeline run summary, then move on.
ⓧ I'd like to double check the full code
Here is the complete azure-pipelines.yml file. Make sure yours matches exactly:
trigger:
- main
stages:
- stage: Build
displayName: 'Build stage'
jobs:
- job: Build
displayName: 'Build job'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
inputs:
versionSpec: '20.x'
displayName: 'Install Node.js'
- script: |
npm install
displayName: 'Install dependencies'
- task: ArchiveFiles@2
inputs:
rootFolderOrFile: '$(System.DefaultWorkingDirectory)'
includeRootFolder: false
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip'
replaceExistingArchive: true
displayName: 'Archive files'
- publish: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
artifact: drop
displayName: 'Publish artifact'
Your CI pipeline is now building your app on every push to main. Next up, you'll create the Azure App Service where your app will actually run live on the internet.
Create the Azure App Service
Your pipeline can now build your app and package it into a deployable artifact. The next question is: where does that artifact actually get deployed to?
In this step, you'll create an Azure App Service instance in the Azure Portal. This is the managed hosting platform where your Express app will live once the pipeline deploys it.
In this step, get ready to:
- Create a resource group and App Service in the Azure Portal.
- Verify the default placeholder page loads at your App Service URL.
Create an App Service in Azure Portal
The App Service needs a resource group to live in. A resource group is a logical container that holds related Azure resources together, making them easy to manage and delete as a unit.
- Return to the Azure Portal at portal.azure.com
- In the search bar at the top of the page, type App Services and select **App Services** from the results under **Services**.
- Click Create, then select Web App from the dropdown menu.
You'll land on the Basics tab of the creation wizard. Fill in the following fields:
- Under Subscription, select your Azure subscription.
- Under Resource Group, click Create new, enter rg-cicd-app, then click OK.
- Under Name, enter a globally unique name for your app. This becomes part of your public URL, so use something like yourname-cicd-app.
- Under Publish, select Code.
- Under Runtime stack, select Node 20 LTS.
- Under Operating System, select Windows.
- Under Region, choose a region close to your location.
- Under Pricing plan, select Free F1 from the dropdown. If you don't see it, click Explore pricing plans and select the Free F1 tier.
- Click Review + create at the bottom of the page.
- Review the summary, then click Create.
Why does the name need to be globally unique?
Your app name becomes part of the public URL: https://your-app-name.azurewebsites.net. Since this URL is accessible to anyone on the internet, no two App Services can share the same name across all of Azure.
What is Azure App Service?
Azure App Service is a fully managed hosting platform for web applications. You upload your code and Azure handles the servers, scaling, patching, and networking.
The Free F1 tier gives you a shared environment for testing and learning. It costs nothing but has limited CPU and memory, which is perfect for this project.
Azure will take a minute or two to provision your resources. You'll see a deployment progress screen.
Seeing a name already taken error?
If Azure says the name is unavailable, try adding numbers or your initials to make it unique. Valid characters are a-z, 0-9, and hyphens (-).
If you don't see the Free F1 pricing plan, try selecting a different region. Some regions have limited free-tier availability.
Why can't I create my App Service?
Verify the App Service is running
Once Azure finishes provisioning, you can confirm your App Service is live and ready to receive deployments.
- Click Go to resource on the deployment complete screen.
- In the App Service overview page, click Browse in the top toolbar (or click the URL shown under Default domain).
- A new browser tab opens showing the Azure default placeholder page. This confirms your App Service is running and ready for deployment.
What should I see?
You should see a default Azure landing page with a message indicating your web app is running and waiting for your content. The URL in your browser's address bar will be https://your-app-name.azurewebsites.net (with the name you chose).
Seeing a 404 or error page?
Give it a minute. Azure sometimes takes 30-60 seconds to fully start the App Service after provisioning completes. Refresh the page after waiting.
If the page still doesn't load, return to the App Service overview in the Azure Portal and check that the Status shows Running.
Help me troubleshoot my App Service
Your deployment target is live and waiting for code. In the next step, you'll create a service connection so your pipeline has permission to deploy to this App Service automatically.
Connect Azure DevOps to Azure
Your pipeline can build your app and package it into an artifact. But it still has no way to reach your Azure App Service to deploy that artifact.
To fix this, you'll create a service connection. This is a secure link between Azure DevOps and your Azure subscription that grants your pipeline permission to deploy resources on your behalf.
In this step, get ready to:
- Create an Azure Resource Manager service connection in Azure DevOps.
- Verify the connection is active and ready to use.
Create an Azure Resource Manager service connection
A service connection uses a service principal to authenticate. Think of it as a dedicated identity that Azure DevOps uses to talk to Azure on your behalf, without exposing your personal credentials.
- In your Azure DevOps project, click Project Settings in the bottom-left corner of the sidebar.
- Under the Pipelines section in the left menu, click Service connections.
- Click New service connection.
- Select Azure Resource Manager from the list, then click Next.
- Select Service principal (automatic) and click Next.
What is Service principal (automatic)?
This option tells Azure DevOps to automatically create and configure a service principal in your Azure Active Directory. It handles all the credential setup for you.
The alternative (manual) requires you to pre-create the service principal yourself. The automatic option is faster and less error-prone for learning.
A form appears asking you to configure the connection details.
- In the Subscription dropdown, select your Azure subscription.
- In the Resource group dropdown, select rg-cicd-app.
- In the Service connection name field, enter azure-service-connection.
- Check the box for Grant access permission to all pipelines.
- Click Save.
Why grant access to all pipelines?
This setting allows any pipeline in your project to use this service connection without requiring manual approval each time. For a learning project with a single pipeline, this saves you an extra authorization step later.
Seeing a permissions error?
If Azure DevOps cannot create the service principal, make sure you are signed into the same Microsoft account for both Azure DevOps and the Azure Portal. The subscription dropdown will only show subscriptions linked to your current account.
help me fix a permissions error when creating a service connection in Azure DevOps
Verify the connection
Once saved, Azure DevOps validates the connection automatically. You should see confirmation that everything is working.
- Confirm that azure-service-connection appears in the Service connections list with a green verified status.
What does the green status mean?
A green status means Azure DevOps successfully authenticated with your Azure subscription using the service principal. Your pipeline now has a trusted path to deploy resources into your rg-cicd-app resource group.
Your pipeline now has a secure bridge to Azure. Next up, you'll add a Deploy stage to your YAML pipeline that uses this connection to push your app live to Azure App Service.
Add the Deploy Stage and Go Live
Your Build stage is compiling and packaging the app into an artifact. Your service connection gives Azure DevOps permission to touch your Azure resources.
Now it's time to close the loop. In this step, you'll add a Deploy stage to your pipeline that takes the build artifact and pushes it live to your Azure App Service. Then you'll prove that Continuous Delivery actually works by changing your code and watching the pipeline deploy the update automatically.
In this step, get ready to:
- Add a Deploy stage to your YAML pipeline.
- Run the full pipeline and see your app live on the internet.
- Verify continuous deployment by pushing a code change.
Edit the pipeline to add a Deploy stage
The Build stage creates the artifact. The Deploy stage picks it up and ships it to Azure App Service. You'll connect these two stages so that a successful build automatically triggers a deployment.
- In Azure DevOps, click Pipelines in the left sidebar.
- Click your pipeline name to open it.
- Click Edit in the top right corner to open the YAML editor.
You should see your existing Build stage YAML. You'll add the Deploy stage directly below it.
- Scroll to the very bottom of the file, after the last line of the Build stage.
- Add the following Deploy stage code at the end of the file:
- stage: Deploy
displayName: 'Deploy stage'
dependsOn: Build
condition: succeeded()
jobs:
- deployment: Deploy
displayName: 'Deploy job'
pool:
vmImage: 'ubuntu-latest'
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
inputs:
azureSubscription: 'azure-service-connection'
appType: 'webApp'
appName: '[[APP_NAME="your globally unique app name"]]'
package: '$(Pipeline.Workspace)/drop/$(Build.BuildId).zip'
displayName: 'Deploy to Azure App Service'
What does this code do?
This Deploy stage only runs after the Build stage succeeds. The dependsOn: Build and condition: succeeded() lines enforce this order.
The deployment job type with runOnce strategy is Azure Pipelines' way of handling deployments. It automatically tracks what was deployed and when.
The AzureWebApp@1 task uses your service connection (azure-service-connection) to authenticate with Azure, then pushes the zipped artifact to your App Service.
What is a deployment environment?
The environment: 'production' line tells Azure DevOps to create a logical environment called "production" that tracks deployment history. Azure DevOps creates this environment automatically the first time the pipeline runs through the web editor.
- Make sure the appName value matches the globally unique name you gave your App Service in the previous step.
✔️ Awesome, I've got everything!
Great. Double check you've correctly indented the YAML. Indentation matters in YAML files.
ⓧ I'd like to double check the full code
Here is the complete azure-pipelines.yml with both the Build and Deploy stages:
trigger:
- main
stages:
- stage: Build
displayName: 'Build stage'
jobs:
- job: Build
displayName: 'Build job'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
inputs:
versionSpec: '20.x'
displayName: 'Install Node.js'
- script: |
npm install
displayName: 'Install dependencies'
- task: ArchiveFiles@2
inputs:
rootFolderOrFile: '$(System.DefaultWorkingDirectory)'
includeRootFolder: false
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip'
replaceExistingArchive: true
displayName: 'Archive files'
- publish: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
artifact: drop
displayName: 'Publish artifact'
- stage: Deploy
displayName: 'Deploy stage'
dependsOn: Build
condition: succeeded()
jobs:
- deployment: Deploy
displayName: 'Deploy job'
pool:
vmImage: 'ubuntu-latest'
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
inputs:
azureSubscription: 'azure-service-connection'
appType: 'webApp'
appName: '[[APP_NAME="your globally unique app name"]]'
package: '$(Pipeline.Workspace)/drop/$(Build.BuildId).zip'
displayName: 'Deploy to Azure App Service'
Run the full pipeline
With both stages in place, saving the pipeline file triggers a new run. This time, the pipeline will build your app AND deploy it to Azure.
- Click Save in the top right of the pipeline editor.
- In the commit dialog, leave the default commit message and click Save.
- Azure DevOps automatically triggers a new pipeline run. Click Pipelines in the left sidebar to see it.
- Click the running pipeline to watch its progress.
You should see two stages in the pipeline visualization: Build stage and Deploy stage. Watch them execute in sequence.
What if the pipeline asks for permission?
If this is the first time your pipeline uses the production environment, Azure DevOps may prompt you to grant permission. Click Permit to allow the pipeline to access the environment and continue.
- Wait for both stages to show green checkmarks. This may take 2-3 minutes.
- Once the Deploy stage completes, open a new browser tab.
- Visit your App Service URL:
https://[[APP_NAME="your globally unique app name"]].azurewebsites.net
You should see your Express app with the heading "My CI/CD App" and the welcome message. Your app is live on the internet, deployed entirely through your pipeline.
Seeing the default Azure placeholder page instead of your app?
If the deployment completed successfully (green checkmarks) but you still see the default page, wait 30-60 seconds and refresh. Azure App Service sometimes takes a moment to restart with the new code.
If the Deploy stage failed, click on it to view the logs. Check that your appName in the YAML exactly matches your App Service name, and that azureSubscription matches your service connection name.
help me debug why my Deploy stage failed in Azure DevOps
Verify continuous deployment
The real power of CI/CD is that every code push automatically builds and deploys. Let's prove it works by making a visible change to your app.
- Return to your local terminal.
- Open routes/index.js in your code editor.
- Find this line:
res.render('index', { title: 'My CI/CD App' });
- Change the title string to something different, for example:
res.render('index', { title: 'My CI/CD App - Updated!' });
- Save the file.
- Commit and push your change by running these commands:
git add .
git commit -m "Update welcome message"
git push origin main
- Switch back to Azure DevOps and click Pipelines in the left sidebar.
You should see a new pipeline run triggered automatically by your push. Watch both stages complete, then refresh your App Service URL to see the updated title.
Why did the pipeline trigger automatically?
Your azure-pipelines.yml starts with trigger: - main. This tells Azure Pipelines to start a new run every time code is pushed to the main branch. That's the "continuous" in Continuous Delivery.
Pipeline not triggering automatically?
Make sure you pushed to the main branch. Run git branch to check which branch you're on. If you're on a different branch, the trigger won't fire because it only watches main.
help me figure out why my Azure DevOps pipeline isn't triggering on push
Secret mission
Add a Staging Environment with Manual Approval
Your pipeline deploys straight to production on every push. In real teams, code passes through a staging environment first, and a human approves before it reaches users. Can you add that safety net to your pipeline?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses Azure's Free F1 tier for App Service and Azure DevOps' free tier, so there are no ongoing costs while these plans remain active.
Resources you used:
- Azure Resource Group rg-cicd-app (contains both production and staging App Services, Free F1 tier).
- Azure DevOps project with pipeline, service connection, and environments.
- Local project files (my-cicd-app folder on your Desktop).
Keep everything running
No action needed. Choose this if you're still actively building or experimenting with your pipeline.
- Your App Services remain live on the Free F1 tier at no cost.
- Azure DevOps is free for up to 5 users with 1,800 minutes per month of pipeline time.
- Your pipeline continues to trigger automatically on every push to main.
Pause - I'll come back to this later
Stop pushing code to your repository. Your App Services stay available but the pipeline will not run, so no pipeline minutes are consumed.
- There is no cost impact on the Free F1 tier. Your apps remain accessible at their URLs.
- When you're ready to resume, push a new commit to main and the pipeline will trigger again.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow each of these steps to clean up completely.
Delete the Azure resource group
- Go to portal.azure.com.
- Search for Resource groups in the top search bar.
- Select rg-cicd-app from the list.
- Click Delete resource group.
- Type rg-cicd-app in the confirmation field and click Delete.
What does this remove?
Deleting the resource group removes everything inside it. This includes both your production App Service and your staging App Service in one action.
Delete the Azure DevOps project
- Go to dev.azure.com and sign in to your organization.
- Click Organization settings in the bottom left corner.
- Select Projects in the left sidebar.
- Find your project in the list, select it, and click Delete.
- Type the project name to confirm and click Delete.
Can I undo this?
Azure DevOps soft-deletes projects for 28 days before permanent removal. You can restore the project from Organization settings > Overview > Recently deleted projects during that window.
Remove local project files
Delete the project folder from your Desktop.
macOS
- Open your terminal and run:
rm -rf ~/Desktop/my-cicd-app
Windows
- Open Command Prompt or PowerShell and run:
rmdir /s /q %USERPROFILE%\Desktop\my-cicd-app
That's a Wrap!
That's a Wrap!
Nice work! You just built a real CI/CD pipeline from scratch using Azure DevOps.
Every time you push code to your repository, your pipeline automatically builds, packages, and deploys your Node.js app to a live URL. That is production-grade deployment automation.
You've learned how to:
- Build a multi-stage YAML pipeline in Azure DevOps with separate Build and Deploy stages that run automatically on every push to main.
- Create an Azure Resource Manager service connection to securely link Azure DevOps to your Azure subscription without storing credentials in code.
- Deploy a Node.js app to Azure App Service using the AzureWebApp@1 task and verify continuous deployment by pushing a code change and watching it go live.
- Added a staging environment with manual approval gates for controlled releases before production deployment.
Ready to quiz yourself?