devops

SPA 자동 배포 (Git + Jenkins + AWS)

Git + Jenkins + Docker + S3 + CloudFront 를 이용해 간단한 React.js 기반 SPA를 자동 배포하는 시스템을 만들어 볼 것이다.

SPA는 static web hosting이 지원되는 어떤 플랫폼에 올려도 동작을 하기 때문에 다양한 배포 환경을 구성할 수 있다. Netlify 와 같이 SPA에 최적화된 CI/CD 및 호스팅을 제공하는 SaaS를 이용하거나, Google Firebase, Amazon S3 + CloudFront(CDN) 등의 static web hosting 이 지원되는 Cloud 기반 서비스를 이용하는 방법이 있는데, 이 글에서는 AWS S3 + CloudFront 방식을 진행 할 것이다.

AWS S3와 CloudFront 를 같이 사용하는 이유는 S3는 static web hosting 을 그리고 CloudFront 는 AWS Certificate Manager를 통해 생성한 SSL/TLS 무료 인증서를 설정해 HTTPS 환경을 구현할 수 있기 때문이다.

전체 과정을 요약 하면 다음과 같다.

  1. React 프로젝트 및 git 저장소 생성
    1. webpack 빌드 설정
  2. S3 버킷 및 CloudFront 설정
  3. 빌드 서버 설정
    1. Jenkins 설치
    2. 빌드 의존성 설치
    3. Jenkins 설정
      1. 빌드 스크립트 작성
  4. Github Webhook 과 Jenkins Build Trigger 연결

React 프로젝트 및 git 저장소 생성

먼저 배포를 할 샘플 SPA 앱을 만든다. 이 곳을 참조하여 만든 boilerplate를 이용해 가장 기본적인 수준의 React.js SPA 개발 환경을 생성한다.

배포 환경을 고려하여 webpack 설정을 바꾼다. webpack 설정은 공식 문서를 참조하였다. 기존의 webpack.config.js 파일 하나로 이루어진 설정 파일을 환경에 따라서 다음과 같이 나눈다.

  • webpack.common.js
  • webpack.prod.js
  • webpack.dev.js

공통적으로 적용되는 부분은 webpack.common.js 로 따로 뺐다. 그리고 webpack.prod.js 와 webpack.dev.js 에서 webpack-merge 를 이용해 공통 설정을 불러와 머지를 지시킨다. 중복된 설정이 줄어들어 코드가 간결하다.

clean-webpack-plugin 과 html-webpack-plugin 를 –save-dev 플래그로 설치한다

둘의 용도는 다음과 같다.

  • HtmlWebpackPlugin : webpack 빌드를 통해 생성된 bundle 을 로드할 html 파일을 생성 해준다. 파일명에 해시가 포함되어 빌드 시에 매번 파일명이 바뀌는 경우에 유용하다. 플러그인이 생성한 HTML 파일을 사용하거나, 템플릿 로더로 템플릿을 이용할 수 있다.
  • CleanWebpackPlugin : 매 빌드 시 output 디렉토리를 비워준다.

디버깅을 위한 inline-source-map 을 설치한다.

./dist/index.html 파일을 ./src/assets/index.html 로 이동 시킨 후 webpack.common.js 설정에서 템플릿으로 지정해준다.

package.json 의 scripts 항목에 start 명령을 변경하고, build 명령을 추가한다.

{ 
	"start": "webpack-dev-server --config ./webpack.dev.js",
	"build" : "webpack --config webpack.prod.js"
}

dist 디렉토리가 빌드에 의해 동적으로 생성 되는 파일을 담는 목적으로 바뀌었기 때문에 .gitignore 에 dist 를 추가하여 형상관리에서 제외를 시킨다.

여기 까지 진행된 상태에서 git 에 저장소를 생성하여 소스를 push 한다. 현재 프로젝트 디렉토리가 boilerplate 의 저장소와 연결되어 있으니 우선 .git 디릭토리를 제거하고 git 초기화를 한 뒤 새로 생성한 저장소를 추가한다.

# 저장소 제거
$ rm -rf .git

# git 초기화
$ git init
$ git add .
$ git commit -m 'Initial commit'

# 새로 생성한 저장소 추가
$ git remote add origin git@github.com:devnoff/myProject.git

# 푸시
$ git push origin master

이렇게 git 저장소에 올려진 소스코드는 Jenkins 빌드 설정에 의해 빌드 서버에서 사용되고, 빌드가 요청 되면 git 으로 부터 내려 받아져 빌드 명령에 의해 dependancy 설치 등의 과정을 거치고 빌드가 진행된다.

위에서 작성된 코드는 다음 주소에서 확인할 수 있다.

Github : https://github.com/devnoff/myProject

S3 버킷 및 CloudFront 설정

S3, Cloudfront, ACM, Route53 를 이용해 HTTPS website 호스팅을 하므로써 운영 환경을 구축한다. 방법은 이 글을 참조

빌드 서버에서 AWS CLI 를 이용해 S3 에 빌드된 파일을 업로드 하고 CloudFront CDN에 재배포하는 과정은 빌드 스크립트 작성 부분에서 설명하겠다.

빌드 서버 설정

Jenkins 설치

Jenkins 설정 전 까지 다음의 과정이 선행 되어야 한다.

  1. EC2 Instance 추가
  2. NginX 설치, SSL 설정
  3. Docker 설치 – 설치 방법
  4. Dockerized Jenkins 설치 – 설치 방법
  5. 빌드 의존성 설치 : 배포할 React.js 앱의 빌드에 필요한 node.js 와 webpack 을 Jenkins 컨테이너에 설치

결과적으로 필자가 설정한 환경에서 Jenkins 는 맨앞단에 NginX 두고 reverse proxy 설정을 통해 443 포트와 docker 컨테이너의 외부 포트인 8080 을 연결해 주고, docker 컨테이너의 내부 50000 포트를 통해 실행된다.

