반응형

Django 프로젝트를 배포하는 방법은 서비스 규모나 환경에 따라서 여러가지가 있겠지만..

여기서는 단일 서버에 docker를 이용하여 서비스를 운영한다고 가정을 하고 배포하는 상황을 준비하였습니다.

 

 

서비스 구성

기본적으로 source code는 git을 통해서 관리가 되고 있고, 서버에 docker는 이미 설치되어 있다고 가정하겠습니다. 실제 상황에서는 CI/CD 툴을 이용해서 관리하겠지만, 여기서는 Django 프로젝트를 배포하는 것에만 포커스 하도록 하겠습니다.

 

대략적인 서비스 구성은 아래 그림과 같습니다.

 

실제 서비스라면 database도 준비가 되어야겠지만.. 여기서는 docker에서 nginx와 python만 container를 띄울 예정입니다. 여기서 storage는 static 파일들이 들어갈 위치인데요, Django의 경우 운영환경에서는 django app은 wsgi (혹은 asgi) 형태로 서비스를 제공하며, static 파일들은 웹서버가 직접 처리해 주는 형태로 서비스를 하기 때문에, storage를 별도로 지정해 두었습니다.

외부에서 해당 서버에 요청이 오면, 먼저 nginx가 요청을 받아서 처리를 하게 되는데, static 파일 등은 nginx 가 알아서 처리해주고, 그 외에 django app이 처리해야할 부분만 reverse proxy 형태로 전달해 주게 됩니다.

 

 

Django 서비스 배포 절차

Django 서비스를 배포할 때는 다음과 같은 과정들이 필요합니다.

1. python 가상환경 준비 (처음 한번만 필요)

2. 소스 코드 업데이트

3. 의존 모듈 설치

4. static 파일 collect

5. model migration

6. django app service launch

 

다음에는 각각의 작업을 command로 살펴보겠습니다.

 

 

 

Django 서비스 배포 - 디렉터리 설정

위에서 언급하진 않았지만, 여기서는 아래와 같이 디렉터리를 설정해 보았습니다.

 

기본적으로 /opt/docker 폴더에서 각각의 서비스 컨테이너에 대한 폴더를 생성합니다. (nginx, python)

각 서비스 컨테이너 별로 루트에 docker-compose.yaml 파일들을 위치시키게 됩니다.

nginx 에 대해서는 설정파일들은 conf 폴더에 넣을 예정이고, static 파일들은 static 폴더에 넣어 두면 됩니다.

python 도 역시 설정파일들을 conf 폴더에 넣고, 그 외에 필요한 script들은 scripts 폴더에 보관합니다.

그 외에 python 에는 app 폴더를 생성할 예정인데요, app 폴더는 git clone 후에 rename 해서 생성할 예정입니다. 

 

 

Django 서비스 배포 - 가상환경 준비

가상환경은 application 별로 독립된 python 환경을 구성하기 위해서 준비하는 과정인데, docker image를 빌드하는 경우에는 따로 독립된 환경을 사용하지 않아도 되지만, 여기서는 범용 python 이미지를 사용할 예정이기 때문에, local storage에 가상 환경을 생성해서 사용할 예정입니다. command는 다음과 같은데, 가상환경은 python docker container 안에서 실행할 예정입니다.

 

> python -m venv .venv

 

 

Django 서비스 배포 - 소스 코드 업데이트

소스코드 업데이트는 git pull (혹은 clone) 명령어로 처리가 가능합니다. 물론 먼저 서버에 git이 설치가 되어 있어야 합니다.

 

처음 소스를 받아올 때는 clone 명령어를 사용합니다.

 

> git clone <repository url>

 

 

추후에 업데이트 하는 경우에는 pull 명령어를 이용하면 됩니다.

 

> git pull

 

 

Django 서비스 배포 - 의존 모듈 설치

python에서 필요로 하는 모듈을 설치합니다. 로컬 개발환경에서는 간단히 pip 명령어로 처리가 되지만.. 여기서는 가상환경을 이용하기 때문에 아래와 같이 명령어를 입력합니다. 역시 python docker container 안에서 실행되어야 합니다.

 

> ./.venv/bin/python -m pip intstall -r requirements.txt

 

 

Django 서비스 배포 - static 파일 collect

소스 코드 repository 에 넣어둔 static 파일들을 모아주는 과정입니다.여기서는 가상환경을 이용하기 때문에 아래와 같이 명령어를 입력합니다. 역시 python docker container 안에서 실행되어야 합니다.

 

> ./.venv/bin/python manage.py collectstatic --no-input

 

 

Django 서비스 배포 - model migration

모델을 migration 합니다. 실제로는 database 테이블을 생성 또는 변경 하는 과정입니다. 역시 python docker container 안에서 실행되어야 합니다.

 

> ./.venv/bin/python manage.py migrate

 

 

Django 서비스 배포 - django app service launch

django app 서비스를 실행합니다. 앞에서 잠시 언급한데로 wsgi 혹은 asgi 를 이용할 예정이기 때문에, 로컬 개발환경에서와는 조금 다른 명령어를 사용합니다. 역시 python docker container 안에서 실행되어야 합니다. 그리고 host를 지정하지 않으면 127.0.0.1 로 지정이 되는데, 컨테이너 외부에서 접속을 하기 위해서는 0.0.0.0 으로 바인드를 해줘야 합니다. port는 nginx에서 매핑할 예정이기 때문에 크게 중요하지 않아서 default 포트인 8000으로 지정해 주었습니다. 

 

> ./.venv/bin/python -m uvicorn <asgi applcation> --host 0.0.0.0 --port 8000 --workers 4

 

 

참고로 uvicorn을 이용하기 위해서는 python module 을 미리 설치해 주어야 합니다. 만약 requirements.txt에 해당 모듈이 들어있지 않다면 별도로 설치를 해 줍니다. (2024년 2월 현재 0.27.1 버전이 최신이네요..)

 

> ./.venv/bin/python -m pip install uvicorn==0.27.1

 

 

실제 예시

이번에는 위에서 설명한 내용을 실제로 서버에 반영해 보겠습니다. 소스코드는 예전에 제가 생성해 두었던 기본 django tutorial 코드를 이용하도록 하겠습니다. https://github.com/shineum/django_tutorial.git

 

먼저 /opt/docker/python 폴더에서 소스 코드를 clone 합니다.

 

> git clone https://github.com/shineum/django_tutorial

 

 

그리고 django_tutorial 폴더를 app 으로 rename 합니다.

 

> mv django_tutorial app

 

 

django app에서 사용할 script 들을 https://github.com/shineum/django_scripts.git 에서 다운로드 합니다. 그리고 script 들을 scripts 폴더에 위치시킵니다.

 

docker 에서 가상 환경을 생성합니다. python 은 3.12.2-slim-bullseye 버전을 사용하였는데, 버전은 상황에 맞게 설정하면 됩니다.

 

> docker run -v ./app:/opt/app -v ./scripts/init.sh:/entrypoint.sh -it --rm python:3.12.2-slim-bullseye sh ./entrypoint.sh

 

 

소스 코드는 바로 clone 해서 준비가 되어 있으니 여기서는 업데이트가 필요 없지만, 코드가 변경된 경우에는 git pull 명령어를 이용해서 반영해 주면 됩니다. 

 

 

바로 의존 모듈을 설치해 보겠습니다. 가상 환경 생성과 비슷한데, script 만 init 에서 install 로 변경해 주면 됩니다.

 

> docker run -v ./app:/opt/app -v ./scripts/install.sh:/entrypoint.sh -it --rm python:3.12.2-slim-bullseye sh ./entrypoint.sh

 

 

다음 단계인 collect static 을 실행합니다. 이번에는 볼륨매핑 옵션에 nginx의 static 파일 위치를 지정해 주어야 합니다.

> docker run -v ./app:/opt/app -v /opt/docker/nginx/static:/opt/app/static -v ./scripts/collectstatic.sh:/entrypoint.sh -it --rm python:3.12.2-slim-bullseye sh ./entrypoint.sh

 

제대로 실행이 되었다면 /opt/docker/nginx/static 폴더에 static 파일들이 업데이트 되어야 합니다.

 

 

모델 업데이트는 script만 db로 변경해 줍니다. database를 사용하는 경우에는 db에 필요한 테이블이 생성이 되는데, 여기서는 db.sqlite3 파일이 생성됩니다.

 

> docker run -v ./app:/opt/app -v ./scripts/db.sh:/entrypoint.sh -it --rm python:3.12.2-slim-bullseye sh ./entrypoint.sh

 

 

