2017년 회고 & 블로그 연말정산

올해, 2017년.

올해는 정말 내게 큰 변화가 있던 한해였다. 특히 개발/프로그래밍쪽으로 다양한 기회를 여러가지 받게 되었고, 정말 많은 성장을 했다고 느낀다.

올해를 월별로 정리해보자면…

1월, DjangoGirls Seoul

1월, 장고걸스 서울에 운영진으로 함께 시작하고 ‘나만의 웹크롤러 만들기’ 시리즈 연재를 시작했다. 그리고 작년 12월에 시작한 TDD 스터디도 함께 진행했다.

2월, 스터디 & 첫 외주

2월, 장고를 이용해 교대 학생 대상으로 새로운 서비스를 오픈했다. 토이 프로젝트로 실제 배포까지 이뤄본 케이스. 이때 Vue를 처음 듣고 이용해보았다. 그리고 데이터분석 관련한 스터디도 진행해 데이터분석 분야에도 관심을 갖기 시작했다. 장고걸스 서울에서 장고 스터디를 운영하는 등 스터디를 다양하게 진행했다. 그리고 장고를 사용한 웹 사이트 개발을 하는 외주를 하나 받기도 했다. 처음으로 돈을 받고 개발을 하는 것이라 굉장한 부담이 되었지만 지금 생각해보면, 이때 약간의 자신감을 얻었다.

3월, 개강!

3월, 블로그 테마를 현재 테마로 바꾸고 웹 크롤러 시리즈를 조금씩 더 쌓아갔다. 그리고 React 기초를 조금씩 배워보려고 시작했다가, JS에 대해 이해가 없어(ES6와 Babel이 뭔지도 모르는 상황) 전혀 진도를 내지 못했다. 게다가 개강이 겹쳐 일정을 내기가 상당히 힘들어졌었다. TDD 스터디도 조금씩 사람이 빠져 3월에는 더이상 진행하지 못하고 마무리 되었다.

4월, 9XD

4월, 9XD 8회 모임을 DevSisters에서 가지게 되었다. 사람들과 이야기를 나누며 느꼈던 것이 작년 말과 이때 내가 공부한 것과 만든 것의 큰 차이가 없다는 것이어서 약간 아쉬웠지만..(1Q에 대체 뭘했나..하던 생각) 그래도 글을 쓰고있는 지금 돌아보니 한 것이 조금은 있는것 같아보인다. :) 9XD 모임에서 발표하는 사람들을 보고 ‘아, 나도 저렇게 발표할 수 있을만큼 실력이 되면 좋겠다’ 라고 생각도 해보고 GraphQL에 대해 알게 되는 계기가 되기도 했다. 그리고 4월말 장고걸스 서울에서 격월 Meetup을 토즈에서 열었는데 생각보다 반응이 좋았다! 내가 사회를 맡았는데 진행에 사람들의 집중을 모으는 것이 어려워 약간 아쉬웠지만 전체적으로 굉장히 즐거운 모임이었다.

5월, ETH

5월, 학교 내 오버워치 대회를 마무리 했다. 우리과 친구들이 (나빼고) 다 잘해서 우승! 그리고 베를린 필하모닉 유로파 콘서트를 보고왔다.(메가박스에서 실시간 스트리밍으로 보여준다. 짱짱.) 그리고 3학년 1학기 실습도 다녀왔다. 학교 생활에 바빴던 5월. 이 기간에 비트코인과 이더리움이 급상승하고 주변에서 아는 사람들은 비트코인과 이더리움을 이야기하기 시작했다.(이때 샀어야 했는데) 이때 가격과 지금 가격이 10배 차이(…) 그리고 5월에 있었던 4회 파이썬 격월 세미나에서 “굥대생의 Hello World!”라는 제목으로 발표를 진행했다. 이 발표는 ‘내가 어떻게 개발을 시작하게 되었나’ 였는데, 지금 글을 쓰는 12월에 돌아보면 이 사이에 정말 많은 성장의 기회를 가졌다는 체감이 된다. 7개월이 아니라 마치 1년 7개월 전의 일 같은 기분.

6월, 터닝포인트

6월, 아는 분의 추천으로 키움증권에서 Python강의를 몇차례 진행했다. 종강 직후 학교 동아리 친구들과 3일정도 일본에 여행을 다녀왔다. 이때 에어팟을 사려고 재고를 미친듯이 찾아다니다 귀국하는 날 아침에 애플스토어에 재고가 들어온 것을 보고 체크아웃하기 직전에 뛰어가 구매했다.(이 에어팟은 지금도 잘 쓰고 다닌다.) 그리고 6월에는 장고걸스 행사 중 가장 큰 행사인 워크숍을 열었다. 이때 MS에서 장소 제공과 Azure Credit을 제공해줘 이 가이드로 수정 가이드를 만들었는데, 블로그에 올려뒀던 가이드에 약간 문제가 있어서 정신없이 뛰어다녔던 기억이 난다.

그리고 6월에는 정말 큰 이벤트가 있었다. 바로 우아한형제들에서 우아한테크캠프 인턴을 모집했던 것. 이때 지원하면서 내가 얻고싶었던 것은 바로 ‘내가 개발을 해도 될까?’라는 질문에 대한 답이었다. 사실 이 질문은 아직도 내가 품고있는 질문이다. ‘나는 과연 개발자로 살아갈 수 있는 사람일까?’ 그래도 굉장히 멋진 기회(코드스쿼드에서 교육을 진행했다!)라고 생각해 떨어질 때 떨어지더라도 지원을 하자! 라는 생각으로 지원서를 넣었고 운좋게 서류와 코딩테스트를 통과했지만 마지막 면접에서 제대로 대답을 하지 못했는지 최종적으로는 불합격 메일을 받았다. 사실 이때 나는 ‘아, 그냥 이길이 내 길이 아닌 것을 빨리 알게 되어서 다행이다.’ 라는 생각을 했다. 말 그대로 취미로 개발을 하더라도 직업으로 삼지는 못하겠구나, 라고 생각하던 중…. 전화가 왔다. 자리가 났는데 혹시 아직 올 생각이 있냐는 전화. 바로 네 라고 대답하고 아, 그래도 문 닫고 들어왔구나.. 하고 생각했다. 그리고 이때 참여하기로 결정한 것은 정말 최고의 선택이었다.

7월, 우아한테크캠프

7월, 우아한테크캠프에서 웹 프론트 트랙에 들어가 JS에 대해 정말 친숙하게 되었다. 그전까지는 잘 쓰지도 못하면서 투덜댔다면 지금은 잘 쓰지는 못하더라도 조금은 알고 투털대는 수준이 되었다. 코드스쿼드 윤지수마스터님이 메인으로 진행해주시고 김정마스터님과 정호영마스터님이 진행하신 트랙도 정말 재미있게 들었다. 그리고 알고리즘의 ㅇ 도 모르던 내가 4차례 정도 진행한 김범준CTO님의 수업을 통해 nlogn이 뭔지, 그리고 문제 해결 능력에 대해 다시한번 생각해보게 되었다.

그뿐만 아니라 7월에는 정말 다양한 제의가 들어왔다. 인프런에서 크롤링 온라인 강의 제안과 패스트캠퍼스에서 크롤링 강의제안을 받았다. 어디서 보셨나 물으니 블로그 글을 보고 연락을 주셨다고 했다. 게다가 출판사에서 집필 제의까지 들어왔다. 정말 믿기지 않을 정도로 좋은 제의를 해주셔서 정말 감사한 마음으로 받았다. (지금 생각해도 놀라운 기회들이다.)

여러분 블로깅하세요, 꼭 하세요!!

8월, 파이콘

8월, 우아한테크캠프에서 팀별로 프로젝트를 진행했다. 웹 프론트에서 CSS가..(한숨) 정말로 까다롭다는 것을 다시한번 느끼게 되었다. 그리고 혼자 개발하는 것이 아니라 여럿이서 팀으로 개발을 한다는 것에서 협업에 대해 많은 경험을 하고 여러가지 생각을 하는 기회가 되었다.

그리고 파이콘 튜토리얼을 신청한 것을 진행하기 위한 사전 준비모임을 갔다가, 발표할 기회까지 얻게 되었다.(원래는 파이콘 발표 신청을 했다가 떨어졌었다.) 준비할 시간이 몇주 없었지만 작년 파이콘을 보며 올해 이루고 싶었던 목표 중 하나였던 파이콘 발표하기를 할 수 있게 된 것이 굉장히 기뻤다. 파이콘 전날 리허설을 하니 딱 45분이 맞았는데, 행사 당일에도 시간이 잘 맞아 다행이었다. 사실 행사장이 2층이라 사람들이 많이 안올까 걱정했는데 사람들이 의자를 다 채우는 것도 모자라 바닥에 앉고 뒤에 가득가득 서있는 모습을 보고 ‘세상에…’ 라는 생각을 했다. 그리고 이 발표는 또다른 기회들을 불러왔다.

여러분 커뮤니티하세요, 꼭 하세요!!

9월, 휴학!!

9월, 휴학하기로 마음먹었다. 사실 이 결정을 하기까지 굉장히 많은 고민을 거쳤다. 교대에서는 휴학을 웬만하면 하지 않고 4년을 쭉 다니다 임용고시를 치른 뒤 졸업하는 것이 일반적이기 때문. 하지만 나에게 주어진 수많은 기회들을 손끝사이로 흘려보내고 싶지 않았다. 욕심이라고 말할 수 있지만, 이 멋진 기회들을 모두 잡고 싶었다. 그래서 휴학을 하기로 결정했다.

휴학하면 여유로울 줄 알았더니…

사실 휴학하고 나서 잠시 여유로웠지만 여유로울 새도 없이 금새 바빠졌다. 크롤링 강의를 진행할 때 만약 이때 학교를 다니고 있었다면 패캠에서 첫기 강의를 망칠 뻔 했다. 강의자료가 모두 준비된 상태가 아니라 매주 2회차씩 강의자료를 준비했더니.. 매주 두번 마감에 쫓기는 기분으로 4주를 보냈던 것 같다. 그러다가 파이콘에서 받은 제의가 하나가 나왔다. 넥슨에서 아르바이트 형식으로 웹 개발 하기.

10월, 넥슨

넥슨 인텔리전스랩스 어뷰징탐지TF 팀장님이 파이콘에서 10월~12월에 짧게라도 웹 개발(주로 프론트)을 해보지 않겠냐는 제의를 해 주셨다. 내가 과연 회사에서 개발을 할 수 있는 실력이 될까-하는 약간의 두려움이 있었지만 제의를 받기로 결정했다. 우아한형제들에서 인턴을 진행했지만 실무에 대한 경험은 아니었기 때문에 ‘나는 과연 개발자로 살아갈 수 있는 사람일까?’에 대한 답을 명확히 얻지는 못했기 때문이다. 우아한테크캠프에서 배운 프론트와 기존에 하던 백엔드 지식을 기반으로 개발을 나름 열심히 진행했다. 작지만 조금씩 서비스를 만들어가며 기존에 사용하지 않던 스택(PySpark, Hadoop,EMR와 같은 분산처리 등)도 알게 되고 딥러닝 등을 이용한 데이터 분석을 하는 모습을 바로 옆에서 지켜보니 자연스럽에 데이터분석 분야에도 관심을 갖게 되었다.

그리고 10월에는 메이커페어 이벤트가 있었다. GEEKHUB이라는 이름을 가진 모임에서 ‘공대탈출’이라는 이름으로 부스를 열고 운영도 해보며 친구들과 함께 무언가를 진행했다는 것에 뿌듯함을 느끼기도 했다.

11월, 다시:Django

11월 초에는 장고걸스 서울에서 세미나를 열었다. AskDjango의 진석님이 Azure+크롤링을, Hannal님이 장고Admin을, 허신영님이 Kaggle에 대한 강의를 해주셔서 굉장히 높은 퀄리티의 세미나가 되었다. 그리고 처음으로 100명을 염두에 둔 행사이기도 했다. (보통 장고걸스 서울에서 연 행사는 워크숍을 제외하고는 30명 내 규모로 이뤄졌다.) 이때 장고걸스 서울이라는 커뮤니티가 굉장히 많이 성장했다는 생각이 들었다.

12월, 개발자로의 삶을 고민하다

12월 첫날부터 Vuetiful Korea 세미나가 있었다. Vue를 많이 사용하지는 않지만 그래도 관심은 지속적으로 가지고 있기 때문에 이번에도 참석. 그리고 바로 다음주에 파이썬 연말 세미나가 있었다. ‘헛된꿈’이라는 간단한 투표 웹 사이트가 있었는데 재미로 ‘좋아요’수를 눌리는 코드를 짜보기도 했다.(재미있다)

그러다 굉장히 멋진 제의가 들어왔다. 넥슨에 계속 다니지 않겠냐는 제의로, 내년부터 정직원으로 다니지 않겠냐는 것. 사실 이 제의를 받고 ‘나는 과연 개발자로 살아갈 수 있는 사람일까?’에 대한 답을 조금은 찾은 듯한 느낌이 들었다. 어쩌면, 나는 개발자로 살아가도 괜찮지 않을까 하는 그런 작은 자신감. 코딩 테스트와 면접을 거쳐 내년에도 지금 있는 팀에 계속 다니기로 이야기가 되었다.

정말 좋은 기회들이 다가왔다.

나는 운이 좋다. 정말로 좋은 편이다. 지금까지 한 선택, 멀게는 교대 진학을 선택한 것부터 올해의 중요한 선택들에서 굉장히 좋은 선택지들을 골라왔다고 생각한다. 물론 아직 모르는 것이 한참 많은 늅늅이지만, 그래도 이제 어디 가서 ‘개발해요’라고 말은 하고 다닐 수 있을 것 같다.

올 한해는 정말 1년이 아니라 3년을 보낸 것 같은 기분이다. 수많은 멋진 기회들이 주어지고 그 기회들을 통해 성장했다. 작년보다 성장한 올해, 올 한해는 지금까지 내 삶에서 최고점을 주고싶다. 내년에도 올해와 같이 즐기며 성장할 수 있길 꿈꾼다.

어제보다 나은 오늘의 내가 되기를.

올해의 후기를 마무리짓고 이제 블로그 이야기를 조금 더 해보려 한다.

블로그 이야기

어떻게 블로그를 시작하게 되었나?

