다음을 통해 공유


중요 업무용 워크로드에 대한 보안 고려 사항

중요 업무용 워크로드는 본질적으로 보호되어야 합니다. 애플리케이션 또는 해당 인프라가 손상되면 가용성이 위험합니다. 이 아키텍처의 초점은 애플리케이션이 모든 상황에서 성능과 가용성을 유지하도록 안정성을 최대화하는 것입니다. 보안 제어는 주로 가용성 및 안정성에 영향을 주는 위협을 완화하기 위해 적용됩니다.

참고 항목

비즈니스 요구 사항에는 더 많은 보안 조치가 필요할 수 있습니다. 중요 업무용 워크로드에 대한 Azure Well-Architected Framework 보안 고려 사항의 지침에 따라 구현에서 컨트롤을 확장하는 것이 좋습니다.

ID 및 액세스 관리

애플리케이션 수준에서 이 아키텍처는 API 키 기반의 간단한 인증 체계를 일부 제한된 작업(예: 카탈로그 항목 만들기 또는 주석 삭제)에 사용합니다. 사용자 인증 및 사용자 역할과 같은 고급 시나리오는 기준 아키텍처범위를 벗어집니다.

애플리케이션에 사용자 인증 및 계정 관리가 필요한 경우 ID 및 액세스 관리에 대한 권장 사항을 따릅니다. 일부 전략에는 관리 ID 공급자 사용, 사용자 지정 ID 관리 방지 및 가능한 경우 암호 없는 인증 사용이 포함됩니다.

최소 권한 액세스 모델

사용자와 애플리케이션이 해당 기능을 수행하는 데 필요한 최소한의 액세스 수준을 얻을 수 있도록 액세스 정책을 구성합니다. 개발자는 일반적으로 프로덕션 인프라에 액세스할 필요가 없지만 배포 파이프라인에는 모든 권한이 필요합니다. Kubernetes 클러스터는 컨테이너 이미지를 레지스트리로 푸시하지 않지만 GitHub 워크플로는 푸시할 수 있습니다. 프런트 엔드 API는 일반적으로 메시지 브로커에서 메시지를 받지 않으며 백 엔드 작업자가 반드시 broker에 새 메시지를 보낼 필요는 없습니다. 이러한 결정은 워크로드에 따라 달라지고 할당하는 액세스 수준은 각 구성 요소의 기능을 반영해야 합니다.

Azure 중요 업무용 참조 구현의 예는 다음과 같습니다.

  • Azure Event Hubs에서 작동하는 각 애플리케이션 구성 요소는 수신 대기() 또는 보내기(CatalogServiceBackgroundProcessor) 권한이 있는 연결 문자열 사용합니다. 해당 액세스 수준은 모든 Pod가 해당 기능을 수행하는 데 필요한 최소 액세스 권한만 갖도록 합니다.
  • AKS(Azure Kubernetes Service) 에이전트 풀의 서비스 주체에는 Azure Key Vault의 비밀에 대한 가져오기나열 권한만 있습니다.
  • AKS Kubelet ID에는 전역 컨테이너 레지스트리에 액세스할 수 있는 AcrPull 권한만 있습니다.

관리 ID

중요 업무용 워크로드의 보안을 향상하려면 가능한 경우 연결 문자열 또는 API 키와 같은 서비스 기반 비밀을 사용하지 마세요. Azure 서비스에서 해당 기능을 지원하는 경우 관리 ID를 사용하는 것이 좋습니다.

참조 구현은 AKS 에이전트 풀("Kubelet ID")에서 서비스 할당 관리 ID 를 사용하여 전역 Azure Container Registry 및 스탬프의 키 자격 증명 모음에 액세스합니다. 적절한 기본 제공 역할은 액세스를 제한하는 데 사용됩니다. 예를 들어 이 Terraform 코드는 Kubelet ID에 역할만 AcrPull 할당합니다.

resource "azurerm_role_assignment" "acrpull_role" {
  scope                = data.azurerm_container_registry.global.id
  role_definition_name = "AcrPull"
  principal_id         = azurerm_kubernetes_cluster.stamp.kubelet_identity.0.object_id
}

비밀

