Back to: Microservices using ASP.NET Core Web API Tutorials
Managing Secrets and Configuration in CI/CD
In the previous chapter, we built a CI pipeline that automatically builds, tests, and publishes our application. Now comes a very important real-world topic: security and configuration management. In real projects, we never store sensitive information like database passwords, API keys, or deployment credentials directly in code or YAML files. Instead, we use secure mechanisms like GitHub Secrets and environment-based configuration.
What We Will Do in This Chapter?
By the end of this chapter, we will do the following practical tasks:
- Add clean configuration sections to our ASP.NET Core Web API project.
- Keep non-sensitive settings in the appsettings.json file.
- Keep environment-specific values in appsettings.Development.json and appsettings.Production.json files.
- Create GitHub Variables for non-sensitive CI/CD values.
- Create GitHub Secrets for sensitive values.
- Create GitHub Environments such as Development and Production.
- Create GitHub Environments specific variables and secrets.
- Update the GitHub Actions workflow to read variables and secrets safely.
This gives us a clean foundation for deployment automation in the next chapter.
Step 1: First, Understand What Goes Where
Normal settings go in configuration files. Sensitive values go in secrets. Pipeline-only non-sensitive values go in variables. The simple rule:
- appsettings.json = Normal application configuration.
- appsettings.Development.json = Development-only values.
- appsettings.Production.json = Production-only values.
- GitHub Variables = Non-sensitive pipeline values.
- GitHub Secrets = Sensitive pipeline values such as passwords, tokens, API keys, and connection strings.
- GitHub Environments = Environment-specific secrets and variables.
Step 2: Add Clean Application Configuration to appsettings.json
Now, let us improve our project configuration. Open appsettings.json and update it like this:
{
"ApplicationSettings": {
"ApplicationName": "Student Management API",
"SupportEmail": "support@demoapp.com"
},
"ExternalServices": {
"StudentPortalBaseUrl": "https://localhost:7001"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
This file should contain only safe, normal configuration values. It is fine to store items such as the application name, base URL, or support email here, as long as the values are not sensitive.
Step 3: Add Development-Specific Configuration
Now open appsettings.Development.json and add values that differ when running locally.
{
"ApplicationSettings": {
"ApplicationName": "Student Management API - Development",
"SupportEmail": "devsupport@demoapp.com"
},
"ExternalServices": {
"StudentPortalBaseUrl": "https://dev-studentportal.local"
}
}
This file is used when the app runs in the Development environment. ASP.NET Core supports environment-specific configuration files, such as appsettings.Development.json, which override corresponding values in the main appsettings.json.
So now:
- appsettings.json gives the default value
- appsettings.Development.json overrides that value in Development
Step 4: Add Production-Specific Configuration
Now, create or update appsettings.Production.json like this:
{
"ApplicationSettings": {
"ApplicationName": "Student Management API - Production",
"SupportEmail": "support@studentmanagement.com"
},
"ExternalServices": {
"StudentPortalBaseUrl": "https://studentmanagement.com"
}
}
When the app runs in the production environment, these values can override the defaults. Development and Production should not always use the same values. That is why environment-specific files exist.
Step 5: Read Configuration Inside the API
Now, let us see that the configuration is actually working. When we run the app locally in Development, the endpoint will show Development values. Later, when the app runs in Production, the endpoint can show Production values instead. Open HealthController.cs and update it like this:
using Microsoft.AspNetCore.Mvc;
namespace StudentManagement.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class HealthController : ControllerBase
{
private readonly IConfiguration _configuration;
public HealthController(IConfiguration configuration)
{
_configuration = configuration;
}
[HttpGet]
public IActionResult Get()
{
return Ok(new
{
Status = "Healthy",
ApplicationName = _configuration["ApplicationSettings:ApplicationName"],
SupportEmail = _configuration["ApplicationSettings:SupportEmail"],
StudentPortalBaseUrl = _configuration["ExternalServices:StudentPortalBaseUrl"]
});
}
}
}
ASP.NET Core exposes configuration through IConfiguration, and configuration keys can be read using hierarchical paths like Section:ChildKey.
Step 6: Run the Project and Test the Configuration
Run the project from Visual Studio and call: https://localhost:xxxx/api/health
If the app is running in Development, you should see the Development values from appsettings.Development.json.
Step 7: Create a GitHub Repository Secret
Now, let us do a real GitHub step. Open your GitHub repository and go to:
- Settings
- Secrets and variables
- Actions
- New repository secret
Create a sample secret like this:
- Name: DEMO_API_KEY
- Value: Any Sample Secret Value
- Then click on the Add Secret button
GitHub repository secrets can be stored at the repository scope, and workflows can access them using the secrets context.
Step 8: Create a GitHub Repository Variable
Now, create one non-sensitive variable. In the same GitHub area:
- Settings
- Secrets and variables
- Actions
- Variables
- New repository variable
Create something like:
- Name: APP_ENVIRONMENT
- Value: Development
- Then click on the Add Variable button
GitHub variables are meant for non-sensitive configuration values, and workflows can access them using the vars context.
Step 9: Use the Secret and Variable in the Workflow
Creating a variable or secret in GitHub only stores it in GitHub. It does not automatically become available inside every workflow step. GitHub says variables are accessed through the vars context, and secrets are only readable in a workflow when you explicitly include them.
Let’s understand it with one simple analogy. Think of GitHub as a locker room.
- A variable or secret is like an item stored in a locker.
- Your workflow is like a worker entering the room.
- The worker does not automatically carry every item from every locker.
- You must explicitly say which item to take and where to use it.
Syntax: ${{ vars.APP_ENVIRONMENT }} or ${{ secrets.DEMO_API_KEY }} in the workflow.
Example to use Variables in a workflow:
– name: Show non-sensitive variable
run: echo “Workflow environment is ${{ vars.APP_ENVIRONMENT }}”
Here, APP_ENVIRONMENT is a GitHub variable, so we access it with: ${{ vars.APP_ENVIRONMENT }}
GitHub reads the value of APP_ENVIRONMENT and replaces it before the command runs. So, if you created this variable in GitHub:
- APP_ENVIRONMENT = Development
Then this line behaves like:
- run: echo “Workflow environment is Development.”
So, the log shows: Workflow environment is Development
Example to use Secret in a workflow:
– name: Check secret availability
env:
DEMO_API_KEY: ${{ secrets.DEMO_API_KEY }}
run: |
if [ -z “$DEMO_API_KEY” ]; then
echo “Secret is missing.”
exit 1
fi
echo “Secret is available for workflow use.”
Code Explanation:
This is the part that needs the most clarity.
Step-A: secrets.DEMO_API_KEY
This part: ${{ secrets.DEMO_API_KEY }} means read the stored secret value from GitHub.
Step-B: Why put it inside env?
env:
DEMO_API_KEY: ${{ secrets.DEMO_API_KEY }}
This means create an environment variable named DEMO_API_KEY for this step, and fill it with the GitHub secret value.
GitHub allows secrets to be passed to a step as an environment variable using env. This is why inside the shell script we use $DEMO_API_KEY and not ${{ secrets.DEMO_API_KEY }}
So, the flow is:
- GitHub reads the secret
- GitHub puts it into the step’s environment variable
- The shell script uses that environment variable
Step-C: What does this check do?
if [ -z “$DEMO_API_KEY” ]; then
echo “Secret is missing.”
exit 1
fi
Here:
-z means is this string empty?
So, this logic says:
- If the secret value is empty, print “Secret is missing”
- Then stop the workflow with exit 1
Because GitHub returns an empty string when a secret is missing, this is a safe way to verify that the secret exists.
Step 10: Create GitHub Environments
Now, we will move from repository-wide or global settings to environment-specific settings.
In GitHub, go to:
- Settings
- Environments
- New environment
Create:
- Development
- Production
GitHub Environments are used to group environment-specific deployment controls, such as environment secrets and variables.
Why This Matters?
Repository-level secrets and variables apply broadly. Environment-level secrets and variables let you say:
- Development uses one value
- Production uses another value
That is exactly what we want in real CI/CD.
Step 11: Add Environment-Specific Secret and Variable
Now, inside the Development environment, add:
- Secret: DEPLOY_API_KEY = dev-deploy-key-123
- Variable: TARGET_SERVER_NAME = Dev-IIS-Server
Inside the Production environment, add:
- Secret: DEPLOY_API_KEY = prod-deploy-key-456
- Variable: TARGET_SERVER_NAME = Prod-IIS-Server
Step 12: Connect a Job to an Environment
Now update the workflow job like this:
jobs:
build-test-publish:
name: Build, Test, and Publish
runs-on: ubuntu-latest
environment:
name: Development
deployment: false
This tells GitHub:
- Run this job using the Development environment
- Apply any rules configured on that environment
- Make the Development environment secrets and variables available to this job
- But do not treat it as a real deployment record because deployment: false is set. This is specifically useful for CI or testing jobs that need environment secrets without actually deploying.
Step 13: Final Workflow Example
The following version uses all four clearly:
- Repository variable: APP_ENVIRONMENT
- Repository secret: DEMO_API_KEY
- Environment variable: TARGET_SERVER_NAME
- Environment secret: DEPLOY_API_KEY
- Job attached to the Development environment
Please update the ci.yml as follows.
name: StudentManagement API CI
on:
# Run CI when code is pushed to main or any feature branch
push:
branches:
- main
- feature/**
# Run CI when a pull request is created or updated for main
pull_request:
branches:
- main
jobs:
build-test-publish:
name: Build, Test, and Publish
runs-on: ubuntu-latest
# Attach this job to the Development environment
# So, it can use environment-level variables and secrets
environment:
name: Development
deployment: false
steps:
# Step 1: Download the repository code
- name: Checkout source code
uses: actions/checkout@v5
# Step 2: Install .NET 8 SDK
- name: Setup .NET 8 SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: '8.0.x'
# Step 3: Show repository-level variable
- name: Show repository variable
run: echo "Workflow environment is ${{ vars.APP_ENVIRONMENT }}"
# Step 4: Show environment-level variable
- name: Show environment variable
run: echo "Target server is ${{ vars.TARGET_SERVER_NAME }}"
# Step 5: Check repository-level secret
- name: Check repository secret
env:
DEMO_API_KEY: ${{ secrets.DEMO_API_KEY }}
run: |
if [ -z "$DEMO_API_KEY" ]; then
echo "Repository secret DEMO_API_KEY is missing."
exit 1
fi
echo "Repository secret is available."
# Step 6: Check environment-level secret
- name: Check environment secret
env:
DEPLOY_API_KEY: ${{ secrets.DEPLOY_API_KEY }}
run: |
if [ -z "$DEPLOY_API_KEY" ]; then
echo "Environment secret DEPLOY_API_KEY is missing."
exit 1
fi
echo "Environment secret is available."
# Step 7: Restore NuGet packages
- name: Restore dependencies
run: dotnet restore StudentManagement.API.sln
# Step 8: Build the solution in Release mode
- name: Build solution
run: dotnet build StudentManagement.API.sln --configuration Release --no-restore
# Step 9: Run unit tests
- name: Run tests
run: dotnet test StudentManagement.API.sln --configuration Release --no-build --verbosity normal
# Step 10: Publish the API project
- name: Publish Web API
run: dotnet publish StudentManagement.API/StudentManagement.API.csproj --configuration Release --output ./publish --no-build
# Step 11: Store the publish output as an artifact
- name: Upload publish artifact
uses: actions/upload-artifact@v6
with:
name: studentmanagement-api-publish
path: ./publish
Conclusion
In this chapter, we prepared our StudentManagement.API project to improve CI/CD safety by separating normal configuration from sensitive values. We stored normal application settings in appsettings.json, used environment-specific JSON files for Development and Production, created GitHub variables for non-sensitive pipeline values, created GitHub secrets for sensitive values, and introduced GitHub Environments for environment-level control.
