마지막으로 직접 인증서를 발급하고 이를 개인용 사이트에 적용하는 방법을 상세히 알아보겠습니다. 

 

이 테스트는 CentOS 7.x , OpenSSL, Nginx에서 실행되었습니다.

 

인증서 발급을 위한 준비

 

인증서를 발급한 기록을 위해 두 가지 파일이 필요합니다. 하나는 발급된 인증서의 정보를 기록하는 것과 serial을 기록하는 파일, 이렇게 두 개의 파일을 준비해야 합니다. 

 

touch /etc/pki/CA/index.txt
echo '00' > /etc/pki/CA/serial

추후 인증서를 발급 후 두 파일을 보면 아래와 같습니다.

V    210109061528Z    00    unknown /C=KR/ST=Seoul/O=COMPANY/OU=DEV1/CN=your_domain/emailAddress=your_email@server.com

index.txt 파일에는 언제 어떤 정보를 포함한 인증서가 발급되었는지가 기록됩니다. 

01

serial 파일에는 00 에서 01로 한 개의 인증서가 발급되었다는 것을 확인할 수 있습니다. 

 

 

이제 CA 개인키를 생성합니다.

openssl genrsa -des3 -out ca.key 1024

그리고 CA의 CSR(Certificate Signing Request)을 생성합니다. csr 파일은 공개키와 사용하는 알고리즘 정보가 포함됩니다.

openssl req -new -key ca.key -out ca.csr

이제 Root CA의 인증서를 생성합니다. 

openssl x509 -req -days 1280 -in ca.csr -signkey ca.key -out ca.crt

이제 개인키에서 패스워드를 삭제하겠습니다. 사실 패스워드가 있는게 당연하겠지만, 웹 서버 가동때마다 계속 패스워드를 물어보게 할 수 없으니 일반적으로 삭제하고 사용합니다. 일반적으로 웹 서버들은 모두 패스워드 삭제를 '강요'합니다.

openssl rsa -in ca.key -out ca_key.pem

이로서 CA로서의 준비를 끝났습니다. 이제 웹서버를 위한 준비를 하겠습니다. 

 

웹 서버 인증서 만들기

웹 서버용 개인키를 생성합니다.

openssl genrsa -des3 -out server.key 1024

웹 서버용 CSR 을 생성합니다.

openssl req -new -key server.key -out server.csr

이제 준비가 되었으니 실제 인증서를 발급해 보겠습니다. 이 글은 개인용 인증서를 생성하고 적용하는 내용을 정리한 글입니다. 공인된 CA에 인증서 생성 요청은 필요하다면 추후 별도로 정리해 보겠습니다. (이미 수많은 글들이 있으니 쉽게 구글링으로 확인하실 수 있습니다.)

 

준비된 CA로 인증서를 제작합니다.

openssl x509 -req -in server.csr -out server.crt -signkey server.key -CA ca.crt -CAkey ca.key -CAcreateserial -days 365

웹 서버에 설정하기 위해 웹 서버용 개인키에서 패스워드를 삭제합니다. (안그러면 웹 서버 실행때마다 물어봅니다.)

openssl rsa -in server.key -out server_key.pem

Diffie-Hellman 키 생성하기 

(* Logjam 취약점이 이전에 발표되었으며, 이로 인해 1024bit는 안전하지 않습니다. 꼭 2048 이상으로 사용하시기 바랍니다. 상세 내용은 구글링으로... ^^a)

openssl dhparam -out dhparam.pem2048

 

이제 위에서 생성한 파일들을 /etc/nginx/ssl 폴더를 만들어 넣어두고 아래와 같이 상위 폴더의 보안 설정에 맞춰줍니다.

restorecon -v -R /etc/nginx

 

웹 서버 (NGINX ) 설정하기 

 

nginx 의 설정을 별도로 수정하지 않았다면 /etc/nginx/default.conf 파일을 수정하면 됩니다.

 

# 기존 http도 https로 리디렉션 되도록 설정.
server {	
	listen      80;
	listen      [::]:80;
	server_name your_domain; # ip 일때는 굳이 쓸 필요가 없다.

# 301 moved permanently 응답과 합께 모든 http 요청을 https로 리디렉션한다.
	location / {
		return 301 https://your_domain:443$request_uri;
	}
}