443 → nginx → 8080 → Docker → 50000 → Jenkins

Nginx 와 reverse proxy를 사용하는 이유는 HTTPS 를 활성화 하고 내부망과 외부망에 대한 접근 권한의 달리 구성하여 보안을 강화하기 위함이다. 참조

빌드 의존성 설치

개발 환경과 버전을 일치하여 SPA 빌드에 필요한 다음의 의존성을 설치한다. (설치 방법은 생략)

  • node.js
  • webpack

Jenkins 설정

새 프로젝트 추가를 추가 한다. item name 으로 workspace 디렉토리가 생성 되는 점에 유의 한다.

untitled

OK를 누르면 프로젝트 설정으로 넘어온다. Github webhook 을 통해 payload 를 받으려면 매개 변수 설정이 필요하다. General 탭에서 다음과 같이 String parameter 매개변수를 추가한다

untitled-1

다음으로 소스 코드 관리 탭으로 이동해서 Git을 선택 후 저장소의 주소를 입력한다.

untitled-2

사전에 Jeknins 컨테이너의 id_rsa 공개키가 github 프로젝트의 Deploy Key 등록하는 과정과 github.com 의 RSA 를 추가하는 과정이 필요하며, 아래와 같이 저장소 접근 방식을 HTTPS로 바꾸고 github 자격증명 정보를 추가하는 것으로 대체할 수 있다.

untitled-3

그리고 자격증명을 위한 인증 정보를 Credentials 항목의 Add 버튼을 눌러 추가한다.

untitled-4

빌드유발(Build Trigger) 탭으로 넘어온다. 여러가지 빌드 유발 옵션이 있는데, 그 중에서 ‘빌드를 원격으로 유발’ 항목을 선택 한다. 이 항목은 Github webhook 과 연동하기 위한 설정으로 다음 챕터인 ‘Github Webhook 과 Jenkins Build Tigger 연결’에서 관련 부가 설명을 하겠다

untitled-5

빌드 탭으로 이동 후 ‘Add build step’ 을 눌러서 ‘Execute Shell’을 선택한다. Shell 명령을 통해 빌드를 진행 할 것이다. 빌드 스크립트를 한줄 한줄씩 작성해본다.

untitled-6

빌드의 전체 과정은 다음과 같다.

  1. dependency 설치
  2. 빌드
  3. s3 버킷 비우기
  4. s3에 빌드 결과물 업로드
  5. cloudfront resource 무효화

우선 package.json 상에 추가/제거된 의존성을 설치하는 명령어를 작성한다. 유의할 점은 아래 보는 바와 같이 실행 파일의 fullpath 를 다 적어주어야 한다.

# Dependency for SPA
/var/jenkins_home/.nvm/versions/node/v10.16.0/bin/npm install

그리고 webpack 빌드 명령을 작성한다. webpack 빌드 명령은 앞서 npm script로 만들어 둔 것을 이용한다.

# Build
/var/jenkins_home/.nvm/versions/node/v10.16.0/bin/npm run build

다음으로 배포할 S3 버킷을 비우고 빌드된 파일을 업로드하는 스크립트를 작성한다. (AWS CLI를 사용하기 위해서는 ~/.aws/config 에 자격증명 정보를 넣어 두어야하는데, 이 과정에 대한 설명은 생략한다.)

# Truncate Bucket
/usr/local/bin/aws s3 rm s3://monospace.kr --recursive

# Upload Build Artifacts
/usr/local/bin/aws s3 cp /var/jenkins_home/workspace/monospace.kr\\ Test/dist s3://monospace.kr/  --recursive

다음으로 CloudFront 배포 스크립트를 작성한다.

CloudFront 에 배포 할때 각 지역 엣지에 업데이트 된 소스코드를 반영하는 방법으로 invalidation 을 이용해서 캐시를 갱신 시키거나 default root object 를 변경해서 새로운 버전을 가리키도록 하는 방법이 있다.

invalidation(무효화) 을 이용하면 빌드 설정이 비교적 단순해 지지만 할당된 무료 캐시 무효화 횟수를 초과하면 과금이 되는 단점이 있고, root object 를 변경하는 방법은 빌드 설정에서 index.html 파일의 이름에 해시 또는 버전 식별자를 추가 하여 생성된 root file (ex. index.a34jgkh454j.html)을 CloudFront 배포 항목의 Default Root Object 이용하도록 해야 하기 때문에 빌드 설정이 다소 복잡 해지지만 별도의 비용이 들지 않는다.

예제에서는 Invalidation 방법을 이용하였다.

# CloudFront Invalidation
/usr/local/bin/aws cloudfront create-invalidation --distribution-id E135Y3KFO54CPN \\
  --paths /index.html

완성된 스크립트는 다음과 같다

# Dependency for SPA
/var/jenkins_home/.nvm/versions/node/v10.16.0/bin/npm install

# Build 
/var/jenkins_home/.nvm/versions/node/v10.16.0/bin/npm run build

# Truncate Bucket
/usr/local/bin/aws s3 rm s3://monospace.kr --recursive

# Upload Build Artifacts
/usr/local/bin/aws s3 cp /var/jenkins_home/workspace/monospace.kr\\ Test/dist s3://monospace.kr/  --recursive

# CloudFront Invalidation
/usr/local/bin/aws cloudfront create-invalidation --distribution-id E135Y3KFO54CPN \\
  --paths /index.html

Github Webhook 과 Jenkins Build Trigger 연결

