회사에서 뿌리는 공짜 바우처가 있길래 어떻게든 회사의 등골을 빨아먹고자 또다시 자격증을 치게 됬다.

사실 DOP-C02는 이전에 응시하려고 신청했다가 공부를 제대로 안해서 노쇼를(...) 하고 말았다. 적어도 쳐보고 감이라도 잡았어야 했는데.

 

 

건강상의 이슈와 이런저런 일들이 겹쳐서 공부는 많이 못했다..

원래 스테판 마렉씨의 강의도 좀 듣고 모르는 서비스는 document 찾아가며 딥다이브 하고 덤프도 넉넉하게 150개 넘게 풀고.. 

그랬어야 했는데 이번에는 개념정리고 뭐고 그냥 냅다 덤프부터 풀었다. 그리고 100개 밖에 못풀었다..ㅋ

 

사실 좀 미련하게 공부를 하는 편인데.. 그래서 덤프는 구매해도 해설서는 구매하지 않고 혼자서 똥꼬쇼하면서 오답정리하는 편이다.

심지어 영문 덤프.. (기계번역이 이상하기 때문에 차라리 원문으로 보는 게 낫다고 생각하는 편이다) 

문제 번역... 해석.... 풀기... 오답정리... 해설정리...

미련하게 공부하는 인간.... 차라리 한글이었으면 50개는 더 봤겠다 싶다.

 

시험은 늘 치던 곳... HWG테스팅 강남센터로 가서 쳤다. 

12시 30분 시험인데 택시가 막혀서 12시 25분에 시험장에 들어섰다; 안내 메일에는 15분 전에 도착하지 않으면 응시가 거부될수도 있다고 그랬는데 다행히도 감독관님이 너그러우신 분인지.. 반깁스를 발에 차고 절뚝거리는 내가 불쌍해 보였는지.. 그냥 응시하게 해주셨다.

 

문제는 75문제.

공부가 제대로 안되서인지.. 풀다가 이게 뭔소린고 싶었던 순간들이 굉장히 많았고.. 시간이 부족한건 아니었으나 75문제가 이렇게 많았나? 싶었다. 겁나 어려웠다... ANS-C01 쳤을때보다 더 빡셌던 느낌; 시간도 1시간 40분 정도 걸려서 풀었던 것 같다. 

 

문제를 100개밖에 못풀어서 그런지 처음 듣는 서비스들도 있었다. 그런 것들은 그냥.. 대충 제시문들 읽어보고 말이 안되겠다 싶은걸 삘(?)로 솎아내면서 찍었다.

 

기계 번역도 그날따라 유독 이상한 느낌이었다. 특히 IAM role 관련해서 assumeRole, permission grant의 주체와 allow의 주체를 명확히 구분해야 하는데 헷갈려서 결국 영어로 다시 봤다. 덤프를 영문으로 공부해서 참 다행이었다..

 

밤 10시가 되어도 메일이 안오길래 뭐가 이렇게 느려? 빨리 불합격인지 알려줘야 내가 재응시 날짜를 잡을거 아니냐, 하고

바로 https://aws.training/certification 사이트에 들어가봤는데 생각지도 못하게 PASS가 떠있었다;

 

간당간당한 점수

 

37점만 부족했어도 그대로 불합격... 어찌저찌 잘 찍었나 보다... 회사에서 그래도 cloud/k8s를 사용하고 있는게 시험치는데 도움은 줬구나 싶었다. 

 

사실 2달 뒤에 (또 회사의 등골을 빨아먹고자) SCS-C03 도 신청해놨는데... 오늘 시험 불합격하면 그냥 시험 변경해서 DOP-C02 다시 칠 생각이었다. 무튼 간에 그렇게 하지 않아도 참 다행?스럽게 됬다.

 

 +) 덤프는 카톡 오픈채팅에서 그냥 돈주고 구함

 

 

우리 회사에서는 Azure와 AWS 둘 다 사용하고 있는지라 Azure 에 대한 역량 역시도 필요로 했다.

AWS와 달리 Azure의 유효기간은 1년(...) 이라 선뜻 취득하고 싶은 마음이 들지는 않았지만,

어쨌든 윗분들이 주니어들을 평가할만한 게 자격증만큼 또 좋은게 없으니까... 일단 가장 기본격이 되는 AZ-104를 취득해보기로 했다.

(AZ-900은 너무 기본이라 굳이 취득할 시도는 하지 않았다.)

 

일단 대충 ExamTopics에 들어가서 Azure 문제들은 어떤 식으로 나오나~ 하고 한번 봤는데...

초반 50문제까지는 진짜 그놈의 Entra ID (구: Azure AD) 관련 문제들이 많아서

라이선스는 무엇이고 M365그룹이니 Security Group이니 하는건 또 무엇이니... 

 

인프라는 커녕 일단 저놈의 Entra ID 부터 정복을 해야 문제를 풀 수 있겠다 싶어 Microsoft Learn에서 무료로 제공하는 학습 모듈을 좀 공부해보기로 했다. (AZ-104 학습 모듈)

 

아래에 보면 이렇게 모듈들이 쭈욱 있는데.. 그냥 공부하는 김에 다 봤다. 실제로 실무에 써먹어야 하는 부분도 있으니. 근데 실제로 꽤 많이 도움이 되었던 것 같다.

 

 

Microsoft Learn 모듈을 좀 공부하고 나서 ExamTopics 덤프를 풀기 시작했다. 

