OTA 업데이트 NGINX API Gateway로 사용

IoT(The Internet of Things)는 음성 인식 디바이스에서 조명, 커피 머신, 냉장고에 이르기까지 이미 우리 주변에 있으며 우리는 항상 깨닫지 못하는 방식으로 매일 IoT와 상호 작용합니다. IoT 디바이스를 확장할 때 가장 큰 문제 중 하나는 디바이스 관리, 특히 일반적으로 OTA (Over-the-air)라고 하는 무선 업데이트입니다.

OTA는 IoT 디바이스에 새로운 펌웨어를 배포하는 데 매우 중요합니다. 이를 사용하지 않으면 소비자 또는 현장에서 디바이스를 방문하는 기술자의 개입이 필요합니다.

이 포스트는 NGINX Open Source 또는 NGINX Plus를 API Gateway로 사용하여 OTA 업데이트를 디바이스에 자동으로 배포하는 방법을 보여줍니다. 간단한 API를 사용하여 펌웨어 버전 관리를 처리하고(기기에서 최신 버전을 알 수 있도록) 파일 자체를 전달합니다.

포스트의 목적을 위해 통신에 WiFi를 사용하는 ESP32‑DevkitC 디바이스를 사용하고 있습니다. 아래에 설명된 설정은 디바이스 기능과 펌웨어 작성 방법에 따라 대부분의 디바이스 유형에 쉽게 적용됩니다.

목차

1. OTA(Over-The-Air)란?
2. 디바이스 설정
3. NGINX 인스턴스 설정
4. NGINX JavaScript 모듈로 OTA 자동화
5. OTA 고급 기능 활용

1. OTA(Over-The-Air)란?

OTA(Over-The-Air)는 무선 네트워크를 통해 펌웨어 및 소프트웨어 업데이트를 진행하는 기술입니다. 이를 통해 기존에 설치된 디바이스에서 새로운 버전의 펌웨어나 소프트웨어 업데이트를 쉽고 빠르게 적용할 수 있습니다. 이를 통해 디바이스의 성능 향상, 버그 수정, 보안 강화 등을 수행할 수 있으며, 사용자 경험을 개선할 수 있습니다.

OTA는 기존의 소프트웨어 업데이트 방식인 유선 연결을 통한 업데이트 방식보다 훨씬 효율적입니다. 유선 연결을 통한 업데이트 방식은 업데이트를 진행하기 위해 모든 디바이스를 수동으로 연결해야 하기 때문에 번거롭고 시간이 많이 소요됩니다. 또한, 모든 디바이스가 유선 연결을 지원하지 않는 경우도 있습니다. 이에 반해 OTA는 무선 네트워크를 통해 업데이트를 진행하므로, 디바이스를 수동으로 연결할 필요 없이 매우 편리하게 업데이트를 적용할 수 있습니다.

따라서, OTA는 디바이스 관리 및 유지보수 측면에서 매우 중요한 기술이며, NGINX API Gateway를 사용하여 OTA 업데이트를 구현할 수 있습니다. 이를 통해 빠르고 안정적인 OTA 업데이트를 구현할 수 있습니다.

2. 디바이스 설정

먼저, 아직 ESP32‑DevkitC를 설정하고 WiFi에 연결하지 않았다면 Espressif의 튜토리얼을 사용하여 지금 설정하세요.

디바이스가 WiFi를 Rocking하면 다음 단계는 업데이트된 펌웨어가 포함된 Arduino 스케치를 로드하는 것입니다. 이를 위해서는 Arduino IDE가 필요하므로, 아직 가지고 있지 않다면 다음과 같이 하십시오:

  1. Arduino 웹 사이트에서 Arduino IDE를 다운로드하여 설치하고 실행합니다.
  2. 도구 > 라이브러리 관리로 이동하고 Arduino IDE가 업데이트될 때까지 기다립니다.
  3. 무선으로 ESP32 펌웨어를 업데이트하기 위한 소프트웨어를 설치하려면 esp32FOTA를 검색하고 선택합니다.

Arduino IDE를 사용하여 WiFi 또는 USB를 통해 디바이스에 연결하고 다음 스케치를 로드합니다. 11번 줄의 <domain-url>을 도메인 이름으로 바꾸십시오:

#include <esp32fota.h>
#include <WiFi.h>
 
const char *ssid = "";
const char *password = "";
 
esp32FOTA esp32FOTA("esp32-fota-http", 1);
 
void setup()
{
  esp32FOTA.checkURL = "https://<domain-url>/ota";
  Serial.begin(115200);
  setup_wifi();
}
 
void setup_wifi()
{
  delay(10);
  Serial.print("Connecting to ");
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }
}
 
void loop()
{
  bool updatedNeeded = esp32FOTA.execHTTPcheck();
  if (updatedNeeded)
  {
    esp32FOTA.execOTA();
  }
  delay(2000);
}