가능하면 Azure 리소스에 액세스할 때 키 대신 Microsoft Entra 인증을 사용합니다. Azure Cosmos DB 및 Azure Storage같은 많은 Azure 서비스는 키 인증을 완전히 사용하지 않도록 설정하는 옵션을 지원합니다. AKS는 Microsoft Entra 워크로드 ID 지원합니다.

Microsoft Entra 인증을 사용할 수 없는 시나리오의 경우 각 배포 스탬프에는 키를 저장하는 전용 Key Vault 인스턴스가 있습니다. 이러한 키는 배포 중에 자동으로 만들어지고 Terraform을 사용하여 Key Vault에 저장됩니다. 엔드 투 엔드 환경의 개발자를 제외한 어떤 인간 운영자도 비밀과 상호 작용할 수 없습니다. 또한 Key Vault 액세스 정책은 비밀에 액세스할 수 있는 사용자 계정이 없도록 구성됩니다.

참고 항목

이 워크로드는 사용자 지정 인증서를 사용하지 않지만 동일한 원칙이 적용됩니다.

AKS 클러스터 에서 비밀 저장소 용 Key Vault 공급자를 사용하면 애플리케이션에서 비밀을 사용할 수 있습니다. CSI 드라이버는 Key Vault에서 키를 로드하고 파일로 개별 Pod에 탑재합니다.

#
# /src/config/csi-secrets-driver/chart/csi-secrets-driver-config/templates/csi-secrets-driver.yaml
#
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: azure-kv
spec:
  provider: azure
  parameters:
    usePodIdentity: "false"
    useVMManagedIdentity: "true"
    userAssignedIdentityID: {{ .Values.azure.managedIdentityClientId | quote }}
    keyvaultName: {{ .Values.azure.keyVaultName | quote }}
    tenantId: {{ .Values.azure.tenantId | quote }}
    objects: |
      array:
        {{- range .Values.kvSecrets }}
        - |
          objectName: {{ . | quote }}
          objectAlias: {{ . | lower | replace "-" "_" | quote }}
          objectType: secret
        {{- end }}

참조 구현은 Azure Pipelines와 함께 Helm을 사용하여 Key Vault의 모든 키 이름을 포함하는 CSI 드라이버를 배포합니다. 또한 드라이버는 Key Vault에서 변경되는 경우 탑재된 비밀을 새로 고치는 역할을 담당합니다.

소비자 쪽에서 두 .NET 애플리케이션은 모두 기본 제공 기능을 사용하여 파일에서 구성을 읽습니다(AddKeyPerFile).

//
// /src/app/AlwaysOn.BackgroundProcessor/Program.cs
// + using Microsoft.Extensions.Configuration;
//
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration((context, config) =>
    {
        // Load values from Kubernetes CSI Key Vault driver mount point.
        config.AddKeyPerFile(directoryPath: "/mnt/secrets-store/", optional: true, reloadOnChange: true);
        
        // More configuration if needed...
    })
    .ConfigureWebHostDefaults(webBuilder =>
    {
        webBuilder.UseStartup<Startup>();
    });

CSI 드라이버의 자동 다시 로드 조합은 reloadOnChange: true Key Vault에서 키가 변경될 때 새 값이 클러스터에 탑재되도록 하는 데 도움이 됩니다. 이 프로세스는 애플리케이션에서 비밀 회전을 보장하지 않습니다. 이 구현에서는 변경 사항을 적용하기 위해 Pod를 다시 시작해야 하는 단일 Azure Cosmos DB 클라이언트 인스턴스를 사용합니다.

사용자 지정 도메인 및 TLS

웹 기반 워크로드는 HTTPS를 사용하여 클라이언트에서 API로 또는 API에서 API로의 통신과 같은 모든 상호 작용 수준에서 맨 인 더 미들 공격을 방지해야 합니다. 만료된 인증서는 여전히 중단 및 성능 저하 환경의 일반적인 원인이므로 인증서 회전을 자동화해야 합니다.

참조 구현은 사용자 지정 도메인 이름(예: contoso.com.)이 있는 HTTPS를 완벽하게 지원합니다. 또한 적절한 구성을 환경과 prod 환경에 모두 int 적용합니다. 환경에 대한 e2e 사용자 지정 도메인을 추가할 수도 있습니다. 그러나 이 참조 구현은 Azure Front Door에서 SSL 인증서와 함께 사용자 지정 도메인을 사용하는 경우의 수명이 e2e 짧고 배포 시간이 길어지므로 사용자 지정 도메인 이름을 사용하지 않습니다.

