Calculating DTU in AWS RDS for MSSQL

On-Prem이나 VM 형태로 사용하던 MSSQL Server를 Azure SQL로 Migration을 한다고 가정해보자. 기존에 사용하던 처리 성능에 맞춰 Cloud 상에 알맞은 수준의 리소스를 준비하고 옮겨야 할 것이다.

기존에 사용하던 SQL Server는 CPU와 Memory, Disk I/O등 Hardware적인 요소로 처리 성능을 나타낸다. 하지만 Azure SQL에서는 DTU(Database Throughput Unit)라는 단위를 사용하여 처리 성능을 나타내기 때문에 성능 비교가 쉽지가 않다.

DTU는 Hardware 성능 요소인 CPU, Disk I/O와 Database Log flush 발생양을 이용하여서 계산해낸 단위이다. 때문에 MSSQL Server에서 각 Metric들을 추출해 낼 수 있다면, DTU를 계산해 낼 수 있다. DTU 계산은 이미 Azure DTU Calculator라는 계산기가 제공되고 있기 때문에 추출해낸 Metric 값들을 설명대로 잘 넣어 주기만 하면 쉽게 확인해 볼 수 있다.

Metric을 추출하는 방법은 대상이 되는 SQL Server에서 DTU Calculator에서 제공하는 PowerShell Script를 관리자 권한으로 실행시켜 주기만 하면 된다. Script는 CPU, Disk I/O, Database Log flush에 대한 Metric들을 DTU Calculator에서 요구하는 형식으로 추출해준다. 때문에 일반적으로 On-Prem이나 VM 형태로 MSSQL Server를 사용하고 있다면, 계산해 내는데 별다른 어려움이 없을 것이다.

하지만 AWS RDS for MSSQL 같은 경우 MSSQL Server를 AWS에서 Cloud Service로 제공하기 위해 Wrapping한 서비스다보니 SQL Server에 대한 관리자 권한을 얻을 수가 없다. 이러한 이유 때문에 DTU Calculator에서 제공하는 PowerShell Script를 사용할 수 가 없다.

이런 경우, DTU 계산에 필요한 Metric들을 AWS에서 별도로 추출해야 한다. AWS에는 Cloudwatch라는 서비스를 제공하고 있는데, AWS 서비스들에 대한 Performance Metric을 기록하고 제공하는 역할을 한다.

Cloudwatch에서 metric을 추출하기 위해서는 AWS CLI를 사용하는 것이 편리하다. AWS Console의 우측 상단에 있는 CLI 실행 아이콘을 눌러 AWS CLI를 실행해 보자.

AWS CLI가 실행 되었다면, 아래 조회 명령어(list-metrics)를 실행시켜 제공되는 metric들에 대한 정보를 확인해 보자.

aws cloudwatch list-metrics --namespace AWS/RDS --dimensions Name=DBInstanceIdentifier,Value=[RDS for MSSQL Name]

잘 실행되었다면, cloudwatch에서 제공하는 Metric 리스트들을 확인 할 수 있다. 리스트 중 DTU계산에 필요한 CPU Processor Time과 Disk Reads/sec, Writes/sec에 값에 해당 하는 Metric 다음과 같다. (참고로, Log Bytes Flushed/sec에 해당하는 metric은 제공되지 않는다.)

{
    "Namespace": "AWS/RDS",
    "MetricName": "ReadIOPS",
    "Dimensions": [
        {
            "Name": "DBInstanceIdentifier",
            "Value": "[RDS for MSSQL Name]"
        }
    ]
},
{
    "Namespace": "AWS/RDS",
    "MetricName": "WriteIOPS",
    "Dimensions": [
        {
            "Name": "DBInstanceIdentifier",
            "Value": "[RDS for MSSQL Name]"
        }
    ]
},
{
    "Namespace": "AWS/RDS",
    "MetricName": "CPUUtilization",
    "Dimensions": [
        {
            "Name": "DBInstanceIdentifier",
            "Value": "[RDS for MSSQL Name]"
        }
    ]
}

이제, 필요한 metric 정보에 대해서 확인 하였으니 수집해보자. 아래 metric 수집 명령어(get-metric-statistics)를 사용하면 수집된 metric정보가 csv파일로 저장된다.

aws cloudwatch get-metric-statistics --namespace AWS/RDS --dimensions Name=DBInstanceIdentifier,Value=[RDS for MSSQL Name] --metric-name=CPUUtilization --period 3600 --statistics Average --start-time 2021-01-01T00:00:00.000Z --end-time 2021-02-01T00:00:00.000Z | jq -r '.Datapoints[] | [.Timestamp, .Average, .Unit] | @csv' | cat > cpu.csv

aws cloudwatch get-metric-statistics --namespace AWS/RDS --dimensions Name=DBInstanceIdentifier,Value=[RDS for MSSQL Name] --metric-name=ReadIOPS --period 3600 --statistics Average --start-time 2021-01-01T00:00:00.000Z --end-time 2021-02-01T00:00:00.000Z | jq -r '.Datapoints[] | [.Timestamp, .Average, .Unit] | @csv' | cat > read.csv

aws cloudwatch get-metric-statistics --namespace AWS/RDS --dimensions Name=DBInstanceIdentifier,Value=[RDS for MSSQL Name] --metric-name=WriteIOPS --period 3600 --statistics Average --start-time 2021-01-01T00:00:00.000Z --end-time 2021-02-01T00:00:00.000Z | jq -r '.Datapoints[] | [.Timestamp, .Average, .Unit] | @csv' | cat > write.csv

생성한 csv 파일은 AWS CLI Storage에 저장 되어있다. 우측 상단의 Actions > Download File 메뉴를 사용하면 로컬 환경으로 다운 받을 수 있다.

3개의 파일 모두 다운 받아서 DTU 계산에 필요한 형식으로 맞춰준다.

  • % Processor Time = CPUUtilization
  • Disk Reads/sec = ReadIOPS
  • Disk Writes/sec = WriteIOPS
  • Log Bytes Flushed/sec에 해당하는 데이터가 없기때문에 0으로 넣어준다.

그림과 같은 형태로 구성될 것이다.

RDS for MSSQL metrics for DTU Calculator

이제, 한땀 한땀 준비한 데이터를 Azure DTU Calculator에 넣어 주기만 하면 되는데 한가지 주의할 점이 있다. 우리가 얻은 데이터는 RDS for MSSQL Server에 대한 정보이다. 때문에 여러 DB Instance에 대한 계산을 하는 Elastic Database 메뉴를 선택하여 계산 하도록 해야한다.