그럼에도 불구하고 Entra ID (Azure AD) 문제는 좀 많이 헤매긴 했다. 특히 Azure AD -> Entra ID 로 바뀌면서 deprecated된 기능들이 꽤나 있었고 과거와 현재 기준으로 답이 달라지는 문제들이 상당히 있어서 그 문제들은 따로 분류를 해놓고 봤다.

 

그래도 초반 50문제쯤 넘기고 나면 이제 Azure Resource 위주로 문제가 나오고, 여기서부터는 좀 익숙해서 그럭저럭 풀 수 있었다.

다만 역시나 과거와 현재 기준으로 답이 다른 문제들이 있어서 그런 문제들은 표시를 해 두었다.

 

예를 들어, 같은 테넌트 내에서 (같은 지역이라고 전제를 할 때) subscription을 옮길 수 있는 리소스를 묻는 문제가 있었는데,

예전 같았으면 Recovery Services Vault는 이동이 불가했으나, 이제는 같은 테넌트 + 같은 지역일 경우 subscription을 옮길 수 있게 됬다. 아무튼 틈틈히 Azure 공식 문서를 확인해보면서 내 나름의 해설들을 정리해 나갔다.

 

어쨋건 대충 2~3주 정도? 1주 정도는 ms learn 모듈 + 공식 문서로 좀 공부하고.. 남은 2주는 덤프 풀고 오답정리하고..

 

추석 연휴까지 끼고 해서 더더욱 공부하기 싫어서... 120문제까지밖에 못 보고 그냥 시험을 보러 갔다.

온라인으로 칠까 하다가 본인인증에 발목잡히기 싫어서 그냥 편하게 오프라인 시험을 보러 갔다.

 

테헤란로4길 31 (인예지 빌딩) 2층에 위치한 앤아버 어학원 이라는 곳이었는데, 늘 치던 HWG 강남 센터에 자리를 못잡아서 차선책으로 여기를 왔는데 나름 강남역에서 가까워서 괜찮은 것 같다.

 

다만 AWS 시험과는 달리 무진장 일찍 시험을 친다... 아침 9시...

휴일인데 출근하는 시간대처럼 일어나서 시험을 보러 갔다.

 

여권과 신분증 둘 다 확인받고 얼굴 사진 찍고 입실 시간 적고 짐 다 넣고 들어가서 시험 치고... 다 치면 알아서 나오고... 끗.

 

AWS 시험과 달리 더 좋았던 건 Microsoft Learn 검색이 된다! 사실 몰랐는데 옆 사이드바에 무슨 기능들이 잔뜩 있길래 뭐지 하고 클릭해봤는데 어쩌다 알아냈다; 하마터면 모르고 그냥 답안 제출할뻔. 덕분에 한 5문제는 답 고쳐서 맞춘 것 같다. ㅎ

 

점수는 답안 제출하자마자 바로 화면에 나온다. 합격 여부와 함께. 이것도 매우 좋았던 것 같다.

덕분에 홀가분하게 시험장 나와서 편한 마음으로 점심을 먹을 수 있었다.

 

합-격

 

+)

Github에 누군가 ExamTopics의 문제들을 그대로 pdf로 올려놨다. 덕분에 잘 봤던 것 같다. (여어기)

다만 discussion까지 보려면 일일히 구글에 문제의 일부를 검색해서 봐야 한다. 근데 그새 조치를 해놨는지 검색해서 들어가면 3초 보여주고 구매하라는 모달이 떠서 화면을 방해한다.

AWS Certified Developer Associate (DVA-C02) 을 취득한지 어언 1년 후, 

이번에는 AWS Certified Advanced Networking - Specialty (ANS-C01) 을 취득하게 되었다.

 

다른 자격증을 두고 이걸 도전하게 된 이유는,

회사에서 대부분의 Cloud 아키텍처가 온전히 Cloud Native가 아닌, Hybrid 아키텍처가 대부분이었기 때문이었다.

 

대다수의 서비스들이 Transit Gateway (TGW) 을 통해 Multi-VPC 를 통합하여 east-west communication 및

Direct Connect 전용선으로 기존 data center와의 bidrectional communication을 수행하고 있었다.

 

처음에 아키텍처를 봤을 땐 대체 뭐가 뭔지 알 수 없었고 각 서비스들이 뭔지 모르니

traffic의 대체 왜 여기서 저기로 흐르는 지도 알 수 없었다. 

 

그래서 결국 AWS의 Networking Service들을 공부할 겸 ANS-C01를 취득해보기로 했다.

 

 

공부 방법

처음에는 무작정 dump로 풀려고 했다. DVA-C01을 그런 방식으로도 충분히 취득했었기 때문이었다. 

근데 VPC, ELB, Route53 까지는 그럭저럭 document를 참고하면서 풀 수 있었는데

 

Transit Gateway

Site-to-Site VPN

Direct Connect

 

이 3개는 진짜 document를 몇 번이고 참고해도 쉽사리 문제를 풀 수 없었다.

게다가 저 3개는 문제에서 한 개만 나오는 게 아니라 최소 2개씩은 결합된 아키텍처에 관해 묻는 경우가 많아서 제대로 알아둬야 했다.

 

그래서 일단 dump를 푸는 건 잠시 중단하고 바로 udemy에서 관련 강의를 검색해서 들었다.

 

 

AWS 자격증을 취득하려는 사람들이 많이 보는 Stephane Maarek 선생님의 강의.

근데 ANS-C01 강의는 Stephane 선생님이 처음에 인사만 해주실 뿐 나머지 강의는 죄다 Chetan Agrawal 분이 다 해주신다;

 