server {
	listen       443 ssl http2 default_server;
	listen      [::]:443 ssl http2 default_server;
	server_name  your_domain; # ip 일때는 굳이 쓸 필요가 없다.

	ssl_certificate /etc/nginx/ssl/ca.crt;
	ssl_certificate_key /etc/nginx/ssl/ca_key.pem;
	ssl_session_timeout 1d;
	ssl_session_cache shared:SSL:50m;
	ssl_session_tickets off;

	ssl_dhparam /etc/nginx/ssl/dhparam.pem;
	ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
	# Cipher Suites 에 대해서는 이 링크(https://rsec.kr/?p=455)를 참고하세요. 너무 잘 설명해 두셔서 감동이었습니다. -o-b
	ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384;
	ssl_ecdh_curve secp384r1;

	ssl_prefer_server_ciphers on;
	ssl_stapling off;	# for self-signed cert
	ssl_stapling_verify off; # for self-signed cert

	location ~ /\.ht {
		deny all;
	}
	.
	.
	.

 만약 443이 아닌 다른 임의의 포트를 사용한다면 semanage에 추가해야 합니다. (selinux를 사용하지 않는 상태라면 통과입니다.)

semanage port -a -t http_port_t -p tcp 4430  # 포트 추가하기
semanage port -l | grep http_port_t # 포트 적용되었는지 확인

firewall-cmd --permanent --zone=public --add-port=4430/tcp # firewall에 4430포트 오픈
firewall-cmd --reload # 변경 사항 적용하기

이제 웹서버를 재실행 해보시기 바랍니다. http로 접속하시면 바로 https로 포워딩 되는지도 잘 확인해보시고요.

 

도움이 되셨으면 합니다.

 

모두 새해 복 많이 받으시고 멋진 2020년 되시기 바랍니다.

 

이만  ~~ !

이번에는 우리가 알고 있는 공인인증서는 무슨 역할을 하고, 왜 인증된 기관에서 받아야 하는지 알아보겠습니다.

인증서(Certificate)는 무엇이며 왜 만들어졌는가?

 

인증서는 아래와 같은 내용이 포함되어 있습니다.

- 발행 장소

- 소유자 이름과 email 주소

- 인증서의 용도, 유효기간

- Common Name(CN)

- Public Key

- Hash

 

인증서에 포함된 Public Key는 발행 주체(예를들면 서비스를 제공하는 웹 사이트 등)의 PK이며, 이 정보들은 인증기관의 private key로 암호화 되었습니다. 브라우저들은 유명한 인증 기관의 Public Key를 이미 포함하고 있어 인증서를 받아 발행 주체의 Public Key를 확인할 수 있습니다. 

 

개인적인 생각으로는 사용자가 서비스를 제공하는 웹 사이트에 접속할 때 별도로 Public Key를 전달하면 될 것을 왜 인증서를 만들고, 인증 기관까지 만들어서 이럴까 하는 생각도 들긴 합니다. 하지만, 인증서를 공인된 인증 기관에 신청할 때 신청 주체가 해당 도메인의 주인이 맞는지, 어떤 도메인을 언제까지 사용하는지에 대한 정보를 등록, 확인하는 절차가 있기 때문에, 사용자가 해당 사이트에 접속했을 때 공인된 인증서가 있다면 그 사이트를 보다 더 신뢰하고 정보를 교환할 수 있을 겁니다.

 

인증기관의 역할과 작동원리

 

인증서 발급을 신청한 주체는 Root Certification Authority (이하 CA)라 부르는 인증서 발급 기관에 인증서 발급을 신청합니다. CA의 인증서는 웹브라우저에 기본으로 설치되어 있으며, 인증서 신청주체는 일정 비용을 지불하고 인증을 요청할 수 있습니다. 또한 Let's Encrypt (무료이며 자동화된 개방형 인증 기관) 같은 무료 인증 기관도 있습니다. 

 

이제 유저들은 어떤 사이트에 https로 접속할 때 인증서를 먼저 확인하게 됩니다. SSL 의 이전 흐름도에 인증 과정을 추가하면 아래와 같습니다.

(* 인증서를 활용한 데이터 교환 절차)

인증기관은 발급하는 역할도 하지만, 철회 인증서(Revoked Certificate)들도 관리합니다. Root CA 인증서들은 모두 해당 기관에서 자체 서명(Self-signed)되어 있으며. 이제 다음 글에서는 자체 서명된 인증서를 만들고 이를 개인용 사이트 혹은 다른 용도로 어떻게 사용할 수 있는지 알아보겠습니다.

 

 

 

 

 

 

회사 자체에서 사용되는 웹 사이트에 SSL을 도입할 일이 생겼는데, SSL 도입 자체가 처음이라 도입 과정의 내용을 정리해 보았습니다. Self Signed Certificate이라고 하는 용어도 이번에 첨 알게 되었네요. 

 

필요한 사전 내용부터 도입 과정, 예외상항에 대해서 순서대로 정리 보았으니 참고 되시길 바랍니다. 

(* 처음 해보는 거라 잘못된 정보, 오류가 있을 수 있습니다. ^^;;)

알고 가야 할 것들!

SSL(Secure Socket Layer)이 무엇인지는 너무 많은 글들이 있으니 여기서는 SSL 도입에 필요한 요점만 정리하겠습니다. 사실 TLS(Transport Layer Security)지만 SSL이라고 부르기 때문에 그대로 사용하겠습니다. 

 

HTTP와 HTTPS의 차이

HTTP는 평문(텍스트 그 자체!)으로 데이터를 전송합니다. 즉, 전송되는 데이터를 손쉽게 들여다 볼 수 있습니다. HTTPS는 Secure이 붙은 만큼 전송되는 데이터를 볼 수 있다 한들 암호화가 되어 있어서 무슨 내용인지 바로 알 수 없습니다.

 

그럼 서로 주고 받는 데이터를 암호화 한다면 복호화는 어떻게 할 수 있을까요? 보내는 곳에서 암호화한 방법을 알아야 받는 곳에서 복호화를 할 수 있을텐데?

 

세상에는 수많은 사이트가 존재합니다. 서로 데이터를 주고 받기 전에 암호화를 어떻게 할지 미리 알려줘야 하는데(대칭키 방식) 접속할 때마다 자기가 어떤 방법으로 암호화를 하는지 알려줘야 한다면, 이 방법을 알려주는 순간에 해커들이 이 정보를 훔쳐간다면 무용지물이 되겠죠. 그렇다고 사전에 암호화 하는 방법을 서로 미리 정해놓고 공유한다면 이 또한 안전하지도 않을 뿐더라, 암호화 방식이 바뀔 경우 대책이 없게 됩니다.

 

이를 보완하기 위해 개인키/공개키라는 개념이 나옵니다. 

 

Private Key(개인키)/Public Key(공개키)란?

말 그대로 개인키는 나만 가지고 있고, 공개키는 누구에게든 전달할 수 있는 키입니다. 공개키로 암호화된 데이터는 개인키로만 복호화 할 수 있습니다. 이 역도 성립합니다. 개인키로 암호화한 데이터는 공개키로 복호화 할 수 있습니다.

 

(*Private/Public Key 설명을 돕기 위한 그림으로 실제 위와 같이 데이터를 교환하지는 않습니다.)

그래서 공개키로 암호화된 데이터를 아무리 들여다 봐도 개인키를 모르는 이상 복호화 할 수 없습니다. ( 여기서 굳이 양자컴퓨터를 언급하지는 않겠습니다. ;; )

 

그런데 이 그림처럼 통신하기 위해서는 많은 연산이 필요합니다. 이를 해결하기 위해 핸드쉐이킹 단계 (클라이언트가 서버에 처음 연결하여 통신을 준비하는 단계) 에서 실제 데이터 전송을 위한 대칭키를 결정하는 단계에서만 사용됩니다. 이후부터 세션이 종료될 때까지 결정한 대칭키를 이용해 데이터를 교환하고, 세션이 종료될 때 사용한 대칭키는 파기하게 됩니다. 

 

(* 공개키 기법의 Hankshaing 과정)

 

분명히 간단히 정리하려고 했는데, 정리 하다보니 정도 모르는 것들이 또 나와서 정리하고 그림도 그리다보니... 

 

조금 길어졌네요.

 

다음 글에서는 우리가 알고 있는 공인인증서는 무슨 역할을 하고, 왜 인증된 기관에서 받아야 하는지 알아보겠습니다.

 

그 후 마지막에는 Self-signed Certificate 작성과 적용 방법에 대해서 정리하고 마무리 하겠습니다. 

 

비지니스 로직에서 logstash로 tcp를 바로 데이터를 전송한다면 아래와 같은 흐름으로 설계가 될 것입니다. 

 

logstash에 tcp로 직접 데이터 전송 시

 

이 경우 logstash 혹은 elasticsearch에 문제가 있을 경우 바로 데이터 손실이 발생할 수 있습니다. 

 

logstash와 elasticsearch의 문제 발생 혹은 점검 중 데이터 손실이 발생하지 않도록 전송할 데이터를 일시적으로 혹은 일정 기간 동안 보관했다가 logstash와 elasticsearch가 정상화 되었을 때 다시 전달할 수 있도록 아래와 같이 만들어 보겠습니다. 

 

개선된 데이터 전송 흐름

 

임시 저장소로는 Redis를 사용하고, 데이터 전송 흐름을 제어하기 위해 nodejs로 간단한 agent를 만들어 사용해 보겠습니다. 전달받은 로그는 모두 JSON 포멧의 텍스트로 가정합니다.

const http = require('http'); 
const redis = require('redis'); 
const Logstash = require('logstash-client'); 
const schedule = require('node-schedule'); 
const JSON = require('JSON'); 
const express = require('express'); 
const app = express(); 
const bodyParser = require('body-parser'); 
const cors = require('cors'); 

app.use(cors()); 
app.use(bodyParser.json()); 
app.use(bodyParser.urlencoded({ extended: true })); 

redis_cli = redis.createClient(6379,"127.0.0.1"); 
var logstash = new Logstash({  
	type: 'tcp',
	host: "your_logstash_ip",  
	port: "your_logstash_port"
}); 

const keyList = ["logstash"]; 
const hostConn = [logstash]; 
const maxCountForReading = 100; // Don't modify to -1 
var bWorking = false; 
var isEmpty = function(value){ 
	if( value == "" || value == null || value == undefined || ( value != null && typeof value == "object" && !Object.keys(value).length ) ){ 
    	return true 
    }else{ 
    	return false 
    } 
}; 
const waitFor = (ms) => new Promise(r => setTimeout(r, ms)); 
async function asyncForEach(array, callback) {  
	for (let index = 0; index < array.length; index++) {  
    	await callback(array[index], index, array)  
    } 
} 
Date.prototype.yyyymmdd = function() {
      return this.getUTCFullYear()  +
      "/" +  (this.getUTCMonth() + 1) +
      "/" +  this.getUTCDate();
};

const server = app.listen("port_be_received_from_your_app", ()=>{  
	console.log(" Start agent ! "); 
});

//	redis에서 데이터를 읽어 logstash로 전송합니다.
function rangeFromRedis(ls, key){  
	return new Promise( function (res, rej){  
    	redis_cli.lpop(key, function(err, data){  
        	if(isEmpty(data)){  
            	rej("null");  
            }else if(err){  
            	rej(err);  
            }else{  
            	try {
            		ls.send(JSON.parse(data)); // data는 json 포멧의 텍스트 파일로 가정합니다. 이를 json 데이터로 변환하여 logstash로 전달합니다.
                }catch(e){
                	//	record 'e' somewhere you want to write in 
                    console.log(" ERROR : ", e );
                    redis_cli.lpush(key,data);
                }finally{
                    res();
                }
            }  
        });  
    }); 
} 

//	전달받은 data를 redis에 기록합니다.
async function insertToRedis(key, element, expire_days){  
	return new Promise((resolve, reject) => {  
    	redis_cli.rpush(key, element, function(err, data){  
        	if(err)  
            	reject( err );  
            else{  
            	if(expire_days !== undefined)  
                	redis_cli.expireat(key, parseInt((+new Date/1000) + 86400*parseInt(expire_days))); // expire_days후 삭제되도록 유효기간을 설정합니다.
                resolve();  
            }  
        });  
    }); 
} 

app.use(function(req,res,next){  
	next(); 
}); 

//	기록할 데이터를 post로 your_address/put에 전달하면,
//	redis에 바로 전송할 데이터를 기록하고, 

app.route('/put').post(function(req,res,next){  
	req.accepts('application/json; charset=utf-8');  

	let buffer = Buffer.from(JSON.stringify(req.body));  
    insertToRedis( 'logstash', buffer.toString('utf8'));  
    var backupkey = 'logstash-' + new Date().yyyymmdd();  
    insertToRedis( backupkey, buffer.toString('utf8'), 7);  //  여분으로 데이터가 전송된 날짜를 키로하는 공간에 7일간 추가 보관합니다.
    res.json('{"result":"ok"}'); 
}); 

//	지정된 스케쥴에 upload 함수를 호출하면 redis에서 데이터를 읽어 
//	logstash로 전송을 시도하고 실패할 경우 다시 redis에 기록합니다.
async function upload(){  
	var loopIdx = 0;  
    await asyncForEach(keyList, async (key) => {  
    	var bConn = hostConn[loopIdx].connected;  
        if(bConn == false){  
        	console.log("failed to logstash connection!!");  
            await waitFor(1000);  
            if(++loopIdx >= keyList.length){  
            	bWorking = false;  
            }  
        }else{  
        	// find on redis with key 
            rangeFromRedis(hostConn[loopIdx], key).then( function(data){ 
            	if(++loopIdx >= keyList.length){  
                    bWorking = false;  
                }  
            }).catch(function(err){  
            	if(err != "null")  
                	console.log("upload error = " + err);  
                if(++loopIdx >= keyList.length){  
                	bWorking = false;  
                }  
            });  
        }  
    }); 
} 
async function doPeriodicWork(){  
    await upload();  
    bWorking = false; 
} 
var j = schedule.scheduleJob('* * * * * *', function(){  
	if(bWorking == true){  
    	return;  
    }  
    bWorking = true;  
    doPeriodicWork(); 
});

(* 위 코드는 샘플의 예외적인 종료 상황 및 각종 예외 상황에 대응하는 코드는 글의 길이 문제로 제외되어 있습니다.)

 

logstash의 pipeline은 아래와 같이 작성할 수 있습니다. 

input {
	tcp {
		codec => "json"
		port => 7020
	}
}

output {
	elasticsearch {
		hosts => ["your_elasticsearch_ip:9200"]
		index => "logstash-%{+YYYY.MM.DD}"
	}
}

 

이제 일어날 수 있는 여러 상황에 대해서 어떻게 작동하고 후속 처리를 해야 하는지 가정해 보겠습니다.

 

[정상 Flow]

- 로그를 전송하는 서비스들은 agent에서 제공하는 REST API인 /put 을 이용해서 전달 후 자기 할 일을 하면 됩니다.

- agent는 매 초마다 지정된 키에 기록된 데이터를 한 개 가져와 Logstash로 전송합니다. 

(한번에 여러 개의 데이터를 전송하려면 spop을 사용하시면 됩니다.)

 

[예외 상황]

1. logstash가 다운되거나 접속할 수 없을 경우

  >> agent가 Redis에서 pop한 데이터를 logstash로 전송을 시도할 때 에러가 발생.

       pop한 데이터를 다시 left push로 다음에 먼저 가져올 수 있게 삽입한다. 

       이를 logstash가 정상화 될 때까지 반복한다. 

 

2. Elasticsearch가 다운되거나 접속할 수 없을 경우

 >> logstash는 (데이터가 전달되는)이벤트가 발생할 경우 input, filter, output 플러그인을 거쳐 최종 output 과정이 완료 처리되어야 해당 이벤트가 완료되었다고 처리하고, 이 과정 중 문제가 발생하면 해당 이벤트를 메모리에 그대로 유지합니다. logstash 5.1 버전부터는 Persistant Queue(이하 PQ, 예외 상황을 대비하여 디스크에 이벤트를 기록)를 제공합니다.

 >> logstash의 이런 과정에도 불구하고 데이터 분실이 발생한 경우, 위 샘플 코드에서는 일자를 기준으로 인덱스를 생성하도록 되어 있으며, Redis에 7일간 해당 데이터를 기록하고 있습니다. 특정 일에 문제가 발생한 경우 Elasticsearch에서 특정 일의 데이터를 지우고 'logstash-yyyy/mm/dd' 로 기록된 key를 'logstash'키로 복사하여 다시 데이터를 전송하여 복구할 수 있습니다. 

 

3. Elasticsearch, Logstash 버전 업그레이드가 필요한 경우

 >> logstash를 잠시 종료해두시면 '예외 상황 1'의 상황이 되어 Redis에 데이터가 쌓이게 됩니다. 업그레이드 후 logstash가 정상화되면 데이터를 모두 장성적으로 전송됩니다.

 

4. agent가 다운된다면.. 

 >> agent는 로그를 받아 기록하고 주기적으로 전송하는 단순한 기능이라 거의 문제가 발생하지 않습니다. 만약 걱정된다면 예외상황에 대한 처리 및 로깅을 강화화고 pm2 를 사용하여 다운 될 경우 바로 재시작 되도록 해주시기 바랍니다. 

  

 

이상입니다. Redis와 NodeJS로 작성된 코드가 작동되어야 하지만 데이터 손실 여부를 덜어줄 수 있으니 꼭 활용해 보시기 바랍니다. :)