3. NGINX 인스턴스 설정

이제 보드가 설정되었으므로 NGINX를 API Gateway로 설정하여 버전 관리 및 디바이스로의 전달을 처리할 차례입니다.

1. nginx.conf의 http 블록에 다음 include 지시문을 추가합니다.

http {
    include /etc/nginx/conf.d/api*.conf;
}

2. 다음 내용으로 /etc/nginx/conf.d/api.conf를 생성합니다. HTTPS를 사용하고 18–31행에 구성 지시문을 포함하는 것이 좋습니다. 이 경우 인증서와 키를 생성하고 ssl_certificate 및 ssl_certificate_key 지시문(18–19행)을 사용하여 경로를 지정해야 합니다.

이전 섹션의 스케치에서와 같이 8행과 11행에서 <domain-name>을 도메인 이름으로 바꾸십시오.

location 블록(행 10-12)은 API(/ota)에 대한 URI와 NGINX가 반환하는 응답(상태 코드 200과 스케치가 사용하는 데이터)을 JSON 형식으로 지정합니다. 지금은 하드 코딩되어 있지만 다음 섹션에서는 이를 좀 더 동적으로 만들 것이므로 계속 읽어 보십시오.

log_format api_main '$remote_addr - $remote_user [$time_local] "$request"'
                    '$status $body_bytes_sent "$http_referer" "$http_user_agent"';
 
server {
    access_log /var/log/nginx/api_access.log api_main;
 
    listen 443;
    server_name <domain-url>;
  
    location /ota {
        return 200 '{"type":"esp32-fota-http", "version": 100, "host": "<domain-url>", "port": 80, "bin": "/esp32-fota-http-100.bin"}';
    }
 
    proxy_intercept_errors on;
    default_type application/json;
 
    # TLS config
    ssl_certificate ~/fullchain.pem;
    ssl_certificate_key ~/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.1 TLSv1 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_ciphers EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH:!aNULL:!MD5:!DSS;
    ssl_ecdh_curve secp384r1;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:10m;
    ssl_session_tickets off;
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_dhparam /etc/ssl/certs/dhparam.pem;
    add_header Strict-Transport-Security "max-age=63072000; includeSubdomains";
    proxy_cookie_domain ~(?P([-0-9a-z]+\.)?[-0-9a-z]+\.[a-z]+)$ "$secure_domain; secure";
}

3. 다음 명령을 실행하여 구성 구문 유효성 확인하고 구성을 reload 합니다.

$ nginx -t
$ nginx -s reload

4. 디바이스의 전원을 켜기 전에 API가 예상대로 작동하는지 확인하고 api.conf의 location 블록에 로드한 JSON을 반환하는지 확인하십시오.

$ curl -X GET https://domain-url/ota
{"type":"esp32-fota-http", "version": 100, "host": "domain-url", "port": 80, "bin": "/esp32-fota-http-100.bin"}

문제가 발생하면 api_access.log 파일을 확인하고 단계를 다시 추적하십시오. 사용하는 기기에 따라 HTTPS가 지원되지 않을 수 있습니다.

4. NGINX JavaScript 모듈로 OTA 자동화

이제 약간의 NGINX JavaScript 마법을 소개합니다. 폴더에서 읽고 디바이스를 로드할 새 OTA 업데이트 파일이 준비되면 API Output을 업데이트하는 JavaScript 코드를 작성하겠습니다.

이 섹션은 NGINX Plus R23 이상에서 js_include 지시문을 대체하는 js_import 지시문을 사용하도록 업데이트되었습니다. 구성 예 섹션은 NGINX 구성 및 JavaScript 파일에 대한 올바른 구문을 보여줍니다.

1. 아직 설치하지 않은 경우 NGINX 호스트에 아래와 같이 NGINX JavaScript 모듈을 설치합니다. (For Debian and Ubuntu)

$ apt-get install nginx-plus-module-njs

2. 모듈을 활성화하려면 nginx.conf를 편집하고 최상위 (“main”) 지시문에 다음 load_module 지시문을 추가합니다.

load_module modules/ngx_http_js_module.so;
load_module modules/ngx_stream_js_module.so;

3. api.conf를 편집하고 다음과 같이 수정합니다. (이 포스트의 목적을 위해, 우리는 대신 api_njs.conf라는 복사본을 수정하여 만듭니다.)

  • api.conf의 4행에서 시작하는 server 블록 위에 이 js_import 지시문을 추가합니다(위 참조; 이 지시문을 api_njs.conf에 추가하면 이제 server 블록이 6행에서 시작됩니다).
js_import ota.js;
  • location /ota 블록에서 return 지시문을 제거(또는 주석 처리) 하고 다음 js_content 지시문을 삽입합니다.
    location /ota {
        js_content ota.ota;
    }

다음 내용으로 ota.js를 생성합니다. 다음을 확인하십시오.