인도 출신이셔서 그런지 처음에 영어 발음을 알아듣기 힘들어서 허들이 좀 있었지만 영어 자막 켜놓고 듣다 보면 상당히 익숙해진다. 

 

어쨌거나 가격은 거의 10만원 가까이 하는 고가이지만 허구한날 세일을 때리는 Udemy 덕에 14000원을 주고 결제했다.

 

모든 강의를 다 듣지는 않았고, 위의 저 3가지 (Transit Gateway, Site-to-Site VPN, Direct Connect) 만 들었다. 시간만 더 있었으면 Route 53 쪽의 DNS 관련 부분 (Private Hosted Zone, IB/OB endpoint) 들도 듣고 싶었는데, 일단 저 3개만 들어도 어지간한 덤프는 다 풀 수 있었다.

 

덤프는 examtopics에서 7만원 넘게 주고 263개의 문제를 전부 구매했지만 실질적으로 140개밖에 못 보고 시험장에 들어가긴 했다;

 

 

시험 후기

DVA-C01도 그렇고 이번 시험도 어김없이 실제 시험장에 가서 시험을 쳤다. 

 

내가 시험을 친 곳은 강남역 바로 인근의 HWG Testing Center.

 

 

시험시간은 16:45 ~ 17:45 였는데 실제로 시험시간은 한 2시간 넘게 준다.

45분보다 일찍 도착해도 그냥 시험자 신원 확인하고 난 뒤 바로 시험을 칠 수 있게 해준다.

 

신분증이랑 신용카드(체크카드) 둘 다 준비해서 가면 감독관님께서 신원 확인하시고 시험장으로 안내해주신다.

그리고 준비된 컴퓨터에서 바로 문제를 풀고 다 풀고 나면 CCTV 쪽으로 손을 들면 감독관님께서 시험 종료 확인해주시고 밖으로 보내주신다.

 

시험은 한국어로 응시했는데, 한국어로 해도 영어 원문을 볼 수 있게 해주니 기계 번역의 어색함으로 인해 문제를 해석하기 힘들다면 그냥 영어 원문을 참고하면 된다. 나는 덤프를 영어로 푸는 연습을 해서 그런지 한국어 문제를 빠르게 읽을 수 있었다.

 

총 65문제였고, 좀 미심쩍은 문제는 플래그(flag) 표시를 해두고 나중에 다시 그 문제로 돌아와서 검토할 수 있다.

 

 

결과

총 65문제였고, 문제를 다 풀고 시험을 치고 나오니 배고픔이 밀려들어서 결과는 잊어버리고 그냥 밥 먹고 자유를 만끽하며 노는 데만 집중했던 것 같다. 그러다가 지금쯤 결과 나왔으려나? 하고 20시쯤 메일함을 열어봤는데,

 

합격이었다! 

사실 풀면서 대부분의 문제가 덤프랑 똑같아서 (제시문 순서도 안 바꾸고 그대로 낸 문제들이 많았다;) 어지간한 이상은 대부분 다 풀었던 것 같다. 다만 점수는 내 예상보다 좀 많이 낮았다.

 

 

 

800점은 넘었을거라 생각했는데.. 여전히 제대로 이해하지 못하고 헷갈려한 부분들이 있었나 보다... 어쨌거나 합격했으니 됬다. 

어쨌거나 공부하면서 aws hybrid architecture를 봐도 더 이상 아무것도 몰라서 헤매지는 않을 것 같다.

 

+) Certified Developer Associate (DVA-C02) 취득후기는 여기

https://velog.io/@devhslee02/AWS-DVA-C02-%ED%95%A9%EA%B2%A9-%ED%9B%84%EA%B8%B0

 

[AWS] Certified Developer (DVA-C02) 합격 후기

백엔드 커리어를 시작한 지 만 1년,회사에서 AWS 서비스를 좀 만지다 보니 더 공부를 해야겠다 싶었고, 내친김에 자격증도 따자 싶어서DVA-C02 (Certified Developer) 공부를 시작했었다.공부 기간은 2월

velog.io

 

 

 

ETCD 란

etcd는 Kubernetes Cluster의 모든 상태 정보를 키-값 형태로 저장하는 데이터베이스이다.

etcd에 장애가 생기면 모든 컴포넌트가 미아가 되기 때문에 가용성이 매우 중요하다. 그래서 보통 이중화, 삼중화 이상을 해 놓는다.


etcd는 클러스터링하여 분산 실행하는 RSM (Replicated State Machine) 구조로 되어있다.

간단히 말해 Leader-Follower 구조인데, Leader node가 클라이언트의 요청을 처리하다가 문제가 생기면 새로운 Leader node가 선출되어 계속해서 가용성을 유지하는 구조이다.  (Follower는 Leader의 상태를 복제해둔 거라고 보면 된다)


etcd는 서버 하나당 프로세스 한 개당 사용할 수 있기 때문에 보통통 etcd 자체를 클러스터링 한 다음 마스터 서버에 분산을 하여 안전성을 보장하는 방식으로 아키텍처를 구성한다.

 

 

minikube를 통해 etcd 살펴보기

minikube를 통해 간단히 etcd의 구성을 살펴보겠다.

 

minikube start

kubectl describe pod etcd-minikube -n kube-system

 

 

ETCDCTL


etcdctl은 etcd를 다루기 위한 유틸리티 (cli) 이다. v2와 v3를 사용할 수 있는데, 기본적으로 v2를 사용한다.

이 명령어를 사용할 때는 etcd api server에 접근하기 위해 인증서 파일 경로를 지정해야 한다.

 

 