배포의 전체 자동화를 사용하도록 설정하려면 Azure DNS 영역을 통해 사용자 지정 도메인을 관리해야 합니다. 인프라 배포 파이프라인은 Azure DNS 영역에서 CNAME 레코드를 동적으로 만들고 이러한 레코드를 Azure Front Door 인스턴스에 자동으로 매핑합니다.

Azure Front Door 관리 SSL 인증서를 사용하도록 설정하면 수동 SSL 인증서 갱신에 대한 요구 사항이 제거됩니다. TLS 1.2는 최소 버전으로 구성됩니다.

#
# /src/infra/workload/globalresources/frontdoor.tf
#
resource "azurerm_frontdoor_custom_https_configuration" "custom_domain_https" {
  count                             = var.custom_fqdn != "" ? 1 : 0
  frontend_endpoint_id              = "${azurerm_frontdoor.main.id}/frontendEndpoints/${local.frontdoor_custom_frontend_name}"
  custom_https_provisioning_enabled = true

  custom_https_configuration {
    certificate_source = "FrontDoor"
  }
}

사용자 지정 도메인으로 프로비전되지 않은 환경은 기본 Azure Front Door 엔드포인트를 통해 액세스할 수 있습니다. 예를 들어 다음과 같은 env123.azurefd.net주소에서 연결할 수 있습니다.

참고 항목

클러스터 수신 컨트롤러에서 사용자 지정 도메인은 두 경우 모두에서 사용되지 않습니다. 대신, 이러한 엔드포인트에 대해 무료 SSL 인증서를 발급할 수 있는 Let's Encrypt와 같은 [prefix]-cluster.[region].cloudapp.azure.com Azure 제공 DNS 이름이 사용됩니다.

참조 구현에서는 Jetstack을 cert-manager 사용하여 수신 규칙에 대해 Let's Encrypt의 SSL/TLS 인증서를 자동으로 프로비전합니다. Let's Encrypt에서 인증서를 요청하는 것과 같은 ClusterIssuer추가 구성 설정은 src/config/cert-manager/chart에 저장된 별도의 cert-manager-config Helm 차트를 통해 배포됩니다.

이 구현에서는 각 네임스페이스에 대한 발급자가 없도록 하는 대신 Issuer 사용합니다ClusterIssuer. 자세한 내용은 cert-manager 설명서cert-manager 릴리스 정보를 참조하세요.

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:

구성

모든 애플리케이션 런타임 구성은 비밀 및 민감하지 않은 설정을 포함하여 Key Vault에 저장됩니다. Azure 앱 Configuration과 같은 구성 저장소를 사용하여 설정을 저장할 수 있습니다. 그러나 단일 저장소를 사용하면 중요 업무용 애플리케이션의 잠재적 실패 지점 수가 줄어듭니다. 런타임 구성에 Key Vault를 사용하여 전체 구현을 간소화합니다.

키 자격 증명 모음은 배포 파이프라인에 의해 채워져야 합니다. 구현에서 필요한 값(예: 데이터베이스 연결 문자열)은 Terraform에서 직접 제공되거나 배포 파이프라인에서 Terraform 변수로 전달됩니다.

개별 환경(예: e2e, int및)의 인프라 및 prod배포 구성은 소스 코드 리포지토리의 일부인 변수 파일에 저장됩니다. 이 방법에는 다음과 같은 두 가지 이점이 있습니다.

  • 환경의 모든 변경 내용은 환경에 적용되기 전에 배포 파이프라인을 통해 추적되고 진행됩니다.
  • 배포는 분기의 코드를 기반으로 하므로 개별 e2e 환경을 다르게 구성할 수 있습니다.

한 가지 예외는 파이프라인에 대한 중요한 값의 저장입니다. 이러한 값은 비밀로 Azure DevOps 변수 그룹에 저장됩니다.

컨테이너 보안

모든 컨테이너화된 워크로드에 대한 컨테이너 이미지를 보호해야 합니다.