Collo를 좀 더 알기쉽게 전달하기 위해 개요 및 샘플들을 제작하여 SlideShare에 공유 중입니다. 

 

이 글에서도 링크를 추가하여 계속 업데이트 하도록 하겠습니다.

 

https://www.slideshare.net/winninghabit/collo-01-kr

 

Collo -01 , kr

Collo를 소개합니다! https://github.com/blackwitch/Collo

www.slideshare.net

https://www.slideshare.net/winninghabit/collo-02-kr

불러오는 중입니다...

 

'개발 이야기 > Collo' 카테고리의 다른 글

Collo - slideshare 링크 공유  (0) 2019.11.05
Collo - 실시간 마이그레이션 툴  (0) 2019.09.30

한동안 회사에서 진행한 Data Warehouse(이하 DW) 및 통계시스템 구축이 최근 완료되었습니다. 

 

그리고 데이타를 저장한 Database(이하 DB) 종류, 저장된 로그의 파일 포멧 그리고 서버 위치도 다른 데이터들을 한 곳으로 손쉽게 모으기 위해 만들었던 천 줄 내외의 Javascript 코드를 정리해서 Collo라는 이름으로 github에 며칠 전 공개하게 되었습니다.

 

Collo의 주 목표는 실시간으로 누적되는 데이터를 해당 시스템의 부하없이 DW로 가져오는 것이었으며, 마이그레이션에 대한 모든 기능을 포함한 솔루션이 아닌 손쉽게 수정, 조작 가능한 작은 유틸리티 제작을 목표로 한 프로젝트였습니다. 그리고 데이타를 가져오는 성능 보다는 안정성에 더 중점을 두어 제작하였습니다. 그리고 유지보수를 위해서도 언제든, 누구든 분석하기 쉽게 가능한 작은 코드 수를 유지하려고 노력했습니다. 혼자 개발을 했기 때문에 개발 시간 단축, 각 저장소에 대한 안정된 연결을 유지하기 위해 공개된 npm들을 최대한 활용하였습니다.

 

