본문 바로가기
WIKI 기술 지식 베이스

Microsoft Entra 테넌트 데이터

원문 보기 위키 갱신

Backstage 카탈로그는 Microsoft Graph API를 통해 Microsoft Entra ID의 테넌트에서 조직 데이터(사용자와 팀)를 직접 가져오도록 설정할 수 있어요.

출처: 문서

본문

Backstage 카탈로그는 Microsoft Graph API를 통해 Microsoft Entra ID의 테넌트에서 조직 데이터(사용자와 팀)를 직접 가져오도록 설정할 수 있어요.

설치

이 패키지는 기본으로 설치되지 않으므로 @backstage/plugin-catalog-backend-module-msgraph를 백엔드 패키지에 추가해야 해요.

Backstage 루트 디렉터리에서

yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-msgraph

다음으로 app-config.yaml에 기본 구성을 추가하세요.

app-config.yaml

catalog:  providers:    microsoftGraphOrg:      default:        tenantId: ${AZURE_TENANT_ID}        user:          filter: userType eq 'member'        group:          filter: >            securityEnabled eq false            and mailEnabled eq true            and groupTypes/any(c:c+eq+'Unified')        schedule:          frequency: PT1H          timeout: PT50M

note

대규모 조직에서는 이 플러그인이 오래 걸릴 수 있으므로, 낮은 frequency/timeout을 설정하거나 첫 시도에 많은 사용자/그룹을 가져오지 않도록 주의하세요.

마지막으로 다음 줄을 추가해 백엔드를 업데이트하세요.

packages/backend/src/index.ts

backend.add(import('@backstage/plugin-catalog-backend'));backend.add(import('@backstage/plugin-catalog-backend-module-msgraph'));

대규모 테넌트를 위한 증분 수집

전체 데이터셋을 한 번에 메모리에 로드할 수 없는 아주 큰 Azure AD 테넌트의 경우, @backstage/plugin-catalog-backend-module-msgraph-incremental 패키지가 메모리 효율적인 대안을 제공해요. 사용자와 그룹을 한 페이지씩 처리하고 @odata.nextLink 커서를 유지해서, 팟 재시작 후 마지막으로 완료된 페이지부터 수집을 재개해요.

Backstage 루트 디렉터리에서

yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-incremental-ingestionyarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-msgraph-incremental

packages/backend/src/index.ts

backend.add(import('@backstage/plugin-catalog-backend'));backend.add(  import('@backstage/plugin-catalog-backend-module-incremental-ingestion'),);backend.add(  import('@backstage/plugin-catalog-backend-module-msgraph-incremental'),);

표준 모듈과 같은 catalog.providers.microsoftGraphOrg 구성을 사용해요. 증분 제공자는 다음 옵션을 지원하지 않아요: userGroupMember*와 groupIncludeSubGroups. 그것들이 필요하다면 MicrosoftGraphOrgEntityProvider를 사용하세요.

| | | MicrosoftGraphOrgEntityProvider | Incremental provider | Memory usage | Full dataset in RAM | One page at a time | Resume on restart | Starts from scratch | Resumes from cursor | userGroupMember* options | Supported | Not supported | groupIncludeSubGroups | Supported | Not supported | Suitable for large tenants | No | Yes

Microsoft Graph로 인증하기

로컬 개발

로컬 개발 환경에서는 Azure CLI나 Azure PowerShell을 설치하고 로그인해 두는 것이 좋아요. @azure/identity-vscode를 설치하면 Azure 확장이 있는 VSCode를 사용할 수도 있어요. 이들이 설정되면 자격 증명을 구성하거나 특별한 권한을 부여하지 않아도 플러그인이 Microsoft Graph API로 인증해요. 그렇게 할 수 없다면 앱 등록을 만들어야 해요.

앱 등록

다른 인증 방법이 모두 작동하지 않으면 Azure 포털에서 앱 등록을 만들 수 있어요. 기본적으로 graph 플러그인은 Microsoft Graph에 대해 다음 애플리케이션 권한(Delegated 아님)을 요구해요.

  • GroupMember.Read.All

  • User.Read.All

조직이 이 권한에 대해 관리자 동의를 요구한다면 그 동의를 받아야 해요.