Jenkins 의 보안을 위해서는 CSRF(cross site request forgery) Protection 옵션을 화성화 해야하는데, 이 설정이 활성화 되어 있으면 github webhook 에서 jenkins 로 빌드 트리거를 실행 시 No valid crumb 에러가 발생한다. CSRF Protection 옵션을 끄더라도 Global Security 설정에서 Anonymous 에 대한 READ 권한이 주어져야 동작을 하게 되는데, 이 경우 외부에서 누구나 jenkins 를 열람할 수 있게 되므로 다른 방법이 필요하다.

사용자 추가 및 API 토큰 생성

원격으로 빌드를 실행하기 위한 사용자를 추가하고 사용자의 API 토큰을 이용하면 jenkins의 보안설정을 유지하면서 github webhook 을 받을 수 있다.

  1. 우선 사용자를 추가한다. 이 글에서는 builder 라는 이름을 썼다.
    a. Jenkins > Jenkins 관리 > Manage Users 로 이동 한다.
    b. ‘사용자 생성’ 메뉴에서 ‘builder’ 라는 계정명의 사용자를 생성한다.
    untitled-7
  1. 그리고 builder 의 권한을 설정한다.
    a. Jenkins > Jenkins 관리 > Configure Global Security 메뉴로 이동 한다.
    b. Authorization항목에서 Matrix-based security 선택한다
    c. ‘Add user or group’ 버튼을 눌러 아까 생성한 ‘builder’ 를 입력한다
    d. ‘builder’의 권한을 다음과 같이 설정한다
    > Overall : Read
    > Job : Workspace, Read, Build
    untitled-8

    결과적으로 이런 모습이 된다.
  1. API 토큰 생성
    a. 로그아웃을 한 뒤 ‘builder’ 계정으로 다시 로그인 한다.
    b. 대시보드 좌측 ‘사람’ 메뉴에서 ‘builder’를 선택 후 좌측 ‘설정’을 눌러 설정 화면으로 이동 한다.
    c. API Token 항목에서 ‘Add new Token’ 버튼을 눌러서 토큰 이름을 넣고 Generate 버튼으로 토큰을 생성한다.
    untitled-9

    토큰 이름은 아무 것이나 해도 상관없다. 토큰을 사용할 때는 토큰 이름이 아닌 계정명을 함께 사용한다.
  1. 빌드 트리거 설정
    대시보드에서 프로젝트를 선택하고 ‘구성’ 메뉴로 들어간다. 그리고 ‘빌드 유발’ 항목에서 ‘빌드를 원격으로 유발’은 이미 선택해 두어서 활성화 되어있다.
    untitled-10

예제 에서는 helloworld 라고 토큰을 입력하였다. 실제 사용 시에는 사람이 읽을 수 없는 해싱된 문자열을 사용하는 것이 좋다. 아래에 예시에서 보는 것 처럼 URL을 통하여 빌드를 유발시킬 수 있다. 필자가 테스트로 구현 중인 jenkins URL을 기준으로 다음과 같이 되겠다.

<http://ci.monospace.kr/job/monospace.kr%20Test/build?token=helloworld>

하지만 이대로 URL을 호출하면 앞서 언급한 것과 같이 No valid crumb 에러가 뜬다. 이대로는 github webhook payload 설정에 넣을 수 없다.

생성한 계정과 토큰의 사용

이제 앞에서 만든 builder 계정과 API 토큰을 이용할 차례다. Jenkins Host 주소 앞에 :@ 형식으로 인증 정보를 전달하면 정상적으로 빌드가 되는 것을 확인할 수 있다.

https://builder:114bf74b38dde25c8bc7db803785cdf432@ci.monospace.kr/job/monospace.kr> Test/buildWithParameters?token=helloworld

Github webhook 설정

Github 프로젝트의 설정 페이지에 Webhooks 메뉴로 들어가서 ‘Add webhook’ 버튼을 눌러 웹훅을 생성 페이지로 이동 한다.

untitled-11

앞서 생성한 빌드 트리거를 Payload URL 에 넣는다. 아래에 설정 중에 글에는 다루지 않았지만 필자가 Jenkins를 구동 중인 서버는 NginX에 SSL 인증서를 설치하여 HTTPS 활성화 해두었기 때문에 SSL verification 을 사용하는 것으로 설정 하면 되지만, HTTPS를 지원하지 않는다면 Disabled 로 해두어야 동작한다. (물론 실제 운영 환경이라면 반드시 SSL verification 을 활성화 해야한다.)

그 아래로 어떤 이벤트로 웹 훅을 유발 시킬 것인가에 대한 설정이 있는데, 상황에 맞게 적절하게 설정하면 되겠다. 여기서는 푸시가 되었을 때 웹훅을 유발 하도록 설정하였다.

이제 아래 초록색 ‘Add webhook’ 버튼을 눌러 완료한다.

untitled-12

이제 모든 단계가 완료되었다. 이제 로컬 개발 환경에서 SPA 소스코드를 수정한 뒤 push 를 해보자.

untitled-13
파일을 수정하고
untitled-14
커밋 후 푸시
untitled-15
Github Webhook 이 정상 실행 되었고
untitled-16
Jenkins 에서도 빌드가 실행 되었다.
untitled-17
빌드 넘버로 들어가서 Console Outout 확인
untitled-18
성공!

 

서버

AWS Auto Scaling, MySQL Read replication

예상하지 못한 트래픽의 폭주와 최적의 성능 및 비용을 고려한 서버 구성, 그리고 각 파트의 세팅 과정에 대해 기록하였습니다. 계속 업데이트 되는 문서 입니다.


 

머릿말,

웹사이트의 게시글이 소셜네트워크 서비스에서 바이럴이 되면서 예상치 못한 과도한 트래픽이 발생하였다. 우리 서비스에게는 좋은 일 이지만, 이러한 상황에 대비가 되지 않은 인프라 관리자에게는 재앙과 같은 일이다.