계산이 끝나면 아래와 같이 나오는데 이 결과를 통해서 AWS RDS에서 사용하던 성능 수준을 Azure SQL에서 그대로 사용하기 위해서 구성 해야하는 Service Tire/Performance Level을 확인 할 수 있다.

DTU Result

Dependency Injection Service Lifetimes

DI(Dependency Injection)을 구성할때 lifetime을 정의해야한다. lifetime은 DI가 동작할때 서비스 인스턴스가 생성되는 조건을 정의 하는 것이다.
(참고로, DI에 대한 개념 설명은 “Dependency Injection 개념과 Ninject 사용법”이란 포스팅에 설명해 두었다. 추가적인 내용이 필요 하다면 참고하길 바란다.)

lifetime에는 다음과 같이 3가지가 있다.

  1. Transient – 모든 요청에 대해서 매번 새로 인스턴스를 생성한다.
  2. Scoped – scope 당 1회 인스턴스를 생성한다.
  3. Singleton – DI가 구성되고 특정 시점에 1회 인스턴스를 생성되고 나면, 모든 호출에 대해서 동일한 인스턴스를 재활용한다.

글로만 읽으면 잘 이해가 가지 않는다. 좀 더 쉽게 이해를 하기 위해서 간단히 코딩을 하여 알아보자.

.Net Core Console 프로젝트를 생성하고 Progrma.cs에 다음과 같이 코드를 넣어 보자.

namespace DILifetimesTest
{
    public interface IService
    {
        void Info();
    }

    public interface ISingleton : IService { }
    public interface IScoped : IService { }
    public interface ITransient : IService { }

    public abstract class Operation : ISingleton, IScoped, ITransient
    {
        private Guid _operationId;
        private string _lifeTime;

        public Operation(string lifeTime)
        {
            _operationId = Guid.NewGuid();
            _lifeTime = lifeTime;

            Console.WriteLine($"{_lifeTime} Service Created.");
        }

        public void Info()
        {
            Console.WriteLine($"{_lifeTime}: {_operationId}");
        }
    }

    public class SingletonOperation : Operation
    {
        public SingletonOperation() : base("Singleton") { }
    }
    public class ScopedOperation : Operation
    {
        public ScopedOperation() : base("Scoped") { }
    }
    public class TransientOperation : Operation
    {
        public TransientOperation() : base("Transient") { }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var serviceProvider = new ServiceCollection()
               .AddTransient<ITransient, TransientOperation>()
               .AddScoped<IScoped, ScopedOperation>()
               .AddSingleton<ISingleton, SingletonOperation>()
               .BuildServiceProvider();


            Console.WriteLine("========== Request 1 ============");
            serviceProvider.GetService<ITransient>().Info();
            serviceProvider.GetService<IScoped>().Info();
            serviceProvider.GetService<ISingleton>().Info();
            Console.WriteLine("========== ========= ============\r\n");

            Console.WriteLine("========== Request 2 ============");
            serviceProvider.GetService<ITransient>().Info();
            serviceProvider.GetService<IScoped>().Info();
            serviceProvider.GetService<ISingleton>().Info();
            Console.WriteLine("========== ========= ============\r\n");
                        
            using (var scope1 = serviceProvider.CreateScope())
            {
                Console.WriteLine("========== Request 3 (scope1)============");
                scope1.ServiceProvider.GetService<ITransient>().Info();
                scope1.ServiceProvider.GetService<IScoped>().Info();                
                scope1.ServiceProvider.GetService<ISingleton>().Info();
                Console.WriteLine("========== ========= ============\r\n");

                Console.WriteLine("========== Request 4 (scope1)============");
                scope1.ServiceProvider.GetService<ITransient>().Info();
                scope1.ServiceProvider.GetService<IScoped>().Info();
                scope1.ServiceProvider.GetService<ISingleton>().Info();
                Console.WriteLine("========== ========= ============\r\n");
            }

            using (var scope2 = serviceProvider.CreateScope())
            {
                Console.WriteLine("========== Request 5 (scope2)============");
                scope2.ServiceProvider.GetService<ITransient>().Info();
                scope2.ServiceProvider.GetService<IScoped>().Info();
                scope2.ServiceProvider.GetService<ISingleton>().Info();
                Console.WriteLine("========== ========= ============\r\n");
            }

            Console.WriteLine("========== Request 6 ============");
            serviceProvider.GetService<ITransient>().Info();
            serviceProvider.GetService<IScoped>().Info();
            serviceProvider.GetService<ISingleton>().Info();
            Console.WriteLine("========== ========= ============\r\n");

            Console.ReadKey();
        }
    }
}

코드를 실행해 보면 다음과 같이 결과를 얻을 수 있다.

========== Request 1 ============
Transient Service Created.
Transient: 2abfdf9f-5223-4a2c-953f-1c7269745073
Scoped Service Created.
Scoped: ab819f16-7b39-4f61-b077-3f7c5e38025b
Singleton Service Created.
Singleton: 47e04473-f743-41ec-9f2a-e2b08e1a216e
========== ========= ============

========== Request 2 ============
Transient Service Created.
Transient: 883343bd-0f24-4416-a56c-04b1a7cba89f
Scoped: ab819f16-7b39-4f61-b077-3f7c5e38025b
Singleton: 47e04473-f743-41ec-9f2a-e2b08e1a216e
========== ========= ============

========== Request 3 (scope1)============
Transient Service Created.
Transient: 5eba1fb5-f53b-49cc-bab4-5e1e103b4e17
Scoped Service Created.
Scoped: a0cdf863-e10b-4925-8731-3fe0f2a085ff
Singleton: 47e04473-f743-41ec-9f2a-e2b08e1a216e
========== ========= ============

========== Request 4 (scope1)============
Transient Service Created.
Transient: d6124182-1fbc-402d-997f-fd9024fa5187
Scoped: a0cdf863-e10b-4925-8731-3fe0f2a085ff
Singleton: 47e04473-f743-41ec-9f2a-e2b08e1a216e
========== ========= ============

========== Request 5 (scope2)============
Transient Service Created.
Transient: 8a200580-965e-4d77-bfad-08cc722e6a13
Scoped Service Created.
Scoped: 72c67b06-6296-49da-b162-853c6f16e1d0
Singleton: 47e04473-f743-41ec-9f2a-e2b08e1a216e
========== ========= ============

========== Request 6 ============
Transient Service Created.
Transient: 0885afac-3b12-4c8a-8e60-0fb68b0f32a4
Scoped: ab819f16-7b39-4f61-b077-3f7c5e38025b
Singleton: 47e04473-f743-41ec-9f2a-e2b08e1a216e
========== ========= ============

출력된 결과를 살펴 보면 Transit은 매 서비스 요청마다 새로 인스턴스가 생성된 것을 확인 할 수 있다.

