diff --git a/.github/scripts/create-appinsights-alert.sh b/.github/scripts/create-appinsights-alert.sh index 8c650e6..c4d8a7a 100755 --- a/.github/scripts/create-appinsights-alert.sh +++ b/.github/scripts/create-appinsights-alert.sh @@ -17,7 +17,7 @@ # Set parameters with defaults RESOURCE_GROUP=${1:-"rg-octopets"} CONTAINER_APP_NAME=${2:-"octopetsapi"} -APP_INSIGHTS_NAME=${3:-"octopets_appinsights-abc123xyz456"} +APP_INSIGHTS_NAME=${3:-"octopets_appinsights-abcd123xyz456"} SUBSCRIPTION_ID=${4:-"12345678-abcd-9e8f-7g6h-5i4j3k2l1m0n"} LOCATION=${5:-"eastus"} diff --git a/.github/scripts/setup-managed-identity-oidc.ps1 b/.github/scripts/setup-managed-identity-oidc.ps1 new file mode 100644 index 0000000..f238915 --- /dev/null +++ b/.github/scripts/setup-managed-identity-oidc.ps1 @@ -0,0 +1,200 @@ +# Azure OIDC Setup Script using User-Assigned Managed Identity +# This script is perfect when you don't have permissions to create Service Principals + +param( + [Parameter(Mandatory=$true)] + [string]$SubscriptionId, + + [Parameter(Mandatory=$true)] + [string]$ResourceGroupName, + + [Parameter(Mandatory=$true)] + [string]$GitHubRepo, + + [Parameter(Mandatory=$false)] + [string]$AzureLocation = "eastus2", + + [Parameter(Mandatory=$false)] + [string]$IdentityName = "github-actions-identity" +) + +Write-Host "🚀 Setting up OIDC with User-Assigned Managed Identity" -ForegroundColor Green +Write-Host "📋 Configuration:" -ForegroundColor Cyan +Write-Host " Subscription: $SubscriptionId" -ForegroundColor White +Write-Host " Resource Group: $ResourceGroupName" -ForegroundColor White +Write-Host " Managed Identity: $IdentityName" -ForegroundColor White +Write-Host " GitHub Repository: $GitHubRepo" -ForegroundColor White +Write-Host " Location: $AzureLocation" -ForegroundColor White + +# Check if user is logged in to Azure +Write-Host "`n🔐 Checking Azure authentication..." -ForegroundColor Blue +$currentAccount = az account show --query "user.name" -o tsv 2>$null +if (-not $currentAccount) { + Write-Host "❌ Please login to Azure CLI first: az login" -ForegroundColor Red + exit 1 +} + +Write-Host "✅ Logged in as: $currentAccount" -ForegroundColor Green + +# Set subscription +Write-Host "🔧 Setting subscription..." -ForegroundColor Blue +az account set --subscription $SubscriptionId + +# Check if resource group exists, create if not +Write-Host "🔧 Checking resource group..." -ForegroundColor Blue +$rgExists = az group exists --name $ResourceGroupName +if ($rgExists -eq "false") { + Write-Host "📦 Creating resource group: $ResourceGroupName" -ForegroundColor Yellow + az group create --name $ResourceGroupName --location $AzureLocation + Write-Host "✅ Resource group created successfully!" -ForegroundColor Green +} else { + Write-Host "✅ Resource group exists: $ResourceGroupName" -ForegroundColor Green +} + +# Check if managed identity already exists +Write-Host "🔧 Checking if managed identity exists..." -ForegroundColor Blue +$existingIdentity = az identity show --resource-group $ResourceGroupName --name $IdentityName 2>$null +if ($existingIdentity) { + Write-Host "⚠️ Managed identity '$IdentityName' already exists" -ForegroundColor Yellow + + # Ensure the output is valid JSON + Write-Host "🔧 Debugging raw output before parsing..." -ForegroundColor Blue + Write-Host "Raw output: $existingIdentity" -ForegroundColor White + + if (-not $existingIdentity) { + Write-Host "❌ Invalid JSON output from az identity show" -ForegroundColor Red + Write-Host "Raw output: $existingIdentity" -ForegroundColor White + exit 1 + } + + $identity = $existingIdentity | ConvertFrom-Json + $clientId = $identity.clientId + $principalId = $identity.principalId + + # Validate extracted values + if (-not $clientId -or -not $principalId) { + Write-Host "❌ Failed to extract Client ID or Principal ID" -ForegroundColor Red + Write-Host "Raw output: $existingIdentity" -ForegroundColor White + exit 1 + } + + Write-Host "✅ Extracted Client ID: $clientId" -ForegroundColor Green + Write-Host "✅ Extracted Principal ID: $principalId" -ForegroundColor Green + Write-Host "✅ Using existing managed identity" -ForegroundColor Green +} else { + # Create user-assigned managed identity + Write-Host "🔧 Creating user-assigned managed identity..." -ForegroundColor Blue + $identity = az identity create ` + --resource-group $ResourceGroupName ` + --name $IdentityName ` + --location $AzureLocation | ConvertFrom-Json + + Write-Host "✅ User-Assigned Managed Identity created successfully!" -ForegroundColor Green + + $clientId = $identity.clientId + $principalId = $identity.principalId +} + +Write-Host " Client ID: $clientId" -ForegroundColor White +Write-Host " Principal ID: $principalId" -ForegroundColor White + +# Get tenant ID +$tenantId = az account show --query tenantId --output tsv + +# Check current role assignments +Write-Host "🔧 Checking role assignments..." -ForegroundColor Blue +$existingRoles = az role assignment list --assignee $principalId --scope "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName" | ConvertFrom-Json + +$hasContributor = $existingRoles | Where-Object { $_.roleDefinitionName -eq "Contributor" } + +if (-not $hasContributor) { + # Assign Contributor role to the managed identity + Write-Host "🔧 Assigning Contributor role..." -ForegroundColor Blue + az role assignment create ` + --assignee-object-id $principalId ` + --assignee-principal-type ServicePrincipal ` + --role "Contributor" ` + --scope "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName" + + Write-Host "✅ Contributor role assigned successfully!" -ForegroundColor Green +} else { + Write-Host "✅ Contributor role already assigned" -ForegroundColor Green +} + +# List existing federated credentials +Write-Host "🔧 Checking existing federated credentials..." -ForegroundColor Blue +$existingCreds = az identity federated-credential list --identity-name $IdentityName --resource-group $ResourceGroupName | ConvertFrom-Json + +$mainBranchCred = $existingCreds | Where-Object { $_.subject -eq "repo:${GitHubRepo}:ref:refs/heads/main" } +$prCred = $existingCreds | Where-Object { $_.subject -eq "repo:${GitHubRepo}:pull_request" } + +# Create federated credential for main branch +if (-not $mainBranchCred) { + Write-Host "🔧 Creating federated credential for main branch..." -ForegroundColor Blue + az identity federated-credential create ` + --name "github-main" ` + --identity-name $IdentityName ` + --resource-group $ResourceGroupName ` + --issuer "https://token.actions.githubusercontent.com" ` + --subject "repo:${GitHubRepo}:ref:refs/heads/main" ` + --audiences "api://AzureADTokenExchange" + + Write-Host "✅ Main branch federated credential created!" -ForegroundColor Green +} else { + Write-Host "✅ Main branch federated credential already exists" -ForegroundColor Green +} + +# Create federated credential for pull requests +if (-not $prCred) { + Write-Host "🔧 Creating federated credential for pull requests..." -ForegroundColor Blue + az identity federated-credential create ` + --name "github-pr" ` + --identity-name $IdentityName ` + --resource-group $ResourceGroupName ` + --issuer "https://token.actions.githubusercontent.com" ` + --subject "repo:${GitHubRepo}:pull_request" ` + --audiences "api://AzureADTokenExchange" + + Write-Host "✅ Pull request federated credential created!" -ForegroundColor Green +} else { + Write-Host "✅ Pull request federated credential already exists" -ForegroundColor Green +} + +Write-Host "`n🎉 OIDC setup completed successfully!" -ForegroundColor Green + +# Display GitHub secrets to set +Write-Host "`n📋 GitHub Secrets to Configure:" -ForegroundColor Cyan +Write-Host "================================" -ForegroundColor Cyan +Write-Host "AZURE_CLIENT_ID: $clientId" -ForegroundColor White +Write-Host "AZURE_TENANT_ID: $tenantId" -ForegroundColor White +Write-Host "AZURE_SUBSCRIPTION_ID: $SubscriptionId" -ForegroundColor White + +Write-Host "`n📋 GitHub Variables to Configure:" -ForegroundColor Cyan +Write-Host "==================================" -ForegroundColor Cyan +Write-Host "AZURE_ENV_NAME: dev (or your preferred environment name)" -ForegroundColor White +Write-Host "AZURE_LOCATION: $AzureLocation" -ForegroundColor White + +Write-Host "`n🔗 Setup Instructions:" -ForegroundColor Magenta +Write-Host "1. Go to your GitHub repository: https://github.com/$GitHubRepo" -ForegroundColor White +Write-Host "2. Navigate to Settings > Secrets and variables > Actions" -ForegroundColor White +Write-Host "3. Click 'New repository secret' and add each secret above" -ForegroundColor White +Write-Host "4. Click the 'Variables' tab and add each variable above" -ForegroundColor White +Write-Host "5. Commit and push your .github/workflows/*.yml files" -ForegroundColor White +Write-Host "6. Test by creating a pull request or pushing to main branch" -ForegroundColor White + +Write-Host "`n🛡️ Security Benefits of Managed Identity:" -ForegroundColor Green +Write-Host "✅ No secrets to manage or rotate" -ForegroundColor White +Write-Host "✅ Azure-managed lifecycle" -ForegroundColor White +Write-Host "✅ Integrated with Azure RBAC" -ForegroundColor White +Write-Host "✅ Short-lived tokens only" -ForegroundColor White + +Write-Host "`n💡 Pro Tips:" -ForegroundColor Cyan +Write-Host "• Use validate-oidc.ps1 to verify your setup" -ForegroundColor White +Write-Host "• Consider different managed identities for dev/staging/prod" -ForegroundColor White +Write-Host "• The managed identity is scoped to your resource group only" -ForegroundColor White + +Write-Host "`n🔍 Troubleshooting:" -ForegroundColor Yellow +Write-Host "If you get permission errors, ensure you have:" -ForegroundColor White +Write-Host "• 'Managed Identity Contributor' role in the subscription/RG" -ForegroundColor White +Write-Host "• 'User Access Administrator' role to assign roles" -ForegroundColor White +Write-Host "• Or ask an admin to run this script for you" -ForegroundColor White diff --git a/.github/scripts/setup-managed-identity-oidc.sh b/.github/scripts/setup-managed-identity-oidc.sh new file mode 100755 index 0000000..2198e26 --- /dev/null +++ b/.github/scripts/setup-managed-identity-oidc.sh @@ -0,0 +1,299 @@ +#!/bin/bash + +# Azure OIDC Setup Script using User-Assigned Managed Identity (Bash version) +# This script is perfect when you don't have permissions to create Service Principals + +set -e # Exit on any error + +# Function to print colored output +print_status() { + echo -e "\033[32m✅ $1\033[0m" +} + +print_info() { + echo -e "\033[34m🔧 $1\033[0m" +} + +print_error() { + echo -e "\033[31m❌ $1\033[0m" +} + +print_warning() { + echo -e "\033[33m⚠️ $1\033[0m" +} + +print_header() { + echo -e "\033[36m$1\033[0m" +} + +# Function to show usage +show_usage() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Required Options:" + echo " -s, --subscription-id SUBSCRIPTION_ID Azure subscription ID" + echo " -g, --resource-group RESOURCE_GROUP Resource group name" + echo " -r, --github-repo GITHUB_REPO GitHub repository (format: owner/repo)" + echo "" + echo "Optional Options:" + echo " -l, --location LOCATION Azure location (default: eastus2)" + echo " -i, --identity-name IDENTITY_NAME Managed identity name (default: github-actions-identity)" + echo " -h, --help Show this help message" + echo "" + echo "Example:" + echo " $0 -s 'your-subscription-id' -g 'rg-recipe-app' -r 'username/repo-name'" +} + +# Parse command line arguments +SUBSCRIPTION_ID="" +RESOURCE_GROUP_NAME="" +GITHUB_REPO="" +AZURE_LOCATION="eastus2" +IDENTITY_NAME="github-actions-identity" + +while [[ $# -gt 0 ]]; do + case $1 in + -s|--subscription-id) + SUBSCRIPTION_ID="$2" + shift 2 + ;; + -g|--resource-group) + RESOURCE_GROUP_NAME="$2" + shift 2 + ;; + -r|--github-repo) + GITHUB_REPO="$2" + shift 2 + ;; + -l|--location) + AZURE_LOCATION="$2" + shift 2 + ;; + -i|--identity-name) + IDENTITY_NAME="$2" + shift 2 + ;; + -h|--help) + show_usage + exit 0 + ;; + *) + print_error "Unknown option: $1" + show_usage + exit 1 + ;; + esac +done + +# Validate required parameters +if [[ -z "$SUBSCRIPTION_ID" || -z "$RESOURCE_GROUP_NAME" || -z "$GITHUB_REPO" ]]; then + print_error "Missing required parameters" + show_usage + exit 1 +fi + +print_header "🚀 Setting up OIDC with User-Assigned Managed Identity" +print_header "📋 Configuration:" +echo " Subscription: $SUBSCRIPTION_ID" +echo " Resource Group: $RESOURCE_GROUP_NAME" +echo " Managed Identity: $IDENTITY_NAME" +echo " GitHub Repository: $GITHUB_REPO" +echo " Location: $AZURE_LOCATION" + +# Check if Azure CLI is installed +if ! command -v az &> /dev/null; then + print_error "Azure CLI is not installed. Please install it first:" + echo " https://docs.microsoft.com/en-us/cli/azure/install-azure-cli" + exit 1 +fi + +# Check if user is logged in to Azure +print_info "Checking Azure authentication..." +if ! CURRENT_ACCOUNT=$(az account show --query "user.name" -o tsv 2>/dev/null); then + print_error "Please login to Azure CLI first: az login" + exit 1 +fi + +print_status "Logged in as: $CURRENT_ACCOUNT" + +# Set subscription +print_info "Setting subscription..." +az account set --subscription "$SUBSCRIPTION_ID" + +# Check if resource group exists, create if not +print_info "Checking resource group..." +if az group exists --name "$RESOURCE_GROUP_NAME" | grep -q "false"; then + print_warning "Creating resource group: $RESOURCE_GROUP_NAME" + az group create --name "$RESOURCE_GROUP_NAME" --location "$AZURE_LOCATION" + print_status "Resource group created successfully!" +else + print_status "Resource group exists: $RESOURCE_GROUP_NAME" +fi + +# Check if managed identity already exists +print_info "Checking if managed identity exists..." +if EXISTING_IDENTITY=$(az identity show --resource-group "$RESOURCE_GROUP_NAME" --name "$IDENTITY_NAME" --output json 2>/dev/null); then + print_warning "Managed identity '$IDENTITY_NAME' already exists" + print_status "Using existing managed identity" + + # Debug: Print raw output before parsing + print_info "Debugging raw output before parsing..." + echo "Raw output: $EXISTING_IDENTITY" + + # Ensure the output is valid JSON + if ! echo "$EXISTING_IDENTITY" | jq empty; then + print_error "Invalid JSON output from az identity show" + echo "Raw output: $EXISTING_IDENTITY" + + # Fallback parsing if jq fails + CLIENT_ID=$(echo "$EXISTING_IDENTITY" | grep -o '"clientId":"[^"]*' | cut -d'"' -f4) + PRINCIPAL_ID=$(echo "$EXISTING_IDENTITY" | grep -o '"principalId":"[^"]*' | cut -d'"' -f4) + else + CLIENT_ID=$(echo "$EXISTING_IDENTITY" | jq -r '.clientId') + PRINCIPAL_ID=$(echo "$EXISTING_IDENTITY" | jq -r '.principalId') + fi + + # Debug: Print extracted values after parsing + print_info "Debugging extracted values..." + echo "Extracted Client ID: $CLIENT_ID" + echo "Extracted Principal ID: $PRINCIPAL_ID" + + # Validate extracted values + if [[ -z "$CLIENT_ID" || -z "$PRINCIPAL_ID" ]]; then + print_error "Failed to extract Client ID or Principal ID" + echo "Raw output: $EXISTING_IDENTITY" + exit 1 + fi +else + # Create user-assigned managed identity + print_info "Creating user-assigned managed identity..." + IDENTITY_JSON=$(az identity create \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --name "$IDENTITY_NAME" \ + --location "$AZURE_LOCATION" \ + --output json) + + # Debug: Print raw output + echo "Raw output of az identity create: $IDENTITY_JSON" + + # Ensure the output is valid JSON + if ! echo "$IDENTITY_JSON" | jq empty; then + print_error "Invalid JSON output from az identity create" + echo "Raw output: $IDENTITY_JSON" + exit 1 + fi + + CLIENT_ID=$(echo "$IDENTITY_JSON" | jq -r '.clientId') + PRINCIPAL_ID=$(echo "$IDENTITY_JSON" | jq -r '.principalId') + + print_status "User-Assigned Managed Identity created successfully!" +fi + +# Debug: Print extracted values +print_info "Using extracted values for Client ID and Principal ID" +echo "Client ID: $CLIENT_ID" +echo "Principal ID: $PRINCIPAL_ID" + +# Get tenant ID +TENANT_ID=$(az account show --query tenantId --output tsv) + +# Check current role assignments +print_info "Checking role assignments..." +EXISTING_ROLES=$(az role assignment list --assignee "$PRINCIPAL_ID" --scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP_NAME") + +if echo "$EXISTING_ROLES" | jq -e '.[] | select(.roleDefinitionName == "Contributor")' > /dev/null; then + print_status "Contributor role already assigned" +else + # Assign Contributor role to the managed identity + print_info "Assigning Contributor role..." + az role assignment create \ + --assignee-object-id "$PRINCIPAL_ID" \ + --assignee-principal-type ServicePrincipal \ + --role "Contributor" \ + --scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP_NAME" + + print_status "Contributor role assigned successfully!" +fi + +# List existing federated credentials +print_info "Checking existing federated credentials..." +EXISTING_CREDS=$(az identity federated-credential list --identity-name "$IDENTITY_NAME" --resource-group "$RESOURCE_GROUP_NAME") + +# Check for main branch credential +if echo "$EXISTING_CREDS" | jq -e ".[] | select(.subject == \"repo:${GITHUB_REPO}:ref:refs/heads/main\")" > /dev/null; then + print_status "Main branch federated credential already exists" +else + # Create federated credential for main branch + print_info "Creating federated credential for main branch..." + az identity federated-credential create \ + --name "github-main" \ + --identity-name "$IDENTITY_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --issuer "https://token.actions.githubusercontent.com" \ + --subject "repo:${GITHUB_REPO}:ref:refs/heads/main" \ + --audiences "api://AzureADTokenExchange" + + print_status "Main branch federated credential created!" +fi + +# Check for pull request credential +if echo "$EXISTING_CREDS" | jq -e ".[] | select(.subject == \"repo:${GITHUB_REPO}:pull_request\")" > /dev/null; then + print_status "Pull request federated credential already exists" +else + # Create federated credential for pull requests + print_info "Creating federated credential for pull requests..." + az identity federated-credential create \ + --name "github-pr" \ + --identity-name "$IDENTITY_NAME" \ + --resource-group "$RESOURCE_GROUP_NAME" \ + --issuer "https://token.actions.githubusercontent.com" \ + --subject "repo:${GITHUB_REPO}:pull_request" \ + --audiences "api://AzureADTokenExchange" + + print_status "Pull request federated credential created!" +fi + +print_header "🎉 OIDC setup completed successfully!" + +# Display GitHub secrets to set +print_header "" +print_header "📋 GitHub Secrets to Configure:" +print_header "================================" +echo "AZURE_CLIENT_ID: $CLIENT_ID" +echo "AZURE_TENANT_ID: $TENANT_ID" +echo "AZURE_SUBSCRIPTION_ID: $SUBSCRIPTION_ID" + +print_header "" +print_header "📋 GitHub Variables to Configure:" +print_header "==================================" +echo "AZURE_ENV_NAME: dev (or your preferred environment name)" +echo "AZURE_LOCATION: $AZURE_LOCATION" + +print_header "" +print_header "🔗 Setup Instructions:" +echo "1. Go to your GitHub repository: https://github.com/$GITHUB_REPO" +echo "2. Navigate to Settings > Secrets and variables > Actions" +echo "3. Click 'New repository secret' and add each secret above" +echo "4. Click the 'Variables' tab and add each variable above" +echo "5. Commit and push your .github/workflows/*.yml files" +echo "6. Test by creating a pull request or pushing to main branch" + +print_header "" +print_header "🛡️ Security Benefits of Managed Identity:" +print_status "No secrets to manage or rotate" +print_status "Azure-managed lifecycle" +print_status "Integrated with Azure RBAC" +print_status "Short-lived tokens only" + +print_header "" +print_header "💡 Pro Tips:" +echo "• Use validate-oidc.sh to verify your setup" +echo "• Consider different managed identities for dev/staging/prod" +echo "• The managed identity is scoped to your resource group only" + +print_header "" +print_header "🔍 Troubleshooting:" +echo "If you get permission errors, ensure you have:" +echo "• 'Managed Identity Contributor' role in the subscription/RG" +echo "• 'User Access Administrator' role to assign roles" +echo "• Or ask an admin to run this script for you" diff --git a/.github/workflows/azd-oidc-pwsh.yml b/.github/workflows/azd-oidc-pwsh.yml new file mode 100644 index 0000000..d548540 --- /dev/null +++ b/.github/workflows/azd-oidc-pwsh.yml @@ -0,0 +1,97 @@ +name: Deploy with AZD and OIDC (PowerShell) + +on: + workflow_dispatch: + +# Required permissions for OIDC +permissions: + id-token: write + contents: read + pull-requests: write + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install azd + uses: Azure/setup-azd@v2 + + - name: Log in with Azure (OIDC) + uses: Azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + enable-oidc: true + federated-credential-provider: github + + - name: AZD Login (Headless) + shell: pwsh + run: | + Write-Host "🔑 Logging in with AZD using federated credentials..." + azd auth login ` + --client-id "$Env:AZURE_CLIENT_ID" ` + --federated-credential-provider "github" ` + --tenant-id "$Env:AZURE_TENANT_ID" ` + --no-browser + Write-Host "✅ AZD login successful" + env: + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Deploy with azd + shell: pwsh + run: | + # Set environment variables + $Env:AZURE_ENV_NAME = "${{ vars.AZURE_ENV_NAME }}" -or "dev" + $Env:AZURE_LOCATION = "${{ vars.AZURE_LOCATION }}" -or "eastus2" + $Env:AZURE_SUBSCRIPTION_ID = "${{ secrets.AZURE_SUBSCRIPTION_ID }}" + + Write-Host "🚀 Starting deployment with azd..." + Write-Host "Environment: $Env:AZURE_ENV_NAME" + Write-Host "Location: $Env:AZURE_LOCATION" + + # Initialize azd environment if it doesn't exist + try { + azd env select $Env:AZURE_ENV_NAME 2>$null + } catch { + Write-Host "📦 Creating new azd environment: $Env:AZURE_ENV_NAME" + azd env new $Env:AZURE_ENV_NAME --location $Env:AZURE_LOCATION --subscription $Env:AZURE_SUBSCRIPTION_ID + } + + # Deploy the application + if ("${{ github.event_name }}" -eq "pull_request") { + Write-Host "🔍 PR detected - running provision with preview" + azd provision --preview + } else { + Write-Host "🚀 Main branch - deploying application" + azd up --no-prompt + } + env: + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Get deployment outputs + if: github.event_name != 'pull_request' + shell: pwsh + run: | + Write-Host "📋 Getting deployment information..." + azd show --output table + + # Try to get endpoint + try { + $output = azd show --output json | ConvertFrom-Json + $endpoint = $output.services[0].endpoint + if ($endpoint) { + Write-Host "🌐 Application deployed to: $endpoint" + echo "ENDPOINT=$endpoint" >> $env:GITHUB_OUTPUT + } + } catch { + Write-Host "⚠️ Could not retrieve endpoint information" + } diff --git a/.github/workflows/azd-oidc-simple.yml b/.github/workflows/azd-oidc-simple.yml new file mode 100644 index 0000000..05f1572 --- /dev/null +++ b/.github/workflows/azd-oidc-simple.yml @@ -0,0 +1,166 @@ +name: Deploy with AZD and OIDC + +on: + workflow_dispatch: + push: + branches: + - main + pull_request: + branches: + - main + +# Required permissions for OIDC +permissions: + id-token: write + contents: read + pull-requests: write + +jobs: + deploy: + runs-on: ubuntu-latest + env: + AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} + AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} + AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} + DOTNET_CORE_VERSION: 9.0.x + AZD_DEBUG: ${{ vars.AZD_DEBUG || false }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_CORE_VERSION }} + + - name: Install azd + uses: Azure/setup-azd@v2 + + - name: Configure Azure CLI authentication + shell: bash + run: | + echo "🔑 Setting up Azure CLI authentication..." + export AZURE_CLIENT_ID="${{ vars.AZURE_CLIENT_ID }}" + export AZURE_TENANT_ID="${{ vars.AZURE_TENANT_ID }}" + export AZURE_SUBSCRIPTION_ID="${{ vars.AZURE_SUBSCRIPTION_ID }}" + + # Display last 4 characters of client ID for debugging + if [[ -n "$AZURE_CLIENT_ID" ]]; then + CLIENT_ID_LENGTH=${#AZURE_CLIENT_ID} + LAST_FOUR=${AZURE_CLIENT_ID:$CLIENT_ID_LENGTH-4:4} + echo "🔑 Using client ID ending with: $LAST_FOUR" + else + echo "⚠️ Client ID is not set" + fi + + - name: Log in with Azure (OIDC) + uses: azure/login@v2 + with: + client-id: ${{ vars.AZURE_CLIENT_ID }} + tenant-id: ${{ vars.AZURE_TENANT_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} + + - name: Set Docker Image Tag Environment Variable + run: | + # Using a descriptive tag that combines branch, run number, and short SHA + # This is the best practice for traceability and immutability. + COMMIT_SHA_SHORT=${GITHUB_SHA::7} + IMAGE_TAG="${{ github.ref_name }}-${{ github.run_number }}-${COMMIT_SHA_SHORT}" + export AZURE_CONTAINER_IMAGE_TAG=${IMAGE_TAG} + + echo "AZURE_CONTAINER_IMAGE_TAG=${IMAGE_TAG}" >> $GITHUB_ENV + echo "Generated image tag: ${IMAGE_TAG}" + + - name: Deploy with azd + shell: bash + run: | + export AZURE_ENV_NAME=${{ vars.AZURE_ENV_NAME || 'octopets' }} + export AZURE_LOCATION="${{ vars.AZURE_LOCATION || 'eastus2' }}" + export AZURE_SUBSCRIPTION_ID="${{ vars.AZURE_SUBSCRIPTION_ID }}" + export AZURE_CLIENT_ID="${{ vars.AZURE_CLIENT_ID }}" + export AZURE_TENANT_ID="${{ vars.AZURE_TENANT_ID }}" + + echo "🔍 Diagnostic information:" + echo "AZD version: $(azd version)" + echo "Current OIDC token: $(if [[ -n $ACTIONS_ID_TOKEN_REQUEST_TOKEN ]]; then echo 'Present'; else echo 'Not found'; fi)" + + echo "🚀 Starting deployment with azd..." + echo "Environment: $AZURE_ENV_NAME" + echo "Location: $AZURE_LOCATION" + echo "Azure container image tag: $AZURE_CONTAINER_IMAGE_TAG" + + azd auth login --client-id $AZURE_CLIENT_ID --federated-credential-provider github --tenant-id $AZURE_TENANT_ID + + echo "🔍 Changing working directory to ./apphost ..." + pwd + cd apphost + pwd + + if ! azd env select $AZURE_ENV_NAME 2>/dev/null; then + echo "📦 Creating new azd environment: $AZURE_ENV_NAME" + azd env new $AZURE_ENV_NAME --location $AZURE_LOCATION --subscription $AZURE_SUBSCRIPTION_ID + fi + + if [ "${{ github.event_name }}" == "pull_request" ]; then + echo "🔍 PR detected - running provision with preview" + azd provision --preview + else + echo "🚀 Main branch - deploying application" # Set container image tag before running azd up + azd env set AZURE_CONTAINER_IMAGE_TAG $AZURE_CONTAINER_IMAGE_TAG + echo "✅ Set container image tag to: $AZURE_CONTAINER_IMAGE_TAG" + + # Run azd up with the correct environment name + if [ "$AZD_DEBUG" = "true" ]; then + echo "🐞 Debug mode enabled for AZD" + azd up --no-prompt --debug + else + echo "🚀 Running AZD in normal mode" + azd up --no-prompt + fi + + # Add resource tag to container app with the image tag + echo "🏷️ Adding resource tag to container app..." + # Get the container app resource ID + RESOURCE_GROUP=$(azd env get-values | grep AZURE_RESOURCE_GROUP | cut -d '=' -f2 || echo "") + CONTAINER_APP_NAME="octopetsapi" + + if [ -z "$RESOURCE_GROUP" ]; then + echo "⚠️ Could not determine resource group from azd env get-values" + # Try alternate method to get resource group + RESOURCE_GROUP=$(az group list --query "[?contains(name,'$AZURE_ENV_NAME')].name" -o tsv | head -n 1) + echo "Alternate method found resource group: $RESOURCE_GROUP" + fi + + if [ -n "$RESOURCE_GROUP" ] && [ -n "$CONTAINER_APP_NAME" ]; then + echo "Resource Group: $RESOURCE_GROUP, Container App: $CONTAINER_APP_NAME" + + # Check if container app exists + if az containerapp show --resource-group "$RESOURCE_GROUP" --name "$CONTAINER_APP_NAME" &>/dev/null; then + echo "✅ Container app found, applying tag..." + # Echo the exact tag being set + echo "🏷️ Setting tag: CONTAINER_IMAGE_TAG=$AZURE_CONTAINER_IMAGE_TAG" on resource $CONTAINER_APP_NAME + # Tag the container app resource with the image tag + az tag update --resource-id "/subscriptions/$AZURE_SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.App/containerApps/$CONTAINER_APP_NAME" \ + --operation Merge \ + --tags "CONTAINER_IMAGE_TAG=$AZURE_CONTAINER_IMAGE_TAG" || \ + echo "⚠️ Failed to add tag to container app, but continuing workflow" + else + echo "⚠️ Container app '$CONTAINER_APP_NAME' not found in resource group '$RESOURCE_GROUP'" + # List existing container apps in the resource group + echo "Available container apps in the resource group:" + az containerapp list --resource-group "$RESOURCE_GROUP" --query "[].name" -o tsv || echo "No container apps found or failed to list" + fi + else + echo "⚠️ Could not determine container app details for tagging" + fi + fi + + - name: Get deployment outputs + if: github.event_name != 'pull_request' + shell: bash + run: | + echo "📋 Getting deployment information..." + azd show --output table || true diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml new file mode 100644 index 0000000..cc65edc --- /dev/null +++ b/.github/workflows/azure-dev.yml @@ -0,0 +1,133 @@ +# Run when commits are pushed to main +on: + # push: + # # Run when commits are pushed to mainline branch (main or master) + # # Set this to the mainline branch you are using + # branches: + # - main + # pull_request: + # branches: [main] + workflow_dispatch: + inputs: + environment: + description: "Environment to deploy to" + required: true + default: "dev" + branch: + description: "Branch to deploy from" + required: true + default: "main" + +# Set up permissions for deploying with secretless Azure federated credentials +# https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Clinux#set-up-azure-login-with-openid-connect-authentication +permissions: + id-token: write + contents: read + +jobs: + build: + runs-on: ubuntu-latest + env: + AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} + AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} + AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install azd + uses: Azure/setup-azd@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.x.x + 9.x.x + + - name: Log in with Azure (OIDC) + uses: Azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID || vars.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID || vars.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID || vars.AZURE_SUBSCRIPTION_ID }} + enable-oidc: true + federated-credential-provider: github + # Ensure additional settings for headless operation + allow-no-subscriptions: false + + - name: Verify Client ID + shell: pwsh + run: | + Write-Host "🔍 Verifying authentication configuration..." + $clientId = "${{ secrets.AZURE_CLIENT_ID || vars.AZURE_CLIENT_ID }}" + if ($clientId) { + $lastFour = $clientId.Substring($clientId.Length - 4) + Write-Host "🔑 Using client ID ending with: $lastFour" + } else { + Write-Host "⚠️ Client ID is not set" -ForegroundColor Yellow + } + + - name: Configure AZD for Azure CLI authentication + shell: pwsh + run: | + Write-Host "🔑 Setting up AZD to use Azure CLI authentication..." + + # Create the azd config directory if it doesn't exist + New-Item -Path ~/.azd -ItemType Directory -Force + + # Configure AZD to use Azure CLI authentication instead of interactive browser login + Set-Content -Path ~/.azd/config.json -Value '{"defaults":{"auth":{"useAzureCli":true}}}' + + # Verify AZD is using the correct configuration + Get-Content -Path ~/.azd/config.json + + # Set environment variables for the deployment + $env:AZURE_ENV_NAME = "${{ github.event.inputs.environment || 'dev' }}" + $env:AZURE_LOCATION = "${{ vars.AZURE_LOCATION || 'eastus2' }}" + $env:AZURE_SUBSCRIPTION_ID = "${{ secrets.AZURE_SUBSCRIPTION_ID || vars.AZURE_SUBSCRIPTION_ID }}" + + Write-Host "✅ AZD configured to use Azure CLI authentication" + Write-Host "🌍 Environment: $env:AZURE_ENV_NAME" + Write-Host "📍 Location: $env:AZURE_LOCATION" + + + - name: Provision Infrastructure + shell: pwsh + run: | + Write-Host "🔍 Diagnostic information:" + Write-Host "Azure CLI version: $(az --version | Select-Object -First 1)" + Write-Host "AZD version: $(azd version)" + Write-Host "Current account: $(az account show --query name -o tsv || echo 'Not logged in')" + Write-Host "Current OIDC token: $(if ($env:ACTIONS_ID_TOKEN_REQUEST_TOKEN) { 'Present' } else { 'Not found' })" + + # Verify Azure login status + if (-not (az account show 2>$null)) { + Write-Host "❌ Azure CLI is not logged in. The Azure/login action should have handled this." -ForegroundColor Red + exit 1 + } + + Write-Host "🚀 Provisioning infrastructure..." + cd apphost + + # Select or create environment + if (-not (azd env select $env:AZURE_ENV_NAME 2>$null)) { + Write-Host "📦 Creating new azd environment: $env:AZURE_ENV_NAME" + azd env new $env:AZURE_ENV_NAME --location $env:AZURE_LOCATION --subscription $env:AZURE_SUBSCRIPTION_ID + } + + azd provision --no-prompt + + - name: Deploy Application + shell: pwsh + run: | + Write-Host "🚀 Deploying application..." + # Ensure we're in the right directory + cd apphost + + # Deploy the application + azd deploy --no-prompt + + Write-Host "📋 Getting deployment information..." + azd show --output table +