당시 서버 구성은 과도한 트래픽에 대비해서 Route53 을 통해 DNS단에서 두 대의 서버로 강제로 트래픽을 분산 시키는 구성을 가지고 있었다. 나름 서버 한대의 가용 범위를 초과하더라도 감당할 수 있도록 Test 서버의 자원의 일부를 Production deployment 용으로 할당하는 비교적 쉬운 방법으로 대비를 했었는데, 예측을 완전히 벗어나는 엄청난 트래픽이 발생했던 것이다.

부랴부랴 Production 서버의 snapshot 을 생성하여 EC2 Instance 를 추가하고 Route53 로 트래픽을 강제로 할당하여 상황에 대응하였다. 당시에는 EC2의 Load balancer 및 Auto-scaling 을 사용해 본 적이 없어서 빠르게 적용할 수 있는 위의 방법을 사용하였다.

Instance 가 추가 되었음에도 Route53 health checks 상태가 100%로 복구 되지는 않았다.  그러나 웹사이트의 접속 자체가 불가능한게 아니라서 추가적인 증설은 진행하지 않고 두었는데, 지금 와서 보면 이는 잘 못 된 판단이다. DNS health check 에서 가용율이 100% 가 아니라는 것은 분명 어딘가에서  트래픽을 잃고 있다는 뜻이기 때문이다.

트래픽을 잃고 있는 곳은 health check 을 통한  load balancing 이 이루어 지고 있는 부분인데, 30초 마다 health check 을 하여3번 연속 접속실패를 할경우 서버의 이용이 불가능하다고 판단하고 트래픽을 내부에서 설정된 다른 서버로 라우팅 시킨다. 이때 보통 15대의 health checker 가 상태 체크를 하는데 2초에 한번꼴로 체킹이 이루어진다는 뜻이다. 그렇다면 약 2초*3회 / 분산서버대수 만큼의 시간동안 트래픽을 잃게 된다. 서버가 3대라면 2초의 시간동안 트래픽을 잃게되는데 적은 시간 같지만 트래픽이 증가하여 서버가 불안정할 경우 결코 적지 않은 시간 된다. health check  설정을 10초에 단위로 줄일 수도 있는데, 이 경우 1초에 1~2회 정도로 빈번히 체크를 하게 되며 30초단위로(월 0.5불) 체크하는 것에 비해 크지 않은 비용(월 1불)으로 이용할 수 있다.

아무튼 위의 경험을 통해서 비지니스 상황에 유연하고 신속하게 대응할 수 있는 서버구성을 구축하고자한다.

웹사이트의 최적화를 통해 서버의 효율을 증대시켜 서버의 가용성을 높일 수가 있기 때문에 인프라적인 대비에 앞에서 선행하고자 검토 했었는데, 이를 통해 얻게 될 서버의 가용성 범위가 물리적 증축에 비해 그리 크지 않고 비지니스의 상황이 더 나은 안정성을 요구함에 따라 인프라적인 대비를 우선 진행 하도록 하였다.

 

현 서버 구성,

  • AWS EC2 Instance(m3.medium $0.098/h) * 2  : Global distribution
    • www01(Production) : WP, RestAPI, MariaDB
    • dev01(Development, Production) : WP (Mirror), WP (Dev), MariaDB (Dev)
  • Aliyun ECS Instance(ecs.n1.tiny, ecs.t1.small) * 2  : China distribution
  • AWS Route53
    • Geolocation based routing – GL, CN
    • Weighted routing – Load balancing

 

변경 후 서버 구성,

중국쪽 서비스를 무기한 연기함에 따라 해당지역에 설정된 리소스를 모두 제거한다. 하나의 인스턴스 내에 구동되던 Database 및 웹 서버를 각각 저 사양의 Instance로 나누어 로드 밸런싱이 필요한 상황을 최소화 한다. Auto-scaling, ElastiCache, MySQL read replication 등의 기술을 적용하여 성능 개선 및 가용성, 탄력성 증대를 얻는다.

  • AWS EC2 Instance(t2.small $0.04/h) *2
    • static01 : MariaDB (Master), RestAPI, WP
    • elastic01 : MariaDB (Read replica), WP
  • EC2 Elastic load balancer
  • AWS ElastiCache (Cache server)
  • EC2 Auto scaling
  • AWS Route53
    • Weighted routing

 

변경 작업,

  1. WP에 HyperDB 플러그인 설치하여 Master/Slave 의 Read/Write 설정
    • Master :
      • read hostname – master01
      • write hostname – master01
    • Slave :
      • read hostname – slave01
      • write hostname – master01
  2. Instance initialize script 작성
    • Pull latest source from repository
    • Get database dump and setup slave
    • Update hostname alias of hosts file
      • xxx.xxx.xxx.xxx master01
      • yyy.yyy.yyy.yyy slave01

 

 

1. Instance Setup – static01

 

Launch new EC2 Instance : Ubuntu 14.04 LTS HVM

EC2 콘솔에서 인스턴스를 추가 한다. t2.micro로 생성하였는데, 추후에 실서버로 돌리때는 t2.small로 업그레이드 할 예정이다.

 

Install EasyEngine

Nginx + PHP(HHVM) + MariaDB + WordPress 를 간편하게 설치해줌과 동시에 최적의 세팅을 도와주는 EasyEngine 을 설치한다.

# install easyengine
wget -qO ee rt.cx/ee && sudo bash ee

 

Create a site with EasyEngine

EasyEngine을 통해 웹사이트를 설치한다. 대부분의 ee 의 기본 설정값(WordPress, DB)들은 추후에 별도로 덮어씌어지므로 변경하지 않는다.

# create site on example.com with hhvm
ee site create example.com --wp --hhvm

 

 

Move MySQL binary log to separate EBS volume

