부산에서 매주 진행되는 스터디입니다.

부산에서 다른 스터디 내용을 보실려면 카페 에서 보실수 있습니다.

https://www.udemy.com/docker-and-kubernetes-the-complete-guide 을 공부하고 있습니다

도커 & 쿠버네티스 9주차 스터디 

pod 자세한 설명 스크립트

# kubectl describe <object type> <object name>

> kubectl describe pod client-pod

...........

Name:         client-pod
Namespace:    default
Node:         minikube/10.0.2.15
Start Time:   Sat, 02 Feb 2019 12:05:16 +0900
Labels:       component=web
Annotations:  kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"labels":{"component":"web"},"name":"client-pod","namespace":"default"},"spec":{"container...
Status:       Running
IP:           172.17.0.16
Containers:
  client:
    Container ID:   docker://465ecbe522f537a36c26c021d88c1efb21782daf1e6fffd1e93be3469701a4d5
    Image:          bear2u/multi-worker
    Image ID:       docker-pullable://bear2u/multi-worker@sha256:6559ad68144e14b8f6f3054ab0f19056853ea07a7c4ead068d9140bd0a33b926
    Port:           3000/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Sat, 09 Feb 2019 10:24:04 +0900
    Last State:     Terminated
      Reason:       Completed
      Exit Code:    0
      Started:      Sat, 09 Feb 2019 10:06:12 +0900
      Finished:     Sat, 09 Feb 2019 10:24:01 +0900
    Ready:          True
    Restart Count:  3
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-28mbg (ro)
Conditions:
  Type           Status
  Initialized    True
  Ready          True
  PodScheduled   True
Volumes:
  default-token-28mbg:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-28mbg
    Optional:    false
QoS Class:       BestEffort
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type    Reason                 Age               From               Message
  ----    ------                 ----              ----               -------
  Normal  Scheduled              6d                default-scheduler  Successfully assigned client-pod to minikube
  Normal  SuccessfulMountVolume  6d                kubelet, minikube  MountVolume.SetUp succeeded for volume "default-token-28mbg"
  Normal  Pulling                6d                kubelet, minikube  pulling image "bear2u/multi-client"
  Normal  Pulled                 6d                kubelet, minikube  Successfully pulled image "bear2u/multi-client"
  Normal  Created                6d                kubelet, minikube  Created container
  Normal  Started                6d                kubelet, minikube  Started container
  Normal  SuccessfulMountVolume  12h               kubelet, minikube  MountVolume.SetUp succeeded for volume "default-token-28mbg"
  Normal  SandboxChanged         12h               kubelet, minikube  Pod sandbox changed, it will be killed andre-created.
  Normal  Pulling                12h               kubelet, minikube  pulling image "bear2u/multi-client"
  Normal  Pulled                 12h               kubelet, minikube  Successfully pulled image "bear2u/multi-client"
  Normal  Created                12h               kubelet, minikube  Created container
  Normal  Started                12h               kubelet, minikube  Started container
  Normal  SuccessfulMountVolume  25m               kubelet, minikube  MountVolume.SetUp succeeded for volume "default-token-28mbg"
  Normal  SandboxChanged         25m               kubelet, minikube  Pod sandbox changed, it will be killed andre-created.
  Normal  Pulling                25m               kubelet, minikube  pulling image "bear2u/multi-client"
  Normal  Pulled                 25m               kubelet, minikube  Successfully pulled image "bear2u/multi-client"
  Normal  Killing                7m                kubelet, minikube  Killing container with id docker://client:Container spec hash changed (3635549375 vs 3145631940).. Container will be killed and recreated.
  Normal  Pulling                7m                kubelet, minikube  pulling image "bear2u/multi-worker"
  Normal  Created                7m (x2 over 25m)  kubelet, minikube  Created container
  Normal  Pulled                 7m                kubelet, minikube  Successfully pulled image "bear2u/multi-worker"
  Normal  Started                7m (x2 over 25m)  kubelet, minikube  Started container

업데이트 오류

만약 pod 설정파일에서 containerPort를 변경시 어떻게 되는지 보자

> kubectl apply -f client-pod.yaml

.......

the Pod "client-pod" is invalid: spec: Forbidden: pod updates may not change fields other than `spec.containers[*].image`, `spec.initContainers[*].image`, `spec.activeDeadlineSeconds` or `spec.tolerations` (only additions to existing tolerations)
{"Volumes":[{"Name":"default-token-28mbg","HostPath":null,"EmptyDir":null,"GCEPersistentDisk":null,"AWSElasticBlockStore":null,"GitRepo":null,"Secret":{"SecretName":"default-token-28mbg","Items":null,"DefaultMode":420,"Optional":null},"NFS":null,"ISCSI":null,"Glusterfs":null,"PersistentVolumeClaim":null,"RBD":null,"Quobyte":null,"FlexVolume":null,"Cinder":null,"CephFS":null,"Flocker":null,"DownwardAPI":null,"FC":null,"AzureFile":null,"ConfigMap":null,"VsphereVolume":null,"AzureDisk":null,"PhotonPersistentDisk":null,"Projected":null,"PortworxVolume":null,"ScaleIO":null,"StorageOS":null}],"InitContainers":null,"Containers":[{"Name":"client","Image":"bear2u/multi-worker","Command":null,"Args":null,"WorkingDir":"","Ports":[{"Name":"","HostPort":0,"ContainerPort":

A: 9999,"Protocol":"TCP","HostIP":""}],"EnvFrom":null,"Env":null,"Resources":{"Limits":null,"Requests":null},"VolumeMounts":[{"Name":"default-token-28mbg","ReadOnly":true,"MountPath":"/var/run/secrets/kubernetes.io/serviceaccount","SubPath":"","MountPropagation":null}],"VolumeDevices":null,"LivenessProbe":null,"ReadinessProbe":null,"Lifecycle":null,"TerminationMessagePath":"/dev/termination-log","TerminationMessagePolicy":"File","ImagePullPolicy":"Always","SecurityContext":null,"Stdin":false,"StdinOnce":false,"TTY":false}],"RestartPolicy":"Always","TerminationGracePeriodSeconds":30,"ActiveDeadlineSeconds":null,"DNSPolicy":"ClusterFirst","NodeSelector":null,"ServiceAccountName":"default","AutomountServiceAccountToken":null,"NodeName":"minikube","SecurityContext":{"HostNetwork":false,"HostPID":false,"HostIPC":false,"ShareProcessNamespace":null,"SELinuxOptions":null,"RunAsUser":null,"RunAsGroup":null,"RunAsNonRoot":null,"SupplementalGroups":null,"FSGroup":null},"ImagePullSecrets":null,"Hostname":"","Subdomain":"","Affinity":null,"SchedulerName":"default-scheduler","Tolerations":[{"Key":"node.kubernetes.io/not-ready","Operator":"Exists","Value":"","Effect":"NoExecute","TolerationSeconds":300},{"Key":"node.kubernetes.io/unreachable","Operator":"Exists","Value":"","Effect":"NoExecute","TolerationSeconds":300}],"HostAliases":null,"PriorityClassName":"","Priority":null,"DNSConfig":null}

B: 3000,"Protocol":"TCP","HostIP":""}],"EnvFrom":null,"Env":null,"Resources":{"Limits":null,"Requests":null},"VolumeMounts":[{"Name":"default-token-28mbg","ReadOnly":true,"MountPath":"/var/run/secrets/kubernetes.io/serviceaccount","SubPath":"","MountPropagation":null}],"VolumeDevices":null,"LivenessProbe":null,"ReadinessProbe":null,"Lifecycle":null,"TerminationMessagePath":"/dev/termination-log","TerminationMessagePolicy":"File","ImagePullPolicy":"Always","SecurityContext":null,"Stdin":false,"StdinOnce":false,"TTY":false}],"RestartPolicy":"Always","TerminationGracePeriodSeconds":30,"ActiveDeadlineSeconds":null,"DNSPolicy":"ClusterFirst","NodeSelector":null,"ServiceAccountName":"default","AutomountServiceAccountToken":null,"NodeName":"minikube","SecurityContext":{"HostNetwork":false,"HostPID":false,"HostIPC":false,"ShareProcessNamespace":null,"SELinuxOptions":null,"RunAsUser":null,"RunAsGroup":null,"RunAsNonRoot":null,"SupplementalGroups":null,"FSGroup":null},"ImagePullSecrets":null,"Hostname":"","Subdomain":"","Affinity":null,"SchedulerName":"default-scheduler","Tolerations":[{"Key":"node.kubernetes.io/not-ready","Operator":"Exists","Value":"","Effect":"NoExecute","TolerationSeconds":300},{"Key":"node.kubernetes.io/unreachable","Operator":"Exists","Value":"","Effect":"NoExecute","TolerationSeconds":300}],"HostAliases":null,"PriorityClassName":"","Priority":null,"DNSConfig":null}

image-20190209104247175

  • 이미지만 변경할수 있다는 걸 유념하자

Deployment

기존 포드형태에서는 이미지말고는 변경이 안된다. 이걸 극복하기 위해서 Depoyment라는 개념을 하나 더 추가를 하자

Deployment에서는 Pod 설정을 가지고 있다.

pod 에서 포트를 변경시 Deployment 에서는 포트를 죽이고 새로운 포트를 올린다.

그럼 새 파일을 만들어서 설정 파일을 만들자

client-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: client-deployment
spec:
  replicas: 1
  selector: 
    matchLabels:
      component: web
  template:
    metadata:
      labels:
        component: web
    spec:
      containers:         
        - name: client
          image: bear2u/multi-client
          ports:
            - containerPort: 3000 
  • template 밑에 포드를 구성
  • replicas 를 1개이상 설정시 템플릿을 여러개 만든다

image-20190209113138607

설정 파일 삭제

기존 설정 파일 삭제를 할 때 사용되는 스크립트

kubectl delete -f client-pod.yaml

10초 정도 후에 확인을 해보자

kubectl get pods

...
no resources found

새로운 deployment 설정 파일 적용

$ kubectl apply -f client-deployment.yaml
deployment.apps "client-deployment" configured
  • pods -> deployments 로 명령어가 변경이 되는 점 유의
$ kubectl get deployments
NAME                  DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
client-deployment     1         1         1            1           56d
  • pods 확인시 deployment 로 자동 생성되는 걸 확인 가능하다
$ kubectl get pods
NAME                                  READY     STATUS    RESTARTS   AGE
client-deployment-848b54d879-ch26z    1/1       Running   5          56d
  • 이미지를 바꿔서 새롭게 deployment 에서 포드가 변경되는 걸 보자
$ kubectl get pods
NAME                                  READY     STATUS              RESTARTS   AGE
client-deployment-848b54d879-ch26z    1/1       Running             5          56d
client-deployment-89bb69575-54pnn     0/1       ContainerCreating   0          5s
$ kubectl get pods
NAME                                  READY     STATUS    RESTARTS   AGE
client-deployment-89bb69575-54pnn     1/1       Running   0          43s
  • 자세한 설명을 보여주는 명령어
$ kubectl get pods -o wide
NAME                                  READY     STATUS    RESTARTS   AGE       IP            NODE
client-deployment-89bb69575-54pnn     1/1       Running   0          7m        172.17.0.1   minikube

image-20190209120048329

kubectl describe pods client-deployment
  • deployment.yaml 에서 replica를 변경시 숫자를 주목하자
...
replicas: 5
....
$ kubectl apply -f client-deployment.yaml

$ kubectl get deployment
NAME                  DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
client-deployment     5         3         3            1           56d
  • 하나씩 하나씩 올라가는 걸 확인 할 수 있다.

도커 업데이트 이미지 배포

만약 도커내 이미지가 업데이트 되는 경우 배포를 하는 경우에 대해서 알아보자

  • 도커 허브에 푸시
  • 마지막 이미지를 다시 가져와서 배포
kubectl apply -f client-deployment.yaml

nothing changed