django app이 잘 실행되는지 테스트를 해 봅니다. 아래 커맨드를 실행하고, 해당 서버로 접속을 해 봅니다.

> docker run -v ./app:/opt/app -v ./scripts/run_test.sh:/entrypoint.sh -p 8000:8000 -it --rm python:3.12.2-slim-bullseye sh ./entrypoint.sh

 

 

아래 화면은 Ubuntu desktop 에서 실행했을 때의 결과인데, django 404 error 페이지가 나오지만 실행은 잘 되는 것을 알 수 있었습니다. 실제로는 localhost 대신 서버 주소를 입력해야합니다.

 

 

그리고, admin 페이지는 아래와 같이 나옵니다.

 

 

이제 정식으로 django app 을 실행하기 위해서 docker-compose.yaml 파일을 아래와 같이 작성합니다.

 

/opt/docker/python/docker-compose.yaml

version: "3.7"
services:
  python:
    image: python:3.12.2-slim-bullseye
    volumes:
      - ./app:/opt/app
      - ./scripts/run.sh:/entrypoint.sh
    command: sh entrypoint.sh
    ports:
      - "8000:8000"

 

 

그리고 아래 명령어를 이용하여 컨테이너를 실행합니다.

> docker compose up -d

 

 

django app 이 asgi 형태로 실행되고 있어서, static 파일들 (css, image 등) 이 화면에 표현되지 않습니다.

 

테스트는 잘 실행이 되었지만, docker compose 실행이 제대로 되지 않는다면, 아마도 asgi 파일의 위치 문제일 가능성이 큽니다. run.sh 파일을 편집에서 해당 프로젝트에서 asgi 파일이 있는 경로를 지정해 주면 됩니다.

 

static 파일 문제를 해결하기 위해서 nginx를 셋업하도록 하겠습니다.

 

 

nginx 셋업

nginx도 docker로 이용할 예정이기 때문에 별다른 설치는 필요하지 않습니다. 

nginx에서는 static 파일에 대한 요청을 처리하고, 나머지 요청은 django app으로 보내 줍니다. 실제로는 ssl 처리 등의  복잡한 기능이 필요하지만, 여기서는 아래와 같이 간단한 설정 파일을 준비합니다. 여기서 proxy_pass에 사용한 172.17.0.1 은 docker의 기본 host 주소 입니다. 원래는 docker network 을 이용해야 하지만, 간단한 예시이기 때문에, docker 기본 host 아이피를 적어주었습니다.

 

/opt/docker/nginx/conf/app.conf

server {
    listen 80;
    listen [::]:80;
    server_name localhost;

    location /static {
        alias /opt/app/static;
    }

    location / {
        proxy_pass http://172.17.0.1:8000/;
    }
}

 

 

그리고 docker-compose.yaml 파일을 아래와 같이 작성합니다.

 

/opt/docker/nginx/docker-compose.yaml

version: "3.7"
services:
  python:
    image: nginx:1.24.0
    volumes:
      - ./static:/opt/app/static
      - ./conf/app.conf:/etc/nginx/conf.d/app.conf
    ports:
      - "80:80"
      - "443:443"

 

 

아래 명령어를 이용하여 컨테이너를 실행합니다.

> docker compose up -d

 

 

그리고 나서 browser에서 확인해 보면.. 아래와 같은 에러가 나옵니다. 

 

 

에러 내용은.. 허용되지 않는 host 라는 건데요.. 해결하기 위해서는 django app의 소스코드를 수정해야 합니다.

 

/opt/docker/python/app/config/settings.py 파일을 열어서 28번째 줄을 다음과 같이 변경합니다. 

 

실제 운영환경에서는 이런 식의 소스 코드 변경을 막기위해서 이러한 설정 값은 모두 환경변수에서 읽어서 처리하게 해야 합니다.

 

 

그리고 python (django app) 의 docker 를 새로 띄우면, 아래와 같이 잘 나오게 됩니다.

 

 

눈치 빠르신 분은 이미 알아채셨겠지만.. 접속 주소가 localhost:8000 에서 localhost 로 변경이 되었습니다. nginx 가 연결을 해주기 때문에 이제는 localhost 로 접속하면 됩니다. 기존의 8000 포트는 iptables 혹은 firewall 등을 이용해서 외부에서 접근하는 것을 막아 두는 것이 좋습니다.

 

반응형

'프로그래밍 > Python - Django' 카테고리의 다른 글

Django URL patterns  (0) 2024.03.17
Django Query Tips - User, Group, Permission  (0) 2024.03.02
Django - Proxy Models  (0) 2023.02.23
Django Tutorial Part 1 - HelloWorld  (0) 2022.11.01
Django - 프로젝트 시작하기  (0) 2022.10.28
반응형

피보나치 수는 첫째 및 둘째 항이 1이며, 그 뒤의 모든 항은 바로 앞 두 항의 합인 수열로 정의되어 있습니다.

경우에 따라 0번째 항의 값을 0으로 정의하기도 합니다.

 

피보나치 수를 0번째부터 시작하여 20개를 나열하면 다음과 같습니다.

 

피보나치 수에 대해서 자세한 내용을 알고 싶으신 분은 아래 링크를 참고하시기 바랍니다.

https://en.wikipedia.org/wiki/Fibonacci_sequence#Primes_and_divisibility

 

Fibonacci sequence - Wikipedia

From Wikipedia, the free encyclopedia Numbers obtained by adding the two previous ones A tiling with squares whose side lengths are successive Fibonacci numbers: 1, 1, 2, 3, 5, 8, 13 and 21 In mathematics, the Fibonacci sequence is a sequence in which each

en.wikipedia.org

한글 위키 페이지도 있으나.. 내용이 조금 부실한 편이라 영문 위키 페이지를 링크로 달았습니다.

 

Recursion 을 이용한 풀이

위의 정의대로 Python을 이용해서 구현해 보면, 대략 다음과 같은 코드로 구현이 가능합니다.

def fibo(n):
    if n < 1:
        return 0
    elif n < 3:
        return 1
    return fibo(n-1) + fibo(n-2)

fibo(19)를 출력해보면, 4181이 나오는 것을 확인할 수 있습니다.

 

 

Memoization 을 이용한 성능 개선

Recursion 을 이용하여 구현한 코드는 중복된 계산이 많아서 퍼포먼스에 문제가 있습니다. 

 

fibo(5) 를 구하기 위해서 fibo(4) + fibo(3) 이 호출되고,

fibo(4) 를 구하기 위해서 다시 fibo(3) + fibo(2) 가 호출되고..

 

같은 계산을 여러 번 중복하는 것을 기록해 두어 성능을 개선해 보겠습니다. 아래 코드는 memoization 을 이용하여 성능을 개선한 코드입니다.

mCache = {}
mCache[1] = 1
mCache[2] = 1

def fibo_memo(n):
    if n in mCache:
        return mCache[n]
    elif n < 1:
        return 0
    ret = fibo_memo(n-1) + fibo_memo(n-2)
    mCache[n] = ret
    return ret

 

한 번 계산을 했던 값은 mCache에 값을 기록해 두어, 중복 계산을 피해서 퍼포먼스를 향상시켰습니다.

 

 

Dynamic Programming 을 이용한 풀이

이번에는 테이블을 생성해서 값을 채워가는 방식으로 피보나치 값을 계산해 보겠습니다.

def fibo_dp(n): 
    if n < 1:
        return 0
    
    buf = [0] * (n+1)
    buf[1] = 1
    for i in range(2, n+1):
        buf[i] = buf[i-1] + buf[i-2]
    return buf[n]

 

피보나치 수열의 정의대로 순차적으로 계산해서 마지막 값을 돌려줍니다. 

 

 

Dynamic Programming 개선

위에서 살펴본 Dynamic Programming에서는 테이블을 이용하였기 때문에, n+1 만큼의 메모리를 할당해서 값을 구하였습니다. 하지만 실제로는 이전의 계산 결과는 더 이상 쓰이지 않기 때문에, 이전값, 현재값을 가지고 있는 것으로 충분합니다. 위의 코드는 아래와 같이 개선이 가능합니다.

def fibo_dp2(n): 
    if n < 1:
        return 0
    
    prev = 0
    current = 1
    for i in range(1, n):
        prev, current = current, prev + current
    return current

 

 

Matrix를 이용한 풀이

마지막으로 matrix를 이용하여 피보나치 값을 구해 보도록 하겠습니다.

피보나치 수열은 행렬을 이용하면 다음과 같이 정의가 가능합니다.