MySQL 로그의 증가로 인해 인스턴스의 용량이 초과되어 서비스가 중단되는 것을 막기 위해 MySQL  바이너리 로그를 별도의 EBS volume에 저장하도록 설정한다. 바이너리 로그 파일은 replication 및 복원용으로만 이용되므로 비용대비 용량이 매우 제한적인 고성능 SSD volume 에 저장해둘 필요가 없다. 대신에 비용이 저렴하여 용량제한에서 매우 자유로운 Cold HDD 를 이용한다.

참조:
Wednesday, June 27, 2012 MySQL with Replication and storage on separate EBS Volume
인스턴스에서 Amazon EBS 볼륨 분리

 

Local Memcached vs ElastiCache Memcached vs No cache comparison

 

Auto-scaling 및 load balancing 을 적용하기 전에 하나의 EC2 인스턴스의 가용성 테스트를 진행하였다. 이 테스트를 통해 확인하고자 하는 것은 인스턴스 하나가 가진 성능을 최대한으로 이용할 수 있는 최적의 세팅을 찾는 것이다.  테스트는 1분 동안 가상의 트래픽을 발생시켜 트래픽량에 따른 응답시간을 측정하는 식으로 진행하였다.

테스트를 진행한 세팅은 다음과 같다.

  • 1 EC2 Instance with no cache
  • 1 EC2 Instance with local cache (Memcached localhost)
  • 1 EC2 Instance with cloud cache (Memcached on 1 ElastiCache Node)

테스트를 진행한 페이지는 다음과 같다.

  • Landing page
  • City page
  • Article page

테스트를 진행한 트래픽 상황은 다음과 같다.

  • 1 user/sec
  • 3 users/sec
  • 5 users/sec
  • 6 users/sec
  • 10 ~ 100 users/sec

결론을 먼저 말하자면, 캐시를 무조건 사용해야 하며 멀티서버 환경에서 캐시의 동기화를 위해 클라우드 캐시를 이용하는 것이 최적의 세팅이다.

캐시를 사용하지 않은 테스트에서 초당 유저수를 늘릴 수록 그에 비례하여 서버의 응답속도도 늘어났으며 6 users/sec 의 설정으로 트래픽을 발생 시켰을때 데이터베이스 서버가 다운되어 버렸다. 평균 응답 속도는  다음과 같다.

  • 1 user/sec — 1000ms
  • 3 users/sec — 3500ms
  • 5 users/sec — 7000ms
  • 6 users/sec — time out

이에 반해 캐시를 사용했을 때는 초당 2명 이상의 트래픽 환경에서는 캐시를 사용하지 않을때 보다 월등한 성능을 보였다. 최초 응답속도는 트래픽에 따라서 큰 차이 없이 느린편이었다. 이는 캐싱하는데 시간이 걸리기 때문인데, 캐시가 되고난 이후 부터는 아주 빠른 속도를 보였다. 평균 응답속도는 다음과 같다.

  • 1 user/sec — 560ms
  • 5 users/sec — 510ms
  • 20 users/sec — 400ms
  • 100 users/sec — 1600ms
  • 200 users/sec — 3800ms

눈에 띠는 점은 초당 200 명의 트래픽을 발생시켜도 서버가 죽지 않는 다는 것이다. 게다가 CloudWatch 상에 5분간의 평균  CPU Utilization 값이 최대 13% 정도로 1분간의 값을 환산 했을때 65% 정도로 안정적인 수치를 보여주었다. 캐시는 로컬호스트에 설치한 Memcached와 ElastiCache의 것을 이용하는 두가지 방법을 이용하였다. 두 세팅모두 비슷한 결과를 보여주었지만, 로컬 인스턴스의 캐시를 이용할 경우 탄력적인 멀티서버환경에서 캐시간 동기화를 세팅하기가 매우 번거롭기 때문에 클라우드 캐시를 사용하기로 결정했다.

참조:
memcache
ElastiCache in WordPress

 

 

 

 

 

서버

Replication Setting

트래블로그 DB Replication 설정

Master

  • AWS EC2 SG
  • IP : 54.251.45.254
  • Port : 3306

Slave

  • Aliyun ECS Shanghai
  • IP : 139.196.203.14
  • Port : 3306

 

1. 마스터에서 로그 파일 및 포지션 가져오기


MariaDB [(none)]> FLUSH TABLES WITH READ LOCK;

MariaDB [(none)]> SHOW MASTER STATUS;

2. 슬레이브에서 마스터 설정


CHANGE MASTER TO MASTER_HOST='54.251.45.254',
MASTER_USER='repliUser',
MASTER_PASSWORD='YourPassword!',
MASTER_PORT=3306,
MASTER_LOG_FILE='mariadb-bin.000037',
MASTER_LOG_POS=13533875,
MASTER_CONNECT_RETRY=10;

3. 마스터 언락


MariaDB [(none)]> UNLOCK TABLES;

 

서버

Aliyun Ubuntu LEMP HHVM Setup

Aliyun ECS 인스턴스를 생성 후 LEMP + HHVM 을 세팅하기 까지 과정

1. 사용자 추가

쉘에 접속 후 root 계정을 대신해 사용할 수 있는 관리자 계정을 생성한다.


root@iZ11ngunt9tZ:/# adduser devnoff
Adding user `devnoff' ...
Adding new group `devnoff' (1000) ...
Adding new user `devnoff' (1000) with group `devnoff' ...
Enter new UNIX password:
Retype new UNIX password:
passwd: password updated successfully
Changing the user information for devnoff
Enter the new value, or press ENTER for the default
	Full Name []: devnoff
	Room Number []:
	Work Phone []:
	Home Phone []:
	Other []:
Is the information correct? [Y/n] Y

초기화 된 우분투에서 adduser를 하였는데, locale 설정 에러가 떴다.
이문제는 다음 코드로 해결

export LANGUAGE=en_US.UTF-8
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
locale-gen en_US.UTF-8
dpkg-reconfigure locales