ClientId/ClientSecret으로 인증할 때는 AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET 환경 변수를 설정하거나 구성에서 값을 지정할 수 있어요.

microsoftGraphOrg:  default:    ##...    clientId: 9ef1aac6-b454-4e69-9cf5-7199df049281    clientSecret: REDACTED

클라이언트 비밀이 아니라 인증서로 인증하려면 AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_CERTIFICATE_PATH 환경을 설정할 수 있어요.

관리 ID

Managed Identity를 지원하고 ID가 구성된 리소스(예: Azure App Services, Azure Container Apps)에 배포한다면 추가 구성 없이 Managed Identity가 자동으로 감지돼요. 앱에 관리 ID가 여러 개라면 Azure Identity에 어떤 ID를 사용할지 알려주기 위해 AZURE_CLIENT_ID 환경 변수를 설정해야 할 수 있어요.

관리 ID에 위의 App Registration에서 언급한 것과 같은 권한을 부여하려면 이 가이드를 따라가세요.

가져온 사용자와 그룹 필터링

기본적으로 이 플러그인은 디렉터리의 모든 활성 사용자와 모든 그룹을 가져와요. 비활성 사용자 계정(accountEnabled eq false)은 자동으로 제외돼요. 이는 필터와 검색 쿼리를 통해 더 커스터마이즈할 수 있어요. 커스텀 user.filter는 and를 사용해 기본 accountEnabled eq true 필터와 결합돼요.

그룹

검색 쿼리나 필터를 구성해 더 작은 그룹 집합을 얻을 수 있어요. filter와 search가 모두 제공되면 그룹은 수집되기 위해 둘 다 일치해야 해요.

microsoftGraphOrg:  providerId:    group:      filter: securityEnabled eq false and mailEnabled eq true and groupTypes/any(c:c+eq+'Unified')      search: '"description:One" AND ("displayName:Video" OR "displayName:Drive")'

search와/또는 filter 쿼리에 일치하는 그룹만 수집하고 싶지 않고, 일치하는 그룹의 멤버인 그룹도 수집하고 싶다면 includeSubGroups 구성을 사용할 수 있어요.

microsoftGraphOrg:  providerId:    group:      filter: securityEnabled eq false and mailEnabled eq true and groupTypes/any(c:c+eq+'Unified')      search: '"description:One" AND ("displayName:Video" OR "displayName:Drive")'      includeSubGroups: true

이 그룹들 외에도 조직을 위해 추가 그룹이 하나 생성돼요. 가져온 모든 그룹은 이 그룹의 자식이 돼요.

기본적으로 제공자는 msgraph /group 엔드포인트를 사용해 그룹을 얻지만, path 구성을 설정해 다른 엔드포인트를 사용할 수도 있어요. /microsoft.graph.group을 포함하는 모든 엔드포인트는 올바른 유형의 그룹 객체를 반환해요. 자세한 내용은 usage를 참고하세요.

사용자

사용자를 가져오는 두 가지 모드가 있어요. filter에 일치하는 모든 사용자 객체를 가져올 수 있어요. accountEnabled eq true 기본 필터는 자동으로 적용되며, 제공한 커스텀 필터와 결합돼요.

microsoftGraphOrg:  providerId:    user:      filter: userType eq 'member'

또는 특정 그룹의 멤버인 사용자를 가져올 수도 있어요. search와 filter 쿼리에 일치하는 각 그룹에 대해, 각 그룹 멤버가 가져와져요. 직접 그룹 멤버만 가져오며, transient 사용자는 가져오지 않아요.

microsoftGraphOrg:  providerId:    userGroupMember:      filter: "displayName eq 'Backstage Users'"      search: '"description:One" AND ("displayName:Video" OR "displayName:Drive")'

기본적으로 제공자는 msgraph /user 엔드포인트를 사용해 사용자를 얻지만, path 구성을 설정해 다른 엔드포인트를 사용할 수도 있어요. /microsoft.graph.user를 포함하는 모든 엔드포인트는 올바른 유형의 사용자 객체를 반환해요. 자세한 내용은 usage를 참고하세요.

path 매개변수 사용하기