5행의 </path/to/firmware>를 무선으로 업데이트할 펌웨어 파일을 저장하는 디렉토리의 경로로 바꿉니다. 공개적으로 액세스할 수 있는 인터넷 위치여야 합니다.

21번 줄의 <domain-url> 도메인 이름으로 바꿉니다.

var fs = require('fs');

function ota(r) {

  var otaFolder = '</path/to/firmware>';
  var base = 100;
  var fName = 'esp32-fota-http-';
  var jsonOut = {};

  while (base++){
    try{
      var file = fs.readFileSync(otaFolder+fName+base+'.bin');
    } catch (err) {
      base--;
      break;
    }
  }

  jsonOut['type'] = 'esp32-fota-http';
  jsonOut['version'] = base;
  jsonOut['host'] = '<domain-url>';
  jsonOut['port'] = 80;
  jsonOut['bin'] = '/'+fName+base+'.bin';

  r.return(200, JSON.stringify(jsonOut));

}

export default { ota }

12행에서 Node.js fs 모듈의 readFileSync 함수를 호출하여 otaFolder라는 디렉토리에서 최신 버전의 OTA 업데이트 파일을 찾습니다. 시퀀스의 다음 파일 버전이 존재하는지 확인하기 위해 readFileSync를 사용하여 가장 높은 버전 번호를 찾기 위해 디렉토리의 파일 이름을 반복합니다. 버전이 존재하지 않는 경우 이전 버전이 최신 버전으로 결정됩니다.

12행에서는 Node.jsfs 모듈의 readFileSync 함수를 호출하여 otaFolder로 명명된 디렉토리에서 OTA 업데이트 파일의 최신 버전을 찾습니다. 디렉터리의 파일 이름을 루프 하여 가장 높은 버전 번호를 찾습니다. readFileSync를 사용하여 시퀀스의 다음 파일 버전이 있는지 확인합니다. 버전이 없는 경우 이전(찾은) 버전이 최신 버전으로 결정됩니다.

이렇게 하려면 파일 이름의 형식이 다음과 같아야 합니다.

esp32-fota-http-version.bin

여기서 version은 파일의 첫 번째 버전에서 100입니다. 후속 버전(101, 102, 103 등)에 대해 버전 번호를 순차적으로 증가합니다.

작성 시점에 NGINX JavaScript 모듈은 readdir 기능을 지원하지 않고 개별 파일에 대한 작업만 지원하기 때문에 readFileSync를 사용하고 있습니다. [편집기 – 2020년 7월 NGINX JavaScript 버전 0.4.2에서 readdir() 지원이 추가되었습니다.]

5. 다음 명령을 실행하여 구성 구문의 유효성을 검사하고 구성을 reload 합니다.

$ nginx -t
$ nginx -s reload

6. 이전 섹션의 curl 명령을 반복하여 API가 예상대로 작동하는지 확인합니다.

$ curl -X GET https://domain-url/ota
{"type":"esp32-fota-http", "version": 100, "host": "domain-url", "port": 80, "bin": "/esp32-fota-http-100.bin"}

5. OTA 고급 기능 활용

IoT 와 우리가 사용할 수 있는 디바이스에는 다른 편리한 기능이 많이 있습니다.

디바이스와 서버 간의 더 나은 인증으로 보안을 강화하기 위해 많은 최신 디바이스에는 디바이스 내 x.509 인증서 생성을 허용하는 암호화 칩(예: Arduino MKR 시리즈)이 장착되어 있습니다.

NGINX의 고급 Load Balancing은 OTA 업데이트 파일을 다른 서버 기반 지역 또는 여러 펌웨어 서버 또는 위치에 배포할 수 있음을 의미합니다.

업데이트(또는 확인) 시기는 IoT 디바이스마다 다르므로 이전 버전의 OTA 업데이트를 사용할 수 있도록 유지해야 합니다. 디바이스가 비활성 상태이거나 일정 시간 동안 일시적으로 인터넷에 액세스할 수 없는 경우가 있으므로 펌웨어 버전과 종속성을 추적해야 합니다.

OTA는 소프트웨어 측면에서 디바이스 기능을 개선하고 업데이트할 수 있으므로 디바이스 빌드의 수명을 늘립니다. 물론 OTA는 더 안전한 디바이스를 만드는 데 도움이 되므로 보안 업그레이드 및 패치와 같은 작업에 훨씬 더 중요합니다.

1개의 디바이스가 10개가 되기도 전에 10,000개가 될 수 있으므로 확장하기 전에 확실한 OTA 업데이트 계획을 세우는 것이 중요합니다.

NGINX Plus API Gateway를 직접 사용해 보거나 테스트해 보려면 지금 30일 무료 평가판을 신청하거나 사용 사례에 대해 최신 소식을 빠르게 전달받고 싶으시면 아래 뉴스레터를 구독하세요.