하지만 이미 변경된 내용이 없기 때문에 적용이 안된다. 이 부분을 해결하기 위해서는 다음 3가지의 방법이 있을 수 있다

  • 수동으로 설정을 삭제한다 (비추천)
  • 태그별로 이미지를 만들어서 업데이트 진행(많이 쓰는 편)
    • 이미지가 변경되기 때문에 새로운 포드가 올라가고 업데이트를 진행한다
    • ex) bear2u/multi-client:v1...v2
  • 선언형 명령어로 업데이트 선언

새로운 업데이트 단계

  1. 도커 이미지 태그별로 생성

    docker build -t bear2u/multi-client:v2 .
    
  2. 도커 허브로 푸시

    docker push <tag>
    
  3. 쿠버네티스 업데이트 명령어로 실행

    # kubectl set image <object_type> / <object_name> <container_name> = <new image to use>
    
    $ kubectl set image deployment/client-deployment client=bear2u/multi-client:v2
    
  4. 업데이트 서버 테스트

    http://192.168.99.100:31515
    

image-20190209130349946

image-20190209131039743

  • 현재 로컬에 있는 도커와 쿠버네티스(미니큐브)는 다른 가상서버라서 도커가 분리되서 공유되지 않는다

해당 미니큐브 서버로 접속하기 위해서는 eval 명령어를 이용할 수 있다. (도커 환경 설정이 가능하다)

실행시 미니큐브에 있는 도커 콘테이너를 볼수 있다

$ eval $(minikube docker-env)
$ docker ps
........
CONTAINER ID        IMAGE                        COMMAND                  CREATED             STATUS PORTS                                                                NAMES
5551a5172d45        bear2u/multi-client          "nginx -g 'daemon of…"   14 minutes ago      Up 14 minutes                                                                      k8s_client_client-deployment-747d8c754-kncgq_default_17678d42-2c1f-11e9-8634-08002761bac3_0
5d2e22a0c7c9        k8s.gcr.io/pause-amd64:3.1   "/pause"                 14 minutes ago      Up 14 minutes                                                                      k8s_POD_client-deployment-747d8c754-kncgq_d
$ minikube docker-env

export DOCKER_TLS_VERIFY="1"
export DOCKER_HOST="tcp://192.168.99.100:2376"
export DOCKER_CERT_PATH="/Users/bear2u/.minikube/certs"
export DOCKER_API_VERSION="1.35"
# Run this command to configure your shell:
# eval $(minikube docker-env)

그럼 이 과정이 필요한 이유는 ?

  • 직접 쿠버네티스 노드 안에 도커 로그를 확인할 경우

  • 쿠버네티스내 캐싱된 도커들 정리할 경우

    • docker system prune -a
      

kubectl 로 minikube 도커로 접속가능

kubectl exec -it <docker image name>
kubectl logs <docker image name>

결론

오늘 공부한 내용은 다음과 같다.

  • 포드에 대한 설명 및 실행 스크립트
  • 업데이트시 하는 방법에 대해서 고민
  • Pods와 Depolyment와의 관계
  • minikube 내 도커 시스템 접속 및 관리
  • 설정 파일 삭제 방법

이상으로 9주차 도커 & 쿠버네티스 스터디 정리 내용이다. 참석해주셔서 감사합니다.

부산에서 매주 진행되는 스터디입니다.

부산에서 다른 스터디 내용을 보실려면 카페 에서 보실수 있습니다.

https://www.udemy.com/docker-and-kubernetes-the-complete-guide 을 공부하고 있습니다

Kubernetes

minikube start

상태확인

minikube status

클러스터 정보 확인

kubectl cluster-info

목표

다양한 도커 이미지들을 활용해서 로컬 쿠버네티스를 통해서 올리는 게 목표

도커 컴포즈와 쿠버네티스 비교

  • docker-compose
    • 이미지들을 각각 빌드해서 올린다
    • 우리가 원하는 컨테이너를 만든다
    • 각 네트워크 속성들에 대해 정의를 각각 한다.
  • kubernetes
    • 이미 모든 이미지들이 만들어져있다고 가정한다
    • 우리가 만들기를 원하는 하나의 오브젝트에 하나의 설정을 준비한다
    • 우리는 모든 네트워크를 수동으로 설정해야 한다

우리는 도커 허브에 이미 올라가있는 이미지를 가져와서 쿠버네티스에서 설정하고 배포를 하는 과정을 진행을 할 예정이다.

포드 구성 파일 추가

  • docker-hub 에서 bear2u/multi-client 검색
  • 폴더를 새로 추가 (mkdir sample-k8s)
  • 설정 파일 추가
    • client-pod.yaml

apiVersion: v1 kind: Pod metadata: ## 1 name: client-pod labels: component: web spec: ## 2 containers: - name: client image: bear2u/multi-client ports: ## 3 - containerPort: 3000

  1. kind는 만들고자 하는 오브젝트 유형
  2. 다른 설정에서 불러와서 사용할 이름 정의
  3. 포드 내 콘테이너들 설정 정의를 한다
  4. 포드 내 콘테이너 포트를 정의를 한다. (내부용인걸 유념)

노드 설정 파일 추가

client-node-port.yaml

apiVersion: v1 kind: Service metadata: name: client-node-port spec: type: NodePort ports: # 1 - port: 3050 # 2 targetPort: 3000 # 3 nodePort: 31515 selector: # 4 component: web

서비스 형태의 설정파일을 추가한다.

  • kind : Service 정의해서 서비스 설정 파일인걸 체크
  1. port는 해당 서비스 포트를 뜻한다. 외부로 향하는 포트는 아니다.
  2. targetPort는 내부에 콘테이너 포트를 뜻한다.
  3. nodePort는 외부에서 접속시 들어올 수 있는 포트를 뜻한다.

현재 개발 서버이기 때문에 따로 RB(로드 발랜싱)은 지정하지 않는다.

설정파일

설정파일은 다음 4가지로 만들어질 수 있다.

  • StatefulSet
  • ReplicaController
  • Pod
  • Service

apiVersion은 다음 두가지 종류를 가진다.

image-20190208222316470

기본 적인 노드의 구성

image-20190208222519850

  • 포드는 하나이상의 콘테이너를 담고 있어야 한다
  • 하나의 포드에 여러개의 콘테이너를 담는 경우는 정말 긴밀하게 연결되어 있는 경우를 생각하자

설정 파일 타입

image-20190208223230713

바람직한 예

image-20190208222759200

구성도 (미니 큐브)

image-20190208223333452

image-20190208223526446

image-20190208223630053

설정 파일 선언

이제 설정 파일 두개를 kubectl 를 통해서 선언을 하도록 하자.

kubectl apply -f client-pod.yaml
kubectl apply -f client-node-port.yaml

pods & services

설정한 내용에 대해서 포드 동작되는 지 체크할려면 아래와 같이 할 수 있다

kubectl get pods

.......
client-pod                            1/1       Running   1          1h

서비스에 대해서도 가져올 수 있다

kubectl get services

........
client-node-port              NodePort    10.109.106.191   <none>        3050:31515/TCP   6d

웹 테스트

이제 서버에 동작이 잘 되는지 체크하자.

우선 IP를 가져오자. vm box를 통해서 가져와야 하기 때문에 머신을 통해야 한다

minikube ip

........
192.168.99.100

이제 실행해보자. 웹 브라우저에 다음과 같이 쳐서 들어갈 수 있다

http://192.168.99.100:31515/

전체 배포 흐름

image-20190208224323680

참고

  • docker ps 를 하면 현재 실행되고 있는 콘테이너들이 보인다. 그걸 죽이더라도 쿠버네티스가 다시 시작한다.
  • 선언형으로 이루어져서 운영자는 마스터에 설정파일을 적용하면 스케쥴로 인해 포드수를 서서히 올리거나 줄이도록 한다.
  • 물론 명령형으로 수동으로 올리거나 내릴순 있지만 추천은 안함


React,MongoDB,Express,Nginx 도커 개발 환경 구성하기

이번 시간에는 대중적으로 많이 쓰이는 환경을 도커로 하나씩 구축해볼 예정이다.

구성 스택은 다음과 같다.

  1. Client : React
  2. Api Server : Node Express
  3. DB : Mongo
  4. Server : Nginx

Root

우선 환경을 구성한다. 기본적으로 3개의 폴더와 docker-compose.yaml 파일로 구성된다.

- client
	- ...sources
	- Dockerfile.dev
- nginx
	- default.conf
	- Dockerfile.dev	
- server
	- ...sources
	- Dockerfile.dev
- docker-compose.yaml	

Client

create-react-app 을 이용해서 구성한다.

- ./client

# create-react-app client

dockerfile.dev 파일을 만들어 준다.

# 노드가 담긴 alpine 이미지 가져오기
FROM node:10.15-alpine

//작업할 디렉토리 설정
WORKDIR "/app"

//npm install을 캐싱하기 위해 package.json만 따로 카피
COPY ./package.json ./
RUN npm install
 
// 소스 복사 
COPY . .

//client 소스 실행
CMD ["npm","run","start"]

Server

express generator를 이용해서 만들 예정이다.

npm install -g express-generator

express server

package.json에는 실시간 개발환경을 위해서 nodemon을 추가해주자.

  "scripts": {
    "dev": "nodemon ./bin/www",
    "start": "node ./bin/www"
  },

서버쪽에도 dockerfile.dev을 만들어보자

FROM node:10.15-alpine

WORKDIR "/app"

COPY ./package.json ./

RUN npm install -g nodemon \
 && npm install
 
COPY . .

CMD ["npm","run","dev"]

Nginx

프록시 서버로 동작될 예정이다.

client와 server 부분을 따로 프록시로 관리한다.

server는 / 로 들어오는 요청을 /api 로 바꿔준다.

파일 두개를 생성해주자

  • default.conf
# nginx/default.conf

upstream client {
    server client:3000;
}

upstream server {
    server server:3050;
}

server {
    listen 80;

    location / {        
        proxy_pass http://client;
    }

    location /sockjs-node {
        proxy_pass http://client;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
    }

    location /api {
        rewrite /api/(.*) /$1 break;
        proxy_pass http://server;
    }
}
  • Dockerfile.dev
# nginx/Dockerfile.dev

FROM nginx
COPY ./default.conf /etc/nginx/conf.d/default.conf

Docker-compose

이제 기본 생성된 파일들을 묶어주고 간편하게 한번에 올릴 수 있게 하자.

  • docker-compose.yaml
version: "3"
services:
  client:                 
    build:
      dockerfile: Dockerfile.dev
      context: ./client    
    volumes:
      - ./client/:/app      
      - /app/node_modules  
    networks:
      - backend           
  server:                          
    build:
      dockerfile: Dockerfile.dev
      context: ./server    
    volumes:
      - ./server/:/app      
      - /app/node_modules      
    environment:
      - NODE_PATH=src
      - PORT=3050
      - DB_HOST=mongo
      - DB=test
      - REDIS_HOST=redis
      - REDIS_PORT=6379      
    networks:
      - backend  
    depends_on:
      - mongo
      - redis 
    ports:
      - "5000:3050"   
  redis:
    container_name: redis
    image: redis
    environment:
      - ALLOW_EMPTY_PASSWORD=yes
    networks:
      - backend 
    volumes:
      - data:/data/redis   
    ports:
      - "6379:6379"
    restart: always    
  mongo:
    container_name: mongo
    image: mongo
    volumes:
      - data:/data/db
    ports:
      - "27017:27017" 
    networks:
      - backend

  nginx:
    restart: always
    build:
      dockerfile: Dockerfile.dev
      context: ./nginx 
    ports:
      - '3000:80' 
    networks:
      - backend
    

networks: 
  backend:
    driver: bridge

volumes:
  data:
    driver: local  

위 내용을 잠깐 설명하자면 기본적으로 서비스는 5개를 실행하고 있다.

  • client
  • server
  • mongo
  • redis
  • nginx

