Cloud Native Application 란

소규모의 독립적인 서비스 컬렉션입니다. 새로운 애플리케이션을 개발하고 최적화하고 모든 환경을 연결하는 작업을 가속화하여 비지니스 요구사항의 변화속도에 맞춰 사용자들이 원하는 애플리케이션을 의미합니다.

Cloud Native Application 구축방법

DevOps 를 도입하여 공통 목적과 주기적인 피드백을 통해 개발팀과 운영팀의 협업을 지원할 수 있도록 하며, Container 를 도입하여 애플리케이션 배포 유닛 및 독립적인 실행환경을 제공하여 지원하도록 합니다. 즉 하나의 대규모 릴리스 및 업데이트를 기다리는 것이 아니라, 마이크로서비스 처럼 여러 서비스가 탄력적으로 결합된 하나의 컬렉션으로 구축하는 것을 의미합니다.

참고: https://www.redhat.com/ko/topics/cloud-native-apps

Cloud Native Application 시스템 구성도

인터넷에 이미 멋진 말로 포장되어 있는 글이 많은데, 의미와 정의를 들어봐도 도대체 무언인지 감히 잡히지 않았고, 너무 추상적 이였습니다. 일단 하나 만들어 보는 프로젝트가 시작되었습니다. EC2를 사용하지 않고, 말 그대로 Serverless 를 목표로 AWS 의 서비스를 최대한 사용하여 시스템을 구축한 경험을 정리하려고 합니다. 네트워크 전문가가 아니기에, VPC 니 인프라니 구성도는 보다는 순수한 개발자 입장에서 구성도를 그려보았습니다.


                     ┌─────────────────────────────────────────────────────────── Amazon Web Service  ────────────────────────────────────────────────────────────────────────────────────┐
                     │                                                                                                                           ┌────────────┐                           │
                     │                                                                                                                           │            │                           │
                     │                                                                                                                     ┌─────┤ Redis      │                           │
┌─────────────┐      │                           ┌─────────────┐          ┌────────────┐      ┌──────────────┐        ┌───────────┐        │     │            │         ┌────────────┐    │
│             │      │     push                  │             │  mirror  │            │      │              │        │           │        │     └────────────┘         │ Cloud      │    │
│  Developer  ├──────┼─────────────────────────► │   Gitlab    │ ───────► │ CodeCommit ├──────┤ CodePipeline ├────────┤ CodeBuild │        │                            │ Watch      │    │
│             │      │                           │             │          │            │      │              │        │           │        │                            │            │    │
└─────────────┘      │                           └─────────────┘          └───┬───┬────┘      └──────────────┘        └───────────┘        │                            └─────┬──────┘    │
                     │                                                        │   │                                                        │     ┌────────────┐               │           │
                     │                                                        │   │                                                        │     │ RDS        │               │           │
                     │                                                        │   │                                                        ├─────┤ Postgresql │               │           │
                     │                                                        │   │                                                        │     └────────────┘               │           │
┌─────────────┐      │              ┌─────────────┐        ┌───────────┐      │   │       ┌─────────┐                        ┌─────────┐   │                                  │           │
│             │      │              │             │        │           │      │   └───────┤         │                        │         │   │                                  │           │
│ Angular     ├──────┼──────────────┤ CloudFront  ├────────┤   S3      ├──────┘           │   ECS   ├────────────────────────┤ Fargate ├───┤                               scheduler      │         
│             │      │              │             │        │           │               ┌──┤         │                        │         │   │                                  │           │
└──────┬──────┘      │              └──────┬──────┘        └───────────┘               │  └───────┬─┘                        └─────────┘   │     ┌────────────┐          ┌────┴───────┐   │      ┌────────────┐    ┌────────────┐
       │             │                     │                                           │          │                                        ├─────┤ DynamoDB   ├──────────┤ Lambda     ├───┼──────┤ Interface  ├────┤ Oracle     │
       │             │                     │                                           │          │                                        │     │            │          │            │   │      │ API        │    │            │
┌──────┴──────┐      │                     │                                           │          │                                        │     └────────────┘          └────────────┘   │      └────────────┘    └────────────┘
│  SSO        │      │                     │              ┌─────────────┐              │          │                                        │                                              │
└─────────────┘      │                     │              │             │              │          │                                        │                                              │
                     │                     └──────────────┤ ApiGateway  ├──────────────┘          │                                        │                                              │
                     │                                    │             │                 ┌───────┴─┐                                      │     ┌────────────┐                           │
                     │                                    └─────────────┘                 │   ECR   │                                      │     │ Elastic    │                           │
                     │                                                                    │         │                                      └─────┤ Search     │                           │
                     │                                                                    └─────────┘                                            └────────────┘                           │
                     │                                                                                                                                                                    │
                     └────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