etcdctl를 사용하여 etcd 데이터베이스에서 '/registry/pods/' (Kubernetes의 모든 pod가 저장된 곳) 경로에 저장된 키를 가져오는 예시를 수행해보겠다.

kubectl exec -it \
-n kube-system etcd-minikube \
-- sh -c 'ETCDCTL_CACERT=/var/lib/minikube/certs/etcd/ca.crt \
ETCDCTL_CERT=/var/lib/minikube/certs/etcd/peer.crt \
ETCDCTL_KEY=/var/lib/minikube/certs/etcd/peer.key \
ETCDCTL_API=3 \
etcdctl \
get \
--keys-only \
--prefix=true \
"/registry/pods/" '

 

위의 명령어를 수행하면 아래와 같이 '/registry/pods/'로 시작하는 모든 경로를 가져오는 것을 확인할 수 있다.

 

위의 명령어를 좀 더 자세히 설명해보자면 다음과 같다.

kubectl exec -it \ # etcd-minikube 파드에 접속하여 명령을 실행하기 위해 'kubectl exec' 사용, -it 옵션은 인터랙티브 모드 설정
-n kube-system etcd-minikube \ # 'kube-system' 네임스페이스의 'etcd-minikube' pod를 지정
-- sh -c 'ETCDCTL_CACERT=/var/lib/minikube/certs/etcd/ca.crt \ # CA 인증서 경로 설정, etcd 서버 인증서 검증용
ETCDCTL_CERT=/var/lib/minikube/certs/etcd/peer.crt \ # 클라이언트 인증서 경로 설정, etcd 서버와의 통신용
ETCDCTL_KEY=/var/lib/minikube/certs/etcd/peer.key \ # 클라이언트 인증서에 대응하는 프라이빗 키 경로 설정
ETCDCTL_API=3 \ # etcd API 버전을 v3로 설정
etcdctl \ # etcd와 상호작용하기 위한 CLI 도구 실행
get \ # etcd 데이터베이스에서 데이터를 조회하는 명령
--keys-only \ # 키만 가져오도록 설정, 값은 포함하지 않음
--prefix=true \ # 지정된 prefix와 일치하는 모든 키를 검색
"/registry/pods/" ' # Kubernetes의 모든 pod 정보가 저장된 경로 prefix

 

 

+) etcd 에 값 저장하고 읽기

etcd는 데이터베이스니까 값을 저장하고 읽는 것도 가능하다.

etcd pod의 container shell에 접속해서 키-값을 저장하고 읽는 명령을 실행해보도록 하겠다.

kubectl -n kube-system exec -it etcd-minikube -- /bin/sh

 

아래와 같이 etcd shell에 접속이 된 걸 확인할 수 있다.

 

 

간단히 'Name'이라는 키를 저장해보도록 하겠다.

ETCDCTL_API=3 etcdctl \ 
 --cacert /var/lib/minikube/certs/etcd/ca.crt \
 --cert /var/lib/minikube/certs/etcd/peer.crt \
 --key /var/lib/minikube/certs/etcd/peer.key \
 put Name kubernetes

 

키 저장에 성공하면 아래와 같이 'OK'라는 문장이 뜬다.

 

 

위에서 저장한 키를 다시 읽어보도록 하겠다.

ETCDCTL_API=3 etcdctl \ 
--cacert /var/lib/minikube/certs/etcd/ca.crt \
--cert /var/lib/minikube/certs/etcd/peer.crt \
--key /var/lib/minikube/certs/etcd/peer.key \
get Name

 

위에서 넣은 값이 그대로 출력되는 것을 확인할 수 있다.

 

 

모든 클러스터 정보 조회

클러스터의 모든 리소스 정보를 조회하려면 get 명령어의 프리픽스를 클러스터의 모든 리소스가 저장된 최상위 경로인 "/"로 설정해주면 된다.

ETCDCTL_API=3 etcdctl \ 
--cacert /var/lib/minikube/certs/etcd/ca.crt \
--cert /var/lib/minikube/certs/etcd/peer.crt \
--key /var/lib/minikube/certs/etcd/peer.key \
get / --prefix --keys-only

 

그러면 아래와 같이 모든 구성 정보가 출력되는 것을 확인할 수 있다.

'Kubernetes' 카테고리의 다른 글

[Kubernetes] 쿠버네티스를 사용해야 하는 이유  (0) 2024.08.01

오늘날 많은 서비스들이 Container를 통해 운영되고 있다.

 

개발과 운영환경 간의 동일 서비스 보장, 대규모 시스템에서의 마이크로서비스 등 Container가 쓰이고 있는 이유는 정말 다양하다.

하지만 단순히 Container 기술뿐만 아니라 이렇게 다량으로 운영되고 있는 Container를 관리해줄 중앙 구심점 역시도 필요하다.

 

Kubernetes 란

 

 

 

Kubernetes, 줄여서 K8S라고도 부르는 쿠버네티스는 Google에서 개발한 오픈소스 Container Orchestration 플랫폼이다.

 

Container Orchestration 도구는 Kubernetes 말고도 RedHat 사의 OpenShift 등 여러가지가 있지만 

 

완전 오픈소스인데다 어디서든 Reference를 구할 수 있을 만큼 생태계가 풍부하고, On-premise나 Cloud Native 등 환경에 구애받지 않고 어디서든 구동할 수 있다는 점 때문에 현재 Container Orchestration의 표준이 되었다.

 

 

Container Orchestration 이란

 

간단히 말하자면 그냥 수많은 Container들을 관리해주는 것인데, 아래의 Action들을 포함한다

 

자동화된 스케일링

- 트래픽의 변동에 따라 Container를 자동으로 증가시키거나 감소

 