이 많은 서비스를 저 파일 하나로 묶어서 배포를 할수 있다. 멋지다.

자 그럼 실행해서 잘되는 지 체크 해보자.

실행

# --build는 내부 Dockerfile 이 변경시 다시 컴파일 해준다. 
docker-compose up --build or docker-compose up

docker-compose stop or docker-compose down

일단 이상태로는 기본 환경만 올라간 상태라서 뭘 할수 있는 건 아니다.

소스를 좀 더 추가해서 연동을 해보자.

Client

  • App.js
    • npm install axios
import React, { Component } from "react";
import axios from "axios";
import Items from './Items';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      title: "",
      content: "",
      change: false
    };

    this.handleChange = this.handleChange.bind(this);
    this.sumbit = this.sumbit.bind(this);
  }

  handleChange(event) {
    const target = event.target;
    const value = target.value;
    const name = target.name;

    console.log(name, value);

    this.setState({
      [name]: value
    });
  }

  async sumbit(event){
    event.preventDefault();
    console.log(`${this.state.title},${this.state.content}`);

    await axios
      .post("/api/todos", {
        title: this.state.title,
        content: this.state.content
      })
      .then((result) => {
        this.setState({
          change : true
        });
      })
  }

  render() {
    return (
      <div style={{ margin: "100px" }}>
        <div>
          <form>
            <p>투두리스트</p>
            <input type="text" name="title" onChange={this.handleChange} />
            <br />
            <textarea name="content" onChange={this.handleChange} />
            <button onClick={this.sumbit}>전송</button>
          </form>
        </div>
        <Items change={this.state.change}/>
      </div>
    );
  }
}

export default App;
  
await axios
      .post("/api/todos", {
        title: this.state.title,
        content: this.state.content
      })

여기에서 /api를 통해서 바로 도커내 api서버로 접근을 할 수 있다.

그리고 추가된 리스트는 Items.js 컴포넌트를 만들어서 관리한다.

import React, { Component } from "react";
import axios from "axios";

class Items extends Component {
  constructor(props) {
    super(props);
    this.state = {
        Todos: []
    }
  }

  componentWillReceiveProps(props) {
      console.log(props);
      this.renderTodos();
  }

  componentDidMount() {
      this.renderTodos();
  }

  async renderTodos() {

    try {
        let todos = await axios.get("/api/todos");

        this.setState({
            Todos: todos.data.map(todo => {
                console.log(this.getItem(todo));
                return this.getItem(todo);
            })
        });
    } catch (err) {
        console.log(err);
    }
  }

  getItem(todo) {
    //   console.log(todo);
    return (
      <div key={todo._id}>
        <div style={{ padding: "10px", border:"1px solid red" }}>          
          <div>{todo.title}</div>
          <div>{todo.content}</div>
          <div>{todo.regdate}</div>
        </div>
      </div>
    );
  }

  render() {
    return <div>{this.state.Todos}</div>;
  }
}

export default Items;

결과 화면

image-20190119144313239

아주 심플하지만 작동은 잘 된다. 서버쪽 소스도 업데이트를 해야 잘 나오는 것 명심하자.

Server

Server에서 추가 사항은 몽고 디비와 연동해서 데이터 주고 받는 부분이다.

  • app.js
    • npm install mongoose
  var createError = require('http-errors');
  var express = require('express');
  var path = require('path');
  var cookieParser = require('cookie-parser');
  var logger = require('morgan');
  var cors = require('cors');
  var mongoose = require('mongoose');
  
  var db = mongoose.connection;
  db.on('error', console.error);
  db.once('open', function(){    
      console.log("Connected to mongod server");
  });
  
  mongoose.connect('mongodb://mongo/todo');
  
  var indexRouter = require('./routes/index');
  var usersRouter = require('./routes/users');
  var todoRouter = require('./routes/todo');
  
  var app = express();
  
  //Cors 설정
  app.use(cors());
  
  // view engine setup
  app.set('views', path.join(__dirname, 'views'));
  app.set('view engine', 'jade');
  
  app.use(logger('dev'));
  app.use(express.json());
  app.use(express.urlencoded({ extended: false }));
  app.use(cookieParser());
  app.use(express.static(path.join(__dirname, 'public')));
  
  app.use('/', indexRouter);
  app.use('/users', usersRouter);
  app.use('/todos', todoRouter);
  
  // catch 404 and forward to error handler
  app.use(function(req, res, next) {
    next(createError(404));
  });
  
  // error handler
  app.use(function(err, req, res, next) {
    // set locals, only providing error in development
    res.locals.message = err.message;
    res.locals.error = req.app.get('env') === 'development' ? err : {};
  
    // render the error page
    res.status(err.status || 500);
    res.render('error');
  });
  
  module.exports = app;
  
  • todo.js
var express = require('express');
var router = express.Router();

var Todo = require('../models/todo');

/* GET home page. */
router.get('/', function(req, res, next) {
  return Todo.find({}).sort({regdate: -1}).then((todos) => res.send(todos));
});

router.post('/', function(req, res, next) {
  const todo = Todo();
  const title = req.body.title;
  const content = req.body.content;

  todo.title = title;
  todo.content = content;

  return todo.save().then((todo) => res.send(todo)).catch((error) => res.status(500).send({error}));
});

module.exports = router;
  • model/todo.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;

var todoSchema = new Schema({
    content: String,
    title: String,
    isdone : Boolean,
    regdate: { type: Date, default: Date.now  }
});

module.exports = mongoose.model('todo', todoSchema);

마무리

다시 docker-compose up --build로 통해서 올려보면 작동이 잘 되는 걸 볼수 있다.

전체 소스는 여기에서 받을수 있다.

이상으로 GDG 부산에서 진행하는 도커 & 쿠버네티스 스터디 내용이었습니다.

참가해주셔서 감사합니다.

도커&쿠버네티스 7주차 스터디 정리내용

부산에서 매주 진행되는 스터디입니다.

부산에서 다른 스터디 내용을 보실려면 카페 에서 보실수 있습니다.

https://www.udemy.com/docker-and-kubernetes-the-complete-guide 을 공부하고 있습니다

1주차 스터디

2주차 스터디

3주차 스터디

4주차 스터디

5주차 스터디

6주차 스터디

이번주 스터디 내용

이번주는 저번 시간에 이어서 Dockerhub로 올린 이미지를 aws에 beanstalk로 올리는 걸 진행하도록 하겠다.

4주차에는 바로 도커이미지를 aws beanstalk 로 올리는 걸 진행했었다.

이제는 실제 Production에 사용되는 형태로 진행하는 걸 배워볼 것이다.

여기서 진행된 소스는 여기 Github 에서 받을수 있다.

배우게 되는 내용

  • AWS EC2 Task Definition
  • AWS VPS 등 기본 설정

1542771228782

AWS EB 설정

aws eb에 여러개의 도커 이미지(client, serverworker)등을 올리기 위해서는 설정 파일이 필요하다.

  • Dockerrun.aws.json

1542764486465

1542764529514

AWS ECS Task Definition

Beanstalk 에는 어떻게 컨테이너들이 동작될지를 알수가 없다. 그래서 aws ec2에서는 task definition 기능을 통해서 이미지를 어떻게 동작될지를 정의를 할 수 있다.

1542764875545

실습

/Dockerrun.aws.json

Dockerhub 에 올라간 tag/name 을 확인하자.

  • name : 이미지 이름

  • image : 도커 허브에 올라간 이미지 이름

  • hostname : docker-compose.yml 에 있는 이름

  • essential : 만약 콘테이너가 어떤 이유로 문제가 생겼을 경우 연관되어 있는 다른 컨테이너들을 같이 내릴건지 여부 ( true 이면 다같이 내린다. )

  • memory : 각 컨터이너마다 얼마나 메모리를 할당할건지 지정

  • portMapping : 외부 포트와 내부 콘테이너 포트를 바인딩

  • links : 콘테이너가 연관되어 있는 걸 연결한다.

    1542765885341

{
    "AWSEBDockerrunVersion" : 2,
    "containerDefinitions" : [
        {
            "name": "client",
            "image": "bear2u/multi-client",
            "hostname": "client",
            "essential": false,
            "memory": 128
        },
        {
            "name": "server",
            "image": "bear2u/multi-server",
            "hostname": "api",
            "essential": false,
            "memory": 128
        },
        {
            "name": "worker",
            "image": "bear2u/multi-worker",
            "hostname": "worker",
            "essential": false,
            "memory": 128
        },
        {
            "name": "nginx",
            "image": "bear2u/multi-nginx",
            "hostname": "nginx",
            "essential": true,
            "portMappings": [
                {
                    "hostPort": 80,
                    "containerPort": 80
                }
            ],
            "links": ["client", "server"],
            "memory": 128
        }
    ]
}

Aws Beanstalk

AWS 콘솔에 들어가서 새로운 어플리케이션을 만들어준다.

  • multi-docker

1542766325151

create one > Multi-container Docker 선택

1542766420194

1542766452949

1542766606837

Prod. 구조

Dockerrun.aws.json 내부에는 따로 postgres 와 redis 에 관한 DB 콘테이너가 없다. 그러면 어떻게 설정할까?

기존에 배웠던 개발 구조

1542766660010

배우게 될 배포(Production) 구조



위 그림에서 보다시피 Redis 는 AWS Elastic cache 로 빠져있고 postgres 는 aws rds 로 빼서 사용하고 있는 걸 볼수 있다. 가능하다면 이런 구조를 만드는게 더 이점이 많다고 한다.

이렇게 빼는 이유에 대해서 알아보자.

AWS Elastic Cache (Redis)

  • 자동으로 redis 인스턴스를 만들어주고 유지보수 해준다.
  • 스케일링하기 쉽다.
  • 로깅 및 유지관리
  • 보안성이 우리가 하는것보다 전문가들이 하기 때문에 더 안전하다.

AWS RDS (Postgres)

  • 자동으로 postgres 인스턴스를 만들수 있고 유지보수 하기도 쉽다.

  • 스케일링 하기 쉽다.

  • 로깅 및 유지보수하기 쉽다.

  • 우리가 하는것보다 안전하다.

  • 자동으로 백업 및 롤백 지원

다음에는 내부 콘테이너에 넣어서 하는 방법에 대해서도 공부할 예정이다.

EB와 DB 연결

1542767378510

현재로써는 EB에서는 RDS와 EC 연결을 서로 알수 없는 구조이다. 이걸 연결하기 위해서는 aws 콘솔에서 서로 묶어주는 게 필요하다.

1542767495818

1542767712822

VPC

VPC 에 가보면 MultiDocker-env 라는 security group 이 만들어진 걸 볼 수 있다.

1542767849145

EB 와 DB 간의 연결

1542767924722