다음으로 Scoped는 1번 요청에서 생성된것이 2번 6번에서 재활용되었고, 그 외 Scope가 새로 할당 될 때는 새 인스턴스가 생성되고 해당 Scope안에서는 Scope가 종료될때까지 동일한 인스턴스가 재활용되는 것을 확인 할 수 있다.

마지막으로 Sigleton은 1회 인스턴스가 생성되면, 요청이나 Scope 할당과 상관없이 동일한 인스턴스가 재활용 되는 것을 확인 할 수 있다.

위와 같이 각각 lifetime 옵션에 따라 서비스 처리 특성이 다르기 때문에 서비스 요청 특성에 맞춰 알맞는 lifetime을 설정하는 것이 중요할 것 같다.

[출처] : https://www.c-sharpcorner.com/article/dependency-injection-service-lifetimes/

Jupyterhub on Azure Kubernetes Service

데이터 과학을 수행할때 주로 사용되는 언어로는 Python과 R이 있다. 그리고 이 2가지 언어를 지원하는 IDE 환경도 많이 나와 있는데, 그 중 협업 환경에서 많이 선호되는 Jupyterhub 사용에 대해서 알아보겠다.

Jupyterhub은 Project Jupyter라는 비영리 단체에서 개발한 오픈소스 프로젝트다. BSD라이선스를 따르고 있어서 누구나 100% 무료로 사용할 수 있다.

Jupyterhub는 특정 사용자 그룹별로 Jupyter Notebook(이하 Notebook)이라는 가상 개발 환경을 제공한다. 데이터 과학을 수행하는 사용자는 Notebook이라는 가상 개발 환경안에서 업무를 수행하면 된다. 즉, Jupyterhub는 여러 Notebook들을 공유하는 서버인 것이다. 때문에 사용자는 공유 서버를 통해서 자신이 원하는 Notebook 가상 환경 및 리소스를 제공받을 수 있기 때문에 설치 및 유지 관리 작업에 부담을주지 않는다. 또한 특정 사용자 혹은 그룹별로 별도의 가상환경을 구성할 수 있기 때문에 시스템 관리가 용이하다. 

Jupyterhub는 2가지 배포본을 제공되고 있는데 첫 번째는 가상머신 환경에 설치하는 배포본이고 두 번째는 Serverless 환경인 Kubernetes에 설치하는 배포본이다.

클라우드 상에서 운용하기에는 Scale Set을 자유롭게 확장 및 유지관리 할 수 있는 Kubernetes(Serverless framework)환경이 좋기 때문에 가상머신 설치방법은 건너띄고 Jupyterhub를 Kubernetes에 설치 및 구성하는 방법알 알아 보겠다.

참고로, 여기에서 사용된 Kubernetes는 Azure에서 제공하는 AKS(Azure Kubernetes Service)를 이용하였다.

Jupyterhub를 Azure Kubernetes에 설치하기

먼저, Jupyterhub를 설치할 AKS 클러스터에 대한 크리덴셜을 가져오고 최근 환경으로 설정한다.

RESOURCENAME = 'Jupyter'
CLUSTERNAME = 'Jupyterhub'

az aks get-credentials --resource-group=$RESOURCENAME --name=$CLUSTERNAME
kubectl config set-cluster $ClusterName

Jupyterhub를 바로 설치하기 전 jupyterhub를 환경을 구성할 내용을 준비해야 한다.

Jupyterhub 사전 준비작업

Kubernetes는 Serverless 환경이기 때문에 작업한 파일을 영구적으로 보존할 스토리지 볼륨이 필요하다. 다음과 같이 Storage Class를 만들어 준다.

vim storageclass.yaml

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: azurefile
provisioner: kubernetes.io/azure-file
mountOptions:
  - dir_mode=0777
  - file_mode=0777
  - uid=1000
  - gid=1000
  - mfsymlinks
  - nobrl
  - cache=none
parameters:
  skuName: Standard_LRS

kubectl apply -f storageclass.yaml

잘 만들어 졌는지 확인한다.

kubectl get storageclass

다음으로 jupyterhub에 접근할 Client들이 사용할 인증 보안 토큰을 다음과 같이 32byte 임의의 16진 문자열로 생성한다.

openssl rand -hex 32
36806100da02acb12199b94067a55c1231172123b05f061a428777eb65b238fd

위에서 준비한 환경설정 정보들을 가지고 다음과 같이 jupyter 환경을 구성한다. StorageClass, 초기 관리자 계정 정보, 인증토큰을 다음과 같이 넣어준다.

vim config.yaml

singleuser:
  extraEnv:
    EDITOR: "vim"
  storage:
    dynamic:
      storageClass: azurefile
auth:
  admin:
    users:
      - administrator
proxy:
  secretToken: "36806100da02acb12199b94067a55c1231172123b05f061a428777eb65b238fd"

마지막으로, Jupyterhub 배포본을 제공 받기 위한 helm repository를 설정해 준다.

helm repo list
helm repo add jupyterhub https://jupyterhub.github.io/helm-chart/
helm repo add stable https://kubernetes-charts.storage.googleapis.com
helm repo update

이제, 준비를 다했으니 설치해 보자.

Jupyterhub 설치하기

Kubernetes에 Jupyterhub가 설치될 네임스페이스를 만들어준다.

RELEASE=jupyterhub
NAMESPACE=jupyterhub

kubectl create namespace  $NAMESPACE 

사전 준비단계에서 만들어 둔 config.yaml을 다음과 같이 실행한다.

helm upgrade --install $RELEASE jupyterhub/jupyterhub --namespace $NAMESPACE --values config.yaml

위 명령을 실행하고 나면 설치 과정이 백그라운드로 실행 되기 때문에 어떻게 진행 되고 있는지 확인 하기 어렵다.
아래 명령어를 통해서 pod가 정상적으로 올라오는지 확인 해야 한다.

kubectl get pod --namespace $NAMESPACE

정상적으로 Running 상태를 확인 했다면, 정상 설치가 된것이다.
아래 명령을 통해서 서비스에 할당된 Public IP를 확인해 본다.

kubectl get service --namespace $NAMESPACE

확인 된 Public IP를 웹브라우저를 통해서 들어가면 로그인 하라고 나올 것이다.
위에서 설정한 admin계정 이름을 사용하여 접속하면 된다.

여기까지 간단히 Jupyterhub를 Azure Kubernetes Service에 설치하고 접속하는 것 까지 알아보았다. 이제 여러 Notebook 가상 환경을 구성하고 여러 사용자 혹은 그룹과 함께 사용해 보길 바란다.

Azure DevOps 개요

DevOps 란?