자동화된 Roll-out, Roll-back

- Roll-out: 업데이트 시, 점진적으로 새 버전을 배포하고 모니터링하여 문제가 없으면 점차적으로 전체 시스템에 배포

- Roll-back: 문제가 발생하면 자동으로 이전 버전으로 롤백하여 서비스 중단을 최소화

 

자동화된 복구
- 컨테이너 상태를 계속 감시하고 있는 control tower 필요

- 문제가 발생한 Container를 재시작하고 가용성을 높이고 다운타임을 감소

 

자동화된 Bin Packing

- Bin packing: 리소스를 최대한 효율적으로 사용하기 위해 컨테이너를 클러스터 노드에 자동으로 배치하는 기능

 

시크릿과 구성 관리

- 애플리케이션에서 사용하는 secret을 안전하게 저장

- 환경 설정이나 구성 파일을 중앙에서 관리하여 운영의 일관성을 유지


Service Discovery와 로드 밸런싱

- 각 서비스가 서로 통신할 수 있도록 네트워크를 설정

- 로드 밸런서를 사용하여 Container 간에 트래픽을 균등하게 분배하여 특정 Container에 과부하가 걸리지 않도록 관리

 

Storage Orchestration
- 클라우드 기반의 스토리지 서비스나 로컬 디스크를 Container에 mount

 

IaC를 사용한 운영

- 인프라 설정을 코드로 정의하고 자동으로 provisioning

 

 

Kubernetes의 구성

출처: Kubernetes 공식 페이지

 

Kubernetes는 크게 Control PlaneData plane으로 나뉜다

 

Control plane (Master node) 은 Kubernetes 전체를 통제/관리하는 역할을 담당하며, 아래의 4가지를 포함한다.

  • kube-apiserver
  • etcd
  • kube-scheduler
  • kube-controller-manager

 

Data plane (Worker node) 은 실제 Container Runtime이 있는 부분이며, 실제 애플리케이션이 배포되는 곳이다.

아래 4가지를 포함한다.

  • kubelet
  • kube-proxy
  • continer runtime

 

사용자가 Kubernetes를 사용하기 위해 kubectl (보통 '쿠베시티엘' 이라고 발음) 이라는 명령어를 사용하는데, 이 명령어는 Control plane 으로 전달되며 Container 워크로드에 대한 작업을 수행하게 된다.

 

 

 

 

'Kubernetes' 카테고리의 다른 글

[Kubernetes] ETCD  (0) 2024.08.01

RTM에서 가상머신(CentOS 7)을 접속하여 스크롤 사용을 위한 패키지를 설치하려고 했는데 

아래와 같은 에러가 발생했다.

 

 

?? 이게 뭔 소리지

 

혹시나 네트워크 설정이 안되어 있나 싶어서 ping도 쏴보고 DNS 설정까지 확인했는데 네트워크 문제는 아닌 것 같았다.

Yum 리포지토리 설정이 잘못되어 있는 듯 했다.

 

 

Base 리포지토리 설정을 우선 확인해봤다.

sudo vi /etc/yum.repos.d/CentOS-Base.repo

 

그리고 모든 섹션의 mirrorlist 부분을 주석처리하고 baseurl을 활성화해주고

 

yum 캐시를 모두 지워줬다.

sudo yum clean all

 

그리고 다시 캐시를 생성해주려는데

sudo yum makecache

 

 

 

이번엔 baseurl이 유효하지 않아서 생긴 문제인듯했다. 

 

열심히 구글링을 해서 찾아낸 다른 baseurl로 교체해주고 다시 yum 캐시를 생성해줬다.

 

baseurl=http://vault.centos.org/altarch/7/os/$basearch/

 

 

sudo yum clean all
sudo yum makecache

 

 

대충 패키지 아무거나 깔아봤다. 어쨌든 해결!

 

UTM에서 초기에 CentOS 7 (또는 다른 운영체제)를 설치하기 위해

이미지 파일(iso)을 등록하고, 메모리와 디스크까지 할당해준 후 

 

 

위와 같이 'install CentOS 7'을 눌러 네트워크 설정, 파티셔닝, 루트 계정 비밀번호 설정, 사용자 계정까지 생성해 준 후에 

reboot를 눌러줬음에도 불구하고 다시 install 화면이 뜬다면,

 

우선 해당 가상 머신을 잠시 stop하고 설정에 들어간다 (빨간색 네모박스)

 

그러면 왼쪽 카테고리에서 아래쪽을 보면 Drives 부분이 있을텐데,

 

거기서 연결된 ISO 이미지를 제거하기 위해 해당 드라이브를 Delete 해줘야 한다.

 

그리고 나서 다시 그 가상 머신을 실행해보면,

비로소 CentOS 에 로그인할 수 있게 된다....

 

설치가 완료된 후엔 설치 디스크를 제거하여 더 이상 ISO 파일에서 부팅이 되는 것을 방지해줘야 하는 것 같다.

'Linux' 카테고리의 다른 글

[Linux/CentOS 7] yum: Could not retrieve mirrorlist 에러  (0) 2024.07.02

application bootstrap 전이 아니라 그 이후, 즉 런타임에서 환경변수를 읽어와야 할 때가 있다.

 

가령 TypeOrmModule의 경우, DataSource 초기화를 위해 필요한 모든 커넥션 정보들이 환경변수로부터 다 읽어져와야 해서 데이터베이스 관련 환경변수들이 빠졌는지 아닌지는 쉽게 확인할 수 있다.

 

