Health Check 기능으로 NGINX 애플리케이션 가용성 향상

NGINX Plus의 Active Health Check 기능은 일반적으로 백엔드 시스템의 일반적인 가용성 및 End-to-End 애플리케이션 상태를 테스트하는 데 사용됩니다.

어떤 기관이나 조직의 공개 웹사이트를 운영하는 일은 쉽지 않습니다.
비즈니스 측면에서는 웹사이트가 미션 크리티컬한 요소로 여겨져 100% 가동률과 완벽한 안정성이 요구됩니다.

그러나 동시에 조직의 요구를 충족하기 위해 웹사이트를 자주 업데이트해야 하는 경우도 있습니다.
이러한 안정성과 지속적인 변화라는 이중적 요구 사항은 운영팀이 직면하는 주요 과제 중 하나입니다.
하지만 더 큰 과제는 나쁜 행위자, 해커, “스크립트 키디(script kiddies)” 등으로부터 웹사이트를 방어하는 것입니다.

홈페이지 변조는 디지털적인 범죄 중 하나로,
마치 상점이나 본사의 정면에 불법적인 그래피티가 그려지는 것과 유사합니다 홈페이지 변조는 계속적인 위협으로 작용하며 발생하면 원하지 않는 관심을 불러일으키고 수치심을 초래할 수 있습니다.
웹사이트를 운영하는 책임자들은 또한 홈페이지 변조가 CEO와의 어려운 대화를 초래하거나 직무 제한 사유가 될 수 있다는 것을 인식하게 될 수 있습니다

보안은 다중 레이어 접근 방식이 필요합니다.
이 포스트에서는 NGINX Plus를 보안 스택의 중요한 요소로 사용하는 방법을 설명합니다.
이를 위해서 방화벽, 침입 탐지 시스템, 강력한 계정 관리 등과 함께 NGINX Plus의 Active Health Check 기능을 사용하여 웹사이트 홈페이지를 예방적으로 모니터링하고,
예상치 못한 변경 사항이 감지되면 이전 버전으로 자동 복원하도록 구성할 수 있습니다.

목차

1. ETag를 사용하여 변경 사항 감지
2. Health Check를 위한 JavaScript 해싱으로 스케일 아웃 변경 감지

1. ETag를 사용하여 변경 사항 감지

NGINX Plus를 사용하여 홈페이지 변조를 감지하는 방법을 탐색하기 위해, 공개 웹사이트의 주요 구성 요소를 대표하는 간단한 테스트 환경을 사용할 것입니다.

이 간단한 솔루션은 백엔드 웹 서버에 있는 홈 페이지 리소스의 HTTP ETag를 사용합니다.
일반적으로 캐시된 리소스에 대한 변경을 탐지하는 데 사용되며, 정의에 따라 리소스가 변경될 때마다 ETag 값이 변경됩니다. 홈 페이지의 헤더를 검사하여 ETag 값을 얻을 수 있습니다.

$ curl -i http://10.0.0.1/
HTTP/1.1 200 OK
Date: Fri, 21 Jul 2017 15:05:25 GMT
Content-Type: text/html
Content-Length: 612
ETag: "58ad6e69-264"

그런 다음 백엔드 웹 서버가 동일한ETag를 반환하는 경우에 통과하는 Health Check 를 정의합니다.

match homepage_etag {
    header ETag = '"58ad6e69-264"';
}

upstream my_website {
    server 10.0.0.1:80;
    zone   health 64k; # Allow workers to share health info
}

server {
    listen 80;
    location / {
        proxy_pass   http://my_website;
        health_check match=homepage_etag;
    }
}

match 블록은 14번째 행의 health_check 지시문이 충족해야 하는 조건을 정의합니다. 여기서는 HTTP 헤더 ETag가 백엔드 웹 서버에서 얻은 값과 정확히 일치해야합니다. health_check 지시문은 기본적으로 루트 URI (/)를 테스트하므로 홈페이지를 확인하기 위해 추가 구성이 필요하지 않습니다.