데브옵스(DevOps)는 소프트웨어의 개발(Development)과 운영(Operations)의 합성어로서, 소프트웨어 개발자와 정보기술 전문가 간의 소통, 협업 및 통합을 강조하는 개발 환경이나 문화를 말한다.

주로 아래 그림과 같이 개발과 운영간의 연속적인 사이클로 설명할 수 있다.

이를 통해서 얻는 장점은 다음과 같다.

  1. 신속한 제공
    • 릴리스의 빈도와 속도를 개선하여 제품을 더 빠르게 혁신하고 향상할 수 있다. 새로운 기능의 릴리스와 버그 수정 속도가 빨라질수록 고객의 요구에 더 빠르게 대응할 수 있다.
  2. 안정성
    • 최종 사용자에게 지속적으로 긍정적인 경험을 제공하는 한편 더욱 빠르게 안정적으로 제공할 수 있도록, 애플리케이션 업데이트와 인프라 변경의 품질을 보장할 수 있다.
  3. 협업 강화
    • 개발자와 운영 팀은 긴밀하게 협력하고, 많은 책임을 공유할 수 있도록, 워크플로를 결합한다. 이를 통해 비효율성을 줄이고 시간을 절약할 할 수 있다.
  4. 보안
    • 제어를 유지하고 규정을 준수하면서 신속하게 진행할 수 있다. 자동화된 규정 준수 정책, 세분화된 제어 및 구성 관리 기술을 사용할 수 있다.

그럼, DevOps를 실현하기 위해서는 어떻게 해야 하는가?

DevOps를 실현하기 위해서는 CI(Continuous Integration)/CD(Continuous Deployment(Delivery))라는 2가지 작업을 해야 한다.

CI(Continuous Integration)은 Development에 속하는 작업으로 지속적으로 프로젝트의 요구사항을 추적하며, 개발된 코드를 테스트 및 빌드를 수행한다.

  1. 프로젝트 기획 + 요구사항 추적
    • 프로젝트 시작
    • 기획(프로젝트 방법론 채택)
    • 작업관리(Backlog 관리)
    • 진행상황 추적
  2. 개발 + 테스트
    • 코드작성
    • 단위 테스트
    • 소스제어
    • 빌드
    • 빌드 확인

CD(Continuous Deployment(Delivery))는 Operations에 속하는 작업으로 CI가 완료되어 빌드된 소스를 통합 테스트(개발, QA, Staging)를 거쳐 배포를 하며, 배포된 사항들을 지속적으로 모니터링하고 프로젝트 요구사항에 피드백하는 작업을 수행한다.

  1. 빌드 + 배포
    • 자동화된 기능 테스트
    • 통합 테스트 환경(Dev)
    • 사전 제작 환경
      (QA, Load testing)
    • 스테이징 환경(Staging)
  2. 모니터링 + 피드백
    • 모니터링
    • 피드백

이제, DevOps를 하기위한 구체적인 작업을 알았으니, 실제 구성을 하도록 하는 제품들에 대해서 알아보자.

Azure DevOps vs Other Software

DevOps를 하기 위한 솔루션들은 이미 시중에 엄청나게 나와 있으며, 대게 오픈소스 형태로 많이 제공되고 있어서 바로 가져다 사용할 수 있다.

위 그림에서 볼 수 있듯이 DevOps의 각 단계에 맞추어서 원하는 (특화된)제품을 선택하여 사용하면 된다.

모든 단계를 빠짐 없이 구현한다고 가정하여, 예를 들면 다음과 같이 DevOps가 구현 될 수 있다.

  1. Slack으로 요구사항 관리를 하고
  2. Git으로 소스코드 관리를 하고
  3. Maven으로 빌드를 하고
  4. JUnit으로 테스트하고
  5. Jenkins로 Docker에 배포하고
  6. Kubernetes로 운영하며
  7. Splunk로 모니터링 한다.

그런데 이런 경우, 벌써 필요한 제품에 8개나 된다. 프로젝트에 참여하는 모든인원이 이 제품들에 대해서 이해하고 사용하기 어려우며, 각 제품에대한 담당자들있어야 제대로 운영 될 수 있을 것이다. 게다가, 각기 다른 제품이라 다음 단계로 넘어가기 위한 추가적인 관리를 해야할 것이다.

이렇게 되면, 규모가 작은 곳에서는 몇가지 단계를 건너띄고 관리를 하게 되는데 이런 부분에서 예외사항이 생기기 시작하고, 결국 프로젝트 끝에서는 DevOps를 거의하지 못하는 상황이 생길 수 도있다.

반면, Azure DevOps는 하나의 DevOps 관리 솔루션을 제공한다. 때문에 Azure DevOps 하나만 사용해도 모든 절차를 구성 할 수 있다.
그리고 만약, 기존에 사용하던 제품이 있다 하더라도 아래 그림과 같이 3rd-party를 마이그레이션 혹은 연동 설정 할 수 있도록 하기 때문에 Azure DevOps 제품안에서 하나로 통합 관리 할 수 있다.

마지막으로, Azure DevOps를 사용하는 간단한 시나리오에 대해서 알아보자.

Azure DevOps에서는 파이프라인이라는 형태로 CI/CD를 구성하도록 되어 있으며, 각 단계 구성은 아래 그림과 같다.

  1. Project (Agile) Board를 통해서 프로젝트 요구사항 추적 관리를 하고
  2. Repo에서 각 Agile Board Task에 대한 소스코드 관리를 하고
  3. 소스가 커밋이 되면 CI 파이프라인을 통해 빌드 + 테스트를 수행하고
  4. CI가 완료되면 Trigger 형태로 CD 파이프라인을 실행하고
  5. CD 파이프라인을 통해 통합 테스트 + 배포를 하고
  6. (옵션)담당자에게 최종 승인을 받고
  7. 운영 적용 및 모니터링을 한다.

Azure DevOps 이 외의 제품을 사용 했을 때와 단계는 거의 동일 하지만, 여기서 주목 할 점은 Azure DevOps 단일 제품에서 모두 제공 받고 구성 할 수 있다는 것이다.

(**여기서 다 설명 못한 부분이지만 CI/CD 과정 중에 Function(Trigger) 형태로 여러 기능들을 다양하게 엮을 수도 있다. 예를 들어 6번)

각각의 솔루션 전문가들이 있어서 운영한다면 문제가 없겠지만, DevOps를 처음 도입한다던가 규모가 작아 축소 운영을 해야하는 상황이라면 고려해 볼 수 있을 것 같다.

Azure Key Vault 개요