Postgres 생성

  1. RDS (postgres`) 를 만든다.

하단에 프리티어 체크 박스를 꼭 활성화를 해주자.

1542768159192

  1. Postgress 기본 입력
  • identifier: multi-docker-postgress
  • username : postgres
  • password : postgresspassword

1542768322944

1542768448718

1542768508822

Elastic chache (Redis) 생성

1542768613773

Node type 값을 t2로 선택을 하는 걸 꼭 유의하자.

1542768704289

1542769330034

1542769364030

Create Security Group

1542769789607

1542769829241

inbound 설정

1542769905067

각 서비스 Security Group 지정

  • 방금 만든 security 설정을 elasticache와 rds 에 지정해준다.

REDIS VPC 지정

1542770208533

RDS 수정

1542770335728

1542770365368

1542770404982

Elastic Beanstalk 수정

1542770512070

1542770547234

EB 환경설정

1542770672978

1542770953026

Redis Host

1542770773324

RDS Host

1542770894210

AWS IAM 키 설정

Service 에서 IAM 검색해서 들어가자

1542771348365

1542771412892

1542771460621

1542771499669

Travis Setting

Travis -> github -> id 설정

1542771598675

Travis.yml deploy 설정

sudo: required
services:
  - docker

before_install:
  - docker build -t bear2u/react-test -f ./client/Dockerfile.dev ./client

script:
  - docker run bear2u/react-test npm test -- --coverage

after_success:
  - docker build -t bear2u/multi-client ./client
  - docker build -t bear2u/multi-nginx ./nginx
  - docker build -t bear2u/multi-server ./server
  - docker build -t bear2u/multi-worker ./worker
  # Log in to the docker CLI
  - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_ID" --password-stdin
  # Take those images and push them to docker hub
  - docker push bear2u/multi-client
  - docker push bear2u/multi-nginx
  - docker push bear2u/multi-server
  - docker push bear2u/multi-worker

# add this
deploy:
  provider: elasticbeanstalk
  region: ap-northeast-2
  app: multi-docker
  env: MultiDocker-env
  bucket_name : elasticbeanstalk-ap-northeast-2-234249885524
  bucket_path : docker-multi
  on:
    branch: master
  access_key_id: $AWS_ACCESS_KEY
  secret_access_key:
    secure: $AWS_SECRET_KEY

배포

이제 github master 브랜치에 수정된 내용을 push를 해보고 travis와 beanstalk 에서 결과를 확인하자.

1542772294954

만약에 beanstalk 에 문제가 발생한 경우 Logs 를 통해서 확인 가능하다.

1542772467846

1542772646987

Cleaning Service

테스팅을 완료했으면 aws 리소스들을 삭제하자.

  • beanstalk
  • rds
  • redis
  • iam
  • security group
  • vpn

여기까지 7주차 스터디 내용이다. 다음주부터 본격적으로 쿠버네티스를 통해 관리하는 걸 진행하도록 한다.

참석해주셔서 감사합니다.


도커&쿠버네티스 6주차 스터디 정리내용

부산에서 매주 진행되는 스터디입니다.

부산에서 다른 스터디 내용을 보실려면 카페 에서 보실수 있습니다.

https://www.udemy.com/docker-and-kubernetes-the-complete-guide 을 공부하고 있습니다

1주차 스터디

2주차 스터디

3주차 스터디

4주차 스터디

5주차 스터디

스터디 내용

이번 6주차에는 이전 주차에서 공부한 내용을 기반으로 Client Production 을 Docker Hub 로 Travis를 통해서 자동 배포하는 걸 배울 예정이다.

그리고 Production 을 하는 방법에 대해서도 공부하도록 한다.

이전에 배운 내용(Single) 과 이번 주차 부터 배울 내용에 대한 Flow 를 알아보도록 하자.

Single Container Flow

  • Github 에 푸시
  • Travis 에 자동으로 레포를 웹훅으로 가져온다.
  • Travis에서 이미지를 빌드하고 코드들을 테스팅 한다.
  • AWE EB(ElasticBean) 에 Travis는 코드를 푸시한다.
  • EB는 이미지를 빌드하고 그걸 배포한다.

1541856986612

Multi Container Flow

  • Github 에 푸시
  • Travis에 자동으로 레포를 웹훅으로 가져온다.
  • Travis 는 이미지를 테스트하고 코드를 테스트한다.
  • **Travis 는 Production 이미지를 빌드한다. **
  • Travis 는 도커허브에 Prod. 이미지들을 푸시한다.
    • Docker Hub ID,PWD 는 따로 셋팅에서 저장한다.
  • Travis 는 AWS EB 에 프로젝트를 푸시한다.
  • EB는 도커허브로 부터 이미지들을 가져와서 배포한다.

1541857220104

사전 준비

자 그럼 시작을 해보자.

우선 소스를 이전꺼 다음으로 진행할텐데 Github checkout 으로 같이 시작을 할 수 있다.

> git clone https://github.com/bear2u/docker-study2.git
...
> git checkout 64b470215f24a1450cd7d70d72f79700f0781542
....

Dockerfile

worker / Dockerfile

Dockerfile.dev 파일 내용과 같으며 마지막에 커맨드 라인만 dev -> start 로 변경해준다.

FROM node:alpine
WORKDIR "/app"
COPY ./package.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "start"]

server / Dockerfile

FROM node:alpine
WORKDIR "/app"
COPY ./package.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "start"]

nginx / Dockerfile

FROM nginx
COPY ./default.conf /etc/ngnix/conf.d/default.conf

Client with another nginx

client 은 좀 복잡하다.

싱글 콘테이너를 만들때 nginx 와 react build를 통해서 서버를 구성했었다.

1541858471574

하지만 멀티 콘테이너를 구성시 외부에도 nginx 를 두고 내부에도 nginx 를 세팅해야 하는 걸 명심하자.

1541858595682

그럼 멀티 콘테이너 구성시 어떻게 하는지 알아보자.

Client 내부 ngnix 구성

client / nginxdefault.conf

3000 포트로 들어오도록 허용하며 index 파일을 메인으로 한다.

server {
    listen 3000;

    location / {
        root /usr/share/nginx/html;
        index index.html index.htm;
        try_files $uri $uri/ /index.html;
    }
}

client/Dockerfile

  • builder 이미지에 build 된 Prod 파일들을 넣고
  • nginx 설정을 복사를 하고
  • nginx 에 이전 builder 내 /app/build 폴더에 있는 Prod 파일들을 html 로 복사를 해준다.
FROM node:alpine as builder
WORKDIR '/app'
COPY ./package.json ./
RUN npm install
COPY . .
RUN npm run build

FROM nginx
EXPOSE 3000
COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/build /usr/share/nginx/html

Client test 수정

현재로썬 Client Test 진행시 충돌이 발생될 수 있다고 한다. 당장은 비활성화를 하고 진행하도록 하자.

client / src/ App.test.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

it('renders without crashing', () => {});

이제 준비가 다 되었다. Travis 를 연결해서 배포를 해보자.

Travis + Github 연동

만약 새로운 프로젝트이면 Github에 저장소를 만들어서 푸시하고Travis 에서 활성화를 해주도록 하자.

  • Github 에 푸시

  • Travis 에서 연동

    • 만약 목록에 안나온다면 왼쪽 상단에 Sync Account를 클릭해서 동기화를 하자.

      1541860215515

1541860438426

Travis 설정

1541860575095

.travis.yml

주의점은 travis 설정시 태그를 꼭 도커 허브 아이디를 태그명으로 지정해줘야 한다.

sudo: required
services:
  - docker

before_install:
  - docker build -t bear2u/react-test -f ./client/Dockerfile.dev ./client

script:
  - docker run bear2u/react-test npm test -- --coverage

after_success:
  - docker build -t bear2u/multi-client ./client
  - docker build -t bear2u/multi-nginx ./nginx
  - docker build -t bear2u/multi-server ./server
  - docker build -t bear2u/multi-worker ./worker
  # Log in to the docker CLI
  - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_ID" --password-stdin
  # Take those images and push them to docker hub
  - docker push bear2u/multi-client
  - docker push bear2u/multi-nginx
  - docker push bear2u/multi-server
  - docker push bear2u/multi-worker

Beanstalk 에 올리는 과정을 제외한 docker hub에 올리는 것까지 진행되었다.

도커 허브 관련해서 id, password 는 셋팅을 통해서 해준다.

Settings

1541861411570

그럼 Github에 푸시를 해본다.

Travis 가 정상적으로 연동되어서 hub 에 푸시가 되는 걸 확인 할 수 있다.

.... 


Time:        1.19s
Ran all test suites.
--------------------------|----------|----------|----------|----------|-------------------|
File                      |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
--------------------------|----------|----------|----------|----------|-------------------|
All files                 |        0 |        0 |        0 |        0 |                   |
 App.js                   |        0 |      100 |        0 |        0 |                10 |
 Fib.js                   |        0 |      100 |        0 |        0 |... 44,45,52,56,62 |
 OtherPage.js             |        0 |      100 |        0 |        0 |                 5 |
 index.js                 |        0 |        0 |        0 |        0 |     1,2,3,4,5,7,8 |
 registerServiceWorker.js |        0 |        0 |        0 |        0 |... 36,137,138,139 |
--------------------------|----------|----------|----------|----------|-------------------|
travis_time:end:2e40ccb6:start=1541861556459182738,finish=1541861559398870991,duration=29396882

......
......


ac7ed8526610: Pushed
5761fba18cc8: Pushed
e9aaefdfed5d: Pushed
latest: digest: sha256:2eadc96e313836b05b807d46c2692c2818856c11aa1aa15dde69edd2103a5315 size: 1782
travis_time:end:0578baae:start=1541861604426303298,finish=1541861609935082744,duration=5508779446
�[0Ktravis_fold:end:after_success.9
�[0K
Done. Your build exited with 0.

1541861811733

이렇게 도커 허브에 정상적으로 올라간 걸 볼수 있다.

여기까지 소스는 Github 에서 확인이 가능하다.

다음 시간에는 aws 의 beanstalk 으로 올려서 docker hub를 통해서 이미지를 가져와서 서비스를 실행하는 방법에 대해서 공부할 예정이다.

이상으로 도커 & 쿠버네티스 6주차 스터디 정리 내용이다.

참석해주셔서 감사합니다.

도커 & 쿠버네티스 5주차

부산에서 매주 진행되는 스터디입니다.

부산에서 다른 스터디 내용을 보실려면 카페 에서 보실수 있습니다.

https://www.udemy.com/docker-and-kubernetes-the-complete-guide 을 공부하고 있습니다.

1주차 스터디

2주차 스터디

3주차 스터디

4주차 스터디

5주차 스터디 공부 내용

이번 차에서는 좀 더 복잡한 형태의 서비스를 구성해볼 예정이다.

인덱스를 넣으면 해당되는 피보나치 수열을 계산하는 출력해주는 앱을 개발할 예정이다.

화면 구성

화면은 React 로 작성될 예정이며 아래와 같이 구성된다.

  • 인덱스를 입력받는다.

  • Indicies I hava seen 에서는 지금까지 입력받은 인덱스를 저장해서 보여준다. ( by postgres db)

  • 마지막 Worker 라는 로직을 통해서 수열이 계산되어 지고 그 값이 Redis 에 저장되서 보여주게 된다.

서비스 구성


  • Nginx` 서버를 통해서 접근된다.

  • 프론트는 React Server 로 프록시 된다.

  • API 서버는 Express Server 로 프록시 된다.

  • Express ServerRedis 로 값을 호출하지만 있는 경우 바로 리턴이 되겠지만 없는 경우 Worker 를 통해서 계산된 값을 다시 가져와서 저장하고 리턴을 한다.

  • 마지막으로 유저가 입력한 index은 저장을 postgres 통해서 한다.

이후에 진행되는 https://github.com/bear2u/docker-study2 여기에서 받을수 있다.

소스 구성

Worker

피보나치 수열을 계산하는 로직이 담긴 서비스를 개발한다.

  • redis 추가시 구독

  • 새로운 값이 입력되는 경우 fib(index) 함수를 통해서 값을 계산해서 다시 redis 에 저장한다.

keys.js

module.exports = {
 redisHost: process.env.REDIS_HOST,
 redisPort: process.env.REDIS_PORT
};

index.js

const keys = require('./keys');
const redis = require('redis');

const redisClient = redis.createClient({
 host: keys.redisHost,
 port: keys.redisPort,
 retry_strategy: () => 1000
});
const sub = redisClient.duplicate();

function fib(index) {
 if (index < 2) return 1;
 return fib(index - 1) + fib(index - 2);
}

sub.on('message', (channel, message) => {
 redisClient.hset('values', message, fib(parseInt(message)));
});
sub.subscribe('insert');

package.json

{
 "dependencies": {
   "nodemon": "1.18.3",
   "redis": "2.8.0"
},
 "scripts": {
   "start": "node index.js",
   "dev": "nodemon"
}
}

Server

keys.js

  • 설정값은 추후 도커 환경변수로 입력받게 된다.

module.exports = {
 redisHost: process.env.REDIS_HOST,
 redisPort: process.env.REDIS_PORT,
 pgUser: process.env.PGUSER,
 pgHost: process.env.PGHOST,
 pgDatabase: process.env.PGDATABASE,
 pgPassword: process.env.PGPASSWORD,
 pgPort: process.env.PGPORT
};

index.js

  • express 서버 사용

  • postgres 호출

  • redis 호출

  • api 호출 따른 restful 작성

const keys = require('./keys');

// Express App Setup
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');

const app = express();
app.use(cors());
app.use(bodyParser.json());

// Postgres Client Setup
const { Pool } = require('pg');
const pgClient = new Pool({
 user: keys.pgUser,
 host: keys.pgHost,
 database: keys.pgDatabase,
 password: keys.pgPassword,
 port: keys.pgPort
});
pgClient.on('error', () => console.log('Lost PG connection'));

pgClient
.query('CREATE TABLE IF NOT EXISTS values (number INT)')
.catch(err => console.log(err));

// Redis Client Setup
const redis = require('redis');
const redisClient = redis.createClient({
 host: keys.redisHost,
 port: keys.redisPort,
 retry_strategy: () => 1000
});
const redisPublisher = redisClient.duplicate();

// Express route handlers

app.get('/', (req, res) => {
 res.send('Hi');
});

app.get('/values/all', async (req, res) => {
 const values = await pgClient.query('SELECT * from values');

 res.send(values.rows);
});

app.get('/values/current', async (req, res) => {
 redisClient.hgetall('values', (err, values) => {
   res.send(values);
});
});

app.post('/values', async (req, res) => {
 const index = req.body.index;

 if (parseInt(index) > 40) {
   return res.status(422).send('Index too high');
}

 redisClient.hset('values', index, 'Nothing yet!');
 redisPublisher.publish('insert', index);
 pgClient.query('INSERT INTO values(number) VALUES($1)', [index]);

 res.send({ working: true });
});

app.listen(5000, err => {
 console.log('Listening');
});

package.json

{
 "dependencies": {
   "express": "4.16.3",
   "pg": "7.4.3",
   "redis": "2.8.0",
   "cors": "2.8.4",
   "nodemon": "1.18.3",
   "body-parser": "*"
},
 "scripts": {
   "dev": "nodemon",
   "start": "node index.js"
}
}

Client

  • Routing 을 통해서 OtherPage.js 호출

  • react-create-app 을 통해서 설치

  • Fib.js, App.js, Otherpage.js 참고

Fib.js

import React, { Component } from 'react';
import axios from 'axios';

class Fib extends Component {
 state = {
   seenIndexes: [],
   values: {},
   index: ''
};

 componentDidMount() {
   this.fetchValues();
   this.fetchIndexes();
}

 async fetchValues() {
   const values = await axios.get('/api/values/current');
   this.setState({ values: values.data });
}

 async fetchIndexes() {
   const seenIndexes = await axios.get('/api/values/all');
   this.setState({
     seenIndexes: seenIndexes.data
  });
}

 handleSubmit = async event => {
   event.preventDefault();

   await axios.post('/api/values', {
     index: this.state.index
  });
   this.setState({ index: '' });
};

 renderSeenIndexes() {
   return this.state.seenIndexes.map(({ number }) => number).join(', ');
}

 renderValues() {
   const entries = [];

   for (let key in this.state.values) {
     entries.push(
       <div key={key}>
         For index {key} I calculated {this.state.values[key]}
       </div>
    );
  }

   return entries;
}

 render() {
   return (
     <div>
       <form onSubmit={this.handleSubmit}>
         <label>Enter your index:</label>
         <input
           value={this.state.index}
           onChange={event => this.setState({ index: event.target.value })}
         />
         <button>Submit</button>
       </form>

       <h3>Indexes I have seen:</h3>
      {this.renderSeenIndexes()}

       <h3>Calculated Values:</h3>
      {this.renderValues()}
     </div>
  );
}
}

export default Fib;

OtherPage.js

import React from 'react';
import { Link } from 'react-router-dom';

export default () => {
 return (
   <div>
     Im some other page
     <Link to="/">Go back to home page!</Link>
   </div>
);
};

App.js

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import { BrowserRouter as Router, Route, Link } from 'react-router-dom';
import OtherPage from './OtherPage';
import Fib from './Fib';

class App extends Component {
 render() {
   return (
     <Router>
       <div className="App">
         <header className="App-header">
           <img src={logo} className="App-logo" alt="logo" />
           <h1 className="App-title">Welcome to React</h1>
           <Link to="/">Home</Link>
           <Link to="/otherpage">Other Page</Link>
         </header>
         <div>
           <Route exact path="/" component={Fib} />
           <Route path="/otherpage" component={OtherPage} />
         </div>
       </div>
     </Router>
  );
}
}

export default App;

위의 모든 소스는 여기에서 받을수 있다.

자 그럼 실제 이러한 서비스들을 각각의 도커로 만들어서 묶어서 연결하는지를 공부해도록 하자. 기대되는 순간이 아닐수 없다.

도커 구성

Complex 폴더

자 그럼 이제 도커 세팅을 해보자. 우선 기본 환경은 세개의 폴더로 구성된 루트에서 작업이 시작될 예정이다.

complex 폴더

Client Dockerfile.dev

Client > Dockerfile.dev

FROM node:alpine
WORKDIR '/app'
COPY './package.json' ./
RUN npm install
COPY . .
CMD ["npm", "run", "start"]

이게 낯설다면 이전 주차들을 다시 공부하도록 하자.

> docker build -f Dockerfile.dev .
....
> docker run 도커 아이디

Server Dockerfile.dev

Server > Dockerfile.dev

FROM node:alpine
WORKDIR "/app"
COPY ./package.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
> docker build -f Dockerfile.dev .
....
> docker run 도커 아이디

커넥션 오류가 나올것이다. 아직 DB를 올린게 아니라서 그렇다.

Worker

workder > Dockerfile.dev

FROM node:alpine
WORKDIR "/app"
COPY ./package.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]