제작 중간부터는 사용 가능한 저장소를 하나 둘 늘리면서 내가 다뤄보지 않은 저장소에 대해서도 지원하면 좋겠다는 생각을 했고, 이 작은 프로젝트가 같은 고민과 과제를 안고 있는 누군가에게 도움이 되었으면 했습니다. 이때부터 오픈소스로 공개를 목표로 하게 되었습니다. 게다가 관리툴의 UI도 엉망이어서 누군가의 도움을 받길 원했습니다. (첫번째 목표였을지도... -_-a)

 

이 카테고리에는 Collo에 대한 활용 방법, 예시들을 올릴 예정입니다. 

 

https://github.com/blackwitch/Collo

 

 

 

 

'개발 이야기 > Collo' 카테고리의 다른 글

Collo - slideshare 링크 공유  (0) 2019.11.05
Collo - 실시간 마이그레이션 툴  (0) 2019.09.30

(* 이 글은 NodeJS 10.15.1 을 기준으로 작성되었습니다. )

한국은 GMT +9 를 기준으로 시간을 사용하며, 데이터를 다룰때도 일반적으로는 GMT +9 기준을 사용합니다. 단일 솔루션만 다룰 때는 거의 신경쓰지 않겠지만, 이기종 혹은 여러 솔루션을 한번에 컨트롤 할 때는 상당히 거슬리는 문제가 됩니다. 특히 월드 와이드 서비스를 하고 있다면요. 대부분의 DB에서 UTC 기준으로 값을 저장하기 때문에 큰 문제는 없지만, 간혹 일부 npm에서 datetime 자료형을 다룰 때 미묘한 차이가 있습니다.

