This pipeline will include steps for checking formatting, building the application, running unit and integration tests, building and pushing a Docker image, and deploying the application.
az ad sp create-for-rbac --name terraform --role Contributor --role "Role Based Access Control Administrator" \
--scopes /subscriptions/{your_subscription_id}
Replace {your_subscription_id} with your subscription id
$env:ARM_SUBSCRIPTION_ID = "{your_subscription_id}"
$env:ARM_TENANT_ID = "{tenant_id}"
$env:ARM_CLIENT_ID = "{client_id}"
$env:ARM_CLIENT_SECRET = "{client_secret}"
In your repository’s .github/workflows directory, create a YAML file (e.g., cicd.yml)
This is where you define the workflow that GitHub Actions will run. The on section specifies when the workflow should be triggered. In this case, it’s set to run whenever there’s a push or a pull request to the master branch. The jobs section is where you define the tasks that make up your workflow.
name: Build and Test
on:
push:
branches: [ master ]
permissions:
id-token: write
contents: read
pull-requests: write
env:
ARM_CLIENT_ID: "${{ secrets.ARM_CLIENT_ID }}"
ARM_SUBSCRIPTION_ID: "${{ secrets.ARM_SUBSCRIPTION_ID }}"
ARM_TENANT_ID: "${{ secrets.ARM_TENANT_ID }}"
ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }}
Sentry__Environment: "dev"
jobs:
It applies a set of style rules to your code and can detect when code violates these rules. The –check option makes it return a non-zero exit code when it finds badly formatted code, causing the workflow to fail.
For more info on installation https://csharpier.com
check-formatting:
name: Check Formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: |
dotnet tool restore
dotnet csharpier --check .
The dotnet build command compiles your code into a runnable application. The –no-restore option tells it not to restore NuGet packages (since this is done in a separate step).
build:
name: 'Build'
runs-on: ubuntu-latest
needs: check-formatting
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '^7.0'
- name: Restore Dependencies
run: dotnet restore
- name: Build The Application
run: dotnet build --no-restore
The dotnet test command runs your unit tests. The –no-restore option tells it not to restore NuGet packages, and the –verbosity normal option tells it to output detailed messages about what it’s doing.
unit-tests:
name: 'Unit Tests'
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '^7.0'
- name: Run Unit Tests
run: dotnet test --verbosity normal --filter "UnitTest"
Here is an example of using Terraform to set up infrastructure for integration tests, which is then destroyed upon completion of the tests.
integration-tests:
name: 'Integration Tests'
runs-on: ubuntu-latest
needs: build
env:
#this is needed since we are running terraform with read-only permissions
ARM_SKIP_PROVIDER_REGISTRATION: true
outputs:
tfplanExitCode: ${{ steps.tf-plan.outputs.exitcode }}
steps:
# Checkout the repository to the GitHub Actions runner
- name: Checkout
uses: actions/checkout@v3
# Install the latest version of the Terraform CLI
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_wrapper: false
# Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc.
- name: Terraform Init
run: terraform init
working-directory: ./.github/terraform-intergration-test
# Checks that all Terraform configuration files adhere to a canonical format
# Will fail the build if not
- name: Terraform Format
run: terraform fmt -check
working-directory: ./.github/terraform-intergration-test
# Generates an execution plan for Terraform
# An exit code of 0 indicated no changes, 1 a terraform failure, 2 there are pending changes.
- name: Terraform Plan
id: tf-plan
run: |
export exitcode=0
terraform plan -detailed-exitcode -no-color -out tfplan || export exitcode=$?
echo "exitcode=$exitcode" >> $GITHUB_OUTPUT
if [ $exitcode -eq 1 ]; then
echo Terraform Plan Failed!
exit 1
else
exit 0
fi
working-directory: ./.github/terraform-intergration-test
# Create string output of Terraform Plan
- name: Create String Output
id: tf-plan-string
run: |
TERRAFORM_PLAN=$(terraform show -no-color tfplan)
delimiter="$(openssl rand -hex 8)"
echo "summary<<${delimiter}" >> $GITHUB_OUTPUT
echo "## Terraform Plan Output" >> $GITHUB_OUTPUT
echo "<details><summary>Click to expand</summary>" >> $GITHUB_OUTPUT
echo "" >> $GITHUB_OUTPUT
echo '```terraform' >> $GITHUB_OUTPUT
echo "$TERRAFORM_PLAN" >> $GITHUB_OUTPUT
echo '```' >> $GITHUB_OUTPUT
echo "</details>" >> $GITHUB_OUTPUT
echo "${delimiter}" >> $GITHUB_OUTPUT
working-directory: ./.github/terraform-intergration-test
# Publish Terraform Plan as task summary
- name: Publish Terraform Plan to Task Summary
env:
SUMMARY: ${{ steps.tf-plan-string.outputs.summary }}
run: |
echo "$SUMMARY" >> $GITHUB_STEP_SUMMARY
working-directory: ./.github/terraform-intergration-test
# Terraform Apply
- name: Terraform Apply
run: |
terraform apply -auto-approve tfplan
host=$(terraform output -raw postgresql_host)
connectionString=$(terraform output -raw postgresql_connection_string)
keyvault_secret_name=$(terraform output -raw keyvault_secret_name)
keyvault_url=$(terraform output -raw keyvault_url)
example_function_keys=$(terraform output -raw example_function_keys)
example_blob_url=$(terraform output -raw example_blob_url)
example_function_uri=$(terraform output -raw example_function_uri)
cicd=true
echo "ConnectionStrings__ConnectionString=$connectionString" >> $GITHUB_ENV
echo "example_DATABASE_HOST=$host" >> $GITHUB_ENV
echo "AzureKeyVault__SecretName=$keyvault_secret_name" >> $GITHUB_ENV
echo "AzureKeyVault__Url=$keyvault_url" >> $GITHUB_ENV
echo "AzureFunction__Keys=$example_function_keys" >> $GITHUB_ENV
echo "AzureFunction__exampleBlobUrl=$example_blob_url" >> $GITHUB_ENV
echo "AzureFunction__Url=$example_function_uri" >> $GITHUB_ENV
echo "cicd=$cicd" >> $GITHUB_ENV
working-directory: ./.github/terraform-intergration-test
# Integration tests
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '^7.0'
- name: Run Integration Tests
run: dotnet test --verbosity normal --filter "IntegrationTest"
# Terraform Destroy
- name: Terraform Destroy
if: always()
run: terraform destroy -auto-approve
working-directory: ./.github/terraform-intergration-test
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "=3.84.0"
}
}
}
provider "azurerm" {
features {
key_vault {
purge_soft_deleted_secrets_on_destroy = true
recover_soft_deleted_secrets = true
}
resource_group {
prevent_deletion_if_contains_resources = false
}
}
subscription_id = var.subscription_id
}
resource "azurerm_resource_group" "example" {
name = var.resource_group_name
location = var.resource_group_location
tags = {
environment = var.tag_environment
}
}
resource "random_string" "random" {
length = 10
lower = true
numeric = false
special = false
upper = false
}
resource "azurerm_postgresql_flexible_server" "example" {
name = "${var.postgresql_name}${lower(random_string.random.id)}"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
version = "14"
administrator_login = var.administrator_login
administrator_password = var.administrator_password
zone = "1"
storage_mb = 32768
sku_name = "B_Standard_B1ms"
tags = {
environment = var.tag_environment
}
}
resource "azurerm_postgresql_flexible_server_firewall_rule" "example" {
name = "${var.example_fw_name}${lower(random_string.random.id)}"
server_id = azurerm_postgresql_flexible_server.example.id
start_ip_address = "0.0.0.0"
end_ip_address = "255.255.255.255"
}
resource "azurerm_storage_account" "example" {
name = "${var.storage_account_name}${lower(random_string.random.id)}"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
account_tier = "Standard"
account_replication_type = "LRS"
min_tls_version = "TLS1_2"
}
resource "azurerm_storage_container" "example" {
name = "example"
storage_account_name = azurerm_storage_account.example.name
container_access_type = "blob"
}
resource "azurerm_service_plan" "example" {
name = "${var.example_app_service_plan_name}${lower(random_string.random.id)}"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
os_type = "Linux"
sku_name = "B1"
}
resource "azurerm_application_insights" "example" {
name = "${var.example_appinsights_plan_name}${lower(random_string.random.id)}"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
application_type = "other"
}
resource "azurerm_linux_function_app" "example" {
name = "${var.function_name}${lower(random_string.random.id)}"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
storage_account_name = azurerm_storage_account.example.name
storage_account_access_key = azurerm_storage_account.example.primary_access_key
service_plan_id = azurerm_service_plan.example.id
site_config {
application_insights_key = azurerm_application_insights.example.instrumentation_key
application_insights_connection_string = azurerm_application_insights.example.connection_string
application_stack {
python_version = "3.11"
}
}
app_settings = {
"ENABLE_ORYX_BUILD" = true
"SCM_DO_BUILD_DURING_DEPLOYMENT" = true
}
zip_deploy_file = "functions.zip"
identity {
type = "SystemAssigned"
}
}
data "azurerm_client_config" "current" {}
resource "azurerm_key_vault" "example" {
name = "${var.key_vault_name}${lower(random_string.random.id)}"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
tenant_id = data.azurerm_client_config.current.tenant_id
sku_name = "standard"
soft_delete_retention_days = 7
}
resource "azurerm_key_vault_access_policy" "example" {
key_vault_id = azurerm_key_vault.example.id
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = data.azurerm_client_config.current.object_id
key_permissions = [
"Create",
"Get",
"List",
]
secret_permissions = [
"Set",
"Get",
"List",
"Delete",
"Purge",
"Recover"
]
}
resource "azurerm_key_vault_access_policy" "example-function" {
key_vault_id = azurerm_key_vault.example.id
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = azurerm_linux_function_app.example.identity[0].principal_id
key_permissions = [
"Get",
]
secret_permissions = [
"Get",
]
depends_on = [
azurerm_key_vault_access_policy.example
]
}
resource "azurerm_role_assignment" "example" {
scope = azurerm_key_vault.example.id
role_definition_name = "Reader"
principal_id = azurerm_linux_function_app.example.identity[0].principal_id
depends_on = [
azurerm_key_vault_access_policy.example-function
]
}
resource "azurerm_key_vault_secret" "example" {
name = "${var.key_vault_secret_name}${lower(random_string.random.id)}"
value = azurerm_storage_account.example.primary_connection_string
key_vault_id = azurerm_key_vault.example.id
depends_on = [
azurerm_key_vault_access_policy.example
]
}
data "azurerm_function_app_host_keys" "example" {
name = azurerm_linux_function_app.example.name
resource_group_name = azurerm_resource_group.example.name
depends_on = [
azurerm_resource_group.example,
azurerm_linux_function_app.example
]
}
output "example_function_keys" {
value = data.azurerm_function_app_host_keys.example.default_function_key
sensitive = true
}
output "example_blob_url" {
value = "https://${azurerm_storage_account.example.name}.blob.core.windows.net/example"
}
output "example_function_uri" {
value = "https://${azurerm_linux_function_app.example.name}.azurewebsites.net"
}
output "postgresql_host" {
value = "${azurerm_postgresql_flexible_server.example.name}.postgres.database.azure.com"
sensitive = true
}
output "postgresql_connection_string" {
value = "Host=${azurerm_postgresql_flexible_server.example.name}.postgres.database.azure.com;Database=example;
Username=${var.administrator_login};Password=${var.administrator_password};"
sensitive = true
}
output "keyvault_secret_name" {
value = azurerm_key_vault_secret.example.name
}
output "keyvault_url" {
value = azurerm_key_vault.example.vault_uri
}
variable "subscription_id" {
type = string
default = "8e53319d-b091-4158-b1b5-d74c44a1554c"
}
variable "resource_group_name" {
type = string
default = "example-be-cicd-rg"
}
variable "resource_group_location" {
type = string
default = "East Asia"
}
variable "tag_environment" {
type = string
default = "dev"
}
variable "postgresql_name" {
type = string
default = "examplepostgres"
}
variable "administrator_login" {
type = string
default = "psqladmin"
}
variable "administrator_password" {
type = string
default = "123456789"
}
variable "example_fw_name" {
type = string
default = "examplefw"
}
variable "function_name" {
type = string
default = "examplefunction"
}
variable "storage_account_name" {
type = string
default = "examplestorage"
}
variable "example_app_service_plan_name" {
type = string
default = "exampleappserviceplan"
}
variable "example_appinsights_plan_name" {
type = string
default = "exampleappinsights"
}
variable "key_vault_name" {
type = string
default = "examplekeyvault"
}
variable "key_vault_secret_name" {
type = string
default = "example-storage-account-connection-string"
}
build-docker-image:
name: 'Build And Push Docker Image'
runs-on: ubuntu-latest
needs: [unit-tests, integration-tests]
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
push: true
tags: mydocker/example-api:v1.0
Remember to replace the placeholders with your actual values. Also, make sure to store sensitive information like your DockerHub username and token as secrets in your GitHub repository.
This is a basic example and might need to be adjusted based on your application’s specific needs. For more information, check out the GitHub Actions documentation.
deploy:
name: 'Deploy'
runs-on: ubuntu-latest
needs: [build-docker-image]
steps:
- name: 'Sign in via Azure CLI'
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- uses: azure/webapps-deploy@v2
with:
app-name: 'myapp'
images: 'mydocker/example-api:v1.0'
Copyright (c) 2024