Dcoker-Compose

우리가 구성할 건 다음과 같은 형태를 지닌다.

그러면 하나씩 구성해보도록 하자. 마지막에 설정한 환경변수는 각각의 폴더내 keys.js 에 들어갈 내용들이다.

module.exports = {
 redisHost: process.env.REDIS_HOST,
 redisPort: process.env.REDIS_PORT,
 pgUser: process.env.PGUSER,
 pgHost: process.env.PGHOST,
 pgDatabase: process.env.PGDATABASE,
 pgPassword: process.env.PGPASSWORD,
 pgPort: process.env.PGPORT
};

docker-compose.yml

dockerfile들을 한번에 순서대로 만들기 위한 통합파일을 만들어준다. 위치는 root 에서 만든다.

docker-compose.yml

  • Postgres

    version: '3'
    services:
    postgres:
      image: 'postgres:latest'
    > docker-compose up

  • redis

    redis:
      image: 'redis:latest'
    docker-compose up

  • server

    • Specify build

      build:
        dockerfile: Dockerfile.dev
        context: ./server
    • Specify volumes

      volumes:
          - /app/node_modules
          - ./server:/app
    • Specify env variables

      • 환경 설정시 variableName 지정을 안 하는 경우 현재 시스템에 있는 변수로 적용된다.

          # Specify env variables
        environment:
          - REDIS_HOST:redis
          - REDIS_PORT:6379
          - PGUSER:postgres
          - PGHOST:postgres
          - PGDATABASE:postgres
          - PGPASSWORD:postgres_password
          - PGPORT:5432
  • Client

      client:
      build:
        dockerfile: Dockerfile.dev
        context: ./client
      volumes:
        - /app/node_modules
        - ./client:/app  
  • worker

      worker:
      build:
        dockerfile: Dockerfile.dev
        context: ./client    
      volumes:
        - /app/node_modules
        - ./worker:/app

전체 소스는 다음과 같다.

docker-compose.yml

version: '3'
services:
postgres:
  image: 'postgres:latest'
redis:
  image: 'redis:latest'  
server:
  # Specify build  
  build:
    dockerfile: Dockerfile.dev
    context: ./server      
  # Specify volumes
  volumes:
    - /app/node_modules
    - ./server:/app
  # Specify env variables
  environment:
    - REDIS_HOST:redis
    - REDIS_PORT:6379
    - PGUSER:postgres
    - PGHOST:postgres
    - PGDATABASE:postgres
    - PGPASSWORD:postgres_password
    - PGPORT:5432
client:
  build:
    dockerfile: Dockerfile.dev
    context: ./client    
  volumes:
    - /app/node_modules
    - ./client:/app    
worker:
  build:
    dockerfile: Dockerfile.dev
    context: ./client    
  volumes:
    - /app/node_modules
    - ./worker:/app      

nginx

Proxy 설정을 해서 프론트와 백단을 분리를 해보자.

nginx docker 이미지에 기본 설정을 clientserver 를 추가해서 proxy 해주도록 하자.

nginx/default.conf


upstream client {
  server client:3000;
}

upstream server {
  server server:5000;
}

server {
  listen 80;

  location / {
      proxy_pass http://client;
  }

  location /api {
      rewrite /api/(.*) /$1 break;
      proxy_pass http://server;
  }
}
  • rewrite /api/(.*) /$1 break;/api 로 들어오는 경우 내부에서 다시 / 로변경해주기 위함

  • client 는 3000 포트로 내부에서 proxy 되며

  • server 는 5000 포트로 내부에서 proxy 된다.

  • 외부에서는 80 포트만으로 제어한다.

DockerFile.dev 생성

nginx/Dockerfile.dev

FROM nginx
COPY ./default.conf /etc/nginx/conf.d/default.conf
  • nginx 도커 이미지를 가져온다.

  • 기본 설정 default.conf 파일을 도커 이미지내 설정으로 덮어쓴다.

docker-compose 에 nginx 추가

  • nginx 폴더내 Dockerfile.dev 를 참조해서 생성한다.

  • localhost는 3050으로 설정하고 nginx 도커는 80 으로 바인딩 해준다.

  • 문제가 생길 경우 자동으로 재실행을 해준다.

  nginx:
  restart: always
  build:
    dockerfile: Dockerfile.dev
    context: ./nginx  
  ports:
    - '3050:80'  

docker-compose 실행

이제 잘되는지 실행을 해보자. 모든 도커 파일을 다시 만들도록 하자.

docker-compose up --build

서버가 정상적으로 실행된 것 확인하기 위해선 진입점을 확인해야 한다.

이전에 nginx 서버는 3050포트와 바인딩된 상태이다.

    ports:
    - '3050:80'

그럼 이제 확인해본다.

http://localhost:3050

React 서버 실행

정상적으로 React 서버가 뜨는 걸 볼수 있다.

하지만 개발자 도구에 콘솔을 열어보면 웹소켓 문제가 보인다.

실시간 연동이 안될 뿐이지 다시 새로고침을 해보면 정상적으로 저장된 걸 볼수 있다.

  • 5번째 피보나치 수는 8이다.

그러면 nginx 서버에서 웹소켓을 설정해서 실시간 반영이 되도록 해보자.

nginx>default.conf

location /sockjs-node {
  proxy_pass http://client;
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection "Upgrade";
}

Dockerfile 을 새로 빌드해서 up 을 해보자.

정상적으로 보여지는 걸 볼수 있다.

문제점

  1. 만약 submit 을 했는데 실시간으로 변경이 안되는 경우

client/Fib.js 에 다음과 같은 코드를 넣어야 한다.

componentDidMount() {
setInterval(() => {
  this.fetchValues();
  this.fetchIndexes();
}, 1000)
}
  1. docker-compose up 을 할때 오류가 나면 다시 up 을 해주자.

  2. window에서 종종 오류가 나기 때문에 mac이나 리눅스에서 하길 추천한다.

여기까지 소스는 Github 에서 받을 수 있다.

이상으로 5주차 도커&쿠버네이트 수업 정리를 마치도록 한다.

Docker&Kubernetes 4주차 스터디 정리