일반적으로, Application Security를 하기위해 Key Vault를 적용하는 사례/방법은 아래와 같이 크게 4가지가 있다.

  1. General Secret Storage
    • 다양한 종류의 비밀들을 저장 할 수 있다.
      (예, 민감한 환경설정, 데이터베이스 크리덴셜, API 키 등)
    • Plan Text file,프로젝트 환경설정 관리, DB에 저장 등, 보통 사용되는 방식 보다 Vault Read나 Vault API를 사용하여 Query하는 것이 훨씬 안전하다.
    • Vault의 Audit Log를 통해서 접근들을 보호 할 수 있다.
  2. Employee Credential Storage
    • General Secret Storage의 확장되는 개념이다.
    • 웹 서비스에 접근하는 직원들의 인증 정보를 저장하기 좋다.
    • Audit Log를 통해서 어떤 직원이 비밀에 접근(및 작업) 했는지 쉽게 알 수 있다.
  3. API Key Generation for Scripts
    • 이상적인 Vault의 기능인 “Dynamic Secrets” 을 통해서 비밀에 접근하는 키를 일정 기간동안 생성하고 만료 시킬 수 있다.
    • Keypair는 스크립트 동작으로만 존재 하며, 키의 생성 작업은 완벽하게 로그로 남는다.
    • IAM(Identity Access Management) 사용 이상의 기술이지만, 때로는 하드코딩으로 접근 제한을 두는 것이더 효율 적일 수 있다.
  4. Data Encryption
    • Vault를 사용하여 비밀을 저장할 수있을뿐만 아니라 다른 곳에 저장된 데이터를 암호화/복호화 할 수 있다.
    • 이 것을 사용하면 응용 프로그램이 기본 데이터 저장소에 데이터를 저장하면서 데이터를 암호화를 할 수 있다.

Azure Key Vault와 같은 서비스는 Azure에만 있는 것은 아니고 대부분의 클라우드 서비스에서 데이터 암화에 대한 KMS(Key Management Service)를 제공하고 있다. 그러면 이러한 서비스이 어떤 차이 점이 있는지 알아보자

AWS KMS(Amazon Web Service Key Management Service)

  • 암호화 표준은 RSA-OAEP와 PKCS#1v1.5(현재 2.2까지 나왔음.)를 사용한다.
  • CMK(Customer Master Key)와 Data Key를 이용하여 데이터를 암호화 작업을 한다.
  1. CMK
    • 256-bit AES 키이며, 이 키는 내보낼 수 없다.
    • 키에 사용되는 대칭 알고리즘은 AES-GCM-256을 사용한다.
    • CMK를 사용하여 최대 4KB의 데이터를 암호화하고 해제 할 수 있다.
  2. Data Key
    • CMK를 사용하여 생성한다.
    • 많은 양의 데이터를 암호화 하는데 사용한다.

GCP KMS(Google Cloud Platform Key Management Service)

  • 암호화 표준은 RSA-OAEP와 AES-GCM을 사용한다.
  • 암호화 Key의 정확한 제어 엑세스 관리를 위해 Key ring, Key, Key version이라는 계층 구조를 가지고 있다.
  1. Key ring
    • 조직화 목적을 위해 키를 그룹화 한 것 이다.
    • Key는 Key를 포함하는 Key ring에서 권한을 상속받는다. 연관된 권한을 가진 Key를 Key ring으로 그룹화하면 각 키를 개벽적으로 작업을 수행할 필요 없이 Key ring 수준에서 해당 키를 대상으로 권한을 부여, 취소, 수정 할 수 있다.
  2. Key
    • AES-256키를 GCM(Galois Counter Mode)로 사용한다.
    • 특정한 용도를 위해 사용되는 암호화 키를 나타내는 명명된 객체이다.
    • 시간이 경과 되어 새로운 키 버전이 생성되면 키자료인 암호화에 사용되는 실제 비트가 변경될 수 있음.
  3. Key version
    • 일정한 시점에 하나의 키와 관련된 키 자료를 나타낸다.
    • 임의의 여러 버전이 존재 할 수 있으며 버전은 최소 하나 이상 있어야 한다.

Azure Key Vault

  • 암호화 표준은 RSA-OAEP를 사용한다.
  • 비밀정보를 중앙 위치에 보관하고 보안 액세스, 권한 제어 및 액세스 로깅을 제공한다.
  • 비밀 정보를 접근하고 사용하려면 Key Vault에 인증 해야하며, Key Vault 인증은 AAD(Azure Active Directory)에서 인증 지원하는 ID(App Client ID)를 통해 인증 받을 수 있다.
    • 접근 정책은 ‘작업’을 기반으로 한다.
      – Application 별로 비밀 값 읽기, 비밀 이름 나열, 비밀 만들기 등의 권한을 부여한다.
  • 제공되는 보안 컨트롤
    • Data Protect
      – TDE(투명한 데이터 암호화)와 Azure Key Vault를 통합하면 TDE 보호기라는 고객 관리형 비대칭 키를 사용하여 DEK(데이터베이스 암호화 키)를 암호화할 수 있습니다.
    • Network ACL
      – 가상 네트워크 서비스 End-Point를 사용하면 지정된 가상 네트워크에 대한 액세스를 제한할 수 있습니다.

정리하면, AWS, GCP의 KMS는 암호화 키를 안전하게 저장 하는 것과 키를 암호화하는 작업(암호화와 복호화)에  초점이 맞춰져 있다고 볼 수 있고, 반면, Azure Key Vault는 백엔드에서는 키 암호화 작업을 통해 비밀을 저장하는 등, KMS와 유사하게 기능들을 제공하지만 그 외 추가 적인 관리 기능들을 제공하기 때문에 비밀 관리 솔루션을 제공한다고 볼 수 있다.

Azure Cosmos DB 개요

  • 투명한 다중 마스터 복제 및 잘 정의된 5개의 일관성 모델이 포함된 턴키 방식으로 전 세계 배포
  • 규모와 상관없이 SLA로 지원되며 상위 1%에 속하는 99.999% 고가용성의 한 자릿수 밀리초 대기 시간
  • Apache Spark 및 Jupyter Notebook에 대한 기본 제공 지원과 함께 Cassandra, MongoDB, SQL, Gremlin, Etcd, Table의 유선 프로토콜 호환 API 엔드포인트가 포함된 다중 모델

Cosmos DB에서 제공하는 잘 정의된 5개의 일관성 모델

  1. Strong – 사용자는 항상 최신 커밋 쓰기의 읽기가 보장.
  2. Bounded Staleness – 설정된 지연시간 동안 처리되는 항목에 대해서는 읽기 보장이 되지 않음.
  3. Session – 클라이언트 세션을 범위로 관리(단일 “writer” 세션으로 가정), 단조로운 읽기, 단조로운 쓰기, 읽기 – 쓰기 및 쓰기 – 후행 읽기 보장
  4. Consistent Prefix – 쓰기 순서에 따라 일부 처리된 것을 읽을 수 있음.
  5. Eventual – 읽기에 대한 순서 보장이 없음.