그러나 모듈 초기화에서 체크하지 않는, 메서드 내부에서 그 때 그 때 읽어서 활용하는 환경변수의 경우 env에 없어도 애플리케이션 실행에 문제가 되지 않는다. 그 변수를 호출하는 쪽에서 에러를 내면 그제서야 알아차리는 식이다.

 

Joi 라는 잘 만들어진 라이브러리를 사용하여 validation schema를 만들고, 이를 ConfigModule에 등록해둔다면 런타임에서 환경변수가 없다거나 유효하지 않다는 에러를 맞닥뜨리지 않아도 될 것이다.

 

Joi 설치

 

Joi 라이브러리를 설치해준다.

npm i joi

 

혹시 @nestjs/config가 없다면 이 역시 설치해준다

npm i @nestjs/config

 

Schema 작성

 

애플리케이션에서 사용해야 하는 모든 환경변수들을 schema에 다 정의해준다.

 

schema 이름은 대충 아무거나 짓고,

변수의 이름과 타입 (필요하다면 세부 타입까지)을 Joi.object 안에다 적어준다.

// env-validation.ts

import Joi from 'joi';

const JoiValidationSchema = Joi.object({
  NODE_ENV: Joi.string()
    .valid('development', 'production', 'local')
    .default('local'),
  PORT: Joi.number().port().default(3000),

  DB_HOST: Joi.string(),
  DB_PORT: Joi.number().port(),
  DB_USER: Joi.string(),
  DB_NAME: Joi.string(),
  DB_PASS: Joi.string(),

  ADMIN_MAIL: Joi.string().email(),
  ADMIN_AUTH_URI: Joi.string().uri()

  REDIS_HOST: Joi.string().ip(),
  REDIS_PORT: Joi.number().port().default(6379),
  REDIS_SESSION_TTL: Joi.number().default(3600),
  REDIS_PASSWORD: Joi.string(),
});

 

Schema 적용

 

AppModule 내부에 ConfigModule를 import해준 후, 아까 작성한 schema를 등록해준다.

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { JoiValidationSchema } from './config/env-validation';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      validationSchema: JoiValidationSchema
    }),
    ...
  ],
  controllers: [...],
  providers: [...],
})
export class AppModule {}

 

환경변수는 앞으로도 계속 추가되거나 변경될 여지가 있다. 그럴 경우엔 일일히 개발자가 Joi Schema 역시 업데이트해줘야 한다.

 

하지만

본인이구현해놓고

 Joi schema를 업데이트해야 한다는 사실을 까먹을 수도 있으니 환경변수 파일을 읽어서 schema와 싱크를 맞추도록 조치를 취해줘야 한다.

 

우선 위의 JoiValidationSchema 밑에 환경변수를 읽어오는 함수를 추가해줬다.

오로지 키(변수 이름)만 가져오도록 했고, 주석('#')은 제외했다.

function parseEnvContent(content) {
  const lines = content.split('\n');
  const envObject = {};
  lines.forEach((line) => {
    if (line && !line.startsWith('#')) {
      const [key, value] = line.split('=');
      envObject[key.trim()] = value.trim();
    }
  });
  return envObject;
}

 

환경(NODE_ENV)별로 변수를 각각 검사해서 세부 에러를 잡아내기 위해 아래의 함수도 추가해줬다.

function validateEnvSchema(nodeEnv: string, envFilePath: string, schema: Joi.ObjectSchema) {
  const envFileContent = fs.readFileSync(envFilePath, 'utf8');
  const envObject = parseEnvContent(envFileContent);

  const envKeys = Object.keys(envObject);
  const schemaKeys = Object.keys(schema.describe().keys);

  const missingKeys = envKeys.filter(key => !schemaKeys.includes(key));

  if (missingKeys.length > 0) {
    console.error(`[${nodeEnv}] Missing keys in Joi schema:\n${missingKeys.join('\n')}`);
    process.exit(1);
  }
}

 

그리고 바로 위의 함수들을 호출하게끔 해주면 된다.

// env-validation.ts
import fs from 'fs';
import path from 'path'

...

validateEnvSchema('local', path.join('.env.local'), JoiValidationSchema);
validateEnvSchema('development', path.join('.env.development'), JoiValidationSchema);
validateEnvSchema('production', path.join('.env.production'), JoiValidationSchema);

 

+) 환경별로 Schema 분리

 

만약 로컬과 배포환경 간에 필수로 요구해야 하는 변수들이 조금씩 다르다면 공통 부분만 따로 빼내어 각각 별도의 schema를 만드는 것도 가능할 것 같다.

const JoiValidationSchema = {
  /* 로컬/배포 환경에서 공통으로 쓰이는 변수들 */
};

export const DeployValidationSchema = Joi.object({
  ...JoiValidationSchema,
  /* 배포(개발계/운영계) 환경에서만 쓰이는 변수들 */
})

export const LocalValidationSchema = Joi.object({
  ...JoiValidationSchema,
  /* 로컬 환경에서만 쓰이는 변수들 */
})

...

validateEnvSchema('local', path.join('.env.local'), LocalValidationSchema);
validateEnvSchema('development', path.join('.env.development'), DeployValidationSchema);
validateEnvSchema('production', path.join('.env.production'), DeployValidationSchema);

 

이렇게 할 경우 AppModule도 수정해주면 된다.

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      validationSchema: process.env.NODE_ENV === 'local' ? LocalValidationSchema : DeployValidationSchema,
    }),
  ...
})
export class AppModule {}

 

NODE_ENV에 따라 schema를 달리 적용하도록 수정해줬다.

'NestJS' 카테고리의 다른 글

