Continuous Integration and Continuous Deployment (CI/CD) is fundamental to modern software development, enabling teams to deliver high-quality software rapidly and reliably. This guide provides comprehensive implementation strategies for various CI/CD platforms.
Continuous Integration and Continuous Deployment (CI/CD) is fundamental to modern software development, enabling teams to deliver high-quality software rapidly and reliably. This guide provides comprehensive implementation strategies for various CI/CD platforms.
CI/CD Fundamentals
Core Principles
┌─────────────────────────────────────────────────────────────────┐
│ CI/CD Pipeline Flow │
├─────────────────────────────────────────────────────────────────┤
│ Stage │ Activities │
│ ├─ Source │ Code commit, branch policies, code review │
│ ├─ Build │ Compile, package, dependency management │
│ ├─ Test │ Unit, integration, security, performance │
│ ├─ Deploy │ Staging, production, rollback capabilities │
│ └─ Monitor │ Health checks, metrics, alerting │
└─────────────────────────────────────────────────────────────────┘
Pipeline Benefits
- Faster Time to Market: Automated processes reduce manual delays
- Higher Quality: Consistent testing and validation
- Reduced Risk: Smaller, frequent deployments
- Improved Reliability: Standardized deployment processes
- Better Collaboration: Shared responsibility and visibility
GitHub Actions Implementation
Comprehensive Workflow Configuration
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
schedule:
- cron: '0 2 * * 0' # Weekly security scan
env:
NODE_VERSION: '18'
PYTHON_VERSION: '3.11'
DOCKER_REGISTRY: 'your-registry.azurecr.io'
AZURE_WEBAPP_NAME: 'your-webapp'
jobs:
# Security and Code Quality
security-scan:
name: Security & Code Quality
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for better analysis
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v2
if: always()
with:
sarif_file: 'trivy-results.sarif'
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
# Build and Test Matrix
build-test:
name: Build & Test
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [16, 18, 20]
include:
- os: ubuntu-latest
node-version: 18
coverage: true
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linting
run: npm run lint
- name: Run type checking
run: npm run type-check
- name: Run unit tests
run: npm run test:unit
- name: Run integration tests
run: npm run test:integration
env:
CI: true
- name: Generate coverage report
if: matrix.coverage
run: npm run test:coverage
- name: Upload coverage to Codecov
if: matrix.coverage
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage/lcov.info
flags: unittests
name: codecov-umbrella
- name: Build application
run: npm run build
- name: Upload build artifacts
if: matrix.os == 'ubuntu-latest' && matrix.node-version == 18
uses: actions/upload-artifact@v3
with:
name: build-artifacts
path: |
dist/
package.json
package-lock.json
retention-days: 30
# Docker Build and Push
docker-build:
name: Docker Build & Push
runs-on: ubuntu-latest
needs: [security-scan, build-test]
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: build-artifacts
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Azure Container Registry
uses: azure/docker-login@v1
with:
login-server: ${{ env.DOCKER_REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.DOCKER_REGISTRY }}/myapp
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
NODE_VERSION=${{ env.NODE_VERSION }}
BUILD_DATE=${{ steps.meta.outputs.labels }}
# Performance Testing
performance-test:
name: Performance Testing
runs-on: ubuntu-latest
needs: build-test
if: github.event_name == 'pull_request'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Start application
run: |
npm run build
npm start &
sleep 30
- name: Run Lighthouse CI
run: |
npm install -g @lhci/cli@0.12.x
lhci autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
- name: Run load tests
run: |
npm install -g artillery
artillery run tests/performance/load-test.yml
# Staging Deployment
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
needs: [docker-build]
if: github.ref == 'refs/heads/main'
environment:
name: staging
url: https://staging.${{ env.AZURE_WEBAPP_NAME }}.azurewebsites.net
steps:
- name: Azure Login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Deploy to Azure Web App
uses: azure/webapps-deploy@v2
with:
app-name: ${{ env.AZURE_WEBAPP_NAME }}-staging
images: ${{ env.DOCKER_REGISTRY }}/myapp:main
- name: Run smoke tests
run: |
curl -f https://staging.${{ env.AZURE_WEBAPP_NAME }}.azurewebsites.net/health || exit 1
# Production Deployment
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
needs: [deploy-staging]
if: github.ref == 'refs/heads/main'
environment:
name: production
url: https://${{ env.AZURE_WEBAPP_NAME }}.azurewebsites.net
steps:
- name: Azure Login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Blue-Green Deployment
run: |
# Deploy to blue slot
az webapp deployment slot create \
--name ${{ env.AZURE_WEBAPP_NAME }} \
--resource-group myResourceGroup \
--slot blue
- name: Deploy to Blue Slot
uses: azure/webapps-deploy@v2
with:
app-name: ${{ env.AZURE_WEBAPP_NAME }}
slot-name: blue
images: ${{ env.DOCKER_REGISTRY }}/myapp:main
- name: Run production smoke tests
run: |
curl -f https://${{ env.AZURE_WEBAPP_NAME }}-blue.azurewebsites.net/health || exit 1
- name: Swap to production
run: |
az webapp deployment slot swap \
--name ${{ env.AZURE_WEBAPP_NAME }} \
--resource-group myResourceGroup \
--slot blue \
--target-slot production
- name: Verify deployment
run: |
curl -f https://${{ env.AZURE_WEBAPP_NAME }}.azurewebsites.net/health || exit 1
# Notification
notify:
name: Notify Team
runs-on: ubuntu-latest
needs: [deploy-production]
if: always()
steps:
- name: Notify Slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
channel: '#deployments'
text: |
Deployment to production completed
Status: ${{ job.status }}
Commit: ${{ github.sha }}
Author: ${{ github.actor }}
Azure DevOps Pipelines
Multi-Stage Pipeline Configuration
# azure-pipelines.yml
trigger:
branches:
include:
- main
- develop
paths:
exclude:
- docs/*
- README.md
pr:
branches:
include:
- main
drafts: false
variables:
# Build Variables
buildConfiguration: 'Release'
dotNetFramework: 'net8.0'
dotNetVersion: '8.0.x'
# Azure Variables
azureServiceConnection: 'azure-service-connection'
containerRegistry: 'myregistry.azurecr.io'
imageRepository: 'myapp'
dockerfilePath: '$(Build.SourcesDirectory)/Dockerfile'
# Environment Variables
vmImageName: 'ubuntu-latest'
resources:
repositories:
- repository: templates
type: git
name: DevOps/pipeline-templates
ref: refs/heads/main
stages:
# Continuous Integration
- stage: CI
displayName: 'Continuous Integration'
jobs:
- job: SecurityScan
displayName: 'Security and Code Quality'
pool:
vmImage: $(vmImageName)
steps:
- checkout: self
fetchDepth: 0
- task: SonarCloudPrepare@1
displayName: 'Prepare SonarCloud analysis'
inputs:
SonarCloud: 'SonarCloud-Connection'
organization: 'myorg'
scannerMode: 'MSBuild'
projectKey: 'myproject'
- task: UseDotNet@2
displayName: 'Use .NET SDK'
inputs:
packageType: 'sdk'
version: $(dotNetVersion)
- task: DotNetCoreCLI@2
displayName: 'Restore packages'
inputs:
command: 'restore'
projects: '**/*.csproj'
- task: DotNetCoreCLI@2
displayName: 'Build solution'
inputs:
command: 'build'
projects: '**/*.csproj'
arguments: '--configuration $(buildConfiguration) --no-restore'
- task: SonarCloudAnalyze@1
displayName: 'Run SonarCloud analysis'
- task: SonarCloudPublish@1
displayName: 'Publish SonarCloud results'
- task: WhiteSource@21
displayName: 'WhiteSource security scan'
inputs:
cwd: '$(System.DefaultWorkingDirectory)'
- job: BuildAndTest
displayName: 'Build and Test'
pool:
vmImage: $(vmImageName)
strategy:
matrix:
Debug:
buildConfiguration: 'Debug'
Release:
buildConfiguration: 'Release'
steps:
- template: templates/dotnet-build-test.yml@templates
parameters:
buildConfiguration: $(buildConfiguration)
publishTestResults: true
publishCodeCoverage: true
- job: DockerBuild
displayName: 'Docker Build'
dependsOn: [SecurityScan, BuildAndTest]
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
pool:
vmImage: $(vmImageName)
steps:
- task: Docker@2
displayName: 'Build and push image'
inputs:
containerRegistry: $(azureServiceConnection)
repository: $(imageRepository)
command: 'buildAndPush'
Dockerfile: $(dockerfilePath)
tags: |
$(Build.BuildId)
latest
# Staging Deployment
- stage: DeployStaging
displayName: 'Deploy to Staging'
dependsOn: CI
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
variables:
environmentName: 'staging'
jobs:
- deployment: DeployStaging
displayName: 'Deploy to Staging Environment'
pool:
vmImage: $(vmImageName)
environment: 'staging'
strategy:
runOnce:
deploy:
steps:
- template: templates/azure-web-app-deploy.yml@templates
parameters:
azureSubscription: $(azureServiceConnection)
appName: 'myapp-staging'
containerRegistry: $(containerRegistry)
imageRepository: $(imageRepository)
tag: $(Build.BuildId)
- job: StagingTests
displayName: 'Run Staging Tests'
dependsOn: DeployStaging
pool:
vmImage: $(vmImageName)
steps:
- task: PowerShell@2
displayName: 'Run smoke tests'
inputs:
targetType: 'inline'
script: |
$response = Invoke-WebRequest -Uri "https://myapp-staging.azurewebsites.net/health" -UseBasicParsing
if ($response.StatusCode -ne 200)
{
throw "Health check failed"
}
# Production Deployment
- stage: DeployProduction
displayName: 'Deploy to Production'
dependsOn: DeployStaging
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
variables:
environmentName: 'production'
jobs:
- deployment: DeployProduction
displayName: 'Deploy to Production Environment'
pool:
vmImage: $(vmImageName)
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- template: templates/azure-web-app-deploy.yml@templates
parameters:
azureSubscription: $(azureServiceConnection)
appName: 'myapp-production'
containerRegistry: $(containerRegistry)
imageRepository: $(imageRepository)
tag: $(Build.BuildId)
useBlueGreenDeployment: true
- task: AzureAppServiceManage@0
displayName: 'Restart App Service'
inputs:
azureSubscription: $(azureServiceConnection)
WebAppName: 'myapp-production'
Action: 'Restart web app'
- job: ProductionTests
displayName: 'Run Production Tests'
dependsOn: DeployProduction
pool:
vmImage: $(vmImageName)
steps:
- task: PowerShell@2
displayName: 'Run production health checks'
inputs:
targetType: 'inline'
script: |
# Multiple health check endpoints
$endpoints = @(
"https://myapp-production.azurewebsites.net/health",
"https://myapp-production.azurewebsites.net/ready",
"https://myapp-production.azurewebsites.net/api/status"
)
foreach ($endpoint in $endpoints)
{
try
{
$response = Invoke-WebRequest -Uri $endpoint -UseBasicParsing -TimeoutSec 30
Write-Host "✓ $endpoint - Status: $($response.StatusCode)"
}
catch
{
Write-Error "✗ $endpoint - Failed: $($_.Exception.Message)"
throw
}
}
Jenkins Pipeline Implementation
Declarative Pipeline with Advanced Features
// Jenkinsfile
pipeline {
agent {
kubernetes {
yaml """
apiVersion: v1
kind: Pod
spec:
containers:
- name: docker
image: docker:20.10.7-dind
securityContext:
privileged: true
- name: node
image: node:18-alpine
command:
- cat
tty: true
- name: python
image: python:3.11-slim
command:
- cat
tty: true
- name: sonar
image: sonarqube:9.9-community
command:
- cat
tty: true
"""
}
}
environment {
DOCKER_REGISTRY = 'your-registry.com'
APP_NAME = 'myapp'
KUBECONFIG = credentials('kubeconfig')
SONAR_TOKEN = credentials('sonar-token')
SLACK_WEBHOOK = credentials('slack-webhook')
}
options {
buildDiscarder(logRotator(numToKeepStr: '10'))
timeout(time: 60, unit: 'MINUTES')
retry(2)
skipStagesAfterUnstable()
parallelsAlwaysFailFast()
}
triggers {
cron(env.BRANCH_NAME == 'main' ? 'H 2 * * 0' : '')
pollSCM(env.BRANCH_NAME == 'main' ? 'H/5 * * * *' : '')
}
stages {
stage('Preparation') {
steps {
script {
// Set dynamic variables
env.BUILD_VERSION = sh(
script: "echo '${env.BUILD_NUMBER}-${env.GIT_COMMIT[0..7]}'",
returnStdout: true
).trim()
env.IS_MAIN_BRANCH = env.BRANCH_NAME == 'main'
env.IS_PR = env.CHANGE_ID != null
}
// Checkout with Git LFS support
checkout([
$class: 'GitSCM',
branches: scm.branches,
extensions: [
[$class: 'GitLFSPull'],
[$class: 'CleanCheckout']
],
userRemoteConfigs: scm.userRemoteConfigs
])
}
}
stage('Quality Gates') {
parallel {
stage('Security Scan') {
steps {
container('python') {
sh '''
pip install safety bandit semgrep
# Check for known vulnerabilities
safety check --json --output safety-report.json || true
# Static security analysis
bandit -r . -f json -o bandit-report.json || true
# SAST scanning
semgrep --config=auto --json --output=semgrep-report.json . || true
'''
}
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: '.',
reportFiles: 'safety-report.json,bandit-report.json,semgrep-report.json',
reportName: 'Security Scan Report'
])
}
}
stage('Code Quality') {
steps {
container('sonar') {
script {
def scannerHome = tool 'SonarScanner'
withSonarQubeEnv('SonarQube') {
sh """
${scannerHome}/bin/sonar-scanner \
-Dsonar.projectKey=${env.APP_NAME} \
-Dsonar.projectName=${env.APP_NAME} \
-Dsonar.projectVersion=${env.BUILD_VERSION} \
-Dsonar.sources=. \
-Dsonar.exclusions=**/node_modules/**,**/dist/**,**/*.test.js \
-Dsonar.javascript.lcov.reportPaths=coverage/lcov.info
"""
}
}
}
timeout(time: 10, unit: 'MINUTES') {
script {
def qg = waitForQualityGate()
if (qg.status != 'OK') {
error "Pipeline aborted due to quality gate failure: ${qg.status}"
}
}
}
}
}
stage('License Compliance') {
steps {
container('node') {
sh '''
npm install -g license-checker
license-checker --onlyAllow 'MIT;Apache-2.0;BSD-2-Clause;BSD-3-Clause;ISC' \
--excludePrivatePackages \
--json > license-report.json
'''
}
archiveArtifacts artifacts: 'license-report.json', fingerprint: true
}
}
}
}
stage('Build & Test') {
matrix {
axes {
axis {
name 'NODE_VERSION'
values '16', '18', '20'
}
axis {
name 'ENVIRONMENT'
values 'development', 'production'
}
}
excludes {
exclude {
axis {
name 'NODE_VERSION'
values '16'
}
axis {
name 'ENVIRONMENT'
values 'production'
}
}
}
stages {
stage('Matrix Build') {
steps {
container('node') {
sh """
node --version
npm --version
# Install dependencies
npm ci
# Run linting
npm run lint
# Run tests with coverage
NODE_ENV=${ENVIRONMENT} npm run test:coverage
# Build application
NODE_ENV=${ENVIRONMENT} npm run build
"""
}
}
post {
always {
publishTestResults testResultsPattern: 'test-results.xml'
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'coverage',
reportFiles: 'index.html',
reportName: "Coverage Report - Node ${NODE_VERSION} ${ENVIRONMENT}"
])
}
}
}
}
}
}
stage('Integration Tests') {
steps {
script {
// Start test services
sh '''
docker-compose -f docker-compose.test.yml up -d
sleep 30
'''
try {
container('node') {
sh 'npm run test:integration'
}
} finally {
// Cleanup test services
sh 'docker-compose -f docker-compose.test.yml down -v'
}
}
}
}
stage('Build Docker Image') {
when {
anyOf {
branch 'main'
branch 'develop'
changeRequest()
}
}
steps {
container('docker') {
script {
def imageTag = env.IS_MAIN_BRANCH ? 'latest' : env.BUILD_VERSION
def image = docker.build("${env.DOCKER_REGISTRY}/${env.APP_NAME}:${imageTag}")
// Security scan of Docker image
sh """
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy image \
--format json \
--output trivy-image-report.json \
${env.DOCKER_REGISTRY}/${env.APP_NAME}:${imageTag}
"""
if (env.IS_MAIN_BRANCH) {
docker.withRegistry("https://${env.DOCKER_REGISTRY}", 'docker-registry-credentials') {
image.push()
image.push(env.BUILD_VERSION)
}
}
}
}
}
}
stage('Deploy') {
when {
branch 'main'
}
stages {
stage('Deploy to Staging') {
steps {
script {
deployToEnvironment('staging', env.BUILD_VERSION)
}
}
post {
success {
script {
runSmokeTests('staging')
}
}
}
}
stage('Approval Gate') {
steps {
script {
def deployment = input(
id: 'productionDeployment',
message: 'Deploy to production?',
submitter: 'deployment-team',
parameters: [
choice(
name: 'DEPLOYMENT_STRATEGY',
choices: ['blue-green', 'rolling', 'canary'],
description: 'Deployment strategy'
),
booleanParam(
name: 'SKIP_SMOKE_TESTS',
defaultValue: false,
description: 'Skip smoke tests'
)
]
)
env.DEPLOYMENT_STRATEGY = deployment.DEPLOYMENT_STRATEGY
env.SKIP_SMOKE_TESTS = deployment.SKIP_SMOKE_TESTS
}
}
}
stage('Deploy to Production') {
steps {
script {
deployToEnvironment('production', env.BUILD_VERSION, env.DEPLOYMENT_STRATEGY)
}
}
post {
success {
script {
if (!env.SKIP_SMOKE_TESTS.toBoolean()) {
runSmokeTests('production')
}
}
}
}
}
}
}
}
post {
always {
// Archive artifacts
archiveArtifacts artifacts: '**/*-report.json,**/coverage/**', allowEmptyArchive: true
// Clean workspace
cleanWs()
}
success {
script {
if (env.IS_MAIN_BRANCH) {
slackSend(
channel: '#deployments',
color: 'good',
message: """
✅ Deployment Successful
Project: ${env.APP_NAME}
Version: ${env.BUILD_VERSION}
Duration: ${currentBuild.durationString}
"""
)
}
}
}
failure {
slackSend(
channel: '#deployments',
color: 'danger',
message: """
❌ Pipeline Failed
Project: ${env.APP_NAME}
Branch: ${env.BRANCH_NAME}
Build: ${env.BUILD_NUMBER}
Stage: ${env.STAGE_NAME}
"""
)
}
}
}
// Helper functions
def deployToEnvironment(environment, version, strategy = 'rolling') {
sh """
helm upgrade --install ${env.APP_NAME}-${environment} ./helm-chart \
--namespace ${environment} \
--set image.tag=${version} \
--set environment=${environment} \
--set deployment.strategy=${strategy} \
--wait --timeout=600s
"""
}
def runSmokeTests(environment) {
sh """
curl -f https://${env.APP_NAME}-${environment}.example.com/health || exit 1
curl -f https://${env.APP_NAME}-${environment}.example.com/ready || exit 1
"""
}
Infrastructure as Code Integration
Terraform CI/CD Pipeline
# .github/workflows/terraform.yml
name: Terraform CI/CD
on:
push:
branches: [main]
paths: ['terraform/**']
pull_request:
branches: [main]
paths: ['terraform/**']
env:
TF_VERSION: '1.7.0'
WORKING_DIR: './terraform'
jobs:
terraform-validate:
name: Terraform Validate
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ env.WORKING_DIR }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Terraform Format Check
run: terraform fmt -check -recursive
- name: Terraform Initialize
run: terraform init -backend=false
- name: Terraform Validate
run: terraform validate
- name: Run TFLint
uses: terraform-linters/setup-tflint@v4
with:
tflint_version: v0.50.0
- name: TFLint
run: |
tflint --init
tflint --format sarif > tflint-results.sarif
- name: Upload TFLint results
uses: github/codeql-action/upload-sarif@v2
if: always()
with:
sarif_file: ${{ env.WORKING_DIR }}/tflint-results.sarif
- name: Run Checkov
uses: bridgecrewio/checkov-action@master
with:
directory: ${{ env.WORKING_DIR }}
framework: terraform
output_format: sarif
output_file_path: checkov-results.sarif
terraform-plan:
name: Terraform Plan
needs: terraform-validate
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ env.WORKING_DIR }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Azure Login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Terraform Initialize
run: |
terraform init \
-backend-config="resource_group_name=${{ secrets.TF_STATE_RG }}" \
-backend-config="storage_account_name=${{ secrets.TF_STATE_SA }}" \
-backend-config="container_name=tfstate" \
-backend-config="key=main.tfstate"
- name: Terraform Plan
run: |
terraform plan \
-var-file="environments/staging.tfvars" \
-out=tfplan \
-detailed-exitcode
continue-on-error: true
id: plan
- name: Generate Plan Summary
run: |
terraform show -no-color tfplan > plan-output.txt
# Create plan summary for PR comment
echo "## Terraform Plan Summary" > plan-summary.md
echo "" >> plan-summary.md
echo "**Plan Result:** ${{ steps.plan.outcome }}" >> plan-summary.md
echo "" >> plan-summary.md
echo "<details><summary>Show Plan Details</summary>" >> plan-summary.md
echo "" >> plan-summary.md
echo "\`\`\`" >> plan-summary.md
cat plan-output.txt >> plan-summary.md
echo "\`\`\`" >> plan-summary.md
echo "</details>" >> plan-summary.md
- name: Comment PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const planSummary = fs.readFileSync('terraform/plan-summary.md', 'utf8');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: planSummary
});
terraform-apply:
name: Terraform Apply
needs: terraform-plan
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
defaults:
run:
working-directory: ${{ env.WORKING_DIR }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Azure Login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Terraform Initialize
run: |
terraform init \
-backend-config="resource_group_name=${{ secrets.TF_STATE_RG }}" \
-backend-config="storage_account_name=${{ secrets.TF_STATE_SA }}" \
-backend-config="container_name=tfstate" \
-backend-config="key=main.tfstate"
- name: Terraform Apply
run: |
terraform apply \
-var-file="environments/production.tfvars" \
-auto-approve
- name: Output Infrastructure Info
run: |
terraform output -json > infrastructure-outputs.json
- name: Upload Infrastructure Outputs
uses: actions/upload-artifact@v3
with:
name: infrastructure-outputs
path: terraform/infrastructure-outputs.json