node-mysql(구분을 위해 임시로 node-를 붙였습니다)의 경우, 저장된 datetime 값을 그대로 가져오기 때문에 전혀 문제가 없는데 반해, node-mssql은 useUTC옵션을 제대로 설정하지 않으면 잘못된 날짜 정보를 가져오게 되어 문제가 발생합니다. 

예를 들면 아래와 같습니다. 
(아래 테스트는 DB : MSSQL/2008R2, MySQL/MariaDB10.4, npm package version : node-mssql/4.2.0, node-mysql/2.16.0 하에서 진행되었습니다.)

MSSQL과 MySQL의 어떤 테이블의 datetime 컬럼에 "2019-01-01 00:00:00" 라는 값을 넣어보겠습니다. 이 값은 UTC 기준으로 값이 저장됩니다. string 형태로 출력해보면 "Mon, 31 Dec 2018 15:00:00 GMT", 실제 저장된 값은 "15462684000000"입니다.

이 값을 MSSQL의 management studio, MySQL는 prompt 혹은 HeidiSQL 모두에서 현지 시간으로 보입니다. 즉 "2019-01-01 00:00:00"으로 보이게 됩니다.

이제 node-mssql/node-mysql로 값을 읽어보면 아래와 같은 결과가 나옵니다.

2019-01-01 09:00:00  (MSSQL NPM으로 읽어온 경우) 
2019-01-01 00:00:00  (MySQL NPM으로 읽어온 경우) 