스터디 내용

  • Github 배포

  • Travis CI 환경설정 추가 및 구성

  • ec2 elastic beantalk 구성 및 자동 배포

개발 flow

준비물

그럼 시작해보자.

Github 설정

  1. 깃헙 레포를 만든다.

  2. 현재 소스를 깃헙 레포와 연결한다.

  3. 깃헙에 푸시한다.

git init
git add .
git commit -m "first commit"
git remote add origin https://github.com/bear2u/docker-react2.git
git push -u origin master

기존에 없는 경우 시작점을 여기 클론해서 진행이 가능하다.

Github에 푸시를 했으면 정상적으로 파일이 다 올라간 걸 확인할 수 있다.

Travis 설정

https://travis-ci.org/ 에 가서 깃헙에 연동해서 로그인을 하자.

설정에 가면 내가 가진 모든repository 가 나올텐데 거기에서 금방 올린 docker-react2 라는 repo를 검색해서 활성화를 해준다.

그럼 docker-react2 를 클릭하면 처음 연동을 하면 비어있는 빌드 목록을 볼수 있을 것이다.

그럼 순서는 어떻게 이루어지는 지 살펴보자.

  1. Travis에게 우리는 동작되는 도커의 카피를 하라고 말한다.

  2. 그리고 Dockerfile.dev를 통해서 이미지를 만들고

  3. Travis에게 테스팅을 돌려서 문제가 없는지 체크

  4. 마지막으로 Travis -> Aws E/B 에게 배포를 말함

이제 소스에서 Travis 환경설정 파일을 만들어 주면 된다.

  • 파일이름 : .travis.yml

#1
sudo: required
#2
services:
- docker
#3
before_install:
- docker build -t bear2u/docker-react -f Dockerfile.dev .
#4
script:
- docker run bear2u/docker-react npm run test -- --coverage
  1. 관리자 권한 필요하다.

  2. 서비스를 어떤 게 필요한지를 미리 정의

  3. 사전 설치

    1. 현재 위치에서 Dockerfile.dev를 이용해서 빌드한다.

  4. docker test을 하는데 커버리지를 지정한다.

그럼 이제 일단 테스팅이 잘 되는지 살펴보자.

git add .
git commit -m 'added travis config'
git push origin master

푸시가 되면 travis 에서 자동적으로 시스템이 시작된다.

정상적으로 테스팅 결과가 나오는 지 확인해본다.

AWS Elastic Beanstalk 설정

AWS Elastic Beanstalk은 도커나 가상 앱을 EC2나 S3를 통해서 바로 올릴수 있도록 도와주는 서비스인것 같다.

이제 aws 에서 Elastic Beanstalk 서비스를 찾아서 어플리케이션을 만들자.

  • Apllication Name: docker-react

  • Sample App

플랫폼을 docker로 선택해야 한다. 그리고 Create environment

완료가 되는 경우 오른쪽 상단에 링크를 클릭시 샘플 앱이 실행되는 걸 확인할 수 있다.

Travis + aws e/b 설정

그럼 travis 설정에서 aws 에 배포하는 내용을 추가하도록 하자.

  • bucket_name 의 경우 S3의 위치를 뜻한다.

#1
sudo: required
#2
services:
- docker
#3
before_install:
- docker build -t bear2u/docker-react -f Dockerfile.dev .
#4
script:
- docker run bear2u/docker-react npm run test -- --coverage

deploy:
provider: elasticbeanstalk
region: "ap-northeast-2"
app: "docker-react"
env: "DockerReact-env"
bucket_name: "elasticbeanstalk-ap-northeast-2-110932418490"
bucket_path: "docker-react"
on:
  branch: master

aws e/b api 키 설정

Travis 에서 aws에 배포할려면 api 가 필요하다.

이 api 서비스를 이용하기 위해선 aws에서iam 이 필요하다.

유저를 추가해준다.

마지막에 까지 진행시 나오는 액세스키와 secret 키가 중요하기 때문에 꼭 백업해놓자.

Travis 에 Access Key 와 Secret Key 설정

Travis에서 docker-react2 클릭 후 설정에 들어가서 키를 2개를 입력해놓으면 된다. 이 입력해놓은 걸 나중에 travis.yml 에서 변수명으로 가져와서 사용할 수 있는 것이다.

travis.yml 파일에 마지막에 api를 추가해준다.

  access_key_id: $AWS_ACCESS_KEY
secret_access_key:
  secure: "$AWS_SECRET_KEY"

그럼 다시 테스트를 해보자.

git add .
git commit -m 'added aws eb'
git push orgin master

그런데 aws e/b 대쉬보드에서 확인해보면 실패가 뜬다.

이유는 포트를 오픈을 안했다.

포트 개방

  • Dockerfile

  • EXPOSE 80

    FROM node:alpine as builder
    WORKDIR '/app'
    COPY package.json .
    RUN npm install
    COPY . .
    RUN npm run build

    FROM nginx
    EXPOSE 80
    COPY --from=builder /app/build /usr/share/nginx/html

aws에서 성공적으로 올라가졌는지 체크해보자.

링크도 클릭해서 잘 나오는지 확인해보자.

빌드 실패가 계속 나오는 경우

EC2 프리티어중 t2.micro 인스턴스가 주로 npm install 사용시 타임 아웃이 걸리는 문제가 발생될 수 있다.

Dockerfile 에 수정을 이렇게 해보자.

COPY package*.json ./

브랜치 변경

처음 계획은 feature 라는 브랜치에 푸시를 하면 마스터에 머지를 하고 테스팅을 돌려서 배포까지 하는 구조를 말했다.

git checkout -b feature

그리고 소스를 수정하고

  • src/App.js

  • Good Choice React in feature

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {
render() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Good Choice React in feature
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}
}

export default App;

새로운 브랜치 Push, PR, Merge

다시 푸시를 한 뒤에 github 에 들어가서 머지를 해보자.

새로운 브랜치를 만들었기 때문에 travis 가 설정하게끔 해줘야 한다.

이제 정상적으로 머지를 하면 된다.

그럼 자동적으로 travis로 신호를 줘서 테스팅 및 배포를 하게 된다.

이제 다시 수정된 화면을 보자.

정상적으로 수정된 내용이 나오는 걸 볼수 있다.

끝나면 삭제를 하면 된다.

오른쪽 위에 메뉴에 삭제를 할수 있다.

여기까지 4주차 도커&쿠버네이트 스터디 정리 내용이다.

도커 & 쿠버네이트 3주차 스터디 정리

이번 시간에는 실제 운영되는 배포까지 실습해보겠다.

소스는 여기에서 확인가능하다.

https://github.com/bear2u/docker3

기본적으로 프로세스는 다음과 같다.

Development -> Testing -> Deployment -> Developm...

  1. 개발자가 소스를 github feature 브랜치에 푸시

  2. 그럼 마스터 브랜치에 PR을 요청하면

  3. 그럼 Travis CI 에서 테스팅을 진행

  4. 테스팅을 진행해서 문제가 없는 경우 마스터 브랜치에 머지

  5. aws에 정상적으로 배포

그럼 실습을 진행해보자. 우선 노드와 React를 이용할 예정이다. 노드 버전을 체크 해보자.

node -v

v8.11.3

만약 설치가 안된 경우 os에 맞는 노드를 설치하자.

그럼 리엑트 프로젝트를 만들어보자.

> npm install -g create-react-app

> create-react-app frontend

그럼 해당 위치에 frontend가 만들어져 있을 것이다.

명령어 정리

  • npm run start : 개발 서버 실행

  • npm run test : 테스트 진행

  • npm run build : production 를 위해 번들링 진행

잘 되는지 보기 위해

npm run start

화면이 잘 나오는지 체크 해보자.

vscode 에디터를 열어서 Dockerfile.dev를 하나 만들자. 이 파일은 개발 전용 도커를 열어서 내부에서 방금 만든 리엑트 서버를 실행하기 위한 테스트 용도 이다.

  • Dockerfile.dev

FROM node:alpine

WORKDIR '/app'

COPY package.json .
RUN npm install

COPY . .

CMD ["npm", "run", "start"]

잠깐 설명을 해보자면

FROM node:alpine
  • alpine 리눅스 os으로 부터 이미지를 가져온다.

WORKDIR '/app'
  • 도커내 os에서 /app 위치를 작업폴더로 지정

COPY package.json .
RUN npm install
  • 로컬 package.json을 복사한다. 그리고 npm install을 한다. 이렇게 하는 이유는 package.json 내용만 변경 될 경우 캐싱을 이용안하고 설치를 하기 위해서다. 즉 package.json 파일이 변경이 안된 경우에는 따로 npm install을 진행 안한다.

COPY . .