배포 과정

  1. developer 는 private 으로 구축한 gitlab 서버에 source 를 push 합니다.

    • frontend : angular
    • backend : springboot
  2. gitlab 에서는 mirroring 이 작동하여 CodeCommit 으로 소스를 넣습니다.

  3. CloudWatch 에서 CodeCommit 을 감지하고 있다가 CodePipeline Job이 실행되게 됩니다.

  4. Codepipeline 은 사전에 정의된 CodeBuild 를 작동시키고 아래 2가지 경우에 따라 다르게 Deploy 합니다.

    • frontend 는 빌드된 html/js 파일이 S3 로 업로드 하고
    • backend 는 Dockfile 로 말려진 docker image 가 ECR에 업로드가 되고, ECR 에서는 ECS Task 에 의해서 Fargate 형태로 deploy 됩니다.

git brnach 전략

  • git 을 사용하는 것보다 git branch 전략을 세우고, 그것을 준수하는 것이 더 중요합니다.
  • 보통 두개의 branch 를 관리하는 github(main-develop) 방식과, 5개의 브랜치를 관리하는 git-flow 방식(main-feature-develop-hotfix-release)이 있습니다.
  • 현 환경에서는 스테이징 환경이 없기 때문에 3개의 브랜치를 관리를 관리하는 것으로 사용하도록 하였습니다. 향후에 필요한 경우 stage 브랜치를 별도로 만들어 같은 방식으로 관리합니다.
  • 각 브랜치의 역활은 다음과 같습니다.
구분 내용
release prd 환경애 배포된 상태
main 언제든 배포가 가능한 상태 prd 에 이미 배포가 되어 있을수도 있습니다.
develop 개발 환경 배포
  • 개발자는 로컬에서 개인브랜치를 생성하여 작업을 하고 develop 브랜치 merge 합니다.
  • 담당자는 develop merge 된 것을 확인합니다.
  • 확인이 완료된 소스는 main 브랜치에 merge 합니다.
  • 배포 관리자는 main 브랜치에서 특정 시점의 commit 내역을 release 로 merge 하여 사전에 구성된 CI/CD 환경으로 운영환경에 배포하도록 합니다.

주의할 점)

  • release 브랜치는 main 브랜치를 거치지 않고 push 할수 없습니다.
  • 운영환경 핫핏스가 필요한 경우 release 브랜치를 가져와 수정한 후에 develop -> main -> release 순으로 merge 합니다.
  • 만일, develop -> main 브랜치가 충돌나는 경우 본인의 핫픽스 내역을 main 에 적용시켜 놓습니다. 그리고 역으로 develop 으로 병합시켜 놓습니다.
  • main 브랜치는 언제든 배포가 가능한 상태를 유지해야 합니다.

작업 흐름

  1. end user는 CloudFront 에 정의된 주소를 입력하고, 웹사이트에 접근합니다.
  2. CloudFront 에서는 S3 에 업로드된 빌드된 html/js 을 end-user 에게 내려주게 됩니다.
  3. end user에게 내려온 html 파일에서 로그인 버튼을 클릭합니다.
  4. id/pwd 는 CloudFront 로 전달되게 되고, CloudFront/api 주소를 감지하게 되어 Api Gateway 로 데이터를 전달합니다.(마치 web server 처럼)
  5. id/pwd 는 Api GatewayECS 를 통해 fargate 접근하게 되고
  6. Fargate 에서 postgresql 로 id/pwd를 검증합니다.
  7. 검증이 완료된 id/pwd는 JWT를 발급하고, 동시에 redis 에 데이터를 올려 놓습니다.
  8. 발급된 JWT는 다시 역방향으로 end-user에게 전달합니다.
  9. 이후 end-user는 jwt interceptor 를 통해 api에 접근을 합니다.

JWT flowchart

JWT 토큰은 아래와 같은 로직으로 구현하였습니다. access tokenrefresh token 은 아래와 같이 정의하여 사용하였습니다.

구분 내용
access token api 를 호출하기 위한 토큰, 유효기간이 짧음
refresh token access token 을 교체하기 위한 용도, 유효기간이 상대적으로 김

         ┌─────────┐           ┌─────────────┐           ┌────────────┐           ┌────────────┐
         │ client  │           │ backend api │           │     DB     │           │   redis    │
         └────┬────┘           └──────┬──────┘           └─────┬──────┘           └─────┬──────┘
              │                       │                        │                        │
        login ├──────────────────────►│                        │                        │
              │         id/pwd        │───────────────────────►│                        │
              │                       │         valid          │                        │
              │                       │                        │                        │
              │                       │────────────────────────┼───────────────────────►│
              │                       │          save spring security context           │
              │◀──────────────────────│                        │                        │
              │     access token      │                        │                        │
              │     refresh token     │                        │                        │
              │                       │                        │                        │