이 구성은 홈페이지의 무단 변경을 차단하여 효과적으로 서비스를 보호합니다. ETag가 변경되면 건강 검사가 실패하고 백엔드 웹 서버가 사용 불가능한 것으로 간주됩니다.
이러한 수준의 보호는 원하지 않는 결과를 초래할 수 있습니다. 오프라인인 사이트는 대체로 변조된 사이트와 마찬가지로 문제가 됩니다.
또한 일상적인 변경 사항은 NGINX 구성에서 ETag 값을 업데이트 할 때까지 장애를 발생시킵니다.

이러한 문제를 해결하기 위해 NGINX 내에서 콘텐츠 캐싱을 활성화하고, Health Check가 실패할 경우 마지막으로 알려진 정상 버전을 계속 제공하도록 구성합니다.
이 구성은 NGINX가 캐시된 콘텐츠를 백엔드 웹 서버에서 지속적으로 가져오는 대신 직접 서비스하도록 함으로써 전반적인 웹 사이트 성능을 향상시키는 추가적인 이점이 있습니다.

proxy_cache_path /var/cache/nginx_hc levels=1:2 keys_zone=hc_cache:1m max_size=2g inactive=48h;

server {
    listen 80;
    location / {
        proxy_pass   http://my_website;
        health_check match=homepage_etag;

        proxy_cache           hc_cache;
        proxy_cache_valid     60m;
        proxy_cache_use_stale error;
    }
}

10번째 행의 proxy_cache_path 지시문은 캐시된 콘텐츠의 위치를 정의합니다. keys_zone 매개 변수는 캐시 인덱스 (hc_cache라고 함)에 대한 공유 메모리 영역을 정의하고 1 MB를 할당합니다. max_size 매개 변수는 NGINX Plus가 캐시에서 최근에 요청된 리소스를 제거하여 새로운 캐시 항목을 위한 공간을 만드는 시점을 정의합니다. inactive 매개 변수는 백엔드 웹 서버에서 수신한 Cache-Control 속성과 관계없이 디스크에 캐시된 콘텐츠를 마지막 요청 시점부터 유지할 시간을 정의합니다.

location 블록 내에서 proxy_cache 지시문은 캐시 키를 저장하는 데 사용되는 공유 메모리 영역의 이름 (hc_cache)을 지정하여 이 location에 대한 캐싱을 가능하게합니다. proxy_cache_valid 지시문은 리소스가 만료되기 전에 NGINX가 캐시에서 얼마 동안 제공할 수 있는지를 정의합니다. 이 값은 백엔드 웹 서버에서 수신한 Cache-Control 속성에 의해 재정의됩니다.

백엔드 웹 서버가 비정상적으로 간주될 때 마지막으로 알려진 좋은 버전의 홈페이지를 제공해야하는 요구 사항을 충족하기 위해, proxy_cache_use_stale 지시문을 사용하여 만료된 콘텐츠를 백엔드가 사용 불가능 할 때에도 제공할 수 있도록합니다.

2. Health Check 를 위한 JavaScript 해싱으로 스케일 아웃 변경 감지

이 섹션의 코드는 NGINX JavaScript 구현의 변경 사항을 반영하기 위해 다음과 같이 업데이트되었습니다.

  • NGINX JavaScript 0.2.4에서 도입된 Stream 모듈용 리팩토링 된 세션 (s) 객체를 사용합니다.
  • NGINX Plus R23 이상에서 js_include 지시문을 대체하는 js_import 지시문을 사용합니다. 자세한 정보는 NGINX JavaScript 모듈에 대한 문서를 참조하십시오. 예제 구성 섹션에는 NGINX 구성 및 JavaScript 파일에 대한 올바른 구문이 표시됩니다.

HTTP ETag를 감지하는 것은 간단하면서도 효과적인 해결책이지만, 로드 밸런싱에서는 확장성이 부족합니다.
로드 밸런싱 환경에서는 NGINX Plus의 단일 인스턴스가 백엔드 웹 서버마다 다른 ETag 값을 받을 수 있으며, 컨텐츠는 동일하지만 그렇습니다.