CMD ["npm", run", "start"]
  • 로컬 소스폴더를 해당 도커 이미지내 /app 폴더로 복사

  • 마지막으로 npm run start 를 통해 서버 실행

그럼 도커파일을 빌드를 해보자.

> docker build .

unable to prepare context: unable to evaluate symlinks in Dockerfile path: lstat ~/docker/frontend/Dockerfile: no such file or directory

하지만 오류가 나올거로 예상된다. 이유는 빌드파일을 개발용으로 해서 직접 지정을 해서 빌드를 해줘야 한다. 일반적으로 docker build . 를 하는 경우 해당 위치에서 Dockerfile 를 찾는다.

다시 파일이름을 지정해서 빌드해보자.

  • Docerfile.dev 파일을 현재 위치에서 찾는다.

> docker build -f Dockerfile.dev .

Sending build context to Docker daemon 138.9MB
Step 1/6 : FROM node:alpine
---> 7ca2f9cb5536
Step 2/6 : WORKDIR '/app'
---> Using cache
---> 0fbd7e754211
Step 3/6 : COPY package.json .
---> Using cache
---> 0f93f7b66b3b
Step 4/6 : RUN npm install
---> Using cache
---> 5248cc4d46df
Step 5/6 : COPY . .
---> daa3db343355
Step 6/6 : CMD ["npm", run", "start"]
---> Running in c0e8e7a66d50
Removing intermediate container c0e8e7a66d50
---> b87605719148
Successfully built b87605719148

정상적으로 만들어 졌지만 처음 만들어질때 용량이 큰걸 확인할 수가 있다.

Sending build context to Docker daemon  138.9MB

로컬내 node_modules 폴더가 같이 포함된 경우로 보여진다. 이제 삭제하고 다시 실행해보자.

> docker build -f Dockerfile.dev .

Sending build context to Docker daemon 678.9kB
Step 1/6 : FROM node:alpine
---> 7ca2f9cb5536
Step 2/6 : WORKDIR '/app'
---> Using cache
---> 0fbd7e754211
Step 3/6 : COPY package.json .
---> Using cache
---> 0f93f7b66b3b
Step 4/6 : RUN npm install
---> Using cache
---> 5248cc4d46df
Step 5/6 : COPY . .
---> 226600115590
Step 6/6 : CMD ["npm", run", "start"]
---> Running in 948b682ec173
Removing intermediate container 948b682ec173
---> 2211f808c9dc
Successfully built 2211f808c9dc

삭제하고 다시 실행해보면 용량이 줄어든 걸 볼 수 있다.

이제 서버를 실행

  • 방금 빌드 한 이미지 아이디를 이용한다.

> docker run 2211f808c9dc


> frontend@0.1.0 start /app
> react-scripts start

Starting the development server...

정상적으로 서버가 올라가는 걸 볼수 있다. 하지만 로컬에서 접속시 안될것이다. 이유는 포트를 도커쪽과 바인딩을 해줘야 하기 때문이다.

그럼 다시 바인딩을 해보자.

> docker run -p 3000:3000 2211f808c9dc

> frontend@0.1.0 start /app
> react-scripts start

Starting the development server...
....

정상적으로 올라갔다. 브라우저를 통해서 접속시 제대로 올라가는 걸 볼수 있다.

로컬 서버 소스를 변경시??

그럼 만약 로컬 소스를 변경시 자동으로 도커내 이미지 소스와 연동되서 변경이 되는 걸까?

테스트를 위해서 src/app.js를 변경 해보자.

...
Hello World
...

그런데 아직 도커 웹 서버는 변경이 안되는 것 같다. 이유는 도커를 빌드시 해당 소스에 대한 스냅샷을 만들어서 이미지를 올리기 때문이다.

그럼 변경때마다 올려줘서 새로운 이미지로 만들어 줘야 하는 걸까?

윈도우 기반 앱 개발시 변경되는 시기가 늦는 경우가 있다. 이럴 경우 .env 파일을 루트에 만들고 CHOKIDAR_USEPOLLING=true 추가를 하자. 그 이유에 대해서는 링크 를 참고하자.

지금까지 구조에서는 로컬 폴더내 소스들이 그대로 스냅샷을 만들어서 복사해서 사용하는 구조이다. 하지만 로컬 서버내 소스가 그대로 변경사항을 적용하기 위해서는 reference 을 만들어서 소스 폴더들을(app) 매핑하는게 좋아보인다.

소스 폴더를 레퍼런스로 매핑하자

  • 다시 이미지를 빌드

  • docker run with volume

$ docker build -f Dockerfile.dev .

Sending build context to Docker daemon 679.9kB
Step 1/6 : FROM node:alpine
---> 7ca2f9cb5536
Step 2/6 : WORKDIR '/app'
---> Using cache
---> 0fbd7e754211
Step 3/6 : COPY package.json .
---> Using cache
---> 0f93f7b66b3b
Step 4/6 : RUN npm install
---> Using cache
---> 5248cc4d46df
Step 5/6 : COPY . .
---> d88f67529cd4
Step 6/6 : CMD ["npm", "run", "start"]
---> Running in 12cbb9466cce
Removing intermediate container 12cbb9466cce
---> 9df085e7b11b
Successfully built 9df085e7b11b
  • docker run with volume

$ docker run -p 3000:3000 -v $(pwd):/app 9df085e7b11b

> frontend@0.1.0 start /app
> react-scripts start

sh: react-scripts: not found
npm ERR! file sh
npm ERR! code ELIFECYCLE
npm ERR! errno ENOENT
npm ERR! syscall spawn
npm ERR! frontend@0.1.0 start: `react-scripts start`
npm ERR! spawn ENOENT
npm ERR!
npm ERR! Failed at the frontend@0.1.0 start script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm WARN Local package.json exists, but node_modules missing, did you mean to install?

npm ERR! A complete log of this run can be found in:
npm ERR!     /root/.npm/_logs/2018-10-21T20_59_02_912Z-debug.log

에러가 발생했다. 디펜시즈들을 찾을수가 없다고 한다. 이유는 app 폴더를 레퍼런스를 만들었지만 처음에 node_modules 폴더를 삭제를 했었다. 그래서 그 안에 레퍼런스를 찾을수가 없다.

** 즉 dockerfile에서 COPY가 진행될때 기존에 만든 유저 폴더를 복사하기 때문에 node_modules 가 없어지기 때문이다.

node_moduels 레퍼런스를 다시 설정해주자.

$ docker run -p 3000:3000 -v /app/node_modules -v $(pwd):/app 9df085e7b11b

> frontend@0.1.0 start /app
> react-scripts start

Starting the development server...

Compiled successfully!

You can now view frontend in the browser.

Local:           http://localhost:3000/
On Your Network: http://172.17.0.2:3000/

Note that the development build is not optimized.
To create a production build, use yarn build.

정상적으로 실행되는 걸 볼수 있다. 정리하자면

docker run -p 3000:3000 -v /app/node_modules -v $(pwd):/app 9df085e7b11b

을 통해서 /app/node_modules$(pwd):/app 을 통해서 레퍼런스를 만들고 바인딩을 해주고 있다.

pwd 는 현재 내 위치를 뜻한다.

그럼 src/app.js 소스를 다시 수정해보자.

...
Good React
...

정상적으로 리로딩 되서 화면에 나오는 걸 볼수 있다.

docker run ~~ 뭐시기... 아 너무 길어

  • docker run -p 3000:3000 -v /app/node_modules -v $(pwd):/app 9df085e7b11b !!

  • 명령어가 너무 길다. 이걸 편하게 쓸수 있지 않을까?

  • docker-compose.yml 을 이용할 수 있다.

  • docker-compose.yml

version: '3'
services:
web:
  build: .
  ports:
    - "3000:3000"
  volumes:
    - /app/node_modules
    - .:/app

docker-compose up or docker-compose up --build

  • 두 명령어의 차이점은 새로 빌드를 하는 경우 --build를 통해서 올릴수 있다.

$ docker-compose up --build
Building web
ERROR: Cannot locate specified Dockerfile: Dockerfile

그런데 실행해보면 빌드 오류가 나올것이다. 이유는 처음에 빌드시 설명했듯이 Dockerfile을 찾을수가 없기 때문이다. 참고로 현재 폴더에는 Dockerfile.dev로 되어 있다.

다시 해당 도커 파일을 지정해보자.

version: '3'
services:
web:
  build:
    context: .
    dockerfile: Dockerfile.dev
  ports:
    - "3000:3000"
  volumes:
    - /app/node_modules
    - .:/app  

    build:
    context: .
    dockerfile: Dockerfile.dev

해당 폴더내 Dockerfile.dev 를 지정해줄 수 있다.

그럼 다시 up을 해보자.

$ docker-compose up
Recreating frontend_web_1 ... done
Attaching to frontend_web_1
web_1 |
web_1 | > frontend@0.1.0 start /app
web_1 | > react-scripts start
web_1 |
web_1 | Starting the development server...
web_1 |
web_1 | Compiled successfully!
web_1 |
web_1 | You can now view frontend in the browser.
web_1 |
web_1 |   Local:           http://localhost:3000/
web_1 |   On Your Network: http://172.18.0.2:3000/
web_1 |
web_1 | Note that the development build is not optimized.
web_1 | To create a production build, use yarn build.
web_1 |

정상적으로 웹에서 테스팅도 해보면 실행되는 걸 확인할 수 있다. 그리고 /src/app.js 소스도 수정해보자.

...
Good Choice React
...

테스트를 설정해보자.

우선 위에 과정들에서 테스팅을 한다고 했을 경우

  • 우선 docker build를 다시 하자.


docker build -f Dockerfile.dev .
  • docker run test를 진행

$ docker run ac00898b924c npm run test


> frontend@0.1.0 test /app
> react-scripts test

PASS src/App.test.js
✓ renders without crashing (32ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:       2.138s
Ran all test suites.


일단 테스팅이 되긴 하는데...명령어를 더이상 입력못하는 상태가 되었다. <br/> 그럼 -it 옵션을 붙여서 직접 sh로 들어가서 실행해보자.

docker run -it ac00898b924c npm run test

PASS src/App.test.js
✓ renders without crashing (5ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:       0.095s, estimated 1s
Ran all test suites.

Watch Usage
› Press f to run only failed tests.
› Press o to only run tests related to changed files.
› Press p to filter by a filename regex pattern.
› Press t to filter by a test name regex pattern.
› Press q to quit watch mode.
› Press Enter to trigger a test run.

명령어도 직접 입력도 되고 정상적으로 테스팅이 되는 걸 볼수 있다.

하지만 테스팅 소스 수정하면??

로컬에 있는 테스팅 소스를 수정시 정상적으로 업데이트가 안되는 걸 볼수 있다.

테스팅을 2개 한다고 가정해보자. 기존에는 한개였음

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

it('renders without crashing', () => {
 const div = document.createElement('div');
 ReactDOM.render(<App />, div);
 ReactDOM.unmountComponentAtNode(div);
});

it('renders without crashing', () => {
 const div = document.createElement('div');
 ReactDOM.render(<App />, div);
 ReactDOM.unmountComponentAtNode(div);
});

테스팅을 1개에서 2개로 수정한 후 다시 docker 에서 실행해보자.

 PASS  src/App.test.js
  renders without crashing (26ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.643s
Ran all test suites.

Watch Usage
Press f to run only failed tests.
Press o to only run tests related to changed files.
Press p to filter by a filename regex pattern.
Press t to filter by a test name regex pattern.
Press q to quit watch mode.
Press Enter to trigger a test run.

여전히 1개만 테스팅이 되는 걸 볼수 있다.

해결 방안 중 하나는 docker run을 한 상태에서 다른 콘솔에서 다시 테스팅을 진행하는 방법이다.

$ docker compose-up
....
$ docker ps

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                    NAMES
2c673669e1d6        frontend_web        "npm run start"     20 minutes ago      Up 20 minutes       0.0.0.0:3000->3000/tcp   frontend_web_1

$ docker exec -it 2c673669e1d6 npm run test
...

exec 명령어를 통해서 실행되어 있는 서버에 라이브로 붙어서 테스팅을 할 수 있다.

하지만 이건 좋은 프렉티스가 아니다.

docker-compose 에 새로운 명령어를 추가하자.

  • docker-compose.yml

  • tests 콘테이너를 추가

version: '3'
services:
web:
  build:
    context: .
    dockerfile: Dockerfile.dev
  ports:
    - "3000:3000"
  volumes:
    - /app/node_modules
    - .:/app  
tests:
  build:
    context: .
    dockerfile: Dockerfile.dev
  volumes:  
    - /app/node_modules
    - .:/app  
  command: ["npm", "run", "test"]  

그럼 실행해보자

$ docker-compose up --build

Building web
Step 1/6 : FROM node:alpine
 ---> 7ca2f9cb5536
Step 2/6 : WORKDIR '/app'
 ---> Using cache
 ---> 0fbd7e754211
Step 3/6 : COPY package.json .
 ---> Using cache
 ---> 5df741ed1932
Step 4/6 : RUN npm install
 ---> Using cache
 ---> 667dcc1e7cff
Step 5/6 : COPY . .
 ---> 4d785a4cbb04
Step 6/6 : CMD ["npm", "run", "start"]
 ---> Running in c8a14cb7bbf7
Removing intermediate container c8a14cb7bbf7
 ---> 7e86b10b989a
Successfully built 7e86b10b989a
Successfully tagged frontend_web:latest
Building tests
Step 1/6 : FROM node:alpine
 ---> 7ca2f9cb5536
Step 2/6 : WORKDIR '/app'
 ---> Using cache
 ---> 0fbd7e754211
Step 3/6 : COPY package.json .
 ---> Using cache
 ---> 5df741ed1932
Step 4/6 : RUN npm install
 ---> Using cache
 ---> 667dcc1e7cff
Step 5/6 : COPY . .
 ---> Using cache
 ---> 4d785a4cbb04
Step 6/6 : CMD ["npm", "run", "start"]
 ---> Using cache
 ---> 7e86b10b989a
Successfully built 7e86b10b989a
Successfully tagged frontend_tests:latest
Recreating frontend_tests_1 ... done
Recreating frontend_web_1   ... done
Attaching to frontend_tests_1, frontend_web_1
tests_1  | 
tests_1  | > frontend@0.1.0 test /app
tests_1  | > react-scripts test
tests_1  | 
web_1    | 
web_1    | > frontend@0.1.0 start /app
web_1    | > react-scripts start
web_1    | 
web_1    | Starting the development server...
web_1    | 
tests_1  | PASS src/App.test.js
tests_1  |   ✓ renders without crashing (39ms)
tests_1  | 
tests_1  | Test Suites: 1 passed, 1 total
tests_1  | Tests:       1 passed, 1 total
tests_1  | Snapshots:   0 total
tests_1  | Time:        3.597s
tests_1  | Ran all test suites.
tests_1  | 
web_1    | Compiled successfully!
web_1    | 
web_1    | You can now view frontend in the browser.
web_1    | 
web_1    |   Local:            http://localhost:3000/
web_1    |   On Your Network:  http://172.18.0.2:3000/
web_1    | 
web_1    | Note that the development build is not optimized.
web_1    | To create a production build, use yarn build.
web_1    | 

그런 후에 test에 하나의 테스트를 더 추가를 해보자.

  • App.test.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

it('renders without crashing', () => {
 const div = document.createElement('div');
 ReactDOM.render(<App />, div);
 ReactDOM.unmountComponentAtNode(div);
});

it('renders without crashing', () => {
 const div = document.createElement('div');
 ReactDOM.render(<App />, div);
 ReactDOM.unmountComponentAtNode(div);
});

it('renders without crashing', () => {
 const div = document.createElement('div');
 ReactDOM.render(<App />, div);
 ReactDOM.unmountComponentAtNode(div);
});

그럼 도커내에서 자동적으로 테스팅이 추가되서 실행되는 걸 볼수 있다.

tests_1  | PASS src/App.test.js
tests_1  |   renders without crashing (29ms)
tests_1  |   renders without crashing (3ms)
tests_1  |   renders without crashing (3ms)
tests_1  |
tests_1  | Test Suites: 1 passed, 1 total
tests_1  | Tests:       3 passed, 3 total
tests_1  | Snapshots:   0 total
tests_1  | Time:        2.647s, estimated 3s
tests_1  | Ran all test suites.
tests_1  |

컨테이너가 2개가 올라간걸 볼수 있다.

다른 콘솔을 열어서 실행해보자.

$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                    NAMES
0ba2a75bd96a        frontend_web        "npm run start"     5 minutes ago       Up 5 minutes        0.0.0.0:3000->3000/tcp   frontend_web_1
2a40cff30f4f        frontend_tests      "npm run test"      5 minutes ago       Up 5 minutes                                 frontend_tests_1

Nginx 웹서버 추가 등록

  • 이제 프로덕션 하자.

  • Dockerfile 파일내

  1. React 프로젝트를 빌드

  2. npm run build

  3. 최종 나온 빌드 소스를 nginx html 폴더 복사

  4. 그리고 빌드 된걸 run을 해서 서버를 실행하자.

  • Dockerfile 파일 생성

FROM node:alpine as builder
WORKDIR '/app'
COPY package.json .
RUN npm install
COPY . .
RUN npm run build

FROM nginx
COPY --from=builder /app/build /usr/share/nginx/html

$ docker build .


Sending build context to Docker daemon   683kB
Step 1/8 : FROM node:alpine as builder
---> 7ca2f9cb5536
Step 2/8 : WORKDIR '/app'
---> Using cache
---> 0fbd7e754211
Step 3/8 : COPY package.json .
---> Using cache
---> 0f93f7b66b3b
Step 4/8 : RUN npm install
---> Using cache
---> 5248cc4d46df
Step 5/8 : COPY . .
---> 8a445e0ba651
Step 6/8 : RUN npm run build
---> Running in 56d7fc2d10e8

> frontend@0.1.0 build /app
> react-scripts build

Creating an optimized production build...
Compiled successfully.

File sizes after gzip:

32.4 KB build/static/js/1.6105a37c.chunk.js
763 B   build/static/js/runtime~main.229c360f.js
689 B   build/static/js/main.7d28dc35.chunk.js
510 B   build/static/css/main.6e4ad141.chunk.css

The project was built assuming it is hosted at the server root.
You can control this with the homepage field in your package.json.
For example, add this to build it for GitHub Pages:

"homepage" : "http://myname.github.io/myapp",

The build folder is ready to be deployed.
You may serve it with a static server:

yarn global add serve
serve -s build

Find out more about deployment here:

http://bit.ly/CRA-deploy

Removing intermediate container 56d7fc2d10e8
---> a5886e8cd892
Step 7/8 : FROM nginx
---> dbfc48660aeb
Step 8/8 : COPY --from=builder /app/build /usr/share/nginx/html
---> b85d40ae190b
Successfully built b85d40ae190b

정상적으로 빌드 된 걸 볼수 있다.

이제 마지막으로 실행해보자.

docker run -p 8080:80 b85d40ae190b

http://localhost:8080/ 으로 실행하면 nginx 서버로 접속되는 걸 볼수 있다.

이상으로 도커 & 쿠버네이트 3주차 스터디 한 내용이었다.



도커 2주차 수업

복습

http://javaexpert.tistory.com/967?category=719756

docker-compose를 만드는 방법에 대해 진행

1. package.json

{
    "dependencies": {
        "express": "*",
        "redis": "2.8.0"
    },
    "scripts": {
        "start": "node index.js"
    }
}

2. index.js

  • Docker compose를 통해서 내부 네트워크 설정을 진행
  • redis를 연결시 redis-server을 바로 호출 해서 연결함
  • process를 강제로 죽는 오류를 발생시켜서 도커 컨테이너상에서 오류에 대한 여러가지 대처방법을 배움
  • "no",always,on-failure등 여러가지 프로세스 오류에 대처할 수 있음

const express = require("express"); const redis = require('redis'); const process = require('process'); const app = express(); const client = redis.createClient({ host: "redis-server", port: 6379 }); client.set("visits", 0); app.get("/", (req, res) => { process.exit(1); //오류 발생시 자동 재시작되는지 체크 client.get("visits", (err, visits) => { if(err != null) { console.error(err); } res.send("Number of visits is " + visits); client.set('visits', parseInt(visits) + 1); }); }); app.listen(8081, () => { console.log('Listening on port2 8081'); });

3. Dockerfile

  • alpine 노드 이미지 사용
  • /app 에 작업 폴더 만들고 진행
  • package.json을 복사해서 이 부분만 캐싱 되서 변경될 경우 npm install 진행
  • 소스를 복사
  • npm start 명령어 실행
FROM node:alpine

WORKDIR '/app'

COPY package.json .
RUN npm install

COPY . .

CMD ["npm", "start"]

4.docker-compose.yml

version: '3'
services:
  redis-server:
    image: 'redis'
  node-app:
    restart: on-failure
    build: .
    ports:
      - "4001:8081" 

참고사항

  • docker-compose up 을 통해서 올릴수 있음
  • Dockerfile 내용이 변경된 경우 docker-compose up --build를 통해서 다시 빌드해서 올릴 수 있음
  • 내부 소스 파일 내용이 변경된 경우에도 build로 통해서 진행해야함

여기까지 2주차를 진행했으며 다음시간에 여러 컨테이너에 올리는 방법에 대해서 배워보자.

NodeJS 로 도커 시스템 구축

package.json

{
    "dependencies": {
        "express": "*"
    },
    "scripts": {
        "start": "node index.js"
    }
}

index.js

const express = require('express');

const app = express();

app.get('/', (req, res) => {
    res.send('Hi there');
});

app.listen(8080, () => {
    console.log('Listening on port 8080');
});

Dockerfile

# Specify a base image From node:alpine # install some depenendencies RUN npm install # Default command CMD ["npm", "start"]

실행시 node를 찾을수가 없다. 이유는 alpine은 리눅스 기본 이미지이기 때문에 node가 포함되어 있지 않다.
node:alpine을 지정해서 사용할 수 있다.

> docker build .

npm WARN saveError ENOENT: no such file or directory, open '/package.json'
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN enoent ENOENT: no such file or directory, open '/package.json'
npm WARN !invalid#2 No description
npm WARN !invalid#2 No repository field.
npm WARN !invalid#2 No README data
npm WARN !invalid#2 No license field.

node:alpine container 에는 package.json 파일이 포함되어 있지 않다. 그래서 오류가 표시됨

해결방법 : Docker image에 파일을 복사하는 명령어를 붙인다.

# Specify a base image
FROM node:alpine

# install some depenendencies
COPY ./ ./
RUN npm install

# Default command
CMD ["npm", "start"] 

이지지를 commit 오류가 나올텐데 이미지를 생성하는 명령어를 붙여보자.

>> docker build -t gdgbusan/simpleweb .

Sending build context to Docker daemon  66.05kB
Step 1/4 : FROM node:alpine
 ---> 5206c0dd451a
Step 2/4 : COPY ./ ./
 ---> Using cache
 ---> d0fe38353187
Step 3/4 : RUN npm install
 ---> Using cache
 ---> 893984d57822
Step 4/4 : CMD ["npm": "start"]
 ---> Using cache
 ---> e7716218c007
Successfully built e7716218c007
Successfully tagged gdgbusan/simpleweb:latest
>> docker images

....
gdgbusan/simpleweb   latest              e7716218c007        2 minutes ago       72.9MB
....

이제 실행을 해보자.

docker run gdgbusan/simpleweb


> @ start /
> node index.js

Listening on port 8080

잘 되는 걸 확인 할 수 있다.

포트 바인딩 문제

Web에서 호출시 페이지를 찾을수 없는 오류가 나올것이다. 
localhost 에서는 8080으로 호출하지만 docker 에는 8080으로 바인딩이 안되어 있기 때문에 오류가 나는 것이다.

>> docker run -p 8080:8080 gdgbusan/simpleweb

작업 디렉토리 문제

빌드를 하는 경우 기존 파일이나 폴더등이 겹칠수 있는 문제가 발생할 수 있다.

>> docker run -it gdgbusan/simpleweb sh
/ # ls
Dockerfile         bin                etc                index.js           media              node_modules       package-lock.json  proc               run                srv                tmp                var
README.md          dev                home               lib                mnt                opt                package.json       root               sbin               sys                usr

위 내용에서 만약 lib 또는 usr 라는 폴더가 로컬에서 작업 디렉토리로 있는 경우 문제가 될 수 있다.

해결 방안

작업 영역을 따로 지정해줘서 소스를 다 옮겨준다.

.Dockerfile

# Specify a base image
FROM node:alpine

## Define WORKDIR
WORKDIR /user/app

# install some depenendencies
COPY ./ ./
RUN npm install

# Default command
CMD ["npm", "start"] 
> docker run -it gdgbusan/simpleweb sh

/user/app # ls
Dockerfile         README.md          index.js           node_modules       package-lock.json  package.json

/user/app 으로 들어간걸 볼수 있다.

Container ID 를 이용해서 접속 할 수 있다.

>> docker ps
CONTAINER ID        IMAGE                COMMAND             CREATED             STATUS              PORTS                    NAMES
570087c1cc0d        gdgbusan/simpleweb   "npm start"         4 minutes ago       Up 4 minutes        0.0.0.0:8080->8080/tcp   sleepy_archimedes
>> docker exec -it 5700 sh
/user/app # ls
Dockerfile         README.md          index.js           node_modules       package-lock.json  package.json

작업내용 변경 문제

서버쪽에서 변경 사항이 생긴 경우 전체 파일이 변경되서 npm install을 해줘야 하는 경우가 생긴다. 이런 경우를 대비해서 package.json 의 경우에만 캐싱을 따로 처리해주자.

# Specify a base image
FROM node:alpine

WORKDIR /user/app

# install some depenendencies
COPY ./package.json ./
RUN npm install
# Caching
COPY ./ ./

# Default command
CMD ["npm", "start"] 

index.js 에서 변경된 경우 의존성은 캐싱되서 그대로고 소스만 변경해서 만들어준다.

> Dockerfile

PS G:\workspace_docker\simpleweb> docker build -t gdgbusan/simpleweb .
Sending build context to Docker daemon  69.63kB
Step 1/6 : FROM node:alpine
 ---> 5206c0dd451a
Step 2/6 : WORKDIR /user/app
 ---> Using cache
 ---> f86704b2b479
Step 3/6 : COPY ./package.json ./
 ---> Using cache
 ---> a2309c91cf66
Step 4/6 : RUN npm install
 ---> Using cache
 ---> 67444f1d46ea
Step 5/6 : COPY ./ ./ //<-- 캐싱이 안됨
 ---> 9e44a0ef1b60
Step 6/6 : CMD ["npm", "start"]
 ---> Using cache
 ---> cff8acc3464b
Successfully built cff8acc3464b
Successfully tagged gdgbusan/simpleweb:latest


+ Recent posts