call some api ├──────────────────────►│                        │                        │
              │        valid          │────────────────────────┼───────────────────────►│
              │    access token       │         check spring security context           │
              │                       │                        │                        │
              │◀──────────────────────│                        │                        │
              │       200/ok          │                        │                        │
              │                       │                        │                        │
              │──────────────────────►│                        │                        │
              │       expired         │                        │                        │
              │    access token       │                        │                        │
              │                       │                        │                        │
              │◀──────────────────────│                        │                        │
              │    401/unauthorized   │                        │                        │
              │                       │                        │                        │
              │                       │                        │                        │
              ├──────────────────────►│                        │                        │
              │        valid          │                        │                        │
              │    refresh token      │────────────────────────┼───────────────────────►│
              │                       │           save spring security context          │
              │                       │                        │                        │
              │◀──────────────────────│                        │                        │
              │    new access token   │                        │                        │
              │    new refresh token  │                        │                        │
              │                       │                        │                        │
              │──────────────────────►│                        │                        │
              │       expired         │                        │                        │
              │    access token       │                        │                        │
              │                       │                        │                        │
              │◀──────────────────────│                        │                        │
              │    401/unauthorized   │                        │                        │
              │                       │                        │                        │
              │                       │                        │                        │
              ├──────────────────────►│                        │                        │
              │       expired         │                        │                        │
              │    refresh token      │                        │                        │
              │                       │                        │                        │
       logout │◀──────────────────────│                        │                        │
              │    401/unauthorized   │                        │                        │

일반적인 경우

  1. client 는 id/pwd 를 입력하여 인증 정보를 요청합니다.
  2. backend 에서는 rds 에 있는 데이터를 확인하여, role 이 포함된 JWT 토큰을 생성합니다.(access token/refresh token)
  3. backend 에서는 spring security context 인가 정보(토큰 만료일과 동일한 시간으로) 를 redis 에 저장한 후에 access token/refresh token 을 클라이언트에게 응답합니다.
  4. 이후 client 에서는 모든 요청의 헤더에 access token 을 포함하여 요청합니다.
  5. 토큰이 유효하다면, redis 에 올려 놓은 spring security 값을 확인하여 결과를 리턴하게 됩니다.

access token 이 만료된 경우

  1. 모든 토큰을 유효기간을 가지고 있으며, 만료될수가 있습니다.
  2. 만료된 토큰을 포함하여 backend call 호출 합니다.
  3. backend 는 별도의 처리 추가 처리 없이 401 으 리턴합니다.
  4. 401 응답을 받은 client 는 보관하고 있던, refresh token 으로 토큰 갱신을 요청합니다.
  5. backend 는 refresh token 유효성을 확인한 후에 일반적인 경우 2 단계부터 재 수행합니다.

refresh token 도 만료된 경우

  1. 이미 access token 을 포함한 요청에서 401을 받은 상태 입니다.
  2. refresh token 도 만료된 상태에서 backend 에 요청하게 되면 역시 401 을 응답 받습니다.
  3. 두개 모두 401의 응답을 받았기 때문에 client 에서는 로그아웃 처리를 하여 login 전 화면으로 이동 시킵니다.

Fargate 뒤쪽의 4개 DB 용도

구분 용도
Redis cache 혹은 token (JWT expired datetime 기간에 맞추어 자동삭제) 정보를 저장.
DynamoDB 기간계시스템으로 전달한 데이터를 저장
Postgresql 관계형 기초 코드성 데이터
ElasticSearch Fargate에서 filebeat 로 로그성 데이터를 수집

기간계 시스템과 Interface

  1. DynamoDB 에 데이터가 저장되면, lambda trigger 로 Interface API 서버를 통해 Oracle로 전송합니다.
  2. 전송 성공 여부에 따라 DynamoDB 에 데이터를 구분 플래그 처리를 합니다.
  • 성공 : 성공 플래그 처리
  • 실패 : DynamoDB 에 재시도를 위해 실패 데이터를 남기게 됩니다.
    • 매일 새벽에 Cloudwatch 에 실패한 데이터만 보아서 다시 Interface API 호출을 재시도 합니다.

Firebase Cloud Messaging 을 통한 공지사항 전송

CloudWatch rule 에 일정시간에 대상자에게 lambda 로 FCM을 통해 push 메세지를 보내게 됩니다.

마무리

100% Serverless 시스템이 구축가능할까? 프로젝트 시작시에는 이게 가능할까 싶었지만, 결국 성공하였다. 관리형 100% AWS 관리형 서비스를 이용하였고, 개발자는 Web Server, Web Application Server, DB Server 관리에 대한 장애 및 백업에 대한 걱정없이 순수하게 비지스니 로직 개발에만 집중할 수 있게 되었습니다. 역시나 가장 여려웠던 부분은 IAM 부분 이였다. AWS의 시작과 끝 IAM 인거 같습니다. 어떤 서비스에게 어떤 권한까지 부여해야 할지, 키 관리는 어떻게 해야할지를 사전에 결정하는 것이 어려웠습니다.

추가로 X-Ray 까지 넣어서, 서비스 map 까지 적용하여 한눈에 데이터 흐름을 한눈에 파악할 수 있도록 구성하였습니다. 향후 cloud 로 전환시에 이 경험을 바탕으로 조그나마 도움이 될 수 있을거 같습니다.

이름 처럼 뜬 구름 잡는(?) cloud 를 직접 종합적으로 경험해 보고, 전체를 약간 이나마 이해할 수 있었던 좋은 경험이였습니다. 파트별 담당자와 조각난 노하우를 이것 저것 모아 완료하는데 결국 팀원들의 도움이 가장 컷기에 감사드립니다.

기타

구분 내용
frontend angular 11, andorid kotlin, iOS swift
backend API Springboot Restful API, JPA, Docker, JWT
lambda nodejs