기본적으로 제공자는 msgraph /group과 /user 엔드포인트를 사용해 그룹과 사용자를 얻지만, path 구성을 설정해 다른 엔드포인트를 사용할 수도 있어요. /microsoft.graph.user를 포함하는 모든 엔드포인트는 올바른 유형의 사용자 객체를, /microsoft.graph.group을 포함하는 모든 엔드포인트는 올바른 유형의 그룹 객체를 반환해요.

예시

다음 조직 구조가 주어지면 path 매개변수를 사용해 그룹 someRootGroup의 멤버인 모든 수준의 사용자와 그룹을 얻을 수 있어요.

구성은 이렇게 생겼어요.

microsoftGraphOrg:  providerId:    group:      path: /groups/{someRootGroup id}/transitiveMembers/microsoft.graph.group    user:      path: /groups/{someRootGroup id}/transitiveMembers/microsoft.graph.user

transitive members 엔드포인트를 사용하면 그룹 someRootGroup의 멤버인 모든 수준의 사용자와 그룹이 모두 반환돼요.

사용자 사진

기본적으로 사용자의 사진이 가져와져 각 사용자 엔티티에 추가돼요. 아주 큰 조직에서는 이것이 매우 오래 걸릴 수 있어 비현실적이며, loadPhotos를 false로 설정해 비활성화할 수 있어요.

microsoftGraphOrg:  providerId:    user:      filter: ...      loadPhotos: false

userGroupMember를 사용한다면 loadPhotos 구성은 여전히 users: 아래에서 search와 filters를 생략한 채 관리해야 해요.

microsoftGraphOrg:  providerId:    user:      loadPhotos: false    userGroupMember:      filter: "displayName eq 'Backstage Users'"      search: '"description:One" AND ("displayName:Video" OR "displayName:Drive")'

변환 커스터마이즈하기

커스텀 트랜스포머를 제공해 수집된 엔티티를 커스터마이즈할 수 있어요. 이것들로 내장 로직을 완전히 대체하거나, 기본 트랜스포머(defaultGroupTransformer, defaultUserTransformer, defaultOrganizationTransformer)를 사용해 조정할 수 있어요. undefined를 반환하면 엔티티를 Backstage에서 제외할 수도 있어요.

커스텀 트랜스포머를 사용할 때 반환되는 데이터를 커스터마이즈하고 싶을 수 있어요. 필요한 데이터를 얻으려면 Microsoft Graph 쿼리를 조정하는 여러 구성 옵션을 제공할 수 있어요.

microsoftGraphOrg:  providerId:    user:      expand: manager    group:      expand: member      select: ['id', 'displayName', 'description']

Provider Config Transformer 사용하기

동적 구성 확장을 사용하면 msgraph 카탈로그 플러그인이 재배포 없이 런타임에 설정을 조정할 수 있어요. 이 기능은 실시간 이벤트나 변화하는 조건에 따라 구성이 업데이트되어야 하는 시나리오에 유용해요. 예를 들어 동기화 일정, 필터, 검색 매개변수를 동적으로 조정해 성능과 응답성을 최적화할 수 있어요.

note

각 예약 수집에서 사용되지 않는 필드(예: id, schedule)를 조정하는 것은 효과가 없습니다.

warning

구성을 즉석에서 동적으로 변경하면 시스템 불안정이나 구성 오류 같은 의도하지 않은 결과가 발생할 수 있습니다. 트랜스포머가 예상대로 동작하는지 신중히 검토하세요!

예시 사용 사례:

  • 필터 확장: userGroupMember와 groupFilter 같은 필터를 동적으로 조정하세요.

  • 검색 매개변수 조정: groupSearch와 userSelect 같은 검색 매개변수를 즉석에서 변경하세요.

커스텀 트랜스포머 사용하기

트랜스포머는 microsoftGraphOrgEntityProviderTransformExtensionPoint를 확장해 구성할 수 있어요. 예시는 다음과 같아요.

packages/backend/src/index.ts