2. 추가한 사용자에게 sudo 권한 부여

  • /etc/sudoers 쓰기 권한 부여: # chmod u+w /etc/sudoers
  • root 계정 아래에 ‘devnoff ALL=(ALL:ALL) ALL’ 추가
  • /etc/sudoers 권한 돌려 놓기: # chmod u-w /etc/sudoers

3. NginX + MariaDB + PHP 설치

Nginx

  • sudo apt-get update
  • sudo apt-get install nginx

MariaDB

10.1 버전 설치를 위한 리포지 토리 추가

sudo apt-get install software-properties-common
sudo apt-key adv --recv-keys --keyserver hkp://keyserver.ubuntu.com:80 0xcbcb082a1bb943db
sudo add-apt-repository 'deb [arch=amd64,i386] http://mirrors.opencas.cn/mariadb/repo/10.1/ubuntu trusty main'

설치

sudo apt-get update
sudo apt-get install mariadb-server

시스템 시작시 실행하도록

sudo update-rc.d mysql defaults

PHP

5.6 버전 설치 설정

sudo add-apt-repository ppa:ondrej/php5-5.6
sudo apt-get update
sudo apt-get install php5 php5-fpm php5-mysql

보안을 위한 php.ini 변경

sudo nano /etc/php5/fpm/php.ini
cgi.fix_pathinfo=0

php5-fpm 재시동

sudo service php5-fpm restart

NginX 설정

server {
    client_max_body_size 20M;
    listen 80;
    server_name localhost;
    root /home/devnoff/www;
    index index.php;

    location = /favicon.ico {
            log_not_found off;
            access_log off;
    }

    location = /robots.txt {
            allow all;
            log_not_found off;
            access_log off;
    }

    location / {
            # This is cool because no php is touched for static content.
            # include the &amp;amp;amp;quot;?$args&amp;amp;amp;quot; part so non-default permalinks doesn't break when using query string
            try_files $uri $uri/ /index.php?$args;
    }

    error_log /home/devnoff/travelog_wp/error.log error;

    location ~ \.php$ {
            fastcgi_pass   unix:/var/run/php5-fpm.sock;
            fastcgi_index   index.php;
            fastcgi_split_path_info ^(.+\.php)(.*)$;
            fastcgi_param   SCRIPT_FILENAME $document_root$fastcgi_script_name;
            include         fastcgi_params;
            fastcgi_read_timeout 600;
    }
    location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
            expires max;
            log_not_found off;
    }
}

NginX 재시동

sudo service nginx restart

HHVM 설치

wget -O - http://mirrors.noc.im/hhvm/conf/hhvm.gpg.key | sudo apt-key add -
echo deb http://mirrors.noc.im/hhvm/ubuntu trusty main | sudo tee /etc/apt/sources.list.d/hhvm.list
sudo apt-get update
sudo apt-get install hhvm

NginX PHP 설정 주석 처리

#    location ~ \.php$ {
#            fastcgi_pass   unix:/var/run/php5-fpm.sock;
#            fastcgi_index   index.php;
#            fastcgi_split_path_info ^(.+\.php)(.*)$;
#            fastcgi_param   SCRIPT_FILENAME $document_root$fastcgi_script_name;
#            include         fastcgi_params;
#            fastcgi_read_timeout 600;
#    }

스크립트 실행

sudo /usr/share/hhvm/install_fastcgi.sh

mysql 튜너
http://mysqltuner.com/

서버

우분투 리눅스: 사용자를 그룹에 추가하기

[번역글 입니다]

어떻게 우분투 리눅스 운영체제에서 CLI를 이용하여 유저를 그룹에 추가할 수 있을까요?

다음의 명령을 사용할 필요가 있습니다:

[a] useradd 명령 – 새 유저를 만들거나 유저의 기본 정보를 변경하거나 새유저를 2차 그룹(secondary group)에 추가함

[b] usermod 명령 – 시스템 계정을 수정 및 기존 사용자 계정을 변경함

첫째, root 사용자로 로그인

당신은 반드시 root 사용자로 로그인 해야합니다. 명령줄에서 ‘su -‘ 와 root 비밀번호를 치는  것으로 root 사용자로 전환할 수 있습니다. 그러나 우분투 리눅스 아래에서 root유저로 전활하기 위해 sudo 명령어를 사용하는 것을 추천합니다.

su -

OR

sudo -s

OR

sudo useradd ...

우분투 리눅스: 새 사용자를 2차 그룹(secondary group)에 추가

다음 문법을 사용하세요:

useradd -G Group-name Username
passwd Username

foo 라고 불리는 그룹을 생성하고 tom 이라는 사용자를 2차그룹 foo에 추가하세요.
$ sudo groupadd foo
$ sudo useradd -G foo tom

또는
# groupadd foo
# useradd -G foo tom

세팅을 확인하세요:

id tom
groups tom

마침내, tom 사용자를 위해 비밀번호를 설정 하세요 :
$ sudo passwd tom
또는
# passwd tom
당신은 사용자 tom 을 복수의 그룹 – foo, bar 그리고 ftp -에 추가할 수 있습니다 :
# useradd -G foo,bar,ftp tom

우분투 리눅스: 새 사용자를 1차 그룹(primary group)에 추가

사용자 tom을 www 그룹에 추가하기 위해 다음명령어를 사용하세요:

useradd -g www tom
id tom
groups tom

우분투 리눅스: 기존 사용자를 기존 그룹에 추가

-a 옵션을 사용한 usermod 명령으로 기존 유저 jerry를 ftp(보조 및 2차) 그룹에 추가하기 -즉, 유저를 보조 그룹(들)에 추가하기. 오직 -G 옵션을 사용하세요.

usermod -a -G ftp jerry
id jerry