ETag 대신에 예상치 못한 변경 사항을 감지하는 더 견고한 방법은 각 백엔드 웹 서버에서 실제 컨텐츠의 암호화 해시를 사용하는 것입니다.
이를 위해 각 백엔드 웹 서버에 인접한 웹 서비스를 제공하여 지정된 URL에 대한 SHA-256 해시 값을 제공합니다.
이 인접한 웹 서비스, 즉 해시 서비스는 NGINX Plus Health Check 에서만 사용되어 각 백엔드 웹 서버에서 현재 홈페이지 컨텐츠에 대한 해시 값을 얻습니다.

백엔드 웹 서버는 해시 서비스를 제공하기 위해 NGINX 또는 NGINX Plus를 실행할 필요가 없습니다. NGINX는 이를 위해 기존 웹 서버 소프트웨어와 함께 설치할 수 있으며 구성이 거의 필요하지 않습니다.

stream {
    js_import conf.d/hash_service.js;

    server {
        listen 4256;
        js_filter hash_service.hashResponseBody;
        proxy_pass 127.0.0.1:80;
    }
}

해시 서비스는 Stream 컨텍스트 내에서 간단한 TCP 프록시로 구현되며, 포트 80에서 수신된 로컬 웹 서버의 HTTP 응답에 대해 해시 작업을 수행하기 위해 NGINX JavaScript 모듈을 사용합니다.
해시 서비스 코드는 아래에 나와 있습니다.

/*
 * A JavaScript implementation of the Secure Hash Algorithm, SHA-256, as defined
 * in FIPS 180-2
 * Version 2.2 Copyright Angel Marin, Paul Johnston 2000 - 2009.
 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
 * Distributed under the BSD License
 * See http://pajhome.org.uk/crypt/md5 for details.
 * Also http://anmar.eu.org/projects/jssha2/
 */