이 참조 구현에서는 SDK가 아닌 런타임 이미지를 기반으로 하는 Workload Docker 컨테이너를 사용하여 공간 및 잠재적인 공격 노출 영역을 최소화합니다. 다른 도구(예: ping, wget또는 curl)가 설치되어 있지 않습니다.

애플리케이션은 이미지 빌드 프로세스의 일부로 만들어진 권한이 없는 사용자 workload 로 실행됩니다.

RUN groupadd -r workload && useradd --no-log-init -r -g workload workload
USER workload

참조 구현에서는 Helm을 사용하여 개별 구성 요소를 배포하는 데 필요한 YAML 매니페스트를 패키지합니다. 이 프로세스에는 Kubernetes 배포, 서비스, Horizontal Pod 자동 크기 조정 구성 및 보안 컨텍스트가 포함됩니다. 모든 Helm 차트에는 Kubernetes 모범 사례를 따르는 기본 보안 조치가 포함됩니다.

이러한 보안 조치는 다음과 같습니다.

  • readOnlyFilesystem: 각 컨테이너의 루트 파일 시스템 / 컨테이너가 호스트 파일 시스템에 쓰지 못하도록 읽기 전용으로 설정됩니다. 이 제한은 공격자가 더 많은 도구를 다운로드하고 코드를 컨테이너에 유지하지 못하도록 방지합니다. 읽기-쓰기 액세스 권한이 필요한 디렉터리는 볼륨으로 탑재됩니다.
  • privileged: 모든 컨테이너가 권한 없는 것으로 실행되도록 설정됩니다. 권한 있는 컨테이너를 실행하면 컨테이너에 대한 모든 기능이 제공되고 디바이스 제어 그룹 컨트롤러가 적용하는 모든 제한 사항도 해제됩니다.
  • allowPrivilegeEscalation: 컨테이너 내부에서 부모 프로세스보다 더 많은 권한을 얻지 못하도록 방지합니다.

이러한 보안 조치는 가능한 경우와 같이 cert-manager 비 Microsoft 컨테이너 및 Helm 차트에 대해서도 구성됩니다. Azure Policy를 사용하여 이러한 보안 조치를 감사할 수 있습니다.

#
# Example:
# /src/app/charts/backgroundprocessor/values.yaml
#
containerSecurityContext:
  privileged: false
  readOnlyRootFilesystem: true
  allowPrivilegeEscalation: false

각 환경(예prodint: 모든 e2e 환경)에는 스탬프가 배포되는 각 지역에 대한 전역 복제가 있는 컨테이너 레지스트리의 전용 인스턴스가 있습니다.

참고 항목

이 참조 구현은 Docker 이미지의 취약성 검사를 사용하지 않습니다. 잠재적으로 GitHub Actions와 함께 컨테이너 레지스트리에 Microsoft Defender를 사용하는 것이 좋습니다.

트래픽 수신

Azure Front Door는 이 아키텍처의 글로벌 부하 분산 장치입니다. 모든 웹 요청은 적절한 백 엔드를 선택하는 Azure Front Door를 통해 라우팅됩니다. 중요 업무용 애플리케이션은 WAF(웹 애플리케이션 방화벽)와 같은 다른 Azure Front Door 기능을 활용해야 합니다.

웹 애플리케이션 방화벽

중요한 Azure Front Door 기능은 Azure Front Door가 통과하는 트래픽을 검사할 수 있도록 하기 때문에 WAF입니다. 방지 모드에서는 모든 의심스러운 요청이 차단됩니다. 구현에서 두 개의 규칙 집합이 구성됩니다. 이러한 규칙 집합은 다음과 같습니다 Microsoft_DefaultRuleSet Microsoft_BotManagerRuleSet.

WAF를 사용하여 Azure Front Door를 배포하는 경우 검색 모드로 시작하는 것이 좋습니다. 자연 고객 트래픽을 사용하여 해당 동작을 면밀히 모니터링하고 검색 규칙을 미세 조정합니다. 가양성 제거 또는 가양성 드문 경우 방지 모드로 전환합니다. 이 프로세스는 모든 애플리케이션이 다르고 일부 페이로드는 특정 워크로드에 대해 합법적인 경우에도 악의적인 것으로 간주될 수 있기 때문에 필요합니다.

라우팅