기존 사용자 jerry의 1차 그룹(primary group)을 www 그룹으로 변경 (-g 옵션을 쓰세요):

usermod -g www jerry

 

원문: http://www.cyberciti.biz/faq/ubuntu-add-user-to-group/

서버

hosts

실제 운영중인 서비스에 웹서버와 데이터베이스 서버가 원격으로 연결되어 있고 개발은 클론으로 로컬에 두 서버를 구현해서 한다면, 실 서버에 변경사항을 반영할때 설정파일의 hostname 을 무시하고 반영하거나 매번 이름을 변경해주어야 한다.

좀 더 쉽게 얘기를 하면 웹서버와 데이터베이스가 같은 호스트내에 있다면 호스트네이임은 보통 localhost 또는 127.0.0.1 일 것이고, db 접속 설정의 hostname에 이를 사용하는데, 만약 다를 경우 db의 hostname 은 234.231.xxx.xxx 등으로 달리 설정되므로 설정파일의 변경사항을 반영할때 주의를 해야한다.

이 설정을 조금 더 안전하게 하기 위해 hosts 파일에서 hostname 의 별칭을 지정하여 사용할 수 있다. Unix/Linux 계열의 시스템에서 파일의 경로는 /etc/hosts 이다.

설정 예시

127.0.0.1                  localhost db01

127.0.0.1                  nickname2

234.240.xxx.xxx    db02

127.0.0.1 의 IP 주소에 대해서 localhost, db01, nickname2 라는 별칭을 지정하였다.

234.240.xxx.xxx 의 원격 IP주소에 대해서는 db02 라는 별칭을 지정하였다.

 

아래는 Codeigniter 로 구현된 웹서버의 db 설정이다

$db[‘default’][‘hostname’] = ‘db01’;
$db[‘default’][‘username’] = ‘root’;
$db[‘default’][‘password’] = ‘root’;

$db[‘eanprod’][‘hostname’] = ‘db02’;
$db[‘eanprod’][‘username’] = ‘user’;
$db[‘eanprod’][‘password’] = ‘password’;

이렇게 함으로써 로컬 개발 서버에서 작업하던 것을 실서버로 반영할때 설정파일을 무시한다거나 매번 변경하는 작업을 피할 수 있다.

 

참조: http://en.wikipedia.org/wiki/Hosts_(file)

개발일지

Usage Analysis 3 Days

3일 동안 앱업데이트 후 지난 한달간의 앱 이용형태를 추적한 데이터를 바탕으로 이용 형태에 대한 분석을 진행하였다.

첫날.

Flurry 에서 4일치의 Event log 를 긁어 와서, 이벤트가 가장 많이 발생한 30명의 Top User를 추출하였다. 이를 바탕으로 앱을 많이 이용하는 사용자는 이용 성향이 뚜렷하게 구분되는 편인데, 디스커버에서 시간을 보내거나 자신들의 컨텐츠만을 보거나 작성하는 행태를 보였다. 디스커버에서 시간을 보낸 Top User 중 절반은 자신의 컨텐츠가 거의 없었고 , 최근 1달 이내 가입한 이용자였다.

둘째날.

Flurry 에서 한달치 Event Log를 긇어왔다. Flurry에서 이를 한번에 받을 수 있는 방법을 제공하지 않아서, 반자동으로 작동하는 코드를 짜서 약 두시간 가량 받았다. 전체 1만번 정도의 세션이 만들어졌으며, 약 1천명 정도의 사용자가 앱을 실행하였다. 다시 말해 지난 한달간 천여명의 상용자가 한명 당 열번 정도 앱을 실행하였는데, 이 중 30%는 그 기간동안 가입한 이용자이다.

셋째날.

데이터를 입체적으로 분석하기 위해서, 각 화면 및 이벤트마다  네 가지 사용성(Discover, Search, Write, Lookback)에 대한 점수를 매겼다. 그리고 사용자 별로 네 가지 사용성에 대한 성향을 수치화 시켰다. 이를 다시 이용량 순으로 정렬하여 지난 한달 간 이용자의 이용 행태를 한 눈에 볼 수 있는 자료로 만들었다.

 

 

Flurry Event Log -> Usage Trend

1. 사용자별 전체 이벤트 발생 횟수를 합한 결과값 순으로 사용자의 각 이벤트의 합계.

2. 이벤트별 사용성 점수에 따른 사용성 요약

3. 이벤트가 많이 발생한 순으로 사용자 컨텐츠 통계와 사용성 요약 보기

 

Flurry Event Log -> Using Pattern

 

 

개발일지

Facebook Graph API Day 1

Spotsetter 라는 앱이 페이스북 컨텐츠를 가져와서 서비스에 이용하는 방식에 대해 분석한다. 페이스북을 통해 트래블로그에 가입한 유저에 대해서 아래의 조건에 대한 컨텐츠를 가져오는 방법을 알아보았다.

  1. 유저가 페이스북에서 작성한 게시글 중 플레이스 정보가 있는 것 가져오기
  2. 유저가 페이스북에서 체크인하거나 유저의 친구의 채크인(또는 상태)에 태그가 된 것 가져오기
  3. 유저의 페이스북 친구들이 체크인 하거나 그들의 친구들의 체크인(또는 상태)에 태그가 된 것 가져오기
  4. 특정 Region 에 속한 게시글(본인 및 친구가 작성하거나 태그된) 가져오기
  5. 특정 Place의 게시글(본인 및 친구가 작성하거나 태그된) 가져오기
  6. 유저가 like한 컨텐츠 가져오기(유저 성향 분석)

조건에 따라 FQL(Facebook Query Language) 과 Graph API 중 쿼리하기 용이한 것을 이용하였다.

1. 유저가 페이스북에서 작성한 게시글 중 플레이스 정보가 있는 것 가져오기