var hexcase=0;function hex_sha256(a){return rstr2hex(rstr_sha256(str2rstr_utf8(a)))}function hex_hmac_sha256(a,b){return rstr2hex(rstr_hmac_sha256(str2rstr_utf8(a),str2rstr_utf8(b)))}function sha256_vm_test(){return hex_sha256("abc").toLowerCase()=="ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"}function rstr_sha256(a){return binb2rstr(binb_sha256(rstr2binb(a),a.length*8))}function rstr_hmac_sha256(c,f){var e=rstr2binb(c);if(e.length>16){e=binb_sha256(e,c.length*8)}var a=Array(16),d=Array(16);for(var b=0;b<16;b++){a[b]=e[b]^909522486;d[b]=e[b]^1549556828}var g=binb_sha256(a.concat(rstr2binb(f)),512+f.length*8);return binb2rstr(binb_sha256(d.concat(g),512+256))}function rstr2hex(c){try{hexcase}catch(g){hexcase=0}var f=hexcase?"0123456789ABCDEF":"0123456789abcdef";var b="";var a;for(var d=0;d<c.length;d++){a=c.charCodeAt(d);b+=f.charAt((a>>>4)&15)+f.charAt(a&15)}return b}function str2rstr_utf8(c){var b="";var d=-1;var a,e;while(++d<c.length){a=c.charCodeAt(d);e=d+1<c.length?c.charCodeAt(d+1):0;if(55296<=a&&a<=56319&&56320<=e&&e<=57343){a=65536+((a&1023)<<10)+(e&1023);d++}if(a<=127){b+=String.fromCharCode(a)}else{if(a<=2047){b+=String.fromCharCode(192|((a>>>6)&31),128|(a&63))}else{if(a<=65535){b+=String.fromCharCode(224|((a>>>12)&15),128|((a>>>6)&63),128|(a&63))}else{if(a<=2097151){b+=String.fromCharCode(240|((a>>>18)&7),128|((a>>>12)&63),128|((a>>>6)&63),128|(a&63))}}}}}return b}function rstr2binb(b){var a=Array(b.length>>2);for(var c=0;c<a.length;c++){a[c]=0}for(c=0;c<b.length*8;c+=8){a[c>>5]|=(b.charCodeAt(c/8)&255)<<(24-c%32)}return a}function binb2rstr(b){var a="";for(var c=0;c<b.length*32;c+=8){a+=String.fromCharCode((b[c>>5]>>>(24-c%32))&255)}return a}function sha256_S(b,a){return(b>>>a)|(b<<(32-a))}function sha256_R(b,a){return(b>>>a)}function sha256_Ch(a,c,b){return((a&c)^((~a)&b))}function sha256_Maj(a,c,b){return((a&c)^(a&b)^(c&b))}function sha256_Sigma0256(a){return(sha256_S(a,2)^sha256_S(a,13)^sha256_S(a,22))}function sha256_Sigma1256(a){return(sha256_S(a,6)^sha256_S(a,11)^sha256_S(a,25))}function sha256_Gamma0256(a){return(sha256_S(a,7)^sha256_S(a,18)^sha256_R(a,3))}function sha256_Gamma1256(a){return(sha256_S(a,17)^sha256_S(a,19)^sha256_R(a,10))}function sha256_Sigma0512(a){return(sha256_S(a,28)^sha256_S(a,34)^sha256_S(a,39))}function sha256_Sigma1512(a){return(sha256_S(a,14)^sha256_S(a,18)^sha256_S(a,41))}function sha256_Gamma0512(a){return(sha256_S(a,1)^sha256_S(a,8)^sha256_R(a,7))}function sha256_Gamma1512(a){return(sha256_S(a,19)^sha256_S(a,61)^sha256_R(a,6))}var sha256_K=new Array(1116352408,1899447441,-1245643825,-373957723,961987163,1508970993,-1841331548,-1424204075,-670586216,310598401,607225278,1426881987,1925078388,-2132889090,-1680079193,-1046744716,-459576895,-272742522,264347078,604807628,770255983,1249150122,1555081692,1996064986,-1740746414,-1473132947,-1341970488,-1084653625,-958395405,-710438585,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,-2117940946,-1838011259,-1564481375,-1474664885,-1035236496,-949202525,-778901479,-694614492,-200395387,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,-2067236844,-1933114872,-1866530822,-1538233109,-1090935817,-965641998);function binb_sha256(n,o){var p=new Array(1779033703,-1150833019,1013904242,-1521486534,1359893119,-1694144372,528734635,1541459225);var k=new Array(64);var B,A,z,y,w,u,t,s;var r,q,x,v;n[o>>5]|=128<<(24-o%32);n[((o+64>>9)<<4)+15]=o;for(r=0;r<n.length;r+=16){B=p[0];A=p[1];z=p[2];y=p[3];w=p[4];u=p[5];t=p[6];s=p[7];for(q=0;q<64;q++){if(q<16){k[q]=n[q+r]}else{k[q]=safe_add(safe_add(safe_add(sha256_Gamma1256(k[q-2]),k[q-7]),sha256_Gamma0256(k[q-15])),k[q-16])}x=safe_add(safe_add(safe_add(safe_add(s,sha256_Sigma1256(w)),sha256_Ch(w,u,t)),sha256_K[q]),k[q]);v=safe_add(sha256_Sigma0256(B),sha256_Maj(B,A,z));s=t;t=u;u=w;w=safe_add(y,x);y=z;z=A;A=B;B=safe_add(x,v)}p[0]=safe_add(B,p[0]);p[1]=safe_add(A,p[1]);p[2]=safe_add(z,p[2]);p[3]=safe_add(y,p[3]);p[4]=safe_add(w,p[4]);p[5]=safe_add(u,p[5]);p[6]=safe_add(t,p[6]);p[7]=safe_add(s,p[7])}return p}function safe_add(a,d){var c=(a&65535)+(d&65535);var b=(a>>16)+(d>>16)+(c>>16);return(b<<16)|(c&65535)};

