Lorsque l'entreprise connaît une croissance rapide, un ingénieur DevOps est confronté au défi d'automatiser les processus dans tous les systèmes utilisés pour le CI/CD, y compris la gestion des accès manuels. De telles tâches de routine peuvent être acceptables lorsque vous disposez d'une petite équipe, jusqu'à 10 versions comportant 3 étapes chacune et 10 groupes variables.
Cependant, imaginez que votre équipe s'agrandisse et compte des centaines de personnes devant gérer manuellement jusqu'à 500 versions, chacune comportant 10 étapes et 200 groupes variables. Dans ce cas, la gestion manuelle des accès ralentit considérablement la résolution des demandes des clients.
À ce stade, chaque entreprise est confrontée à la nécessité d’une automatisation efficace.
Au sein de Social Discovery Group, nous nous appuyons sur Azure DevOps comme principale technologie cloud. Cependant, il lui manque actuellement la fonctionnalité permettant d’automatiser les droits, les autorisations et les niveaux d’accès. Pour répondre rapidement aux besoins des utilisateurs, nous avons décidé de trouver des solutions personnalisées et d'automatiser l'attribution des autorisations.
Dans cet article, nous partagerons quelques bonnes idées sur la façon d'automatiser certaines tâches de routine dans Azure DevOps, en nous concentrant sur :
Actuellement, Azure propose 3 options pour attribuer des autorisations : à l'aide de l'interface graphique du site, à l'aide de l'az DevOps CLI et des requêtes API. La première option est la plus simple et la plus rapide ; Cependant, à mesure que l'entreprise évolue et que le nombre de pipelines et de variables CI/CD augmente, l'attribution manuelle d'autorisations prend du temps.
La deuxième option pour accorder des autorisations est limitée en fonctionnalité ; en d’autres termes, l’utilisateur ne peut pas aller au-delà de ce que propose une équipe spécifique. La troisième option via l'API REST est la plus polyvalente pour le moment. Prenons un exemple spécifique dans lequel nous disposons de nombreux pipelines de versions comportant plusieurs étapes :
Chaque étape de déploiement nécessite l'autorisation d'un utilisateur/groupe spécifique (ce que l'on appelle la fonction d'approbations préalables au déploiement). Un problème apparaît ici lorsqu'un utilisateur ou un groupe spécifique doit être ajouté ou supprimé d'une liste donnée d'approbateurs.
Au fur et à mesure que l'entreprise se développe, lorsque le nombre de pipelines et d'étapes augmente de N fois, le cas de routine devient un problème, et la solution à ce problème n'est possible que via des requêtes API. Voici à quoi ressemble la mise en œuvre de notre côté :
" pool: vmImage: ubuntu-latest parameters: - name: folder_names type: string default: '\' - name: stage_names type: string default: 'Dev' - name: group_names type: string default: 'Devops' variables: - name: organization value: ORGANIZATION - name: project value: PROJECT - name: pat Value: PAT steps: - bash: | export organization="$(organization)" export project="$(project)" export pat="$(pat)" export folder_names='${{ parameters.folder_names }}' export stage_names='${{ parameters.stage_names }}' export group_names='${{ parameters.group_names }}' export pipeline_name_array=() export stage_name_array=() export group_name_array=() IFS=',' read -ra folder_name_array <<< "$folder_names" IFS=',' read -ra stage_name_array <<< "$stage_names" IFS=',' read -ra group_name_array <<< "$group_names" # Make a GET request to retrieve the release pipelines within the specified project pipeline_url="https://vsrm.dev.azure.com/$organization/$project/_apis/release/definitions?api-version=7.0" pipeline_response=$(curl -s -u ":$pat" "$pipeline_url") # Check for errors in the response if [[ "$pipeline_response" == *"error"* ]]; then echo "Error fetching release pipeline data." exit 1 fi pipeline_names=($(echo "$pipeline_response" | jq -r '.value[].name')) pipeline_ids=($(echo "$pipeline_response" | jq -r '.value[].id')) for ((i=0; i<${#pipeline_names[@]}; i++)); do pipeline_name="${pipeline_names[i]}" pipeline_id="${pipeline_ids[i]}" # Make a GET request to retrieve the release pipeline details pipeline_detail_url="https://vsrm.dev.azure.com/$organization/$project/_apis/release/definitions/$pipeline_id?api-version=7.0" pipeline_detail_response=$(curl -s -u ":$pat" "$pipeline_detail_url") # Extract the releaseDefinition.path from the pipeline details pipeline_path=$(echo "$pipeline_detail_response" | jq -r '.path') # Check if the pipeline_path matches any of the specified folder_names for folder_name in "${folder_name_array[@]}"; do if [[ "$pipeline_path" == *"$folder_name"* ]]; then # Make a GET request to retrieve all releases for the specified pipeline releases_url="https://vsrm.dev.azure.com/$organization/$project/_apis/release/releases?api-version=7.0&definitionId=$pipeline_id" releases_response=$(curl -s -u ":$pat" "$releases_url") # Extract the release names release_names=$(echo "$releases_response" | jq -r '.value[].id') release_definition_url="https://vsrm.dev.azure.com/$organization/$project/_apis/release/definitions/$pipeline_id?api-version=7.0" release_definition_response=$(curl -s -u ":$pat" "$release_definition_url") # Iterate through each group name for group_name in "${group_name_array[@]}"; do # Make a GET request to retrieve the list of groups groups_response=$(curl -s -u ":$pat" "https://vssps.dev.azure.com/$organization/_apis/graph/groups?api-version=7.1-preview.1") # Find the origin_id for the specified group name origin_id=$(echo "$groups_response" | jq -r ".value[] | select(.displayName == \"$group_name\") | .originId") if [ -z "$origin_id" ]; then echo "Group '$group_name' not found or origin_id not available for release '$release_name'." else # Iterate through each stage name for stage_name in "${stage_name_array[@]}"; do # Construct the JSON structure for the new approval new_approval='{ "rank": 1, "isAutomated": false, "isNotificationOn": false, "approver": { "id": "'"$origin_id"'" } }' # Use jq to update the JSON structure for the specified stage updated_definition=$(echo "$release_definition_response" | jq --argjson new_approval "$new_approval" '.environments |= map(if .name == "'"$stage_name"'" then .preDeployApprovals.approvals += [$new_approval] else . end)') # Make a PUT request to update the release definition for the specified stage put_response=$(curl -s -u ":$pat" -X PUT -H "Content-Type: application/json" -d "$updated_definition" "$release_definition_url") release_definition_response=$(curl -s -u ":$pat" "$release_definition_url") # Check if the update was successful if [[ "$put_response" == *"The resource could not be found."* ]]; then echo "Error updating release definition for stage '$stage_name' and group '$group_name'." else echo "Pre-deployment approval added successfully to stage '$stage_name' for group '$group_name' in '$pipeline_id'." fi done fi done fi done done displayName: 'PreDeployApprovals' "
Un problème similaire se pose avec la section Bibliothèque lorsque de nombreux groupes de variables sont créés, dans lesquels un mot spécifique est utilisé pour indiquer une affiliation à quelque chose. Décomposons un exemple dans lequel plusieurs groupes de variables sont créés contenant le mot « SERVICES » pour indiquer leur affiliation avec des variables liées au service.
Et examinez également un scénario dans lequel un groupe spécifique a besoin d'accéder à toutes les variables avec certaines autorisations.
La solution à ce cas courant est également possible uniquement via l'API REST :
" variables: - name: organization value: ORGANIZATION - name: project value: PROJECT - name: pat value: PAT parameters: - name: searchWord type: string default: 'SERVICES' - name: UserOrGroupName type: string default: 'Devops' - name: UserRights type: string default: 'Administrator' steps: - bash: | export organization="$(organization)" export project="$(project)" export pat="$(pat)" export userOrGroupName='${{ parameters.UserOrGroupName }}' export UserRights='${{ parameters.UserRights }}' export searchWord='${{ parameters.searchWord }}' # Perform the API request and store the JSON response in a variable response=$(curl -s -u ":$pat" "https://dev.azure.com/$organization/$project/_apis/distributedtask/variablegroups?api-version=6.0") # Initialize an empty array to store matching JSON objects matching_json=() # Loop through the JSON objects and append matching objects to the array while read -r json; do if [[ $(echo "$json" | jq -r '.name' | grep "$searchWord") ]]; then matching_json+=("$json") fi done < <(echo "$response" | jq -c '.value[]') # Iterate through the matching variable groups and assign permissions for group in "${matching_json[@]}"; do # Extract the variable group ID and name variableGroupId=$(echo "$group" | jq -r '.id') variableGroupName=$(echo "$group" | jq -r '.name') # Determine the type of userOrGroupName (username or group name) if [[ $myString != *'@'* ]]; then # If userOrGroupName matches the username format, it's treated as a username roleName=$UserRights assignType="group" groupsResponse=$(curl -s -u ":$pat" "https://vssps.dev.azure.com/$organization/_apis/graph/groups?api-version=6.0-preview.1") #groupOriginId=$(curl -s -u ":$pat" "https://vssps.dev.azure.com/$organization/_apis/identities?searchFilter=$userOrGroupName&api-version=6.0" | jq -r '.value[0].originId') groupOriginId=$(echo "$groupsResponse" | jq -r ".value[] | select(.displayName == \"$userOrGroupName\") | .originId") # Get the user's originId using Azure DevOps REST API #userOriginId=$(curl -s -u ":$pat" "https://vssps.dev.azure.com/$organization/_apis/identities?searchFilter=$userOrGroupName&api-version=6.0" | jq -r '.value[0].principalName') else # Otherwise, it's treated as a group name roleName=$UserRights assignType="user" userOriginId=$(curl -s -u ":$pat" "https://vssps.dev.azure.com/$organization/_apis/identities?searchFilter=$userOrGroupName&api-version=6.0" | jq -r '.value[0].principalName') # Get the group's originId using Azure DevOps REST API #groupOriginId=$(curl -s -u ":$pat" "https://vssps.dev.azure.com/$organization/_apis/identities?searchFilter=$userOrGroupName&api-version=6.0" | jq -r '.value[0].originId') fi # Construct the API URL for assigning permissions apiUrl="https://dev.azure.com/$organization/_apis/securityroles/scopes/distributedtask.variablegroup/roleassignments/resources/19802563-1b7b-43d6-81e0-16cf29d68c0d$"$variableGroupId"?api-version=5.1-preview" # Assign permissions based on the userOrGroupName type if [ "$assignType" == "group" ]; then # Assign permissions to a group # Construct the request body with group's originId requestBody="[{ \"roleName\": \"$roleName\", \"userId\": \"$groupOriginId\" }]" else # Assign permissions to a user # Construct the request body with the user's uniqueName requestBody="[{ \"roleName\": \"$roleName\", \"uniqueName\": \"$userOrGroupName\" }]" fi # Execute the REST API call to assign permissions response=$(curl -s -u ":$pat" -X PUT -H "Content-Type: application/json" -d "$requestBody" "$apiUrl") # Check if the permissions were successfully assigned if [[ "$response" == *"error"* ]]; then echo "Error assigning permissions to $assignType '$userOrGroupName' in variable group '$variableGroupName'." else echo "Permissions assigned successfully to $assignType '$userOrGroupName' in variable group '$variableGroupName'." fi done displayName: 'variable-groups' "
En automatisant ces processus, nous avons acquis une compréhension plus détaillée des structures de droits et d'autorisations dans Azure DevOps, améliorant ainsi la sécurité du projet.
Nous disposons désormais de pipelines stables et polyvalents pour un large éventail de tâches, accélérant le temps de traitement des demandes des utilisateurs et améliorant leur qualité d’exécution.
De plus, la vitesse et la qualité globales du développement de notre système se sont améliorées car nous consacrons plus de temps à d'autres tâches CI/CD, nous éloignant ainsi des actions de routine.
Nous avons déployé des efforts considérables pour automatiser les tâches quotidiennes, explorer et analyser diverses options de mise en œuvre, déboguer et affiner. Disposer de pipelines et de scripts prêts à l'emploi est extrêmement pratique, car ils peuvent accomplir des tâches en quelques secondes.