기술과 관련된 블로그를 시작한 것은 wordpress.com에서 만든 블로그였다. 2014년 7월부터 사용했고, 이때가 Pogoplug에 ArchLinux/Debian을 설치해가며 커스텀 NAS를 만들고 사용했던 시기였다. 그래서 Nginx가 무엇이고, 웹서버가 무엇이며 FTP가 뭔지, 그리고 웹 상에서 동작하는게 무엇인지 보고 php라는것도 설치해 사용하고 뭔지 모르겠지만 Wordpress도 받아 설치해보고 MySql와 phpMyAdmin등도 사용해보았다. 이때도 “블로그를 쓰면 좋다더라..”라는 막연히 ‘카더라’ 식의 블로그 찬양설을 듣다 그냥 하나씩 해본 것을 정리해보는 식으로 블로그를 작성했다. 윈도에서 RDP를 어떻게 쓰는게 좋나, iptime NAS에 커스텀 리눅스를 어떻게 까나 등등… 이런 개발적인데 비개발적인, 마치 “코딩이랑 무관합니다만,”에 올라올 것 같은 글들을 하나씩 정리해갔다.

왜 Github Pages에 블로그를 옮겼나?

그러다가 2016년 6월 즈음 구글 검색을 하다가 보게 된 것이 바로 아래 사진의 Syntax Highlighter였다.

Syntax Highlighting

블로그에 코드를 적을 때 단순하게 흰색에 고정폭 글씨만 쓰는 것이 아니라 더 다양하고 보기 좋은(개발할 맛이 나는) 코드 하이라이팅을 적용하는 것을 보았던 것!

이걸 보고 바로 뽐뿌가 와버려 내 블로그에 적용하려 했지만…

CSS수정은 유료랍니다 호갱님

CSS 수정은 유료 플랜에서만 사용할 수 있었다. 그래서 어떤 것을 사용해야하나, 티스토리를 사용해야하나 등 고민을 했다.

그러다가 Jekyll + Github Pages의 조합으로 블로그를 만들 수 있다는 사실을 보고, 마크다운 문법만 조금 공부하면 되겠다는 생각으로 jekyllthemes.org라는 Jekyll 테마 모음 사이트에서 적당히 예뻐보이는 사이트 하나를 골라 zip파일을 받은 뒤 기초적인 사이트 정보 수정만 하고 beomi.github.io 레포에 커밋을 하고 올렸더니 블로그가 완성이 되었다!

이때 내가 뭔가를 많이 하지 않았는데도(심지어 Jekyll을 설치도 하지 않았음) 블로그를 수정하고 올릴 수 있어서 Jekyll에 거부감 없이 시작할 수 있었다. 만세! 참고로 예전에 사용한 테마는 hagura라는 테마.

이때 단순히 글 목록만 보이는 테마였지만 그래도 꽤 예뻐 보여서 글쓰는 맛을 즐겼다.

왜 테마를 바꿨나?

지금 사용하고 있는 테마인 trophy라는 테마를 선택하게 된 것은 구글링을 하다 이 테마가 ‘카테고리’를 지원한다는 사실에 선택을 한 것이 컸다. ‘나만의 웹 크롤러 만들기’ 시리즈를 연재하다가 연재본이 하나로 엮여있는 공간을 마련하기 귀찮아 그냥 테마가 카테고리를 지원하면 좋겠다는 생각에 이것을 선택하게 되었고, 이 테마는 지금도 만족도가 매우 높은 상태다.

물론 테마 색깔이나 레이아웃 등등 일부 CSS/SCSS파일을 수정해 사용하고 있고, 이 테마를 쓰면서 가장 큰 문제는 글 쓰기 전에 글의 메인 이미지인 고해상도이미지를 만드는 것이다. (지금은 보통 1920x1080px로 만들고, 100kb내외로 만든다.)

사실 이 작업이 생각보다 귀찮아서 이 블로그를 보는 분들이 무작정 예쁘다고 이 테마를 선택하지는 않기를 바라는 사소한 마음… 만들다 보면 ‘아 내가 메인 이미지를 만드는 센스가 없구나’ 라는걸 깨닫게 됩니다.

올해 블로그 성과는 어땠나?

사실 깃헙에 블로그를 만들고 시작한 것이 2016년 5월이고 본격적으로 방문자 유입이 된 것은 올해라고 볼 수 있다.(구글이 그렇다고 한다.)

우선 올해 사용자부터 보면.. 약 6만명이 이용한 것으로 보인다. Adblock등을 이용해 GA를 차단한 경우는 수집하지 못하기 때문에 GA가 차단된 경우는 제외된 수치니 이것보다는 약간 더 많지 않을까 싶다.

올해사용자

그리고 올해 페이지 뷰는 약 17만 8천뷰정도가 나온 것으로 보인다.(사용자 수가 많을 수록 당연히 페이지 뷰수도 높다.)

올해페이지뷰

방문자 유입은 주로 Google 검색을 통해 들어오는 것으로 보인다. 혹은 어딘가에서 링크로 들어오거나, 페이스북에 글을 올린 날은 페이스북에서 유입이 급격히 늘어나기도 한다.

방문자유입소스

소스별 이탈율과 평균 세션 시간을 살펴보면 구글 검색등으로 들어온 경우가 가장 긴 세션시간(글 읽는 시간)을 유지하고 있다는 것을 볼 수 있다. 소셜공유, 즉 페이스북을 통해 들어온 경우에는 1분내외의 짧은 시간에 글을 훑어보고 바로 나가버리는 형태의 사용 패턴이 나타나는 것을 볼 수 있다.

소스별이탈률

전체적으로 고정 방문자수/페이지 뷰수에 큰 영향을 미치는 것은 검색엔진을 통해서 유입된 것임을 확인할 수 있다.

그렇다면 구글 검색결과에는 얼마나 많이 떴을까?

구글에는 Search Console을 통해 웹 사이트에 구글 검색을 통해 얼마나 유입이 이뤄졌는지 볼 수 있다. 다만 1년치를 보지는 못하고 최근 90일만 조회 가능하기 때문에 최근 90일을 조회해보면 검색 화면에 노출된 수는 약 20만회, 그리고 클릭으로 이어진 경우는 약 2만9천회임을 볼 수 있다. 당연히 노출이 많이 될수록 클릭 수도 올라간다.

Search Console 그래프

그렇다면 사람들이 어떤 키워드로 검색해 들어왔을까?

검색한 단어

역시 크롤링에 관련한 단어가 최상위권을 모두 차지하고 있다. 평균 게재 순위가 높을수록 상위에 노출되고 5위 안으로 들어갈 경우 해당 검색 결과에서 유의미한 유입이 이뤄지는 것을 알 수 있다.

맺으며..

2017년은 본격적으로 블로그를 페이스북에 올리는 등의 방법으로 홍보를 시작했습니다.

사실 블로그를 작성하며 가장 많이 드는 고민이 “세상에 이미 이 자료들이 있고, 검색하면 나오는데 굳이 내가 작성할 필요가 있을까?” 라는 고민입니다.

하지만 저는 블로그를 단순한 코드 조각이 아니라 내가 아는 지식들을 꿰어진 구슬처럼 유의미한 가이드로 사용할 수 있도록 재가공해 제공한다는 측면에서 의미가 있다고 생각하고 지속적으로 글을 작성합니다. 지식이 많은 것도 중요하지만, 누군가에게 내 글이 답답한 부분을 뚫어주는 글이 될 수 있기를 바라며 글을 씁니다. 블로그 글에서 이번 가이드는 이라고 표현하는 이유가 단순히 ‘글’ 이 아니라 ‘가이드’의 역할을 할 수 있기를 바라며 작성하기 때문입니다.

이 글을 읽으신 분들도 블로그를 시작하시고, 커뮤니티를 시작하시면 좋겠습니다.

앞으로도 커뮤니티에서 많은 분들을 뵙고 더 성장할 수 있으면 좋겠습니다.

긴 글 읽어주셔서 감사합니다.

Direct S3 Upload with Lambda

들어가며

이전 글인 AWS Lambda에 Tensorflow/Keras 배포하기에서 Lambda 함수가 실행되는 트리거는 s3버킷에 파일이 생성되는 것이었습니다.

물론 파일을 올릴 수 있는 방법은 여러가지가 있습니다. 아주 단순하게 POST 폼 전송 요청을 받고 boto3등을 이용해 서버에서 s3으로 파일을 전송할 수도 있고, 더 단순하게는 AWS s3 콘솔을 이용해 파일을 올리라고 할 수도 있습니다.

하지만 이런 부분에는 약간의 단점이 함께 있습니다.

첫 번째 방법처럼 파일을 수신해 다시 s3에 올린다면 그 과정에서 서버 한대가 상시로 켜져있어야 하고 전송되는 속도 역시 서버에 의해 제약을 받습니다. 한편 두 번째 방법은 가장 단순하지만 제3자에게서 파일을 받기 위해서 AWS 계정(비록 제한된 권한이 있다 하더라도)을 제공한다는 것 자체가 문제가 됩니다.

따라서 이번 글에서는 사용자의 브라우저에서 바로 s3으로 POST 요청을 전송할 수 있도록 만드는 과정을 다룹니다.

시나리오

사용자는 아주 일반적인 Form 하나를 보게 됩니다. 여기에서 드래그-드롭 혹은 파일 선택을 이용해 일반적으로 파일을 올리게 됩니다. 물론 이 때 올라가는 주소는 AWS S3의 주소가 됩니다.

하지만 이게 바로 이뤄진다면 문제가 생길 소지가 많습니다. 아무나 s3 버킷에 파일을 올린다면 악의적인 파일이 올라올 수도 있고, 기존의 파일을 덮어쓰기하게 될 수도 있기 때문입니다.

따라서 중간에 s3에 post 요청을 할 수 있도록 인증(signing)해주는 서버가 필요합니다. 다만 이 때 서버는 요청별로 응답을 해주면 되기 때문에 AWS Lambda를 이용해 제공할 수 있습니다.

따라서 다음과 같은 형태로 진행이 됩니다.

전체 처리 과정 모식도

S3에 POST 요청을 하기 전 Signing 서버에 업로드하는 파일 정보와 위치등을 보낸 뒤, Lambda에서 해당 POST 요청에 대한 인증 정보가 들어간 header를 반환하면 그 헤더 정보를 담아 실제 S3에 POST 요청을 하는 방식입니다.

만약 Signing하는 과정 없이 업로드가 이뤄진다면 s3 버킷을 누구나 쓸 수 있는 Public Bucket으로 만들어야 하는 위험성이 있습니다. 하지만 이와 같이 제한적 권한을 가진 iam 계정을 생성하고 Signing하는 과정을 거친다면 조금 더 안전하게 사용할 수 있습니다.

Note: 이번 글에서는 API Gateway + Lambda 조합으로 Signing서버를 구성하기 때문에 만약 추가적인 인증 과정을 붙인다면 API Gateway단에서 이뤄지는 것이 좋습니다.

버킷 만들기 & 권한 설정

새로운 버킷은 https://s3.console.aws.amazon.com/s3/home에서 추가할 수 있습니다.

새로운 버킷을 하나 만들어주세요. 이번 글에서는 s3-signature-dev-py3라는 이름으로 만들어 진행해 보았습니다.

버킷에 GET/POST 요청을 하기 위해 CORS 설정을 해줘야 합니다.(localhost:8000와 같은 제 3의 URL에서 s3 버킷의 주소로 POST 요청을 날리기 위해서는 CORS 설정이 필수입니다.)

아래 스크린샷과 같이 CORS 설정을 진행해 주세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<CORSConfiguration>
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<MaxAgeSeconds>3000</MaxAgeSeconds>
<AllowedHeader>Authorization</AllowedHeader>
</CORSRule>
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedMethod>POST</AllowedMethod>
<MaxAgeSeconds>3000</MaxAgeSeconds>
<AllowedHeader>Authorization</AllowedHeader>
</CORSRule>
</CORSConfiguration>

이처럼 구성해주면 모든 도메인(*)에서 요청한 GET/POST 요청을 정상적인 크로스-도메인 요청으로 받아들입니다.

Note: 만약 여러분이 여러분의 프론트 서비스에서만 이 요청을 허용하려면 AllowedOrigin부분을 여러분이 사용하는 프론트 서비스의 도메인으로 변경해주세요.

이제 s3을 사용할 준비는 마쳤습니다.

버킷 액세스 iam 계정 만들기

새로운 iam 계정은 https://console.aws.amazon.com/iam/home?region=ap-northeast-2#/users$new?step=details에서 만들 수 있습니다.

다음으로는 앞서 만들어준 버킷에 액세스를 할 수 있는 iam 계정을 만들어야 합니다. 이번에 사용할 유저 이름도 s3-signature-dev-py3로 만들어 줍시다. 아래 스크린샷처럼 Programmatic access를 위한 사용자를 만들어 줍시다.

우리는 버킷내 uploads폴더에 파일을 ‘업로드만 가능’한, PutObjectPutObjectAcl이라는 아주 제한적인 권한을 가진 계정을 만들어 줄 것이기 때문에 다음과 같이 Create Policy를 눌러 json 기반으로 계정 정책을 새로 생성해 줍시다.