/*
 * NGINX JavaScript js_filter function
 * Returns the hashed value of the response body as a header with no body
 */
var res = '';
function hashResponseBody(s) {
    s.on('upload', function (data, flags) {
        res = res + data;
        var buffer_to_send = '';
        var body_pos = res.indexOf("\r\n\r\n"); // Find where the headers end

        if (body_pos) {
            var hash = hex_sha256(res.substr(body_pos + 4)); // Skip CRLFCRLF

            buffer_to_send  = "HTTP/1.1 204 No Content\n";
            buffer_to_send += "Hash: " + hash + "\n";
            buffer_to_send += "\r\n\r\n";

            s.send(buffer_to_send);
        }
    });
}

export default { hashResponseBody }

이 구성을 사용하면 해시 서비스를 쿼리하거나 홈페이지 출력을 외부 해시 라이브러리로 전송하여 홈페이지의 SHA-256 해시를 얻습니다.
여기서는 OpenSSL을 사용하여 해시 서비스의 결과가 올바른지 확인합니다.

$ curl -i http://127.0.0.1:4256/
HTTP/1.1 204 No Content
Hash: 38ffd4972ae513a0c79a8be4573403edcd709f0f572105362b08ff50cf6de521

$ curl -s http://127.0.0.1/ | openssl dgst -sha256
(stdin)= 38ffd4972ae513a0c79a8be4573403edcd709f0f572105362b08ff50cf6de521

해시 서비스는 해시 값만 단일 HTTP 헤더로 사용하여 최소 HTTP 응답을 반환합니다.

이제 NGINX Plus 구성을 수정하여 Health Check에 해시 서비스를 사용할 수 있습니다.

match homepage_hash {
    status 204;
    header hash = 38ffd4972ae513a0c79a8be4573403edcd709f0f572105362b08ff50cf6de521;
}

upstream my_website {
    server 10.0.0.1:80;
    server 10.0.0.2:80;
    server 10.0.0.3:80;
    zone   health 64k; # Allow workers to share health info
}

proxy_cache_path /var/cache/nginx_hc levels=1:2 keys_zone=hc_cache:1m max_size=2g inactive=48h;

server {
    listen 80;
    location / {
        proxy_pass   http://my_website;
        health_check match=homepage_hash port=4256;

        proxy_cache           hc_cache;
        proxy_cache_valid     60m;
        proxy_cache_use_stale error;
    }
}

match 블록은 이제 상태 코드가 204이고, 해시 값을 가진 헤더가 포함된 것으로 건강한 응답을 정의합니다. 이 해시 값은 해시 서비스에서 얻어온 것입니다. 각 백엔드 웹 서버에서 해시 서비스를 사용하려면 health_check 지시어가 해시 서비스에서 사용하는 포트 번호를 지정해야 합니다. 이렇게 하면 upstream 그룹에서 각 서버 지시어에서 지정한 포트를 무시하고 해시 서비스를 사용할 수 있습니다 (7-9번째 줄).

각 백엔드 웹 서버에서 홈페이지 컨텐츠가 동일하다고 예상할 수 있으므로,
홈페이지 컨텐츠가 변경되면 NGINX Plus는 해당 백엔드 서버를 upstream 그룹에서 제거합니다.
그러나 NGINX Plus는 캐시에서 마지막으로 알려진 좋은 컨텐츠를 계속 제공하여 사용자가 영향을 받지 않습니다. NGINX Plus 대시보드를 사용하여 실패한 health check 를 가진 백엔드 서버를 확인할 수 있으며,
이는 NGINX Plus가 성능, 보안, 신뢰성 및 확장성을 갖춘 사이트 제공을 돕는 또 다른 방법입니다.

프로덕션급 또는 엔터프라이즈급 NGINX인 NGINX Plus를 직접 사용해 보거나 테스트해 보려면 지금 30일 무료 평가판을 신청하세요. 사용 사례에 대해 최신 소식을 빠르게 전달받고 싶으시면 아래 뉴스레터를 구독하세요.