Azure Front Door를 통해 들어오는 요청만 같은 CatalogService HealthServiceAPI 컨테이너로 라우팅됩니다. Nginx 수신 구성을 사용하여 이 동작을 적용합니다. 헤더의 존재 여부와 특정 환경의 X-Azure-FDID 전역 Azure Front Door 인스턴스에 적합한 헤더인지 확인합니다.

#
# /src/app/charts/catalogservice/templates/ingress.yaml
#
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  # ...
  annotations:
    # To restrict traffic coming only through our Azure Front Door instance, we use a header check on the X-Azure-FDID.
    # The pipeline injects the value. Therefore, it's important to treat this ID as a sensitive value.
    nginx.ingress.kubernetes.io/modsecurity-snippet: |
      SecRuleEngine On
      SecRule &REQUEST_HEADERS:X-Azure-FDID \"@eq 0\"  \"log,deny,id:106,status:403,msg:\'Front Door ID not present\'\"
      SecRule REQUEST_HEADERS:X-Azure-FDID \"@rx ^(?!{{ .Values.azure.frontdoorid }}).*$\"  \"log,deny,id:107,status:403,msg:\'Wrong Front Door ID\'\"
  # ...

배포 파이프라인은 이 헤더가 제대로 채워지는 데 도움이 되지만, Azure Front Door를 통해서가 아니라 각 클러스터를 직접 검색하기 때문에 스모크 테스트에 대해 이 제한을 무시해야 합니다. 참조 구현에서는 스모크 테스트가 배포의 일부로 실행된다는 사실을 사용합니다. 이 디자인을 사용하면 헤더 값을 알 수 있고 스모크 테스트 HTTP 요청에 추가할 수 있습니다.

#
# /.ado/pipelines/scripts/Run-SmokeTests.ps1
#
$header = @{
  "X-Azure-FDID" = "$frontdoorHeaderId"
  "TEST-DATA"  = "true" # Header to indicate that posted comments and ratings are for tests and can be deleted again by the app.
}

보안 배포

운영 우수성에 대한 잘 설계된 기준 원칙을 따르려면 모든 배포를 완전히 자동화합니다. 실행을 트리거하거나 게이트를 승인하는 것 외에는 수동 단계가 필요하지 않습니다.

보안 조치를 사용하지 않도록 설정할 수 있는 악의적인 시도 또는 우발적인 잘못된 구성을 방지해야 합니다. 참조 구현은 인프라 및 애플리케이션 배포 모두에 동일한 파이프라인을 사용하므로 잠재적인 구성 드리프트가 자동으로 롤백됩니다. 이 롤백은 인프라의 무결성을 유지하고 애플리케이션 코드와 정렬하는 데 도움이 됩니다. 변경 내용은 다음 배포에서 삭제됩니다.

Terraform은 파이프라인을 실행하는 동안 배포에 중요한 값을 생성하거나 Azure DevOps가 이를 비밀로 제공합니다. 이러한 값은 역할 기반 액세스 제한으로 보호됩니다.

참고 항목

GitHub 워크플로는 비밀 값에 대해 별도의 저장소의 유사한 개념을 제공합니다. 비밀은 GitHub Actions에서 사용할 수 있는 암호화된 환경 변수입니다.

이러한 아티팩트가 잠재적으로 비밀 값이나 애플리케이션의 내부 작동에 대한 정보를 포함할 수 있으므로 파이프라인에서 생성하는 모든 아티팩트에 주의해야 합니다. 참조 구현의 Azure DevOps 배포는 Terraform 출력을 사용하여 두 개의 파일을 생성합니다. 한 파일은 스탬프용이고 한 파일은 글로벌 인프라용입니다. 이러한 파일에는 인프라를 손상시킬 수 있는 암호가 포함되어 있지 않습니다. 그러나 이러한 파일은 클러스터 ID, IP 주소, 스토리지 계정 이름, Key Vault 이름, Azure Cosmos DB 데이터베이스 이름 및 Azure Front Door 헤더 ID를 비롯한 인프라에 대한 정보를 표시하므로 중요한 것으로 간주해야 합니다.