Reauest Unit(RU)

  • 처리량 단위는 RU(Request Unit)로 표현
  • 1RU – 1KB 항목을 읽는데 드는 비용
  • 컨테이너(데이터베이스)당 기본 400RU로 시작하며, 최소 100RU 단위로 스케일 조정 할 수 있음.

Cosmos DB offers a choice of APIs

원하는 데이터 모델 및 API를 선택하여 바로 사용 및 적용 할 수 있다.

출처 : https://docs.microsoft.com/en-us/azure/cosmos-db/

[.NET]An existing connection was forcibly closed by the remote host 해결 방법

SSL-TLS(이하 TLS)보안 인증된 서버 Application의 API 호출 할때, An existing connection was forcibly closed by the remote host에러가 발생하는 경우가 있다.

이런 경우는 서버의 TLS 버전을 기본값인 1.0을 사용하지 않고 상위 버전인 1.1이나 1.2를 사용하고 있기 때문이다. (TLS 버전은 2018년 4월 기준으로 1.0부터 1.3까지 있으나 1.3은 아직 정식으로 발표되지 않았다.)
각 버전에 따라 네트워크 전송계층에서 사용하는 암호와 알고리즘이 달라지기 때문에 Server-Client 간에 사용 버전이 다르면 바로 An existing connection was forcibly closed by the remote host오류가 발생한다.

.NET에서는 별도의 설정이 없으면 TLS 버전을 기본값 1.0으로 셋팅하여 HTTP 요청을 하도록 되어 있기 때문에 상위 TLS 버전을 사용하는 서버를 그냥 호출하면 위와 같은 오류가 나온다. 이런경우, 상위 TLS 버전을 사용하여 통신 할수 있도록 System.Net.Security DLL을 이용하여 Security Protocol을 다음과 같이 설정해 주어야 한다.

(ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;)

using (HttpClient client = new HttpClient())
{
    string jsonString = "data list";

    client.BaseAddress = new Uri("https://<Your App Name>.azurewebsites.net");
    
    client.DefaultRequestHeaders.Accept.Clear();
    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    
    ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
    var response = client.PostAsync("/api/getYourData", new StringContent(jsonString, Encoding.UTF8, "application/json")).Result;

    if (response.IsSuccessStatusCode)
    {
        result = response.Content.ReadAsStringAsync().Result;
    }
}

호출 대상 서버의 TLS 제공 버전을 정확하게 알고 있으면 좋겠지만, 그렇지 않은 경우, 위 예시 코드처럼 지원하는 TLS버전 리스트를 Or(‘|’)로 묶어서 설정 하면 Client에서 요청 할때 알아서 알맞는 TLS Protocol 버전으로 통신(Handshake)을 하도록 되어 있어서 크게 신경 쓰지 않아도 된다.

단지, 너무 오래된(취약점이 있는)버전의 경우 보안 이슈가 될 수 있으니 적당한 지원 버전 영역을 관리 하는 것을 권장한다. 

Azure App Service의 사용자 도메인 및 SSL/TLS 설정

Web 프로젝트를 하다보면 가장 큰 이슈가 바로 네트워크 통신 구간의 보안이다. 그리고 통신 구간 보안을 하기위해서 주로 권장되는 것이 SSL/TLS Protocol(이하 SSL/TSL)을 사용하는 것이다.
SSL/TLS는 ISO 7계층 중 세션 계층(Session Layer)의 Protocol로 양 끝단의 응용 프로세스가 통신을 관리하기 위한 방법 중 하나이다.

사실, 정확히 말하면 TLS(Transport Layer Security)는 SSL(Secure Sockets Layer)가 표준화 되면서 바뀐 이름이라 정확히 말하면 SSL보다는 TLS라고 말하는 것이 더 바람직 하다. 단지, 초기에 보안 인증서(보통 알고 있는 SSL 인증서)를 지칭할때나 많은 Open Source 프로젝트 등에서 SSL이라는 용어를 많이 사용하다 보니 TLS보다는 SSL로 말하는 것이 이해가 빠르기 때문에 일반적으로 SSL이라는 용어를 사용한다. (한 예로 아래 설명할 Azure App Service 설정에서도 인증서 설정 메뉴를 ‘SSL 보안’ 이라는 이름으로 사용하고 있다.) 

TLS에 대해서 좀 더 설명해 보면, 인터넷 상에서 통신할 때 주고받는 데이터를 보호하기 위한 표준화된 암호화 프로토콜로, 넷스케이프사에 의해 개발된 SSL3.0 버전을 기반으로 만들어 졌다. 
TLS는 통신 과정에서 전송계층의 암호화 방식으로 HTTP뿐만 아니라 FTP, XMPP등 응용 계층(Application Layer)프로토콜의 종류에 상관없이 사용할 수 있다는 장점이 있으며, 기본적 으로 인증(Authentication), 암호화(Encryption), 데이터 무결성(Integrity)을 지원 한다. 

Protocol Year
SSL 1.0
SSL 2.01995
SSL 3.01996
TLS 1.01999SHA1
TLS 1.12006IANA 등록 파라메터의 지원
TLS 1.22008SHA256
TLS 1.32018Enctypted SNI(ESNI)

이제, Azure App Service에 별도의 도메인 및 TLS(SSL)인증서를 설정하고 통신을 해보도록 하자.

(참고로, App Service Plan을 무료 이상을 사용한다면 Azure에서 기본 제공하는 App Service 도메인(<your app name>.azruewebsites.net)에는 HTTPS 통신을 하기위한 TLS 인증서가 기본적으로 탑제 되어 있어서, 굳이 외부로 노출되는 별도의 도메인과 인증서를 구입하지 않아도 HTTPS 통신을 할 수 있도록 지원 해준다.)

이미 App Service Plan에 배포한 App을 선택하여 설정 메뉴들에 보면 ‘사용자 지정 도메인‘이라는 메뉴가 있다. 메뉴를 선택하면, 아래 그림과 같이 별도로 구매한 도메인을 해당 App에 지정하여 사용할 수 있다.
단, 구입한 도메인에 Azure에서 기본 제공하는 도메인(<your app name>.azruewebsites.net)을 별칭으로 미리 등록해 두어야 유효성 검사를 할 수 있다. 