import { createBackendModule } from '@backstage/backend-plugin-api';import { microsoftGraphOrgEntityProviderTransformExtensionPoint } from '@backstage/plugin-catalog-backend-module-msgraph/alpha';import {  myUserTransformer,  myGroupTransformer,  myOrganizationTransformer,  myProviderConfigTransformer,} from './transformers';backend.add(  createBackendModule({    pluginId: 'catalog',    moduleId: 'microsoft-graph-extensions',    register(env) {      env.registerInit({        deps: {          microsoftGraphTransformers:            microsoftGraphOrgEntityProviderTransformExtensionPoint,        },        async init({ microsoftGraphTransformers }) {          microsoftGraphTransformers.setUserTransformer(myUserTransformer);          microsoftGraphTransformers.setGroupTransformer(myGroupTransformer);          microsoftGraphTransformers.setOrganizationTransformer(            myOrganizationTransformer,          );          microsoftGraphTransformers.setProviderConfigTransformer(            myProviderConfigTransformer,          );        },      });    },  }),);

myUserTransformer, myGroupTransformer, myOrganizationTransformer, myProviderConfigTransformer 트랜스포머 함수는 아래 섹션의 예시에서 가져온 거예요.

트랜스포머 예시

다음은 각 종류의 트랜스포머에 대한 예시예요. 이것들을 위한 transformers.ts 파일을 packages/backend/src 폴더에 만드는 것을 권장해요.

먼저, 각 종류의 트랜스포머에 대해 기본 트랜스포머를 그대로 전달하는 함수가 있는 파일의 기본 구조를 설정해 봅시다.

packages/backend/src/extensions/transformers.ts

import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';import {  defaultGroupTransformer,  defaultUserTransformer,  defaultOrganizationTransformer,  microsoftGraphOrgEntityProviderTransformExtensionPoint,  MicrosoftGraphProviderConfig,} from '@backstage/plugin-catalog-backend-module-msgraph';import { GroupEntity, UserEntity } from '@backstage/catalog-model';import { createBackendModule } from '@backstage/backend-plugin-api';// The Group transformer transforms Groups that are ingested from MS Graphexport async function myGroupTransformer(  group: MicrosoftGraph.Group,  groupPhoto?: string,): Promise<GroupEntity | undefined> {  const backstageGroup = await defaultGroupTransformer(group, groupPhoto);  return backstageGroup;}// The User transformer transforms Users that are ingested from MS Graphexport async function myUserTransformer(  graphUser: MicrosoftGraph.User,  userPhoto?: string,): Promise<UserEntity | undefined> {  const backstageUser = await defaultUserTransformer(graphUser, userPhoto);  return backstageUser;}// The Organization transformer transforms the root MS Graph Organization into a Groupexport async function myOrganizationTransformer(  graphOrganization: MicrosoftGraph.Organization,): Promise<GroupEntity | undefined> {  const backstageOrg = await defaultOrganizationTransformer(graphOrganization);  return backstageOrg;}// The Provider Config transformer enables modification of the plugin configexport async function myProviderConfigTransformer(  provider: MicrosoftGraphProviderConfig,): Promise<MicrosoftGraphProviderConfig> {  return provider;}// Wrapping these functions in a Module allows us to inject them into the Catalog plugin easilyexport default createBackendModule({  pluginId: 'catalog',  moduleId: 'msgraph-org',  register(reg) {    reg.registerInit({      deps: {        microsoftGraphTransformers:          microsoftGraphOrgEntityProviderTransformExtensionPoint,      },      async init({ microsoftGraphTransformers }) {        // Set th... [truncated]

이제 각 제공자를 우리의 필요에 맞게 커스터마이즈해 봅시다.

이 Group Transformer 예시는 기본 로직을 완전히 제거하고 커스텀 로직으로 대체해요.