[NestJS] Guard와 Decorator로 Oauth 로그인 구현하기  (1) 2024.06.14

소위 '소셜로그인'이라 부르는 타사 플랫폼의 개인정보를 이용해 서비스에 로그인할 수 있게 하는 걸 구현하기 위해선

Oauth 2.0 표준에 맞게 백엔드를 구현해야 한다.

 

Oauth 의 구성요소

 

Oauth 인증의 구성요소, 즉 역할은 4가지가 있다.

 

  • Client: 리소스를 요청하는 주체
    • '리소스'는 단순 유저의 개인 정보뿐만이 아니라 구글 캘린더, 네이버 블로그 포스트 등 다양한 정보가 될 수 있음
  • Resource Owner: 리소스에 대한 접근을 제어하고 권한을 부여하는 주체.
    • e.g.) Google, Naver와 같은 플랫폼
  • Authorization Server: Access Token을 발급해주는 서버
  • Resource Server: Access Token을 가진 요청을 검사하여 리소스를 반환하는 서버

 

Oauth 플로우

 

(출처) RFC 6749

 

간략히 정리하면 이러하다.

 

1. Client가 Access Token을 발급받기 위한 인가(Authorization)을 요청한다.

2. 1에서 받은 인가를 통해 리소스를 발급받기 위한 Access Token을 요청한다.

3. Access Token을 통해 리소스를 요청한다.

 

하지만 아무 Client나 인가나 리소스를 요청할 수 있는 건 아니고, 어떤 Client가 인가와 리소스를 요청할 것인지Authoriation Server에 등록해둬야 한다.

 

즉, 구글이나 네이버의 소셜로그인을 구현하고자 하는 경우, 해당 제공자들의 Oauth 콘솔 같은 데 들어가서 Client를 등록하고 그 Client임을 식별할 Credentials(client id, secret)를 취득해 둬야 한다.

 

그리고 이 Client가 접근할 수 있는 리소스의 종류와 범위(scope, e.g. 이메일은 필수 동의, 성별은 이용자 선택 제공 등)를 정해준다.

 

이 때, 웹 페이지에서 인가를 얻고자 하는 경우, 정상적인 endpoint에서 요청을 보내는지 검사하기 위해 redirection URI 라는 것을 별도로 등록해야 한다. redirection URI를 등록해두고 해당 Client가 추후 올바른 Credential과 redirection URI를 가지고 인가를 요청한 경우, Authorization Server가 해당 redirection URI 뒤에 쿼리스트링으로 인가 코드를 포함시켜서 리턴해준다.

 

그렇게 얻은 인가 코드를 가지고 Credential(client id, client secret)과 redirection URI과 함께 다시 Authorization Server에 보내어 Access Token을 요청한다. 그러면 Authorization Server가 Credential을 검사하고 해당 인가코드와 그 인가코드가 발급된 uri와 redirection URI가 일치하는지 확인한 후에 Access Token을 발급해 준다.

(리소스 제공자에 따라 추가 프로퍼티가 필요할 수 있다)

 

그리고 나서 발급받은 Access Token을 가지고 리소스를 요청하면 지정해놓은 scope에 해당하는 리소스들을 Resource Server가 반환하는 식이다.

 

구글, 네이버, 카카오 통합하기

 

소셜로그인은 딱 3개 구현해 봤다. 구글, 네이버, 카카오.

 

이전에는 각 제공자별로 Guard를 따로 구현해 줬는데 잘 생각해보니 셋 다 Access Token과 user profile을 조회하는 url만 좀 다를 뿐이지 쿼리스트링으로 포함시켜줘야 할 프로퍼티는 똑같았다.

 

(네이버의 경우 state 라는 프로퍼티가 추가로 필요하긴 하다)

// google
토큰 발급 url: 'https://oauth2.googleapis.com/token',
프로필 조회 url: 'https://www.googleapis.com/oauth2/v3/userinfo',

// kakao
토큰 발급 url: 'https://kauth.kakao.com/oauth/token',
프로필 조회 url: 'https://kapi.kakao.com/v2/user/me',

// naver
토큰 발급 url: 'https://nid.naver.com/oauth2.0/token',
프로필 조회 url: 'https://openapi.naver.com/v1/nid/me',

 

 

그래서 이 세 개를 하나의 Guard로 통합해서 구현할 수 있을 것 같다는 생각이 들었다.

일단 하드코딩을 피하기 위해 제공자 타입을 enum으로 만들고 위의 url들을 일반 전역변수로 저장해 뒀다.

export enum LoginProvider {
  GOOGLE = 'google',
  KAKAO = 'kakao',
  NAVER = 'naver',
}

export const SocialLogin = {
  google: {
    tokenUrl: 'https://oauth2.googleapis.com/token',
    infoUrl: 'https://www.googleapis.com/oauth2/v3/userinfo',
  },
  kakao: {
    tokenUrl: 'https://kauth.kakao.com/oauth/token',
    infoUrl: 'https://kapi.kakao.com/v2/user/me',
  },
  naver: {
    tokenUrl: 'https://nid.naver.com/oauth2.0/token',
    infoUrl: 'https://openapi.naver.com/v1/nid/me',
  },
};

 

OauthGuard 구현

 

인가코드는 프론트 쪽에서 받아서 백엔드로 넘겨주고 있다고 가정하고 Guard에서 Access Token을 받아 바로 프로필 데이터를 요청하는 것까지 구현해 둘 것이다.

 

Guard 하나로 구글, 네이버, 카카오를 다 커버하려면 Guard에 어떤 제공자에게 요청할건지 메타데이터를 넘겨줘야 한다.

