비즈니스가 빠르게 성장할 때 DevOps 엔지니어는 수동 액세스 관리를 포함하여 CI/CD에 사용되는 모든 시스템의 프로세스를 자동화해야 하는 과제에 직면합니다. 이러한 일상적인 작업은 소규모 팀, 각각 3단계의 릴리스, 10개의 가변 그룹이 있는 경우 허용될 수 있습니다.
그러나 팀이 각각 10개의 단계와 200개의 변수 그룹이 있는 최대 500개의 릴리스를 수동으로 작업해야 하는 수백 명의 사용자로 성장한다고 상상해 보십시오. 이 경우 수동 액세스 관리로 인해 고객 요청 해결 속도가 크게 느려집니다.
이 단계에서 모든 비즈니스는 효율적인 자동화가 필요합니다.
Social Discovery Group에서는 Azure DevOps를 기본 클라우드 기술로 사용합니다. 그러나 현재는 권한, 사용 권한 및 액세스 수준을 자동화하는 기능이 부족합니다. 사용자 요구 사항을 신속하게 해결하기 위해 몇 가지 맞춤형 솔루션을 찾고 권한 할당을 자동화하기로 결정했습니다.
이 문서에서는 다음 사항에 중점을 두고 Azure DevOps에서 특정 일상적인 작업을 자동화하는 방법에 대한 몇 가지 유용한 정보를 공유합니다.
현재 Azure는 권한 할당을 위해 사이트의 그래픽 인터페이스 사용, 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 작업에 더 많은 시간을 할당함으로써 전반적인 시스템 개발 속도와 품질이 향상되었습니다.
우리는 일상적인 작업을 자동화하고, 다양한 구현 옵션을 탐색 및 분석하고, 디버그 및 개선하기 위해 상당한 노력을 기울였습니다. 이미 만들어진 파이프라인과 스크립트를 가지고 있으면 몇 초 만에 작업을 완료할 수 있으므로 매우 편리합니다.