다음으로, 바로 밑에 있는 ‘SSL 설정‘메뉴가 있는데 클릭해 보면 위에서 사용자 지정한 도메인에 TLS인증서를 등록 설정 할 수 있다.
아래 그림과 같이 TLS인증서에 대한 설정(HTTPS만 사용, TLS 버전 1.0)을 한 후, 바인딩 버튼을 눌러 도메인과 도메인용으로 구입한 인증서를 선택하여 도메인-인증서 바인딩 추가 하도록 하면 해당 App을 위해 별도 구입한 도메인과 인증서 셋팅이 완료 된다.
(아래 그림은 인증서 업로드가 이미 업로드 상태를 전제로 진행 하였다. 인증서 업로드는 스크롤 다운을 해보면 Azure Service Certificate 가져오기와 인증서 업로드 메뉴가 있다. 각 인증서 구입한 상황에 따라 업로드 하면 된다.)

위와 같이 설정을 하면 HTTP를 사용해도 네트워크 전송계층에서 자동으로 HTTPS로 Redirection해주어 HTTP사용을 막고 무조건 TLS로 보안된 HTTP통신, 즉 HTTPS 통신을 할 수 있게 된다.
TLS 버전은 기본 1.0으로 셋팅 되도록 하는데, 만약 상황에 따라 버전을 1.1이나 1.2를 사용해야 될때가 있다면, 해당 Web Project의 API 호출 시 Client Application에서 Security Port를 별도로 설정 해주어야 하니 참고 하길 바란다.

Azure에서 이중화로 구성된 VM에 Load Balancer 구성하기

Azure에서 IaaS 서비스인 가상머신을 사용하여 시스템을 구성할 때 가장 먼저 고려해야 하는 사항이 바로, 이중화 구성에 대한 부하 분산 설정이다. 

이전 Azure ASM(Azure Service Manager, 클래식 버전)에서는 Cloud Service라는 PaaS 서비스를 이용하여 부하 분산이 되도록 하였다. Cloud Service의 경우 초기 설정이나 사용에 제한이 있었는데 나에게 가장 어려웠던 2가지가 “NAT가 해시 기반 모드만 지원한다“라는 것과 “유휴시간 설정을 별도로 할 수 없다“라는 것이었다. 이렇게 되면 Local Storage를 많이 이용하는 시스템의 경우 바로 이중화 작업을 하기 힘들어진다.

이후, Resource Group라는 개념이 추가된 Azure ARM(Azure Resource Manager, 리소스 버전)에서는 Load  Balancer를 독립적으로 PaaS 서비스로 제공하게 되면서, 위 문제점 들을 해소할 수 있게 되었다.

이제 위 언급한 문제점들을 어떻게 설정하는지 확인해보자.

우선, Load Balancer에 연결할 VM(2대)을 간단하게 생성해 보자.

위 그림을 살펴보면, 좌측 1번째 VM은 생성할 때 가상 네트워크, 서브넷, 네트워크 보안 그룹(방화벽), 가용성 집합을 새로 만드는 것을 확인할 수 있다. 그리고 우측 2번째 VM은 1번 VM을 생성했을 때 만들어진 것들을 그대로 사용하여 같은 가상 네트워크 서브넷과 가용성 집합 내에 만들어지는 것을 확인할 수 있다. 

VM 2대 모두 생성 완료되었다면 만들어진 리소스 그룹에는  위에 언급한 서비스 리소스들이 구성된 것을 볼 수 있다.

이제, 생성된 각 VM에 원격접속을 하여 IIS 기능을 추가하고 기본 웹 사이트(Default Web Site)가 동작하도록 설정한다.
이때, 각 VM의 사이트에 부하 분산이 될 때 식별될 수 있도록 간단히 index.html 페이지에 표시해 주도록 하면 좋다.
(원격제어 및 IIS 설정, 기본 사이트 구성 등 관련 절차는 생략한다.)    

IIS 사이트가 잘 구성되었다면, 각 사이트(Default Web Site)가 아래 그림과 같이 나타날 것이다. 

여기까지 가 Load Balancer를 사용하기 전까지 준비 단계이다. ㅡㅡ;;;;

이제 본격 적으로 Load Balancer 서비스와 어렵게 구성한 VM 2대를 가지고 부하 분산이 되도록 구성하여야 하는데…. 그전에 부하 분산의 2가지 형태에 대해서 설명하고 진행 하겠다.

Azure Load balancer의 부하분산모드에는 해시 기반 모드와 소스 IP 선호도 모드 2가지를 제공있는데 각 특성은 아래와 같다.

1. 해시 기반 모드
기본 배포 모드로 5개의 Tuple을 가지고 connection을 hash하여 DIP에 연결해 주는 방식이다. 
5 Tuple의 구성은 Source IP, Source Port, Destination IP, Destination Port, Protocol type으로 구성되 있다. 동일한 세션 내에서는 동일한 DIP 인스턴스로 보내지게 되지만, Source의 세션이 새로 시작되면 connection이 다른 DIP 인스턴스로 보내질 수 있다.

2. 소스 IP 선호도 모드
2개 혹은 3개의 Tuple을 가지고 connection을 hash하여 DIP에 연결해 주는 방식이다.
2 Tuple의 구성은 Source IP, Destination IP로 구성되어 있으며, 3 Tuple은 Source IP, Destination Protocol type으로 구성되 있다. 동일한 Source은 항상 동일한 DIP 인스턴스로 보내지게 된다. 단, 이경우 connection traffic이 불균형하게 분산될 수 있다.

