Spring for Kubernetes

스프링으로 개발된 java 서비스를 kubernetes 기반에 서비스하는 방법에 대해서 간략히 살펴보자. 이를 쉽게하는 maven 라이브러리가 존재하는데 아래와 같이 pom.xml에 정의하고 사용할 수 있다.

	<properties>
		<java.version>1.8</java.version>
		<docker.prefix>demo</docker.prefix>
		<spring-cloud-dependencies.version>Finchley.RELEASE</spring-cloud-dependencies.version>
        <spring.cloud.k8s.version>1.0.1.RELEASE</spring.cloud.k8s.version>
	</properties>
	
	<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud-dependencies.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-kubernetes-dependencies</artifactId>
                <version>${spring.cloud.k8s.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

aspectjweaver 모듈의 버전의 충돌이 있어 aspectj 라이브러리를 별도로 정의하게 된다. spring boot 의 버전을 2.0.x 에 맞추면 추가로 정의할 필요가 없다. 2.1 버전에 맞는 모듈이 나오는 것을 확인하고 boot 버전을 올리자.
현재 문서 작성 시점에는 2.0.9 버전을 사용하였다.

ConfigMap

동일 서비스가 각기 다른 환경에서 수행될 수 있다. 이를 위해서 profile 방식으로 속성 설정 파일을 구분해서 서비스가 동작하도록 구성되었다. 도커 기반의 배포에서 다양한 환경으로의 배포가 쉬워지면서 결국 다양한 설정 파일들이 필요한데 이를 컨테이너 기반의 동적인 설정 방식이 필요하다.

kubenetes 기반의 configmap 을 사용하기 위해서 pom.xml 에 아래와 같이 라이브러리를 정의해 주어야 한다.

	<dependency>
	    <groupId>org.springframework.cloud</groupId>
	    <artifactId>spring-cloud-starter-kubernetes</artifactId>
	</dependency>
	<dependency>
	    <groupId>org.springframework.cloud</groupId>
	    <artifactId>spring-cloud-starter-kubernetes-config</artifactId>
	</dependency>

config class

서비스 속성을 정의한 application.yaml 파일의 설정 값을 받을 수 있도록 config class 를 아래와 같이 작성한다.

@Configuration
@ConfigurationProperties(prefix="default")
public class DefaultConfig {
	private String name = "undefined";

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
}

일반적인 방식으로 /src/main/resources 에 application.yaml 파일을 아래와 같이 정의한다.

spring:
  application.name: hello
server.port: 8080
default.name: Default

서비스에 접근하면 설정값을 반환하는 controller 를 아래와 같이 작성한다.

@RestController
class HelloController {
    @Autowired
    private DefaultConfig config;
    
	@RequestMapping("/")
	public String index() {
		String response = "Hello, world.(" + config.getName() + ")";
		System.out.println(response);
		return response;
	}
}

maven 빌드해서 도커 이미지를 등록하고 kubectl create 명령으로 배포한다. curl 프로그램으로 해당 서비스를 접근하면 아래와 같이 application.yaml 에 정의한 값으로 결과가 나타난다.

$ curl $(minikube service hello --url)
Hello, world.(Default)

create ConfigMap

YAML 파일로 아래와 같이 설정값을 정의한다. @ConfigurationProperties 를 이용해서 설정 클래스를 만들 수 있도록 applicaltion.properties 키 이름에 embeded 하여 설정값들을 정의한다. kubectl create 로 등록한다.

apiVersion: v1
kind: ConfigMap
metadata:
  name: hello
data:
  application.properties: |-
    default.name=Hello
    special.name=SpecialName

application.yaml 설정 파일에 kubernetes 의 config map 을 사용하도록 아래와 같이 수정한다. spring.cloud.kubernetes.config 항목이 추가된 것을 볼 수 있다. ConfigMap 이 hello 라는 이름으로 등록되어 있어서 이를 지칭하도록 구성한다.

spring:
  application.name: hello
  cloud.kubernetes:
    reload.enabled: true
    config:
      sources:
        - name: hello
server.port: 8080
default.name: Default

kubectl delete/create 로 위에 정의한 서비스를 다시 배포한다. spring java가 다 로딩되도록 잠시 기다린 후에 다시 서비스를 접근하여 결과를 보면 config map 에 정의된 값이 적용되었음을 알 수 있다.

$ curl $(minikube service hello --url)
Hello, world.(Hello)

RBAC

그러나 처음 서비스를 배포하고 설정을 잘 가져오는지 확인해보면 아직 생성된 값(Hello)이 바뀌지 않고 기존 설정값(Default)이 그대로 임을 알 수 있다. 뭔가 아직 더 남은 작업이 있음을 알 수 있다. 원인을 알기 위해 pod 의 로그를 확인하면 아래와 같은 오류를 발견할 수 있다.

2019-05-15 00:18:28.761 WARN 1 — [ main] o.s.cloud.kubernetes.StandardPodUtils : Failed to get pod with name:[hello-5d45d95969-brn7x]. You should look into this if things aren’t working as you expect. Are you missing serviceaccount permissions?
io.fabric8.kubernetes.client.KubernetesClientException: Failure executing: GET at: https://10.96.0.1/api/v1/namespaces/default/pods/hello-5d45d95969-brn7x. Message: Forbidden!Configured service account doesn’t have access. Service account may have been revoked. pods “hello-5d45d95969-brn7x” is forbidden: User “system:serviceaccount:default:default” cannot get resource “pods” in API group “” in the namespace “default”.

Kubernetes 클러스터는 역할 기반의 권한관리를 하고 있다. 실제 계정은 다른 계정관리 시스템에서 관리하고 이와 연계해서 권한관리가 되고 있다. 위의 오류 메시지를 토대로 현재의 서비스는 default 계정으로 동작하고 있고 configmap 에 접근할 수 있는 역할을 배정받지 못했기 때문이다. 가장 간단한 방법은 모든 권한을 가지고 있는 admin 역할을 default 계정에 부여하면 된다. 물론 운영환경에서는 위험한 결정일 수 있다. 아래 명령으로 계정에 권한을 부여하자.

kubectl create clusterrolebinding sa-admin \
--clusterrole=cluster-admin --serviceaccount=default:default

kubectl delete/create 로 재 배포하면 아래와 같이 설정값이 바뀌어서 나온다.

$ curl $(minikube service hello --url)
Hello, world.(Hello) 

TODO: 현재 reload 는 반영이 안되고 있음. 재배포를 해야 반영이 됨.

Ribbon

외부 서비스를 호출하는 경우 장애가 발생하면 대처하기 어렵다. 장애에 대비한 처리를 쉽게 추가할 수 있게 한다.

아래와 같이 pom.xml 에 ribbon 관련 라이브러리를 추가한다.

	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
	</dependency>
	<dependency>
	    <groupId>org.springframework.cloud</groupId>
	    <artifactId>spring-cloud-starter-kubernetes</artifactId>
	</dependency>
	<dependency>
	    <groupId>org.springframework.cloud</groupId>
	    <artifactId>spring-cloud-starter-kubernetes-ribbon</artifactId>
	</dependency>

application.yaml 파일에 아래와 같이 ribbon 의 기본 설정 값을 지정한다.

backend:
  ribbon:
    eureka:
      enabled: false
    client:
      enabled: true
    ServerListRefreshInterval: 5000

hystrix.command.BackendCall.execution.isolation.thread.timeoutInMilliseconds: 5000
hystrix.threadpool.BackendCallThread.coreSize: 5

Ribbon 이 동작할 방식을 아래와 같이 설정 클래스를 통하여 정의한다. 상대 서비스의 확인 방법으로 ping url 을 사용하고 서버 관리 방식은 장애있는 서버는 다시 회복할 때까지 제외하는 방식을 규칙으로 사용한다. 빌드하기 전에 maven update 하는 것 잊지말자.

public class RibbonConfig {

	@Autowired
	IClientConfig ribbonClientConfig;

	@Bean
	public IPing ribbonPing(IClientConfig config) {
		return new PingUrl();
	}

	@Bean
	public IRule ribbonRule(IClientConfig config) {
		return new AvailabilityFilteringRule();
	}

}

application 이 필요한 모듈들과 같이 동작하도록 아래와 같이 annotation 들을 정의한다. ribbon client가 위에서 정의한 설정 클래스를 사용하도록 정의해 준다. 클러스터에 배포된 이름으로 서비스를 찾을 수 있도록 discovery client 도 사용한다.

@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
@RibbonClient(name = "name-service", configuration = RibbonConfig.class)
public class HelloDockerApplication {
	@LoadBalanced
	@Bean
	RestTemplate restTemplate() {
		return new RestTemplate();
	}

호출할 대상의 서비스를 관리하는 클래스를 아래와 같이 작성한다. 호출에 실패할 경우 대신 처리할 함수의 이름을 선언하게 되어있다.

@Service
public class SlackService {

	private final RestTemplate restTemplate;

	public SlackService(RestTemplate restTemplate) {
		this.restTemplate = restTemplate;
	}

	@HystrixCommand(fallbackMethod = "getFallbackStatus", commandProperties = {
			@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000") })
	public String getStatus() {
		return this.restTemplate.getForObject("https://name-service/", String.class);
	}

	private String getFallbackStatus() {
		return "{ \"health\": false }";
	}
	
    public String getUnsafeStatus() {
        return this.restTemplate.getForObject("http://name-service/", String.class);
    }

}

이제 서비스의 기능을 API 로 노출하도록 controller 에 함수를 추가한다.

@RestController
class HelloController {
    @Autowired
    private NameService service;
    
...
	
	@GetMapping("/health")
	public String slackCheck(@RequestParam(name="safe", 
	    defaultValue="true") boolean safe) {
		if (safe)
			return service.getStatus();
		else
			return service.getUnsafeStatus();
	}
}

kubectl 로 배포하고 아래 명령으로 실행 결과를 보자. 처음 실행하면 서비스 연결에 시간이 걸린다. 제한 시간을 1초로 해서 오류가 발생한다. 하지만 다시 실행하면 아래와 같이 성공되었음을 볼 수 있다.

$ curl $(minikube servith hello --url)/health
{ "health": true }

아래 명령으로 name-service 를 삭제하고 장애를 확인해 보자

kubectl delete -f name-service.yaml 

동일할 명령을 실행해 보면 아래와 같다.

$ curl $(minikube service hello --url)/health
{ "healthy": false }

보통 연관 서비스에 문제가 생기면 이를 이용한 서비스도 장애로 이어진다. 하지만 fallback 함수를 정의해서 문제 발생 시 대처할 수 있다. 위와 같이 오류가 발생하지 않고 결과를 보여줄 수 있다. 좀더 수준을 높인다면 캐시 등을 활용해서 사용하는 서비스의 장애에 대비하는 처리를 추가할 수 있겠다. 만일 ribbon 을 사용하지 않으면 아래와 같이 오류 메시지가 나타난다.

$ curl $(minikube service hello --url)/health?safe=false
{"timestamp":"2019-05-21T09:21:00.157+0000","status":500,"error":"Internal Server Error","message":"I/O error on GET request for \"http://name-service/\": com.netflix.client.ClientException: Number of retries on next server exceeded max 1 retries, while making a call for: name-service:80; nested exception is java.io.IOException: com.netflix.client.ClientException: Number of retries on next server 
exceeded max 1 retries, while making a call for: name-service:80","path":"/health"}

Ribbon 은 해당 서비스가 살아있는지 주기적으로 점검한다. 해당 서비스를 다시 배포해서 실행시키지 않으면 아래와 같은 로그가 쌓이게 된다.

Did not find any endpoints in ribbon in namespace [default] for 
name [name-service] and portName [null]

Secret

데이터베이스 접근에 필요한 계정 및 비번 같은 중요한 값들은 노출되지 않도록 보관 되어야 한다. 이러한 키와 값들을 관리하는 좋은 방법이 secret 이다.

kubectl create secret generic name-secret \
--from-literal=username=app_admin \
--from-literal=password=XpqlrmfOD328#\!2

비밀번호의 특수문자는(예 !) 앞에 \ 문자로 표기(예 \!)를 해주어야 한다. –from-file 옵션의 경우 파일을 지정할 수 있는데 파일 안에 기록할 경우는 그럴 필요가 없다.

아래와 같이 등록된 secret 을 확인할 수 있다. yaml 형식으로 그 내용을 볼 수 있는데 정의된 키명은 평문으로 보이고 그 값은 base64 로 변환되어 보여진다.

$ kubectl get secrets
NAME                TYPE                                DATA   AGE
name-secret         Opaque                              2      55s
default-token-5q86c kubernetes.io/service-account-token 3      7d
$ kubectl get secret name-secret -o yaml
apiVersion: v1
data:
  password: WHBkbHFtZk9EMzI4IyEy
  username: YXBwX2FkbWlu
kind: Secret
metadata:
  creationTimestamp: "2019-05-14T04:21:15Z"
  name: db-user-pass
  namespace: default
  resourceVersion: "157074"
  selfLink: /api/v1/namespaces/default/secrets/db-user-pass
  uid: b68ef20b-75ff-11e9-bd6c-080027aba474
type: Opaque

서비스에서 데이터베이스에 연결하려면 이렇게 정의된 secret 를 사용할 수 있어야 한다. 서비스를 배포하는 yaml 파일에 컨테이너가 실행하는 환경변수로 선언할 수 있다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: name-service
spec:
  ...
  template:
    metadata:
      labels:
        app: name-service
    spec:
      containers:
        ...
        - env:
            - name: DB_USERNAME
              valueFrom:
                secretKeyRef:
                  name: name-secret
                  key: username
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: name-secret
                  key: password

이전에 다룬 선언들은 … 표시로 생략하였다.

이제 컨테이너가 실행될 때, 환경 변수로 DB_USERNAME, DB_PASSWORD가 설정된다. 이 값을 application.properties 에 전달할 수 있도록 ${key_name} 형식으로 정의할 수 있다.

spring:
  application:
    name: name-service
  cloud.kubernetes:
    reload.enabled: true
    secrets.name: name-secret
  datasource: 
    url: jdbc:mysql://name-rds.cgbxh8v0llox.ap-northeast-2.rds.amazonaws.com:3306/database123?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
    driverClassName: com.mysql.jdbc.Driver
    maxActive: 500
  data.jpa.repositories.enabled: true
  jpa:
    hibernate.ddl-auto: update
    show-sql: false
    
server:
  port: 8080

기존의 속성 설정값에서 username, password 만 secret에 맞게 설정하였다. 서비스를 다시 배포하면 소스 상에는 정의되어있지 않지만 실행 시점에 컨테이너에 전달된 값으로 데이터베이스에 접속이 되는 것을 확인할 수 있다.

Reference

https://github.com/spring-cloud/spring-cloud-kubernetes
https://github.com/eugenp/tutorials/tree/master/spring-cloud/spring-cloud-kubernetes
https://medium.com/containerum/configuring-permissions-in-kubernetes-with-rbac-a456a9717d5d
https://github.com/wardviaene/kubernetes-course
https://www.poeticoding.com/create-a-high-availability-kubernetes-cluster-on-aws-with-kops/

Getting start with Minikube

개발자 환경에서 간단하게 사용할 수 있는 k8s가 있어 시험적으로 사용하고 그 진행 과정을 정리해 보았다. 늘 기억의 한계가 있고 이렇게나마 기록을 해야 반복적인 실수를 줄일 수 있을 것 같아 거칠게라도 끄적여 본다.

Setup for mac os

설치를 편하게 하기 위해서 brew cask 를 사용한다. brew 에 설치할 수 있는 목록에 없는 앱들을 설치할 수 있다. brew-cask 참고 사이트

install docker

brew cask install docker

위의 명령으로 도커를 설치하면 docker, kubectl 명령을 사용할 수 있다.

install VM

맥에서 사용할 수 있는 vm 중에서 virtualbox 가 일반적이어서 이를 설치한다. vmware도 가능하나 유료이다. xhyve 역시 가능했지만 deprecate 되어서 최신 버전에서는 사용할 수 없다.

brew cask install virtualbox

설치 중에 오류가 나는데 맥 설정에서 oracle inc 의 앱들에 대해서 허가해 주고 다시 설치하면 성공적으로 설치가 완료된다.

install minikube

brew cask install minikube
minikube config set vm-driver virtualbox

minikube 설정 명령은 이전에 설치된 cluster 에 대해서 영향을 미치지 않기 때문에 삭제하고 다시 실행하라는 주의 사항이 나온다. 처음 실행 것이라면 흘려 넘기면 된다.

/usr/local/bin 아래에 kubectl, minikube 가 생성이 된다.

아래 명령으로 실행해보자.

$ minikube start
😄  minikube v1.0.1 on darwin (amd64)
🤹  Downloading Kubernetes v1.14.1 images in the background ...
🔥  Creating virtualbox VM (CPUs=2, Memory=2048MB, Disk=20000MB) ...
📶  "minikube" IP address is 192.168.99.100
🐳  Configuring Docker as the container runtime ...
🐳  Version of container runtime is 18.06.3-ce
⌛  Waiting for image downloads to complete ...
✨  Preparing Kubernetes environment ...
💾  Downloading kubelet v1.14.1
💾  Downloading kubeadm v1.14.1
🚜  Pulling images required by Kubernetes v1.14.1 ...
🚀  Launching Kubernetes v1.14.1 using kubeadm ... 
⌛  Waiting for pods: apiserver proxy etcd scheduler controller dns
🔑  Configuring cluster permissions ...
🤔  Verifying component health .....
💗  kubectl is now configured to use "minikube"
🏄  Done! Thank you for using minikube!

기존의 설치된 kubectl 이 버전이 안 맞을 수 있다. kubectl version 명령을 이용해서 클라이언트와 서버의 버전을 비교해 보면 알 수 있다. 안 맞는 경우는 minikube 를 설치하며 기존의 kubectl 을 대체하지 못해서 발생한다. 아래와 같이 이전 버전을 수동으로 대체시키면 된다.

sudo mv /usr/local/bin/kubectl /usr/local/bin/kubectl.org
brew link --overwrite kubernetes-cli

build container image

command line

자바 프로젝트에 아래와 같은 내용으로 Dockerfile 을 생성한다.

FROM openjdk:8-jre-alpine
VOLUME /tmp
ARG JAR_FILE
ADD target/${JAR_FILE} ${JAR_FILE}
ENV JAVA_OPTS=""
ENV JARFILE /${JAR_FILE}
EXPOSE 8080
ENTRYPOINT [ "sh", "-c", "java -Dspring.profiles.active=dev -jar $JARFILE" ]

JDK 는 8버전이고 가장 작은 크기인 jre-alpine 을 선택하였다. ADD 명령으로 jar 파일을 도커 안으로 추가한다. 컨테이너가 로딩되면 바로 실행할 명령을 ENTRYPOINT 에 기록한다. 바로 자바 프로젝트를 개발 환경으로 실행하는 명령이다. 실제 운영환경에서는 profile 을 dev 에서 prd 로 바꾸면 된다.

jar 파일을 JARFILE 변수명으로 처리한 것은 매번 배포할 때마다 바뀌는 버전에 따라 이 파일을 수정하지 않도록 변수로 처리하였다. pom.xml 파일에 지정된 jar 파일명을 이미지 빌드 시에 변수로 전달 받을 수 있다. dockerfile 에서 ARG 로 선언한다. java 실행 명령에도 파일명이 필요한데 sh 환경변수로 정의해서 명령어에 반영되도록 처리하였다.

도커 빌드를 사용해서 위에 정의된 dockerfile 에 따라 컨테이너 이미지를 빌드하기 위해 아래 명령을 실행한다.

docker build --build-arg JAR_FILE=hello-1.0.jar \
--tag demo/hello:1.0 .

이미지가 완성이 되면 아래 명령으로 컨테이너로 실행시킨다.

docker run -p 8080:8080 -d --rm --name hello -t demo/hello:1.0

-d 옵션으로 독립적으로 실행 되므로 터미널 창에서 다른 작업이 가능하다. –rm 옵션으로 컨테이너를 중지시키면 컨테이너 자체가 사라진다. 아니면 docker rm 명령으로 지워야한다. 아래 명령으로 컨테이너 실행 여부를 확인한다.

$ docker ps
CONTAINER ID  IMAGE           COMMAND                  CREATED
      STATUS              PORTS                    NAMES
ccd00eafafb6  demo/hello:1.0  "sh -c 'java -Dsprin…"   14 minutes ago
      Up 14 minutes       0.0.0.0:8080->8080/tcp   hello

컨테이너는 아래 명령으로 중지시킬 수 있다. 위 옵션 때문에 중지 후 바로 삭제가 된다. –rm 옵션이 없다면 STATUS 에 Exit 로 바뀌어 있고 목록에는 계속 나타난다.

docker stop hello

maven build

Maven 빌드를 이용해서 이미지를 생성할 수 있는데 dockerfile-maven-plugin 과 docker-maven-plugin 플러그인을 사용하면 된다. 아래와 같이 pom.xml 파일에 정의하면 된다.

우선 docker registry 에 동일 버전으로 계속 push 하게 되면 tag 명이 없이 이미지들이 쌓이게 된다. 따라서 아래와 같이 clean 단계에서 이미지를 지우는 작업을 추가할 수 있다.

   <build>
        <plugins>
            <plugin>
                <groupId>com.spotify</groupId>
                <artifactId>docker-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <id>remove-image</id>
                        <phase>clean</phase>
                        <goals>
                            <goal>removeImage</goal>
                        </goals>
                        <configuration>
                            <imageName>${docker.prefix}/${project.artifactId}</imageName>
                            <imageTags>
                                <imageTag>${project.version}</imageTag>
                            </imageTags>
                            <verbose>true</verbose>
                        </configuration>
                    </execution>
                </executions>
                </plugin>

다음은 빌드된 jar 파일을 위에서 만든 dockerfile 에 따라 컨테이너 이미지에 넣기 위해 플러그인을 선언한다. 플러그인의 선언 순서는 “spring-boot-maven-plugin” 다음으로 위치해야 정상적으로 빌드된 jar 파일에 작업할 수 있다.

<plugin>
    <groupId>com.spotify</groupId>
    <artifactId>dockerfile-maven-plugin</artifactId>
    <version>1.4.3</version>
    <configuration>
        <repository>${docker.prefix}/${project.artifactId}</repository>
        <tag>${project.version}</tag>
        <buildArgs>
            <JAR_FILE>${project.build.finalName}.jar</JAR_FILE>
        </buildArgs>
    </configuration>
    <executions>
        <execution>
            <id>default</id>
            <phase>package</phase>
            <goals>
                <goal>build</goal>
            </goals>
        </execution>
    </executions>
</plugin>

변수로 선언된 부분에 들어가는 값을 아래와 같이 속성으로 정의해 주어야 합니다.

<properties>
    <java.version>1.8</java.version>
    <docker.prefix>demo</docker.prefix>
</properties>

프로젝트 폴더를 선택하고 마우스 우클릭으로 Run as > Maven build 를 하면 clean compile package 과정을 거치면서 위에 선언한 build goal 을 실행하게 된다. 처리 결과는 docker build 명령의 실행결과와 동일함을 알 수 있다.

run under minikube

minikube 의 동작 환경에 맞게 현재 터미널 세션의 환경 설정을 해야한다. 간단히 다음 명령으로 한번에 설정이 가능하다.

eval $(minikube docker-env)

위에서 생성한 image 를 다음 명령으로 실행할 수 있다. 옵션 중에 image-pull-policy 를 사용하는데 이는 로컬의 컨테이너 이미지를 사용하기 위해서는 Never 로 지정해 주어야 외부 docker 저장소에서 다운 받지 않고 로컬 이미지를 사용하게 되기 때문이다.

kubectl run hello --image=demo/hello:1.0 --port=8080 --image-pull-policy=Never

아래 명령으로 실행 결과를 확인할 수 있다.

$ kubectl get deployments
NAME      DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
hello     1         1         1            1           12s
$ kubectl get pods
NAME                   READY     STATUS    RESTARTS   AGE
hello-99f6c69f7-nj86p  1/1       Running   0          2m

주의사항

eclipse 에서 빌드하는 경우 docker 이미지를 찾을 수 없어서 STATUS 항목이 ErrImageNeverPull 로 표시가 된다. docker image list 명령으로 찾아 보면 해당 이름으로 등록된 이미지가 없다. 하지만 터미널에서 mvn 명령으로 빌드한 경우에는 등록이 된다. 이유는 eclipse 실행 환경과 터미널에 설정된 환경이 다르다. 위에서 터미널에서 실행한 환경 설정이 eclipse 에서는 안 되어있기 때문이다. 누락된 환경 설정을 eclipse 에 정의하여야 한다.

프로젝트 폴더 우클릭하고 Rus as > Run Configurations 을 클릭하면 maven 빌드 설정을 할 수 있는 창이 열린다. Environment 탭에 아래와 같이 환경변수와 값을 등록해주어야 한다. 값들이 사용자 환경마다 다르므로 minikube docker-env 명령으로 설정 값을 확인하고 맞춰주어야 한다.

변수명
DOCKER_HOST tcp://192.168.99.100:2376
DOCKER_CERT_PATH /Users/yourname/.minikube/certs

위의 설정이 없는 경우 minikube 에 도커 이미지를 등록할 수 없다. 현재 개발자의 pc 안에만 남기 때문에 minikube 에서는 이 이미지를 가지고 작업을 할 수 없다. 따라서 ErrImageNeverPull 라는 오류가 발생하는 것이다.

컨테이너 이미지가 pod에 배포가 성공적으로 되었다면 외부에서 이 컨테이너로 접근 할 수 있도록 아래 명령으로 노출을 시켜줘야 한다.

$ kubectl expose deployments hello --type=NodePort
service "hello" exposed

아래 명령은 브라우저를 실행해서 해당 웹 페이지를 볼 수 있게 한다.

curl $(minikube service hello-minikube --url)

만일 배포된 컨테이너 버전을 바꾸고 싶다면 (버전이 올라간 경우), 아래와 같이 명령을 실행하면 된다.

kubectl set image deployment/hello hello=demo/hello:1.1

using configuration file

위와 같이 명령을 이용하여 각각 배포와 서비스 노출을 하는 방법도 있으나 설정 파일을 만들고 필요한 설정을 미리 만들어서 한번에 실행하는 것이 더 편하다. 아래와 같이 YAML 파일을 만든다.

kind: Service
apiVersion: v1
metadata:
  name: hello
spec:
  selector:
    app: hello
  ports:
  - protocol: TCP
    port: 8080
    nodePort: 30001
  type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello
spec:
  selector:
    matchLabels:
      app: hello
  replicas: 1
  template:
    metadata:
      labels:
        app: hello
    spec:
      containers:
        - image: demo/hello-docker:1.0
          imagePullPolicy: Never
          name: hello
          ports:
            - containerPort: 8080

설정 파일을 지정해서 kubectl 을 실행한다.

kubectl create -f hello-docker.yaml

위의 명령으로 배포와 서비스 노출이 한번에 이루어진다. 아래 명령으로 서비스 생성까지 확인이 가능하다.

$ kubectl get services
NAME       TYPE       CLUSTER-IP   EXTERNAL-IP  PORT(S)         AGE
hello      NodePort   10.108.87.36 <none>       8080:30001/TCP  7m38s
kubernetes ClusterIP  10.96.0.1    <none>       443/TCP         5d20h

ending service

아래 명령으로 pod 에 실행되는 컨테이너를 종료할 수 있다.

kubectl delete service hello
kubectl delete deployment hello

만일 yaml 파일을 이용해서 실행한 경우라면 아래 명령으로 중지시킬 수 있다.

kubectl delete -f hello-docker.yaml

이력을 깨끗이 지우고 싶다면 아래와 같이 한다.

docker rmi demo/hello:1.0 -f
minikube stop
eval $(minikube docker-env -u)
minikube delete

Reference

https://howtodoinjava.com/docker/docker-hello-world-example/

https://kubernetes.io/ko/docs/setup/minikube/

Packaging a Java Application in a Docker Image with Maven

API Gateway

마이크로 서비스를 운영하기 위해서는 작은 기능들을 개발하고 수시로 배포하게 되는데 이런 기능들을 손쉽게 외부에 제공하는 것이 용이 하지는 않습니다. 누구나에게 오픈된 기능이라도 비정상적인 접근에 대해 차단이 필요하기도 하고 – 요즘 외부에 기능이 오픈되면 바로 admin 계정을 얻기 위한 불법저긴 시도들이 로그에 넘쳐납니다 – 그런 인증에 대한 고급 기능(예를 들어 oauth2 지원 등)이 아니더라도 간단한 로그를 남기려 한다면 서비스 마다 로그 기능을 추가하는 것이 마이크로 하다고 보기는 어렵습니다.

뿐만 아니라 서비스 운영 중에 패치나 업그레이드가 필요한 경우 끊김 없는 안정적인 – 동적으로 빌드해서 서비스하는 경우도 있지만 – 서비스를 하려 한다면 2대 이상의 서버에 서비스를 구성하고 패치하는 동안 1대로 운영을 하는 방법이 필요할 수 있습니다. 보통은 load balancing 을 위해 장비를 두고 장애 있는 서버는 온라인 상에서 제외하는 방식을 그대로 응용한 것이라고 볼 수 있습니다. 어찌 보면 일시적이지만 패치나 배포하는 중의 서비스 중단은 장애라고 볼 수 있기 때문입니다.

         +----------+                 +----------+
         |    L4    |                 |  Gateway |
         +----+-+---+                 +----+-+---+
              | |                          | |
        +-----+ +----+               +-----+ +----+
        |            |               |            |
        v            v               v            v
   +----------+  +---+-----+    +----------+  +---+-----+
   | Service  |  | Service |    | Service  |  | Service |
   |    1     |  |    2    |    |    1     |  |    2    |
   +----------+  +---------+    +----------+  +---------+

마이크로 서비스는 보통 쉽게 사용할 수 있는 REST API 형태로 서비스를 제공합니다. 결과물로 받을 수 있는 데이터의 형태도 json 타입으로 자바스크립트에서 객체 형태로 쉽게 변환해서 조작하기에 매우 쉽습니다. 웹 서비스 형태로 제공되던 xml 형태와 비교하면 크기도 작고 눈으로 봐도 이해가 쉬운 형식으로 되어 있습니다. (가독성을 위해 jsonformatter 사이트를 자주 이용합니다.)

$ curl https://jsonplaceholder.typicode.com/users/1
{
  "id": 1,
  "name": "Leanne Graham",
  "username": "Bret",
  "email": "Sincere@april.biz",
  "address": {
    "street": "Kulas Light",
    "suite": "Apt. 556",
    "city": "Gwenborough",
    "zipcode": "92998-3874",
    "geo": {
      "lat": "-37.3159",
      "lng": "81.1496"
    }
  },
  "phone": "1-770-736-8031 x56442",
  "website": "hildegard.org",
  "company": {
    "name": "Romaguera-Crona",
    "catchPhrase": "Multi-layered client-server neural-net",
    "bs": "harness real-time e-markets"
  }
}

위와 같이 curl 프로그램을 사용해서 간단하게 REST API를 호출해서 사용자의 정보를 조회해 보았습니다.  json 형식이 간단하고 쉽게 눈에 들어오는 모양이라는 것을 아실 수 있습니다.

마이크로 서비스는 위 예제에서 보신 것처럼 여러 곳에서 제공되는 작은 기능들을 가져와서 사용할 수 있고 직접 만들어서 제공하기도 합니다. 요즘 전자 상거래 사이트에서 구매 결제를 직접 제공하지 않고 사이트 외부의 결제 서비스를 이용하여 결제할 수 있게 하는 것과 마찬가지 입니다. 이렇게 내부 외부의 흩어져 있는 서비스를 하나의 gateway에서 묶어서 제공하므로 복잡함과 수고로움을 덜기도 합니다. 예를 들어 자신의 PC 에 gateway 를 설치하고 실행시켜서 위 예제 사이트 주소를 등록해 놓으면 개발하지 않고도 자신의 PC에서 동일하게 동작하는 것을 볼 수 있습니다. 세팅 방법은 다음에 자세히 설명하겠습니다.

$ curl http://localhost:8080/users/1

API 를 서비스 한다는 것은 제공하는 주소로 많은 사용자들이 호출하고 있다는 것을 의미합니다. 새로운 기능을 더 추가해서 서비스하려 할 때도 기존의 기능을 사용하는 사용자를 고려할 필요가 있습니다. 그래서 새로운 버전은 새로운 주소를 제공할 필요가 있습니다. 기존의 주소를 변경해서 2중으로 코드를 관리하는 것이 상당히 번거롭습니다. Gateway 에서 주소 내에 버전(v1,v2)만 추가해 놓으면  되므로 새로운 서버 주소를 만들 필요도 없고 동일한 코드로 – 소스코드에는 v1,v2 없이 /users/{id} 로 정의 되어 있어도 –  별도의 서버에 배포만 하면 됩니다.

$ curl http://your.host.com/v1/users/1
$ curl http://your.host.com/v2/users/1

저는 이런 Gateway 의 필요성이 있다고 생각이 되었고 AWS 에서는 API Gateway 서비스가 제공되고 있어 그대로 활용하는 것도 방법일 수 있지만 모든 서비스가 Cloud 에 다 있는 것이 아니기 때문에 기존의 IDC 에 있는 서비스들도 참여시킬 수 있도록 설치형 서비스를 찾아 보았습니다. 오픈 소스로 제공되는 Kong API Gateway 를 알게 되었고 이를 사용해서 마이크로서비스를 구성했습니다.

Kong 은 Nginx 기반에 lua 언어로 개발된 plugin 형태로 API Gateway 를 제공하고 있습니다. 일단 Nginx 기반이라 – proxy 기능, 캐시 기능, 부하 분산 기능 등을 그대로 사용 – 안정성에서 만족 하였고 실제 로그를 확인하면 Gateway 의 처리 속도나 부하가 극히 적은 것을 알 수 있습니다.  Community 버전을 제공하고 있어서 무료로 사용 가능하다는 것이 또한 강점입니다. 제가 처음 사용할 당시는 무료버전이던 것이 기업용 버전이 추가되어 유료버전으로도 제공되고 있습니다. kong 자체가 plugin 이라 추가적인 plugin 으로 확장이 가능하고 오픈 소스를 가져다가 자신에 맞게 수정해서 사용할 수 있습니다. 기존 plugin 을 수정해서 Kafka 로그 plugin 을 추가한 사례를 나중에 설명하겠습니다.

해당 사이트의 설명을 보면 기본적으로 제공되는 plugin 정보를 얻을 수 있습니다. 저희는 인증에 관련해서 JWT, OAuth2 를 사용해서 복잡한 개발없이 간단히 인증처리를 구현했습니다. 모바일 접근을 허용하기 위해서 CORS 를 사용하였고 아직 사용하지는 않았지만 성능을 높이기 위한 캐시 기능(proxy caching)이라든가 해커의 공격을 대비한 Rate limiting 기능도 사용해보려 합니다. AWS 의 lamda 서비스와 연동도 가능하고 다양한 Log 기능도 제공합니다. 저희가 로그 기능을 확장 하려는 이유는 요청되고 응답되는 데이터가 로그에 포함되지 않아서 그 부분을 추가해서 로그를 설치했습니다.

물론 AWS에는 막강한 서비스가 많습니다. AWS API Gateway 역시 강력합니다. 그러나 aws 외에 서비스를 하거나 내부 인증 시스템과 연동되어야 하거나 자체 서비스를 활용해야 한다면 Kong 과 같은 오픈 서비스가 필요하지 않을까 합니다. 간단히 개발, 확인할 수 있도록 Docker를 이용한 kong 설정을 시작으로 문서들을 작성해보겠습니다.

AWS에 Spinnaker 설치

환경 구성하기

VPC 만들기

기존에 VPC를 가지고 있다면 다음 파트로 넘어가면 된다. 처음 AWS를 사용한다면 VPC 부터 생성해야 한다. VPC 는 virtual private cloud 의 약자이며 아마존 네트워크 내에 자신의 인터넷 데이터 센터(IDC)를 가상으로 만들어 둔다는 것으로 이해하면 된다.

AWS 콘솔로 로그인 한다. 상단 Service 를 클릭하고 VPC 화면으로 이동한다.

aws-console-menu

Start VPC Wizard 를 클릭한다. “VPC with a Single Public Subnet” 탭이 선택되어있나 확인하고 Select 버튼을 클릭한다.

vpc-configuration

IP CIDR block 에는 적당한 네트워크 구간을 정의한다. 4자리 숫자 중 어디까지 고정시키고 나머지 숫자로 서버를 정의하는 행위이므로 적절한 숫자를 고려해야 한다. 10.1.1.0/24 는 10.1.1.1~256 로 시작하는 네트워크 공간을 확보하는 것이고 그 이상의 서버가 생성이 된다면 재구성을 해야 하기 때문이다.

VPC name 을 입력한다. defaultvpc 이런 식으로 입력한다.

VPC 내에 서브 네트워크를 정의한다. 예를 든다면 vpc 는 10.123.0.0/16 으로 subnet 은 10.123.180.0/24 로 정의해서 vpc 내에 256개의 서브넷이 가능하고 서브넷 안에 251개의 서버를 구성할 수 있다. 이 IP 번호는 사설 번호 이므로 임의로 정할 수 있고 내 안에서만 겹치지 않으면 된다. 외부에서 접속하는 IP 주소는 public IP 로 따로 부여 받는 것이고 갯수의 제한이 있다.

subnet name 은 예를 들어 defaultvpc.internal.ap-east-2 이런 식으로 붙여주면 된다. 본인이 관리하는 명명 규칙이 있으면 좋다.

Create VPC 버튼을 클릭하여 생성한다.

AWS 역할 만들기

spinnaker 가 동작할 역할을 생성해야 한다. AWS 콘솔의 상단에 Services를 클릭하고 나타나는 메뉴에서 IAM 을 선택한다. 왼쪽 항목에서 Roles를 클릭하고 오른쪽 상단의 Create New Role 버튼을 클릭한다.

create-new-role

역할명을 입력(spinnakerRole)하고 Next Step 를 누른다. 역할이 담당할 서비스로 목록 중에 Amazon EC2 의 Select 버튼을 클릭한다.

역할에 적용될 정책을 선택한다. 역할 하나에 10개의 정책까지만 적용할 수 있다. PowerUserAccess 의 체크상자를 클릭해서 선택한다. (목록에 내용이 너무 많아 filter 에 power를 입력하면 쉽게 찾을 수 있다.) Next Step을 누르고 이어서 Create Role 을 눌러 역할을 생성한다.

생성이 완료되고 역할 목록 화면이 다시 나타나는데 그 중에 생성한 역할이 추가되어 있다. 생성한 역할을 클릭하면 역할의 상세 내용이 나타난다. 화면 하단에 Inline Policies 제목을 클릭하면 inline 정책을 등록할 수 있는 click here 링크가 나타난다.

create-new-role-2

‘Policy Generator` 를 선택하고 Select 버튼을 클릭한다.

create-new-role-3

AWS Service 항목에 AWS Identity and Access Management 를 선택한다. Actions 항목에는 PassRole 를 선택한다. Amazon Resource Name (ARN) 항목에 * (별표)를 입력한다. Add Statement를 클릭하여 입력 내용을 등록하고 Next Step을 눌러 다음 단계로 넘어간다.

create-new-role-4

아래와 같은 정책 정의를 보여 준다.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1463021281000",
            "Effect": "Allow",
            "Action": [
                "iam:PassRole"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

Apply Policy 를 클릭해서 적용한다. 이후에 만드는 서버의 이 역할을 가지고 인증 정보를 사용할 수 있다.

키 만들기

AWS 콘솔 화면에서 Services > EC2 화면으로 이동한다. 왼쪽 메뉴 그룹 중에 Network &amp; Security 그룹 제목이 있다. 그 안에 Key Pairs 항목을 클릭한다. 오른쪽 상단의 Create Key Pair버튼을 클릭한다. 키 이름으로 aws-spinnaker-keypair 라고 입력한다. (본인이 암기하기 쉬운 것으로 하면된다.)

Create 버튼을 클릭하면 키 파일이 다운로드 된다. 브라우저 따라 다르겠지만 mac book의 사파리로 작업을 한 경우 다운로드 된 파일이 txt 확장자로 저장되었다. 관리를 위해서 txt 는 지우고 pem 으로 끝나는 파일로 변경하자.

생성된 키는 다운로드 뿐 아니라 화면의 키 목록에도 저장되어 있다. 키 파일은 다시 다운로드가 되지 않으며 잃어버린 경우 다른 키 파일로 생성하고 이전 키 파일을 사용한 모든 자원들에 대해 새 파일로 교체해야 한다. 그러니까 잃어버리지 않도록 주의한다.

키 파일을 이용해 ssh 등으로 서버에 접속하려 한다면 키 파일의 권한 속성을 바꾸어야 한다. 키 파일이 안전하다는 전제 하에 프로그램들이 동작하므로 권한 속성을 점검하는 것 같다. 아래와 같이 명령을 실행한다.

$ sudo chmod 400 aws-spinnaker-keypair.pem
Password:

계정 만들기

spinnaker 를 위한 신분증을 만든다. Services > IAM 화면으로 이동한 다음 오른쪽 상단의 Create New Users 버튼을 클릭한다. 여러명의 사용자 이름을 등록할 수 있는데 첫 칸에 spinnakerUser를 입력하고 Create 버튼을 클릭한다. 신분증은 접근키 값과 비밀키 값으로 선언된다. 키값을 메모하거나 Download Credentials 버튼을 클릭하여 자신의 컴퓨터에 저장할 수 있다. 저장된 파일은 credential.cvs 파일로 생성되는데 이후 식별이 쉽도록 계정이름과 동일하게 파일명을 변경하도록 한다. OK를 클릭하면 결과 화면이 닫히고 계정 목록이 나타나는데 신규 계정이 등록된 것을 확인할 수 있다.

aws CLI에 의해 홈 디렉토리에 credentials 파일과 config 파일이 생성이 된다. credentials 에는 인증 정보가 있고 config 에는 기본 접속하는 region 이 정의되어 있다. 이 기본 프로파일에 추가로 방금 생성한 계정을 추가로 만들 수 있다. 자신의 컴퓨터에 2개 파일에 대해서 아래와 같이 작업을 한다.

접속키와 비밀키를 credentials 파일에 아래와 같이 추가한다.

[default]
aws_access_key_id=AKIAIOSFODNN7EXAMPLE
aws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

[spinnakerUser]
aws_access_key_id=AKIAI44QH8DHBEXAMPLE
aws_secret_access_key=je7MtGbClwBF/2Zp9Utk/h3yCo8nvbEXAMPLEKEY

기본 접속하게 될 region 은 config 파일에 아래와 같이 추가할 수 있다.

[default]
region=us-west-2
output=json

[profile spinnakerUser]
region=ap-east-2
output=text

생성된 계정을 클릭하고 상세 정보에서 permission 탭에 managed policies 와 inline policies 에 역할에서 등록한 정책들과 동일한 방식으로 정책을 적용시킨다.

AWS 서버 만들기

AMI 가져오기

spinnaker 서버를 복제한 이미지 파일(AMI)이 제공된다. 아마존 마켓플레이스에서 선택할 수 있지만 한국 region 에서는 존재하지 않는다. 따라서 us-west-2(Oregon) region 에서 가져와야 한다. 바로 가져오는 방법이 없으므로 오레곤에서 서버를 만들고 그 서버를 복제한 이미지를 만들어 ap-east-2(Seoul) region 으로 전송할 수 있다.

보유한 계정으로 AWS 콘솔에 접속한다. Spinnaker AMI 4b35d42b를 클릭해서 제공된 복제 이미지로 서버를 생성하자. 다른 지역의 AMI 파일은 정상 동작하지 않을 수 있다.

선택할 타입으로 m4.xlarge 를 지정한다. 이보다 작은 크기를 선택한 경우 톰캣 등의 필요 서비스가 동작하지 않는다. Next: Configure Instance Details 를 클릭하고 다음으로 진행한다.

choose-ec2-type

상세 정보에서 외부에서 접근할 수 있도록 Auto-assign Public IP 항목을 Enalble 로 설정하고 IAM Role 은 위에서 생성한 역할(spinnakerRole)을 설정한다. Review and Launch 버튼을 클릭하여 설정을 마무리하고 Launch를 클릭한다. Oregon region에서는 생성한 키가 없으므로 “Create a new key pair” 를 선택하고 임의의 키 이름으로 Downlaod Key Pair 버튼을 클릭한다. Launch Instances 버튼이 활성화 되면 클릭해서 서버가 생성되도록 한다.

서버 목록이 나타나고 진행상태가 표기된다. 서버 생성이 완료되면 목록에서 서버를 선택(체크박스를 체크)하고 상단의 Actions 하위 버튼 중에 Image > Create Image 항목을 클릭한다.

create-ami

복제 이미지가 생성 되는데는 시간이 걸린다. AMI 가 완성되면 이제 전에 생성한 서버는 필요없다. instance 메뉴를 클릭하고 서버를 선택한 다음 Actions > Instance State > Terminate 를 클릭하여 서버를 종료시킨다. Stop 과는 달리 서버의 존재 자체가 aws로 반환될 것이고 비용은 발생하지 않게 된다. 아래 그림처럼 IMAGES 안에 AMIs를 선택하면 생성된 이미지를 발견할 수 있다. 이를 선택하고 Actions > Copy AMI 메뉴 항목을 클릭한다.

copy-ami

AMI를 서버를 구성할 region(Seoul)로 복사한다. Destination region 을 Seoul 로 하고 Copy AMI 버튼을 클릭한다.

copy-ami-2

화면 최 상단의 Oregon region 을 Seoul 로 변경한다. 한국으로 복제되기를 기다린다. 복제가 완료되면 AMI 를 가지고 서버를 생성하자.

NB 복제 이미지를 만들기 위해 생성한 서버는 terminate 시켜야 한다. 이를 잊고 있다가 비용 폭탄을 맞을 수 있다.

AMI 에서 서버 생성

전송이 완료된 AMI 를 선택하고 상단의 Launch 버튼을 클릭한다. 위에서 했던 작업과 마찬가지로 m4.xlarge 를 선택하고 Next 를 클릭한다. 상세 정보에서 Network 항목에 기존에 생성한 vpc 를 선택한다. 해당 vpc 내에 생성된 Subnet 을 선택한다. spinnaker 는 외부에서 접속이 가능해야 하므로 Pulic 구간을 목적으로 만든 subnet 을 선택한다. 이전과 마찬가지로 auto-assign public IP 는 enable 시키고 IAM Role 도 똑같이 spinnakeRole 로 설정한다.

Next: Add Storage 를 클릭한다. size 에 20 Gb 를 입력한다.

Next: Tag Instance 를 클릭한다. aws에서 자원을 식별하기 위해 tag 를 다는 것은 중요한 일이다. 설사 자원을 실수로 terminate 시켜도 빨리 작업하면 tag 를 이용하요 검색해서 다시 복원할 수도 있다. tag 에 name 을 적당히 지정한다. (ex. vpcname.public.spinnaker)

Next 를 누르고 보안그룹을 설정한다. 기존에 외부에서 접근할 용도로 만든 public 구간이 있다면 그 구간에 적용된 보안 그룹을 적용한다. 만일 없다면 새로 보안 그룹을 생성하고 외부에서 접근 되도록 inbound, outbound 정책을 정의한다.

최종 Launch 버튼을 클릭하고 이전에 만든 키를 (예. aws-spinnaker-keypair) 지정한다. 이 키가 없이는 생성된 서버는 접근할 수 없다는 충고에 체크하고 Launch Instances 를 클릭해서 생성한다.

View Instances 를 클릭해서 서버 목록이 나타나는 화면으로 이동한다. 목록에 생성된 서버가 추가되어 있고 Instance State 가 먼저 running 으로 바뀌고 서버가 실행되고 최초 설정에 필요한 후속 작업들이 수행되면서 Status Checks 가 Initializing 에서 2/2 checks passed 로 바뀐다.

서버 접속

public IP 를 자동할당 하도록 지정하였기 때문에 생성된 서버의 정보를 보면 public DNS 에 주소가 지정되어 있음을 알 수 있다. 주소와 다운 받은 키파일을 이용해서 SSH 터널을 만들어 접속하도록 하자. 이 작업은 사용자 컴퓨터에서 terminal을 열어서 작업을 수행한다.

홈 디렉토리 안에 .ssh 폴더에 config 파일을 편집기를 통해서 연다. 예제에서 vi를 사용하였는데 사용자에 따라 맞는 편집기를 이용하면 된다. nano를 많이 쓰는 것 같다.

$ vi ~/.ssh/config

아래 내용으로 붙여 넣는다. HostName 은 aws 콘솔에서 ec2 의 instances 목록 중에 생성한 서버를 선택하면 상세 내용중에 찾을 수 있다. IdentityFile 은 이전 작업에서 서버를 생성할 때 지정한 키 파일의 이름을 적어준다. 단, 경로명을 정확히 기재한다.

Host spinnaker-start
   HostName <Public DNS name of instance you just created>
   IdentityFile </path/to/my-aws-account-keypair.pem>
   ControlMaster yes
   ControlPath ~/.ssh/spinnaker-tunnel.ctl
   RequestTTY no
   LocalForward 9000 127.0.0.1:9000
   LocalForward 8084 127.0.0.1:8084
   LocalForward 8087 127.0.0.1:8087
   User ubuntu

 Host spinnaker-stop
   HostName <Public DNS name of instance you just created>
   IdentityFile </path/to/my-aws-account-keypair.pem>
   ControlPath ~/.ssh/spinnaker-tunnel.ctl
   RequestTTY no

이제는 터널을 시작하고 종료하는 실행 파일을 만든다. 편집기로 파일을 연다.

$ vi ~/spinnaker-tunnel.sh

아래 코드를 붙여 넣는다.

 #!/bin/bash

 socket=$HOME/.ssh/spinnaker-tunnel.ctl

 if [ "$1" == "start" ]; then
   if [ ! \( -e ${socket} \) ]; then
     echo "Starting tunnel to Spinnaker..."
     ssh -f -N spinnaker-start && echo "Done."
   else
     echo "Tunnel to Spinnaker running."
   fi
 fi

 if [ "$1" == "stop" ]; then
   if [ \( -e ${socket} \) ]; then
     echo "Stopping tunnel to Spinnaker..."
     ssh -O "exit" spinnaker-stop && echo "Done."
   else
     echo "Tunnel to Spinnaker stopped."
   fi
 fi

파일을 저장하고 실행 권한을 부여한다.

$ sudo chmod u+x ~/spinnaker-tunnel.sh

터널을 시작해보자. 처음 접속하는 것으로 한번 확인 작업이 있는데 yes 를 입력하면 진행된다.

$ ~/spinnaker-tunnel.sh start
Starting tunnel to Spinnaker...
The authenticity of host ' ()' can't be established.
ECDSA key fingerprint is SHA256:Vyt6eTAYvgXhBRuBawDq1iY...........EkZifeWeg.
Are you sure you want to continue connecting (yes/no)? yes
Done.
$

이제 터널이 정상적으로 이루어졌고 브라우저에서 http://127.0.0.1:9000 으로 접속해 보자. 이 IP 번호는 자신의 컴퓨터 번호인데 터널을 통해서 spinnaker 서버로 접속이 이루어진다.

spinnaker-main-page

참고 사이트

mocha – nodejs test framework

모카는 nodejs 와 browser 환경에서 동작하는 자바 스크립트 테스트 프레임워크 이다. 피처 중심의 테스트를 잘 지원하고 비동기 기능에 대해서도 간단하게 구현 가능하다. 유연하고 정확한 보고서 출력을 지원하며 연속해서 테스트를 수행한다.

설치 및 시험

node 프로젝트 폴더로 이동하여 npm 으로 mocha 설치하고 편집기로 간단한 테스트 코드를 작성한다. 테스트 코드에서 사용하는 chai 모듈을 npm 을 통해서 개발 환경(–dev-save 옵션)으로 설치한다.

$ npm install -g mocha
$ npm install chai –dev-save
$ mkdir test
$ vi test/test.js

테스트 코드는 아래와 같이 작성한다.

var assert = require('chai').assert;
describe('Array', function() {
  describe('#indexOf()', function () {
    it('should return -1 when the value is not present', function () {
      assert.equal(-1, [1,2,3].indexOf(5));
      assert.equal(-1, [1,2,3].indexOf(0));
    });
  });
});

mocha 를 실행하여 작성된 테스트 코드를 실행해 보자.

$ mocha

Array
#indexOf()
✓ should return -1 when the value is not present

1 passing (10ms)

결과 보고서

결과 보고서를 파일 형태로 남기고 싶을 때는 xunit 과 같은 패키지를 사용하는 것이 jenkins 와 같은 CI환경에서 보기에 적당하다. xunit-file 을 설치하자.

$ npm install xunit-file –save-dev

아래 명령으로 테스트 결과를 파일로 기록할 수 있다.

$ XUNIT_FILE=test-report.xml mocha -R xunit-file

테스트 커버리지

테스트 결과만 보면 흡족할 수 있으나 전체 코드 중에 얼마나 테스트 된 건지는 알 수 없다. 모카의 테스트 결과를 이스탄불을 통해서 커버리지 보고서로 받아 볼 수 있다. 이스탄불을 설치하자.

$ npm install –save-dev istanbul

상당히 많은 양의 패키지를 다운로드 받는다. 커버리지 보고서는 json 형태로 현재 폴더 안에 coverage 라는 새로운 폴더가 생기고 그 안에 파일이 저장된다. git 를 사용한다면 보고서 내용이 소스와 같이 취급되지 않도록 예외 항목으로 등록할 필요가 있다.

$ echo “/coverage” >> .gitignore

이제 이스탄불을 실행해 보자. 아래와 같은 결과가 나온다. 상당히 낮은 커버리지를 보여 주는데 테스트 코드를 더 열심히 작성해야겠다.

$ ./node_modules/.bin/istanbul cover _mocha

Array
#indexOf()
✓ should return -1 when the value is not present
  
User Service
#getNew(data)
✓ should return an user class when data is not present
  
2 passing (16ms)
=============================================================================
Writing coverage object [/Users/hwangyongho/Documents/dev/cloud/redis/coverage/coverage.json]
Writing coverage reports at [/Users/hwangyongho/Documents/dev/cloud/redis/coverage]
=============================================================================
  
=============================== Coverage summary ===============================
Statements : 32.69% ( 34/104 )
Branches : 24% ( 6/25 )
Functions : 21.43% ( 6/28 )
Lines : 34.69% ( 34/98 )
===============================================================================

매번 복잡하게 명령을 실행할 것이 아니라 package.json 파일에 coverage 라는 이름으로 아래와 같이 스크립트를 등록하고 npm run coverage 를 실행하면 된다.

/* package.json */
{
  ...
  "scripts": {
    "coverage": "istanbul cover _mocha -- -R spec"
  }
}

참고 사이트

CI with jekins and node.js

Jenkins 설치하기

Yum 으로 설치

yum 에서 jenkins 패키지를 인식 못할 수 있다. 이 경우에는 아래와 같이 패키지를 검색할 저장소를 추가해주면 된다.

$ sudo wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat-stable/jenkins.repo
$ sudo rpm –import http://pkg.jenkins-ci.org/redhat-stable/jenkins-ci.org.key

그리고 다시 yum 으로 설치를 시도한다.

$ sudo yum install jenkins.noarch

아래 명령으로 jenkins 의 jetty 웹 서버를 실행할 수 있다. 기본 설정으로 8080 포트로 사이트가 열리게 되므로 http://servername:8080 으로 접속해 볼 수 있다.

$ sudo service jekins start

jenkins_1

보안 설정

가장 적은 노력으로 jenkin 의 보안 설정하는 방법은 자체 사용자 데이터를 사용하는 것이다. 다음과 같은 단계로 설정한다.

  1. 왼쪽 jenkins 관리 메뉴 선택, Configure Global Security 클릭한다.
  2. Enable Security 클릭, Access Control 항목이 펼쳐진다.
  3. Jenkins’ own user database 를 선택한다.
  4. 사용자 가입 허용을 체크한다.

주의사항 페이지 하단에 저장 버튼을 클릭해야 적용이 된다.

기업 환경에서 구축하는 경우에는 상당수 기업이 윈도우 환경이기 때문에 active directory 기반의 사용자 관리를 하고 있을 것이다. 윈도우 서버에 jenkins 라면 Active Directory plugin을 사용하면 되고 리눅스 서버라면 추가로 LDAP 기반 인증 을 사용할 수 있다. 그외의 설정들은 jenkins wiki 를 참고한다.

권한 설정은 서버에 인증된 사용자가 할 수 있는 작업을 설정하는 것이다. 사용자별로 세밀한 작업 권한을 주기 위해서는 Matrix-based 보안을 설정해야 하는데 소수의 인원이 사용하는 경우라면 인증된 사용자에게 모든 권한을 주는 것이 좋겠다. ‘Logged-in users can do anything’ 을 선택하자. 하단의 저장 버튼을 클릭해서 저장한다.

jenkins 자체 사용자 기반이라면 사용자 계정은 본인이 스스로 계정을 생성해야 한다. 첫 화면의 상단에 ‘가입’을 클릭해서 사용자 정보를 등록하고 계정을 만든다. 생성된 계정으로 로그인 하지 않으면 왼쪽 메뉴에서 작업할 항목이 보이지 않는다.

주의사항 필요한 기간 사용자 가입을 받고 그 이후에는 사용자 가입 허용항목을 해제했다. 처음부터 가입 허용을 하지 않기 위해서는 관리자가 jenkins 관리 에서 manage users 항목으로 들어가서 사용자 생성 메뉴를 이용하면 된다.

Node 설치

NVM 을 이용해서 node 을 설치한 경우, 다양한 설치 경로 때문에 문제가 많다. 패키지 관리자를 이용해서 node를 정해진 경로 /usr/local/bin 에 설치하자.

jenkins 는 기본적으로 자바, 메이븐, 앤트와 동작한다. node 프로젝트에 대해서 test 과정을 담는 시도가 많은데 이런 노력이 아깝기도 하고 모카(자바의 다른 커피종류?)를 이용한 XUnit 이 기존 JUnit 과 거의 유사해서 이를 이용하여 jenkins 로 ci (continuous integration build)를 구성 해보자.

아래와 같이 package.json 설정을 해서 npm 으로 하여금 테스트를 수행하게 하고 XUnit이 그 결과를 xml 형태로 출력하게 하면 jenkins 에서 결과를 읽어볼 수 있도록 한다.

'scripts': {
  'test': 'XUNIT_FILE=test-report.xml ./node_modules/.bin/mocha --recursive -R xunit-file test'
}

mocha 가 프로젝트의 배포에 같이 포함되어야 하므로 –save-dev 옵션으로 설치되어야 node_modules 폴더에 들어가게 된다. 위의 설정으로 npm test 를 수행하게 되면 mocha를 이용해서 test 폴더의 모드 하위 폴더에 대해서 테스트 코드를 수행하게 된다. XUnit 보고서를 사용하므로 “test-reports.xml” 이란 이름의 파일이 결과로 생성된다.

Jenkins 에 Node project 추가

모카 설정이 끝나면 jenkins 에 프로젝트를 등록할 수 있다. Node.js를 위한 jenkins plugin 도 있지만 빌드 하는 동안 node 코드를 실행하는 것에 필요한 것이지 테스트를 위해서는 아무 도움이 되지 못한다. 그래서 freestyle 프로젝트로 진행하기로 한다.

git plug-in 설치

서두에 언급한 것처럼 jenkins 에서 Node 는 기본으로 지원하지는 않는다. 따라서, 별도의 plug-in 을 설치해야 한다. jenkins 관리플러그인 관리로 가서 설치가능 탭을 열어보고 검색을 이용해 git plugin 을 찾는다. 선택을 하고 설치 버튼을 클릭하면 필요 plugin 까지 (git client, scm api plugin 등) 같이 설치된다. 사이트를 재 시작해야 다음 작업을 진행할 수 있다.

git 설정

jenkins 에서 git 의 기능을 활용하는 것이므로 미리 git 를 설치해 두어야 한다. install git 참조. jenkins 관리에서 시스템 설정으로 들어가면 Git 항목이 볼 수 있다. 없다면 사이트를 재시작하고 다시 접속해 본다.

git 인증 정보

git 저장소가 원격 서버에 존재하고 접근 과정에 인증이 필요하다면 인증 정보를 등록해야 한다. 메인 화면 왼쪽에 Credentials 항목을 클릭해서 Add 버튼을 누르고 인증 정보를 추가한다. 인증 정보는 ssh 키를 이용하는데 jenkins 서버에 jenkins 가 어느 계정을 사용하는지 파악하여 동일 계정으로 터미널로 들어가야 한다. jenkins 사이트는 보통 jenkins 계정으로 실행되는데 /etc/sysconfig/jenkins파일에서 아래와 같이 설정되어 있다.

JENKINS_USER=”jenkins”

이제 jenkins 계정으로 접속해서 아래와 같이 키 파일을 만든다.

$ ssh-keygen -t rsa

키 파일 생성 후에 .ssh폴더 안에 id_rsa.pub 라는 파일이 생기는데 이 파일을 git 저장소가 있는 서버로 복사한다.

$ scp -i git-user-key.pem ~/.ssh/id_rsa.pub git-user@git-server:~/

아래 명령은 저장소 서버에 ssh 방식으로 jenkins 서버의 계정이 접속할 수 있도록 인증을 위한 키 내용을 등록하는 명령이다.

$ cat id_rsa.pub >> ~/.ssh/authorized_keys

빌드 작업 추가

item 추가

메인 화면의 왼쪽의 새로운 item 메뉴를 클릭한다. item 이름에 프로젝트 이름을 등록하고 프로젝트 종류 중에 Freestyle 을 선택한다. OK 를 클릭하고 다음으로 넘어간다. 소스코드 관리 항목에 Git 를 선택한다. 저장소의 경로를 입력하는데 git clone 에 사용했던 저장소를 기록한다.

저장소 형태 사용 예
로컬 저장소 file:///var/lib/jenkins/git/project-1.git
원격 저장소 ssh://git-user@git-server/git/project-1.git

그러나 불행히도 git 저장소는 이미 다른 계정의 폴더에 생성이 되어있을 수 있다. 만일 /home/git-user/git/project-1.git과 같이 만들어져 있다면 jenkins가 접근할 수 없으므로 아래와 같이 오류가 발생한다. clone a repo from local machine. Error code 128

jenkins-2

이런 경우는 jenkins를 다른 계정(git-user)으로 실행하도록 변경해야 한다. 아래와 같이 따라하자. 원격 저장소를 이용하는 경우에는 아래 내용은 스킵해도 된다.

로컬 저장소 접근 권한 설정

jenkins 서비스를 실행하는 스크립트 /etc/init.d/jenkins 파일을 보면 $JENKINS 로 시작하는 변수가 몇 군데 발견된다. 이 변수 값은 앞 부분에 설명된 것과 같이 /etc/sysconfig/jenkins 설정 파일에 기록되어 있는데 이 값을 바꾸고자 한다.

jenkins를 중지 시키고 설정 파일을 바꾸기 전에 다른 이름으로 백업한다.

$ sudo /sbin/service jenkins stop
$ cp /etc/sysconfig/jenkins /etc/sysconfig/jenkins.bak

그런 다음 편집기를 이용해서 설정 파일에 jenkins 실행 계정을 git 저장소를 생성한 계정으로 바꿔준다.

JENKINS_USER=”git-user”

그리고 jenkins 가 사용하는 폴더의 권한도 변경된 계정이 접근할 수 있도록 권한을 변경해 주어야 한다. 아래와 같이 명령을 실행한다.

$ sudo chown -R git-user:git-user /var/lib/jenkins
$ sudo chown -R git-user:git-user /var/cache/jenkins
$ sudo chown -R git-user:git-user /var/log/jenkins

jenkins를 변경된 설정으로 다시 실행한다.

$ sudo /sbin/service jenkins start

빌드 설정

freestyle 로 생성 되었으므로 shell 을 직접 실행해서 빌드를 수행할 수 있다. Command 부분에 아래와 같이 입력하면 node_modules 에 필요한 패키지를 다운로드 받고 위에서 정의한 테스트 명령을 실행한다.

npm install
npm test

테스트에는 mocha 를 이용해서 수행되고 결과는 xunit 에 의해 xml 형태로 남게된다. 그 바로 아래 섹션에서 테스트 결과를 jenkins 에서 볼 수 있도록 설정할 수 있다.빌드 후 조치섹션에 아래와 같이 테스트 결과 파일명을 기록하면 된다.

Test report XMLs test-report.xml

화면 하단의 저장 버튼을 눌러서 등록한다.

빌드 수행

메인 화면에서 오른쪽에 등록된 프로젝트의 목록이 나타난다. 방금 등록한 프로젝트를 클릭해서 들어가 보면 왼쪽에 메뉴 중에 Build Now 항목을 클릭해서 테스트를 실행해 보자.

jenkins-3

테스트 결과를 그래프 형식으로 볼 수 있고 오류가 난 경우 해당 오류를 클릭하면 상세한 내용을 볼 수 있도록 화면이 이동될 것 이다. 이번 테스트 동안에 발생한 오류는 환경 변수 설정이 누락되어 데이터 접근에서 서버를 찾지 못하는 오류가 발생하였다. jenkins 플러그인 중에 Environment Injector Plugin 를 사용하면 빌드 수행 전에 필요한 변수 값을 설정할 수 있다.

커버리지 보고서

테스트의 결과는 전체 소스 코드 중, 어느만큼 테스트 된 것인지 확인하는 방법이 필요하다. 테스트 결과 100% 만족한다 하더라도 일부 코드에 대한 테스트만 실행되었다면 크게 의미가 없기 때문이다. 테스트를 수행하는 mocha 라는 패키지가 있듯이 테스트를 수행하는 동안 전체 코드 중 테스트에 점검된 범위를 측정하는 istanbul 이라는 패키지가 있다. jenkins 에서 cobertura plugin 을 통해서 istanbul 의 보고서를 볼 수 있도록 jenkins-mocha 라는 패키지를 설치 해보자. istanbul 의 설치 및 자세한 내용도 mocha – nodejs test framework 문서에서 확인할 수 있다.

$ npm install –save-dev jenkins-mocha

package.json 파일의 devDependencies 항목에 패키지와 버전 정보가 기록된다. 이제 npm test 명령으로 사용된 mocha 명령을 아래와 같이 변경하자.

{
  'scripts': {
    'test': 'jenkins-mocha test/*';
  }
}

이제 npm test 명령을 수행하게 되면 jenkins-mocha 패키지가 mocha 를 이용해 test를 진행하고 istanbul 을 이용해 커버리지 보고서를 만들게 된다. 산출물은 프로젝트 폴더 안에 artifacts 폴더가 생성되고 테스트 결과는 test폴더 안에 xunit.xml 파일로 생기고 커버리지 결과는 coverage 폴더에 cobertura-coverage.xml 파일로 생성된다. 다른 위치에 저장하기 원한다면 각각 $TEST_DIR, $COVERAGE_DIR 환경 변수 값을 설정하면 된다.

jenkins 에도 마찬가지로 cobertura 플러그인을 설치해야 한다. jenkins 관리 메뉴에서 플러그인 관리로 가서 이름으로 검색하면 쉽게 플러그인을 추가할 수 있다.

jenkins-4

플러그인이 추가되면 이제 이번에 추가된 프로젝트로 가서 빌드 후 조치 중에 cobertura 를 통해 커버리지 보고서를 보여 주도록 구성을 추가해야 한다. 프로젝트 “구성” 메뉴를 선택하여 화면 맨 하단에 빌드 후 조치 추가 버튼을 누르고 cobertura 섹션에 커버리지 결과 파일 경로명을 적어주고 이전에 입력한 테스트 결함 결과 보고서 위치도 변경된 위치로 수정해주어야 한다.

jenkins-5

설정이 끝났으면 다시 프로젝트를 빌드한다. 경로명의 철자가 틀리면 빌드의 결과가 실패로 보일 수 있으므로 실패됐다면 다시 한번 확인하자. 테스트 커버리지가 각 항목별로 정리되어 표시된다.

jenkins-6

참고 사이트

Node 에서 Redis 사용하기

웹 사이트를 구성할 때 운영 안정성을 고민할 때가 많다. 서버가 고장이 나서 고객들이 접속하면 어떡하지 걱정도 되고 사업이 잘되서 생각보다 많은 사람이 접속을 해서 한 대의 서버로는 감당이 안될 경우에도 문제가 될 수 있다. 이런 경우에는 여러 대의 서버로 사이트를 구성하는 것이 보편적이다. 이렇게 여러 대를 구성하게 되면 자주 발생하는 문제가 고객이 접속하는 서버가 한 대에 고정되는 것이 아니라 여러 대의 서버로 옮겨 다닌다는 것이다. 이로 인해 사용자가 어느 서버에서 작업한 내용이 다른 서버로 옮겨가면서 다 사라져 버리는 불평을 호소하게 된다. 이를 방지하기 위해서 각 서버는 사용자의 작업 내용을 공유하게 되는데 쉽게 공유할 수 있는 방법이 캐시 서버를 두는 방식과 DB 서버를 두는 방식이 있다.
공유가 필요한 사용자의 작업 내용은 데이터로 저장되기 이전의 내용이고 저장된 이후에는 DB에 보관되어 있으므로 굳이 관리가 필요하지 않다. 따라서 여기서는 캐시 서버를 두는 예제를 다루기로 하고 redis 라는 솔루션을 이용하기로 한다.

Redis Install

yum 으로 설치하기

redis 최신 버전을 위해서 remi 저장소를 필요로 한다. 그리고 redis는 jemalloc 패키지를 사용하는데 이 패키지는 epel 저장소에 있다. 필요 저장소를 설치하기 위해서 아래와 같이 rpm 파일을 설치한다. EPEL,Remi 설치하기 를 참고 하시오.

$ wget http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm
HTTP request sent, awaiting response… 200 OK
Length: 14540 (14K) [application/x-rpm]
Saving to: ‘epel-release-6-8.noarch.rpm’

$ wget http://rpms.famillecollet.com/enterprise/remi-release-6.rpm
HTTP request sent, awaiting response… 200 OK
Length: 7615 (7.4K) [application/x-rpm]
Saving to: ‘remi-release-6.rpm’

$ sudo rpm -Uvh remi-release-6.rpm epel-release-6-8.noarch.rpm
준비 중… ################################# [100%]

yum을 이용하여 redis를 설치하려면 –enablerepo 옵션을 사용하여 epel과 remi 저장소에 접근하도록 지정하여야 한다. 아래와 같이 설치가 완료되면 시스템 서비스로 redis 가 실행되도록 등록하고 서비스를 실행한다.

$ sudo yum –enablerepo=epel,remi install redis -y
Loaded plugins: priorities, update-motd, upgrade-helper
epel/x86_64/metalink | 4.6 kB 00:00
epel/x86_64 | 4.3 kB 00:00
epel/x86_64/group_gz | 149 kB 00:00
epel/x86_64/updateinfo | 739 kB 00:01
epel/x86_64/primary_db | 5.8 MB 00:09

$ sudo chkconfig redis on
$ sudo service redis start

소스로 설치하기

yum 을 이용한 redis 의 설치 버전은 2.4 버전으로 낮은 버전이다. 높은 버전 (예 3.0) 을 설치하려면 소스를 받아서 빌드하고 설치하는 과정이 필요하다. 빌드가 가능하도록 아래와 같이 gcc 컴파일러와 테스트 도구를 설치하자.

$ sudo yum install -y gcc*
$ sudo yum install -y tcl

이제 redis 최신 버전을 받아서 아래와 같이 설치를 진행한다.

$ wget http://download.redis.io/releases/redis-3.0.1.tar.gz
$ tar xzf redis-3.0.1.tar.gz
$ cd redis-3.0.1
$ make
$ make test
$ make install
$ cd utils
$ chmod +x install_server.sh
$ ./install_server.sh

기본 설정을 묻는 질문들이 나오는데 엔터를 쳐서 기본 설정을 그대로 따라가면 된다. 중간에 실행 파일의 위치를 묻는데 which redis-server를 쳐보면 설치된 경로명을 알 수 있다. 여기서는 /usr/local/bin/redis-server 로 되어 있다.

설정 및 실행

설치하면서 실행할 서비스 이름이 달라질 수 있는데 설치 결과 메시지를 주목해서 봐야한다. 여기서는 아래와 같이 실행해야 한다. (포트 번호가 서비스 스크립트에 따라 붙어있다.)

$ sudo service redis_6379 start

redis 가 기본 localhost 에 대해서 동작하도록 되어 있으므로 외부 서버가 이용할 수 있도록 외부에 노출된 서버 ip 를 설정해준다. 아래와 같이 편집기를 이용해서 설정 파일 내용을 수정하자.

$ sudo vi /etc/redis/6379.conf

bind 에 정의된 ip 주소에 서버의 ip를 설정한다. 서버의 ip 주소는 ifconfig 명령으로 알 수 있다. 그리고 외부의 침입을 막기 위해 비밀번호로 보호를 하는 것이 안전하다. requirepass 항목에 적당한 비밀번호를 설정하자. 설정값은 복사해서 바로 사용할 수 있도록 대부분 설정 내용이 주석으로 기술되어 있다.

# Accept connections on the specified port, default is 6379.
# If port 0 is specified Redis will not listen on a TCP socket.
port 6379
# If you want you can bind a single interface, if the bind option is not
# specified all the interfaces will listen for incoming connections.
# bind 127.0.0.1
bind 172.31.15.27
# requirepass foobared
requirepass your_password

redis 서비스를 재 시작한다.

$ sudo service redis restart

redis desktop manager 로 서비스를 접속해본다. 이 RDM 다운로드 링크를 이용해서 다운 받아 설치할 수 있다.

Sample Test Coding

이제 redis 서버에 데이터를 저장하고 읽어가는 기능을 테스트 할 수 있도록 웹 사이트를 구성해 보자. node 에서는 웹 사이트 개발을 편리하게 하기 위해 express 라는 모듈을 이용한다. 사이트의 기본 골격을 만들어주는 명령어도 제공되는데 아래와 같이 설치하고 기본 웹 사이트를 만들어 보자.

install express & redis module

어느 폴더 위치에서도 express를 사용할 수 있도록 -g 옵션을 이용해서 설치하자.

$ sudo npm install -g express
$ npm install -g express-generator

express 명령을 이용해서 기본 웹 사이트 구조를 만들고 그 사이트에서 사용할 redis 모듈을 설치하자. –save 옵션을 이용하면 사이트의 package.json 파일에 어떤 모듈이 설치되었는지 기록이 되고 node_modules 폴더에 다운로드 된다.

$ express redisweb
$ cd redisweb

$ npm install –save connect-redis ioredis
$ npm install –save JSON

setup web application

폴더 바로 안쪽에 app.js 파일에 redis, JSON 모듈을 선언하고 접속 정보를 설정한다. (설정 정보는 별도 설정 파일로 존재하는 것이 좋은데 여기서는 생략한다.)

var Redis = require('ioredis');
var JSON = require('JSON');
client = new Redis({port: 6379, host: '52.79.54.249',
  password: 'your_password', db: 0});

express로 app 을 생성한 코드 다음에 redis 사용 가능하도록 아래 코드와 같이 middleware 에 등록하자. 각 함수에서 redis 를 이용할 수 있도록 request 객체에 cache 이름으로 미리 만들어 둔다.

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

routes 폴더 안에 profile.js 파일을 생성하고 아래와 같이 코드를 입력한다.

var express = require('express');
var JSON = require('JSON&amp;amp;amp;');
var router = express.Router();

router.get('/:name', function(req, res, next) {
  var key = req.params.name;
  req.cache.get(key, function(err,data){
    if (err) {
      console.log(err);
      res.send(err);
      return;
    }
    var value = JSON.parse(data);
    res.json(value);
  });
});

router.post('/', function(req, res, next) {
  var key = req.body.name;
  var value = JSON.stringify(req.body);

  req.cache.set(key, value, function(err, data) {
    if (err) {
      console.log(err);
      res.send(err);
      return;
    }

    req.cache.expire(key, 60);
    req.json(value);
  });
});

module.exports = router;

사용자가 입력한 데이터를 json 형태로 수신 받으면 post 함수에서 req.cache 으로 제공된 redis 로 수신 받은 데이터의 name 항목을 키와 문자열로 변환된 데이터를 저장한다. 저장에 성공하면 키 값의 유효시간을 60초로 하여 시간이 지나면 자동으로 소멸되게 한다.

get 함수에서는 name을 파라미터 값으로 받아서 req.cache 의 redis 를 통해서 데이터를 가져올 수 있다. 가져온 데이터를 json 형태로 사용자 화면에 표기한다.

이제 생성한 profile.js 파일을 app.js 안에 아래와 같이 모듈을 추가한다.

var routes = require('./routes/index');
var users = require('./routes/users');
var profile = require('./routes/profile');

사용자가 접속할 수 있는 주소 경로 /profile를 등록한다. 경로를 등록한 다른 함수 아래에 추가하면 된다.

app.use('/', routes);
app.use('/users', users);
app.use('/profile', profile);

Test

완성된 웹 사이트를 아래 명령으로 실행한다.

$ npm start

http://localhost:3000/ 주소로 브라우저로 접속을 해 보자. 전송할 데이터를 입력한 화면이 완성되지 않은 상태이기 때문에 크롬 브라우저의 POSTMAN 앱을 설치하고 postman 을 통해서 데이터를 작성해서 전송하자. 설치된 postman 을 실행하고 주소를 http://localhost:3000/profile 로 입력하고 그 왼쪽에 GET 을 POST로 변경한다. 그 아래 Headers 탭을 선택하고 Content-Type 항목에 application/json 을 입력한다. Headers 옆에 Body 탭을 선택하고 그 아래 raw 를 클릭하고 그 오른쪽에 입력 형태를 JSON(application/json) 을 선택한다. 그 아래 데이터 입력란에 아래의 json 코드를 입력한다.

{
  'name': 'tom';,
  'address': 'Seoul, Korea',
  'phone': '001-001-0001'
}

이제 send 버튼을 클릭하면 웹 사이트로 입력한 데이터를 전송하고 서버에 tom 을 키 값으로 데이터가 저장된다. 입력한 데이터 아래에 서버에서 응답한 결과가 나타나는데 Body 에 동일한 json 형태의 데이터가 나타나고 Status 에는 200 OK 라고 표시된다.

redis-json-result

 

다시 POST 를 GET 으로 바꾸고 주소에 http://localhost:3000/profile/:name 를 입력하고 send 하면 동일한 json 데이터가 화면 하단에 나타난다.

참고 사이트

NPM 사설 저장소 만들기

node 는 다양한 기능을 패키지 형태로 제공하는데 이를 통해 필요한 기능을 쉽게 추가하여 프로그램을 개발할 수 있다. 뿐만 아니라 npm 도구를 이용해서 최신 업데이트 된 기능을 쉽게 받아올 수 있다. 시스템 환경이 빠르게 변화하고 있고 프로그램이 정상적으로 동작하게 하기 위해서 기능을 최신으로 유지할 필요가 있다. 따라서 프로그램을 제공할 때 최신으로 업데이트 되도록 구성하는 것이 중요하다. 이런 상황은 운영 때만이 아니라 단기간의 프로젝트 중에도 발생할 수 있다. 어느 개발자가 작성한 코드는 여러 개발자들에 의해 사용되어질 수 있는데 이는 중복 개발도 막을 수 있고 따라서 생산성을 높여 개발 기간을 단축할 수 있기 때문이다. 이런 상황에서 공용되는 코드의 변경은 이를 사용하는 많은 개발자들에게 의사소통과 재작업을 일으켜서 오히려 손해가 될 수 있다. 이를 방지하기 위해 내부 공용 패키지에 대해서도 npm 도구를 적용해서 개발할 수 있어야 하고 패키지 변경에 대한 영향도 검토를 위해 테스트 코드가 필요하다.

이 내용은 마크다운 형식으로 작성되었고 마크다운 문법에서 배울 수 있다.

사설 저장소

github에 패키지를 올려서 받을 수 있는 방법도 있지만 여러가지 이유(npmjs.org 서버 다운 등)로 npm의 공공 저장소를 이용하지 못하는 경우 내부 사설 저장소가 대안이 될 수 있으므로 여기서는 다루지 않을 것이다. 구성하기 위해 도입할 수 있는 솔루션은 여럿1 있지만 유료이기도 하여 초기 셋업된 조직의 경우 선택하기 부담스럽다. 무료 중에 많이 사용되는 것으로 찾아본 결과 세가지 방법이 있다.

  1. couchDB를 npm과 연동 시키는 방법
    이 방법은 데이터베이스 자체를 설치해야 하는 부담이 있고 단순히 공공 저장소의 복사본으로 존재하므로 언급하지 않겠다. 자세한 내용은 링크를 따라가서 참조하면 좋겠다.
  2. sinopia 를 npm을 통해서 설치하는 방법
    npm을 사용하므로 쉽게 설치할 수 있다. 별다른 설정도 필요없다. 그러나 불행히도 최신 node 버전으로 제공되지 않아 설치에 어려움이 있어 nvm을 통해 node의 예전 버전을 설치하고 sinopia를 설치 구동시켜보자.
  3. Nexus 서버를 이용하는 방법
    넥서스 서버는 자바, 닷넷, node 등을 다방면에 제공하므로 요즘 같이 다양한 플랫폼을 사용하는 팀에서는 운영하기 유용하다 할 수 있다. 기존의 npmjs.org 의 저장소를 대신(proxy)해서 사용할 수 있고 사설 저장소도 구비할 수 있다. 저장소에 사용자를 등록하고 작업하는 부분이 버전 3부터 제공이 되고 있어 완성본이 나오면 정리하도록 하자. 자세한 방법은 링크를 참조하면 된다.

저장소 설치

여러 개발자 컴퓨터에서 접속할 수 있도록 한대의 서버를 마련하고 여기에 저장소를 설치하려고 한다. 저장소 서버에서 작업할 내용을 보도록 하자.

nvm 설치

일반적으로 nodejs 를 설치하려면 공식 사이트 에서 다운로드 받을 수 있지만 여기서는 sinopia 가 동작할 수 있는 과거 버전의 설치가 필요하므로 node 버전 관리 프로그램 nvm 을 이용하고자 한다. 아래 명령으로 nvm을 설치하는 스크립트를 받을 수 있다. 이 스크립트는 사용자 로그인 시 자동으로 수행되는 스크립트 파일(.bashrc)에 포함되므로 다시 로그인을 해야 사용할 수 있다.

$ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.0/install.sh | bash

다운로드가 완료되면 터미널을 재 접속하라는 메시지가 나온다. 터미널 창을 닫고 다시 접속한다.

$ nvm install 0.12.12
$ nvm use v0.12.12

sinopia 가 동작할 수 있는 버전의 node 를 설치한다. 그리고 해당 버전으로 node 가 동작하도록 지정한다.

$ node -v
v0.12.12

실행되는 node 버전이 같음을 알 수 있다. 이제 node의 npm 을 이용해서 sinopia 를 설치해야 하는데 설치 과정 중, 소스코드를 실행 파일로 만드는 과정이 필요할 수 있다. 이에 필요한 도구를 먼저 설치한다.

$ sudo yum install -y gcc-c++ make

소스코드는 보통 c, c++ 언어로 작성되며 이를 실행 파일로 만드는 도구가 리눅스에서 gcc 라는 것이 있다. 이를 다운받아 실행할 수 있도록 한다.

sinopia 설치

sinopia가 동작 가능한 node 최종 버전이 0.12 이기 때문에 일반적인 node 설치 방법과 다르게 nvm 을 이용해서 해당 버전을 설치하였다. 이제 node 에서 제공하는 npm 이라는 도구를 이용해 sinopia 를 설치할 수 있다.

$ npm install -g sinopia

-g 선택으로 sinopia를 어디에서도 실행 가능하도록 설치한다.

$ sinopia
 warn  --- config file  - /home/admin/sinopia/config.yaml
 warn  --- http address - http://localhost:4873/
 ^C

sinopia를 실행한다. 접속할 수 있는 http 주소가 나타난다. 실행됨을 확인하고 control + c 를 눌러 종료한다. 출력에서 볼 수 있듯이 config.yaml 이 sinopia 의 설정 파일이다. 이 안의 설정을 바꾸어 자신의 환경에 적절하게 조정하자.

맨 처음으로 관리자 정보를 등록하도록 하자. 아래와 같이 node 함수를 사용하여 ‘password’ 라는 관리자 비밀번호를 암호화 한다.

$ cd sinopia
$ node
> crypto.createHash('sha1').update('password').digest('hex')
> 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8'
> .exit
$ cd sinopia
$ vi config.yaml

비밀번호의 암호화를 마치고 .exit 명령으로 node 의 실행을 중단한다. 출력된 16진수 문자열을 복사한다. 사용이 익숙한 (vi 같은)편집기를 사용하여 설정 파일 config.yaml 을 열어서 아래와 같이 고친다. 아래 항목이 없다면 파일 맨 끝에 추가한다.

# a list of users
users:
  admin:
    # crypto.createHash('sha1').update(pass).digest('hex')
    password: 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8

만일 node 로 코딩 지식이 있다면 아래 코드를 이용해서 여러명의 사용자를 한꺼번에 등록할 수 있다. 물론 userlist 에 정의된 사용자는 본인의 사용자들로 변경해서 붙여넣기를 해야 한다.

var fs = require('fs');
var crypto = require('crypto');
var sha512crypt = require('sha512crypt-node');
var salt = crypto.randomBytes(10).toString('base64');

var userlist = [ &amp;amp;quot;user1:password1&amp;amp;quot;, &amp;amp;quot;user2:password2&amp;amp;quot; ];
var ht = fs.createWriteStream('./htpasswd', {flag:'a'});
userlist.forEach(function (val, index) {
  var idpwd = val.split(':');
  if (!idpwd || idpwd.length != 2) return;
  ht.write(idpwd[0]);
  var pwd = sha512crypt.sha512crypt(idpwd[1], salt);
  ht.write(':');
  ht.write(pwd);
  ht.write(':');
  ht.write('\n');
});
ht.end();

이 저장소에 개발자들이 접근할 수 있도록 주소가 필요하다. 기본 설정된 localhost 이름은 서버 안에서만 의미있는 이름이다. 서버 관리자에게 저장소 서버의 ip 주소에 도메인 이름을 지정하도록 요청하고 지정된 이름을 설정 파일 안에 기록해주어야 한다. config.yaml 파일 안에 아래 내용으로 수정 (없다면 추가) 한다.

# listen address and port
listen: npm-registry.company.com:4873

저장소 서버에서의 설정이 마무리 되었다. 이제 sinopia를 실행해 보자.

$ sinopia

저장소 사용하기

이제부터는 서버 작업을 마쳤으므로 자신의 컴퓨터에서 작업을 할 순서이다. 서버 설정 파일에 작성한 저장소 주소를 기억하는가? 자신의 컴퓨터에서 웹 브라우저를 실행하여 해당 주소를 입력해보자. 접속된 화면에서 우측 상단의 로그인 버튼을 클릭해서 서버에서 등록한 admin 계정으로 로그인 한다.

sinopia

저장소 주소 설정

sinopia 는 nmpjs.org 의 저장소와 연결하여 필요한 패키지를 전달해 준다. 만일 nmpjs.org 사이트가 닫혀있어도 이미 전에 다운 받은 적이 있던 패키지들은 sinopia에 담겨 있으므로 여전히 개발자들은 대신해서 패키지를 다운 받을 수 있다. 그렇다면 이제는 내 컴퓨터의 npm 은 지금 만들어논 사설 저장소를 사용하도록 설정해보자.

$ npm set registry "http://npm-registry.company.com:4873"
$ npm config get registry
http://npm-registry.company.com:4873

설정 전에는 .npmrc 파일은 없고 기본적으로 http://registry.npmjs.org 주소를 저장소로 사용하게 된다. npm set 명령으로 저장소 주소를 변경할 수 있고 npm config get 으로 저장소 주소를 확인할 수 있다. 이렇게 설정된 값은 홈 디렉토리 안에 .npmrc 파일 안에 아래와 같이 기록된다.

registry=http://npm-registry.company.com:4873

이제 서버에서 등록한 admin 계정으로 저장소에 로그인 해 보자.

$ npm login
Username: admin
Password:
Email: (this IS public) myid@company.com

비밀번호는 서버에서 암호화 이전에 지정한 단어를 기억해서 입력해야 한다. 메일 주소는 서버에서 체크하지 않으므로 그냥 자신의 주소를 입력한다. 아무 메시지가 나오지 않으면 정상적으로 로그인이 된 것이다. 기본 권한 설정에 따라서 패키지를 다운 받는 것은 로그인이 필요 없지만 (누구나 가능) 패키지를 배포하는 것은 인증된 사용자만이 가능하다.

sample 프로그램

간단한 node 프로그램을 개발하면서 사설 npm 저장소를 이용해보자. 먼저 프로그램 코드를 위치시킬 폴더 tester 폴더를 만들고 npm 을 이용해 폴더 구조를 만들자.

$ mkdir tester
$ cd tester
$ npm init
This utility will walk you through creating a package.json file.
...
save it as a dependency in the package.json file.

Press ^C at any time to quit.
name: (tester)
version: (1.0.0)
description: tester code for test of testee
entry point: (index.js)
test command:
git repository:
keywords: test
license: (ISC)
About to write to /Users/userid/tester/package.json:
{
  'name': 'tester',
  'version': '1.0.0',
  'description': 'tester code for test of testee',
  'main': 'index.js',
  'scripts': {
    'test': '...';
  },
  'keywords': [
    'test'
  ],
  'author': 'My Name <myid@company.com> (http://company.com)';,
  'license': 'ISC'
}

Is this ok? (yes)

초기화 과정 중에 질문이 많지만 그냥 enter 키만 치고 넘어가도 되는 내용들이다. 이제 폴더 내에 필요한 파일들이 생겨져 있다. 초기화 과정의 질문에도 나오지만 실행할 프로그램을 만들어 보자. vi index.js 명령으로 실행 파일을 편집기로 연다. 그리고 아래 코드를 붙여 넣어보자.

var chalk = require('chalk');
console.log(chalk.red('Hello,'), chalk.blue('world.'));
console.log(chalk.yellow('Hello, world.'));

이 내용을 저장하고 편집기를 종료하면 간단하게 Hello, world. 를 출력하는 프로그램이(index.js) 생긴다. 이를 실행하기 위해 코드에 나온 것 같이 chalk 라는 패키지가 필요하다. 아래 명령으로 이전에 설정한 sinopia 저장소를 통해서 패키지를 현재 폴더에 설치하게 된다.

$ npm install --save chalk
tester@1.0.0 /Users/userid/tester
└─┬ chalk@1.1.3
  ├── ansi-styles@2.2.1
  ├── escape-string-regexp@1.0.5
  ├─┬ has-ansi@2.0.0
  │ └── ansi-regex@2.0.0
  ├── strip-ansi@3.0.1
  └── supports-color@2.0.0

npm WARN EPACKAGEJSON tester@1.0.0 No repository field.

실행을 하면 패키지가 다운 받는 과정이 나타나는데 이것은 sinopia 가 npmjs.org를 대신해서 정상적으로 동작하고 있음을 보여준다. 설치할 때, –save 옵션을 사용하므로 package.json 파일에 어떤 패키지들을 설치했는지 기록되게 되는데 나중에 한꺼번에 필요한 패키지를 npm install 명령 하나로 업데이트 할 수 있다. 이제 필요한 패키지도 설치가 됐으니 실행을 해보도록 하자.

$ node index.js
Hello, world.
Hello, world.

출력 결과는 앞 단어는 적색, 뒤는 청색 그리고 다음 줄은 노란색으로 나타난다.

패키지 등록하기

사설 저장소를 만든 근본 목적이 내가 만든 패키지를 공개된 곳이 아닌 우리 안에 두고 우리끼리 공유해서 사용하고 싶어서이다. 이제 간단한 패키지를 만들어 보고 사설 저장소에 저장해 보겠다. 우선 현재 tester 폴더에서 나와서 새로 testee 폴더를 만들자.

$ cd ..
$ mkdir testee
$ cd testee
$ npm init

작업의 순서는 tester 프로그램을 만들 때와 동일하다. 똑같이 편집기를 통해서 아래와 같은 내용으로 index.js 파일을 만들자.

var exports = module.exports = {
  hello: function () {
    return 'hello, testee.';
  }
}

hello 함수를 제공하는 객체를 반환하는 패키지이다. 우선 인증된 사용자 만이 패키지를 배포할 수 있으므로 npm login 을 하고 publish 명령으로 배포를 한다.

$ npm login
Username: admin
Password:
$ npm publish
+ testee@1.0.0

testee 라는 패키지가 정상적으로 사설 저장소에 배포가 되었다. 사설 저장소의 웹 페이지로 들어가 보면

\> testee v1.0.0

By:

test for publishing

라는 메시지가 나타난다. readme 파일등 세밀하게 관리할 필요가 있는데 여기서는 생략하겠다.

패키지 사용하기

이미 tester 예제 프로그램을 통해서 npmjs.org 로부터 정상적으로 패키지를 가져오는 것을 확인하였다. 이제 사설 저장소에서도 방금 배포한 testee 패키지도 정상적으로 받아 올 수 있는지 확인하도록 하자. 이전 tester 프로그램을 작성하던 tester 폴더로 다시 이동해서 아래 명령으로 testee 패키지를 받아보자.

$ npm install --save testee
tester@1.0.0 /Users/userid/tester
└── testee@1.0.0

npm WARN EPACKAGEJSON tester@1.0.0 No repository field.

tester 폴더에서 testee 패키지를 받아 설치된 것을 확인할 수 있다. 마찬가지로 package.json 파일에도 저장되어 있다. 이제 설치된 패키지 함수가 정상적으로 동작하는 지 확인하자. tester 폴더의 index.js 파일을 아래와 같이 수정하자.

var chalk = require('chalk');
var testee = require('testee'); // 추가
console.log(chalk.red('Hello,'), chalk.blue('world.'));
console.log(chalk.yellow(testee.hello())); // 변경

파일을 저장하고 node index.js 명령으로 실행해 보자.

$ node index.js
Hello, world.
hello, testee.

패키지의 함수에서 제공된 문자열이 정상적으로 출력되었다.

맺음말

npm 을 통해서 전 세계 개발자들의 기술을 공유할 수 있어 시스템 구축의 생산성이 비약적으로 높아졌다. 검색만 잠깐 하면 필요한 기능을 제공하는 패키지를 쉽게 찾을 수 있고 소스를 받아서 일일이 환경에 맞춰 빌드 작업하는 것도 사라졌다. 실제 기업에서 작업을 하게되면 공개 패키지로는 해결이 안되는 세밀한 작업들이 많은데 이 또한 기업 내에서 반복적으로 일어나는 것이 많아서 기능을 제대로 구분한다면 패키지로 만들어 재사용할 수 있는 것들이 많다. 추가적으로 생산성이 더욱 향상 될 수 있다는 것이다. 일관성이 적고 늘 변화무쌍한 조직도 있지만 노력에 따라 가능한 패키지들을 뽑아낼 수 있다고 생각된다. 물론 이런 도구를 사용하기 위한 프로세스도 잘 정리되어야 서로간에 불편함을 덜 수 있으니 일하는 방식에 대해서도 더 많이 고려해서 적합한 도구가 제공되어야겠다.

참고 사이트


  1. npm Private Packages 는 월 7달러,