로그인을 처리하는 라우트에 @SetMetadata 로 간단히 정보를 넘겨주면 될 것 같았다.

@UseGuards(OAuthGuard)
@SetMetadata('provider', LoginProvider.GOOGLE)
@Post('login/google')
async googleLogin(...) {}

@UseGuards(OAuthGuard)
@SetMetadata('provider', LoginProvider.NAVER)
@Post('login/naver')
async naverLogin(...) {}

@UseGuards(OAuthGuard)
@SetMetadata('provider', LoginProvider.KAKAO)
@Post('login/kakao')
async kakaoLogin(...) {}

 

 

이제 OauthGuard를 구현해보겠다. 우선 간단히 Guard의 뼈대를 정의해준다.

@Injectable()
export class OAuthGuard implements CanActivate {
  private logger = new Logger(OAuthGuard.name);
  private loginTokenUrl: string;
  private loginInfoUrl: string;

  constructor(
    private reflector: Reflector,
    private config: ConfigService,
  ) {}
}

 

 

라우트에 정의한 메타데이터 값을 읽기 위해 Reflector를 생성자에 주입해준다. ConfigService는 환경변수에서 Credentials를 읽어오기 위해 주입했다.

 

이제 canActivate를 구현해준다.

async canActivate(context: ExecutionContext) {
    const req = context.switchToHttp().getRequest();
    const provider = this.reflector.get('provider', context.getHandler());
    this.loginTokenUrl = SocialLogin[provider].tokenUrl;
    this.loginInfoUrl = SocialLogin[provider].infoUrl;

    const { code } = req.body;

    const client_id = this.config.get(`${provider}.clientId`);
    const client_secret = this.config.get(`${provider}.clientSecret`);
    const redirect_uri = this.config.get(`${provider}.redirectUri`);
}

 

 

라우트에 정의한 메타데이터 값을 Reflector를 이용해서 읽어온다.

그렇게 읽은 리소스 제공자를 가지고 Access Token을 요청할 url과 프로필을 요청할 url을 미리 얻어뒀다.

 

프론트 쪽에서 보내온 code(인가 코드)를 꺼내고 Client Credentials를 환경변수로부터 읽어온다.

 

Access Token을 요청하는 함수는 canActivate 밖으로 빼내어 구현했다. (url의 state는 네이버 로그인 때문에 고정해뒀다)

async getAccessToken(
    code: string,
    clientId: string,
    clientSecret: string,
    redirectUri: string,
): Promise<string> {
    const url = `${this.loginTokenUrl}?code=${code}&client_id=${clientId}&client_secret=${clientSecret}&redirect_uri=${redirectUri}&grant_type=authorization_code&state=RANDOM`;

    try {
      const result = await axios.post(url);
      return result.data.access_token;
    } catch (error) {
      console.error('get access token::', error);
      throw new InternalServerErrorException(error.message);
    }
}

 

 

 

Access Token을 가지고 user profile을 요청하는 함수도 구현해준다.

 async retrieveProfile(
    accessToken: string,
    provider: string,
): Promise<IGoogleUser | IKakaoUser | INaverUser> {
    try {
      const result = await axios.get(this.loginInfoUrl, {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
          Authorization: `Bearer ${accessToken}`,
        },
      });

      if (provider === LoginProvider.GOOGLE) return result.data;
      else if (provider === LoginProvider.NAVER) return result.data.response;
      else if (provider === LoginProvider.KAKAO)
        return result.data.kakao_account;
      else throw new InternalServerErrorException('undefined login provider');
    } catch (error) {
      console.error('get userinfo error::', error);
      throw new InternalServerErrorException(error.message);
    }
}

 

프로필 데이터는 타입으로 써먹어야 되서 interface로 따로 만들어줬다.

export interface IGoogleUser {
  id: string;
  email: string;
  verified_email: boolean;
  picture?: string;
  hd?: string;
  name: string;
  given_name?: string;
  family_name?: string;
}

export interface INaverUser {
  id: string;
  name: string;
  nickname?: string;
  gender?: string;
  email: string;
}

export interface IKakaoUser {
  has_email?: boolean;
  email_needs_agreement?: boolean;
  is_email_valid?: boolean;
  is_email_verified?: boolean;
  email: string;
  name: string;
}

 

위에서 만든 함수들을 canActivate에서 호출시켜주면 Guard 구현은 끝난다.

async canActivate(context: ExecutionContext) {    
    ...
    const accessToken = await this.getAccessToken(
      code,
      client_id,
      client_secret,
      redirect_uri,
    );

    const user = await this.retrieveProfile(accessToken, provider);
    req.user = user;

    return true;
}

 

@OauthUser 구현

 

Guard에서 프로필 데이터를 'user'라는 프로퍼티에 추가했으니 이걸 읽어서 Controller 라우트에서 바로 써먹을 수 있게끔

decorator를 만들어줬다.

export const OauthUser = createParamDecorator(
  (
    data: unknown,
    ctx: ExecutionContext,
  ): IGoogleUser | IKakaoUser | INaverUser => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  },
);

 

위에서 구현한 decoratorController에서 써주면 된다.

@UseGuards(OAuthGuard)
@SetMetadata('provider', LoginProvider.GOOGLE)
@Post('login/google')
async googleLogin(
  @Body() payload: OauthLoginDto,
  @OauthUser() user: IGoogleUser,
) {
  return await this.authUserService.oauthLogin(payload, user);
}

 

'NestJS' 카테고리의 다른 글

[NestJS] Joi로 환경변수 validation  (1) 2024.06.15

+ Recent posts