export async function myGroupTransformer(  group: MicrosoftGraph.Group,  groupPhoto?: string,): Promise<GroupEntity | undefined> {  const backstageGroup = await defaultGroupTransformer(group, groupPhoto);  return backstageGroup;  // All of our groups are prefixed with the organisational unit: 'Engineering - Team A'  // We want to drop the org unit from the group name and use it for the namespace instead  const groupNameArr = group.displayName.split(' - ');  const displayName = groupNameArr[1];  // Standardise name and namespace by replacing spaces with hyphens and converting to lowercase  const namespace = groupNameArr[0].replace(' ', '-').toLowerCase();  const groupName = groupNameArr[1].replace(' ', '-').toLowerCase();  return {    apiVersion: 'backstage.io/v1alpha1',    kind: 'Group',    metadata: {      name: groupName,      description: group.description,      annotations: {},    },    spec: {      type: 'team',      displayName: displayName,      email: group.mail,      children: [],    },  };}

이 User Transformer 예시는 내장 로직을 사용하면서도 사용자 이름을 수정하고 설명을 설정해요.

export async function myUserTransformer(  graphUser: MicrosoftGraph.User,  userPhoto?: string,): Promise<UserEntity | undefined> {  const backstageUser = await defaultUserTransformer(graphUser, userPhoto);  // Make sure the default transformer returned an entity  if (backstageUser) {    // Update the description to make it obvious where this entity came from    backstageUser.metadata.description =      'Loaded from Microsoft Entra ID via MyCustomUserTransformer';    // The default transformer sets the username to the email address with invalid characters subbed out: 'user_domain.com'    // Set the username to the local part of the email address in lowercase without the domain    const newName = backstageUser.metadata.name.split('_')[0].toLowerCase();    backstageUser.metadata.name = newName;    return backstageUser;  }  return undefined;  return backstageUser;}

이 Organization Transformer 예시는 undefined를 반환해 조직 그룹을 완전히 제거해요.

export async function myOrganizationTransformer(  graphOrganization: MicrosoftGraph.Organization,): Promise<GroupEntity | undefined> {  const backstageOrg = await defaultOrganizationTransformer(graphOrganization);  return backstageOrg;  // The org transformer creates a group to be used as the base of the relationship tree for groups  // We don't need this to be created, so return undefined instead of an entity  return undefined;}

이 Config Transformer 예시는 그룹 필터를 확장해 'azure-group-a'도 포함시켜요.

export async function myProviderConfigTransformer(  provider: MicrosoftGraphProviderConfig,): Promise<MicrosoftGraphProviderConfig> {  // The filter in our config file relies on a property that has been intermittantly causing this important group to fail ingestion  // Ensure the group is always discovered by the filter  if (!provider.groupFilter?.includes('azure-group-a')) {    provider.groupFilter = `${provider.groupFilter} or displayName eq 'azure-group-a'`;  }  return provider;}

이제 우리가 만든 새 모듈을 백엔드에 추가하기만 하면 돼요.

packages/backend/src/index.ts

// Your file will have more than this in itconst backend = createBackend();...backend.add(import('./extensions/transformers'));...backend.start();

문제 해결

데이터가 없음

먼저 로그에서 Reading msgraph users and groups 메시지를 확인하세요. 안 보이면 제공자를 등록했는지, 일정이 유효한지 확인하세요.

Read 0 msgraph users and 0 msgraph groups 로그 항목이 보이면 search와 filter 인수를 확인하세요.

시작 메시지(Reading msgraph users and groups)는 보이는데 종료 메시지(Read X msgraph users and Y msgraph groups)는 보이지 않는다면, 데이터 양이 많아 작업이 오래 걸릴 가능성이 커요. 기본 동작은 모든 사용자와 그룹을 가져오는 것으로, 보통 필요보다 많은 데이터예요. 더 작은 데이터 집합을 가져와 보세요(예: filter: displayName eq 'John Smith').

인증 / 토큰 오류

Troubleshooting Azure Identity Authentication Issues를 참고하세요.

Microsoft Graph에서 사용자 읽기 오류: Authorization_RequestDenied - 작업을 완료할 권한이 부족합니다

  • 앱 등록이나 관리 ID에 필요한 모든 권한을 부여했는지 확인하세요

  • 권한이 Delegated가 아니라 Application 권한인지 확인하세요

  • 조직이 "관리자 동의"를 요구하도록 구성했다면 애플리케이션 권한에 대해 동의가 부여되었는지 확인하세요

  • 그룹 쿼리가 Microsoft Teams 그룹을 반환한다면 추가 권한(예: Team.ReadBasic.All, TeamMember.Read.All)을 부여해야 할 수 있어요

  • 추가 select나 expand 필드를 추가했다면 그것들에 추가 권한이 필요할 수 있어요

더 알아보기 (Learn more)