2017년은 정말 많은 것들을 경험했던 한 해였다. 직장도 옮기고, 개발자로서 많은 경험을 했던 한 해였다.
평소에 연단위 회고는 쓰지 않는데, 올해는 이렇게 글로 남기고 싶은 경험이 많은 해여서 처음으로 회고를 남겨본다.
그냥 1월부터 쭉 생각나는 이벤트만 정리해보면…
회사 일과 별개로, 클래식매니저 라는 클래식 스트리밍 플랫폼을 개발했다. 기존 코드를 뒤엎고 나에게 익숙한 스택으로 바닥부터 새로 만들었다. iOS/Android 개발자들과 협업 해본 경험이 없어서 인터페이스를 어떻게 공유할지 걱정이 많았는데, 무사히 MVP 만들어서 내놓을수 있어서 다행이었다. 클래식을 좋아한다면 꼭! 한번 써보는걸 추천한다.
2017년의 가장 큰 이벤트는 정들었던 로앤컴퍼니를 그만두고 빙글로 이직한 것이다.
이런 저런 사정으로 인해 몇달간 로앤컴퍼니에서 혼자서 모든 개발을 다 감당했어야했고, 혼자서 모든 이슈를 해결해야 하는 상황이 심리적으로 부담이 너무 컸다.
태스크들의 데드라인을 못지기는것보다, 코드를 작성할때 이 코드가 맞는 방향인지 “의논”할 사람이 아무도 없는게 너무 외로웠다.
간단한 API 하나 작성하는데, 과연 이렇게 코드를 작성하는게 올바른 방향인지 스스로 확신이 서지 못하는 내 모습을 보는게 너무 고통스러웠다.
브라우저의 버그로 스타일이 망가지는 이슈를 발견했을때에도, (현재도 그렇지만) 푹 빠져있던 서버리스 아키텍쳐에 대해 터놓고 이야기할 팀원이 단 한명도 없었다.
엎친데 덮친 격으로, 대한민국 남자라면 누구나 겪는 군문제가 나에게도 왔다.
그동안 입영 연기를 하고 “언젠간 병특하겠지” 라는 안일한 생각으로 시간을 보내고, 더이상 입영 연기를 할 수 없는 지경까지 와버린 것이다;
그와중 정말 감사하게도 (지금 생각해봐도 천운인것 같다 ㅠㅠ) 빙글에서 오퍼가 왔다.
2016년 10월에 AWSKRUG에서 Apex로 발표를 한 적이 있었는데, 이때 했던 발표가 계기가 되어 연락을 주셨던 것이다 ㅠㅠ
다른 회사들도 많지만, 나에게 압도적으로 빙글이 매력적으로 다가왔던 이유를 꼽아보면 다음과 같았다:
물론 장점만 있는건 아니었다.
포지션이 백엔드라 프론트엔드 개발을 더이상 하지 않는게 아쉬웠다… 하지만 나중에 원한다면 포지션을 변경할수도 있고 (이미 안드로이드 개발하다가 iOS 개발하시는 분이 계시다..)
무엇보다 백엔드 개발자로서 내공을 더 쌓고 싶었기 때문에 그 아쉬움은 마음 한편에 접어둘 수 있었다.
첫 출근의 기억이 아직도 생생한데, 출근하자마자 여러 서비스들의 계정을 만들고 처음으로 할당받은 일은 스팸필터 기능을 마이크로서비스로 옮기는 일이였다.
설렘과 동시에 환경이 달라지면서 과연 내가 이 일을 무사히 끝낼수 있을까 걱정이 많았다.
그 이유는,
놀랍게도 태스크는 정말 금방 끝냈다. 약 4시간만에 첫 버전 개발을 끝내고 프로덕션으로 배포했으며, 마이크로서비스 자체가 분리되어 있는 상태였기 때문에 기존 프로덕션 환경에 영향을 전혀 주지 않았다.
익숙한 환경이 아님에도 불구하고 태스크를 무사히 끝낼수 있었던건, 아마 다음과 같은 이유일 것이다:
4월은 기존 모놀리식 아키텍쳐에서 서버리스 아키텍쳐로 마이그레이션을 위해서, 기반을 미리 다져두는 한달을 보냈다.
주로 데이터 마이그레이션을 위한 태스크들을 처리했다. DynamoDB를 점점 많이 쓰기 시작하면서 본격적으로 DynamoDB 뽕을 맞기 시작했다.
서비스 중단 없이 안전하게 데이터를 라이브 마이그레이션하는 방법을 배웠다. 그것도 종류가 완전 다른 RDB (PostgreSQL)에서 NoSQL (DynamoDB)으로의 마이그레이션…
언젠가부터 특정시간대에 서버가 502를 쏟아내기 시작했다.
원인파악이 생각보다 잘 되지 않아서, 엄청나게 많은 노력을 했었다.
502가 쏟아진 이유는 다음과 같았다:
구체적으로 원인을 분석해보니, 다음과 같았다:
그래서 스케일링 파라미터와 인스턴스가 더 많은 worker를 가용할 수 있도록 인스턴스 타입을 변경했고, 모든 구간에서 Keep-Alive Connection을 쓰도록 변경했으며 (여담이지만 대부분의 nginx reverse proxy example들은 keep-alive가 활성화된 상태가 아니다. 그리고 keep-alive를 활성화 하기 위해 upstream directive를 사용하면, resolved dns record가 TTL대로 갱신이 되지 않고 재시작할까지 고정된다;), 이슈는 사라졌다.
그 이후, 모니터링을 잘 하기 위해서 또 많은 시간을 쏟았다…
비록 장애가 발생했지만, 시스템 성능 최적화, nginx 최적화 (그리고 nginx plus의 위대함), 그리고 성능 모니터링을 종합적으로 배울 수 있어서 기뻤다.
가정의달 한정으로 사용할 광고서버를 Serverless 아키텍쳐로 만들었다.
Primary database가 DynamoDB가 아닌 Aurora를 사용했으며 (그것도 t2.small instance 이다;)
단 한번의 service outage없이 잘 버텨주었다. Aurora를 Typescript 프로젝트에서 편하게 쓰기 위해 TypeORM을 도입했었다. 현재는 프로덕션에서 쓰진 않지만, 아직까지도 잘 동작중이다. 어떻게 Impression / Click과 같은 광고 메트릭을 잘 쌓고, data aggregation을 효율적으로 처리하기위해 모델링해야하는지 배웠다.
로앤컴퍼니에서 공부해서 써먹었던 머신러닝 모델들을 빙글 서비스에도 도입하고 싶었다. 빙글은 데이터가 아주 풍부해서 예전처럼 학습시킬 데이터가 없어서 걱정할 상황은 아니었고, 오히려 데이터가 너무 많아서 이걸 어떻게 처리해야할지가 숙제였다…
나는 팔로워를 추천하는 기능을 개발했어야 했는데, 공부했던 모델중 하나를 프로토타입으로 만들어봤더니 생각보다 결과가 좋았다. 해당 모델을 써보기로 하고, 피쳐를 만들어서 배포까지 끝냈는데, 큰 문제가 있었다.
학습 데이터를 로컬에서 만들어서 썼기 때문에 주기적으로 학습된 데이터를 갱신하기가 어려웠다…
학습 데이터가 DynamoDB에 있어서 테이블을 덤프하거나 데이터를 스캔하기는 어려웠고, 행렬 계산이 무거워서 로컬머신을 하루종일 굴렸어야했다… 이 때문에 Spark 책을 샀지만, 절반정도 읽다 그만두고 지금은 책장에 박혀있다. 책 끝까지 안읽고 기능 개선에 손대지 않은게 아직도 마음에 걸린다… 반성한다…
버그 수정, 마이그레이션 작업 계속 진행
Facebook의 Marketing Insights API를 주기적으로 크롤링해서, 알림을 보내주는 태스크를 진행했었다.
예전에 해본적이 있는 분야라서 태스크 자체는 잘 끝낼수 있었다.
다만, Insights API Limit으로 인해 여러 꼼수 (e.g. 시간대를 분산해서 데이터를 긁기)를 썼지만, 결국 계정 단위로 관리되는 API Limit은 어떻게 우회할 방법이 없어서 그렇게 큰 도움이 되지 못했던것 같아 아쉬웠다.
마이그레이션 및 신규 기능 개발
버그 수정 및 신규기능 개발 (계속)
기존에 Vingle에서 휴가를 쓰려면, 아래와 같은 절차가 필요했다.
휴가를 신청하기 위한 절차가 다소 번거로웠고, 무엇보다 휴가를 관리하는 담당자분께서는 일일히 구글 폼의 응답을 집계해 팀원별 휴가 사용량을 정산했어야 했다.
이를 해결하기 위해, 그냥 Google Forms 양식을 제출하면 HTTP Callback을 받아서 캘린더에 휴가정보를 등록하고, 슬랙에 알림을 보내주도록 변경하는 태스크를 진행했는데. 작업 진행이 생각보다 잘 되지 않았다.
Google Forms에서 폼 제출시 HTTP Outcoming Webhook과 같은 HTTP Callback을 보내려면, Apps Script를 거쳐야하고, Apps Script에서 응답을 JSON으로 serialize해서 보내더라도, 받는쪽에서 serialize된 JSON을 해석하기가 너무 까다로운 구조였기 때문이다.
이걸 읽고 해석할 “파서”를 구현할바에, 그냥 Google Forms랑 똑같은걸 만들자! 생각하고 일을 더 크게 벌려버렸다. 그리고 금방 끝낼수 있을줄 알았던 태스크의 크기가 엄청 커져버렸다.
나는 금방 끝낼수 있을줄 알았는데, 순수하게 비즈니스 로직을 작성하는것보다 기술적인 챌린지를 하는데 시간을 너무 많이 쏟아버렸다.
만든 결과물은 마음에 들지만, 티켓 하나에 소모한 시간이 너무 길었다. 어느정도 스스로 품질과 작업량 간의 밸런스를 맞췄어야했는데, 그러질 못했다. 1:1 미팅에서도 이 이야기가 나왔고, 고쳐야겠다고 다짐했다. 품질에 대한 강박이 너무 심한게 아닌가 생각도 들고;
버그 수정 및 신규 기능 개발 (계속)
악랄한 스패머들이 스팸 필터를 우회해서 스팸 게시글을 올리는 빈도가 높아져서, 손을 한번 대기로 했다.
게시물을 분석할때 Headless Chrome을 쓰도록 구조를 변경했고, CIDR 기반 광역 차단, WHOIS 등록 정보, DNS 레코드 조회 등 복합적인 조건으로 스팸을 걸러내도록 구조를 개선했다. 다행히 아직까지는 새 스팸필터를 우회한 스패머는 없다. DNS나 WHOIS가 어떻게 동작하는지 공부한게 도움이 되어 뿌듯했다.
이미지로 스패밍을 하는 유저도 있었는데, 이를 차단하기 위해 이미지 스팸 필터도 프로토타입을 만들었었다. 적용은 아직 안했지만…
컨퍼런스 참가로 생애 처음으로 한국 이외의 나라를 가보게 되었다.
ICN-SFO-LAS 루트로 라스베가스에서 열리는 AWS re:invent 2017 행사에 참가했다.
엄청나게 많았던 새 기능 소개들, 엄청 큰 회사들의 best practices 세션들도 정말 좋았지만, 그중 가장 기억이 남는건 Chalk Talk으로 진행되던 실시간 비디오 프레임 분석 & 알림 세션이였다. 작은 방에서 30명정도가 참석해 듣는 세션이였는데, 슬라이드 중간 중간 질문을 던지는 방법 (e.g. 이 구조에서는 무엇이 문제인가? frame analysis interval을 너무 짧게 잡으면 무엇이 문제인가? 반대로 길게 잡으면 무엇이 문제인가?) 으로 Follow-up을 잘 해주어서 더 인상이 많이 남았다. 그리고 강연자와 1:1으로 질문/답변을 하지 않고, 주변 사람들이랑 아주 자연스럽게 토론할 수 있는 분위기가 너무 좋았다.
아, 그리고 인상깊었던 슬라이드가 하나 있다.
Amazon CTO인 Werner vogels의 슬라이드가 끝나면서 나온 슬라이드인데,
“그래서 미래는 어떤 모습이죠?” 라는 질문에 대한 답이다.
그래서 미래는 어떤 모습이냐구요? 님이 쓰는 코드는 비즈니스 로직밖에 없을거에여
Serverless 아키텍쳐로 개발을 하면서 느꼈던 것중 하나와 일맥상통하는 이야기라 놀라웠다.
그리고, 처음으로 해외를 가보는 만큼 해외의 개발자들을 만나서 이야기를 나누고 싶었다.
라스베가스로 떠나기 한달 전 쯤, 무작정 Apex Slack에 re:invent를 가는 사람이 없냐고 물었고.
영국에서 금융 스타트업에서 일하는 Tom이라는 친구가 자기가 re:invent에 참석한다며, 만나자고 약속을 잡았다.
그리고 행사 전날 스트립의 한 레스토랑에서 Tom을 만났고, 그날 되지도 않는 영어로 온갖 개발 이야기를 했다. Serverless에 대한 찬양을 시작으로, 위에서 만든 휴가 관리 시스템의 코드, 그리고 자기네 CTO에 대한 잡담까지 짧은 시간이었지만 많은 이야기를 나눌 수 있었다. 되지도 않는 영어로 어떻게든 이야기하는게 정말 어려웠지만, 그래도 Tom이 이야기를 잘 들어주어서 너무 고마웠다.
엑스포에서도 많은 기업들의 부스를 돌아다니며 평소 궁금했던것들을 마음껏 물어보았다.
nginx 부스에 가서는 amplify를 잘 쓰고 있다고 감사를 표했는데, nginx 엔지니어가 amplify를 사용하는 유저가 많이 없는데 잘 써주어서 오히려 우리에게 너무 고맙다고 했다.
불편한게 없는지도 물어보고, 버그를 리포트했더니 정말 고마워했다. 오히려 amplify를 공짜로 쓸 수 있는 우리가 고마운데…
그리고 github 부스에 가서 평소에 엄청 물어보고 싶었던 질문을 던지고 왔다.
프론트 개발을 하다보면 간혹 일부 예제에서 raw.githubcontent.com
도메인으로 JS나 CSS와 같은 정적 컨텐츠를 땡겨쓰는걸 볼 수 있고, 심지어는 일부 웹사이트에서는 그걸 CDN 마냥 쓰는 곳이 있는데. 여기에 대해서 어떻게 생각하는지가 궁금했다.
다행히도 Github 엔지니어는 내 질문을 재미있다고 하시곤 답변을 해주셨는데, 이미 Github 내부에서는 해당 트래픽이 엄청난걸 알고 있고, 그 수준이 DDoS 공격을 받는것과 비슷한 수준이며, 다행히 내부적으로 API 호출을 하는 것이라 Rate Limit이 존재하며, Bandwidth 제한 또한 걸려있다고 답변을 해주셨다. 그리고 자기도 왜 사람들이 그렇게 리소스를 링크해서 쓰는지 전혀 이해가 안가는데 너는 혹시 이유를 알고있냐고 ㅋㅋㅋ 재치있는 답변을 해주셨다.
만약 올해에도 re:invent 행사를 간다면 더 많은 사람들을 만나서, 더 많은 이야기를 해보고싶다. 일주일의 짧은 일정이었지만 많은 것들을 느끼고 배울 수 있었던 시간이었다.
학창시절엔 나는 나름 네임드 (?) 소녀시대 팬이었다. 지금은 흑역사긴 하지만… 그때에는 나름 유명한 업로더였다.
한땀 한땀 비디오를 인코딩하고, 직접 장비 챙겨가서 직캠을 찍기도 했고, 비대칭 회선 (다운로드는 100Mbps 속도가 나오지만 업로드가 10Mbps가 채 못되는 회선) 이라는 극악의 환경에서도 열심히 자료를 공유했었다.
언젠간 현업에서 미디어 서비스를 개발하는 날이 있었으면 했는데, 그게 드디어 이루어졌다.
컨테이너와 코덱의 차이가 무엇인지, B/I/P 프레임이 무엇인지, GOP가 무엇인지, 왜 H.264 비디오 인코딩에서 x264 인코더가 킹왕짱인지, 왜 비디오 해상도는 4의 배수여야 하는지와 같은 이런 잡다한 비디오 인코딩 지식이 빛을 발할수 있었다.
Adaptive Bitrate Streaming을 위해 기존 Adobe Media Server 기반 기술이 아닌 Apple HLS/MPEG-DASH를 사용해볼 수 있었고, 일반적으로 인코더들이 쓰는 megui/avisynth/virtualdub 같은 소프트웨어가 아닌 ffmpeg과 같은 전문 소프트웨어로 우리 만의 파이프라인을 구성해볼 수 있었다.
아직 릴리즈 되지 않은 기능이라 자세한 이야기는 할 수 없지만, Serverless 아키텍쳐로 미디어 파이프라인을 만드는데 성공했다. Youtube와 같은 비디오 서비스 업체들처럼 “빠른 시간 안에”, “품질을 희생하지 않고” 미디어들을 처리할수 있는 기반을 만들어서 기쁘다.
팀원들, 팀원들의 리뷰와 의견들이 더욱 단단한 제품을 만들고, 나의 나쁜 습관을 고치고, 더 좋은 코드 디자인을 가질 수 있도록 큰 도움이 되었다. 짧지만 이 섹션을 빌어 감사를 표하고싶다. 감사합니다!
2017년에도 미국 시차로 살아가는 바이오리듬을 제대로 고치지 못했다.
전보다는 많이 나아지긴 했지만, 조금만 신경쓰지 않으면 금새 바이오리듬이 망가지는걸 종종 겪었다…
배움을 위해 퇴근 후 카페 가는게 올해로 4년째가 된다. 덕분에 많은 기술적인 배움이 있었지만, 이제는 나 자신을 갉아먹는 나쁜 습관이 되어가는것 같다. 카페를 가지 않으면 마음이 불편하고, 하루를 올바르게 마무리하지 못한다는 강박이 든다.
2018년에는 그런 강박을 이겨내고 일찍 잠드는걸 굳히는 (?) 습관을 들여보려고 한다.
매일 카페를 가게 되어 향유하게 되는 지식이 줄어드는건 아쉽긴 하지만, 나에게 가장 당장 필요한건 지식이 아닌 정상적인 라이프사이클이다.
balmbees/dynamo-typeorm / NPM Link
psKill
functiononOpen
hook triggered twice커뮤니티에서는 주로 답변에 시간을 많이 보냈다. 내가 알고 있는 경험이나 지식이 누군가에게 도움이 되는건 뿌듯한 일이다.
읽은 아티클들이 너무 많아서 아티클을 일일히 열거하긴 힘들지만, 강한 인상을 남겼던 것들을 꼽아보자면:
이직 후 블로그에 글 쓴게 하나도 없다. 글 쓰는게 생각보다 시간이 많이 들어가더라.
대신 최근에는 micronote라는 짧은 노트를 쓰기 시작했다.
2017년에는 발표를 포함한 대외활동을 하나도 하지 않았다.
발표할 주제도 딱히 없었지만, 새로운 회사에 빠르게 적응하느라 그런데 신경쓸 여유가 없었던것 같다.
거창한 목표는 없고, 딱 아래 세가지를 목표로 설정하려고 한다.
빙글에서 백엔드 소프트웨어 엔지니어로 많은 배움을 얻을 수 있어서, 내가 도움이 될 수 있는것들은 도움이 될 수 있어서 기쁜 한해였다. 2018년도 2017년만큼 좋은 기억이 많은 한해이길 소망한다.
그리고 2018년에는 빙글러가 더 많이 늘어났으면 좋겠다
여러분 빙글 많이 써주세요!!!
Web: https://www.vingle.net
iOS: https://itunes.apple.com/kr/app/vingle-%EB%B9%99%EA%B8%80-%EA%B4%80%EC%8B%AC%EC%82%AC-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC/id637534820?mt=8
Android: https://play.google.com/store/apps/details?id=com.vingle.android&hl=ko
Vingle에서는 여러 직군의 소프트웨어 엔지니어를 채용중입니다!
https://careers.vingle.net/
현재 개발하고 있는 로톡에서는 Lambda를 쓰는 몇몇 서비스들이 있다.
그 중 가장 사용 빈도가 높은 법률 분야 추천 API는 서울 리전에서 Lambda를 서비스 하기 전에 만들었던 것이라,
도쿄 리전에 배포를 해서 사용을 하고 있었다.
그동안 손댈일이 없어서 계속 도쿄 리전에 두고 있던 상태였는데, 몇몇 추천 파라미터를 변경하게 되면서 서울 리전으로 이전하기로 했다.
한국 - 일본을 오가는게 영 찝찝했기 때문이다.
법률 분야 추천 API를 간략하게 소개하면,
입력받은 텍스트를 분석해 해당 텍스트가 어떤 법률 분야에 속할 수 있는지 추천하는 API이다.
(한때 머신러닝좀 배우겠다고 설쳐댔을때 내부 데모용으로 만들었던게 제품으로 들어갔다.)
예를 들어,
지난 금요일 밤 강남역의 한 술집에서 술을 마시다가 옆 테이블 일행과 시비가 붙었습니다.
상대방이 제 멱살을 잡자 제가 화가 너무 난 나머지 주먹질을 했고 전치 4주의 상해를 입혔습니다.
저는 어떤 처벌을 받게 되나요?
라는 텍스트를 입력받으면 ‘폭행/협박’ 이라는 분야를 추천해 응답하는 것이다.
(사실 추천이라기 보다는 분류기에 가깝다.)
구성은 별거 없다.
문제는 Lambda를 제외한 모든 리소스들이 서울에 있다는 것이다. 서울 안에서도 AWS랑 KT의 ucloud biz가 짬뽕되어 있다.
API 서버에서 도쿄 리전에 있는 analysis
function을 호출할때에도 지연이 있는데,
function 안에서 또 한국과 일본을 오가게 되어서 추가적인 지연이 생기게되어 더 느렸다.
기존에 이미 Apex로 구성을 해둔터라 그냥 리전만 바꾸고 다시 배포를 시도했다.
베포는 문제가 없었는데… 테스트를 위해 함수를 호출해보니 에러가 뜬다… 읭?
Error: function response: Cannot find module ‘debug’.
코드에 손도 안댔는데…. 문제가 생겼다.
logger로 debug
패키지를 쓰는건 맞는데, 왜 안되는지 모르겠다. apex build analysis > built.zip
으로 built zip을 떨구고 unzip -l built.zip
으로 확인해봐도 분명 node_modules/debug
라는 경로에 파일은 있었다.
혹시라도 AWS에서 debug
란 패키지 네임을 쓰기 시작했나? 싶어서 require('debug')
로 불러오던걸 무식하게 (?) require('./node_modules/debug')
로 불러오도록 바꿔도 봤지만, 소용이 없었다.
시간은 자꾸만 흐르고 해결은 되지 않아서 Apex Slack에 푸념을 늘어놓던 와중….
debug
패키지의 내부 스캐폴딩이 바뀐걸 발견했다….
문제는 이렇게 생긴 것이었다:
npm install && npm update
가 들어있어서 빌드 전 debug 패키지가 자동으로 업데이트가 되었고,.apexignore
에 babel이 transpile하는 원본 코드를 배포에 포함시키지 않게 하려고 src/
가 들어있었는데,debug
패키지의 내부 구성이 바뀌면서 .apexignore
에 기재된 src/
glob이 node_modules/debug/src
를 매치시키면서 파일이 누락되어 배포 패키지가 만들어졌고(예전에 도쿄에 배포할땐 debug
패키지의 내부 구성이 변경되기 전이므로 영향을 받지 않았다.)
.apexignore
에 들어있는 src/
glob을 ./src/
로 바꾸고 다시 배포해보니 문제가 사라졌고, 기존처럼 그대로 잘 동작했다.
아… 다시 생각해봐도 황당하다 ㅋㅋㅋㅋㅋㅋㅋ
이제 모든 리소스들은 서울에 있다. 더이상 해외망을 오갈 필요가 없다!!!
서울 리전으로 무사히 이전하고 나서 서비스 안에서 이리저리 써보니 확실히 많이 빨라진걸 체감할 수 있었고,
벤치마크를 돌려봐도 많이 빨라진 것을 알수있다.
아래는 간단하게 벤치마크를 돌려본 결과이다.
첫번째가 도쿄 리전, 두번째가 서울 리전이다.
얼마 전 사이드잡으로 진행하고 있는 서비스에서 소셜 계정으로 로그인한 아바타 이미지(프로필 사진)가 표시되지 않는 일이 발생했다.
그동안 해당 서비스에서는 아바타 이미지를 스토리지에 저장하지 않고 각 SNS 서비스에서 제공하는 아바타 이미지의 URL을 가져다가 썼는데,
Facebook을 비롯한 일부 서비스의 경우 해당 이미지 URL에 access control을 위한 policy가 붙어있었다.
AWS의 CloudFront나 Akamai 같은 CDN의 경우 언제까지 파일에 접근이 가능한지를 Policy를 통해 제어할 수 있는데,
아바타 이미지의 URL에 그것이 붙어있었던 것.
Graph API로 대체를 할까 고민하다가, 이런 의견도 있고 해서 그냥 로그인할때마다 S3에 올리고 (있으면 덮어쓰고) CloudFront를 통해 서빙을 하는 방향으로 결정하고,
아바타 이미지 URL을 내려받고 S3에 해당 이미지를 저장하도록 아래와 같은 스니펫을 작성했다.
뭐 별 문제 없으리라 생각했다.
업로드도 잘 된 것 같았다.
그런데……. 막상 S3에서 확인해보니….
읭??????? 업로드 한 객체의 크기가 0바이트로 뜬다?!?!?!?
혹시나해서 s3.upload
에 전달하는 Body
property를 Readable Stream 대신 Buffer로 던져보기로 했다.
위 스니펫을 작성하고 다시 동일하게 테스트를 진행했다.
업로드는 잘 되었다고 떴고, 다시 업로드된 객체를 조회해보면…
S3에 제대로 파일이 올라간다!!!
여기서 Buffer를 쓰는 방법을 택하고 포기할 수 있었지만,
만약 업로드 할 바이너리가 큰 경우, 동시에 들어오는 로그인 요청이 많아지는 경우를 생각하면 찝찝해서 더 파보기로 했다.
메모리는 소중하니까… ㅠㅠ
이제 어디서 잘못된건지 검증해야하는 포인트가 조금 좁혀졌다.
1) AWS SDK에서 s3.upload
를 통해 파일을 업로드 할 때, Stream을 사용할 수 없거나
2) AWS SDK로 전달되는 Stream에 문제가 있거나
둘 중 하나일 것이다.
AWS SDK에서 s3.upload
를 통해 파일을 업로드할 때, Stream을 사용할 수 없는지 검증해야 했다.
근데 애초에 내가 Stream을 썼던 것은, AWS SDK documentation에서 Stream을 전달할 수 있다고 나왔기 때문이다.
다시 한번 SDK 문서를 체크해봐도 Readable Stream을 쓸 수 있다고 나와있다. 심지어 예제도 Readable Stream을 쓴다.
정말 혹~~시라도 문서가 잘못되어있는 경우를 생각하기 위해, AWS SDK에서 Stream을 받을 수 있는지 없는지 검증해보기로 했다.
동시에 2)를 검증할 수도 있기도 하고…
response
request 쪽에서 response
event로 전달받은 response
readable stream에 문제가 있는지 검증을 하기 위해,fs.createReadStream
로 로컬 파일을 Readable stream으로 가져와 S3로 업로드를 시도해보기로 했다.
그리고 테스트를 해보면…
잘 올라가는것을 확인할 수 있다.
일단 여기서 AWS SDK이 Stream을 지원한다는건 검증한 셈이니, request쪽에서 response
event를 통해 전달한 response
stream에 문제가 있을거라고 예측할 수 있다.
이를 검증하기 위해, 이번에는 http.request
의 shortcut인 http.get
을 통해 s3로 업로드를 시도했다.
역시 위 스니펫으로 테스트해보면…
잘 되는것을 확인할 수 있다.
어쩄거나 request
패키지쪽에서 넘어오는 stream에 문제가 있는 것 같은데…
이것저것 테스트를 더 진행하던 와중에, 아래 스니펫이 정상 동작하는것을 확인하고 이슈는 더 미궁속으로 빠지기 시작했다.
분명 request
쪽에서 넘겨주는 response
stream에 문제가 있었던 것 같은데.
이슈가 발생하는 이전 스니펫에서 다운로드 받은 파일을 s3가 아닌 로컬 파일로 쓰게 하면 잘 되었기 때문이다.
도대체 무엇이 문제인걸까????
멘탈이 반쯤 나간 상태로 이것 저것 케이스를 더 만들어서 테스트를 해보다가,
우연히 아래처럼 response
stream을 다른 stream으로 pipe하는것을 지연했더니,s3.upload
를 통해 업로드를 했던것과 동일하게 0바이트로 파일을 기록한 결과가 나왔다!
여기서 request 대신 아까처럼 http.get
을 통해 pipe를 하면..
이쪽은 잘 되는것을 확인할 수 있다.
자, 이제 무엇이 문제인지 어느정도 추측해볼 수 있다.
이번에 세운 가설은, request
패키지에서 전달하는 response
stream은
flowing mode로 동작한다는 것이다.
즉 s3.upload
API에서 stream에서 넘어오는 data를 읽어들이기 전에 이미 response
스트림에 모든 데이터가 전달되었고,
따로 이미 해당 스트림은 ‘흐르고 있는’ 모드라 그 데이터가 고스란히 유실된 것.
이 부분을 조금 더 깊게 파보기 위해,
Node.js의 꽃 (?) 이라 할 수 있는 Stream에 대해 알아보자.
나는 Node.js에서 가장 매력적이고 핵심적인 부분이 바로 Stream이라 생각한다.
과거 Node.js v0.10 까지는 Stream API에 상당히 많은 변화가 있었다.
혹시라도 Stream를 아직 잘 모른다면, Node.js의 Contributor이자 npm Inc.의 owner인 Isaac Z. Schlueter이
playnode 2012에서 발표한 Stream2 슬라이드를 읽어보는 것을 강력하게 추천한다.
Node.js 공식 문서의 Stream 섹션을 참고해보는것도 아주 좋다.
Node.js에서 Stream은 크게 네가지 종류로 분류된다.
Readable Stream, Writable Stream, Duplex Stream, Transform Stream.
여기서 request
나 http.get
에서 반환하는 response
stream은 Readable Stream에 해당하는데,
Readable Stream의 예로는 우리가 흔히 접할 수 있는 것들을 나열하면 다음과 같다:
1) client의 HTTP Request, server의 HTTP Request (HTTP Class의 IncomingMessage)
2) fs.createReadStream 으로 만든 fs readstream
3) zlib stream
4) crypto stream
5) tcp socket
6) child process의 stdout, stderr
7) process.stdio
Readable Stream에는 두가지 모드가 존재하는데,
flowing
mode 혹은 paused
mode 이다.
flowing
mode에서는 EventEmitter 인터페이스를 통해 최대한 빠르게 데이터가 전달되고,paused
mode에서는 명시적으로 stream.read()
메소드를 호출해 chunk를 읽어야 한다.
기본적으로 Readable Stream은 paused mode이지만, 아래 세가지 방법을 통해 flowing mode로 전환할 수 있다.
data
event handler 등록stream.resume()
메소드 호출stream.pipe()
를 통해 Writable Stream으로 데이터 전달그리고 flowing mode의 Readble Stream은 아래 두가지 방법을 통해 다시 paused mode로 전환할 수 있다.
stream.pause()
메소드 호출data
event handler를 등록 해제하거나 기존에 pipe된 대상 writable stream을 stream.unpipe()
메소드를 통해 파이프 해제두가지 모드와 별개로 Readable Stream은 세가지 상태가 존재한다.
readable._readableState.flowing = null
readable._readableState.flowing = false
readable._readableState.flowing = true
readable._readableState.flowing
이 null
인 경우에는 스트림의 데이터를 소비할 어떠한 매커니즘 (data
event handler를 등록하거나 pipe
method를 통해 다른 writable stream 연결)이 없다는 것이다.
이 상태에서는 스트림이 데이터를 만들어내지 않는다.
스트림에 data
event listener를 등록하거나, readable.pipe()
메소드를 통해 writable stream을 파이프하거나,readable.resume()
메소드를 호출한다면 readable._readableState.flowing
이 true
으로 변경되고 스트림이 데이터를 생성할때마다 이벤트를 호출하기 시작할 것이다.
마지막으로 readable._readableState.flowing
이 false
인 동안에는, 버퍼링을 위해 데이터가 Stream 내부 버퍼에 축적될 것이다.
그렇다면 위 _readableState
property를 찍어보면 스트림이 어떤 상태에 있는지 추적해볼 수 있다.
다시 코드로 돌아가서, 아래처럼 _readableState
property를 찍어보자.
코드에서 스트림에 손도 안댔는데 response
stream의 flowing
state가 null
에서 true
로 바뀐것을 볼 수 있다!
아까 위에서 언급했지만, Readable Stream의 flowing state가 true
인 경우 데이터가 흐르게되어 미리 데이터를 소비할 어떠한 매커니즘을 준비해두지 않는다면 이미 흘러서 나가버린 데이터는 살릴 방법이 없다.
이제 정확히 무엇이 문제인지 알았다.
request
패키지에서 전달하는 response
stream은 paused & buffered stream이 아니다!
자, 그럼 어떻게 response
stream을 paused & buffered stream으로 써멱냐고?
솔루션은 간단하다. 이미 stream에 이를 위한 좋은 (?) 구현체가 있다.
Transform Stream의 구현체이기도 한 Stream.PassThrough
가 바로 그것이다.
이름만 보고 유추할 수 있겠지만, 그냥 입력 받은 데이터를 다시 출력해주는 스트림이다.
다만, 기본적으로 paused state로 동작하고 buffering이 built-in 되어있어서 딱 지금 상황에 쓰기 적절하다.
PassThrough를 쓰도록 처음 코드를 고치면 다음과 같다:
그리고 테스트를 해보면…. 두근두근…
잘 고쳐진 것을 확인할 수 있다!!!!!
Download 100MB+ file & upload to s3
using Buffer
vs
using Stream
request
패키지의 Contributor인 Mikeal Rogers의 코멘트에 따르면,
새로운 스타일의 Stream이 v3 브랜치에서 지원될 것 같아 보인다.
하지만…
현재 v3 브랜치는 마지막 업데이트가 4년 전인 상태로, 사실상 방치중인 상태이다.
언젠가 Stream 관련 업데이트가 나올 때 까지는, 이 방법을 쓸 수밖에 없을 것 같다 ㅠㅠ
TL;DR
TZ
환경 변수(Environment Variable) 통해서 Timezone을 설정할 수 있어요
오늘 AWS Lambda에 크롤러를 만들어 올렸는데 컨텐츠를 제대로 가져오지 못하는 일이 생겼다.
분명 로컬에서는 잘 돌아가는데, Lambda로 배포하고 돌려보면 컨텐츠를 제대로 가져오지 못했다.
원인을 찾아보니, 크롤러에서 요청을 날릴때 query string을 만드는 과정에 날짜를 포매팅하는게 문제가 있었다.
query string을 만들때 moment.js로 날짜를 YYYY-MM-DD
형식으로 포매팅하고 있었는데,
로컬 머신의 경우 GMT+9인 한국 타임존이 이미 설정되어 있는 상황이었고, AWS Lambda가 돌아가는 컨테이너는 UTC라 타임존 차이로 결과가 달라진 것이었다.
예~전에 RDS에서도 기본 Timezone이 UTC로 설정되어 있어서 동일한 이슈를 이미 겪어봤기에 바로 타임존 문제임을 알아챌 수 있었다.
그런데 RDS의 경우 Parameter group에서 Timezone 설정만 변경해주면 되는데,
AWS Lambda의 경우 코드만 올리고 땡이라 그런거 없다.
그리고 일반적으로 리눅스 박스에서 Timezone을 변경하려면 /etc/localtime
혹은 /etc/timezone
을 수정하는 방법을 썼는데,
AWS Lambda는 코드만 돌아가는 서비스라 그걸 변경하기도 불가능하다. 애초에 Root access가 막혀있어서 파일을 수정할 수 없다.
정말 다행히 Lambda에서 환경 변수를 지원해서, TZ
환경 변수를 Asia/Seoul
로 설정해봤더니.
너무 잘 된다. POSIX 짱짱맨!!!!
Node.js 런타임 내에서
TZ
환경변수를 설정할 경우 (e.g.process.env.TZ = 'Asia/Seoul';
),
어떠한 Date 함수를 호출하기 전에 반드시 먼저TZ
환경변수를 설정해야한다.
그렇지 않는다면 Timezone 설정이 적용되지 않는다.
Terraform은 Infrastructure as code를 모토로 하는 Hashicorp의 오픈소스 도구입니다.
인프라 구성을 코드를 통해 효과적이고 안전하게 만들고, 변경하고, 버저닝할 수 있습니다.
SVN, Git과 같은 버전 제어 시스템과 함께 사용한다면 다른 사람과 함께 구성을 변경할 수 있으며,
변경 이력을 투명하게 살펴볼 수 있고, 코드로 작성되므로 자동화가 가능합니다.
다음은 Terraform 공식 홈페이지에서 소개하고 있는 한 예제입니다.
예제를 살펴보면 코드로 인프라를 표현한다는 것이 무엇을 의미하는지 바로 알 수 있습니다.
위 Terraform 코드는 인프라 내에 두개의 리소스를 정의합니다.
하나는 DigitalOcean의 Droplet을 정의하는 리소스이고,
다른 하나는 DNSimple의 레코드를 정의하는 리소스입니다.
여기서 DNSimple의 레코드를 정의한 리소스를 보면 흥미로운 부분이 있는데,
위 DigitalOcean Droplet의 Public IP를 참조하고 있다는 것입니다.
즉, 다른 리소스의 정보를 참조하여 정말 유연하게 인프라를 구성할 수 있죠!
AWS의 CloudFormation, Heat은 코드로 인프라를 구성한다는 점에서 Terraform과 비슷하지만,
다음과 같은 차이가 있습니다.
Terraform은 실행 계획이 캡쳐되면, 실행 단계는 캡쳐된 실행 계획에서 필요로 하는 부분만 반영하도록 제한합니다.
다른 도구들은 계획과 실행 단계가 분리되어 있지 않고 합쳐져있어 오퍼레이터들이 인프라 구성 변경으로 무엇이 변경되는지 추론하도록 강요받지만,
Terraform은 변경사항 적용 이전에 무슨 일이 벌어지는지 정확하게 알 수 있게 제공함으로써, 오퍼레이터들이 변경사항을 적용하는 것을 신뢰할 수 있게 합니다.
Terraform은 단일 실행파일로 배포되고 있으며, 이 덕분에 설치가 아주 간단합니다.
공식 홈페이지의 Downloads 페이지에서 Terraform을 얻을 수 있습니다.
이 글을 작성하는 시점에서는 0.8.1 버전이 최신 버전이고, macOS 기준 Terraform을 설치하는 방법은 다음과 같습니다:
$ # echo $PATH 를 통해 실행 경로를 미리 확인하세요.
$ # 저는 /usr/local/bin 을 즐겨쓰기에 해당 경로를 기준으로 했습니다.
$ wget https://releases.hashicorp.com/terraform/0.8.1/terraform_0.8.1_darwin_amd64.zip
$ unzip terraform_0.8.1_darwin_amd64.zip
$ cp terraform /usr/local/bin/ # root 권한이 필요한 경우 sudo를 사용해야 할 수 있습니다.
참 쉽죠?!
설치를 확인하려면 터미널에서 terraform을 그냥 실행해보면 됩니다.
$ terraform
Terraform을 설치했으니, Terraform으로 새로운 인프라를 구성해보겠습니다.
이 섹션에서는 몇가지 AWS 서비스들을 Terraform으로 구성하는 방법을 소개하므로,
미리 IAM 에서 Access Key를 발급받아 두시길 바랍니다!
우리는 이번 섹션에서 새로운 EC2 인스턴스를 만들기 위해 새로운 Terraform 설정 파일을 만들어보겠습니다.
Terraform 설정 파일은 Hashicorp Configuration Language (HCL) 이라는 문법을 사용하고, .tf
확장자를 사용합니다.
HCL은 JSON과 완벽히 호환되고, Terraform 설정 파일은 JSON으로도 작성하여 사용할 수 있습니다 (이 경우에는 .tf.json
을 사용합니다.)
Terraform 설정 파일에 대한 자세한 내용은 이곳을 참고하시고,
일단 새로운 Terraform 설정 파일을 만들어보겠습니다.
먼저, terraform-101
이라는 디렉토리를 새로 만든 후, 해당 디렉토리에 아래 내용을 example.tf
으로 저장하세요.
(기본적으로 Terraform은 working directory의 모든 .tf
파일을 불러오기 때문에, 만일을 대비해 새로운 디렉터리를 만드는 것입니다.)
지금은 AWS의 Access Key와 Secret Key를 직접 기입했지만,
나중에는 변수에서 두 값을 추출해서 사용하는 방법을 알아볼 것이니 하드코딩된 값을 너무 신경쓰진 마세요!
위 설정은 바로 적용해 볼 수 있는 (완전한) 예제입니다.
기본적인 구조를 설명하면 다음과 같습니다:
provider
block은 알려진 Provider를 구성하기 위해 사용됩니다.
위 예제에서는 EC2 리소스를 정의하기 위해 "aws"
를 사용했습니다.
Provider는 리소스를 생성하고 관리하는 책임 (역할)을 가집니다.
여러 서비스를 관리하기 위해 복수의 Provider를 정의할 수도 있습니다 (e.g. Akamai + AWS + DNSimple)
resource
block은 인프라 내에 존재하는 리소스를 정의하기 위해 사용됩니다.
하나의 리소스는 EC2 인스턴스나 Heroku 애플리케이션과 같은 물리적인 컴포넌트를 의미합니다.
Resource block은 block이 시작되기 전 두개의 문자열을 가지고 있습니다.
바로 리소스의 타입과 리소스의 이름입니다.
위 예제를 기준으로 하면 aws_instance
가 리소스의 타입이고, example
이 리소스의 이름이 됩니다.
Terraform에서는 리소스의 타입에 붙은 접두사(Prefix)를 통해 Provider에 매핑합니다.aws_instance
의 경우, 접두사는 aws
이고, 이는 aws
provider에 의해 관리되는 리소스임을 의미합니다.
Resource block 안에는 해당 리소스와 관련한 속성들이 들어갑니다.
위 예제의 경우 Ubuntu 16.04 LTS AMI를 사용하고, t2.micro
인스턴스를 정의했습니다.
이제 Terraform이 위 환경을 적용할 때 무엇을 할지 살펴보기로 합니다.
위 example.tf
가 있는 디렉토리에서 terraform plan
명령을 실행해보세요.
아래와 비슷한 내용이 출력될 것입니다:
terraform plan
명령은 현재 환경에서 Terraform이 어떠한 변경사항을 만들어내는지 보여줍니다.
출력 내용은 Git과 비슷한 형식으로 어떤 항목들이 변경되는지 알려줍니다.
위의 경우 aws_instance.example
리소스가 새롭게 생성될 것이고,
총 1개의 생성 / 0개의 수정 / 0개의 삭제가 발생한다는 것을 알 수 있죠.
<computed>
라고 표기된 값은 리소스가 생성되기 전까지는 알 수 없습니다.
당연하겠지만 (?) EC2 인스턴스를 생성하기 전에 미리 Private IP를 알아낼 수는 없으니까요.
그렇지만, Terraform에서는 인프라를 구성할 때 다른 리소스에서 <computed>
로 표기된 해당 속성의 값을 참조할 수 있습니다. (이따 자세히 알아보겠습니다!)
위 terraform plan
명령으로 확인한 변경사항이 적절하다고 판단되면, 이제 실제로 리소스를 만들 시간입니다. (위 예제에서 EC2 인스턴스를 만드니까요)
적용은 terraform apply
명령을 사용하면 됩니다.
와우! 인스턴스를 새롭게 만들고, 해당 인스턴스가 준비될 때 까지 기다리는걸 확인할 수 있습니다!
EC2 대시보드에서 확인해보니, 정말 잘 만들어졌네요.
또한 기본적으로 Terraform은 몇몇 상태 정보를 terraform.tfstate
파일에 저장합니다.
이 상태 파일은 아주 중요한데, Terraform이 무엇을 관리하고 있는지 알 수 있도록 여러 리소스의 메타데이터를 실제 리소스 ID로 매핑한 정보를 담고 있기 때문입니다.
그리고 이 상태 파일은 Terraform을 사용하려는 유저가 있다면 (e.g. 다른 팀원), 반드시 환경설정 파일과 함께 저장되어야하고 함께 배포되어야만 합니다.
다만 이 상태 파일에는 잠재적인 비밀 정보를 포함할 수 있으므로, Terraform에서는 원격에서 상태를 저장할 수 있도록 설정을 구성하는 것을 권장하고 있습니다.
원격 상태 관리에 대해서는 다음 링크에서 자세히 살펴보시기 바랍니다:
https://www.terraform.io/docs/state/remote/index.html
현재 인프라 구성 상태를 확인하고 싶은 경우, terraform show
명령을 사용하면 됩니다.
아까 Plan / Apply 명령을 내렸을 때와는 다르게, 생성한 리소스에 대해 자세한 정보를 얻을 수 있는 것을 확인하실 수 있습니다.
우리는 방금 전 Terraform으로 하나의 EC2 인스턴스를 생성하는 첫 인프라를 만들었습니다.
이번에는 Terraform으로 위 인스턴스를 변경해보도록 하겠습니다.
Terraform이 어떻게 변경사항을 다루는지 보세요!
Terraform으로 인프라를 변경할 때, Terraform은 변경될 상태에 필요한 부분만 변경하는 실행 계획을 만듭니다.
방금 만든 EC2 인스턴스는 Ubuntu 16.04 LTS AMI를 사용하고 있는데,
이를 Amazon Linux AMI로 변경해보도록 하겠습니다.
설정을 변경하는 방법은 아주 간단합니다.
우리는 AMI 이미지를 교체하려고 하므로, resource 블럭 내의 ami
속성의 값을 ami-983ce8f6
으로 변경하기만 하면 됩니다.
요렇게요.
변경사항을 반영하는것은 이전 단계에서 배웠습니다.terraform plan
으로 실행 계획을 체크하고, 문제가 없다면 terraform apply
으로 반영하면 됩니다.
설정을 변경하고 나서 terraform plan
명령을 실행하면 다음과 같은 화면이 나올 것입니다:
변경사항 앞에 붙은 -/+
접두사는 Terraform이 해당 리소스를 제거하고 재생성한다는 의미입니다.
반대로, 변경하는 일부 속성이 리소스의 제거 없이 즉시 업데이트가 가능한 경우 Terraform은 ~
접두사로 변경사항을 표시하게 됩니다.
(~
접두사가 붙은 것은 차차 확인하기로 하고, 일단 넘어갑시다!)
EC2 인스턴스의 AMI를 변경하는 것은 새 인스턴스를 생성하는 것을 요구하기 때문에, 기존 리소스를 제거하고 새롭게 생성해야하죠.
Terraform은 우리를 위해 이러한 디테일도 잘 다루어줍니다. (세심해라…)
그래서 실행 계획 (Execution Plan) 에서 Terraform이 무엇을 할지 명확하게 알 수 있죠.
덧붙여서, 실행 계획의 결과를 보면 AMI에 대한 변경사항이 리소스를 삭제하고 재생성하도록 요구했다는 것을 알아챌 수 있습니다.
이 정보를 적절히 사용한다면, 특정 시점에 리소스를 제거하거나 생성하는 업데이트를 피하기 위해 변경사항을 조절할 수도 있습니다.
(e.g. 일단 리소스 재생성 없이 즉시 적용할 수 있는 변경사항부터 적용하고, 리소스 재생성을 요구하는 변경사항은 나중에 적용하기)
이전 실행 계획 섹션에서 우리는 무엇이 변경될 지 알아냈습니다. 그럼 이번엔 적용해보죠.
이전과 동일하게 terraform apply
를 통해 변경사항을 적용하면 됩니다.
실행 계획에서 예측한 대로, 기존 인스턴스를 제거하고, 새 인스턴스를 생성하는 것을 확인할 수 있습니다.
EC2 대시보드에서 확인해보면, 인스턴스를 제거하고 새롭게 생성한 것을 확인할 수 있습니다.
Amazon Linux AMI로 바뀐 것이 보입니다!
terraform show
를 통해 새로운 인스턴스의 속성들을 확인할 수도 있습니다.
여러분들은 Terraform과 함께 인프라를 변경하는 것이 얼마나 쉬운지 보았습니다.
다음 섹션에서는 우리가 만든 인프라를 통째로 제거해 보겠습니다.
우리는 Terraform으로 인프라를 어떻게 생성하고 변경하는지 알아봤습니다.
이번 섹션에서는 여러개의 리소스들을 생성하고 의존성을 표시하는지 알아보기 전에,
Terraform으로 관리하는 인프라를 완전히 제거하는 방법을 알아보도록 하겠습니다.
그러니까, 인프라 내에 포함된 모든 리소스들을 제거하는 방법이겠죠.
여러분들의 인프라를 제거한다는 것은 프로덕션 환경에서 아주 드문 일입니다만,
여러분들이 만약 Terraform을 개발, 테스트, QA 환경과 같은 (자주 바뀌는) 인프라를 구성하기 위해 사용한다면,
(모든 리소스를 삭제하기 위해) 인프라를 제거하는 기능을 유용하게 사용할 수도 있을 것입니다.
우리의 인프라를 제거하기 전에, 우리는 terraform plan -destroy
명령을 통해 어떤 리소스들이 제거되는지 미리 확인할 수 있습니다.-destroy
플래그를 사용하는 경우, 우리는 Terraform에게 인프라를 제거하는 실행 계획을 묻게 됩니다.
인프라를 제거한다는 것은, Terraform이 관리하는 모든 리소스를 제거한다는 것을 의미합니다.
여러분들은 이 명령의 결과로 Terraform이 어떤 어떤 리소스들을 관리하고 있고 제거하는지 검증할 수 있습니다.
현재는 구성해둔 리소스가 AWS의 EC2 인스턴스 하나이므로 해당 인스턴스 하나만 표시가 됩니다.
변경사항 적용과 달리 인프라를 제거하는 작업은 apply
명령이 아닌 destroy
명령을 사용합니다.terraform destroy
명령을 통해 한번 인프라를 제거해보도록 하겠습니다:
당연하겠지만, 정말 인프라를 제거할 것인지 확인하는 절차가 있습니다.
여기서 Ctrl + C
로 인터럽트를 하거나, yes
이외의 응답을 하면 인프라 제거를 취소할 수 있습니다.
Terraform은 안전을 위한 매커니즘으로 오직 yes
만 올바른 응답으로 취급합니다.
지금은 연습이기 때문에 마음 편하게 yes
를 입력할 수 있지만,
인프라를 제거하는 작업은 취소할 수 없으므로 항상 주의해야합니다.
yes
를 입력하고 Enter
키를 눌러 응답하면 실제로 인프라가 제거되는 것을 확인할 수 있습니다.
덧붙여서, terraform apply
명령처럼, Terraform은 어떤 순서로 항목을 제거할지 결정할정도로 똑똑합니다.
이번 예제에서는 오직 한 리소스만 존재하기 때문에 순서를 지정해 리소스를 제거할 필요는 없지만,
여러 리소스가 존재하는 복잡한 케이스의 경우라면 Terraform은 올바른 순서로 리소스를 제거 할 것입니다.
여러분들은 로컬 머신에서 Terraform을 통해 인프라를 만들고, 수정하고, 제거하는 방법을 배웠습니다.
Terraform의 강력함은 간편한 관리도 있지만, 리소스간 의존성을 둘 수 있는 점 또한 큰 장점입니다.
다음 섹션에서는 Terraform에서 리소스 간 의존성을 정의하는 방법을 알아보도록 하겠습니다.
이번 섹션에서 우리는 리소스 의존성을 알아볼 것입니다.
여러분들은 여러복수의 리소스를 정의하는것을 보실 수 있을 뿐만 아니라,
다른 리소스의 정보를 참조하도록 리소스 파라미터를 사용하는 것을 확인할 수 있습니다.
지금까지는 우리는 오직 한 리소스만 포함하는 인프라를 구성했었습니다.
하지만 실제 인프라는 다양한 리소스와 여러 리소스 종류를 가지고 있습니다.
Terraform 설정은 여러 리소스들과 리소스 종류를 포함할 수 있고, 이러한 리소스 종류들은 여러 Provider를 통해 더 늘릴 수 있습니다.
이번 섹션에서는 우리는 여러 리소스를 다루는 예제와 함께,
어떻게 리소스가 특정 리소스의 속성을 참조해 해당 리소스를 구성할 수 있는지 알아보겠습니다.
기존에 관리하던 EC2 인스턴스에 Elastic IP를 할당해 기존 설정을 개선해보도록 하겠습니다.
여러분들의 기존 example.tf
환경설정 파일에 다음 항목을 추가하세요:
|
|
변경 후의 코드는 다음과 같을 것입니다:
위 스니펫에서 정의한 리소스는 aws_eip
리소스 타입을 정의하는 것만 제외하면 이전에 EC2 인스턴스 리소스를 추가할 때와 많이 비슷할 것입니다.
aws_eip
리소스 종류는 Elastic IP를 할당하고, 해당 Elastic IP를 EC2 인스턴스에 연동합니다.aws_eip
리소스 종류는 정의된 단 하나의 파라미터인 instance
속성이 있는데, 해당 속성에 지정된 인스턴스에 해당 Elastic IP를 연동하게 됩니다.
이번에 해당 instance
속성에 설정하는 값은 일종의 템플릿 표기법(interpolation)을 이용하여,
우리가 이전에 관리하던 EC2 인스턴스의 속성 값을 참조할 수 있도록 합니다.
템플릿 표기법(interpolation)은 간단합니다.
위 예제에서는 aws_instance.example
리소스의 id
속성을 참조하라는 의미입니다.
실행 플랜을 보기 위해 terraform plan
명령을 실행해보세요.
아마 다음과 같은 출력이 나올 것입니다:
테라폼은 EC2 인스턴스 하나와 Elastic IP 하나, 총 두개의 리소스를 만들 것입니다.
여기서 aws_eip
의 instance
속성 값이 아직 템플릿 표기법(interpolation) 으로 표시되는데,
이는 해당 변수 (variable)는 aws_instance
가 생성될 때 까지는 알 수 없기 때문입니다.
이 이유로, 해당 값은 적용 시점 (apply-time)에서 되기 떄문에 terraform apply
명령의 결과로만 확인할 수 있습니다.
그렇다면, 얼른 확인해보도록 하죠.
terraform apply
명령을 실행해 현재 구성을 적용합니다.
이번에는 다음과 같은 결과가 나올 것입니다:
여기서 Terraform이 실제로 동작하는 것을 볼 수 있는데,
Terraform이 Elastic IP를 할당받기 이전에 (Elastic IP 리소스를 생성하기 이전에) EC2 인스턴스를 먼저 생성하는것을 볼 수 있습니다.
이전에 Elastic IP의 instance
속성의 값을 EC2 인스턴스의 ID를 참조하는 템플릿 표기법(interpolation)으로 기입했기 때문에,
Terraform은 의존성을 추론할 수 있고, 인스턴스를 먼저 만들어야 함을 알 수 있습니다.
Terraform에서 대부분의 의존성들은 암시적입니다. Terraform은 리소스간의 속성 참조를 기반으로 의존성을 추론하기 때문입니다.
Terraform은 의존성 정보를 통해 리소스들의 관계를 그래프로 그리고,
어떤 순서로 리소스들을 생성할 지 결정할 뿐만 아니라, 어떤 리소스들이 병렬로 동시에 생성될 수 있을지 추론합니다.
위의 예제에서는, Elastic IP가 EC2 인스턴스에 의존적이므로 그 두개의 리소스는 병렬로 (동시에) 생성될 수 없었습니다.
암시적인 의존성은 잘 동작하고 대개 여러분들이 원하는 것이긴 하지만,
여러분들은 모든 리소스에서 사용 가능한 depends_on
속성을 통해 명시적인 의존성을 정의할 수 있습니다.
이번에는 이전 예제와 동일하게 동작하고 불필요하긴 속성이긴 하지만aws_eip
리소스에 depends_on
속성을 정의해 명시적인 의존성을 정의해보도록 하겠습니다.
아래와 같이 depends_on
속성을 기입하세요:
|
|
변경 후의 코드는 다음과 같을 것입니다:
만약 여러분이 Terraform이 생성하는 의존성 구조 (dependency chain)에 대해 확신하지 못했다면,
이번 기회에 terraform graph
명령을 사용해 그래프를 확인해보세요.terraform graph
명령의 출력 결과는 Graphviz로도 확인할 수 있는 dot 형식 (dot-formatted) 입니다.
terraform graph
명령을 실행하면 다음과 같은 결과가 나올 것입니다:
위 이미지에서 하이라이트한 라인을 보면, aws_eip.ip
리소스가 aws_instance.example
리소스에 의존성을 가지고 있음을 확인할 수 있습니다.
Graphviz를 통해 시각화 한 그래프를 확인해보면 의존성을 더욱 확실히 알 수 있습니다:
terraform graph
명령에 대해 더 자세히 알기 원하신다면,
Terraform 공식 문서를 참고해보세요.
우리는 이제 다른 EC2 인스턴스를 구성해 인프라를 늘릴 수도 있습니다.
다른 리소스에 대해 의존성이 없다면, 해당 리소스는 병렬로 (동시에) 생성될 수 있음을 뜻하기도 합니다.
이번에는 Ubuntu 16.04 AMI를 사용하는 another
라는 이름을 가진 새로운 EC2 인스턴스 리소스를 정의해보겠습니다.
기존 환경설정 파일에 다음 리소스를 추가하세요:
|
|
변경 후의 코드는 다음과 같을 것입니다:
terraform graph
명령을 실행해보면 다음과 같이 aws_intance.another
리소스에는 아무런 의존성이 없고,
이는 병렬로 (동시에) 생성될 수 있을 것 같이 보입니다.
이번 그래프 역시 Graphviz로 시각화 해보면 다음과 같을 것입니다:
더욱 눈에 잘 들어오죠?
자, 이번에는 terraform destroy
명령으로 인프라를 제거합시다. (네, 모든 리소스를 삭제할겁니다!)
왜냐하면 인프라를 제거하고 terraform apply
명령을 통해 새롭게 인프라를 생성할때, 병렬로 EC2 인스턴스가 생성되는 것을 보려고 하거든요!
아까 배운대로 terraform plan -destroy
명령으로 인프라 제거시 발생하는 변경사항을 검토하고,terraform destroy
명령을 통해 인프라를 제거합니다:
그 다음 terraform plan
명령으로 새롭게 생성할 인프라를 검토해보고,
마지막으로 terraform apply
명령을 통해 병렬로 (동시에) EC2 리소스가 생성되는지 확인해봅니다!
우와!!! terraform apply
명령을 실행하는 동안, Terraform이 정말 병렬로 EC2 인스턴스를 생성하는것을 확인할 수 있습니다!
어때요? 멋지죠!?
다음 섹션으로 넘어가기 전에 Terraform 환경설정 파일에서 방금 만들었던 another
EC2 인스턴스 리소스를 제거하고 terraform destroy
명령을 다시 실행해 인프라를 제거하세요.
왜냐하면 이제 another
리소스를 쓸 일이 없고, 다음 장에서 소개하는 프로비저닝은 인스턴스가 생성되는 시점에만 동작하기 때문입니다.
여기까지 따라오시는 동안 인프라를 만들고 삭제하는건 많이 해보셨을 것이기 때문에, 이번에는 따로 스크린샷을 첨부하진 않습니다.
(사실 귀찮아서… ㅠㅠ)
이번에 terraform destroy
명령을 실행할 때에는 terraform apply
명령을 실행했을 때와는 반대로 Elastic IP가 먼저 삭제되는 것을 확인하실 수 있으실 겁니다 :)
이번 섹션에서는 복수 리소스를 정의하는 것과 함께 기본적인 리소스 의존성, 그리고 템플릿(interpolation)을 통한 속성 참조를 소개했습니다.
다음 섹션에서는, Provision을 사용해 우리가 실행한 인스턴스에 기본적인 bootstrapping을 수행하는 방법을 알아보도록 하겠습니다.
여기까지 오신 여러분들은 인프라를 생성하고 수정하는건 이제 꽤 익숙해졌을겁니다.
이번에는 프로비저너(Provisioner)들을 사용하여 인스턴스들이 생성된 후 어떻게 해당 인스턴스들을 초기화 (initialize) 할 수 있는지에 대해 알아볼 것입니다.
만약 여러분들이 이미지 기반의 인프라 (아마 Packer로 생성한 이미지)를 이미 사용하고 있다면,
지금까지 알아본 내용으로도 충분할 것입니다.
하지만 인스턴스들에 초기 설정 (initial setup)을 하기 원한다면, 프로비저너(Provisioner)를 사용해 파일을 업로드하고, 쉘 스크립트를 실행하고, 설정 관리을 위한 소프트웨어 같은 것들을 설치하고 실행하도록 여러분을 도울 것입니다.
프로비저너 (Provisioner)를 정의하기 위해서는,
이전에 설정한 Terraform 설정 파일에서 example
EC2 인스턴스의 리소스를 다음처럼 변경하면 됩니다:
|
|
변경 후의 코드는 다음과 같을 것입니다:
여러분들은 리소스 블럭 내부에 프로비저너(Provisioner) 블럭이 새롭게 추가된 것을 확인하실 수 있을겁니다.
이전 섹션에서 여러 리소스를 정의해서 사용할 수 있었던 것 처럼, 프로비저너(Provisioner) 역시 복수개를 정의하여 여러 단계의 프로비저닝 과정을 구성할 수 있습니다.
Terraform은 여러 종류의 프로비저너(Provisioner)를 지원하지만,
일단 이번 예제에서는 local-exec
프로비저너(Provisioner)만을 사용해보도록 하겠습니다.
이 local-exec
프로비저너(Provisioner)는 다른 프로비저너들과는 다르게 인스턴스 접속에 필요한 정보들 (e.g. SSH Key, Username, Password)에 대해 신경쓰지 않아도 되거든요.
local-exec
프로비저너(Provisioner)는 The local-exec provisioner Teraform을 실행하고 있는 로컬 머신에서 명령을 실행하는 역할을 합니다.
위의 프로비저너 스니펫은 example
EC2 인스턴스에 할당된 Elastic IP를 ip_address.txt
파일로 출력합니다.
프로비저너들은 리소스가 생성될때만 동작합니다.
이미 실행중인 서버에 대해 구성을 관리하고 소프트웨어를 변경하기 위해 프로비저너를 사용하는 것은 올바르지 않습니다.
그냥 서버를 부트스트랩(bootstrap)하는 방법이라고 생각하세요.
서버에 대한 구성 관리 (configuration management)가 필요하다면,
진짜 구성 관리 솔루션을 실행시키기 위한 수단으로 Terraform 프로비저닝을 사용하세요!
기존에 생성한 인프라가 제거되었는지 다시 한번 확인하고, terraform apply
명령을 실행하세요:
짠! Terraform이 EC2 인스턴스를 생성하고, local-exec
프로비저너가 동작하는 것을 확인하실 수 있습니다.
Terraform은 프로비저너들의 모든 출력을 콘솔에 표시하지만, 이번에는 redirection으로 인해 출력 결과가 보이지 않을 겁니다.
그래서 ip_address.txt
파일을 확인해 local-exec
프로비저너가 잘 동작했는지 검증해보도록 하죠.
cat ip_address.txt
명령과 terraform show | grep ip
명령을 각각 실행해보고, 결과를 비교해보세요:
우리가 요구한대로, 해당 Elastic IP의 Public IP가 해당 텍스트 파일에 잘 들어간 것을 확인할 수 있습니다!
만일 리소스는 성공적으로 만들었지만 프로비저닝이 실패한다면, Teraform은 에러를 뱉고 해당 리소스를 tainted
(더럽혀진 리소스)로 표기할 것입니다.
tainted
상태로 표시되는 리소스는 물리적으론 생성된 상태이지만, 프로비저닝이 실패했기 때문에 사용하기에 안전하다고 여길수 없습니다.
프로비저닝이 실패한 상태에서 (리소스가 tainted
상태일 때) 여러분들이 다음 실행 계획 (execution plan)을 생성한다면 (terraform plan
명령을 실행한다면),
Terraform은 해당 리소스가 사용하기에 안전하다고 개런티 할 수 없기 때문에 프로비저닝에 실패한 해당 리소스에 대해 프로비저닝을 다시 시도하지 않습니다.
대신 해당 tainted
상태의 리소스를 삭제하고 다시 리소스를 생성한 뒤 프로비저닝을 다시 시도합니다.
Terraform은 또한 apply
단계에서 프로비저닝 실패시 자동으로 작업한 내용을 롤백(rollback)하고 해당 리소스를 제거하지 않습니다.
그냥 tainted
상태로 마크만 해 둘것입니다.
왜냐하면, 실행 계획 (Execution Plan)에서 생성한 계획은 리소스를 ‘생성’ 한다는 것이었지, ‘삭제’ 하는건 아니었기 때문입니다.
만약 롤백을 위해 리소스를 ‘삭제’하게 된다면, 이는 실행 계획의 존재 목적에 반하게 되는 것이기도 합니다.
대신 만약 여러분이 tainted
상태의 리소스가 존재하는 상태에서 새로운 실행 계획을 만든다면 (terraform plan
명령을 실행한다면),
새롭게 만들어진 실행 계획에서는 깨끗한 상태를 위해서 tainted
상태의 해당 리소스를 삭제하도록 할 것입니다.
프로비저닝은 인스턴스를 부트스트랩(bootstrap)이 가능하게 하기 때문에 중요합니다.
다시 한번 말씀드리지만, Terraform에서의 프로비저닝은 구성 관리(configuration management)를 대체하는 위한 것이 아닙니다.
그냥 머신을 처음 쓰기 위해 부트스트랩(bootstrap)하는거에요.
여러분들이 구성 관리(configuration management)를 사용한다면,
Terraform에서의 프로비저닝은 구성 관리 도구(configuration management tool)를 부트스트랩(bootstrap) 하기 위한 수단으로 사용하셔야 합니다.
다음 섹션에서 우리는 설정을 파라미터화(parameterize) 하기 위해 변수(variable)를 알아볼 것입니다.
여러분들은 유용한 설정들을 만들기 위한 Terraform 지식을 충분히 얻었습니다.
하지만 우리는 아직도 액세스 키 (Access Key), AMI와 같은 정보를 설정파일에 직접 기입(hard-coding)해야 합니다.
Terraform 설정을 공유할 수 있고, 버전관리 할 수 있게하려면, 우리는 설정을 ‘파라미터화’ (parameterize) 해야합니다.
이번 섹션에서는 이를 가능하게하는 입력 변수에 대해 소개할 것입니다.
첫번째로 우리가 만들었던 설정 파일에서 AWS 액세스 키 (access key), 비밀 키 (secret key), 그리고 리전 정보를 변수들로 뽑아내 봅시다.
example.tf
설정 파일을 만들었던 디렉토리에서 아래 내용을 variables.tf
파일로 저장하세요.
참고: Terraform은 작업 디렉토리 (Working directory) 내의 모든
.tf
파일을 불러오기 때문에, 파일명은 원하는 이름을 써도 됩니다. 여기선 그냥 편의를 위해서variables.tf
를 쓸 뿐입니다.
위 스니펫은 Terraform 환경설정 내에 세개의 변수를 정의합니다.
처음 두 변수(access_key, secret_key)들은 빈 블럭({}
)을 가지고 있고,
세번째 변수는 기본값(default = "ap-northeast-2"
)을 설정했습니다.
만약 기본값이 설정되어 있으면, 해당 변수는 선택항목(optional) 입니다.
반대로 기본값이 설정되어 있지 않으면, 해당 변수는 필수항목(required)가 되겠죠.
이 상태에서 terraform plan
명령을 실행하면, 아래 화면처럼 Terraform이 할당되지 않은 변수에 입력할 값을 물어볼 것입니다.
이번엔 AWS Provider 설정을 다음과 같이 변경하세요:
|
|
변경 후의 example.tf
코드는 다음과 같을 것입니다:
위 스니펫은 더 많은 템플릿 표기법(interpolations)을 사용합니다.
이전에 리소스를 참조했던 것과는 다르게, 이번에는 var.
접두사를 사용합니다.
이는 Terraform에게 변수에 접근하려는 것임을 알려주게 됩니다.
즉, 위 스니펫은 지정한 변수를 사용해 AWS Provider를 구성할 수 있도록 합니다.
변수에 값을 할당하는 방법은 여러가지 방법이 있습니다.
아래에서 소개하는 각 방법들은 어떤 순서로 변수의 값을 선택되는지에 대한 순서이기도 합니다.
즉, 아래는 내림차순으로 된 변수를 선택할 때 고려되는 우선순위입니다 (아래로 갈수록 우선순위 낮음)
여러분은 명령줄(command-line)에서 -var
플래그를 통해 변수의 값을 직접 할당하실 수 있습니다.apply
, plan
, refresh
와 같이 환경설정을 읽는 Terraform의 명령이라면 -var
플래그를 사용할 수 있습니다.
아래는 명령줄 플래그로 변수의 값을 할당하는 예제입니다:
|
|
다만 이 방법으로 변수에 값을 할당한다면 해당 값들은 저장되지 않기 때문에, Terraform의 명령들을 실행할때마다 반복해서 입력해야만 합니다.
변수의 값들을 쭉 보존하고 싶다면, 파일을 하나 만들어서 안에 값들을 집어넣는 방법을 써도 됩니다.
아래처럼 terraform.tfvars
이름을 가진 파일을 만들기만 하면 됩니다:
만약 현재 작업 디렉토리에 terraform.tfvars
파일이 존재한다면,
Terraform은 자동으로 해당 파일을 읽어 변수에 값을 할당합니다.
만약 변수 값을 가진 파일이 terraform.tfvars
가 아니라면,
여러분은 -var-file
플래그를 사용해 직접 읽어올 파일을 지정할 수 있습니다.
또한 이 파일은 이전에 Terraform 설정 파일 (.tf
)과 동일하게 HCL(Hashicorp Configuration Language)를 사용하고,
Terraform 설정 파일처럼 JSON으로도 작성해서 사용하실 수 있습니다.
Terraform은 사용자 아이디 (username)과 비밀번호를 버전 관리 시스템에 저장하는것을 권장하지 않습니다.
하지만, 여러분들은 로컬에 비밀 변수 파일(secret variables file)을 만들고 -var-file
플래그를 통해 사용할 수 있습니다.
그리고 한 명령에서 복수개의 -var-file
플래그를 사용할 수 있습니다.
가령 버전 관리 시스템에 어떤건 체크인되고 어떤건 체크인되지 않는 상황이라면 다음과 같을 것입니다:
|
|
Terraform은 변수의 값을 찾기 위해 TF_VAR_${name}
형식의 이름을 가진 환경변수 또한 읽을 것입니다.
예를 들면, TF_VAR_access_key
라는 이름의 환경변수가 존재한다면 Terraform은 access_key
라는 이름의 변수의 값으로 해당 환경변수의 값을 할당합니다.
(TF_VAR_access_key=foo
인 경우 access_key
의 값으로 foo
할당)
참고: 환경변수는 오직 문자열 형식으로만 변수 값을 할당합니다.
리스트(List)와 맵(Map) 타입의 변수을 할당하고 싶다면, 다른 매커니즘을 사용해 값을 할당해야 합니다.
만약 여러분이 아무것도 하지 않고 (값 할당 없이) terraform plan
명령이나 terraform plan
명령을 실행한다면,
아래와 같이 Terraform은 사용자에게 값을 입력하도록 물어볼 것입니다.
이전에 언급했듯, 이 방법은 값이 저장되지 않습니다. 하지만 Terraform을 처음 사용하는 사용자에게는 좋은 사용자 경험이 될 것입니다.
참고: UI 입력은 환경변수와 동일하게 오직 문자열 형식으로만 변수 값을 할당합니다.
리스트(List)와 맵(Map) 타입의 변수을 할당하고 싶다면, 다른 매커니즘을 사용해 값을 할당해야 합니다.
만약 위에서 소개한 방법 중 그 어떤 곳에서도 변수의 값을 할당하는 곳이 없고, 정의한 변수가 기본값을 가지고 있다면 해당 기본값을 변수에 할당합니다.
리스트는 명시적으로도 정의할 수 있고 암묵적으로도 정의할 수 있습니다.
|
|
여러분들은 terraform.tfvars
파일에서 리스트의 값들을 할당하실 수 있습니다:
|
|
우리는 기존 환경설정 내의 민감한 정보들(API Key나 Secret Key 같은)을 변수를 사용하도록 교체했습니다.
하지만, AMI의 경우에는 아직까지 직접 기입하고 있죠.
불행하게도, AMI는 사용중인 리전에 따라 지정되어야 합니다. (서울 리전에서의 Ubuntu 16.04 LTS AMI와 버지니아 리전의 Ubuntu 16.04 LTS AMI의 AMI ID가 다르거든요)
한가지 옵션으로 그냥 유저에게 리전에 적합한 AMI를 직접 입력하도록 묻는 것이 있겠지만, Terraform은 맵(Map)을 사용해 그거보다 더 나은 방법으로 문제를 해결할 수 있습니다.
맵(Map)은 테이블에서 특정 키로 값을 찾아 변수에 할당하기 위한 방법입니다.
아래 예제는 이를 정말 잘 보여줍니다. 우리의 AMI들을 맵으로 추출하고 us-west-2
리전을 지원해보도록 만들어 봅시다.
다음 내용을 variables.tf
파일에 추가하세요:
|
|
변경 후 variables.tf
코드는 다음과 같을 것입니다:
Map 타입의 변수는 명시적으로 정의할 수 있고,
아니면 기본 값에 Map 타입의 값을 지정함으로써 해당 변수의 타입이 맵이라는 것을 알려 암묵적으로 정의할수도 있습니다.
위 예제에서는 두 경우 모두를 포함합니다.
자, 그럼 example.tf
의 aws_instance
리소스를 다음처럼 변경하세요:
|
|
변경 후의 example.tf
의 코드는 다음과 같습니다:
이번에 새로운 방식의 템플릿 표기(interpolation)를 보실 겁니다. 바로 함수 호출이죠.lookup
함수는 동적으로 맵의 특정 키의 값을 찾아봅니다.
위 스니펫 기준으로는 var.amis
맵에서 var.region
의 값을 키로 사용하여 값을 찾아봅니다.
지금 위 예제에서는 사용하지 않지만, ${var.amis["ap-northeast-2"]}
를 통해 정적 참조도 가능한 것을 알아두세요.
우리는 위에서 기본 값을 정의했지만, 맵 또한 -var
와 -var-file
플래그를 통해 맵에 대한 값을 할당할 수 있습니다.
예를 들면 다음과 같습니다:
|
|
참고:
-var
플래그를 통해 맵을 할당할 수 있지만,
할당하려는 해당 변수의 기본값을{}
으로 설정해 타입을 암시적으로 설정하거나type = map
을 통해 명시적으로 타입을 정의하여야 합니다.
그렇게 하지 않으면, Terraform은 문자열 타입의 변수로 간주하기 때문에 에러를 출력할 것입니다.
아래는 파일을 파일에서 맵을 정의하고, 맵의 값을 정의하고, 맵을 조회해보는 예제입니다. 먼저 변수를 정의하는 것부터 시작하죠.
아직 example.tf
파일이 있는 디렉토리에 계시다면, map-playground
라는 이름의 새로운 디렉터리를 생성하고
해당 디렉토리로 들어간 후 변수를 정의하기 위해 아래 내용으로 variables.tf
파일을 새로 만들어봅시다.
(이전에도 말씀드렸듯이 Terraform은 현재 작업 디렉토리 (Working directory)내의 모든 .tf
파일을 찾아서 불러들이기 떄문에, 동일한 디렉터리에 새로 설정파일을 생성하게되면 이전 예제와 설정이 섞일 수 있습니다. 이를 방지하기 위해 디렉토리를 새로 만드는 것입니다.)
|
|
그리고, 변수를 정의했으니 값을 할당해줘야겠죠? 이번에는 맵에 대한 값을 파일로 저장합니다.
다음 내용을 terraform.tfvars
파일로 저장하세요.
|
|
마지막으로, 다음 내용을 output.tf
파일로 저장하세요.
아래 스니펫은 lookup
함수를 사용해 amis
맵에서 region
변수의 값을 키로 사용하여 값을 찾아 출력할 것입니다.output
이 무얼 뜻하는지 더 궁금하시겠지만, 일단은 조금만 참으세요. 다음 섹션에서 설명할겁니다.
|
|
자, 그럼 terraform apply -var region=us-west-2
명령을 실행해보세요:
region에 따라 ami가 다르게 출력되는 것을 확인하실 수 있습니다!
Terraform은 여러분들의 설정을 파라미터화하기 위한 변수를 제공합니다.
맵은 여러분들이 조건에 따라 테이블의 값들을 찾아볼 수 있도록 할 것입니다.
다음 장부터는 위에서 맵을 사용해보기 위해 새롭게 만든 디렉토리와 환경설정들을 더이상 필요로 하지 않습니다.map-playground
디렉토리와 디랙토리 내의 모든 파일들을 삭제하고,
이전에 EC2 인스턴스와 Elastic IP를 정의했던 Terraform 설정파일이 있는 디렉토리로 다시 되돌아가세요.
이전 섹션에서 입력 변수(input variables)를 Terraform 구성 설정을 파라미터화(parameterize) 하는 방법으로 소개했습니다.
이번 섹션에서는 출력 변수(output variables)를 Terraform 유저가 데이터를 쉽게 질의하고 볼 수 있도록 관리하는 방법으로 소개할 것입니다.
잠재적으로 복잡한 인프라를 만들때, Terraform은 여러분들의 모든 리소스에 대한 100~1000개의 속성 값들을 저장합니다.
하지만 Terraform를 사용하는 유저는 로드 밸런서 IP, VPN 주소와 같은 오직 몇가지의 중요한 속성 값만 관심을 가지고 있을 것입니다.
출력 변수는 Terraform에게 어떤 정보가 중요한지 알리는 방법입니다.
출력 변수로 지정한 데이터는 terraform apply
명령을 실행하면 출력되고, terraform output
명령을 사용해 질의할 수도 있습니다.
이전에 우리가 생성한 Elastic IP 리소스의 공인 IP(Public IP)를 보기 위해 출력 변수를 정의해봅시다.
example.tf
파일을 열어서 아래 내용을 추가하세요:
|
|
변경 후의 코드는 다음과 같을 것입니다:
위 스니펫은 ip
라는 이름의 출력 변수를 정의합니다. value
필드는 해당 출력 변수가 어떤 값을 가질지를 정의하고, 거의 항상 하나 이상의 템플릿 표기법(interpolations)들을 가지고 있습니다. 대부분의 값들은 동적이기 때문이죠.
이번 경우에는 Elastic IP 리소스의 public_ip
속성을 값으로 출력할 것입니다.
여러개의 출력 변수를 정의하기 위해 복수의 output
블럭을 사용할 수도 있습니다.
출력 변수에 값을 채워넣기 위해 terraform apply
명령을 실행해보세요. 출력 변수를 정의했다면 반드시 한번 해야 합니다.
아마 명령을 실행한 결과가 이전과 조금 다를겁니다.
이렇개 실행 결과의 끝 부분을 보시면 여러분이 지정한 출력 변수를 확인하실 수 있죠:
terraform apply
명령은 출력 변수의 값들을 강조해 보여줍니다.
또한 여러분들은 terraform apply
명령을 수행해 출력 변수에 값을 할당한 이후부터 terraform output [NAME]
명령을 사용해 출력 변수의 값을 질의할 수 있습니다.
별도로 출력 변수의 이름을 인자로 넘기지 않은 경우 (terraform output
명령을 실행한 경우), Terraform은 모든 출력 변수의 이름과 값을 출력할 것입니다.
반대로 출력 변수의 이름을 인자로 넘기는 경우 (terraform output ip
), Teraform은 해당 이름의 출력 변수의 값만을 출력합니다.
terraform output
, terraform output ip
명령을 각각 실행해 결과를 비교해보세요:
이 명령은 스크립트(e.g. 쉘 스크립트)에서 출력 변수의 값을 추출할 때 유용하게 사용할 수 있습니다.
여러분들은 이제 어떻게 Terraform 환경설정을 입력 변수(input variables)로 파라미터화하고,
출력 변수(output variables)로 중요한 데이터를 추출하고, 프로비저너(provisioners)를 사용해 리소스를 부트스트랩(bootstrap)하는지 알겁니다.
다음 섹션에서 우리는 어떻게 모듈(modules)을 사용하고, 구조에 유용한 추상화, 그리고 Terraform 환경설정을 재사용 하는 방법을 알아볼 것입니다.
그동안 가이드를 따라오면서 만들고 수정했던 Terraform 환경설정 파일들과 리소스는 더이상 사용하지 않습니다.terraform destroy
명령을 통해 해당 리소스들을 모두 삭제하시고, 모든 환경설정 파일 또한 삭제하세요. 다음 섹션에서 새롭게 Terraform 환경설정을 생성할 것입니다.
여기까지는 우리는 Terraform을 환경설정을 직접 수정하여 Terraform을 구성했습니다.
인프라가 성장함에 따라, 이 방법은 몇가지 문제를 가지고 있습니다: 인프라를 구성하는 구조(organization)의 부재, 재사용성의 부재, 그리고 팀을 위한 관리의 어려움.
Terraform의 모듈은 그룹으로 관리되는 Terraform 설정들을 가지고 있는 패키지(self-contained packages of Terraform configurations)입니다.
모듈은 재사용한 컴포넌트를 만들고, 구조를 개선시키며, 인프라를 각 부분을 블랙박스로 처리(to treat pieces of infrastructure as a black box.) 할 수 있습니다.
이 섹션은 모듈을 사용하는 기초를 다룹니다. 모듈을 작성하는 방법은 모듈 문서에서 상세히 다루고 있으니 관심이 있으시다면 해당 문서를 참고하시기 바랍니다.
주의: 이 엑션의 예제들은 AWS 프리 티어에 적용되지 않습니다.
조금의 요금이라도 지불할 의사가 없으시다면, 이 섹션의 예제를 실행하지 마세요.
아래 예제에서는 완전한 Consul 클러스터를 구성하는 Consul Terraform 모듈을 사용할 것입니다.
아래 내용으로 새롭게 Terraform 환경설정 파일을 만드세요.
저는 consul-terraform.tf
으로 저장했습니다.
|
|
전체 코드는 다음과 같을 것입니다:
참고로 위
provider
블럭은 AWS CLI에서 사용하는 환경변수를 사용한다면 생략할 수 있습니다. 자세한 내용은 AWS Provider 문서의 Argument Reference와 AWS Provider 문서의 Environment Variables를 참고하세요.
중요: 이 Consul 모듈은 기본 VPC를 가지고 있는 AWS 계정을 필요로합니다.
module
블럭은 Terraform에게 모듈을 만들고 관리하도록 알립니다.
이전에 봤던 리소스를 정의하는 resource
블럭과 아주 유사하죠.
다만 모듈은 리소스와 달리 오직 하나의 논리적 이름 (logical name, 별명으로 생각하시면 됩니다.)만 가지고 있습니다.
위 예제에서는 consul
이 논리적 이름이 되겠죠.
module
블럭 내에 정의된 source
속성은 모듈 내 속성중 유일한 필수 항목입니다.source
속성은 Terraform에게 어디서 모듈을 받아올 지 알립니다.
Terraform은 여러분들을 위해 모듈들을 자동으로 내려받고 관리해줄 것입니다.
위 예제에서는 모듈을 Github에서 가져오지만, Terraform은 Git, Mercurial, HTTP, 그리고 파일 경로와 같은 다양한 소스들에서 모듈을 받아올 수 있습니다.
이외 다른 설정들은 모듈의 인자(parameter) 입니다. 위 예제에서는 변수를 참조하도록 값을 채워 넣었습니다.
여러분들이 모듈을 정의하고 나서 plan
이나 apply
같은 명령을 실행하기 위해서 실행 전에 한번은 모듈을 얻어오는(내려받고 설치하는) 과정이 필요할 것입니다.terraform get
명령으로 바로 그 작업을 수행할 수 있습니다. 바로 실행해보세요:
Terraform이 알아서 모듈을 얻어오는 것을 확인하실 수 있습니다.
terraform get
명령은 내려받지 않은 모듈들이 있다면, 내려받지 않은 모듈들을 내려받을 것입니다.
기본적으로 terraform get
명령은 업데이트를 확인하지 않습니다. 그렇기 때문에 안전하게 (그리고 빠르게) 여러번 실행할 수 있습니다.
만약 업데이트를 확인하고 내려받고 싶으시다면 -u
플래그를 사용하시면 됩니다. Golang을 쓰시는 분들은 뭔가 좀 익숙하시죠?
모듈들을 내려받았다면, 우리는 이제 plan
명령을 통해 계획을 점검하고 apply
명령을 통해 이를 적용해볼 수 있습니다.
여러분들이 terraform plan
명령을 실행한다면 다음과 같은 출력을 볼것입니다:
개념적으로 모듈은 블랙박스(block box)처럼 취급되더라도,
게획(plan)에서 Terraform은 모듈이 괸리하는 각 리소스를 보여주므로 계획에서 수행하는 작업에 대한 상세한 정보를 볼 수 있습니다.
만약 여러분들이 줄여진 계획(실행계획) 출력을 원하신다면 -module-depth=number
플래그를 사용해 Terraform이 모듈별로 요약을 출력하도록 할 수 있습니다.
예로 terraform plan -module-depth=0
명령을 실행해보면 이전과 다르게 출력이 줄어든 것을 확인할 수 있습니다:
다음은 terraform apply
명령을 실행해 모듈을 만들어봅시다.
위에서 경고했듯이, 이 Consul 모듈은 AWS 프리티어에 해당하지 않는 리소스를 생성하기 때문에 약간의 비용이 발생하니 참고하시기 바랍니다.
자, 그럼 terraform apply
명령을 실행해봅시다!
몇분정도만 기다리면, 세개의 서버로 이루어진 Consul 클러스터가 동작하는 것을 얻을 겁니다!
Consul이 어떻게 동작하고, Consul을 어떻게 설치하고, 그리고 Consul을 어떻게 클러스터로 구성하는지 아무런 지식을 가지고 있지 않지만,
우리는 단 몇분만에 실제로 돌아가는 Consul 클러스터를 만들었습니다. 짱이죠?
위의 서버와 같은 속성을 설정해 모듈을 파라미터화(parameterize)했던 것처럼, 모듈은 정보를 출력 할 수도 있습니다 (리소스에서 정보를 출력했던 것처럼요).
여러분들은 모듈들이 어떤 출력을 지원하는지 알기 위해서 각 모듈드의 코드나 문서를 참조해야합니다만,
이 가이드에서는 그냥 여러분들께 Consul 모듈이 구성된 Consul 서버들에 대한 주소 정보를 가지고있는 server_address
라는 이름을 가진 출력값을 가지고 있다는걸 알려드릴겁니다.
이 server_address
를 참조하기 위해서 일단 출력 변수에 저장해보도록 하겠습니다.
모듈 출력은 출력 변수 뿐만 아니라 다른 리소스(Elastic IP 리소스를 구성할 때 EC2 인스턴스의 ID를 참조했던 것 처럼), 다른 Provider를 구성할 때 등 어디에서든지 참조해 사용할 수 있습니다.
이전에 만들었던 consul-terraform.tf
파일에 아래 내용을 추가하세요:
|
|
변경 후 코드는 다음과 같습니다:
모듈 출력을 참조하기 위한 문법은 아주 친숙할겁니다.
문법은 ${module.NAME.ATTRIBUTE}
입니다.NAME
은 전에 우리가 할당한 논리적 이름(logical name)이고,ATTRIBUTE
는 모듈이 출력하는 속성의 이름입니다. server_address
가 이에 해당하겠죠.
여러분들이 terraform apply
명령을 다시 실행한다면 Terraform은 아무런 변경사항을 만들지 않을겁니다만,
명령의 실행 결과에서 Consul 서버의 주소를 consul_address
이름의 출력값으로 확인할 수 있을 것입니다:
생성한 Consul 모듈은 이제 더이상 사용하지 않습니다. 필요치 않은 과금을 방지하려면 반드시
terraform destroy
명령을 통해 인프라를 제거하세요!
어떤 source
종류들을 지원하는지, 어떻게 모듈을 작성하는지 등 모듈에 대한 자세한 정보를 얻기 원하신다면 모듈 문서를 참고하세요.
여러분들이 Terraform이 유용하다는 것을 알 수 있을뿐만 아니라, 여기서 배운 지식들로 여러분들의 인프라 구축을 향상시킬 수 있기를 바랍니다.
드디어 여러분들은 첫 걸음마를 뗐습니다.
더 많은 것들을 배우시고 싶은 분들을 위해 참고할만한 몇가지 링크를 남겨놓으니 참고하시기 바랍니다.
처음에는 직접 가이드를 써보려고 했는데 생각보다 범위가 넓어 Terraform의 Getting Started 문서를 한국어로 번역하는것을 기본으로 살을 좀 더 붙였습니다.
영어실력이 미천하여 다소 번역이 딱딱하거나 엉성할 수 있습니다 ㅠㅠ
사실 저는 DevOps 엔지니어가 아니라 풀스택 개발자입니다만, 최근에는 서비스를 배포하는 방법이 점점 다양하고 중요해지는 것 같아 DevOps를 열심히 공부하고 있습니다.
웹 생태계는 해가 지날수록 눈 깜짝하는 사이에 정말 많은것이 쏟아져나오고 빠르게 변화하고 있는데, DevOps 역시 정말 빠르게 변화하고 진화하는 것 같네요. 흥미진진합니다.
Hashicorp의 다른 제품들, 그리고 그것들을 묶어서 만든 에코시스템인 Atlas도 정말 대단합니다.
Hashicorp - Devops Defined를 보면 목표도 확실한 것 같고요.
예전에 Hashicorp의 대표인 Mitchell Hashimoto의 인터뷰 기사를 본 적이 있는데, DevOps를 바로잡기 위헤서 Terraform과 같은 도구들을 만들었다고 언급하기도 했죠.
오픈소스 프로젝트들에서 vagrant up
뿐만 아니라 terraform apply
도 자주 마주칠 수 있기를 기대해봅니다!
작년즈음 귀뚜라미보일러에서 Wi-Fi를 통해 스마트폰 제어가 가능한 온도 조절기를 발매했다는 소식을 들었습니다만,
아쉽게도 최신형 보일러에서만 사용할 수 있어 써볼 수가 없었습니다.
어느새 기억에서 잊혀져가고 있다가, 최근 클리앙의 한 사용기를 보고 구형 보일러에서도 사용이 가능하다는 것을 알게 되었죠.
그런데 해당 사용기에서 MITM (Man in the middle, 중간자 공격)에 취약하다는 언급이 클리앙 새로운 소식의 뉴스로 등장하게 되면서, 유저들의 수 많은 질타를 받게 됩니다.
현 시점에서는 안드로이드 사용자에 대해서만 HTTPS를 사용하도록 API가 새로 만들어지고, App이 업데이트 되었습니다.
Philips의 Hue, Xiaomi의 Home Gateway를 사용하고 있는 중인데, 국내 IoT 제품들의 완성도를 구경해보고도 싶고, 무엇보다 최근에 발견한 Homebridge라는 Node.js 기반 Homekit API 에뮬레이션 서버에 붙여보고 싶은 욕심이 들더라고요.
그래서.. 질렀고, 어제 수령해서 설치했습니다.
패키지는 A4 크기의 소설책 한권 수준의 크기입니다.
보일러 및 온도 조절기에 따라 신형 (NCTR-10WIFI) / 순간식 (CTR-15WIFI) / 저탕식 (CTR-15WIFI) 로 구분됩니다.
CTR-15WIFI의 경우 순간식/저탕식 구분이 있습니다만, 기기 내 별도 설정 메뉴에 접근해서 변경이 가능합니다.
저는 순간식 모델입니다.
패키지 구성은 온도 조절기 본체, 백플레이트, 백플레이트 고정 나사, 설명서가 전부입니다.
저는 패키지에 포함된 백플레이트 고정 나사가 짧아서 기존 나사를 재활용했습니다.
기존 온도 조절기가 설치되어 있는 모습니다. (좌하단)
기존에 설치된 온도 조절기는 CTR-5700PLUS 모델인데, 이 모델은 보일러 모델에 따라 순간식/저탕식이 나뉩니다.
백플레이트는 기본적으로 장착된 상태인데, 탈거한 모습입니다. 캐패시터가 꽤 눈에띄네요.
옆을 보면 본체 하우징을 탈거할수 있는 홈이 보입니다.
탑 (액정측)하우징에는 걸쇠가, 바텀 하우징에는 홀이 있고 걸쇠가 홀에 걸치는 방식으로 본체 하우징이 고정되는 방식입니다.
궁금한걸 못참는 공돌이라 설치 전에 후다닥 뜯어봅니다.
역시나 캐패시터가 크고 아릅답..지는 않습니다.
궁금헸던 MCU와 Wi-FI 칩셋은 디스플레이 아래에 위치하고 있습니다.
아쉽게도 디스플레이 패널 상단이 납땜되어 있어 들어내서 확인할 수는 없었습니다.
좌상단에 두번째 통신 채널 혹은 디버깅용 포트로 추정되는 홀이 보이네요.
역시나 캐패시터가 눈에 띕니다.
좌하단엔 온도 측정을 위해 사용되는 것으로 추정되는 서미스터 (thermistor)가 보입니다.
안전을 위해 절연장갑을 착용하고, 기존에 사용중이던 온도 조절기는 전원버튼을 눌러서 끕니다.
온도조절기를 위쪽으로 밀어 백플레이트에서 본체를 탈거합니다.
본체에 표시된 극성을 잘 메모해둡니다. (근데 저는 메모했는데도 실수했습니다 ㅋㅋㅋ)
신기하게도 두 온도조절기에 적혀있는 QA 담당자로 추정되는 분의 이름이 똑같습니다 ㅎㅎ
제 것은 얇은 단심으로 된 전선인데, 많이 꼬아진 상태로 스트레스를 많이 받았는지 살짝 움직이니 끊어져버렸습니다.
나사를 풀어 기존 온도조절기에 고정된 전선을 빼냅니다.
새 온도조절기와 기존 온도조절기의 크기가 다르기 때문에 백플레이트도 교체해야합니다.
다행히 스크류 홀의 위치는 똑같아서 특별한 이슈 없이 교체할 수 있습니다.
기존 백플레이트 탈거 후 새로운 백플레이트를 설치합니다.
(새 백플레이트 설치 사진은 깜빡하고 안찍었네요.. ㅠㅠ)
새 온도조절기와 결선을 하기 전에, 기존 전선의 길이가 애매해서 정리하기로 합니다.
일단 여러 전선을 감싸고 있는 첫 피복을 와이어 스트리퍼로 벗겨냅니다.
두가닥 전선의 길이를 니퍼로 깔끔하게 맞춰줍니다.
각 전선은 연결을 위해 와이어 스트리퍼로 피복을 다시 벗겨냅니다.
드라이버를 사용하면 쉽게 빠지지 않는 접속부를 쉽게 꺼낼 수 있습니다.
온도 조절기와 기존 전선을 연결합니다.
쨔잔! 연결 즉시 전원이 잘 들어오는걸 확인할 수 있습니다.
이 상태에서 전원버튼을 눌러서 잠시 꺼두고 (온도조절기가 실제로 꺼지는 건 아니였습니다..)
백플레이트와 결합합니다.
설치가 잘 되었습니다.
(이때까지만 해도 연동에서 말썽을 일으킬 줄은 상상도 못했습니다….)
연동을 위한 AP 모드 진입은 쉽습니다.
전원을 끈 상태에서 예약설정/수온설정 버튼을 5초간 누르고 있으면…
설정을 위한 AP 모드로 진입합니다. 현재 상태에서는 검색은 되나 접속이 되지 않습니다.
30초정도 기다리면 AP모드 활성화를 의미하는 파란 안테나 아이콘이 LCD 우상단에 표시됩니다.
이 상태에서 앱에서 초기 연동 관련 설정을 진행하면 됩니다.
물론 호기심이 발동해 바로 연동 관련 설정을 진행하지 않았습니다 (…)
일단 AP 모드에서 어떤 포트가 열려있는지 궁금했습니다.
설정을 위한 AP Mode라 하더라도 AP Mode는 이름 그대로 Access Point Mode이므로 Wi-Fi 되는 디바이스면 다 붙을수 있지요.
맥북으로 붙어봅니다.
DHCP로 192.168.200.2 ~ 192.168.200.254 대역을 할당해서 쓰네요.
게이트웨이는 192.168.200.1 이므로 192.168.200.1 호스트에 대해 스캔을 해보면…
80번이 열려있네요. 아마 시스템 관련 설정페이지겠지요.
curl으로 빠르게 훑어보면…
역시나 디바이스 관련 설정이 있습니다.
펌웨어 버전, MAC Address, 디바이스 재부팅 버튼, 펌웨어 업데이트 버튼이 보이네요.
브라우저에서 보면 이렇습니다.
lwIP를 서버로 쓰고 있네요. lwIP는 BSD 라이선스인데 별도로 저작권자 표기를 발견하지는 못했습니다.
아쉬운 부분입니다.
더 볼 것이 없어서 스킵을 하려고 하는데….
호스트, 그러니까 온도 조절기가 먹통이 되는 사태가 발생했습니다. 다량의 패킷을 쏴서 그런지 먹통이 된 것 같네요.
전원버튼을 눌러서 전원을 끄고 다시 AP 모드에 진입을 수십번 반복했지만, 파란색 안테나 아이콘이 뜨지 않습니다.
AP 모드 진입 후 파란색 안테나 아이콘이 뜨기까지 십분 이상 기다렸는데도 활성화가 되지 않습니다 -_-;;
아무리 생각해봐도 기기에 붙어있는 ‘전원 버튼’ 은 실제로 온도 제어기의 전원을 제어하는 것은 아닌 것 같아서,
리셋 버튼이나 리셋 핀을 찾아보았습니다만 그 어디에도 없었습니다.
이 상황에서 확실히 온도 제어기를 껐다가 다시 켜는 방법은 연결된 보일러를 끄는 것이라 유일하다고 판단을 했는데…
장식장 뒤편에 보일러실이 있어서 보일러 전원 코드를 뽑을수도 없는 상황이었습니다.
백플레이트 뜯어내서 연결된 전선을 끊고 다시 연결할까 생각해봤지만…
귀찮아서 결국 보일러랑 연결된 차단기를 찾아서 차단기를 내렸다가 다시 올렸습니다.
그랬더니 예상대로 AP모드가 잘 활성화가 되었습니다.
근데 이번에는 아이폰에서 공유기 연결을 위한 초기 설정이 안됩니다.
iPhone 6 Plus / iOS 10.0.2 를 사용하고 있는데, 아이폰에서 AP모드를 붙고 나서 설정을 시도하면 또 먹통이 됩니다… -_-;;;;
차단기 내리고 다시 설정… 을 몇번 반복하다가 그냥 노는 안드로이드 단말 찾아서 설정하니 한방에 붙네요.
허탈했습니다….
공유기에 정상적으로 붙으면 이렇게 PASS 텍스트가 찍힙니다.
회원가입후 기기 등록까지 완료되면 최종적으로 이런 화면이 표시됩니다.
개인적으로 insecure/suspicious 하다고 판단되는 장비들은 별도의 Bridged AP로 고립시키고,
필요시 tcpdump를 통해 언제든지 패킷스니핑이 가능하도록 내부망을 운용하고 있습니다.
대강 요런 모습입니다.
저렇게 Raspberry pi (무려 1세대 Model B 모델!)에 iptime N300UA를 붙여서 Soft AP를 만든것이죠.
App에서 오고 가는 HTTP/HTTPS 패킷들은 주로 Charles Proxy를 통해 쉽고 간편하게 훔쳐볼 수 있고,
breakpoint를 걸어 request/response를 마음대로 제어할 수 있기 때문에 앱에서 사용하는 HTTP/HTTPS
기반 API를 뜯어보기에 좋습니다.
AP 모드에서 초기 연동 설정을 하기 이전에
tcpdump over ssh + wireshark 조합으로 온도 조절기에서 오고 가는 패킷들을 캡쳐하고,
Chalres Proxy를 통해 App에서 오고 가는 패킷들을 캡쳐하도록 설정했습니다.
(공유기 연결 이전에 캡쳐를 시작했기 때문에, DHCP로 IP를 받아오고, ARP 응답을 하는 것이 보입니다.)
초기 공유기와 연결이 되면, TCP를 통해 서버에 기기 등록 관련 메세지를 즉시 보냅니다. 재미있는 점은, 별도로 DNS 질의를 하지 않는다는 것입니다.
즉, 도메인이 아닌 IP가 펌웨어에 하드코딩 되어 있습니다. 물론 IP가 변경될 일은 없겠지만요.
Round-robin DNS로 load balancing을 할 수도 있을거란 생각을 해보지만, 사용자가 소규모라 따로 분산처리가 필요치 않다고 판단했겠지요. (Xiaomi의 경우에는 Round robin DNS를 위해 A 레코드에 18개 호스트가 등록되어 있습니다)
온도 제어기와 서버간 TCP 패킷은 암호화 되어있지 않습니다.
(다시 Xiaomi와 비교해보자면, Xiaomi는 Message Header는 암호화 되어 있지 않지만, message body는 AES-128-CBC로 암호화되어 오고갑니다.)
또 재미있는것은, 인터넷 연결 상태를 확인하기 위해 2분마다 google.com 의 A 레코드를 조회하는 DNS Query를 보낸다는 것입니다.
어쩄거나 패킷이 암호화 되어 있지 않은 상태이기 때문에, 패킷의 구조나 명령 종류에 대해서는 패킷을 분석해보면 쉽게 알아낼 수 있을것입니다.
다르게 생각해보자면, 내부망에 침입이 가능한 경우 API를 통해서가 아니라 직접 디바이스로 명령 패킷을 보낼 수도 있다는 것이겠지요 :)
패킷 수준의 분석은 일단 충분한 정보를 얻었다고 판단해 여기까지 진행하고, HTTP(S) API를 살펴보기로 합니다.
애플리케이션 - 서버간 사용하는 API는 크게 두가지로 나뉩니다.
첫번째는, iOS App과 구버전 Android App에서 사용하는 HTTP API
두번째는, 업데이트된 최신 Android App에서 사용하는 HTTPS API
가 있습니다.
위에서 초기 연동에 문제가 있어서 안드로이드 앱을 사용했기 때문에,
안드로이드에서 사용하는 업데이트된 HTTPS API를 살펴보겠습니다.
Charles Proxy로 가로챈 Request입니다. HTTPS Protocol을 확인할 수 있습니다.
HTTP에서 HTTPS로 변경은 되었으나, POODLE Attack 위험이 있는 SSLv3을 지원하고 있습니다.
또한, HSTS 헤더가 존재하지 않기 때문에 별도의 루트 인증서 설치 없이도 SSL MITM이 쉽게 가능합니다.
회원가입에서 사용하는 주소검색 API는 여전히 HTTP를 사용합니다.
이와 별개로, 우편번호 검색 API에서 질의가 exact match이기 때문에 질의에 공백이 들어가면 검색 결과가 나타나지 않고,
건물명이 포함된 우편번호를 검색하는 경우, 일부만 입력하면 검색결과가 나타나지 않아 불편합니다.
예:
귀뚜라미빌딩
으로 등록된 우편번호 검색시귀뚜라미
만 입력하는 경우 검색 불가
좀 아쉬운건, 아이디/비밀번호를 보내는 의미를 알 수 없는 분리된 API 3개가 존재한다는 것입니다.
위 이미지는 회원가입 이후 로그인시 날아가는 요청들인데, 각 역할은 다음과 같습니다.
개인적으로는 1~3을 굳이 분리할 필요가 있나 생각이 듭니다. 단일 API로 통일해도 충분했을 것 같은데 말이죠 :)
인증 매커니즘은 많이 아쉬운 부분입니다. 이건 그냥 Cookie 대신 Custom HTTP Header를 통해 세션키를 쓰겠다는 것과 다름없어 보이는데요.
디바이스로 보낼 명령은 단일 API를 사용하는 것 같습니다.
실내온도 21도로 설정했을때의 요청입니다.
JSON payload 내 message
field에 직접 Packet payload (in hex string) 를 실어 보내네요.
어디서 본 것 같다고 생각했는데 바로 위 TCP Packet Payload에서 봤던 그것이었습니다.
HTTPS 요청과 응답 역시 시간을 들여 분석해보면 더 자세한 것을 알 수 있을것입니다.
그래도 HTTP 대비 나아진 부분이 보이네요. 더이상 Response payload에 password가 포함되어 있지 않습니다.
iOS 앱에서 사용하는 HTTP API를 위와 같은 방법으로 살펴보았으나,
이외에는 큰 차이가 없어 자세히 기술하지는 않습니다.
어느정도 내부를 뜯어보았으니, 이 다음은 Homebridge plugin을 만들어 Homekit에 붙여볼 차례입니다.
이 부분은 진행인 작업이고, 완료되면 별도 포스트로 찾아뵙도록 하겠습니다!
]]>Philips Hue는 Personal wireless lighting, 개인 무선 조명을 슬로건으로 가지는 Philips의 조명 제품군입니다.
일반적인 LED 조명들과 동일하게 저전력 고효율의 성능을 가지고 있고, 밝기를 조정하는 디밍 (Dimming)이 가능할 뿐만 아니라 원하는 색상을 색상 피커 (Color picker)에서 찍어 표현할 수도 있습니다.
무엇보다 전구별로 각각 다른 색과 밝기를 조합해 장면 (Scene)을 만들어내 특유의 무드를 연출하기 좋죠.
램프들은 지그비(Zigbee)라는 2.4Ghz대역을 사용하는 무선 통신을 통해 브릿지와 연결되고, 앱과 Siri를 통해 제어가 가능합니다.
Zigbee를 사용하기 때문에 Bluetooth로 페어링되는 다른 조명들과 달리 커버리지가 훨씬 넓고 복수의 조명을 동시 제어(그룹 제어)할 수 있으며, 집 밖에서도 제어가 가능합니다.
공개 API가 있어 서드파티 애플리케이션을 만들수도 있고요.
자세한 내용은 아래 링크와 비디오를 참고해보세요.
올해 7월, Slickdeals를 둘러보던 중 Bestbuy via eBay에서 2세대 Hue white and color ambiance starter kit을 $119에 판매한다는 쓰레드를 보자마자 싸다는 이유로 무작정 스타터 킷을 주문했습니다.
1세대도 아닌 2세대 스타터킷이 $199에 주로 판매되는걸 생각한다면 엄청난 딜이었죠.
이미 1세대 스타터킷을 구매해 사용하고 있었지만 2세대는 아래와 같은 변경사항이 있어서 더더욱 유혹을 뿌리칠 수 없었습니다.
그래서 일단 질렀습니다.
2세대 Hue 제품군은 크게 다음과 같은 변화가 있었습니다:
2세대 브릿지는 Apple의 Homekit을 지원 (Homekit 인증을 위해 하드웨어 일부 변경), 이 덕에 Siri를 통해 음성 제어가 가능
2세대 White and color ambiance bulb는 밝기가 1세대에 비해 더 밝아짐
Lightstrip은 Lightstrip Plus라는 이름으로 변경되고 밝기가 밝아졌으며, 여러개의 스트립을 연결하여 길이 연장이 가능
거치형인 Iris가 단종되고 이를 대체하는 Bloom 출시. Iris 대비 크기가 작아짐
이동 거치가 가능한 충전식 조명 Hue Go 출시
Hue White 및 White ambiance bulb 출시
막상 지르고 나서 정신을 차려보니 전압이 호환되는지 확인을 안해봐서 걱정이 되었습니다.
싸다고 일단 질렀는데, 전압이 안맞으면 낭패니까요…
브릿지부터 찾아봅니다. 1세대처럼 DC 어댑터를 쓸테고, 요즘에 나오는 DC 어댑터들은 대개 프리볼트지만…
혹시 모르니까 미국 Hue 홈페이지의 Hue Bridge 페이지에서 찾아봅니다.
오, 역시 프리볼트를 지원합니다!
게다가 5V 2A 입력이라 PoE Splitter를 통해 전원을 공급해도 되고,
어댑터 잭(꼬다리)를 잘라내 USB로 개조한다면 멀티포트 USB 충전기에 꽂아 콘센트 공간을 줄일수도 있습니다.
다음, White and color ambiance bulb을 봅니다.
으..음…?!?!?!?!???!?!?!!!
아닐거야.. 부정하며 아마존의 Product Q&A를 찾아봅니다…
오.. 마이… 갓….
사실을 받아들이기 싫어서… 어떻게 정확한 정보를 알 수 없을까? 고민하다가
미국에 판매되는 전기제품들은 FCC 인증을 받는다는 것을 떠올리고 FCC ID로 데이터를 찾아봅니다.
그리고… 발견한 정확한 FCC 인증정보! 넘나 다행인것…
저기 보이세요? ACDC board 120V230V!!!!
어깨너머 하드웨어를 배운 저는 저게 (특수문자가 짤려서) AC/DC board 120V~230V. 교류 120v~230v 입력을 받는 AC to DC 회로, 그러니까 프리볼트를 의미한다는것을 곧바로 알아챘습니다.
그리고 뒤늦게 Quora에 올라온 Will Philips Hue work with 220v?도 발견했죠…
요악하자면, Hue White ambiance bulb/Hue White bulb를 제외한 대부분의 제품군들은 220v에서도 사용이 가능합니다.
지금 이 포스트를 쓰고 있는 시점에도 Hue Bridge (2nd gen), Hue Lightstrip Plus, Hue White and color ambiance bulb (2nd gen), Hue Iris를 돼지코만 끼워서 잘 사용하고 있습니다.
2세대 스타터킷을 수령해 기존 1세대 브릿지와 램프를 교체하고 나니 집의 조명 시스템을 모두 Hue로 변경하면 좋겠단 생각이 들었습니다.인간의 욕심은 끝이 없다 (…)
기프트카드 털어내기
첫번째로 추가 구매한 램프는 Hue Lightstrip Plus와 Hue Iris입니다.
Hue Iris는 벽을 향해 조명을 쏴서 간접 조명으로 사용하고, Lightstrip Plus 또한 천장에 설치된 천장등의 외곽을 따라 간접 조명으로 사용해보고 싶었습니다.
무엇보다 천장등에 설치된 FL 형광등 전체를 Lightstrip Plus를 조각내 만든 LED Bar로 교체하기 위해서는 사전 테스트가 필요하기도 했고요.
그리고 수령샷.
Lightstrip Plus를 열어보면 이렇게 생겼습니다.
Iris는 허겁지겁 박스를 뜯고 테스트를 하느라 따로 개봉샷을 찍어둔게 없어 실 사용샷으로 대체합니다.
설치는 그냥 콘센트에 꽂고 앱에서 초기 페어링만하는게 전부라 따로 설명할 내용이 없네요;;
기존 거실등은 2개의 전원이 내려오고, 각 전원은 2개의 EL 램프가 연결되어 총 4개의 램프로 구성되어 있었습니다.
이 중, 한 전원에 연결된
기본적으로 미국에서 구입한 Hue Lightstrip은 11자 110v US 플러그를 사용하기 때문에
]]>