(참조 링크: https://docs.microsoft.com/ko-kr/azure/load-balancer/load-balancer-distribution-mode)

이제 진짜 Load Balancer를 만들어 보자. 
준비 과정이 길어서 어려울 것 같지만 사실상 Load Balancer는 리소스 이름하고 Public IP 구성만 하면  수분 내에 다음 그림과 같이 만들어지는 것을 확인 할 수 있다.

이제 생성이 된 부하 분산 장치의 설정을 메뉴 순서대로 구성만 해주면 된다.
(실제로, 좌측 메뉴의 설정을 위에서 아래 순으로 구성만 해주면 되기 때문에 이미지외 설명은 생략 하도록 한다.)

1. 백엔드 풀

2. 상태 프로브

3. 부하 분산 규칙

4. 인바운드 NAT 규칙
위에서 IIS기능 구성 당시 네트워크 보안 그룹에서 80포트에 대한 인바운드 규칙을 이미 해두었기 때문에 규칙 중복 오류가 발생한다. 이때, 당황하지 말고 침착하게 설정을 안하고 지나가도록 하자. ^^

1~4번까지 설정이 완료되면, Load Balancer의 공용 IP(혹은 DNS) 주소를 가지고 사이트에 접근해보자. 브라우저 세션 강제 삭제 및 새로 고침(F5)을 몇 번 해보면 부하 분산 규칙에 따라 사이트(VM 2대)가 바뀌는 것을 볼 수 있다.

[.NET]Swagger UI에 Customized Data Type 표현하기

API를 개발하면서 정의한 호출/응답 Class Model의 Properties에 별도로 Customized Data Type(Attribute)를 적용하여 사용할때가 있는데, 이런경우, Customized Data Type은 자료형이 아니기 때문에 Swagger에서 표현되지 않아, API 사용자에게 특정 Property의 상세한 스팩을 전달 하기 어렵게 된다.

이런 속성들을 나타내려면 SwaggerConfig의 Schema Filter를 사용하면 된다.

Schema Filter를 사용하면, Swagger Document Json 파일에 사용자가 정의한 Customized DataType을 추가 할 수 있게 되어 Swagger UI에서도 사용자가 정의한 Data Type이 표현 되어 진다.

Customized Data Type(Attribute)을 정의하고 Model에 적용하여 확인해보자.

1. Customized Attribute Class를 정의한다. 
(Tip, Attribute Class 정의 위에 Attribute Usage 속성을 명시적으로 정의 해주면, 사용자가 정의한 Customized Data Type(Attribute)이 의도하지 않은 형태로 사용되는 것을 예방 할 수 있다.)

    /// <summary>
    /// Customized Attribute
    /// </summary>
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
    public class CustomizedAttribute : Attribute
    {
        /// <summary>
        /// Customized Attribute
        /// </summary>
        /// <param name="DataType">Attribute Name</param>
        public CustomizedAttribute(string DataType)
        {
            DataTypeName = DataType;
        }

        /// <summary>
        /// Name of DataType
        /// </summary>
        public string DataTypeName { get; set; }
    }

2. 1번에서 정의한 Attribute를 아래와 같이 API에서 응답/호출에 사용할 Model의 Properties에 적용한다.

    /// <summary>
    /// Student Info Model
    /// </summary>
    public class StudentInfo
    {
        /// <summary>
        /// ID
        /// </summary>
        [Required]       
        public string StudentID { get; set; }

        /// <summary>
        /// Name
        /// </summary>
        [Required]
        [CustomizedAttribute("MyType")]
        public string Name { get; set; }

        /// <summary>
        /// Gender
        /// </summary>
        public string Gender { get; set; }

        /// <summary>
        /// Phone Number
        /// </summary>
        [CustomizedAttribute("MyType")]
        public string Phone { get; set; }

        /// <summary>
        /// Address
        /// </summary>
        [CustomizedAttribute("MyType")]
        public string Address { get; set; }
    }

여기까지 하면, 일단은 Model Properties에는 적용되어 시스템에는 반영 되었지만, Swagger UI에서 확인을 할 수 없다.

확인해 보기 위해서 프로젝트를 실행하면 다음과 같이 Customized Data Type을 적용한 Properties들이 일반 자료형인 string으로 표현 되는 것을 볼 수 있다.

이제 String으로 표현된 부분을 Attribute로 정의한 “MyType”으로 나오도록 Schema Filter를 설정해 보자.

1. Schema Filter로 사용할 Filter Class “ApplySchemaVendorExtensions”를 다음과 같이 추가(정의)한다.

    /// <summary>
    /// Vendor Extensions
    /// </summary>
    public class ApplySchemaVendorExtensions : ISchemaFilter
    {
        /// <summary>
        /// Get Customed Attribute Schema
        /// </summary>
        /// <param name="schema">Document Schema</param>
        /// <param name="schemaRegistry">Document Schema Repository</param>
        /// <param name="type">Model Definition</param>
        public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type)
        {            
            var propertyMappings = type.GetProperties()
                                   .Join(schema.properties ?? new Dictionary<string, Schema>(),
                                   x => x.Name.ToLower(), y => y.Key.ToLower(),
                                   (x, y) => new KeyValuePair<PropertyInfo, KeyValuePair<string, Schema>>(x, y)).ToList();

            foreach (var propertyMapping in propertyMappings)
            {
                var propertyInfo = propertyMapping.Key;
                var propertyNameToSchemaKvp = propertyMapping.Value;

                foreach (var attribute in propertyInfo.GetCustomAttributes())
                {
                    SetSchemaDetails(propertyNameToSchemaKvp, attribute);
                }
            }
        }

        /// <summary>
        /// Set Customized Schema Details
        /// </summary>
        /// <param name="propertyNameToSchemaKvp">Property Schema</param>
        /// <param name="propertyAttribute">Customized Attribute</param>
        private static void SetSchemaDetails(KeyValuePair<string, Schema> propertyNameToSchemaKvp, object propertyAttribute)
        {
            var schema = propertyNameToSchemaKvp.Value;

            if (propertyAttribute is CustomizedAttribute)
            {
                string dataType = ((CustomizedAttribute)propertyAttribute).DataTypeName;

                schema.type = dataType;

                schema.example = "string";
            }        
        }
    }

위 코드에서 작성한 Schema Filter에서는, Swagger에서 화면을 구성하기 위한 Swagger Document Json 파일을 생성할때, 해당 Schema Filter에서 사용자가 정의한 Customized Data Type을 Swagger Document Json파일 내용에 반영 하도록 한다. 

2. 1번에서 정의한 Schema Filter인 “ApplySchemaVendorExtensions”을 다음과 같이 SwaggerConfig에 추가 설정하여, 필터가 동작 할 수 있도록 한다.

GlobalConfiguration.Configuration
.EnableSwagger(c =>
{
 c.SchemaFilter<ApplySchemaVendorExtensions>();
})
/*.......... 생략 ............*/

여기까지 Customized Data Type에 대한 정의 및 표현(구현)을 위한 Schema Filter 설정까지 모두 완료 되었다. 

이제, 프로젝트를 다시 실행 시켜 보면 자료형으로 표현 되었던 Properties들이 모두 Customized Data Type인 MyType이 표현 된 것을 확인 할 수 있다. 

Schema Filter 적용 전

Schema Filter 적용 후

– 유의사항 –
Swagger UI는 API 사용자에게 제공되는 스팩 문서인기 때문에, API 사용자가 어떻게 이해 할 것인가를 우선 고려 해야 한다. 위 방법을 통하면, 해당 Model Property가 특정하게 이용(처리)되는 것을 명시적으로 표현 할 수 있지만, 만약 정의한 속성 때문에 API 사용자가 모델에 사용할 Data Type이 혼동이 온다면, 필터를 적용하지 않고 일반 자료형이 표현되게 하고 Implementation Note에 별도로 작성하여 안내하는 것이 더 좋을 수 있다.