이에 대한 쿼리문을 작성하면서 테이블 컬럼의 네이밍만으로 각 테이블간 관계를 파악하기가 조금 어려웠다. 엔티티들의 특성을 고려하여 관계를 추정하여 아래의 쿼리를 통해 위의 조건에 해당하는 결과를 얻을 수 있었다.

{“status”:”select place_id, status_id, message from status where status_id IN (select id from location_post where author_uid = me())”,”checkin”:”select target_id, message from checkin where checkin_id IN (select id from location_post where author_uid = me())”}

Descriptions

  • location_post.id : ID of the object associated with this location
  • status.status_id : The ID of the status message
  • checkin.checkin_id : The ID of the check-in.
  • checkin.target_id : The ID of Event or Page the user is checking into.
  • place.page_id : The Facebook Page ID of the place

location_post 테이블의 id  는 위치정보가 있는 object 를 가리키고, checkin 테이블의 target_id 는 place테이블의 page_id를 가르킨다.

2. 유저가 페이스북에서 체크인하거나 유저의 친구의 채크인(또는 상태)에 태그가 된 것 가져오기

Graph API – “user_id/checkins”

3. 유저의 페이스북 친구들이 체크인 하거나 그들의 친구들의 체크인(또는 상태)에 태그가 된 것 가져오기

Graph API – “user_id?fields=friends.offset(0).limit(20).fields(checkins.fields(coordinates,place,from),name,picture)”

4. 특정 Region 에 속한 게시글(본인 및 친구가 작성하거나 태그된) 가져오기

Graph API 로는 좌표를 이용한 특정 Region 에 대한 위의 쿼리는 불가능 하다. FQL 을 이용하면 쿼리가 가능하긴 하나 ‘join’ 문을 사용할 수 없어서 한번에 데이터를 가져오는데 한계가 있다. 이 시점에 Spotsetter 가 어떻게 서비스를 구현해냈는지 추측할 수 있는데, 최초 페이스북과 연동 후 위의 2,3번 Graph API 를 이용해 사용자 데이터를 모두 가져와 캐싱을하여 자체 DB에서 쿼리를 한 것 일것이다. 캐싱없이 실시간으로 API 를 호출해서 데이터를 만들어 낼 수는 있지만, 원하는 형태의 데이터를 만들기 위해서 매우 비효율적인 과정을 거쳐야 하기때문에 이를 이용하지 않았을 것이며, 앱의 반응속도로 보아 캐싱을 한 것이 틀림 없어 보인다.

5. 특정 Place의 게시글(본인 및 친구가 작성하거나 태그된) 가져오기

4와 마찬가지로 구현

6. 유저가 like한 컨텐츠 가져오기(유저 성향 분석)

Graph API – “user_id/likes”

유저가 like한 페이지 목록을 가져온다


{
	"data": [
		{
			"category": "Tv show",
			"name": "쇼미더머니 2",
			"created_time": "2013-07-05T14:37:32+0000",
			"id": "138621366340709"
		},
		{
			"category": "Company",
			"name": "Lomography Embassy Store Seoul",
			"created_time": "2013-06-15T08:59:30+0000",
			"id": "180737175415514"
		}
	],
		"paging": {
		"next": "https://graph.facebook.com/xxxxxxx/likes?limit=2&offset=2&__after_id=180737175415514"
	}
}

References

개발일지

Bug Fixing, Deals Hong Kong Integration, Tracking User’s Behaviors

Tracking User’s Behaviors

비지니스 파트에서 다음 버전의 프로덕트에 대한 기획을 담당하면서, 설계과정에 필요한 자료를 요청해왔다. 앱 내에서 사용자가 이동하는 경로 및 발생하는 이벤트 그리고 페이지 내에서 머문 시간 등을 사용자의 종류(지역, 언어, 가입경로 등)에 따라 한 눈에 볼 수 있는 자료와 모든 사용자의 액션을 추적해달라고 하는게 요지였다. Flurry  데이터와 트래블로그 API 요청을 기록한 데이터를 종합하여 당장에 원하는 자료는 만들 수 있지만, 모든 액션 및 경로를 추척해달라는 요청에 대해서는 그 영역이매우 광범위하고, 모호하여 바로 진행할 수 없다는 입장을 전달했다.

이에 대해 팀원들간에 논쟁이 있었는데, 개발팀에서 데이터 분석을 전담해야하는 것이 아니냐하는 의견과, ‘모든’이라는 말은 불명확하기 때문에 분석이 필요한 영역에 대한 확실한 정의와 왜 그것을 해야하는지 알려주면 분석 자료를 만들어 주겠다는 의견, 그리고 다 함께 분석작업에 대해서 논의 한 뒤 결정하자는 의견이 대립되었다. 결과적으로 프로덕트 설계를 맡은 비지니스팀에서 분석이 필요한 영역과 자료에 대한 정의를 한 뒤 개발팀에게 가이드라인을 넘겨주는걸로 일단락 되었다.

Bug Fixing

페이스북 연결에 문제가 있다는 리포팅이 있었다. 확인 결과 페이스북 세팅을 유지하는 피처를 적용하면서, 페이스북 object 에 settings라는 프로퍼티를 추가하였는데, 이 프로퍼티가 존재하지 않는 이전버전에서 크래시되는게 원인이었다. not exist property exception 을 대부분의 object에 적용하였는데, 최근에 작업한 object에 그게 빠져있어서 이를 보완하는 코드를 삽입했다.

Deals Hong Kong Integration

파트너 업체의 deal을 앱에서 표시해주는 방안에 대해서 논의 했다. API 는 xml 형태로 보내어 질 것이고, 이 데이터를 이용해 앱에서 모바일 마크업을 이용해 보여 준 뒤 유저의 액션에 따라 사파리로 리다이렉트 시키는 방식으로 구현이 하는 것으로 결정 됐다.