위의 행렬을 두 개 붙여서 확장하면, 다음과 같은 식을 얻을 수 있습니다.

F0 ~ F2에 값을 대입해서, 식을 정리해 보면 다음과 같습니다. 

즉, Fn 을 구하기 위해서는 아래 행렬의 (0,0) 값을 계산하면 됩니다.

행렬식의 계산은 다음과 같이 처리합니다.

1) 지수가 짝수인 경우,

2) 지수가 홀수인 경우,

 

위의 내용을 코드로 구현하면 다음과 같습니다.

def calc_matrix(m1, m2):
    e00 = m1[0][0] * m2[0][0] + m1[0][1] * m2[1][0]
    e01 = m1[0][0] * m2[0][1] + m1[0][1] * m2[1][1]
    e10 = m1[1][0] * m2[0][0] + m1[1][1] * m2[1][0]
    e11 = m1[1][0] * m2[0][1] + m1[1][1] * m2[1][1]
    return [[e00, e01], [e10, e11]]

mBase = [[1, 1], [1, 0]]    
def power(m, n):
    if n <= 1:
        return mBase
    ret = power(m, n//2)
    ret = calc_matrix(ret, ret)
    if n % 2 == 1:
        ret = calc_matrix(ret, mBase)
    return ret

def fibo_mat(n):
    if n < 1:
        return 0
    m = power(mBase, n-1)
    return m[0][0]

지수 형태로 구현하였기 때문에, 시간 복잡도는 O(logN) 이 됩니다.

 

 

퍼포먼스 비교

각 방법에 대해서 F1000 을 구했을 때 걸리는 시간을 측정한 결과입니다.

recursion 의 경우, 시스템에 따라 다를 수 있지만 일정 갯수 이상이 되면, maximum recursion depth exceeded 에러가 발생합니다. 

 

recursion 을 제외한 나머지에 대해서, F10000을 구했을 때 걸리는 시간을 측정한 결과 입니다.

 

반응형
반응형

파티션 문제는 list 의 원소들을 합이 같은 두 개의 부분 집합으로 나눌 수 있지를 확인하는 문제입니다.

 

 

Example

예를 들어 아래와 같은 리스트가 있다면,

[1, 5, 11, 5]

 

리스트를 아래와 같이 나누게 되면, 두 리스트의 합이 11로 같기 때문에, 결과는 True가 됩니다.

[1, 5, 5], [11]

 

 

Recursion 을 이용한 풀이

Recursion 을 이용할 때는, subset에 대한 합계를 체크하는 method를 정의해서 subset 합계가 전체 원소의 합의 절반이 되면 True를 리턴하게 작성합니다.

 

def isSubsetSum(arr, n, target):
	# Termination Cases
	if target == 0:
		return True
	if n == 0 and target != 0:
		return False
    # exclude last element or include last element
	return isSubsetSum(arr, n-1, target) or isSubsetSum(arr, n-1, target-arr[n-1])


def findPartition(arr):
	total = sum(arr)
	if total % 2 != 0:
		return False
	return isSubsetSum(arr, len(arr), total // 2)
    

assert(findPartition([1,2,3,4,5]) == False)
assert(findPartition([1,5,11,5]) == True)
assert(findPartition([1,2,3,4,4,3,2,1]) == True)

 

이 경우, 시간 복잡도는 O(2^N) 이 되며, 공간 복잡도는 O(N) 이 됩니다.

 

 

Recursion 을 이용한 풀이 2 - memoization

Recursion 을 이용할 때는, subset sum 을 확인하는 과정에서 반복되는 계산이 나오기 때문에, 이를 기록해 두고 사용하면 반복 계산을 크게 줄일 수 있습니다. memoization 기법을 사용하여 개선한 코드는 다음과 같습니다.

 

def isSubsetSum(arr, n, target, dp):
	# Termination Cases
	if target == 0:
		return True
	if n == 0 and target != 0:
		return False
	
    # send result from memo
	if dp[n][target] != -1:
	    return dp[n][target]

    # save result
	dp[n][target] = isSubsetSum(arr, n-1, target, dp) or isSubsetSum(arr, n-1, target-arr[n-1], dp)
    
    # return result
	return dp[n][target]


def findPartition(arr):
	total = sum(arr)
	if total % 2 != 0:
		return False

	n = len(arr)
    # init memoization result
	dp = [[-1]*(total+1) for i in range(n+1)]
	return isSubsetSum(arr, n, total // 2, dp)


assert(findPartition([1,2,3,4,5]) == False)
assert(findPartition([1,5,11,5]) == True)
assert(findPartition([1,2,3,4,4,3,2,1]) == True)

 

여기서는 테스트한 list의 아이템이 많지 않아서, Recursion 보다도 느린 결과가 나올 수 있지만, 원소가 증가할 수록 큰 성능 향상을 확인할 수 있을 것입니다.

 

Dynamic Programming 을 이용한 풀이

이번에는 합계 / 원소 테이블을 생성해서, 테이블의 값을 채워가는 방식으로 문제를 해결해 보도록 하겠습니다. 이 방식의 아이디어는 다음과 같습니다.

 

1. row에는 부분집합의 합계를, column에는 리스트의 원소를 하나씩 추가할 수 있게 테이블을 준비합니다.

[1, 3, 7, 3] 의 케이스를 예를 들면, 

전체 원소의 합은 14 (1 + 3 + 7 + 3)  이기 때문에, 원소들의 합이 같게 나눴을 때, 부분 집합의 원소의 합은 7이 되어야 합니다. (전체 원소 합의 절반) 그러면, 테이블은 아래와 같이 준비 됩니다.

  [] [1] [1, 3] [1, 3, 7] [1, 3, 7, 3]
0          
1          
2          
3          
4          
5          
6          
7          

 

2. 부분 집합의 합계가 0이라는 것은, 부분 집합이 공집합이면 언제나 가능하기 때문에 어떤 부분 집합의 부분 집합에 대해서도 합계 0을 만들 수가 있습니다. 그래서 첫 번째 줄은 모두 가능하기 때문에 True로 채웁니다.

  [] [1] [1, 3] [1, 3, 7] [1, 3, 7, 3]
0 True True True True True
1          
2          
3          
4          
5          
6          
7          

 

3. 반대로, 원소가 없는 부분집합에 대해서는 원소의 합계는 항상 0이기 때문에 합계가 0보다 큰 경우는 만드는 것이 불가능합니다. 따라서 첫 번째 컬럼은 False로 채웁니다.

  [] [1] [1, 3] [1, 3, 7] [1, 3, 7, 3]
0 True True True True True
1 False        
2 False        
3 False        
4 False        
5 False        
6 False        
7 False        

 

4. 이제부터는 주어진 합계에 대해서, 원소를 채워가면서, 주어진 합계를 부분 집합의 합으로 만들 수 있는지를 체크합니다. 체크하는 방법은 마지막에 추가한 원소를 제외하고, 주어진 합계에서 해당 원소의 값을 뺀 경우에 대해서 부분 집합의 합을 만들 수 있는지를 체크하면 됩니다.

 

4-1. 먼저 부분 집합 [1] 의 부분 집합으로 합계 1을 만들 수 있는 지를 체크해 보겠습니다. 

마지막에 추가한 원소는 "1" 이기 때문에, 해당 원소를 제거하고, 합계에서도 1을 뺍니다.

그러면, 부분 집합 []의 부분 집합으로 합계 0을 만들 수 있는 지를 체크해서, 그게 가능하면 부분 집합 [1]의 부분집합으로 합계 1을 만들 수 있는 지 알 수 있습니다. 우리는 이미 부분 집합 [] / 합계 0에 대해서 True로 채워 두었기 때문에 가능 하다는 것을 알 수 있습니다. 

 

  [] [1] [1, 3] [1, 3, 7] [1, 3, 7, 3]
0 True True True True True
1 False True      
2 False        
3 False        
4 False        
5 False        
6 False        
7 False        

 

4-2. 이번에는 부분 집합 [1, 3]의 부분 집합으로 합계 1을 만들 수 있는 지를 체크해 보겠습니다.

우리는 이미 부분 집합 [1]의 부분 집합으로 합계 1을 만들 수 있다는 것을 알고 있습니다. 그렇다면 [1]은 [1, 3]의 부분 집합이기 때문에, [1, 3]의 부분집합으로도 부분 집합의 합계 1을 만들 수 있습니다.

 

  [] [1] [1, 3] [1, 3, 7] [1, 3, 7, 3]
0 True True True True True
1 False True True    
2 False        
3 False        
4 False        
5 False        
6 False        
7 False        

 

5. 4번의 방법을 이용해서 테이블을 모두 채우면 아래와 같이 됩니다.

  [] [1] [1, 3] [1, 3, 7] [1, 3, 7, 3]
0 True True True True True
1 False True True True True
2 False False False False False
3 False False True True True
4 False False True True True
5 False False False False False
6 False False False False True
7 False False False True True

 

6. 원래 우리가 찾던 답이 [1, 3, 7, 3]의 부분 집합 중에 합이 7인 것이 있는 지를 찾는 것이 었기 때문에, 위의 테이블의 가장 오른쪽 아래의 값을 읽어서 돌려주면 됩니다.

 

설명은 길었지만, 코드는 기존의 recursion 보다 짧습니다.

def findPartition(arr):
	total = sum(arr)
	if total % 2 != 0:
		return False
	
	half_sum = total // 2
	n = len(arr)
	
	# table setting
	dp = [[True]*(n+1)] + ([[False]*(n+1)] * half_sum)

    # fill the table
	for i in range(1, half_sum+1):
	    for j in range(1, n+1):
	        if dp[i][j-1]:
	            dp[i][j] = True
	            continue
	        val = arr[j-1]
	        if i >= j and dp[i-val][j-1]:
	            dp[i][j] = True
	            
    # return result
	return dp[half_sum][n]


assert(findPartition([1,2,3,4,5]) == False)
assert(findPartition([1,5,11,5]) == True)
assert(findPartition([1,2,3,4,4,3,2,1]) == True)

 

 

Dynamic Programming 공간 최적화

마지막으로 바로 전에 사용한 Dynamic Programming을 최적화 해보도록 하겠습니다. 위의 테이블을 잘 살펴보면, 실제로는 전체 테이블이 없어도 한 컬럼만 가지고 정보를 업데이트하면서 사용이 가능합니다. 다만 이전 단계의 결과를 체크하기 위해서는 top-down 방식이 아닌 bottom-up 방식으로 리스트를 채워가야 합니다.

 

1. 먼저 전체 리스트 원소의 합계의 절반 크기의 리스트를 생성합니다. 인덱스는 부분 집합의 합계를 나타내기 때문에, 0은 True로 세팅하고 나머지는 False로 세팅합니다.

  0 1 2 3 4 5 6 7
Initial True False False False False False False False

 

2. 이번에는 원소를 하나씩 추가해가며, 합계를 제일 큰 수부터 순차적으로 체크해서 리스트를 업데이트 해줍니다.

체크는 다음과 같이 할 수 있습니다.

2-1. 해당 합계를 이미 만들 수 있다면 continue (dp[j] = True 인 경우)

2-2. 해당 합계에서 추가한 원소의 값을 뺀 값을 부분 집합의 합으로 만들 수 있는지를 체크해서 True인 경우, 해당 합계를 True로 업데이트 (dp[j - val] = True 인 경우)

 

ex)

첫번째 원소 1 추가

합계 7에서부터 역순으로 합계 0까지 체크

 

합계 7의 경우 현재 False 이기 때문에,  dp[7-1] 에 있는 값을 체크

    => 현재 dp[6] = False 이기 때문에 업데이트를 하지 않고 진행

 

...

 

합계 1의 경우 현재 False 이기 때문에, dp[1-1]에 있는 값을 체크

    => 현재 dp[0] = True 이기 때문에 dp[1] = True로 업데이트

 

합계 0의 경우 이미 True 이기 때문에 continue

 

  0 1 2 3 4 5 6 7
[1] True True False False False False False False

 

3. 순차적으로 원소를 추가하며 테이블을 업데이트 해주면.. 아래와 같이 업데이트 됩니다.

  0 1 2 3 4 5 6 7
[1, 3] True True False True True False False False

 

  0 1 2 3 4 5 6 7
[1, 3, 7] True True False True True False False True

 

  0 1 2 3 4 5 6 7
[1, 3, 7, 3] True True False True True False True True

 

 

4. 리스트의 마지막 값인 dp[7] 을 읽어서 돌려 줍니다.

 

 

코드로 구현하면 다음과 같습니다.

def findPartition(arr):
	total = sum(arr)
	if total % 2 != 0:
		return False
	
	half_sum = total // 2
	n = len(arr)
	
	# init list
	dp = dp = [True] + [False] * half_sum
	
	# update list
	for i in range(n):
	    val = arr[i]
	    for j in range(half_sum, val-1, -1):
            if dp[j]:
                continue
	        if dp[j-val] == True:
	            dp[j] = True

    # return result
	return dp[half_sum]


assert(findPartition([1,2,3,4,5]) == False)
assert(findPartition([1,5,11,5]) == True)
assert(findPartition([1,2,3,4,4,3,2,1]) == True)

 

실제로, 위의 코드에서 dp[half_sum] = True 가 되는 순간, 더 이상 for 문을 수행할 필요 없이 True를 돌려줘도 됩니다.

여기서는 이전 단계의 합계를 참조하기 위해서 bottom-up (가장 큰 합계를 먼저 체크) 하는 방식을 사용하였습니다. 

 

DP 에서는 어떤 방향으로 테이블 혹은 리스트를 채우냐에 따라 공간 복잡도를 더 작게 구현할 수 있습니다.

반응형
반응형

이번 포스트에서는 PGP Key가 어떤 것인지 알아보고, PGP키를 생성 / 삭제 및 PGP 키를 이용해서 데이터를 encrypt / decrypt 하는 방법 등에 대해서 소개하겠습니다.

 

 

PGP 키

먼저, PGP는 Pretty Good Privacy 의 약자입니다. Phil Zimmermann 에 의해서 개발되었습니다. 초창기 PGP는 보안에 민감한 데이터를 취급할 때, 데이터를 받는 쪽에서만 decrypt 할 수 있게 하기 위해서 사용되었습니다.

 

PGP key를 이용해서 데이터를 받는 과정입니다.

1. 데이터를 받는 쪽에서 먼저 PGP 키를 생성합니다.

2. 데이터를 보내주는 쪽에 생성한 public key를 전달해 줍니다.

3. 데이터를 보내는 쪽에서는 전달 받은 public key를 이용해서 데이터를 encrypt 합니다.

4. 데이터를 전송합니다.

5. 데이터를 받는 쪽에서는 private key를 이용해서 데이터를 decrypt 합니다.

 

PGP 키는 발급시 public key 와 private key 가 생성되는데, public key는 encrypt 시에 사용되며, private key는 decrypt 시에 사용이 됩니다. 이런 비대칭성 (asymmetry) 때문에, public key는 공개 되어도 상관이 없습니다.

 

 

PGP vs GPG

PGP 키를 발급하는 과정에서, 검색을 하다보면 PGP 대신 GPG 가 검색이 되는 경우가 많습니다. GPG는 GNU Privacy Guard 의 약자로 Open Source 버전의 PGP 라고 생각하면 됩니다. Ubuntu 에서 기본으로 제공이 되기 때문에, 이 포스트에서는 GPG 커맨드를 이용해서 키를 관리해 보도록 하겠습니다.

 

 

PGP 키 생성

먼저, PGP키를 생성해 봅니다.

 

아래 커맨드를 이용하면 키를 생성할 수 있습니다.

> gpg --full-generate-key

 

참고로, 옵션 중에 --generate-key, --quick-generate-key 를 사용해도 키를 생성할 수 있으나, 알고리즘, 키 사이즈 혹은 유효기간  등을 원하는 방식으로 설정하기 위해서는 --full-generate-key 옵션을 사용해야 합니다.

 

커맨드를 실행하면, 몇 가지 물어보게 됩니다. 위에서 잠시 이야기 했던, 알고리즘, 키 사이즈, 유효기간을 설정하면 됩니다.

 

위의 필수 정보를 입력하고 나면, PGP 키 사용자에 대한 정보를 입력해야 합니다. (Real name, Email address, Comment)

이 중에 Real name은 아이디라고 생각하면 되는데, local에서 사용할 경우, Real name 만 입력해도 문제 없습니다. public 사이트에 공개해야 하는 경우에는 Email address도 입력해서 unique 한 인증 정보를 입력해야 합니다.

저는 Real name을 testpgp 로 설정하였습니다. 마지막에 O를 입력해서 Okay를 하면, 키가 생성이 됩니다.

 

 

PGP 키 확인

PGP 키가 생성이 되면, 생성된 키들은 key storage에 위치하게 됩니다.

키를 확인하기 위해서는 아래 커맨드를 사용합니다.

 

 Pubilc key 확인 (소문자 k)

> gpg -k

 

Private key 확인 (대문자 K)

> gpg -K

 

실제로 입력을 해보면, 아래와 같이 생성된 키 정보를 확인할 수 있습니다.

 

 

 

데이터 encrypt, decrypt

이번에는 public key를 이용해서 데이터를 encrypt 해보고, private key를 이용해서 encrypt 된 데이터를 decrypt 해보도록 하겠습니다.

 

테스트를 위해서 간단한 텍스트 파일을 하나 생성하였습니다.

 

아래 커맨드를 이용해서 test.txt 파일을 encrypt 합니다.

> gpg -o test.gpg -e -r testpgp test.txt

 

커맨드에서 사용한 옵션은 다음과 같습니다.

-o: output filename, 여기서는 encrypt 되서 출력될 파일을 "test.gpg" 로 지정하였습니다.

-e: encrypt, encrypt 하라는 의미입니다.

-r: recipent, 수신자로 public key 이름을 적으면 됩니다.

마지막으로 test.txt는 encrypt 할 입력 파일 이름입니다.

 

커맨드 실행 후에는 아래와 같이 test.gpg 파일이 생성되었습니다.

 

이번에는 decrypt 하는 커맨드 입니다.

> gpg -o test_decrypted.txt -d test.gpg

 

커맨드에서 사용한 옵션은 다음과 같습니다.

-o: output filename, 여기서는 decrypt 되서 출력될 파일을 "test_decrypted.txt" 로 지정하였습니다.

-d: decrypt, decrypt하라는 의미입니다.

 마지막으로 test.gpg 는 encrypt 된 입력 파일 이름입니다.

 

커맨드 실행 후에 생성된 test_decrypted.txt 파일을 열어보면, test.txt 파일과 같은 내용이 있음을 확인할 수 있습니다.

 

 

PGP 키 내보내기

이번에는 키를 export 해보겠습니다. public 키와 private 키 각각 export 옵션은 다음과 같습니다.

 

 Pubilc key export

> gpg --export -a -r testpgp -o testpgp_pub.asc

 

Private key export (password 입력 필요)

> gpg --export-secret-key -a -r testpgp -o testpgp_pri.asc

 

위의 커맨드를 실행해보면, public key는 "testpgp_pub.asc" 파일로, private key는 "testpgp_pri.asc" 파일로 생성이 됩니다.

 

 

PGP 키 삭제하기

이번에는 키박스에 저장된 키들을 삭제해 보겠습니다. public key와 private key가 같이 있는 경우에는 private key 부터 삭제 합니다.

 

 Private key 삭제

> gpg --delete-secert-key testpgp

 

Public key 삭제

> gpg --delete-key testpgp

 

커맨드 실행 후, 확인 명령어로 키를 체크해 보면, 삭제된 것을 알 수 있습니다.

 

 

PGP 키 가져오기

마지막으로 export 해 뒀던 키를 키박스로 가져오는 커맨드 입니다.

 

 Pubilc key import

> gpg --import testpgp_pub.asc

 

Private key import (password 입력 필요)

> gpg --import testpgp_pri.asc

 

위의 명령어를 실행하면 아래와 같이 출력되면서, 키박스에 키들이 저장됩니다.

 

그런데, 가져오기 후에 PGP 키를 확인해 보면, 아래와 같이 trust 상태가 unknown으로 나오게 됩니다.

 

trust 상태를 바꿔주기 위해서 --edit-key 옵션을 사용합니다.

> gpg --edit-key testpgp

 

명령어를 입력하면 아래와 같은 프롬프트 화면이 나오게 됩니다.

 

프롬프트에 "trust" 를 입력하고, trust 상태를 지정합니다. (여기선 5)

y를 입력해서 변경사항을 확인해 주면 저장이 완료됩니다.

마지막으로 quit 를 입력해서 프롬프트를 종료합니다.


pgp 키 정보를 확인해 보면 trust 상태가 "ultimate" 으로 변경된 것을 확인할 수 있습니다.

 

반응형
반응형

Django의 Proxy 모델은 하나의 데이터 모델을 가지고, 다른 인터페이스를 제공하는 클래스라고 생각하면 됩니다.

모델 상속의 경우, 각 subclass 에 대해서 새로운 데이터 테이블이 생성되지만, proxy 모델은 별도로 데이터 테이블을 생성하지 않고, 기존 테이블을 참조해서 사용하게 됩니다. 

 

Proxy 모델의 정의

Proxy 모델은 원본 모델 클래스를 상속받아, Meta 클래스에 proxy 값을 True 로 설정해 주면 됩니다.

 

from django.db import models

class Person(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)

class MyPerson(Person):
    class Meta:
        proxy = True

 

 

Proxy 모델 활용 - method 재정의

아래 예시에서는 원본 데이터 클래스인 Person 에서는, 인스턴스 출력시, DB에 저장되어 있는 이름을 그대로 출력되고, Proxy 클래스인 MyPerson 에서는, 인스턴스 출력시, 이름이 대문자로 변경되어서 출력됩니다. 같은 데이터를 참조하면서, method 를 다르게 정의해서 사용할 수 있습니다. 

 

from django.db import models

class Person(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)

    def __str__(self):
        return f"{self.firat_name} {self.last_name}"

class MyPerson(Person):
    class Meta:
        proxy = True
 
    def __str__(self):
       return super().__str__().upper()

 

Proxy 모델 활용 - manager: get_queryset

이번에는 proxy 모델과 manager를 이용해서, 원본 데이터를 필터링해서 관련 데이터만 사용하는 예시입니다.

먼저 SomeClass 라고 하는 데이터 모델을 정의하고, 세 개의 필드 status, title, content 를 정의합니다. 이어서 정의할 manager 클래스에서는 SomeClass의 status 값에 따라 원본 데이터를 필터해서 돌려 줄 수 있게 처리합니다. 그리고 마지막으로 proxy 모델들은 원본 데이터 클래스 (SomeClass) 를 상속하고, objects 에 해당 manager 클래스의 인스턴스를 지정해 줍니다. 

 

from django.db import models

STATUS_TYPES = (
    ('n', 'New'),
    ('i', 'In Process'),
    ('c', 'Completed'),
)

# 원본 데이터 클래스
class SomeClass(models.Model):
    status = models.CharField(max_length=1, choices=STATUS_TYPES)
    title = models.CharField(max_length=100)
    content = models.TextField(blank=True, null=True)

# 매니저 클래스
class NewStatusManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(status='n')

# 원본 데이터 테이블에서 status 가 'n' 인 데이터만 가지고 오는 proxy 클래스 
class NewStatusSomeClass(SomeClass):
    objects = NewStatusManager()
    class Meta:
    	proxy = True

# 매니저 클래스
class InProcessStatusManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(status='i')

# 원본 데이터 테이블에서 status 가 'i' 인 데이터만 가지고 오는 proxy 클래스
class InProcessStatusSomeClass(SomeClass):
    objects = InProcessStatusManager()
    class Meta:
    	proxy = True

# 매니저 클래스
class CompletedStatusManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(status='c')

# 원본 데이터 테이블에서 status 가 'c' 인 데이터만 가지고 오는 proxy 클래스
class CompletedStatusSomeClass(SomeClass):
    objects = CompletedStatusManager()
    class Meta:
    	proxy = True

 

 

 

 

Proxy 모델 활용 - manager: create

이번에는 proxy 클래스로 신규 레코드를 생성하는 케이스에 대해서 정의해 보겠습니다.

Proxy 모델에 연결된 manager 클래스에 create method 를 재정의해서, Proxy 형태를 구분하는 컬럼의 값을 지정해 줄 수 있습니다.

 

class SampleManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(type='sample')
        
    def create(self, **kwargs):
        kwargs.update({'type': 'sample'})
        return super().create(**kwargs)
        
...

params = {}
SampleManager.objects.create(**params)

 

 

반응형
반응형

이번 포스트는 앱 배포에 관한 것으로, docker 환경에 구축된 jenkins를 이용해서, git repository 에 저장된 소스를 가지고 와서 docker 이미지를 빌드하고, private docker registry 에 배포하는 과정에 대해서 정리해 보았습니다.

 

 

서버 환경

먼저 서버를 준비해야 하는데, 어차피 docker로 배포가 될 예정이기 때문에, 테스트 환경은 Virtual Box 에 설치한 Ubuntu 22.04 LTS 상에서 docker를 설치해서 진행했습니다.

 

 

Docker 설치

대부분 application은 docker 상에서 구동될 예정이기 때문에 먼저 docker를 설치합니다.

 

설치과정은 다음 링크에 자세히 나와 있습니다.

Install Docker Engine on Ubuntu | Docker Documentation

 

1. 기존에 설치된 버전을 삭제합니다.

sudo apt-get remove docker docker-engine docker.io containerd runc

 

2. apt 패키지 인덱스를 업데이트 하고 필요한 패키지를 설치합니다.

sudo apt-get update
sudo apt-get install ca-certificates curl gnupg lsb-release

 

3. Docker의 GPG Key를 추가합니다.

sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

 

4. Docker 설치를 위한 apt repository를 세팅합니다.

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

 

5. apt 패키지를 다시 업데이트 하고, docker를 설치합니다.

sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin

 

Docker Registry

docker registry는 docker 이미지를 빌드한 후에 배포하기 위해서 사용합니다. docker.io 등에서 제공하는 public registry를 사용하는 경우에는 이 과정을 생략해도 좋습니다.

 

docker-compose.yml 파일을 생성해서 다음과 같이 작성합니다.

version: '3'

services:
  registry:
    container_name: docker_registry
    image: registry:2
    restart: unless-stopped
    ports:
    - "5000:5000"
    environment:
      REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
    volumes:
      - ./data:/data

 

docker-compose 명령어 혹은 docker 명령어로 registry 서버를 시작합니다.

 

docker-compose 가 설치되어 있는 경우

docker-compose up -d

 

docker 명령어를 사용하는 경우

docker compose up -d

 

docker registry 의 경우 https를 사용하는 것이 기본이지만.. 테스트 환경 혹은 private 네트워크 환경에서는 굳이 https를 사용할 필요가 없어서 http를 사용할 경우에는, 아래와 같이 설정을 해 줍니다.

 

1. /etc/docker/daemon.json 파일 오픈 (없으면 생성)

2. 아래와 같은 내용을 추가합니다.

{"insecure-registries": ["host:port"]}

3. docker 서비스를 재시작 합니다.

sudo systemctl restart docker

 

혹시 기본 옵션 파일을 인식하지 못한다면, /etc/default/docker 파일에 DOCKER_OPTS="--config-file=/etc/docker/daemon.json" 을 추가하고, docker 서비스를 재시작 합니다.

 

참고 링크

https://stackoverflow.com/questions/49674004/docker-repository-server-gave-http-response-to-https-client

 

 

Jenkins 시작

Jenkins도 별도로 설치하지 않고, docker 컨테이너 형태로 사용을 할 예정인데요, 이런 식으로 사용하는 경우 docker 관련 플러그인을 사용할 때, docker 명령어를 찾지 못하거나 glibc 버전 문제가 발생할 수 있어서, jenkins docker 이미지에  docker를 추가로 설치해서 사용하도록 하겠습니다.

 

먼저 아래와 같이 Dockerfile 파일을 준비합니다. 

FROM jenkins/jenkins:lts
USER root
RUN apt-get update -qq \
    && apt-get install -qqy apt-transport-https ca-certificates curl gnupg2 software-properties-common
RUN curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add -
RUN add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/debian \
   $(lsb_release -cs) \
   stable"
RUN apt-get update  -qq \
    && apt-get -y install docker-ce
RUN usermod -aG docker jenkins

 

아래 명령어를 이용해서 이미지를 빌드합니다.

docker image build -t custom-jenkins-docker .

 

Jenkins를 띄울 때도, docker-compose.yml 을 작성해서 띄우도록 하겠습니다.

docker-compose.yml 파일을 다음과 같이 작성합니다.

version: '3'

services:
  jenkins:
    container_name: jenkins
    image: custom-jenkins-docker:latest
    restart: unless-stopped
    privileged: true
    user: root
    ports:
     - "8080:8080"
     - "50000:50000"
    volumes:
     - ./data:/var/jenkins_home
     - /var/run/docker.sock:/var/run/docker.sock

 

docker-compose 명령어 혹은 docker 명령어로 jenkins 서버를 시작합니다.

 

docker-compose 가 설치되어 있는 경우

docker-compose up -d

 

docker 명령어를 사용하는 경우

docker compose up -d

 

 

Jenkins 설정

웹 브라우저를 이용해서 http://localhost:8080 으로 접속합니다. 참고로 저는 virtual box의 host 네트워크 설정으로 host PC에서 IP (192.168.56.101) 로 접근할 수 있게 설정하였습니다.

 

처음 실행시 아래와 같은 화면이 나옵니다.

 

화면에 나온 설명대로 docker 컨테이너 내에서 /var/jenkins_home/secrets/initialAdminPassword 파일을 열어보면 초기 비밀번호를 확인할 수 있습니다. 해당 파일은 docker-compose 로 띄울 때, .data 폴더를 /var/jenkins_home 폴더로 매핑시켜 두었기 때문에,  Ubuntu 서버 내에서 .data/secrets/initialAdminPassword 파일을 열어보아도 동일한 결과를 얻을 수 있습니다.

그 외에는 docker 컨테이너 log를 이용해서 아래와 같이 초기 비밀번호를 확인할 수도 있습니다.

docker logs jenkins

 

다음 화면에서는 Install suggested plugins를 클릭해서 기본 플러그인들을 설치합니다.

 

다음 화면에서는 Admin User를 생성해도 되지만 귀찮으시면 Skip 을 눌러서 넘길 수도 있습니다. (Skip시 initial password를 계속 사용하게 됩니다.)

 

Jenkins URL을 세팅하고, 시작합니다. 

 

 

Jenkins 화면에서 Jenkins 관리 > 플러그인 관리 > Available plugins 를 차례로 클릭합니다.

 

검색창에서 docker를 입력합니다.

 

Docker, Docker Commons, Docker Pipeline, Docker API, docker-build-step 를 체크하고, Download now and install after restart 를 클릭합니다.

 

 

설치 후 Jenkins 재시작을 체크하면, Jenkins가 재시작 됩니다.

 

마지막으로 docker socket을 세팅해 줍니다.

Jenkins 관리 > 시스템 설정을 차례로 클릭합니다.

스크롤해서 Docker Builder 항목을 찾아가서 Docker server REST API URL에 아래와 같이 "unix:///var/run/docker.sock" 을 입력합니다.

 

 

빌드 테스트

이제 Jenkins 에 Pipeline 아이템을 추가해서 빌드 테스트를 진행 해보겠습니다.

 

Jenkins Dashboard에서 새로운 Item을 클릭합니다.

 

적당한 이름을 입력하고, Pipeline 을 선택하고 OK 버튼을 눌러서 넘어갑니다.

 

먼저, Docker 이미지에 버전을 태깅하기 위해서 BUILD_NUMBER를 인자로 받도록 합니다.

 

General 섹션에서 "이 빌드는 매개변수가 있습니다" 를 체크하고, String Parameter를 선택하고, 매개변수 명에 "BUILD_NUMBER" 라고 적습니다.

 

쭉 아래로 내려서 Pipeline 섹션에 빌드 스크립트를 작성합니다.

아래와 비슷하게 작성하면 됩니다.

 

 

스크립트 내용은 아래와 같습니다.

node {
    def app
    def imgName = "customapps/django_test"
    stage('Clone Repository') {
        git 'https://github.com/shineum/django_sample.git'
    }
    stage('Build Image') {
        app = docker.build("${imgName}:${env.BUILD_NUMBER}")
    }
    stage('Push Image') {
        docker.withRegistry('http://192.168.56.101:5000') {
            app.push("${env.BUILD_NUMBER}")
            app.push("latest")
        }
    }
    stage('Clean Up') {
        sh "docker images ${imgName} -q | xargs docker rmi -f > /dev/null 2>&1 || true"
    }    
}

 

스크립트 내용은 대략 다음과 같습니다.

 

1. Clone Repository

github에서 소스 코드를 가지고 옵니다.

 

2. Build Image

docker 이미지를 빌드합니다. Jenkins 서버가 아니라 docker를 설치한 Ubuntu 서버에 이미지가 생성됩니다.

 

3. Push Image

해당 docker 이미지를 private registry 서버에 push 합니다. (registry 서버는 virtual box 에서 host 전용 네트워크를 이용해서 ip를 설정하였습니다.)

처음에는 빌드 버전대로 하나 올리고, 새 빌드를 latest 로 세팅합니다.

 

4. Clean Up

Ubuntu 서버에는 앱 이미지가 필요 없기 때문에 불필요한 이미지들을 정리합니다. 이 때, 앱을 빌드할 때 사용되는 base 이미지들을 같이 삭제하게 되면, 빌드 시간이 길어지기 때문에, 앱 배포 이미지만 삭제합니다. 

 

스크립트 작성이 끝났으면 저장하고 빠져나옵니다.

 

 

파라미터와 함께 빌드를 클릭하고, BUILD_NUMBER를 입력합니다.

 

이미지 빌드는 base image를 다운로드해야 하며, 앱에서 필요로 하는 모듈도 다운로드해야 하기 때문에 시간이 조금 걸립니다.

 

빌드가 완료되면 아래와 비슷한 결과가 출력됩니다.

 

확인을 위해 registry 서버에 접속해보면,

 

먼저 카탈로그 확인

http://192.168.56.101:5000/v2/_catalog

 

그리고, 아래와 같이 빌드 관련 정보를 확인할 수 있습니다.

http://192.168.56.101:5000/v2/customapps/django_test/tags/list

다음 명령으로 릴리즈 된 docker 이미지를 registry 서버에서 pull 할 수 있습니다. (latest 대신 버전을 적어줘도 됩니다.)

docker pull 192.168.56.101:5000/customapps/django_test:latest

 

 

빌드 실행

마지막으로 조금 전에 빌드한 이미지를 앱 서버에 배포해서 실행해 보겠습니다.

실제 운영 환경에서는 kubernetes 나 docker swarm 등을 사용하기 때문에 조금 더 복잡하겠지만.. 여기서는 단순하게 Ubuntu 서버에서 docker 이미지를 실행해 보겠습니다.

 

Jenkins Dashboard에서 새로운 Item을 클릭합니다. 이번에도 적당한 이름을 지정하고 Pipeline을 선택합니다.

 

General 섹션에서 "이 빌드는 매개변수가 있습니다" 를 체크하고, String Parameter를 선택하고, 매개변수 명에 "BUILD_NUMBER" 라고 적습니다. 그리고 이번에는 Default Value 에 "latest" 를 적어 줍니다.

 

쭉 아래로 내려서 Pipeline 섹션에 빌드 스크립트를 작성합니다.

아래와 비슷하게 작성하면 됩니다.

 

 

스크립트 내용은 아래와 같습니다.

node {
    def containerName = "cn_django_test"
    def imgName = "customapps/django_test"
    def regServer = "192.168.56.101:5000"
    stage("Stop App") {
        sh "docker stop ${containerName} || true && docker rm ${containerName} || true"
    }
    stage("Clean Up") {
        sh "docker images ${imgName} -q | xargs docker rmi -f > /dev/null 2>&1 || true"
    }
    stage("Start App") {
        sh "docker run -it -d -p 8000:8000 --name ${containerName} ${regServer}/${imgName}:${env.BUILD_NUMBER}"
    }
}

 

스크립트 내용은 대략 다음과 같습니다.

 

1. Stop App

기존에 실행 중인 앱을 정지 합니다.

 

2. Clean Up

기존에 실행 할 때 사용했던 app build 이미지를 제거 합니다.

 

3. Start App

지정한 버전의 app build 이미지를 registry로부터 pull하고 app을 구동합니다.

 

 

스크립트 작성이 끝났으면 저장하고 빠져나옵니다.

 

 

파라미터와 함께 빌드를 클릭하고, BUILD_NUMBER는 "latest"로 두고 실행합니다.

 

빌드가 잘 완료되었는지 확인하고,

 

App URL로 접속해보면, django 앱이 실행되고 있음을 확인할 수 있습니다.

 

 

이상으로 포스트를 마치도록 하겠습니다. 

반응형
반응형

노트북 등의 컴퓨터에서 윈도우11 (Win11 22H2) 재설치시, (유선) 네트워크 연결이 되어 있지 않고, 무선랜 드라이버가 기본으로 설치 되지 않는 경우 (주로 새로 나온 무선랜 장비에서 발생), 네트워크에 연결 화면에서 넘어가지 않는 이슈가 있습니다.

 

화면에 연결 가능한 무선랜 리스트도 나오지 않으며, 다음 버튼이 활성화 되지 않아서 아무것도 할 수 가 없는 상황이 벌어집니다.

 

 

 

이 경우, 다음과 같이 조치 하면 됩니다.

 

1. Shift + F10 을 눌러서 명령 프롬프트 화면을 띄웁니다. 입력은 한 번만 하세요.. 여러 번 누르면 창이 여러 개 한꺼번에 나옵니다. 아래는 두 번 눌러서 창이 두 개가 나왔는데, 하나만 남기고 닫으면 됩니다.

 

 

 

2. OOBE\BYPASSNRO  라고 입력하고 Enter를 입력합니다. (자동으로 재부팅 됨)

 

3. 재부팅이 되면.. 아쉽게도 처음부터 다시 입력해야 하지만, 이번에는 네트워크 설정을 건너뛸 수 있는 액션 링크가 나옵니다. 인터넷에 연결되어 있지 않음 클릭!

 

 

4. 링크를 클릭해서 넘어가서 윈도우 설치를 마칩니다. 다음 화면에서도 네트워크 연결을 권유하는 페이지가 나오지만, 제한된 설치로 계속을 클릭해서 넘깁니다.

 

 

5. 설치 후에는 해당 노트북 (컴퓨터) 의 무선랜 드라이버를 수동으로 설치해 줍니다. (다른 컴퓨터에서 다운로드해서 USB 등의 미디어를 이용해서 복사해 줘야 합니다.)

 

ex) Lenovo 14인치 Slim 7 Pro X (intel) 드라이버

https://pcsupport.lenovo.com/us/en/products/laptops-and-netbooks/yoga-series/yoga-slim-7-prox-14iah7/downloads/driver-list/component?name=Networking%3A%20Wireless%20LAN

 

참고 링크

https://pureinfotech.com/bypass-internet-connection-install-windows-11/

반응형
반응형

언제부턴가 윈도우즈에서 Powershell 을 제공하고 있다는 것을 알고는 있었으나.. 쓸일이 별로 없을 거라 생각해서 눈여겨 보고 있지 않았었는데.. 요즘 윈도우즈로 작업을 많이 하다보니 의외로 유용한 기능이 많아서 몇가지 주요 커맨드를 정리해 봅니다.

 

PowerShell 에 대한 자세한 설명은 Microsoft 사이트에서 제공해 주고 있습니다. 한글 번역도 제공해 주고 있는데, 일부는 기계 번역이 되어 있다고 하네요.. 7.2 LTS 버전에 대한 링크 입니다.

 

영문 링크     한글 링크 

 

 

PowerShell 이란?

PowerShell은 윈도우즈 뿐만 아니라 리눅스, 맥OS 등의 OS에서 사용이 가능한 크로스 플랫폼 지원의 작업 자동화 솔루션입니다. 

 

 

PowerShell 설치

Windows 에서는 Windows 10 이후에는 기본으로 설치되어 있는 것 같고, 그 외에는 winget 등을 이용해서 설치하면 된다고 합니다. (홈페이지에는 모던 Windows 10 이라고 되어 있어서 정확한 버전은 모르겠습니다.) 링크 참조 

 

리눅스는 Ubuntu 기준으로 아래 명령어들을 이용하면 설치가 가능하다고 합니다.  링크 참조 

# Update the list of packages
sudo apt-get update
# Install pre-requisite packages.
sudo apt-get install -y wget apt-transport-https software-properties-common
# Download the Microsoft repository GPG keys
wget -q "https://packages.microsoft.com/config/ubuntu/$(lsb_release -rs)/packages-microsoft-prod.deb"
# Register the Microsoft repository GPG keys
sudo dpkg -i packages-microsoft-prod.deb
# Update the list of packages after we added packages.microsoft.com
sudo apt-get update
# Install PowerShell
sudo apt-get install -y powershell
# Start PowerShell
pwsh

 

맥OS 에서는 홈브루 설치 이후에 cask 를 이용해서 설치하면 됩니다. 링크 참조 

brew install --cask powershell

 

 

주요 명령어

주요 명령어는 제가 많이 사용하는 것들로 정리해 봤습니다.

PowerShell Linux Others 기능 설명
Get-Item ls dir 파일 / 폴더 등의 리스트를 화면에 출력해 줍니다.
참고로 ls 명령어도 사용이 가능한데, 옵션들이 제대로 동작하지 않는 것으로 보아 실제로는 Get-Item이 호출되는 것으로 보입니다.
Get-Content cat   파일의 내용을 보여줍니다. 
참고로 cat 명령어도 사용이 가능한데, cat은 cmdlet Get-Content 라고 명시적으로 나와서 Get-Content 가 호출된다는 것을 알 수 있습니다.
ssh ssh ssh 언제부턴가 ssh 명령어가 되네요.. Secure Shell 을 의미하며, 원격 시스템에 접속해서 명령을 실행하기 위해서 사용합니다.
scp scp scp scp 는 원격 시스템으로 파일을 전송할 때 사용합니다.
Invoke-WebRequest wget, curl   웹 프로토콜 (http, https 등) 을 이용해서 서버의 파일을 가져옵니다.
참고로 wget 이나 curl 을 그냥 써도 Invoke-WebRequest 이 호출됩니다.
Get-Process ps tasklist 프로세스 리스트를 보여줍니다. ps 명령어를 입력해도 Get-Process가 호출되는 것 같습니다. 
Stop-Process kill taskkill 프로세스를 종료합니다. kill 명령어를 입력해도 Stop-Process가 호출 되는 것 같습니다.

 

이 외에도 많은 명령어들이 있으니 필요한 명령어는 아래 링크를 참조하세요.

 

영문 링크     한글 링크 

 

 

활용법

1. 프로세스 검색

먼저 프로세스를 검색해 보겠습니다. 메모장(notepad)를 하나 열고 아래 명령어를 입력합니다. 

Get-Process -Name Notepad

 

아래와 비슷한 결과가 출력될 것입니다.

 

이름으로 프로세스를 검색하는 경우에는, -Name은 생략이 가능합니다.

 

 

만약 프로세스 아이디를 알고 있다면 아래처럼 입력하면 됩니다. (여기서는 아이디가 4556)

Get-Process -Id 4556

 

정확한 프로세스 이름을 모른다면 와일드카드(*)를 이용할 수 있습니다.

Get-Process -Name Note*

 

위의 출력결과에서 Id만 화면에 출력하고 싶다면, 아래와 같이 입력합니다.

Get-Process -Name Notepad | Select-Object Id

여기서 | (pipe) 는 바로 앞의 커맨드의 결과를 받아서 처리하라는 의미이고,  Select-Object는 프로세스 리스트에서 Id 만 선택해서 보여주라는 의미 입니다.

 

결과는 아래처럼 출력됩니다.

 

결과를 리스트로 받기 위해서는 아래와 같이 ExpandProperty 옵션을 추가합니다.

Get-Process -Name Notepad | Select-Object -ExpandProperty Id

 

 

파이프 기능을 활용하면 이전 결과를 필터 할 수도 있습니다. 아래 명령어는 Where-Object 기능을 이용해서 전체 프로세스 리스트 중에 ProcessName이 Notepad 인 것만 필터해서 출력해 줍니다. 즉, 앞에서 입력했던 Get-Process Notepad 와 같은 결과가 나옵니다. 여기서 $_ 는 이전 결과 리스트의 아이템(원소)를 의미합니다.

Get-Process | Where-Object {$_.ProcessName -eq 'Notepad'}

 

 

2. 프로세스 종료

이번에는 프로세스를 강제로 종료하는 명령어입니다. 이전에 실행했던 메모장을 종료하기 위해서는 아래와 같이 입력합니다.

Stop-Process -Name Notepad -Force

Stop-Process의 경우 Get-Process 처럼 와일드 카드로 프로세스를 종료하는 것은 가능하지만, -Name은 생략할 수 없습니다. 참고로 뒤에 붙인 -Force는 다른 사용자가 실행한 프로세스라도 강제로 종료시키라는 의미 입니다.

 

또한, 프로세스 아이디로도 종료가 가능합니다.

Stop-Process -Id 4556 -Force

 

 

3. 리스트

다음은 리스트를 처리하는 명령어들입니다.

 

프로세스 리스트를 ProcessName으로 정렬해서 10개만 보여주는 명령어 입니다.

Get-Process | Sort-Object -Property ProcessName | Select-Object -Skip 0 -First 10

오름차순으로 정렬이 되는데, 내림차순으로 정렬하고 싶다면 -Descending 옵션을 추가하면 됩니다.

 

아래처럼 입력하면 ProcessName을 역순으로 정렬해서, index 0과 index 1에 해당하는 아이템을 화면에 출력해 줍니다.

Get-Process | Sort-Object -Property ProcessName -Descending | Select-Object -Index 0, 1

 

Powershell에서는 아래처럼 입력하면 리스트로 인식합니다.

1,2,3,4,5,6,7,8,9

 

그래서 파이프를 걸어서 5보다 큰 값을 출력하게 할 수 있습니다.

1,2,3,4,5,6,7,8,9 | Where-Object {$_ -gt 5}

 

순차적인 숫자의 리스트는 다음과 같이 생성할 수도 있습니다.

1..9

 

아래 명령어는 리스트에서 중복되지 않는 값만 출력해 줍니다.

1,2,3,1,2,3,1,2,3,4 | Select-Object -Unique

 

아래 명령어는 1 ~ 9 까지의 리스트를 생성해서 마지막 값을 출력해 줍니다.

1..9 | Select-Object -Last 1

 

 

4. ForEach-Object

리스트를 핸들하는 예시입니다.

 

먼저 map 의 예시입니다. map은 리스트를 받아서 리스트를 돌려줍니다. 아래 예시는 [1, 2, 3, 4, 5, 6, 7, 8, 9] 리스트를 생성해서 각 원소에 2를 곱한 결과를 다시 리스트로 반환합니다. 결과는 [2, 4, 6, 8, 10, 12, 14, 16, 18] 이 됩니다.

1..9 | ForEach-Object -Process {$_ * 2}

위의 예시에서 -Process 는 생략가능하며, ForEach-Object 대신 %{$_ * 2} 을 사용해도 같은 결과가 리턴됩니다.

 

이번에는 reduce 예시입니다. reduce는 리스트를 받아서 연산한 결과값을 돌려줍니다. 아래 예시는 [1, 2, 3, 4, 5, 6, 7, 8, 9] 리스트를 생성해서 원소들의 합을 구해서 돌려줍니다. 결과는 45 가 됩니다.

1..9 | ForEach-Object -Begin {$sum = 0} -Process {$sum += $_} -End {$sum}

ForEach-Object 대신 ForEach 라고 써도 되는 것 같습니다. 그리고 Process는 여러 개를 지정하여 순차적으로 수행하게 할 수 있습니다.

1..2 | ForEach-Object -Begin $null -Process {'Process A'}, {'Process B'} -End $null

 

Begin, End 스크립트 등에는 세미콜론(;)을 이용해서 여러 변수를 지정할 수 있습니다. Process에서도, 여러 개의 스크립트를 지정하지 않고, 세미콜론(;)으로 구분된 여러 문장을 실행하는 것도 가능합니다.

1..9 | ForEach-Object -Begin {$a = 0; $b = 1} -Process {$a += $_; $b *= $_} -End {$a; $b}

End는 {$a, $b} 로 표기해도 같은 결과가 나오며, 리스트 형태로 반환됩니다.

 

 

ForEach는 아래와 같이 사용도 가능합니다. (이렇게 호출하면 내부적으로 ForEach-Object 를 호출하지 않는 것 같습니다.)

ForEach ($_ in 1..9) {$_}

결과는 리스트 형태로 [1, 2, 3, 4, 5, 6, 7, 8, 9] 를 반환합니다.

 

파이프와 같이 사용할 때는 아래처럼 사용이 가능합니다.

1..9 | %{$_ * 2}

 

 

5. CSV

다음은 CSV 파일의 내용을 볼 수 있는 명령어 입니다.

 

첨부된 샘플 csv를 이용해서 테스트 했습니다.

 

sample.csv
0.00MB

 

Import-CSV sample.csv

 

아래와 같은 결과가 나올 것입니다.

 

만약 여기서 Last name 만 출력하고 싶다면, 아래처럼 입력하면 됩니다.

Import-CSV sample.csv | Select-Object "Last name"

 

여기서 Identifier가 5000이하인 사람만 출력하고 싶으면, 아래처럼 입력하면 됩니다.

Import-CSV sample.csv | Where-Object {$_.Identifier -le 5000}

 

아래처럼 필터링 되어서 필요한 결과만 나오게 됩니다.

 

 

이 포스트에서 소개한 명령어 이외에도 많은 기능들이 있으니 매뉴얼을 참고하시기 바랍니다.

 

 

ps. 참고로 Windows PowerShell은 대소문자 구분을 하지 않습니다. 다른 OS에서는 확인해 보지 않았지만.. OS 특성상 다른 OS에서는 대소문자 구분을 할 것 같네요..

 

반응형

+ Recent posts