새 창이 뜨면 아래와 같이 arn:aws:s3:::s3-signature-dev-py3/uploads/* 리소스에 대해 PutObjectPutObjectAcl에 대해 Allow를 해 주는 json을 입력하고 저장해줍시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "s3UploadsGrant",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:PutObjectAcl"
],
"Resource": [
"arn:aws:s3:::s3-signature-dev-py3/uploads/*"
]
}
]
}

이제 policy의 name을 입력하고 저장해줍시다.

저장해주고 창을 끈 뒤 이전 페이지로 돌아와 Refresh를 누르면 다음과 같이 앞서 만들어준 Policy가 나오는 것을 볼 수 있습니다. 체크박스에 체크를 누른 뒤 다음을 눌러주세요.

이제 마지막 확인을 눌러주세요.

확인을 누르면 다음과 같이 Access key ID와 Secret access key가 나옵니다. 이 키는 지금만 볼 수 있으니 csv로 받아두거나 따로 기록해 두세요. 그리고 글 아래부분에서 이 키를 사용하게 됩니다.

Signing Lambda 함수 만들기

이제 POST 요청을 받아 인증을 해줄 AWS Lambda함수를 만들어 줍시다.

아래 코드를 받아 AWS Lambda 새 함수를 만들어주세요. (역시 s3-signature-dev-py3라는 이름으로 만들었습니다.)

Github Gist: index.py

이번 함수는 python3의 내장함수만을 이용하기 때문에 따로 zip으로 만들 필요없이 AWS 콘솔 상에서 인라인 코드 편집으로 함수를 생성하는 것이 가능합니다.

아래 스크린샷처럼 lambda_function.py 파일을 위의 gist 코드로 덮어씌워주세요. 그리고 Handler부분을 lambda_function.index로 바꿔 index함수를 실행하도록 만들어 주세요. 그리고 저장을 눌러야 입력한 코드가 저장됩니다.

코드를 조금 뜯어보면 아래와 같이 ACCESS_KEYSECRET_KEY를 저장하는 부분이 있습니다.

1
2
ACCESS_KEY = os.environ.get('ACCESS_KEY')
SECRET_KEY = os.environ.get('SECRET_KEY')

AWS Lambda에서 함수를 실행할 때 아래 환경변수를 가져와 s3 버킷에 액세스하기 때문에 위 두개 값을 아래 스크린샷처럼 채워줍시다. 입력을 마치고 저장을 눌러주면 환경변수가 저장됩니다.

Note: 각 키의 값은 앞서 iam 계정 생성시 만든 값을 넣어주세요!

API Gateway 연결하기

API Gateway는 https://ap-northeast-2.console.aws.amazon.com/apigateway/home?region=ap-northeast-2#/apis/create에서 만들 수 있습니다.

이렇게 만들어 준 AWS Lambda 함수는 각각은 아직 외부에서 결과값을 받아올 수 있는 형태가 아닙니다. 람다 함수를 트리거해주고 결과값을 받아오기 위해서는 AWS API Gateway를 통해 웹 URL로 오는 요청에 따라서 람다 함수가 실행되도록 구성해야 합니다. 또한, CORS역시 활성화 해줘야 합니다.

API Gateway 만들고 Lambda와 연결하기

Resources에서는 Api URL의 하위 URL와 root URL에 대해 각각 메소드들을 정의해 사용할 수 있습니다. 우리는 요청을 받을 때 POST 방식으로 요청을 받아 처리해줄 것이랍니다.

여기서 새 메소드 중 POST를 선택해 줍시다.

메소드에 Lambda 함수를 연결해 주기 위해 다음과 같이 Lambda Function을 선택하고 Proxy는 체크 해제한 뒤, Regionap-northeast-2(서울)리전을 선택하고, 아까 만들어준 함수 이름을 입력한 뒤 Save를 눌러줍시다.

Tip: Lambda Proxy를 활성화 시킬 경우 HTTP 요청이 그대로 들어오는 대신, AWS에서 제공하는 event 객체가 대신 Lambda함수로 넘어가게 됩니다. 우리는 HTTP 요청을 받아 Signing해주는 과정에서 Header와 Body를 유지해야하기 때문에 Proxy를 사용하지 않습니다.

Save를 누르면 다음과 같이 API Gateway에 Lambda함수를 실행할 권한을 연결할지 묻는 창이 뜹니다. 가볍게 OK를 눌러줍시다.

연결이 완료되면 API Gateway가 아래 사진처럼 Lambda 함수와 연결 된 것을 볼 수 있습니다.

CORS 활성화하기

조금만 더 설정을 해주면 API Gateway를 배포할 수 있게 됩니다. 지금 해줘야 하는 작업이 바로 CORS 설정인데요, 우리가 나중에 만들 프론트 페이지의 URL와 s3의 URL이 다르기 때문에 브라우저에서는 보안의 이유로 origin이 다른 리소스들에 대해 접근을 제한합니다. 따라서 CORS를 활성화 해 타 URL(프론트 URL)에서도 요청을 할 수 있도록 설정해줘야 합니다.

Actions에서 Enable CORS를 눌러주세요.

다음과 같이 Access-Control-Allow-Headers의 값을 '*'로 설정한 뒤 Enable CORS 버튼을 눌러 저장해주세요.

다시한번 Confirm을 눌러주시면…

CORS가 활성화되고 Options 메소드가 새로 생기게 됩니다.

이제 API Gateway를 ‘배포’해야 실제로 사용할 수 있습니다.

API Gateway 배포하기

API Gateway의 설정을 모두 마치고나서는 배포를 진행해야 합니다. 아래와 같이 Actions에서 Deploy API를 눌러주세요.

API Gateway는 Deployment Stage를 필요로 합니다. Stage namelive로 설정하고 Deploy를 눌러줍시다.

Tip: Deployment Stage는 API Gateway의 URL 뒤 /stagename의 형식으로 추가 URL을 지정해줍니다. 이를 통해 API를 개발 버전과 실 서비스 버전을 분리해 제공할 수 있습니다.

배포가 완료되면 아래와 같이 API Gateway를 사용할 수 있는 URL을 받을 수 있습니다.

이번에는 https://9n2qae2nak.execute-api.ap-northeast-2.amazonaws.com/live가 Signing Lambda 함수를 실행할 수 있는 API Gateway URL이 됩니다.

파일 업로드 프론트 만들기

이제 파일을 업로드할 form이 있는 Static 웹 사이트를 만들어봅시다.

이번 글에서는 이미 만들어진 파일 업로더인 VanillaJS용 Fine Uploader를 이용해 최소한의 업로드만 구현합니다.

React용 React Fine Uploader와 Vue용 Vue Fine Uploader도 있습니다.

https://github.com/Beomi/s3-direct-uploader-demo 깃헙 레포를 clone받아 app.js를 열어 아래 목록을 수정해주세요.

  • request/endpoint: 여러분이 사용할 s3 버킷 이름 + .s3.amazonaws.com
  • request/accessKey: 앞서 만든 iam 계정의 Access Key
  • objectProperties/region: s3 버킷의 리전
  • objectProperties/key/prefixPath: s3 버킷 내 올릴 폴더 이름(putObject 권한을 부여한 폴더)
  • signature/endpoint: 앞서 만든 AWS Lambda의 API Gateway URL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var uploader = new qq.s3.FineUploader({
debug: false, // defaults to false
element: document.getElementById('fine-uploader'),
request: {
// S3 Bucket URL
endpoint: 'https://s3-signature-dev-py3.s3.amazonaws.com',
// iam ACCESS KEY
accessKey: 'AKIAIHUAMKBO27EZQ6RA'
},
objectProperties: {
region: 'ap-northeast-2',
key(fileId) {
var prefixPath = 'uploads'
var filename = this.getName(fileId)
return prefixPath + '/' + filename
}
},
signature: {
// version
version: 4,
// AWS API Gate URL
endpoint: 'https://9n2qae2nak.execute-api.ap-northeast-2.amazonaws.com/live'
},
retry: {
enableAuto: true // defaults to false
}
});

그리고나서 index.html 파일을 열어보시면 아래 사진과 같은 업로더가 나오게 됩니다.

DEMO: https://beomi.github.io/s3-direct-uploader-demo/

맺으며

이제 여러분은 Serverless하게 파일을 s3에 업로드 할 수 있게 됩니다. 권한 관리와 같은 부분은 API Gateway에 접근 가능한 부분에 제약을 걸어 업로드에 제한을 걸어 줄 수도 있습니다.

ec2등을 사용하지 않고도 간단한 signing만 갖춰 s3에 파일을 안전하게 업로드 하는 방식으로 전체 프로세스를 조금씩 Serverless한 구조로 바꾸는 예시였습니다.

Reference

AWS Lambda에 Tensorflow/Keras 배포하기

Update @ 20190306: amazonlinux:latest 버전이 2버전이 latest로 변경됨에 따라 아래 코드를 amazonlinux:1로 변경

이번 글은 macOS을 기반으로 작성되었지만, docker 명령어를 사용할 수 있는 모든 플랫폼(윈도우/맥/리눅스)에서 따라올 수 있습니다.

들어가며

여러분이 이미지를 받아 텐서플로로 분류를 해 준 뒤 결과를 반환해주는 작업을 하는 모델을 만들었다고 가정해봅시다. 이때 가장 빠르고 간단하게 결과를 내는(Inference/추론을 하는)방법은 Cuda가속을 할 수 있는 고급 시스템 위에서 텐서플로 모델을 이용해 결과를 반환하도록 서비스를 구현할 수 있습니다. 혹은 조금 느리더라도 CPU를 사용하는 EC2와 같은 VM위에서 텐서플로 코드를 동작하게 만들 수도 있습니다.

하지만 만약 여러분이 처리해야하는 이미지가 몇장이 아니라 수십, 수백장을 넘어 수천장을 처리해야한다면 어떻게 될까요?

한 EC2 위에서 서비스를 제공하는 상황에서는 이런 경우라면 for문처럼 이미지 하나하나를 돌며 추론을 한다면 전체 이미지의 결과를 내려면 한참 시간이 걸리게 됩니다. 따라서 일종의 병렬 처리를 생각해 보아야 합니다.

즉, 이미지를 순서대로 하나씩 추론하는 대신, 추론하는 코어 함수만을 빼고 결과를 반환하도록 만들어 주면 됩니다.

물론 병렬 처리를 위해서 여러가지 방법들이 있습니다. 여러개의 GPU를 사용하고 있다면 각 GPU별로 작업을 진행하도록 할 수도 있고, 혹은 EC2를 여러개 띄워서 작업을 분산해 진행할 수도 있습니다. 하지만 이 방법보다 조금 더(혹은 상당히 많이) 빠르게 결과를 얻어낼 수 있는 방법이 있습니다.

바로 AWS Lambda를 이용하는 방법입니다.

AWS Lambda는 현재 각 실행별 최대 3GB메모리와 5분의 실행시간 내에서 원하는 코드를 실행해 한 리전에서 동시에 최대 1000개까지 병렬로 실행할 수 있습니다.

즉, 우리가 1만개의 이미지를 처리해야 한다면 한 리전에서만 1000개를 동시에 진행해 10개를 처리하는 시간 내 모든 작업을 마칠 수 있다는 것이죠. 그리고 작업이 끝나고 서버가 자동으로 끝나기 때문에 동작하지 않는 시간에도 돈을 내는 EC2와는 가격차이가 많이 나게됩니다.

이번 가이드는 다음과 같은 시나리오로 작성했습니다.

  • 시나리오
    1. s3의 어떤 버킷의 특정한 폴더에 ‘이미지 파일’을 올린다.
    2. 이미지 파일이 ‘생성’될 때 AWS Lambda 함수가 실행이 트리거된다.
    3. (Lambda) 모델을 s3에서 다운받아 Tensorflow로 읽는다.
    4. (Lambda) 함수가 트리거 될때 발생한 event 객체를 받아 s3에 업로드된 파일의 정보(버킷, 버킷내 파일의 경로)를 가져온다.
    5. (Lambda) s3에 업로드된 이미지 파일을 boto3을 통해 가져와 Tensorflow로 Inference를 진행한다.
    6. (Lambda) Inference가 끝난 결과물을 s3에 저장하거나 결과값을 DynamoDB에 저장하거나 혹은 API Gateway를 통해 json으로 반환한다.

위와같이 진행할 경우 s3에 파일 업로드를 1개를 하든, 1000개를 하든 업로드 자체에 필요한 시간을 제외하면 실행 시간 자체는 동일하게 유지할 수 있습니다.

(마치 시간복잡도가 O(1)인 척 할 수 있는 것이죠!)

환경 준비하기

오늘 사용한 예제는 Github tf-keras-on-lambda Repo에서 확인할 수 있습니다.

AWS Lambda는 아마존에서 RedHat계열의 OS를 새로 만든 Amazon Linux위에서 동작합니다. 그렇기 때문에 만약 우리가 C의존적인 라이브러리를 사용해야한다면 우선 Amazon Linux에 맞게 pip로 설치를 해줘야 합니다. 그리고 간혹 빌드가 필요한 패키지의 경우 사용하고자 하는 OS에 맞춰 빌드작업 역시 진행해야 합니다. Tensorflow 역시 C의존적인 패키지이기 때문에 OS에 맞는 버전을 받아줘야 합니다. 우리가 사용하는 OS는 macOS혹은 windows이기 때문에 docker를 통해 Amazon Linux를 받아 그 안에서 빌드를 진행합니다.

도커를 사용하고있다면 그대로 진행해주시면 되고, 도커를 설치하지 않으셨다면 우선 도커를 먼저 설치해주세요.

도커는 Docker Community Edition Download Page에서 받으실 수 있습니다.

도커를 받아주세요

여러분이 다음부분을 진행하기 전, docker라는 명령어를 터미널 혹은 cmd상에서 입력시 도커가 실행되어야 합니다. 도커가 실행된다면, 우선은 실행할 준비를 마친 것이랍니다.

물론 여러분 각자의 모델파일이 있어야 합니다. 이번 가이드에서는 Keras 예시 중 Pre-trained squeezenet을 이용한 Image Classification(imagenet)으로 진행합니다.

결과 저장용 DynamoDB 만들기(Optional)

우리가 predict를 진행하고 나서 나온 결과물을 어딘가에 저장해둬야 합니다. AWS에는 DynamoDB라는 간단한 NoSQL DB가 있으니, 이걸 이용해 결과물을 저장해 봅시다.

우선 DynamoDB 메뉴에서 아래와 같이 새 테이블 하나를 만들어 줍시다.

기본키 정도만 문자열 필드 filename을 만들고 테이블을 생성해 줍시다.

이제 기본키만 지키면 나머지 필드는 자유롭게 올릴 수 있습니다. (물론 기본키와 정렬키만 인덱스가 걸리기 때문에, 빠른 속도가 필요하다면 인덱스를 건 뒤 데이터를 추가해줘야 합니다.)

squeezenet ImageNet 모델 s3에 올리기

이번 글에서는 squeezenet Imagenet 모델을 이용해 predict를 진행합니다. squeezenet_weights_tf_dim_ordering_tf_kernels.h5파일이 필요한데, AWS Lambda에서 비용이 들지 않으며 빠른 속도로 모델을 받아오기 위해서는 같은 리전의 s3에 파일을 올려둬야 합니다.

keras-blog 버킷에 올린 모델 파일

이번 글에서는 keras-blog라는 s3 버킷의 squeezenet 폴더에 파일을 올려두었습니다.

각 파일은 아래 링크에서 받을 수 있습니다. 아래 두 파일을 받아 s3 버킷에 올려주세요.

squeezenet.py 만들기

Keras의 squeezenet은 squeezenet.py을 참조합니다. 하지만 이 파일에는 Pre-Trained Model의 경로를 바꿔주기 때문에 이 부분을 약간 수정한 커스텀 squeezenet.py를 만들어주었습니다.

이 파일을 다운받아 여러분의 index.py 옆에 놓아두세요.

도커 + Amazon Linux로 빌드 준비하기

이제 docker라는 명령어로 도커를 사용해봅시다.

우선 여러분이 작업할 폴더 하나를 만들어 주세요. 저는 지금 tf_on_lambda라는 폴더에서 진행하고 있습니다. 이 폴더 안에는 Lambda에서 실행할 python파일이 들어가게 되고, 도커와 이 폴더를 이어줄 것이기 때문에 새 폴더 하나를 만들어서 진행하시는 것을 추천합니다.

폴더를 만들고 들어가셨다면 다음 명령어를 입력해 AmazonLinux 이미지를 받아 도커로 띄워주세요.

1
docker run -v $(pwd):/outputs --name lambdapack -d amazonlinux:1 tail -f /dev/null

도커 컨테이너의 이름을 lambdapack으로 지정하고 현재 폴더($(pwd))를 도커의 /outputs폴더로 연결해줍니다.

도커가 실행된 모습

성공적으로 받아졌다면 다음과 같이 임의의 난수 id가 생깁니다. 그리고 docker ps라는 명령어로 현재 실행중인 컨테이너들을 확인해보면 다음과 같이 lambdapack라는 이름을 가진 컨테이너가 생성된 것을 볼 수 있습니다.

도커 ps

람다가 실행할 python 파일 작성하기

그러면 이제 람다가 실제로 실행할 python 파일을 만들어줍시다. 이번 가이드에서는 이 파일의 이름을 index.py라고 지어보았습니다.

index.py 파일은 사실 어떤 이름으로 해도 상관없습니다만, AWS에 람다 함수를 만들 때 handler함수 위치 지정을 파일이름.함수이름, 즉 index.handler와 같이 적어줄 것이기 때문에 대표적인 이름을 가진 파일로 만들어 주시면 됩니다.

index.py안에는 다음과 같은 내용으로 작성해 봅시다. 중요한 부분은 handler함수입니다.

전체 코드를 바로 이용하시려면 index.py on GIST을 이용하세요.

제일 먼저 해줘야 하는 부분은 우리가 사용할 라이브러리를 import하는 것이죠.

1
2
3
4
5
6
7
8
9
10
11
12
import boto3 # AWS S3 접근용
from tensorflow.python import keras # Keras!
from tensorflow.python.keras.preprocessing import image
from tensorflow.python.keras.applications.resnet50 import preprocess_input, decode_predictions
import numpy as np
import io # File 객체를 메모리상에서만 이용하도록
import os # os.path / os.environ
from PIL import Image # Image 객체
import urllib.request # 파일받기

# (.h5경로변경추가, 레포의 squeezenet.py를 확인하세요.)
from squeezenet import Squeezenet # 커스텀한 squeezenet

이 중 boto3 라이브러리는 AWS Lambda의 python3내에 이미 설치되어있기 때문에 특정 버전boto3을 이용하시려는게 아니라면 도커 컨테이너에 설치하지 않아도 됩니다. (즉, Lambda에 올릴 패키지 zip파일에 boto3이 들어있지 않아도 괜찮습니다.)

import를 끝냈으니 코드를 작성해 봅시다. 우선 S3에서 파일을 다운로드/업로드하는 함수를 만들어줍시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ACCESS_KEY = os.environ.get('ACCESS_KEY')
SECRET_KEY = os.environ.get('SECRET_KEY')

def downloadFromS3(strBucket, s3_path, local_path):
s3_client = boto3.client(
's3',
aws_access_key_id=ACCESS_KEY,
aws_secret_access_key=SECRET_KEY,
)
s3_client.download_file(strBucket, s3_path, local_path)

def uploadToS3(bucket, s3_path, local_path):
s3_client = boto3.client(
's3',
aws_access_key_id=ACCESS_KEY,
aws_secret_access_key=SECRET_KEY,
)
s3_client.upload_file(local_path, bucket, s3_path)

AWS 콘솔에서 람다 함수별로 환경변수를 설정해줄 수 있기 때문에, 위과 같이 os.environ을 통해 설정한 환경변수 값을 가져옵시다.

물론 여기에 설정한 Access Key와 Secret Key의 iam 유저는 당연히 해당 S3 버킷에 R/W권한이 있어야 합니다.

이 두가지 함수를 통해 S3에서 모델을 가져오고, 모델로 추론한 결과물을 S3에 넣어줄 수 있습니다.

Note: s3 버킷과 람다 함수는 같은 Region에 있어야 데이터 전송 비용이 발생하지 않습니다.

이제 가장 중요한 부분인 handler함수를 살펴봅시다.

handler함수는 기본적으로 eventcontext를 인자로 전달받습니다. 이때 우리가 사용하는 인자는 event인자입니다.

우리의 사용 시나리오 중 ‘S3에 이미지 파일을 올린다’, 이 부분이 바로 event의 내용이 됩니다.

S3 Event의 내용

AWS에서 Lambda 실행이 트리거 될 때 전달되는 event는 파이썬의 딕셔너리 형태로 전달됩니다.

만약 여러분이 s3파일이 추가되는 이벤트를 Lambda에 연결해두셨다면 파일이 업로드 될 때 마다 아래와 같은 딕셔너리가 전달됩니다.

아래 예시는 csv_icon.pngkeras-blog라는 버킷내 wowwow 폴더에 올렸을때 발생한 event 객체입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# event 객체
{
'Records': [
{
'eventVersion': '2.0',
'eventSource': 'aws:s3',
'awsRegion': 'ap-northeast-2', # 버킷 리전
'eventTime': '2017-12-13T03:28:13.528Z', # 업로드 완료 시각
'eventName': 'ObjectCreated:Put',
'userIdentity': {'principalId': 'AFK2RA1O3ML1F'},
'requestParameters': {'sourceIPAddress': '123.24.137.5'},
'responseElements': {
'x-amz-request-id': '1214K424C14C384D',
'x-amz-id-2': 'BOTBfAoB/gKBbn412ITN4t2psTW499iMRKZDK/CQTsjrkeSSzSdsDUMGabcdnvHeYNtbTDHoHKs='
},
's3': {
's3SchemaVersion': '1.0', 'configurationId': 'b249eeda-3d48-4319-a7e2-853f964c1a25',
'bucket': {
'name': 'keras-blog', # 버킷 이름
'ownerIdentity': {
'principalId': 'AFK2RA1O3ML1F'
},
'arn': 'arn:aws:s3:::keras-blog'
},
'object': {
'key': 'wowwow/csv_icon.png', # 버킷 내 파일의 절대경로
'size': 11733, # 파일 크기
'eTag': 'f2d12d123aebda1cc1fk17479207e838',
'sequencer': '125B119E4D7B2A0A48'
}
}
}
]
}

여기서 봐야 하는 것은 bucketnameobjectkey입니다. 각각 업로드된 버킷의 이름과 버킷 내 파일이 업로드된 경로를 알려주기 때문에 S3 내 업로드된 파일의 절대경로를 알 수 있습니다.

따라서 handler함수 내 다음과 같이 버킷이름과 버킷 내 파일의 경로를 얻을 수 있습니다.

1
2
3
4
5
6
# 윗부분 생략
def handler(event, context):
bucket_name = event['Records'][0]['s3']['bucket']['name']
# bucket_name은 'keras-blog' 가 됩니다.
file_path = event['Records'][0]['s3']['object']['key']
# file_path는 'wowwow/csv_icon.png'가 됩니다.

이를 통해 파일 업로드 이벤트 발생시마다 어떤 파일을 처리해야할 지 알 수 있습니다.

s3에서 이미지 파일 받아오기

이제 어떤 파일을 처리해야 할지 알 수 있게 되었으니 downloadFromS3 함수를 통해 실제로 파일을 가져와봅시다.

1
2
3
4
5
6
# 윗부분 생략
def handler(event, context):
bucket_name = event['Records'][0]['s3']['bucket']['name']
file_path = event['Records'][0]['s3']['object']['key']
file_name = file_path.split('/')[-1] # csv_icon.png
downloadFromS3(bucket_name, file_path, '/tmp/'+file_name)

위 코드를 보면 s3에 올라간 파일을 /tmp안에 받는 것을 볼 수 있습니다. AWS Lambda에서는 ‘쓰기’ 권한을 가진 것은 오직 /tmp폴더뿐이기 때문에 우리가 파일을 받아 사용하려면 /tmp폴더 내에 다운받아야 합니다. (혹은 온메모리에 File 객체로 들고있는 방법도 있습니다.)

Prediction Model 다운받기 (Optional)

만약 여러분이 그냥 사용한다면 Github에서 파일을 다운받게 됩니다. 이때 속도가 굉장히 느려 lambda비용이 많이 발생하기 때문에 여러분의 s3 버킷에 실제 파일을 올리고 s3에서 파일을 받아 사용하시는 것을 추천합니다.

우리는 위에서 squeezenet모델을 사용했는데, 이때 모델 가중치를 담은 .h5파일을 먼저 받아야 합니다. handler 함수 내 다음 두 파일을 더 받아줍시다.

1
2
3
4
5
6
7
8
9
10
def handler(event, context):
bucket_name = event['Records'][0]['s3']['bucket']['name']
file_path = event['Records'][0]['s3']['object']['key']
file_name = file_path.split('/')[-1]
downloadFromS3(bucket_name, file_path, '/tmp/'+file_name)
downloadFromS3(
'keras-blog',
'squeezenet/squeezenet_weights_tf_dim_ordering_tf_kernels.h5',
'/tmp/squeezenet_weights_tf_dim_ordering_tf_kernels.h5'
) # weights용 h5를 s3에서 받아오기

squeezenet 모델로 Predict 하기

이제 s3에 올라간 이미지 파일을 Lambda내 /tmp폴더에 받았으니 Predict를 진행해봅시다. predict라는 함수를 아래와 같이 이미지 경로를 받아 결과를 반환하도록 만들어 줍시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
# index.py 파일, handler함수보다 앞에 
def predict(img_local_path):
model = Squeezenet(weights='imagenet')
img = image.load_img(img_local_path, target_size=(224, 224))
x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)
preds = model.predict(x)
res = decode_predictions(preds)
return res

def handler(event, context):
# 내용 생략 ...

predict함수는 squeezenet의 Pre-trained 모델을 이용해 이미지 예측을 진행합니다.

그러면 실제 handler함수에서 predict함수를 실행하도록 수정해줍시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def predict(img_local_path):
# 내용 생략 ...

def handler(event, context):
bucket_name = event['Records'][0]['s3']['bucket']['name']
file_path = event['Records'][0]['s3']['object']['key']
file_name = file_path.split('/')[-1]
downloadFromS3(bucket_name, file_path, '/tmp/'+file_name)
downloadFromS3(
'keras-blog',
'squeezenet/squeezenet_weights_tf_dim_ordering_tf_kernels.h5',
'/tmp/squeezenet_weights_tf_dim_ordering_tf_kernels.h5'
)
result = predict('/tmp/'+file_name) # 파일 경로 전달
return result

완성이네요!

DynamoDB에 결과 올리기(Optional)

위 predict의 결과는 단순히 결과가 생기기만 하고 결과를 저장하거나 알려주는 부분은 없습니다. 이번 글에서는 간단한 예시로 AWS DynamoDB에 쌓아보는 부분을 추가해보겠습니다.

predict함수를 통해 생성된 result는 다음과 같은 모습이 됩니다.

1
2
# result[0]의 내용, tuples in list
[('n02099712', 'Labrador_retriever', 0.68165195), ('n02099601', 'golden_retriever', 0.18365686), ('n02104029', 'kuvasz', 0.12076716), ('n02111500', 'Great_Pyrenees', 0.0042763283), ('n04409515', 'tennis_ball', 0.002152696)]

따라서 map을 이용해 다음과 같이 바꿔줄 수 있습니다.

1
2
_tmp_dic = {x[1]:{'N':str(x[2])} for x in result[0]}
dic_for_dynamodb = {'M': _tmp_dic}

그러면 dic_for_dynamodb는 아래와 같은 형태로 나오게 됩니다.

NOTE: 숫자는 floatint가 아닌 str로 바꾸어 전달해야 오류가 나지 않습니다. DynamoDB의 제약입니다.

1
2
3
4
5
6
7
8
9
{
'M':{
'Labrador_retriever': {'N': '0.68165195'},
'golden_retriever': {'N': '0.18365686'},
'kuvasz': {'N': '0.12076716'},
'Great_Pyrenees': {'N': '0.0042763283'},
'tennis_ball': {'N': '0.002152696'}
}
}

데이터를 넣기 위해서는 dict타입으로 만든 객체를 put할수 있는데, 이때 각각의 키에 대해 타입을 알려줘야 합니다. M은 이 객체가 dict타입이라는 것을, N은 이 타입이 숫자라는 것을, S는 문자열이라는 것을 의미합니다.

더 상세한 내용은 DynamoDB에 데이터 넣기를 참고하세요.

이제 이 방식을 이용해 result를 반환하기 전 DynamoDB에 데이터를 넣어줄 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def handler(event, context):
# 중간 생략 ...
result = predict('/tmp/'+file_name)
_tmp_dic = {x[1]:{'N':str(x[2])} for x in result[0]}
dic_for_dynamodb = {'M': _tmp_dic}
dynamo_client = boto3.client(
'dynamodb',
aws_access_key_id=ACCESS_KEY,
aws_secret_access_key=SECRET_KEY,
region_name='ap-northeast-2' # DynamoDB는 리전 이름이 필요합니다.
)
dynamo_client.put_item(
TableName='keras-blog-result', # DynamoDB의 Table이름
Item={
'filename': {
'S': file_name,
},
'predicts': dic_for_dynamodb,
}
)
return result

도커로 Lambda에 올릴 pack.zip파일 만들기

AWS Lambda에 함수를 올리려면 AmazonLinux에 맞게 pip패키지들을 index.py 옆에 같이 설치해준 뒤 압축파일(.zip)으로 묶어 업로드해야 합니다.

이때 약간의 제약이 있는데, AWS콘솔에서 Lambda로 바로 업로드를 하려면 .zip파일이 50MB보다 작아야 하고, S3에 .zip파일을 올린 뒤 Lambda에서 가져와 사용하려면 압축을 푼 크기가 250MB보다 작아야 합니다.

문제는 Tensorflow나 기타 라이브러리를 모두 설치하면 용량이 무지막지하게 커진다는 점인데요, 이를 해결하기 위해 사용하지 않는 부분을 strip하는 방법이 들어갑니다.

앞서만든 AmazonLinux 기반 컨테이너인 lambdapack를 이용해 패키지들을 설치하고 하나의 압축파일로 만들어줍시다.

아래 내용의 buildPack.sh파일을 index.py 옆에 만들어 주세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# buildPack.sh
dev_install () {
yum -y update
yum -y upgrade
yum install -y \
wget \
gcc \
gcc-c++ \
python36-devel \
python36-virtualenv \
python36-pip \
findutils \
zlib-devel \
zip
}

pip_rasterio () {
cd /home/
rm -rf env
python3 -m virtualenv env --python=python3
source env/bin/activate
text="
[global]
index-url=http://ftp.daumkakao.com/pypi/simple
trusted-host=ftp.daumkakao.com
"
echo "$text" > $VIRTUAL_ENV/pip.conf
echo "UNDER: pip.conf ==="
cat $VIRTUAL_ENV/pip.conf
pip install -U pip wheel
pip install --use-wheel "h5py==2.6.0"
pip install "pillow==4.0.0"
pip install protobuf html5lib bleach --no-deps
pip install --use-wheel tensorflow --no-deps
deactivate
}


gather_pack () {
# packing
cd /home/
source env/bin/activate

rm -rf lambdapack
mkdir lambdapack
cd lambdapack

cp -R /home/env/lib/python3.6/site-packages/* .
cp -R /home/env/lib64/python3.6/site-packages/* .
cp /outputs/squeezenet.py /home/lambdapack/squeezenet.py
cp /outputs/index.py /home/lambdapack/index.py
echo "original size $(du -sh /home/lambdapack | cut -f1)"

# cleaning libs
rm -rf external
find . -type d -name "tests" -exec rm -rf {} +

# cleaning
find -name "*.so" | xargs strip
find -name "*.so.*" | xargs strip
rm -r pip
rm -r pip-*
rm -r wheel
rm -r wheel-*
rm easy_install.py
find . -name \*.pyc -delete
echo "stripped size $(du -sh /home/lambdapack | cut -f1)"

# compressing
zip -FS -r1 /outputs/pack.zip * > /dev/null
echo "compressed size $(du -sh /outputs/pack.zip | cut -f1)"
}

main () {
dev_install
pip_rasterio
gather_pack
}

main

dev_install 함수에서는 운영체제에 Python3/pip3등을 설치해 주고, pip_rasterio 함수에서는 가상환경에 들어가 tensorflow등 pip로 패키지들을 설치해 주고, gather_pack 함수에서는 가상환경에 설치된 패키지들과 index.py파일을 한 폴더에 모은 뒤 pack.zip파일로 압축해줍니다.

중간에 pip.conf를 바꾸는 부분을 통해 느린 pip global cdn대신 kakao의 pip 미러서버로 좀 더 패키지들을 빠르게 받을 수 있습니다. 이 방법은 여러분의 pip에도 바로 적용할 수 있습니다.

이 sh 파일을 도커 내에서 실행하기 위해서 다음 명령어를 사용해 실행해주세요.

1
docker exec -it lambdapack /bin/bash /outputs/buildPack.sh

이 명령어는 lambdapack이라는 컨테이너에서 buildPack.sh파일을 실행하게 됩니다.

실행하고 나면 약 50MB안팎의 pack.zip파일 하나가 생긴것을 볼 수 있습니다.

하지만 앞서 언급한 것처럼, AWS 콘솔에서 ‘ZIP 파일 올리기’로 한번에 올릴수 있는 압축파일의 용량은 50MB로 제한됩니다. 따라서 이 zip 파일을 s3에 올린 뒤 zip파일의 HTTP주소를 넣어줘야 합니다.

zip파일 s3에 올리고 AWS Lambda 트리거 만들기

이제 AWS 콘솔을 볼 때가 되었습니다.

지금까지 작업한 것은 Lambda에 올릴 패키지/코드를 압축한 파일인데요, 이 부분을 약간 수정해 이제 실제로 AWS Lambda의 이벤트를 통해 실행해 봅시다.

s3에 파일 업로드하기

S3에 파일을 올린 뒤 파일의 HTTPS주소를 복사해주세요.

s3의 pack.zip HTTP주소

여기에서는 https://s3.ap-northeast-2.amazonaws.com/keras-blog/pack.zip가 주소가 됩니다.

이제 진짜로 AWS Lambda 함수를 만들어봅시다.

Lambda 콘솔 함수만들기에 들어가 “새로 작성”을 선택 후 아래와 같이 내용을 채운 뒤 함수 생성을 눌러주세요.

함수가 생성되고 나면 화면 아래쪽 ‘함수 코드’에서 다음과 같이 AWS s3에서 업로드를 선택하고 런타임을 Python3.6으로 잡은 뒤 핸들러를 index.handler로 바꾸고 S3링크를 넣어준 뒤 ‘저장’을 눌러주세요.

그 뒤, ‘기본 설정’에서 메모리를 1500MB 이상으로, 그리고 제한시간은 30초 이상으로 잡아주세요. 저는 테스트를 위해 3000MB/5분으로 잡아주었습니다.

이제 s3에서 파일이 추가될때 자동으로 실행되도록 만들어 주기 위해 다음과 같이 ‘구성’에서 s3를 선택해주세요.

트리거에서 s3 선택하기

이제 화면 아래에 ‘트리거 구성’ 메뉴가 나오면 아래 스크린샷처럼, 파일을 올릴 s3, 그리고 어떤 이벤트(파일 업로드/삭제/복사 등)를 탐지할지 선택하고, 접두사에서 폴더 경로를 폴더이름/으로 써 준 뒤, 필요한 경우 접미사(주로 파일 확장자)를 써 주면 됩니다.

S3 트리거 구성

이번에는 keras-blog라는 버킷 내 uploads폴더 내에 어떤 파일이든 생성되기만 하면 모두 람다 함수를 실행시키는 것으로 만들어 본 것입니다.

NOTE: 접두사/접미사에 or 조건은 AWS콘솔에서 지원하지 않습니다.

추가버튼을 누르고 난 뒤 저장을 눌러주면 됩니다.

함수 저장하기

트리거가 성공적으로 저장 되었다면 다음과 같은 화면을 볼 수 있을거에요.

람다 함수 다 만든 모습

Lambda 환경변수 추가하기

우리가 앞서 index.py에서 os.environ을 통해 시스템의 환경변수를 가져왔습니다. 이를 정상적으로 동작하게 하기 위해서는 ACCESS_KEYSECRET_KEY을 추가해주어야 합니다. 아래 스크린샷처럼 각각 값을 입력하고 저장해주세요.

환경변수 추가하기

이 키는 AWS iam을 통해 가져올 수 있습니다. 해당 iam계정은 s3 R/W권한, DynamoDB write 권한이 있어야 합니다.

Lambda 내 테스트 돌리기(Optional)

AWS Lambda 콘솔에서도 테스트를 돌릴 수 있습니다.

아래와 같이 event 객체를 만들어 전달하면 실제 이벤트처럼 동작합니다.

예제 테스트

1
2
3
4
5
6
7
8
9
10
{
"Records": [
{
"s3": {
"bucket": {"name": "keras-blog"},
"object": {"key": "uploads/kitten.png"}
}
}
]
}

유의: 실제로 keras-blog버킷 내 uploads폴더 내 kitten.png파일이 있어야 테스트가 성공합니다! (인터넷의 아무 사진이나 넣어두세요.)

테스트가 성공하면 다음과 같이 return된 결과가 json으로 보입니다.

람다콘솔 테스트 성공

마무리: 파일 업로드하고 DB에 쌓이는지 확인하기

이제 s3에 가서 파일을 업로드 해 봅시다.

s3에 파일 업로드

keras-blog 버킷 내 uploads폴더에 고양이 사진 몇 개를 올려봅시다.

몇초 기다리면 DynamoDB에 다음과 같이 파일 이름과 Predict된 결과가 쌓이는 것을 볼 수 있습니다.

DynamoDB에 쌓인 결과

이제 우리는 파일이 1개가 올라가든 1000개가 올라가든 모두 동일한 속도로 결과를 얻을 수 있습니다.

HTML Table을 CSV로 다운로드하기

들어가며

웹 개발을 하다보면 <table>의 내용물을 모두 csv 파일로 받게 해달라는 요구사항이 종종 생깁니다.

가장 일반적인 방법은 csv 형태로 파일을 받을 수 있는 API를 서버가 제공해주는 방법입니다. (백엔드 개발자에게 일을 시킵시다.)

하지만 csv를 던져주는 API 서버가 없다면 프론트에서 보여지는 <table>만이라도 csv로 만들어줘야 합니다.

이번 글은 이럴때 쓰는 방법입니다.

소스코드

우선 이렇게 생긴 HTML이 있다고 생각해 봅시다.

본문이라고는 #, title, content가 들어있는 자그마한 <table> 하나가 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!DOCTYPE html>
<html>
<head lang="ko">
<meta charset="utf-8">
<title>빈 HTML</title>
</head>
<body>
<table id="mytable">
<thead>
<tr>
<th>#</th>
<th>title</th>
<th>content</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Lorem Ipsum</td>
<td>로렘 입섬은 빈칸을 채우기 위한 문구입니다.</td>
</tr>
<tr>
<td>2</td>
<td>Hello World</td>
<td>헬로 월드는 언어를 배우기 시작할때 화면에 표준 출력을 할때 주로 사용하는 문구입니다.</td>
</tr>
</tbody>
</table>

<button id="csvDownloadButton">CSV 다운로드 받기</button>
</body>
</html>

테이블 엘리먼트의 id는 mytable이고, CSV 다운로드 버튼의 id는 csvDownloadButton 입니다.

이제 JS를 조금 추가해봅시다. ES6/ES5에 따라 선택해 사용해주세요.

아래 코드를 <script></script> 태그 사이에 넣어 </body> 바로 앞에 넣어주세요.

ES6을 사용할 경우

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class ToCSV {
constructor() {
// CSV 버튼에 이벤트 등록
document.querySelector('#csvDownloadButton').addEventListener('click', e => {
e.preventDefault()
this.getCSV('mycsv.csv')
})
}

downloadCSV(csv, filename) {
let csvFile;
let downloadLink;

// CSV 파일을 위한 Blob 만들기
csvFile = new Blob([csv], {type: "text/csv"})

// Download link를 위한 a 엘리먼스 생성
downloadLink = document.createElement("a")

// 다운받을 csv 파일 이름 지정하기
downloadLink.download = filename;

// 위에서 만든 blob과 링크를 연결
downloadLink.href = window.URL.createObjectURL(csvFile)

// 링크가 눈에 보일 필요는 없으니 숨겨줍시다.
downloadLink.style.display = "none"

// HTML 가장 아래 부분에 링크를 붙여줍시다.
document.body.appendChild(downloadLink)

// 클릭 이벤트를 발생시켜 실제로 브라우저가 '다운로드'하도록 만들어줍시다.
downloadLink.click()
}

getCSV(filename) {
// csv를 담기 위한 빈 Array를 만듭시다.
const csv = []
const rows = document.querySelectorAll("#mytable table tr")

for (let i = 0; i < rows.length; i++) {
const row = [], cols = rows[i].querySelectorAll("td, th")

for (let j = 0; j < cols.length; j++)
row.push(cols[j].innerText)

csv.push(row.join(","))
}

// Download CSV
this.downloadCSV(csv.join("\n"), filename)
}
}

document.addEventListener('DOMContentLoaded', e => {
new ToCSV()
})

ES5를 사용하실 경우

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function downloadCSV(csv, filename) {
var csvFile;
var downloadLink;

// CSV 파일을 위한 Blob 만들기
csvFile = new Blob([csv], {type: "text/csv"})

// Download link를 위한 a 엘리먼스 생성
downloadLink = document.createElement("a")

// 다운받을 csv 파일 이름 지정하기
downloadLink.download = filename;

// 위에서 만든 blob과 링크를 연결
downloadLink.href = window.URL.createObjectURL(csvFile)

// 링크가 눈에 보일 필요는 없으니 숨겨줍시다.
downloadLink.style.display = "none"

// HTML 가장 아래 부분에 링크를 붙여줍시다.
document.body.appendChild(downloadLink)

// 클릭 이벤트를 발생시켜 실제로 브라우저가 '다운로드'하도록 만들어줍시다.
downloadLink.click()
}

function getCSV(filename) {
// csv를 담기 위한 빈 Array를 만듭시다.
var csv = []
var rows = document.querySelectorAll("#mytable table tr")

for (var i = 0; i < rows.length; i++) {
var row = [], cols = rows[i].querySelectorAll("td, th")

for (var j = 0; j < cols.length; j++)
row.push(cols[j].innerText)

csv.push(row.join(","))
}

// Download CSV
downloadCSV(csv.join("\n"), filename)
}

document.addEventListener('DOMContentLoaded', e => {
// CSV 버튼에 이벤트 등록
document.querySelector('#csvDownloadButton').addEventListener('click', e => {
e.preventDefault()
getCSV('mycsv.csv')
})
})

전체 예시

예제 html_to_csv.html에서 직접 동작하는 것을 확인해 보세요!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
<!DOCTYPE html>
<html>
<head lang="ko">
<meta charset="utf-8">
<title>빈 HTML</title>
</head>
<body>
<table id="mytable">
<thead>
<tr>
<th>#</th>
<th>title</th>
<th>content</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Lorem Ipsum</td>
<td>로렘 입섬은 빈칸을 채우기 위한 문구입니다.</td>
</tr>
<tr>
<td>2</td>
<td>Hello World</td>
<td>헬로 월드는 언어를 배우기 시작할때 화면에 표준 출력을 할때 주로 사용하는 문구입니다.</td>
</tr>
</tbody>
</table>

<button id="csvDownloadButton">CSV 다운로드 받기</button>
</body>
<script type="text/javascript">
class ToCSV {
constructor() {
// CSV 버튼에 이벤트 등록
document.querySelector('#csvDownloadButton').addEventListener('click', e => {
e.preventDefault()
this.getCSV('mycsv.csv')
})
}

downloadCSV(csv, filename) {
let csvFile;
let downloadLink;

// CSV 파일을 위한 Blob 만들기
csvFile = new Blob([csv], {type: "text/csv"})

// Download link를 위한 a 엘리먼스 생성
downloadLink = document.createElement("a")

// 다운받을 csv 파일 이름 지정하기
downloadLink.download = filename;

// 위에서 만든 blob과 링크를 연결
downloadLink.href = window.URL.createObjectURL(csvFile)

// 링크가 눈에 보일 필요는 없으니 숨겨줍시다.
downloadLink.style.display = "none"

// HTML 가장 아래 부분에 링크를 붙여줍시다.
document.body.appendChild(downloadLink)

// 클릭 이벤트를 발생시켜 실제로 브라우저가 '다운로드'하도록 만들어줍시다.
downloadLink.click()
}

getCSV(filename) {
// csv를 담기 위한 빈 Array를 만듭시다.
const csv = []
const rows = document.querySelectorAll("#mytable tr")

for (let i = 0; i < rows.length; i++) {
const row = [], cols = rows[i].querySelectorAll("td, th")

for (let j = 0; j < cols.length; j++)
row.push(cols[j].innerText)

csv.push(row.join(","))
}

// Download CSV
this.downloadCSV(csv.join("\n"), filename)
}
}

document.addEventListener('DOMContentLoaded', e => {
new ToCSV()
})
</script>
</html>

하지만 이렇게하면 한글이 깨지는 문제가 있습니다.

한글 깨지는 문제 해결하기

앞서 한글이 깨지는 이유는 기본적으로 엑셀이 인코딩을 UTF-8로 인식하지 않기 때문에 문제가 발생합니다.

이때 아래 코드를 csv blob을 만들기 전 추가해주면 됩니다.

1
2
3
// 한글 처리를 해주기 위해 BOM 추가하기
const BOM = "\uFEFF";
csv = BOM + csv
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// downloadCSV 함수를 이렇게 수정해 주세요.
downloadCSV(csv, filename) {
let csvFile;
let downloadLink;

// 한글 처리를 해주기 위해 BOM 추가하기
const BOM = "\uFEFF";
csv = BOM + csv

// CSV 파일을 위한 Blob 만들기
csvFile = new Blob([csv], {type: "text/csv"})

// Download link를 위한 a 엘리먼스 생성
downloadLink = document.createElement("a")

// 다운받을 csv 파일 이름 지정하기
downloadLink.download = filename;

// 위에서 만든 blob과 링크를 연결
downloadLink.href = window.URL.createObjectURL(csvFile)

// 링크가 눈에 보일 필요는 없으니 숨겨줍시다.
downloadLink.style.display = "none"

// HTML 가장 아래 부분에 링크를 붙여줍시다.
document.body.appendChild(downloadLink)

// 클릭 이벤트를 발생시켜 실제로 브라우저가 '다운로드'하도록 만들어줍시다.
downloadLink.click()
}

TreeShaking으로 webpack 번들 결과 용량 줄이기

이번 글은 webpack을 사용하고 있다고 가정합니다. 만약 webpack이 뭔지 아직 모르시거나 설치하지 않으셨다면 Webpack과 Babel로 최신 JavaScript 웹프론트 개발환경 만들기를 먼저 읽고 따라가보세요.

들어가며

웹 프론트 개발을 할 때 npm과 webpack을 통해 bundle.js와 같은 번들링된 js파일 하나로 만들어 싱글 페이지 앱을 만드는 경우가 많습니다.

우리가 사용하는 패키지들을 찾아 간단하게 묶고 babel을 통해 하위버전 브라우저에서도 돌아가도록 만들어주는 작업은 마치 마법과 같이 편리합니다.

하지만 이 마법같은 번들링에도 심각한 문제점이 있습니다. 바로 용량이 어마어마해진다는 것이죠.

아무런 처리를 하지 않고 webpack으로 빌드를 할 때의 용량은 스크린샷에 나온 것처럼 무려 1.61MB됩니다.

사실 아직 lodash, bootstrap3, axios와 같은 아주 기본적인 라이브러리들만 넣었음에도 다음과 같이 어마어마하게 무거운 js파일이 생성됩니다.

이제 이 파일을 1/3 크기로 줄여봅시다.

uglifyjs-webpack-plugin

webpack과 함께 파일의 용량을 줄여주는 도구인 uglifyjs를 사용해봅시다.

우선 다음 명령어로 uglifyjs-webpack-plugin를 설치해주세요.

1
npm install --save-dev uglifyjs-webpack-plugin

webpack 실행시 자동으로 용량줄이기

여러분이 webpack을 사용하고 있다면 아마 다음과 같은 webpack.config.js파일을 만들어 사용하고 있을거에요. (세부적인 설정은 다를 수 있어요.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const webpack = require('webpack');
const path = require('path');

module.exports = {
entry: './src/js/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
publicPath: '/dist/',
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
include: path.join(__dirname, 'src'),
exclude: /(node_modules)|(dist)/,
use: {
loader: 'babel-loader',
options: {
presets: [
["env"]
]
}
}
}
]
}
}

위 설정은 단순히 src파일 안의 js들을 dist폴더 안의 bundle.js파일로 묶어주고 있습니다.

이제 여기에서 몇줄만 추가해 주면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const webpack = require('webpack');
const path = require('path');
// 1. UglifyJSPlugin을 가져오세요.
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');

module.exports = {
entry: './src/js/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
publicPath: '/dist/',
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
include: path.join(__dirname, 'src'),
exclude: /(node_modules)|(dist)/,
use: {
loader: 'babel-loader',
options: {
presets: [
["env", {
"targets": {
"browsers": ["last 2 versions", "safari >= 7"]
}
}]
]
}
}
}
]
},
// 2. plugins를 새로 만들고, new UglifyJsPlugin() 을 통해
// UglifyJS를 빌드 과정에 합쳐주세요.
plugins: [
new UglifyJsPlugin()
]
}

이제 빌드를 실행해보면 아래 스크린샷과 같이 bundle.js파일의 용량이 획기적으로 줄어든 것을 볼 수 있습니다. 용량이 1.6MB에서 667KB로 1/3정도로 줄어든 것을 볼 수 있습니다. 간단하죠?

하지만 여기에는 작은 함정이 있습니다. 바로 time, 즉 빌드시마다 걸리는 시간도 그에따라 늘어난 것인데요, 만약 여러분이 webpack-dev-server와 같이 실시간으로 파일을 감시하며 변화 발생시마다 빌드하는 방식을 사용하고 있다면 코드 한줄, 띄어쓰기 하나 수정한 정도로 무려 12초에 달하는 빌드 시간을 기다려야 합니다. (treeshaking 하기 전에는 3초정도밖에 걸리지 않았습니다.)

그래서 항상 treeshaking을 해주는 대신 빌드작업, 즉 서버에 실제로 배포하기 위해 bundle.js파일을 생성할 때만 treeshaking을 해주면 개발도 빠르고 실제 배포시에도 빠르게 작업이 가능합니다.

빌드할때만 사용하기

앞서 다뤘던 package.json파일 중 script부분 아래 build를 다음과 같이 수정해주세요.

1
2
3
4
5
6
7
{
...
"scripts": {
"build": "webpack --optimize-minimize",
},
...
}

그리고 webpack.config.js 파일 중 위에서 넣어주었던 plugins를 통채로 지워주세요.(더이상 필요하지 않아요!)

만약 여러분이 webpack.config.js파일을 정확히 설정해 webpack이라는 명령어가 성공적으로 실행되고있던 상태라면 --optimize-minimize라는 명령어만 뒤에 붙여주면 곧바로 실행됩니다.

이제 여러분이 개발할 때 webpack-dev-server를 통해 빌드가 실행될때는 treeshaking이 되지 않고, 대신 배포를 위해 빌드를 할 때는 최소화된 작은 번들된 js파일을 가질 수 있게 됩니다.

마무리

여러분이 위 과정을 모두 따라왔다면 아마 package.jsonwebpack.config.js파일은 이와 유사하게 생겼을거에요.

1
2
3
4
5
6
7
8
9
// 앞뒤생략한 package.json
{
...
"scripts": {
"build": "webpack --optimize-minimize",
"devserver": "webpack-dev-server --open"
},
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// webpack.config.js 파일
const webpack = require('webpack');
const path = require('path');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');

module.exports = {
entry: './src/js/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
publicPath: '/dist/',
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
include: path.join(__dirname, 'src'),
exclude: /(node_modules)|(dist)/,
use: {
loader: 'babel-loader',
options: {
presets: [
["env", {
"targets": {
"browsers": ["last 2 versions", "safari >= 7"]
}
}]
]
}
}
}
]
}
// Plugin은 필요한 것만 넣어주세요. UglifyJSPlugin은 필요없어요!
}

한글이 보이는 Flask CSV Response 만들기

들어가며

웹 사이트를 만들다 보면 테이블 등을 csv파일로 다운받을 수 있도록 만들어달라는 요청이 자주 있습니다. 이번 글에서는 Flask에서 특정 URL로 들어갈 때 CSV파일을 받을 수 있도록 만들고, 다운받은 CSV파일을 엑셀로 열 때 한글이 깨지지 않게 처리해 봅시다.

이번에는 Flask + SQLAlchemy + Pandas를 사용합니다.

Flask 코드짜기

우선 Flask 코드를 하나 봅시다. app.py라는 이름을 갖고 있다고 생각해 봅시다.

아래 코드는 Post라는 모델을 모두 가져와 df라는 DataFrame객체로 만든 뒤 .to_csv를 통해 csv 객체로 만들어 준 뒤 StringIO를 통해 실제 io가 가능한 바이너리형태로 만들어 줍니다.

또, output을 해주기 전 u'\ufeff'를 미리 넣어줘 이 파일이 ‘UTF-8 with BOM’이라는 방식으로 인코딩 되어있다는 것을 명시적으로 알려줍니다.

인코딩 명시를 빼면 엑셀에서 파일을 열 때 한글이 깨져서 나옵니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# app.py
from io import StringIO
from flask import Flask, jsonify, request, Response
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__) # Flask App 만들기
app.config['SQLALCHEMY_DATABASE_URI'] = '데이터베이스 URI' # SQLAlchemy DB 연결하기

db = SQLAlchemy()
db.init_app(app)

# 기타 설정을 해줬다고 가정합니다.

@app.route('/api/post/csv/') # URL 설정하기
def post_list_csv(self):
queryset = Post.query.all()
df = pd.read_sql(queryset.statement, queryset.session.bind) # Pandas가 SQL을 읽도록 만들어주기
output = StringIO()
output.write(u'\ufeff') # 한글 인코딩 위해 UTF-8 with BOM 설정해주기
df.to_csv(output)
# CSV 파일 형태로 브라우저가 파일다운로드라고 인식하도록 만들어주기
response = Response(
output.getvalue(),
mimetype="text/csv",
content_type='application/octet-stream',
)
response.headers["Content-Disposition"] = "attachment; filename=post_export.csv" # 다운받았을때의 파일 이름 지정해주기
return response

PySpark & Hadoop: 2) EMR 클러스터 띄우고 PySpark로 작업 던지기

이번 글은 PySpark & Hadoop: 1) Ubuntu 16.04에 설치하기와 이어지는 글입니다.

들어가며

이전 글에서 우분투에 JAVA/Hadoop/PySpark를 설치해 spark를 통해 EMR로 작업을 던질 EC2를 하나 생성해보았습니다. 이번에는 동일한 VPC그룹에 EMR 클러스터를 생성하고 PySpark의 yarn설정을 통해 원격 EMR에 작업을 던져봅시다.

EMR 클러스터 띄우기

AWS 콘솔에 들어가 EMR을 검색해 EMR 대시보드로 들어갑시다.

AWS 콘솔에서 EMR검색하기

EMR 대시보드에서 ‘클러스터 생성’을 클릭해주세요.

EMR 대시보드 첫화면에서 클러스터 생성 클릭

이제 아래와 같이 클러스터이름을 적어주고, 시작 모드를 ‘클러스터’로, 릴리즈는 최신 릴리즈 버전(현 5.10이 최신)으로, 애플리케이션은 Spark를 선택해주세요.

그리고 EC2 키 페어를 갖고있다면 기존에 갖고있는 .pem파일을, 없다면 새 키를 만들고 진행하세요.

주황색 표시 한 부분 외에는 기본 설정값 그대로 두면 됩니다. 로깅은 필요한 경우 켜고 필요하지 않은 경우 꺼두면 됩니다.

그리고 할 작업에 따라 인스턴스 유형을 r(많은 메모리), c(많은 CPU), i(많은 스토리지), p(GPU)중 선택하고 인스턴스 개수를 원하는 만큼 선택해주면 됩니다.

많으면 많을수록 Spark작업이 빨리 끝나는 한편 비용도 그만큼 많이 듭니다. 여기서는 기본값인 r3.xlarge 인스턴스 3개로 진행해 봅시다. 인스턴스 3대가 생성되면 한대는 Master 노드가, 나머지 두대는 Core 노드가 됩니다. 앞으로 작업을 던지고 관리하는 부분은 모두 Master노드에서 이루어집니다.

EMR Spark 클러스터 만들기

설정이 끝나고 나면 아래 ‘클러스터 생성’ 버튼을 눌러주세요.

클러스터 생성 클릭

클러스터가 시작되고 ‘준비’ 단계가 될 때까지는 약간의 시간(1~3분)이 걸립니다. ‘마스터 퍼블릭 DNS’가 화면에 뜰 때까지 잠시 기다려 줍시다.

클러스터: PySpark화면

클러스터가 준비가 완료되면 아래와 같이 ‘마스터 퍼블릭 DNS’ 주소가 나옵니다.

클러스터: PySpark DNS나온 화면

‘마스터 퍼블릭 DNS’는 앞으로 설정할때 자주 사용하기 때문에 미리 복사를 해 둡시다.

1
2
# 이번에 만들어진 클러스터의 마스터 퍼블릭 DNS
ec2-13-124-83-135.ap-northeast-2.compute.amazonaws.com

이렇게 나오면 우선 클러스터를 사용할 준비가 완료된 것으로 볼 수 있습니다. 이제 다시 앞 글에서 만든 EC2를 설정해봅시다.

EC2 설정 관리하기

이제 EMR 클러스터가 준비가 완료되었으니 EC2 인스턴스에 다시 ssh로 접속을 해 봅시다.

이전 편인 PySpark & Hadoop: 1) Ubuntu 16.04에 설치하기글을 읽고 따라 왔다면 여러분의 EC2에는 아마 JAVA와 PySpark, 그리고 Hadoop이 설치가 되어있을겁니다.

우리는 Hadoop의 yarn을 통해서 EMR 클러스터에 spark작업을 던져주기 때문에 이 부분을 설정을 조금 해줘야 합니다.

이전 편을 따라왔다면 아래 두 파일을 수정해주면 되고, 만약 따로 Hadoop을 설치해줬다면 which hadoop을 통해서 나오는 주소를 약간 수정해 사용해주면 됩니다.

우선 앞서 우리가 Hadoop을 설치해준 곳은 /usr/local/hadoop/bin/hadoop 입니다.

which hadoop

그리고 우리가 수정해줘야 하는 두 파일은 위와 같은 위치에 있는 core-site.xml파일, 그리고 yarn-site.xml 파일입니다. 즉, 절대 경로는 아래와 같습니다.

1
2
3
4
# core-site.xml
/usr/local/hadoop/etc/hadoop/core-site.xml
# yarn-site.xml
/usr/local/hadoop/etc/hadoop/yarn-site.xml

만약 다른 곳에 설치했다면 /하둡을설치한위치/etc/hadoop/ 안의 core-site.xmlyarn-site.xml을 수정하면 됩니다.

core-site.xml 수정하기

이제 core-site.xml파일을 수정해 봅시다.

core-site.xml 수정

core-site.xml에는 다음과 같이 fs.defaultFS라는 name을 가진 property를 하나 추가해주면 됩니다. 그리고 그 값을 hdfs://마스터퍼블릭DNS로 넣어줘야 합니다.

1
2
3
4
5
6
7
8
9
<!-- core-site.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<configuration>
<property>
<name>fs.defaultFS</name>
<value>hdfs://ec2-13-124-83-135.ap-northeast-2.compute.amazonaws.com</value>
</property>
</configuration>

수정은 vim이나 nano등의 편집기를 이용해주세요.

yarn-site.xml 수정하기

이제 다음 파일인 yarn-site.xml파일을 수정해 봅시다.

yarn-site.xml 수정

yarn-site.xml에는 다음과 같이 두가지 설정을 마스터퍼블릭DNS로 넣어줘야 합니다. address에는 포트도 추가적으로 붙여줘야 합니다.

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0"?>
<configuration>
<property>
<name>yarn.resourcemanager.address</name>
<value>ec2-13-124-83-135.ap-northeast-2.compute.amazonaws.com:8032</value>
</property>
<property>
<name>yarn.resourcemanager.hostname</name>
<value>ec2-13-124-83-135.ap-northeast-2.compute.amazonaws.com</value>
</property>
</configuration>

이렇게 두 파일을 수정해주었으면 EC2에서 설정을 수정할 부분은 끝났습니다.

EMR 클러스터 설정 관리하기

같은버전 Python 설치하기

Spark에서 파이썬 함수(혹은 파일)을 실행할 때 Spark가 실행되고있는 파이썬 버전과 PySpark등을 통해 Spark 서버로 요청된 파이썬 함수의 버전과 일치하지 않으면 Exception을 일으킵니다.

Python driver 3.4_3.5 Exception

현재 EMR에 설치되어있는 python3은 3.4버전인데, EC2(Ubuntu 16.04)의 파이썬 버전은 3.5버전이기 때문에 Exception이 발생합니다.

아래 세 가지 방법 중 하나를 선택해 해결해주세요.

첫번째 방법: Ubuntu EC2에 Python3.4를 설치하기

Ubuntu16은 공식적으로 Python3.4를 지원하지 않습니다. 하지만 간단한 방법으로 Python3.4를 설치할 수 있습니다.

아래 세 줄을 입력해주세요.

1
2
3
sudo add-apt-repository ppa:fkrull/deadsnakes
sudo apt-get update
sudo apt-get install python3.4 -y

이렇게 하면 Python3.4를 사용할 수 있습니다.

막 설치해준 Python3.4에는 아직 pyspark가 설치되어있지 않으니 아래 명령어로 pyspark를 설치해 줍시다.

1
python3.4 -m pip install -U pyspark --no-cache

이제 EMR 클러스터에 작업을 던져줍시다.

두번째 방법: EMR을 이루는 인스턴스에 원하는 Python버전(3.5)를 설치하기

EMR에 python3.5를 설치해 문제를 해결해 봅시다.

만약 여러분이 Ubuntu 17버전을 사용한다면 기본적으로 Python3.6이 설치되어있기 때문에 아래 코드에서 35 대신 36을 이용해주시면 됩니다.

이제 SSH를 통해 EMR Master에 접속해 봅시다.

1
2
3
chmod 400 sshkey.pem # ssh-add는 권한을 따집니다. 400으로 읽기권한만 남겨두세요.
ssh-add sshkey.pem # 여러분의 .pem 파일 경로를 넣어주세요.
ssh hadoop@ec2-13-124-83-135.ap-northeast-2.compute.amazonaws.com

SSH Login EMR

로그인을 하고 나서 파이썬 버전을 알아봅시다. 파이썬 버전은 아래 사진처럼 볼 수 있습니다.

EMR python은 3.4버전

파이썬이 3.4버전인것을 확인할 수 있습니다.

한편, EC2의 파이썬은 아래와 같이 3.5버전입니다.

EC2 python은 3.5버전

이제 EMR에 Python3.5를 설치해 줍시다.

1
sudo yum install python35

위 명령어를 입력하면 python3.5버전이 설치됩니다.

yum install python35

Y/N을 물어보면 y를 눌러줍시다.

Press y to install

python3 -V를 입력해보면 성공적으로 파이썬 3.5버전이 설치된 것을 볼 수 있습니다.

Python3.5.1

이 과정을 Master / Core 각 인스턴스별로 진행해주시면 됩니다. SSH로 접속 후 python35만 설치하면 됩니다.

이제 EMR 클러스터에 작업을 던져줍시다.

세번째 방법: EMR 클러스터 부트스트랩 이용하기

두번째 방법과 같이 EMR 클러스터를 이루는 인스턴스 하나하나에 들어가 설치를 진행하는 것은 굉장히 비효율적입니다.

그래서 EMR 클러스터가 생성되기 전에 두번째 방법에서와 같이 EMR 클러스터 내에 Python35, Python36을 모두 설치해두면 앞으로도 문제가 없을거에요.

이때 사용할 수 있는 방법이 ‘bootstrap action’ 입니다. bootstrap action은 EMR 클러스터가 생성되기 전 .sh같은 쉘 파일등을 실행할 수 있습니다.

우선 우리가 실행해줄 installpy3536.sh 파일을 로컬에서 하나 만들어 줍시다.

1
2
3
4
5
#!/bin/bash

sudo yum install python34 -y
sudo yum install python35 -y
sudo yum install python36 -y

EMR 클러스터는 아마존리눅스상에서 돌아가기 때문에 yum을 통해 패키지를 설치할 수 있습니다. 각각 python3.4/3.5/3.6버전을 받아 설치해주는 명령어입니다.

이 파일을 s3에 올려줍시다.

우선 빈 버킷 혹은 기존 버킷에 파일을 올려주세요.

빈 s3 버킷 만들기

파일 권한은 기본 권한 그대로 두면 됩니다. 그리고 이 파일은 AWS 외부에서 접근하지 않기 때문에 퍼블릭으로 해둘 필요는 없습니다.

파일 업로드 완료

이제 EMR을 실행하러 가 봅시다.

앞서서는 ‘빠른 옵션’을 이용했지만 이제 ‘고급 옵션’을 이용해야 합니다.

새 EMR 클러스터 만들기

고급 옵션에서 소프트웨어 구성을 다음과 같이 체크하고 ‘다음’을 눌러줍시다.

단계1: 소프트웨어 및 단계

다음 단계인 ‘하드웨어’는 기본값 혹은 필요한 만큼 설정해준 뒤 ‘다음’을 눌러줍시다. 여기서는 기본값으로 넣어줬습니다.

단계2: 하드웨어

이번 단계인 ‘일반 클러스터 설정’이 중요합니다. 여기에서 ‘부트스트랩 작업’을 누르고 ‘사용자 지정 작업’을 선택해주세요.

단계3: 일반 클러스터 설정

‘사용자 지정 작업’을 선택한 뒤 ‘구성 및 추가’를 눌러주세요.

구성 및 추가

추가를 누르면 다음과 같이 ‘이름’, ‘스크립트 위치’를 찾아줘야 합니다. 이름을 InstallPython343536이라고 지어봅시다.

이제 스크립트 옆 폴더 버튼을 눌러줍시다.

아까 만든 installpy3536.sh파일이 있는 버킷에 찾아들어가 installpy3536.sh 파일을 선택해줍시다.

선택을 눌러준 뒤 ‘추가’를 눌러줍시다.

아래와 같이 ‘부트스트랩 작업’에 추가되었다면 ‘다음’을 눌러 클러스터를 만들어 줍시다.

이제 마지막으로 SSH접속을 위한 키 페어를 선택한 후 ‘클러스터 생성’을 눌러줍시다.

이제 생성된 EMR 클러스터에는 python3.4/3.5/3.6이 모두 설치되어있습니다. 이 버전 선택은 아래 PYSPARK_PYTHON 값을 설정할때 변경해 사용하면 됩니다.

ubuntu 유저 만들고 hadoop그룹에 추가하기

python 설치를 마쳤다면 이제 ubuntu유저를 만들어줘야 합니다.

EMR 클러스터 마스터 노드에 작업을 추가해 줄 경우 기본적으로 작업을 실행한 유저(우분투 EC2에서 요청시 기본 유저는 ubuntu)의 이름으로 마스터 노드에서 요청한 유저의 홈 폴더를 찾습니다.

만약 EC2에서 EMR로 요청한다면 ubuntu라는 계정 이름으로 EMR 마스터 노드에서 /home/ubuntu라는 폴더를 찾아 이 폴더에 작업할 파이썬 파일과 의존 패키지 등을 두고 작업을 진행합니다. 하지만 EMR은 기본적으로 hadoop이라는 계정을 사용하고, 따라서 ubuntu라는 유저는 추가해줘야 합니다. 그리고 우리가 새로 만들어준 ubuntu 유저는 하둡에 접근할 권한이 없기 때문에 이 유저를 hadoop그룹에 추가해줘야 합니다.

우분투 계정 만들고 하둡 그룹에 추가

위 사진처럼 두 명령어를 입력해 줍시다.

1
2
sudo adduser ubuntu
sudo usermod -a -G hadoop ubuntu

첫 명령어는 ubuntu라는 유저를 만들고 다음에서 hadoop이라는 그룹에 ubuntu유저를 추가합니다.

이제 우리는 EC2에서 EMR로 분산처리할 함수들을 보낼 수 있습니다.

마무리: 파이(pi) 계산 예제 실행하기

한번 PySpark의 기본 예제중 하나인 pi(원주율) 계산을 진행해 봅시다.

공식 예제: https://github.com/apache/spark/blob/master/examples/src/main/python/pi.py

공식 예제는 스파크와 하둡을 로컬에서 사용합니다. 하지만 우리는 EMR 클러스터에 작업을 던져줄 것이기 때문에 약간 코드를 변경해줘야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# pi.py
import sys
from random import random
from operator import add
import os

os.environ["PYSPARK_PYTHON"] = "/usr/bin/python34" # python3.5라면 /usr/bin/python35

from pyspark.sql import SparkSession

if __name__ == "__main__":
"""
Usage: pi [partitions]
"""
# 이 부분을 추가해주시고
spark = SparkSession \
.builder \
.master("yarn") \
.appName("PySpark") \
.getOrCreate()

# 이부분을 주석처리해주세요.
#spark = SparkSession\
# .builder\
# .appName("PythonPi")\
# .getOrCreate()

partitions = int(sys.argv[1]) if len(sys.argv) > 1 else 2
n = 100000 * partitions

def f(_):
x = random() * 2 - 1
y = random() * 2 - 1
return 1 if x ** 2 + y ** 2 <= 1 else 0

count = spark.sparkContext.parallelize(range(1, n + 1), partitions).map(f).reduce(add)
print("Pi is roughly %f" % (4.0 * count / n))

spark.stop()

기존 코드는 builder를 통해 로컬에서 작업을 던져주지만 이렇게 .master("yarn")을 추가해주면 yarn 설정을 통해 아래 작업이 EMR 클러스터에서 동작하게 됩니다.

EC2상에서 아래 명령어로 위 파이썬 파일을 실행해 봅시다.

1
python3.4 pi.py

실행을 해 보면 결과가 잘 나오는 것을 볼 수 있습니다.

pi is roughly 3.144720

만약 두번째/세번째 방법으로 Python3.5를 설치해주셨다면 별다른 설정 없이 python3 pi.py로 실행하셔도 됩니다.

자주 보이는 에러/경고

WARN yarn.Client: Same path resource

새 task를 Spark로 넘겨줄 때 마다 패키지를 찾기 때문에 나오는 에러입니다. 무시해도 됩니다.

Initial job has not accepted any resources

EMR 설정 중 spark.dynamicAllocation.enabledTrue일 경우 생기는 문제입니다.

pi.py파일 코드를 일부 수정해주세요.

기존에 있던 spark 생성하는 부분에 아래 config 몇줄을 추가해주세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 기존 코드를 지우고
# spark = SparkSession \
# .builder \
# .master("yarn") \
# .appName("PySpark") \
# .getOrCreate()

# 아래 코드로 바꿔주세요.
spark = SparkSession.builder \
.master("yarn") \
.appName("PySpark") \
.config("spark.executor.memory", "512M") \
.config("spark.yarn.am.memory", "512M") \
.config("spark.executor.cores", 2) \
.config("spark.executor.instances", 1) \
.config("spark.dynamicAllocation.enabled", False) \
.getOrCreate()

이때 각 config별로 설정되는 값은 여러분이 띄운 EMR에 따라 설정해줘야 합니다. 만약 여러분이 r3.xlarge를 선택했다면 8개의 vCPU, 30.5 GiB 메모리를 사용하기 때문에 저 설정 숫자들을 높게 잡아도 되지만, 만약 c4.large를 선택했다면 2개의 vCPU, 3.8 GiB 메모리를 사용하기 때문에 코드에서 설정한 CPU코어수 혹은 메모리 용량이 클러스터의 CPU개수와 메모리 용량을 초과할 경우 에러가 납니다.

깃헙 Pages에 깃북 배포하기

들어가며

이번에 ‘나만의 웹 크롤러 만들기’ 가이드 시리즈를 이 블로그에서 관리하던 중, 글이 파편화되어있는 상황이며 가이드가 유의미하게 이어진다는 느낌이 적어서 깃북을 통해 새로 가이드를 배포하기로 결정했다.

깃북의 경우 https://gitbook.io에서 제공하는 자체 호스팅 서비스가 있고 오픈소스로 정적 사이트를 제작하는 Gitbook 프로젝트도 있다.

깃북 웹 사이트의 경우 느려지는 경우도 종종 있어 좀 더 관리의 범위가 넓은 깃헙에서 깃북 레포를 통해 깃북을 관리하려고 생각했다.

Gitbook 설치하기

우선 깃북의 경우 node.js기반이기 때문에 시스템에 nodenpm이 설치되어있어야 한다. npm은 node.js를 설치할때 보통 같이 설치된다. 글쓴날짜 기준 9.2.0버전이 node.js의 최신 버전이다.

Node.js설치하기: https://nodejs.org/en/

시스템에 npm이 설치 완료되었다면 이제 아래 명령어로 gitbook-cli를 설치해 주자.

1
2
# 콘솔 / cmd / terminal에서 아래줄을 입력후 엔터!
npm install gitbook-cli -g

Gitbook Init

깃북은 SUMMARY.md파일을 통해 화면 좌측의 내비게이션/목록 부분을 만든다.

한번에 폴더와 기본파일을 모두 생성하려면 아래 명령어를 입력해 주자.

1
2
3
# 콘솔 / cmd / terminal에서 아래줄을 입력후 엔터!
gitbook init my_gitbook
# 사용법: gitbook init 사용하려는폴더이름

위 명령어를 입력하면 현재 위치 아래 my_gitbook이라는 폴더가 생기고, 그 안에 README.mdSUMMARY.md가 생긴다.

Git Init

깃북을 배포하려면 깃헙에 레포지토리를 만들고 파일을 올려야 하기 때문에 우선 git init으로 현재 폴더를 git이 관리하도록 만들어준다.

gh-pages 브랜치 만들기

Github Pages의 경우 크게 3가지 방법으로 호스팅을 진행한다.

  1. 유저이름.github.io라는 레포의 master브랜치
  2. 어떤 레포든 docs 폴더
  3. 어떤 레포든 gh-pages 브랜치

위 세가지가 충족되는 경우 자동으로 깃헙 페이지용도로 인식하고 https://유저이름.github.io/레포명 주소로 정적 호스팅을 진행해 준다.

이번에는 세번째 방법인 gh-pages 브랜치를 이용한다.

물론 1,2,3번 모두 커스텀 도메인 사용이 가능하다.

publish_gitbook.sh 만들기

README.md가 있는 곳 옆에 publish_gitbook.sh파일을 다음 내용으로 만들어 주자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# gitbook 의존 파일을 설치하고 gitbook 빌드를 돌린다.
gitbook install && gitbook build

# github pages가 바라보는 gh-pages 브랜치를 만든다.
git checkout gh-pages

# 최신 gh-pages 브랜치 정보를 가져와 rebase를 진행한다.
git pull origin gh-pages --rebase

# gitbook build로 생긴 _book폴더 아래 모든 정보를 현재 위치로 가져온다.
cp -R _book/* .

# node_modules폴더와 _book폴더를 지워준다.
git clean -fx node_modules
git clean -fx _book

# NOQA
git add .

# 커밋커밋!
git commit -a -m "Update docs"

# gh-pages 브랜치에 PUSH!
git push origin gh-pages

# 다시 master 브랜치로 돌아온다.
git checkout master

위와 같이 publish_gitbook.sh파일을 만들어 주면, 앞으로 작업을 끝낼때마다 ./publish_gitbook.sh라는 명령어로 한번에 깃헙에 작업한 결과물을 빌드해 올릴 수 있다.

SUMMARY.md 관리하기

깃북이 파일을 관리하는 것은 폴더별 관리라기보다는 SUMMARY.md파일 내의 정보를 기반으로 URL을 만들고 글의 순서와 목차를 관리한다.

보통 카테고리/챕터별로 폴더를 만들어 관리하는 방법을 사용하는 것 같은데, 나만의 웹 크롤러 만들기 깃북의 경우 아래와 같은 형태를 사용하고 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
# Summary

- [나만의 웹 크롤러 만들기 시리즈](README.md)
- [requests와 BeautifulSoup으로 웹 크롤러 만들기](posts/2017-01-20-HowToMakeWebCrawler.md)
- [Session을 이용해 로그인하기](posts/2017-01-20-HowToMakeWebCrawler-With-Login.md)
- [Selenium으로 무적 크롤러 만들기](posts/2017-02-27-HowToMakeWebCrawler-With-Selenium.md)
- [Django로 크롤링한 데이터 저장하기](posts/2017-03-01-HowToMakeWebCrawler-Save-with-Django.md)
- [웹페이지 업데이트를 알려주는 Telegram 봇](posts/2017-04-20-HowToMakeWebCrawler-Notice-with-Telegram.md)
- [N배빠른 크롤링, multiprocessing](posts/2017-07-05-HowToMakeWebCrawler-with-Multiprocess.md)
- [Headless 크롬으로 크롤링하기](posts/2017-09-28-HowToMakeWebCrawler-Headless-Chrome.md)

- Tips
- [Selenium Implicitly wait vs Explicitly wait](posts/2017-10-29-HowToMakeWebCrawler-ImplicitWait-vs-ExplicitWait.md)

위와 같이 -를 사용해 글과 카테고리를 구별하고 스페이스를 통해 제목과 링크를 마크다운 문법으로 걸어주면 깃북이 이 파일을 읽고 각각의 파일을 html파일로 만들어준다.

유의할점

깃북에서 ![](이미지링크)와 같은 방식을 사용하면 ‘/‘로 시작하는 절대경로 URL을 상대경로인 ‘../‘로 변환해버리는 문제가 있다.

따라서 SUMMARY.md파일과 같은 위치에 book.json파일을 만들어주고 전역 변수를 사용할 수 있다.

book.json 관리하기

깃북은 book.json파일이 세팅 파일이다. 플러그인을 넣고, 지우고, 전역 변수 등을 설정할 수도 있다.

다음은 Google Analytics 플러그인을 ‘넣고’ 소셜 공유 아이콘을 ‘빼고’, ‘BASE_URL’에 블로그 절대경로를 만들기 위해 URL을 등록해둔 부분이다.

토큰은 Google Analytics에서 받아서 넣어주면 된다.

1
2
3
4
5
6
7
8
9
10
11
{
"plugins": ["ga", "-sharing"],
"pluginsConfig": {
"ga": {
"token": "UA-12341234-1"
}
},
"variables": {
"BASE_URL": "https://beomi.github.io"
}
}

키노트로 고화질 타이틀 이미지 만들기

이번 글은 애플 키노트를 다루니 당연히 macOS가 대상입니다 :)

들어가며

블로그 글을 맥에서 작성하다보면 자연스럽게 키노트로 메인 타이틀이나 설명을 만드는 경우가 많습니다. 아래 화면처럼 이미지와 글자를 조금 배치하는 방식으로 깔끔한 이미지 하나를 만들게 됩니다. (이번 글 대표 이미지도 이 방식으로 만들었습니다.)

하지만 문제가 있습니다.

애플 키노트에서 기본적으로 제공하는 Export to Image, “다음으로 보내기 -> 이미지..”를 사용하는 방법도 물론 있습니다. 하지만 사실 이렇게 작업하면…

화면에서 바라볼때는 고해상도의 레티나 결과물이 나왔지만, 실제로 Export된 이미지 파일을 보면 이미지 픽셀은 1920*1080으로 충분히 고화질이지만 ppi가 72밖에 되지 않는 것을 볼 수 있습니다.

(아니 왜 레티나를 샀는데 보여주지를 못하니 ㅠㅠ)

해결방법

해결방법은 한 단계를 더 거치는데, 다른 프로그램을 사용하는 것이 아니라 맥에 내장되어있는 ‘미리보기’를 이용하는 방법입니다.

단계는 다음과 같습니다.

  • 키노트에서 PDF로 내보내기
  • PDF파일을 ‘미리보기’로 열기
  • 미리보기에서 ‘내보내기’ 기능으로 고해상도 이미지 만들기

네, 귀찮습니다. 하지만 레티나 이미지를 포기할 수는 없으니까요. 요즘은 특히 모바일 기기도 해상도가 굉장히 높으니 그에 맞춰 높은 해상도를 제공해주는 것도 좋지 않을까 생각합니다.

그러면 이제 진행해 봅시다.

만들어봅시다!

PDF로 내보내기

우선 위처럼 키노트를 만들어주세요. 그리고 아래 사진처럼 파일 > 다음으로 보내기 > PDF… 순서대로 클릭을 해주시면 됩니다.

클릭을 해주면 다음과 같은 창이 하나 뜹니다. 여기서 유의하셔야되는 점은 ‘이미지 품질’을 ‘최상’으로 해 두셔야 300ppi로 내보내기가 이루어집니다.

NOTE: 여러분이 글자만 있는 상태라면 (아이콘/사진이 없다면) 굳이 이미지 품질을 신경쓰지는 않으셔도 됩니다. PDF에서 글자는 폰트를 내장해 글자 정보 자체로 읽고 쓰기 때문에 이미지 품질로 인한 차이가 발생하지 않습니다.

미리보기로 PDF 열기

앞서 내보내기로 만든 PDF 파일을 아래처럼 열어줍시다. 보기만해도 높은 해상도인것을 느낄 수 있습니다.

미리보기로 PDF 열어본 모습

이제 상단 메뉴바 ‘파일’에서 ‘보내기’를 눌러봅시다.

미리보기에서 내보내기

이제 아래와 같은 화면이 보일텐데요, PDF가 아니라 JPEG로 바꿔준 뒤 해상도를 150정도로 맞춰준 뒤 저장을 해줍시다.

미리보기 내보내기에서 JPEG, 150PPI로 저장하기

끝났습니다!

이제 여러분은 여러분이 원하는 해상도의 고해상도 이미지를 얻게 되었습니다!

파일 사이즈를 조절하거나 무손실 압축 프로그램등을 사용한다면 좀 더 트래픽량이 줄어 사이트 로딩속도가 빨라질 수 있습니다.

macOS에서는 ImageOptim을 이용해보세요. 이미지가 단순한 경우 용량이 절반으로 줄어들기도 합니다.

PySpark & Hadoop: 1) Ubuntu 16.04에 설치하기

들어가며

Spark의 Python버전인 PySpark를 사용할 때 서버가 AWS EMR등으로 만들어진 클러스터가 존재하고, 우리가 만든 프로그램과 함수가 해당 클러스터 위에서 돌리기 위해서는 PySpark를 로컬이 아니라 원격 서버에 연결해 동작하도록 만들어야 합니다.

이번 글에서는 PySpark와 Hadoop을 설치하고 설정하는 과정으로 원격 EMR로 함수를 실행시켜봅니다.

Note: 이번 글은 Ubuntu 16.04 LTS, Python3.5(Ubuntu내장)를 기준으로 진행합니다.

AWS에서 EC2를 생성해 주세요. VPC는 기본으로 잡아주시면 됩니다. 성능은 t2-micro의 프리티어정도도 괜찮습니다. 무거운 연산은 나중에 다룰 AWS EMR 클러스터에 올려줄 것이기 때문에, 클라이언트 역할을 할 EC2 인스턴스는 저성능이어도 괜찮습니다.

들어가기 전에 우선 apt 업데이트부터 진행해 줍시다.

1
sudo apt-get update && sudo apt-get upgrade -y

PySpark 설치하기

Ubuntu 16 OS에는 기본적으로 Python3이 설치되어있습니다. 하지만 pip는 설치되어있지 않기 때문에 아래 명령어로 먼저 Python3의 pip를 설치해줍시다.

1
2
# pip/pip3을 사용가능하게 만듭니다.
sudo apt-get install python3-pip -y

설치가 완료되면 이제 Python3의 pip를 사용할 수 있습니다. 아래 명령어로 pip를 통해 PySpark를 설치해 봅시다.

1
2
# 최신 버전의 PySpark를 설치합니다.
pip3 install pyspark -U --no-cache

위 명령어에서 -U--upgrade의 약자로, 현재 설치가 되어있어도 최신버전으로 업그레이드 하는 것이고, --no-cache는 로컬에 pip 패키지의 캐싱 파일이 있더라도 pypi서버에서 다시 받아오겠다는 의미입니다.

현재 PySpark 2.2.0은 버전과 다르게 2.2.0.post0라는 버전으로 pypi에 올라가 있습니다. 이로인해 pip install pyspark 로 진행할 경우 Memeory Error가 발생하고 설치가 실패하므로, 2.2.0버전을 설치한다면 위 명령어로 설치를 진행해주세요.

Hadoop 설치하기

JAVA JDK 설치하기

Hadoop을 설치하기 위해서는 JAVA(JDK)가 먼저 설치되어야 합니다. 아래 명령어로 openjdk를 설치해주세요.

1
2
# Java 8을 설치합니다.
sudo apt-get install openjdk-8-jre -y

Hadoop Binary 설치하기

Hadoop은 Apache의 홈페이지에서 최신 릴리즈 링크에서 바이너리 파일의 링크를 가져옵시다.

원하는 Hadoop 버전의 Binary 링크를 클릭해 바이너리를 받을 수 있는 페이지로 들어갑시다. 글쓰는 시점에는 2.8.2가 최신 버전입니다. 링크를 타고 들어가면 아래와 같이 HTTP로 파일을 받을 수 있는 링크가 나옵니다.

글을 보는 시점에는 링크 주소는 다를 수 있지만, HTTP 링크 중 하나를 복사하고 진행하면 됩니다. 이 글에서는 네이버 서버의 미러를 이용합니다.

이제 다시 서버로 돌아가봅시다. 아래 명령어를 통해 wget으로 Hadoop Binary를 서버에 받아줍시다.

1
2
3
wget 여러분이_복사한_URL
# 예시
# wget http://mirror.navercorp.com/apache/hadoop/common/hadoop-2.8.2/hadoop-2.8.2.tar.gz

이제 압축을 풀어줍시다. 아래 명령어로 압축을 /usr/local에 풀어줍시다.

1
sudo tar zxvf ./hadoop-* -C /usr/local

압축을 풀면 /usr/local/hadoop-2.8.2라는 폴더가 생기지만 우리가 사용할때 버전이 붙어있으면 사용하기 귀찮으므로 이름을 /usr/local/hadoop으로 바꾸어줍시다.

1
sudo mv /usr/local/hadoop-* /usr/local/hadoop

이제 파일을 가져오고 설치는 완료되었지만, 실제로 Hadoop을 PySpark등에 붙여 사용하려면 PATH등록을 해줘야 합니다.

아래 명령어를 전체 복사-붙여넣기로 진행해 주세요.

1
2
3
4
5
6
7
8
echo "
export JAVA_HOME=$(readlink -f /usr/bin/java | sed "s:bin/java::")
export PATH=\$PATH:\$JAVA_HOME/bin
export HADOOP_HOME=/usr/local/hadoop
export PATH=\$PATH:\$HADOOP_HOME/bin
export HADOOP_CONF_DIR=\$HADOOP_HOME/etc/hadoop
export YARN_CONF_DIR=\$HADOOP_HOME/etc/hadoop
" >> ~/.bashrc

이제 ssh를 exit한 뒤 다시 서버에 ssh로 접속하신 후, 아래 명령어를 입력해 보세요. 아래 사진처럼 나오면 설치가 성공적으로 진행된 것이랍니다.

1
/usr/local/hadoop/bin/hadoop

끝이지만 끝이 아닌..

사실 PySpark와 Hadoop만을 사용하는 것은 큰 의미가 있는 상황은 아닙니다. AWS EMR와 같은 클러스터를 연결해 막대한 컴퓨팅 파워가 있는 서버에서 돌리는 목적이 Spark를 쓰는 이유입니다. 다음 글에서는 AWS EMR을 구동하고 우리가 방금 설정한 Ubuntu 서버에서 작업을 EMR로 보내는 내용을 다뤄봅니다.

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×