paint-brush
Azure DevOps 中权限、权限和访问级别的自动化by@socialdiscoverygroup
12,562
12,562

Azure DevOps 中权限、权限和访问级别的自动化

当业务快速增长时,DevOps 工程师面临着将所有用于 CI/CD 的系统中的流程自动化的挑战,包括手动访问管理。此类例行任务极大地减慢了客户请求的解决速度。在本文中,Social Discovery Group 团队分享了用于自动化 Azure DevOps 中的权利、权限和访问级别的自定义解决方案。
featured image - Azure DevOps 中权限、权限和访问级别的自动化
Social Discovery Group HackerNoon profile picture
0-item

当业务快速增长时,DevOps 工程师面临着将所有用于 CI/CD 的系统中的流程自动化的挑战,包括手动访问管理。当您有一个小团队、最多 10 个版本(每个版本有 3 个阶段)和 10 个变量组时,此类例行任务可能是可以接受的。


然而,想象一下,如果您的团队发展到数百人,需要手动操作多达 500 个版本,每个版本有 10 个阶段和 200 个变量组。在这种情况下,手动访问管理会大大减慢客户请求的解决速度。


现阶段,每个企业都面临着高效自动化的需求。


在 Social Discovery Group,我们依靠 Azure DevOps 作为主要云技术。然而,目前它缺乏自动化权限、权限和访问级别的功能。为了及时满足用户需求,我们决定寻找一些自定义解决方案并自动分配权限。

如何在 Azure DevOps 中实现自动化

在本文中,我们将分享一些关于如何在 Azure DevOps 中自动化某些日常任务的好方法,重点是:


  • 使用 Azure Pipeline 根据掩码向组或用户分配变量组的权限。


  • 将组或用户权限分配给整个发布组的特定阶段,使用 Azure Pipeline 分类为目录组。

使用的技术堆栈:Azure 服务、Azure DevOps、Bash、Azure CLI

目前,Azure 提供了 3 种分配权限的选项:使用站点的图形界面、使用 az DevOps CLI 和 API 请求。第一个选项是最直接、最快捷的;然而,随着公司规模的扩大以及 CI/CD 管道和变量数量的增加,手动分配权限变得非常耗时。


授予权限的第二个选项在功能上受到限制;换句话说,用户不能超越特定团队提供的内容。通过 REST API 的第三个选项是目前最通用的。让我们考虑一个具体的示例,其中我们有许多具有多个阶段的发布管道:


具有多个阶段的发布管道


具有多个阶段的发布管道


每个部署阶段都需要特定用户/组的许可(所谓的部署前批准功能)。当需要在给定的审批者列表中添加或删除特定用户或组时,就会出现问题。


随着公司规模的扩大,当管道和阶段的数量增加N倍时,常规情况就成为问题,而解决这个问题只能通过API请求。从我们的角度来看,该实现是这样的:


 " 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' "


当创建大量变量组时,库部分会出现类似的问题,其中使用特定单词来指示与某事物的隶属关系。让我们分解一个示例,其中创建了多个包含单词“SERVICES”的变量组,以表示它们与服务相关变量的从属关系。


还要检查特定组需要以特定权限访问所有变量的场景。


当有多个变量组时的库部分


这种常见情况的解决方案也只能通过 REST API 实现:


 " 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' "


通过自动化这些流程,我们对 Azure DevOps 中的权利和权限结构有了更详细的了解,从而增强了项目的安全性。


我们现在拥有适用于各种任务的稳定且多功能的管道,加快了用户请求的处理时间并提高了执行质量。


此外,我们的整体系统开发速度和质量也得到了提高,因为我们将更多的时间分配给其他 CI/CD 任务,而不是常规操作。


我们投入了大量精力来自动化日常任务、探索和分析各种实施选项、调试和完善。拥有现成的管道和脚本非常方便,因为它们可以在几秒钟内完成任务。