Terraform을 사용하는 워크로드의 경우 비밀을 포함한 전체 배포 컨텍스트가 포함되어 있으므로 상태 파일을 보호하기 위해 추가적인 노력을 기울여야 합니다. 상태 파일은 일반적으로 워크로드와 별도의 수명 주기가 있어야 하며 배포 파이프라인에서만 액세스할 수 있어야 하는 스토리지 계정에 저장됩니다. 이 파일에 대한 다른 액세스 권한을 기록하고 적절한 보안 그룹에 경고를 보내야 합니다.

종속성 업데이트

애플리케이션에서 사용하는 라이브러리, 프레임워크 및 도구는 시간이 지남에 따라 업데이트됩니다. 이러한 업데이트는 종종 공격자가 시스템에 무단으로 액세스할 수 있는 보안 문제에 대한 수정 사항을 포함하기 때문에 정기적으로 완료하는 것이 중요합니다.

참조 구현에서는 NuGet, Docker, npm, Terraform 및 GitHub Actions 종속성 업데이트에 GitHub의 Dependabot을 사용합니다. dependabot.yml 구성 파일은 애플리케이션의 다양한 부분이 복잡하기 때문에 PowerShell 스크립트를 사용하여 자동으로 생성됩니다. 예를 들어 각 Terraform 모듈에는 별도의 항목이 필요합니다.

#
# /.github/dependabot.yml
#
version: 2
updates:
- package-ecosystem: "nuget"
  directory: "/src/app/AlwaysOn.HealthService"
  schedule:
    interval: "monthly" 
  target-branch: "component-updates" 

- package-ecosystem: "docker"
  directory: "/src/app/AlwaysOn.HealthService"
  schedule:
    interval: "monthly" 
  target-branch: "component-updates" 

# ... the rest of the file...
  • 업데이트는 최신 라이브러리를 갖추는 것과 오버헤드를 유지 관리 가능한 상태로 유지하는 것 사이의 절충안으로 매월 트리거됩니다. 또한 Terraform과 같은 주요 도구는 지속적으로 모니터링되며 중요한 업데이트는 수동으로 실행됩니다.
  • 끌어오기 요청(PR)은 대신 분기maincomponent-updates 대상으로 합니다.
  • Npm 라이브러리는 다음과 같은 @vue-cli도구를 지원하는 대신 컴파일된 애플리케이션으로 이동하는 종속성만 확인하도록 구성됩니다.

Dependabot은 각 업데이트에 대해 별도의 PR을 만들어 운영 팀에 부담을 줍니다. 참조 구현은 먼저 분기에서 component-updates 업데이트 일괄 처리를 수집한 다음 환경에서 테스트를 실행합니다 e2e . 이러한 테스트가 성공하면 분기를 대상으로 하는 다른 PR을 main 만듭니다.

방어 코딩

코드 오류, 오작동된 배포 및 인프라 오류를 비롯한 다양한 이유로 인해 API 호출이 실패할 수 있습니다. API 호출이 실패하는 경우 호출자 또는 클라이언트 애플리케이션은 해당 정보가 악의적 사용자에게 애플리케이션에 대한 유용한 데이터 요소를 제공할 수 있으므로 광범위한 디버깅 정보를 수신해서는 안 됩니다.

참조 구현은 실패한 응답에서 상관 관계 ID만 반환하여 이 원칙을 보여 줍니다. 예외 메시지 또는 스택 추적과 같은 실패 이유를 공유하지 않습니다. 운영자는 이 ID를 사용하고 헤더를 사용하여 Server-Location Application Insights를 사용하여 인시던트를 조사할 수 있습니다.

//
// Example ASP.NET Core middleware, which adds the Correlation ID to every API response.
//
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
   // ...

    app.Use(async (context, next) =>
    {
        context.Response.OnStarting(o =>
        {
            if (o is HttpContext ctx)
            {
                context.Response.Headers.Add("Server-Name", Environment.MachineName);
                context.Response.Headers.Add("Server-Location", sysConfig.AzureRegion);
                context.Response.Headers.Add("Correlation-ID", Activity.Current?.RootId);
                context.Response.Headers.Add("Requested-Api-Version", ctx.GetRequestedApiVersion()?.ToString());
            }
            return Task.CompletedTask;
        }, context);
        await next();
    });
    
    // ...
}

다음 단계

참조 구현을 배포하여 리소스 및 해당 구성을 완전히 이해합니다.