node-mssql으로 얻은 결과값이 이상한 것을 보실 수 있습니다. node-mssql에는 useUTC 옵션이 있습니다. default 갑이 true로 되어 있어, DB의 datetime이 어디를 기준으로 되어 있던 무조건 UTC 기준값으로 인지합니다. 그래서 최종 출력시에는 Asia/Seoul 의 시간대인 +9시간이 되어 출력된 것이죠. 

 

connection에 사용되는 config값에 useUTC 옵션값을 추가하고 값을 false로 설정하면 현지 시간값으로 읽어옵니다. 사용에 주의 하세요. 

 


이상입니다. 좋은 하루 되세요. :)





JavaScript의 Date 객체는 아래의 특징을 가지고 있습니다. 다른 언어에서도 크게 다르지 않습니다.

- Date 객체는 UTC, 1970년 1월 1일 0시를 기준으로 하며, 밀리세컨트로 시간값을 기록합니다. 
- 만약 입력된 값이 유효하지 않다면 NaN값이 반환됩니다.
- 월은 0부터 시작하며 11이 12월이 됩니다.
- 요일은 0부터 시작하며 0이 일요일, 6일 토요일이 됩니다. 
* 크로스 브라우징 문제가 일부 있습니다. 작성 전 이에 대한 문제를 검토해 보시기 바랍니다. (https://www.google.com/search?q=js+date+cross+browser+format) 보다 상세한 내용은 이 곳(https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Date)을 참고하세요.

아래 예제에서는 JavaScript Date 객체 사용법을 알아보겠습니다.

var date1 = new Date('2019-01-01T00:00:00.000Z'); 
var date2 = new Date('2019-01-01T00:00:00'); 

console.log( "RAW DATE = ",date1, " / ", date2);  
// 결과 (1) : RAW DATE = 2019-01-01T00:00:00.000Z / 2018-12-31T15:00:00.000Z 
console.log( "toString = ",date1.toString(), " / ", date2.toString());  
// 결과 (2) : toString = Tue Jan 01 2019 09:00:00 GMT+0900 (GMT+09:00) / Tue Jan 01 2019 00:00:00 GMT+0900 (GMT+09:00)  

 

date1에는 UTC(협정 세계시, Coordinated Universal Time)로, date2에는 현지 시간을 입력했습니다. Date는 입력된 값을 분석하여 UTC로 값을 보관합니다. 

이를 그대로 출력하면 "결과 (1)"과 같이 UTC 기준으로 출력되는 것을 확인할 수 있습니다. Seoul은 시간은 UTC 기준 +9 시간이므로 UTC로 표현하게 되면 9시간 전의 시간으로 표기됩니다. 두 값을 특정한 조건이나 Date의 함수를 사용하지 않고 출력하면 UTC 기본값으로 출력됩니다. 그래서 date1은 입력값과 동일하게, date2는 9시간 전으로 출력되는 것을 확인할 수 있습니다.  

이제 문자열로 시간을 표현하기 위해서 toString 함수를 사용하니 "결과 (2)"과 같이 date1, date2 모두 현지 시간으로 출력되는 것을 확인할 수 있습니다. date1는 UTC+9에 맞춰 입력된 시간에 +9시간이 되어 출력되고, date2는 현지 시간으로 입력한 값이 그대로 보이는 것을 확인할 수 있습니다. 

그럼 시간 데이타에서 getHours()로 시간을 알아보면 어떻게 나오게 될까요?

var date1 = new Date('2019-01-01T00:00:00.000Z'); 
var date2 = new Date('2019-01-01T00:00:00'); 

console.log( "hours = ",date1.getHours() ," / ", date2.getHours() ); 
// 결과 (3) : hours = 9 / 0 
console.log( "hours = ",date1.getUTCHours() ," / ", date2.getUTCHours() ); 
// 결과 (4) : hours = 0 / 15 

getHours 함수는 현지 시간값을 기준으로 값을 반환하며, UTC 시간값을 기준으로 값을 반환받길 원한다면 getUTCHours 함수를 사용할 수 있습니다. setHours와 setUTCHours도 마찬가지입니다.

var date1 = new Date('2019-01-01T00:00:00.000Z'); 
var date2 = new Date('2019-01-01T00:00:00'); 

date1.setHours(9); 
date2.setHours(9); 

console.log( "DATE1 = ",date1 ," / DATE2 = ", date2 ); 
// 결과 (4) : DATE1 =  2019-01-01T00:00:00.000Z  / DATE2 =  2019-01-01T00:00:00.000Z 

setHours 함수는 첫 인자를 UTC가 아닌, 현지 시간의 시간값으로 인지합니다. 그러므로 당연히 내부에서도 시간값을 설정할 때 UTC가 아닌 현재 시간을 기준으로 주어진 시간값을 설정하게 됩니다. 

date1의 경우 현지 시간은 2019-01-01 09:00:00 이었으며, setHours에서 9시로 지정되었기 때문에 수정된 것은 없습니다. 그래서 출력하면 UTC 기준으로 2019-01-01T00:00:00.000Z 입니다.

date2의 경우 현지 시간은 2019-01-01 00:00:00 이었으며, setHours에서 9시로 지정되어 2019-01-01 09:00:00로 수정되었습니다. 그래서 출력하면 UTC 기준으로 2019-01-01T00:00:00.000Z 입니다. (결과 (1)에서는 2018-12-31T15:00:00.000Z 이었습니다.)


지금까지 JavaScript에서 Date 객체에 대해서 간단히 알아보았습니다. 


MaxScale 추가 설정하다가 이전 내용을 한번에 다 정리해봤습니다. CentOS7 최소 버전이 설치된 환경에서 MariaDB 10.1 버전을 기준으로 진행된 내용입니다.

 

[ MariaDB 설치하기 ]

1. mariadb repo 파일을 만듭니다. 

[mariadb]  
name = MariaDB  
baseurl = http://yum.mariadb.org/10.1/centos7-amd64  
gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB  
gpgcheck=0 

2. MariaDB를 설치합니다.

yum install -y MariaDB MariaDB-server MariaDB-client 

[ Galera Cluster 설정 ]

 

1. 우선 config 파일을 설정합니다.
vi /etc/my.cnf.d/server.cnf

[mysqld] 
init_connect='SET NAMES utf8' 
character-set-server = utf8 
collation-server=utf8_unicode_ci 

[galera] 
# Mandatory settings 
wsrep_on=ON 
wsrep_provider=/usr/lib64/galera/libgalera_smm.so 
wsrep_cluster_address='gcomm://'  ### 추가 서버에서는 기존 가동중인 서버 ip를 입력하세요.
wsrep_cluster_name='cluster_name' 
wsrep_node_address='172.xx.xx.xx'  ### 자신의 ip를 입력하세요.
wsrep_node_name='server1'
wsrep_sst_method=rsync 
wsrep_auto_increment_control=off 
binlog_format=row 
default_storage_engine=InnoDB 
innodb_autoinc_lock_mode=2 

bind-address=0.0.0.0 

wsrep_slave_threads=1 
innodb_flush_log_at_trx_commit=0 


2. 방화벽 설정
  추가할 포트는 TCP 3306/4568/4444 port와 TCP, UDP 4567 입니다.

firewall-cmd --permanent --zone=public --add-port=해당 포트/tcp 


3. selinux 설정

 최소 버전에는 semanage가 없어 설치해야 합니다.

yum install policycoreutils-python  

아래와 같이 추가 설정을 해주세요.

> semanage port -a -t mysqld_port_t -p tcp 4567 
> semanage port -a -t mysqld_port_t -p tcp 4568 
> semanage port -a -t mysqld_port_t -p tcp 4444 
> semanage port -a -t mysqld_port_t -p udp 4567 
> semanage permissive -a mysqld_t 

 

이제 mariadb를 실행하여 테스트용 DB와 테이블을 만들어 서버별로 모두 존재하는지 테스트 해보시면 됩니다. 

클러스터 첫번째 node를 실행할 때는 gcomm에 node ip를 명시하지 않지만, 모두 가동후에는 다른 node들에 대한 ip를 추가한 후 재시작 하시기 바랍니다.

 


[ MaxScale 설치 ]

 

 HA Proxy(http://www.haproxy.org/.  high availability, load balancing, and proxying for TCP and HTTP-based applications. 주로 Load balancer로 사용됨. H/W 형식을 오픈소스로 구현한 프로젝트)와 유사하며, DB read/write 쿼리를 지정된 DB 서버로 분산하는 기능이 있는 등 DB에 좀 더 특화되어 있습니다.

1. yum 저장소 설치

curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash 


2. MaxScale을 설치합니다. 

yum install -y maxscale 


3. MaxScale을 위한 유저 생성합니다. 필요한 권한만 설정합니다.

  create user 'maxscale'@'ip' identified by 'maxscalePW'; 
  grant select on mysql.user to 'maxscale'@'ip'; 
  grant select on mysql.db to 'maxscale'@'ip'; 
  grant select on mysql.tables_priv to 'maxscale'@'ip'; 
  grant show databases on *.* to 'maxscale'@'ip'; 


4. MaxScale을 모니터링 하기 위한 유저 계정을 생성합니다.

  create user 'ms_user'@'%' identified by 'ms_userPW'; 
  grant show databases on *.* to ms_user@'%'; 
  flush privileges; 



6. vi /etc/maxscale.cnf , config 파일 설정, 아래 내용을 통으로 사용하면 됩니다.

#Global MaxScale Settings 
[maxscale] 
threads=auto 

#Define Server Nodes 
[server1] 
type=server 
address=172.xx.xx.01
port=3306 
protocol=MariaDBBackend 

[server2] 
type=server 
address=172.xx.xx.02
port=3306 
protocol=MariaDBBackend 

[server3] 
type=server 
address=172.xx.xx.03
port=3306 
protocol=MariaDBBackend 


#Define Monitoring Service 
[Galera-Monitor] 
type=monitor 
module=galeramon 
servers=server1,server2,server3 
user=maxscale 
password=maxscalePW 
monitor_interval=1000 

#Define Galera Service 
[Galera-Service] 
type=service 
router=readconnroute 
router_options=synced 
servers=server1,server2,server3 
user=maxscale 
passwd=maxscalePW 

#Define Galera Listener 
[Galera-Listener] 
type=listener 
service=Galera-Service 
protocol=MariaDBClient 
port=4306 ### 기존 3306 포트 말고 이 포트로 쿼리를 보내면 됩니다.

#Define Administration Service 
[MaxAdmin-Service] 
type=service 
router=cli 

#Define Administration Listener 
[MaxAdmin-Listener] 
type=listener 
service=MaxAdmin-Service 
protocol=maxscaled 
socket=default 


7. 방화벽 설정. 쿼리를 받기 위한 포트입니다. 수정 가능합니다.

firewall-cmd --permanent --zone=public --add-port=4306/tcp 
firewall-cmd --reload 


8. MaxScale 시작 

systemctl start maxscale.service 
systemctl enable maxscale.service 



9. 테스트

 > maxctrl (maxadmin보다 깔끔하게 출력됩니다.)
          list servers
          show service Galera-Service 

'개발 이야기 > DATABASE' 카테고리의 다른 글

MariaDB, Galera Cluster, MaxScale 전체 정리  (0) 2019.07.04
SELinux for Galera cluster  (0) 2019.07.03
mariadb 시작 오류  (0) 2019.03.18
Galera 포트 리스트 및 용어 정리  (0) 2019.02.19
MariaDB MaxScale (발)번역  (0) 2015.01.23
mysql query browser 세션 문제  (0) 2014.07.02

http://galeracluster.com/library/documentation/selinux.html

 

SELinux Configuration — Galera Cluster Documentation

SELinux Configuration Security-Enhanced Linux, or SELinux, is a kernel module for improving security of Linux operating systems. It integrates support for access control security policies, including mandatory access control (MAC), that limit user applicati

galeracluster.com

 

꼭 읽어봐야 함.

 

tcp 4567, 4444는 설정되어 있다고 나오고, tcp 4568, udp 4567은 설정이 반영됨.

 

semanage 사용하려면 아래와 같이 설치!

 

yum install policycoreutils-python

'개발 이야기 > DATABASE' 카테고리의 다른 글

MariaDB, Galera Cluster, MaxScale 전체 정리  (0) 2019.07.04
SELinux for Galera cluster  (0) 2019.07.03
mariadb 시작 오류  (0) 2019.03.18
Galera 포트 리스트 및 용어 정리  (0) 2019.02.19
MariaDB MaxScale (발)번역  (0) 2015.01.23
mysql query browser 세션 문제  (0) 2014.07.02

+ Recent posts