블로그 이미지
평범하게 살고 싶은 월급쟁이 기술적인 토론 환영합니다.같이 이야기 하고 싶으시면 부담 말고 연락주세요:이메일-bwcho75골뱅이지메일 닷컴. 조대협


Archive»


 
 

ESP01 (ESP8266)을 이용한 HTTP 통신


조대협 (http://bcho.tistory.com)


하드웨어 시리얼과 소프트웨어 시리얼

앞의 글에서 ESP01을 연결해봤는데, ESP01 연결시에 포트를 2,3번 포트를 사용하고, 코드는 SoftSerial 라이브러리를 이용하였다. 아두이노 우노의 0,1 번포트는 시리얼 통신을 위한 RX,TX 포트이다. 이를 Hardware Serial 이라고 한다. 그러면 하드웨어 시리얼 포트를 사용하지 않고 2,3번 포트를 사용한 후에 소프트웨어 시리얼 처리를 한 이유는 무엇을까?


하드웨어 시리얼 포트는 PC와 연결되어 있을때 PC와 통신을 목적으로 사용된다. 그래서 하드웨어 시리얼을 사용하지 않은것인데, ESP8266 관련 라이브러리들을 살펴보니, 대부분 하드웨어 시리얼을 사용하는 것으로 보인다. 그러면 이 문제를 어떻게 해결할것일까?


아두이오 우노 기판을 보면 TX와 RX라고 써있는 LED 가 있는데, 하드웨어 시리얼 통신을 할때만 깜빡 거린다.




PC에 연결이 되어 있을 때, AT 명령을 실행할때는 이 LED가 빛나지 않고, 코드를 업로드 할때만 빛이 난다. 즉 코드 업로드 시에만 하드웨어 시리얼을 PC가 사용한다는 것이기 때문에, 코드를 업로드한후에 ESP01 보드를 TX,RX 단자에 연결하면 하드웨어 시리얼을 사용할 수 있다.

아두이노를 이용한 HTTP 통신

아두이노에 ESP01 모듈을 연결했으면, HTTP 통신을 해보도록 한다. ESP01 (ESP8266 계열)은 TCP/UDP 스택이 이미 들어가 있기 때문에, TCP 통신은 가능하다. HTTP 통신을 하려면 TCP 프로토콜을 이용하여 HTTP 메세지를 보내주면 되는데, AT 명령어를 사용하면 된다.

AT 명령어 레퍼런스는 https://www.electrodragon.com/w/ESP8266_AT-Command_firmware 를 참고하기 바란다. AT명령을 이용하여 WIFI 네트워크에 연결하는 방법은 http://bcho.tistory.com/1283 에 설명되어 있다. WIFI 네트워크에 연결되어 있다고 가정하고 AT명령을 이용해서 HTTP 명령을 보내는 방법을 보자


HTTP  서버 연결

HTTP 요청을 보내기 위해서 먼저 웹서버에 TCP 커넥션을 연결해야 한다. TCP 커넥션 연결 명령어는 AT+CIPSTART로 다음과 같다.


AT+CIPSTART={TCP 연결 번호},”TCP”,”웹서버IP”,{포트번호}


이 명령은 해당 웹서버 IP의 포트로 연결을 하고 연결 번호(이름)를  “0”으로 지정한것이다.

210.118.92.89 서버의 80 포트로 연결하려면 Serial 콘솔에서 AT+CIPSTART=0,”TCP”,”210.118.92.89”,80 명령어를 실행하면 된다. 이 연결의 연결 번호는 0 번이 된다.

명령어 전달

서버로 연결이 되었으면, HTTP 명령어를 보내야 하는데, 명령어 전달은 AT+CIPSEND 명령어를 사용하면 된다. 명령어의 형식은 다음과 같다.


AT+CPISEND={TCP 연결번호},{보내고자 하는 메세지 길이}


예를 들어 “GET /index.html” 명령을 0번 TCP 연결로 보내고자 하면, 메세지의 길이가 15자이기 때문에,

AT+CPISEND=0,15 로 실행하고, 그 다음에

> 가 나오면 GET /index.html 을 전송하면 된다.

예제

실제 개발에서는 콘솔에서 AT 명령을 직접 입력해서 사용하는 것이 아니기 때문에, 이를 코드화 해야 하는데, 아래는 코드화 한 예제이다. (실제 개발에서는 사용하지 않을것이기 때문에 테스트는 하지 않았다)

#include <SoftwareSerial.h>


SoftwareSerial esp(2,3); //TX,RX

String SSID="{Wifi SSID}";

String PASSWORD="{Wifi password}";


void connectWifi(){

 String cmd = "AT+CWMODE=1";

 esp.println(cmd);

 cmd ="AT+CWJAP=\""+SSID+"\",\""+PASSWORD+"\"";

 esp.println(cmd);

 delay(1000);

 if(esp.find("OK")){

   Serial.println("Wifi connected");

 }else{

   Serial.println("Cannot connect to Wifi"+esp);

 }

}


void httpGet(String server,String uri){

 String connect_server_cmd = "AI+CIPSTART=4,\"TCP\",\""+server+"\",80";

 esp.println(connect_server_cmd);

 String httpCmd = "GET "+uri;

 String cmd = "AT+CIPSEND=4,"+httpCmd.length();

 esp.println(cmd);

 esp.println(httpCmd);


}

void setup() {

 // put your setup code here, to run once:

 esp.begin(9600);

 Serial.begin(9600);

 connectWifi();

 httpGet("210.192.111.122","/index.html");

}


void loop() {

 // put your main code here, to run repeatedly:


}


위의 붉은 색으로 표시된 부분을 보면 번호 4번으로 TCP연결을 열고, 4번 채널에 AT+CIPSEND 명령을 이용하여 GET /index.html 을 실행하였다.

SDK의 활용

HTTP 통신의 기본 원리를 이해하였으면, 이제 실제로 HTTP 호출을 해보자. AT 명령을 이용해도 되지만,  HTTP 명령어 종류가 많고, 연결, 메세지 전송/받기등 다양한 명령을 AT 명령으로 직접 코딩하기에는 코딩양이 많고 복잡해진다. 그래서 이런 명령을 잘 추상화하여 단순화 해놓은 SDK를 사용하는 것이 좋은데, 아두이노가 오픈 소스인 만큼, 오픈소스 기반의 SDK가 많다. 장점이기도 하지만 반대로 단점도 되는데, 아두이노나 ESP8266 펌웨어의 버전이 낮으면 동작하지 않는 경우도 있고, 또한 많은 라이브러리들이 아두이노 WIFI 실드를 기준으로 개발되었거나 또는 아두이노 → ESP8266으로 통신하는 용도가 아닌 ESP8266 기반의 아두이노 보드를 기준으로 개발된 라이브러리가 많다.

아두이노 라이브러리 추가하기

우리가 사용할 라이브러리는 WiFiESP라는 라이브러리 (https://github.com/bportaluri/WiFiEsp) 로 소프트웨어 시리얼과 하드웨어 시리얼 통신 양쪽을 모두 지원하며, HTTP 형태로 라이브러리가 잘 패키징 되어 있다. 또한 예제가 비교적 잘 정리되어 있어서 사용하기 좋다.

이런 오픈소스 라이브러리를 사용하려면 라이브러리를 설치해야 하는데, 설치 방법은 다음과 같다.

github에 배포된 코드의 경우, 해당 코드를 아래 그림과 같이 download 버튼을 이용하여 zip 파일로 다운로드 받는다.




다음 Sketch에서 Sketch > Include Library > Add .Zip Library 를 선택해서 앞에서 다운 받은 ZIP 파일을 선택하면 라이브러리로 등록되어 사용이 가능하다.




SDK를 이용하여 HTTP 호출


아래코드는 라이브러리를 추가한후에, 예제에 나온 코드를 그대로 사용한 예이다. 아두이노 2,3번 핀을 ESP01의 TX,RX 핀에 연결해서 소프트웨어 시리얼로 통신하였다. 그래서 코드 부분을 보면 SoftwareSerial Serial1(2,3)으로 소프트웨어 시리얼로 선언되어 있는 것을 확인할 수 있다.


#include "WiFiEsp.h"


// Emulate Serial1 on pins 6/7 if not present

#ifndef HAVE_HWSERIAL1

#endif

#include "SoftwareSerial.h"

SoftwareSerial Serial1(2, 3); // RX, TX


char ssid[] = "{WIFI SSID}";            // your network SSID (name)

char pass[] = "{WIFI Password}";        // your network password

int status = WL_IDLE_STATUS;     // the Wifi radio's status


char server[] = "arduino.cc";


// Initialize the Ethernet client object

WiFiEspClient client;


void setup()

{

 // initialize serial for debugging

 Serial.begin(9600);

 // initialize serial for ESP module

 Serial1.begin(9600);

 // initialize ESP module

 WiFi.init(&Serial1);


 // check for the presence of the shield

 if (WiFi.status() == WL_NO_SHIELD) {

   Serial.println("WiFi shield not present");

   // don't continue

   while (true);

 }


 // attempt to connect to WiFi network

 while ( status != WL_CONNECTED) {

   Serial.print("Attempting to connect to WPA SSID: ");

   Serial.println(ssid);

   // Connect to WPA/WPA2 network

   status = WiFi.begin(ssid, pass);

 }


 // you're connected now, so print out the data

 Serial.println("You're connected to the network");

 

 printWifiStatus();


 Serial.println();

 Serial.println("Starting connection to server...");

 // if you get a connection, report back via serial

 if (client.connect(server, 80)) {

   Serial.println("Connected to server");

   // Make a HTTP request

   client.println("GET /asciilogo.txt HTTP/1.1");

   client.println("Host: arduino.cc");

   client.println("Connection: close");

   client.println();

 }

}


void loop()

{

 // if there are incoming bytes available

 // from the server, read them and print them

 while (client.available()) {

   char c = client.read();

   Serial.write(c);

 }


 // if the server's disconnected, stop the client

 if (!client.connected()) {

   Serial.println();

   Serial.println("Disconnecting from server...");

   client.stop();


   // do nothing forevermore

   while (true);

 }

}



void printWifiStatus()

{

 // print the SSID of the network you're attached to

 Serial.print("SSID: ");

 Serial.println(WiFi.SSID());


 // print your WiFi shield's IP address

 IPAddress ip = WiFi.localIP();

 Serial.print("IP Address: ");

 Serial.println(ip);


 // print the received signal strength

 long rssi = WiFi.RSSI();

 Serial.print("Signal strength (RSSI):");

 Serial.print(rssi);

 Serial.println(" dBm");

}


접속하고자하는 서버의 주소는

char server[] = "arduino.cc";

에 arduino.cc 로 정의되어 있고


   client.println("GET /asciilogo.txt HTTP/1.1");

   client.println("Host: arduino.cc");

   client.println("Connection: close");

   client.println();


명령어를 이용해서 arduino.cc/asciilogo.txt 파일을 HTTP GET으로 읽어오게 되어있다.

읽어온 응답값을

 while (client.available()) {

   char c = client.read();

   Serial.write(c);

 }


코드를 이용해서 client.read를 통해서 한글자씩 읽어온후 화면에 출력하기 위해서 Seirial.write(c)를 이용하여 한글자씩 콘솔에 출력하였다.

실행 결과는 다음과 같다.



결론

테스트를 하면서 가장 어려웠던 점이 아두이노 우노에서 ESP01 로 소프트웨어 시리얼 통신을 하는 경우 HTTP나 WIFI SDK를 찾기가 어려웠다. 대부분 ESP8266을 MCU로 하는 보드 (예를 들어 nodeMCU, ESP12,Wemo D1/D1 mini)를 베이스로 하는 경우가 많았는데, 그도 그럴것이 개발이나 테스트 용도가 아니라 실제 디바이스로 만들어서 사용하려면 우노 + ESP01 조합은 크기도 크고 연결도 복잡해서 효용성이 없다. 네트워크 통신을 하는 시나리오등은 IOT와 같이 센서 데이타를 얻어서 서버에 전송하는 케이스이기 때문에,  하나의 소형 디바이스로 처리가 가능하다면 하나의 디바이스를 사용하는 것이 훨씬 효율적이다. (가격이나 배터리 용량면등).

다음 글에서는 실제로 센서에서 데이타를 읽어서 HTTP를 이용해서 서버에 전송하는 부분을 고민해보도록 하겠다.

참고자료




쿠버네티스 #8

Ingress


조대협 (http://bcho.tistory.com)



쿠버네티스의 서비스는, L4 레이어로 TCP 단에서 Pod들을 밸런싱한다.

서비스의 경우에는 TLS (SSL)이나, VirtualHost와 같이 여러 호스트명을 사용하거나 호스트명에 대한 라우팅이 불가능하고, URL Path에 따른 서비스간 라우팅이 불가능하다.

또한 마이크로 서비스 아키텍쳐 (MSA)의 경우에는 쿠버네티스의 서비스 하나가 MSA의 서비스로 표현되는 경우가 많고 서비스는 하나의 URL로 대표 되는 경우가 많다. (/users, /products, …)

그래서 MSA 서비스간의 라우팅을 하기 위해서는 API 게이트웨이를 넣는 경우가 많은데, 이 경우에는 API 게이트웨이에 대한 관리포인트가 생기기 때문에, URL 기반의 라우팅 정도라면, API 게이트웨이 처럼 무거운 아키텍쳐 컴포넌트가 아니라, L7 로드밸런서 정도로 위의 기능을 모두 제공이 가능하다.


쿠버네티스에서 HTTP(S)기반의 L7 로드밸런싱 기능을 제공하는 컴포넌트를 Ingress라고 한다.

개념을 도식화 해보면 아래와 같은데, Ingress 가 서비스 앞에서 L7 로드밸런서 역할을 하고, URL에 따라서 라우팅을 하게 된다.


Ingress 가 서비스 앞에 붙어서, URL이 /users와 /products 인것을 각각 다른 서비스로 라우팅 해주는 구조가 된다.


Ingress 은 여러가지 구현체가 존재한다.

구글 클라우드의 경우에는 글로벌 로드 밸런서(https://github.com/kubernetes/ingress-gce/blob/master/README.md) 를 Ingress로 사용이 가능하며, 오픈소스 구현체로는 nginx (https://github.com/kubernetes/ingress-nginx/blob/master/README.md)  기반의 ingress 구현체가 있다.  상용 제품으로는 F5 BIG IP Controller (http://clouddocs.f5.com/products/connectors/k8s-bigip-ctlr/v1.5/) 가 현재 사용이 가능하고, 재미있는 제품으로는 오픈소스 API 게이트웨이 솔루션인 Kong (https://konghq.com/blog/kubernetes-ingress-controller-for-kong/)이 Ingress 컨트롤러의 기능을 지원한다.

각 구현체마다 설정 방법이 다소 차이가 있으며, 특히 Ingress 기능은 베타 상태이기 때문에, 향후 변경이 있을 수 있음을 감안하여 사용하자

URL Path 기반의 라우팅

이 글에서는 구글 클라우드 플랫폼의 로드밸런서를 Ingress로 사용하는 것을 예를 들어 설명한다.

위의 그림과 같이 users 와 products 서비스 두개를 구현하여 배포하고, 이를 ingress를 이용하여 URI가  /users/* 와 /products/* 를 각각의 서비스로 라우팅 하는 방법을 구현해보도록 하겠다.


node.js와 users와 products 서비스를 구현한다.

서비스는 앞에서 계속 사용해왔던 간단한 HelloWorld 서비스를 약간 변형해서 사용하였다.


아래는 users 서비스의 server.js 코드로 “Hello World! I’m User server ..”를 HTTP 응답으로 출력하도록 하였다.  Products 서비스는 User server를 product 서버로 문자열만 변경하였다.


var os = require('os');


var http = require('http');

var handleRequest = function(request, response) {

 response.writeHead(200);

 response.end("Hello World! I'm User server "+os.hostname() +" \n");


 //log

 console.log("["+

Date(Date.now()).toLocaleString()+

"] "+os.hostname());

}

var www = http.createServer(handleRequest);

www.listen(8080);


다음으로 서비스를 배포해야 하는데, Ingress를 사용하려면 서비스는 Load Balancer 타입이 아니라, NodePort 타입으로 배포해야 한다.  다음은 user 서비스를 nodeport 서비스로 배포하는 yaml 스크립트이다. (Pod를 컨트롤하는 Deployment 스크립트는 생략하였다.)


users-svc-nodeport.yaml

apiVersion: v1

kind: Service

metadata:

 name: users-node-svc

spec:

 selector:

   app: users

 type: NodePort

 ports:

   - name: http

     port: 80

     protocol: TCP

     targetPort: 8080

 

같은 방식으로, Product 서비스도 아래와 같이 NodePort로 배포한다.

product-svc-nodeport.yaml

apiVersion: v1

kind: Service

metadata:

 name: products-node-svc

spec:

 selector:

   app: products

 type: NodePort

 ports:

   - name: http

     port: 80

     protocol: TCP

     targetPort: 8080

     

이때 별도로 nodeport를 지정해주지 않았는데, 자동으로 쿠버네티스 클러스터가 nodeport를 지정해준다.

아래와 같이 products-node-svc와 users-node-svc가 각각 배포된것을 확인할 수 있고, ClusterIP의 포트는 80, NodePort는 각각 31442, 32220으로 배포된것을 확인할 수 있다.




다음 Ingress를 생성해보자. 다음은 hello-ingress 라는 이름으로 위에서 만든 두개의 서비스를 라우팅해주는 서비스를 생성하기 위한 yaml  파일이다.

hello-ingress.yaml

apiVersion: extensions/v1beta1

kind: Ingress

metadata:

 name: hello-ingress

spec:

 rules:

 - http:

     paths:

     - path: /users/*

       backend:

         serviceName: users-node-svc

         servicePort: 80

     - path: /products/*

       backend:

         serviceName: products-node-svc

         servicePort: 80


spec 부분에, rules.http.paths 부분에, 라우팅할 path와 서비스를 정의해준다.

User 서비스는 /users/* URI인 경우 라우팅하게 하고, 앞에서 만든 users-node-svc로 라우팅하도록 한다. 이때 servicePort는 ClusterIP의 service port를 지정한다. (Google Cloud HTTP Load balancer를 이용하는 Ingress의 경우에는  실질적으로는 nodeport로 통신을 하지만 별도로 지정하지 않고 ingress가 자동으로 해당 서비스의 nodeport를 찾아서 맵핑이 된다. )
(참고 : https://kubernetes.io/docs/concepts/services-networking/ingress/

Lines 12-14: A backend is a service:port combination as described in the services doc. Ingress traffic is typically sent directly to the endpoints matching a backend.)


%kubectl create -f hello-ingress.yaml

을 실행하면 ingress가 생성이 되고 kubectl get ing 명령어를 이용하면 생성된 ingress를 확인할 수 있다.



Ingress 가 생성된 후, 실제로 사용이 가능하기까지는 약 1~2분의 시간이 소요된다. 물리적으로 HTTP 로드밸런서를 생성하고, 이 로드밸런서가 서비스가 배포되어 있는 노드에 대한 HealthCheck를 완료하고 문제가 없으면 서비스를 제공하는데, HealthCheck 주기가 1분이기 때문에, 1~2분 정도를 기다려 주는게 좋다. 그전까지는 404 에러나 500 에러가 날것이다.


준비가 끝난후, curl 명령을 이용해서 ingress의 URL에 /users/ 와 /products를 각각 호출해보면 각각, users를 서비스 하는 서버와, products를 서비스 하는 서버로 라우팅이 되서 각각 다른 메세지가 출력되는 것을 확인할 수 있다.




그러면 내부적으로 클라우드 내에서 Ingress를 위한 인프라가 어떻게 생성되었는지 확인해보자

구글 클라우드 콘솔에서 아래와 같이 Network services > Load balancing 메뉴로 들어가보자



아래와 같이 HTTP 로드밸런서가 생성이 된것을 확인할 수 있다.


이름을 보면 k8s는 쿠버네티스용 로드밸런서임을 뜻하고, 중간에 default는 네임 스페이스를 의미한다. 그리고 ingress의 이름인 hello-ingress로 생성이 되어 있다.

로드밸런서를 클릭해서 디테일을 들어가 보면 아래와 같은 정보를 확인할 수 있다.




3개의 백엔드 (인스턴스 그룹)이 맵핑되었으며, /users/*용, /products/*용 그리고, 디폴트용이 생성되었다.

모든 트래픽이 쿠버네티스 클러스터 노드로 동일하게 들어가기 때문에, Instance group의 이름을 보면 모두 동일한것을 확인할 수 있다. 단, 중간에 Named Port 부분을 보면 포트가 다른것을 볼 수 있는데, 31442, 32220 포트를 사용하고 있고, 앞에서 users, produtcs 서비스를 nodeport로 생성하였을때, 자동으로 할당된 nodeport이다.


개념적으로 다음과 같은 구조가 된다.


(편의상 디폴트 백앤드의 라우팅은 표현에서 제외하였다.)


Ingress에 접속되는 서비스를 LoadBalancer나 ClusterIP타입이 아닌 NodePort 타입을 사용하는 이유는, Ingress로 사용되는 구글 클라우드 로드밸런서에서, 각 서비스에 대한 Hearbeat 체크를 하기 위해서인데, Ingress로 배포된 구글 클라우드 로드밸런서는 각 노드에 대해서 nodeport로 Heartbeat 체크를 해서 문제 있는 노드를 로드밸런서에서 자동으로 제거나 복구가되었을때는 자동으로 추가한다.

Static IP 지정하기

서비스와 마찬가지로 Ingress 역시 Static IP를 지정할 수 있다.

서비스와 마찬가지로, static IP를 gcloud 명령을 이용해서 생성한다. 이때 IP를 regional로 생성할 수 도 있지만, ingress의 경우에는 global IP를 사용할 수 있다. --global 옵션을 주면되는데, global IP의 경우에는 regional IP와는 다르게 구글 클라우드의 망 가속 기능을 이용하기 때문에, 구글 클라우드의 100+ 의 Pop (Point of Presence)를 이용하여 가속이 된다.


조금 더 깊게 설명을 하면, 일반적으로 한국에서 미국으로 트래픽을 보낼 경우 한국 → 인터넷 → 미국 식으로 트래픽이 가는데 반해 global IP를 이용하면, 한국에서 가장 가까운 Pop (일본)으로 접속되고, Pop으로 부터는 구글 클라우드의 전용 네트워크를 이용해서 구글 데이타 센터까지 연결 (한국 → 인터넷 → 일본 Pop → 미국 ) 이 되기 때문에 일반 인터넷으로 연결하는 것 대비에서 빠른 성능을 낼 수 있다.


아래와 같이 gcloud 명령을 이용하여, global IP를 생성한다.




구글 클라우드 콘솔에서, 정적 IP를 확인해보면 아래와 같이 hello-ingress-ip 와 같이 IP가 생성되어 등록되어 있는 것을 확인할 수 있다.



Static IP를 이용해서 hello-ingress-staticip 이름으로 ingress를 만들어보자

다음과 같이 hello-ingress-staticip.yaml 파일을 생성한다.


apiVersion: extensions/v1beta1

kind: Ingress

metadata:

 name: hello-ingress-staticip

 annotations:

   kubernetes.io/ingress.global-static-ip-name: "hello-ingress-ip"

spec:

 rules:

 - http:

     paths:

     - path: /users/*

       backend:

         serviceName: users-node-svc

         servicePort: 80

     - path: /products/*

       backend:

         serviceName: products-node-svc

         servicePort: 80


이 파일을 이용하여, ingress를 생성한 후에, ingress ip를 확인하고 curl 을 이용해서 결과를 확인하면 다음과 같다.


Ingress with TLS

이번에는 Ingress 로드밸런서를 HTTP가 아닌 HTTPS로 생성해보겠다.


SSL 인증서 생성

SSL을 사용하기 위해서는 SSL 인증서를 생성해야 한다. openssl (https://www.openssl.org/)툴을 이용하여 인증서를 생성해보도록 한다.


인증서 생성에 사용할 키를 생성한다.

%openssl genrsa -out hello-ingress.key 2048

명령으로 키를 생성하면 hello-ingress.key라는 이름으로 Private Key 파일이 생성된다.




다음 SSL 인증서를 생성하기 위해서, 인증서 신청서를 생성한다.인증서 신청서 생성시에는 앞에서 생성한 Private Key를 사용한다.

다음 명령어를 실행해서 인증서 신청서 생성을 한다.

%openssl req -new -key hello-ingress.key -out hello-ingress.csr

이때 인증서 내용에 들어갈 국가, 회사 정보, 연락처등을 아래와 같이 입력한다.


인증서 신청서가 hello-ingress.csr 파일로 생성이 되었다. 그러면 이 신청서를 이용하여, SSL 인증서를 생성하자. 테스트이기 때문에 공인 인증 기관에 신청하지 않고, 간단하게 사설 인증서를 생성하도록 하겠다.


다음 명령어를 이용하여 hello-ingress.crt라는 이름으로 SSL 인증서를 생성한다.

%openssl x509 -req -day 265 -in hello-ingress.csr -signkey hello-ingress.key -out hello-ingress.crt



설정하기

SSL 인증서 생성이 완료되었으면, 이 인증서를 이용하여 SSL을 지원하는 ingress를 생성해본다.

SSL 인증을 위해서는 앞서 생성한 인증서와 Private Key 파일이 필요한데, Ingress는 이 파일을 쿠버네티스의 secret 을 이용하여 읽어드린다.


Private Key와 SSL 인증서를 저장할 secret를 생성해보자 앞에서 생성한 hello-ingress.key와 hello-ingress.crt 파일이 ./ssl_cert 디렉토리에 있다고 하자


다음과 같이 kubectl create secret tls 명령을 이용해서 hello-ingress-secret 이란 이름의 secret을 생성한다.

%kubectl create secret tls hello-ingress-serect --key ./ssl_cert/hello-ingress.key --cert ./ssl_cert/hello-ingress.crt


명령을 이용하여 secret을 생성하면, key 이라는 이름으로 hello-ingress.key 파일이 바이너리 형태로 secret에 저장되고 마찬가지로 cert라는 이름으로 hello-ingress.crt 가 저장된다.


생성된 secret을 확인하기 위해서

%kubectl describe secret hello-ingress-secret

명령을 실행해보면 아래와 같이 tls.key 와 tls.crt 항목이 각각 생성된것을 확인할 수 있다.


다음 SSL을 지원하는 ingress를 생성해야 한다.

앞에서 생성한 HTTP ingress와 설정이 다르지 않으나 spec 부분에 tls라는 항목에 SSL 인증서와 Private Key를 저장한 secret 이름을 secretName이라는 항목으로 넘겨줘야 한다.


hello-ingress-tls.yaml


apiVersion: extensions/v1beta1

kind: Ingress

metadata:

 name: hello-ingress-tls

spec:

 tls:

 - secretName: hello-ingress-secret

 rules:

 - http:

     paths:

     - path: /users/*

       backend:

         serviceName: users-node-svc

         servicePort: 80

     - path: /products/*

       backend:

         serviceName: products-node-svc

         servicePort: 80


이 파일을 이용해서 TLS ingress를 생성한 후에, IP를 조회해보자

아래와 같이 35.241.6.159 IP에 hello-ingress-tls 이름으로 ingress가 된것을 확인할 수 있고 포트는 HTTP 포트인 80 포트 이외에, HTTPS포트인 443 포트를 사용하는 것을 볼 수 있다.



다음 HTTPS로 테스트를 해보면 다음과 같이 HTTPS로 접속이 되는 것을 확인할 수 있다.



사설 인증서이기 때문에 위처럼 Not Secure라는 메세지가 뜬다. 인증서 정보를 확인해보면 아래와 같이 앞서 생성한 인증서에 대한 정보가 들어가 있는 것을 확인할 수 있다.



구글 클라우드 서버의 HTTP 포트를 SSH 로 터널링해서 로컬에서 접속하기


조대협 (http://bcho.tistory.com)


구글 클라우드 VM에 서버를 설치한 후 웹을 접근하고자 할때, 설치한 애플리케이션이 ACL (접근 제어) 처리가 안되어 있는 애플리케이션이 있을 수 있다. 특히 관리자 콘솔 같은 경우에 이런 경우가 많은데, 아파치 에어플로우 역시도 설치 후에 웹 서버를 띄우면 포트가 모두 퍼블릭으로 오픈되기 때문에 관리자만 액세스가 가능하도록 ACL 처리를 할 필요가 있다.


이를 위한 방법으로는 몇가지가 있는데


  1. 방화벽으로 특정 포트만 허용 하는 방법
  2. 앞에 nginx나 apache를 넣어서 HTTP BASIC AUTH등 인증 방식을 추가하는 방법
  3. Google Cloud Identity Aware Proxy를 이용하여, 구글 클라우드 계정 사용자에게 접근 권한을 부여 하는 방법
  4. 해당 HTTP 포트를 SSH로 터널링 하는 방법

1번 방법은 IP 가 바뀌면 접근 제어 하기가 번거로우니 패스, 2번은 웹서버 깔아야 하니 패스, 3번은 제일 좋은 방법인데, 로드밸런서등 구체적인 설정을 해야 해서 패스. 그래서 오늘은 가장 간단한 4번 방법을 설명한다.

4번 방법은 로컬에서 localhost:2222를 접속하면 구글 클라우드 상의 인스턴스:8080 으로 포워딩을 해준다. 이때 프로토콜을 SSH를 통해서 터널링이 된다.

매우 간단하게 할 수 있는데, 로컬에 gcloud SDK가 깔려 있을때

gcloud compute ssh {인스턴스명} --project {내프로젝트ID} --zone {인스턴스가 있는 존 이름} --ssh-flag="-L" --ssh-flag="{랩탑에서 접속할 포트번호}:localhost:{인스턴스의 포트 번호}"

로 기입해주면 된다.

예를 들어 terrycho-ml 프로젝트의 us-central1-f 존에 있는 hello-airflow 인스턴스의 8080 포트를 로컬에서  localhost:2222로 접근하도록 포워딩 설정을 할 경우 다음과 같이 하면 된다.


gcloud compute ssh hello-airflow --project terrycho-ml --zone us-central1-f --ssh-flag="-L" --ssh-flag="2222:localhost:8080"

 

REST API를 이용하여, 날씨를 조회하는 간단한 애플리케이션 만들기

 

조대협 (http://bcho.tistor


네트워크를 통한  REST API 호출 방법을 알아보기 위해서, 간단하게, 위도와 경도를 이용하여 온도를 조회해오는 간단한 애플리케이션을 만들어보자

이 애플리케이션은 경도와 위도를 EditText 뷰를 통해서 입력 받아서 GETWEATHER라는 버튼을 누르면 네트워크를 통하여 REST API를 호출 하여, 날씨 정보를 받아오고, 해당 위치의 온도(화씨) 출력해주는 애플리케이션이다.

 



 

날씨 API 는 http://www.openweathermap.org/ 에서 제공하는데, 사용법이 매우 간단하고, 별도의 API인증 절차가 필요하지 않기 때문에 쉽게 사용할 수 있다. 사용 방법은 다음과 같이 쿼리 스트링으로 위도와 경도를 넘겨주면 JSON  형태로 지정된 위치의 날씨 정보를 리턴해준다.

 

http://api.openweathermap.org/data/2.5/weather?lat=37&lon=127

 

아래는 리턴되는 JSON 샘플



이 리턴되는 값중에서 main.temp 에 해당하는 값을 얻어서 출력할 것이다.

 

날씨값을 저장하는 클래스 작성

 

package com.example.terry.simpleweather.client;

/**

 * Created by terry on 2015. 8. 27..

 */

public class Weather {
    int lat;
    int ion;
    int temprature;
    int cloudy;
    String city;

    public void setLat(int lat){ this.lat = lat;}
    public void setIon(int ion){ this.ion = ion;}
    public void setTemprature(int t){ this.temprature = t;}
    public void setCloudy(int cloudy){ this.cloudy = cloudy;}
    public void setCity(String city){ this.city = city;}

    public int getLat(){ return lat;}
    public int getIon() { return ion;}
    public int getTemprature() { return temprature;}
    public int getCloudy() { return cloudy; }
    public String getCity() { return city; }
}

 

다음으로 REST API를 호출하는 OpenWeatherAPIClient.java 코드를 다음과 같이 작성한다.

 

package com.example.terry.simpleweather.client;



import org.json.JSONException;

import org.json.JSONObject;


import java.io.BufferedInputStream;

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStream;

import java.io.InputStreamReader;

import java.net.HttpURLConnection;

import java.net.MalformedURLException;

import java.net.URL;


/**

 * Created by terry on 2015. 8. 27..

 * 목표

 * 1. AsyncTask와 HTTPURLConnection을 이용한 간단한 HTTP 호출 만들기

 * 2. 리턴된 JSON을 파싱하는 방법을 통하여, JSON 객체 다루는 법 습득하기

 * 3. Phone Location (GPS) API 사용 방법 파악하기

 *

 * 참고 자료 : http://developer.android.com/training/basics/network-ops/connecting.html

 * */

public class OpenWeatherAPIClient {

    final static String openWeatherURL = "http://api.openweathermap.org/data/2.5/weather";

    public Weather getWeather(int lat,int lon){

        Weather w = new Weather();

        String urlString = openWeatherURL + "?lat="+lat+"&lon="+lon;


        try {

            // call API by using HTTPURLConnection

            URL url = new URL(urlString);

            HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();

//            urlConnection.setConnectTimeout(CONNECTION_TIMEOUT);

//            urlConnection.setReadTimeout(DATARETRIEVAL_TIMEOUT);


            InputStream in = new BufferedInputStream(urlConnection.getInputStream());

            JSONObject json = new JSONObject(getStringFromInputStream(in));


            // parse JSON

            w = parseJSON(json);

            w.setIon(lon);

            w.setLat(lat);


        }catch(MalformedURLException e){

            System.err.println("Malformed URL");

            e.printStackTrace();

            return null;


        }catch(JSONException e) {

            System.err.println("JSON parsing error");

            e.printStackTrace();

            return null;

        }catch(IOException e){

            System.err.println("URL Connection failed");

            e.printStackTrace();

            return null;

        }


        // set Weather Object


        return w;

    }


    private Weather parseJSON(JSONObject json) throws JSONException {

        Weather w = new Weather();

        w.setTemprature(json.getJSONObject("main").getInt("temp"));

        w.setCity(json.getString("name"));

        //w.setCloudy();


        return w;

    }


    private static String getStringFromInputStream(InputStream is) {


        BufferedReader br = null;

        StringBuilder sb = new StringBuilder();


        String line;

        try {


            br = new BufferedReader(new InputStreamReader(is));

            while ((line = br.readLine()) != null) {

                sb.append(line);

            }


        } catch (IOException e) {

            e.printStackTrace();

        } finally {

            if (br != null) {

                try {

                    br.close();

                } catch (IOException e) {

                    e.printStackTrace();

                }

            }

        }


        return sb.toString();


    }

}

 

 

코드를 하나씩 뜯어보면

안드로이드에서 HTTP 호출을 하기 위해서는 HttpURLConnection이라는 클래스를 사용한다. URL이라는 클래스에 API를 호출하고자 하는 url주소를 지정하여 생성한후, url.openConnection()을 이용해서, HTTP Connection을 열고 호출한다.

URL url = new URL(urlString);

HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();

 

다음으로, 리턴되어 오는 문자열을 읽어서 JSON형태로 파싱을 해야 하는데,

InputStream in = new BufferedInputStream(urlConnection.getInputStream());

먼저 위와 같이 urlConnection으로 부터 getInputStream() 메서드를 통해서 InputStream을 리턴받고,

JSONObject json = new JSONObject(getStringFromInputStream(in));

getStringFromInput(InputStream xx)이라는 메서드를 이용하여 inputStream을 String으로 변환한후에 JSONObject로 변환한다. (여기서 getStringFromInput은 미리 정해진 메서드가 아니고 위의 소스코드 처럼 InputStream 을 String으로 변환하기 위해서 여기서 지정된 코드들이다.)

다음으로 해당 JSON을 파싱해서, main.temp 값을 읽은후 Weather class에 넣어서 리턴을 한다.

w = parseJSON(json);

w.setIon(lon);

w.setLat(lat);

에서 parseJSON이라는 메서드를 호출하는데, parseJSON은 다음과 같다.


private Weather parseJSON(JSONObject json) throws JSONException {

    Weather w = new Weather();

    w.setTemprature(json.getJSONObject("main").getInt("temp"));

    w.setCity(json.getString("name"));

    //w.setCloudy();


    return w;

}


위의 메서드에서는 json에서 먼저 getJSONObject(“main”)을 이용하여 “main” json 문서를 얻고, 그 다음 그 아래 있는 “temp”의 값을 읽어서 Weather 객체에 세팅한다.

 

여기까지 구현을 하면 REST API를 http를 이용하여 호출하고, 리턴으로 온 JSON 문자열을 파싱하여 Weather 객체에 세팅을해서 리턴하는 부분까지 완료가 되었다.

 

그다음 그러면 이 클래스의 메서드를 어떻게 호출하는가? 네트워크 통신은 IO작업으로 시간이 다소 걸리기 때문에, 안드로이드에서는 일반적으로 메서드를 호출하는 방식으로는 불가능하고, 반드시 비동기식으로 호출해야 하는데, 이를 위해서는 AsyncTask라는 클래스를 상속받아서 비동기 클래스를 구현한후, 그 안에서 호출한다.

다음은 OpenWeatherAPITask.java 클래스이다.

 

public class OpenWeatherAPITask extends AsyncTask<Integer, Void, Weather> {

    @Override

    public Weather doInBackground(Integer... params) {

        OpenWeatherAPIClient client = new OpenWeatherAPIClient();


        int lat = params[0];

        int lon = params[1];

        // API 호출

        Weather w = client.getWeather(lat,lon);


        //System.out.println("Weather : "+w.getTemprature());


        // 작업 후 리

        return w;

    }

}

 

위와 같이 AsyncTask를 상속받았으며, 호출은 doInBackground(..)메서드를 구현하여, 그 메서드 안에서 호출을 한다.

위의 코드에서 볼 수 있듯이, doInBackground에서 앞서 작성한 OpenWeatherAPIClient의 객체를 생성한후에, 인자로 넘어온 lat,lon값을 받아서, getWeahter를 통하여, 호출하였다.

 

이제 API를 호출할 준비가 모두 끝났다.

이제 UI를 만들어야 하는데, res/layout/activity_main.xml은 다음과 같다.

 

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"

    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"

    android:paddingRight="@dimen/activity_horizontal_margin"

    android:paddingTop="@dimen/activity_vertical_margin"

    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">




    <TextView

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:textAppearance="?android:attr/textAppearanceLarge"

        android:text="Temperature"

        android:id="@+id/tem"

        android:layout_below="@+id/tvLongtitude"

        android:layout_alignParentStart="true"

        android:layout_marginTop="46dp" />


    <Button

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:text="getWeather"

        android:id="@+id/getWeatherBtn"

        android:onClick="getWeather"

        android:layout_alignBottom="@+id/tem"

        android:layout_alignParentEnd="true" />


    <TextView

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:textAppearance="?android:attr/textAppearanceMedium"

        android:text="latitude"

        android:id="@+id/tvLatitude"

        android:layout_marginTop="27dp"

        android:layout_alignParentTop="true"

        android:layout_alignParentStart="true" />


    <TextView

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:textAppearance="?android:attr/textAppearanceMedium"

        android:text="longtitude"

        android:id="@+id/tvLongtitude"

        android:layout_marginRight="62dp"

        android:layout_marginTop="30dp"

        android:layout_below="@+id/lat"

        android:layout_alignParentStart="true" />


    <EditText

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:id="@+id/lat"

        android:width="100dp"

        android:layout_marginRight="62dp"

        android:layout_alignBottom="@+id/tvLatitude"

        android:layout_toEndOf="@+id/tvLatitude"

        android:layout_marginStart="36dp" />


    <EditText

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:width="100dp"

        android:id="@+id/lon"

        android:layout_marginRight="62dp"

        android:layout_alignBottom="@+id/tvLongtitude"

        android:layout_alignStart="@+id/lat" />


</RelativeLayout>

 

화면 디자인이 끝났으면, 이제 MainActivity에서 버튼을 누르면 API를 호출하도록 구현해보자.

 

package com.example.terry.simpleweather;


import android.support.v7.app.ActionBarActivity;

import android.os.Bundle;

import android.view.Menu;

import android.view.MenuItem;

import android.view.View;

import android.widget.EditText;

import android.widget.TextView;


import com.example.terry.simpleweather.client.OpenWeatherAPITask;

import com.example.terry.simpleweather.client.Weather;


import java.util.concurrent.ExecutionException;



public class MainActivity extends ActionBarActivity {


    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

    }


    @Override

    public boolean onCreateOptionsMenu(Menu menu) {

        // Inflate the menu; this adds items to the action bar if it is present.

        getMenuInflater().inflate(R.menu.menu_main, menu);

        return true;

    }


    @Override

    public boolean onOptionsItemSelected(MenuItem item) {

        // Handle action bar item clicks here. The action bar will

        // automatically handle clicks on the Home/Up button, so long

        // as you specify a parent activity in AndroidManifest.xml.

        int id = item.getItemId();


        //noinspection SimplifiableIfStatement

        if (id == R.id.action_settings) {

            return true;

        }


        return super.onOptionsItemSelected(item);

    }


    // MapView 참고 http://seuny.tistory.com/14

    public void getWeather(View view)

    {

        EditText tvLon = (EditText) findViewById(R.id.lon);

        String strLon = tvLon.getText().toString();

        int lon = Integer.parseInt(strLon);


        EditText tvLat = (EditText) findViewById(R.id.lat);

        String strLat = tvLat.getText().toString();

        int lat = Integer.parseInt(strLat);



        // 날씨를 읽어오는 API 호출

        OpenWeatherAPITask t= new OpenWeatherAPITask();

        try {

            Weather w = t.execute(lon,lat).get();


            System.out.println("Temp :"+w.getTemprature());


            TextView tem = (TextView)findViewById(R.id.tem);

            String temperature = String.valueOf(w.getTemprature());


            tem.setText(temperature);

            //w.getTemprature());



        } catch (InterruptedException e) {

            e.printStackTrace();

        } catch (ExecutionException e) {

            e.printStackTrace();

        }

    }

}

 

 

위에서 getWeather(View )부분에서 호출을하게 되는데, 먼저 앞에서 AsyncTask를 이용해서 만들었던 OpenWeatherAPITask 객체 t를 생성한 후에, t.execute로 호출을하면 된다.

실행 결과는 t.execute()에 대해서 .get()을 호출함으로써 얻을 수 있다.

 

이때 주의할점이 안드로이드는 네트워크 호출과 같은 리소스에 대한 접근을 통제하고 있는데, 이러한 통제를 풀려면, AnrdoidManifest.xml에 다음과 같이 INTERNET 과 NETWORK 접근을 허용하는 내용을 추가해줘야 한다.

<uses-permission android:name="android.permission.INTERNET" />

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

 

다음은 위의 내용을 추가한 AndroidManifest.xml이다.

 

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

    package="com.example.terry.simpleweather" >

    <uses-permission android:name="android.permission.INTERNET" />

    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />


    <application

        android:allowBackup="true"

        android:icon="@mipmap/ic_launcher"

        android:label="@string/app_name"

        android:theme="@style/AppTheme" >

        <activity

            android:name=".MainActivity"

            android:label="@string/app_name" >

            <intent-filter>

                <action android:name="android.intent.action.MAIN" />


                <category android:name="android.intent.category.LAUNCHER" />

            </intent-filter>

        </activity>


    </application>


</manifest>

 

참고 : http://developer.android.com/training/basics/network-ops/connecting.html




9월 15일 추가 내용

  • t.execute().get 보다는  onPostExecute 를 통해서 리턴 값을 받는 것이 더 일반적인 패턴
  • AsyncTask를 사용하기 보다는 근래에는 Retrofit이라는 프레임웍을 사용하는게 더 효율적이며, 성능도 2배이상 빠름.
    관련 튜토리얼 : http://gun0912.tistory.com/30
    아래는 성능 비교 자료