Skip to main content

CSS3 레이아웃

· 6 min read

CSS3

사용할 수 없는 브라우저에서 신규 문법은 무시된다.

BFC

Block Formatting Context이며 아래 조건에서 생성된다.

  • 루트 요소
  • float: right, left
  • position: absolute
  • display: inline-block
  • overflow 값이 visable 외에 다른 값일 때
  • flex item
  • grid item
  • table cell

layout

float

<style>
.box {
float: left;
}
/* 다음 세 가지 방법으로 container 안에 box 를 넣어줄 수 있다. */
.container {
overflow: hidden;
}
.container {
float: left;
}
.container {
display: flow-root;
}
</style>
<div class="container">
<div class="box">
<p>플로팅</p>
</div>
</div>
  • overflow: hidden 으로 플로팅 요소를 잡는 것은 box-shadow가 잘리는 등의 문제가 있다.
  • display: flow-root는 모던 브라우저에서만 지원한다.

position

  • static: 기본 값이다. 코드상 노출된 순서대로 표시된다.
  • relative: 오프셋(top, left...)와 함께 사용된다.
    • 새로운 컨테이너 블록이 되며 하위 absolute 를 가둘 수 있다 .
  • absolute: 흐름에서 벗어나며 자신이 포함된 컨테이너 블록의 가장자리를 기준으로 오프셋만큼 이동한다.
    • 별도의 컨테이너 블록이 선언되지 않았을 경우, viewport가 된다.
  • fixed: viewport를 기준으로 고정된다. 스크롤해도 변하지 않는다.
  • sticky: static + fixed 로 문서와 함께 스크롤 되다가 설정한 위치가 되면 고정된다.
    • 모던 브라우저에서만 지원한다.
<style>
.container {
width: 400px;
height: 400px;
}
/* 이 박스는 맨 위(viewport)에서 10 10 씩 떨어져있다. */
.box {
position: absolute;
top: 10px;
right: 100px;
width: 200px;
}

/* 컨테이너 블록으로 만들면 박스가 들어온다. */
/* .container { position: relative; width: 400px; height: 400px; } */
</style>
<div class="container">
<div class="box">
<p>absolute</p>
</div>
</div>

multi-column

column-count, column-width로 단 효과를 낼 수 있다.

<style>
.columes {
column-width: 200px;
column-count: 2;
}
</style>
<div class="columns">
<p>첫 번째 단</p>
<p>두 번째 단</p>
</div>

axis

  • 주축교차축이 있다.
    • 기본은 flex-direction: row이며 교차축은 수직이다.
    • flex-direction: column이면 교차축은 수평이다.
  • 아이템의 배치는 항상 교차축(cross axis)에서 이뤄진다.
  • 교차축 === 블럭축(block axis) 이다.
  • 그리드에서는 컬럼 축(column axis)이라고도 한다.

flexbox

1차원 레이아웃

  • display: flex 설정시 자식은 flex item이 된다.
  • flex item은 min-content로 설정한 너비보다 작아질 수 없으므로 컨테이너를 벗어난다.
    • min-content는 아이템 내부 단어 중 가장 긴 것을 기준으로 설정된다.
  • flex-wrap: wrap; 속성 설정 시 여러 줄에 걸쳐 표현된다.
  • 줄이 넘어가면 넘어간 줄이 flex container 가 된다.

플렉스 아이템 배치

  • align-items
    • stretch: 기본값으로 늘어난다.
    • flex-start: 요소가 컨테이너 상단에 붙는다.
    • flex-end: 바닥에 붙는다.
    • center: 중앙에 배치된다.
  • align-self: flex item 에서 위 속성을 덮는다.

플렉스 아이템 정렬

  • justify-content
    • flex-direction: row면 가로줄, column이면 세로줄에서 동작한다.
    • flex-start: 기본값
    • flex-end: 플렉스 컨테이너 끝에서부터 추가된다.
    • space-between: 아이템 사이의 공간을 똑같은 간격으로 설정한다.
    • space-around 모든 아이템 양쪽에 똑같은 간격의 마진을 설정한다.
    • space-evenly 모든 공백을 똑같이 설정한다.
      • 아이템-아이템 간 컨테이너-아이템 간의 간격이 똑같다.
    • center: 가운데 설정한다.
  • align-content
    • 교차축 위에서 동작한다.
    • flex-wrap: wrap이고 아이템 배치 공간보다 컨테이너가 길 때 사용할 수 있다.
    • 초기값은 start이다.
    • 나머지 동작은 justify-content와 같다.
  • margin-left: auto; 를 사용하면 원하는 아이템을 반대방향에 배치할 수 있다.

반응형 플렉스박스

  • flex-grow: flex-basis에 설정한 값보다 커질 수 있는지 설정한다.
  • flex-shrink: flex-basis에 설정한 값보다 작아질 수 있는지 설정한다.
    • 500px 컨테이너에 200px 플렉스 아이템이 3개 있다면 활성화시 영역 안에 들어올 것이다.
  • flex-basis: flex-direction 에 따라 너비나 높이의 기본값을 지정한다.
    • flex-basis: content: 주축의 컨텐츠 크기로 설정된다.
    • flex-basis: auto: 플렉스 아이템에 width 속성이 있다면 그 값을 flex-basis 로 사용한다. 특별한 경우가 아니라면 auto가 권장된다.
  • 보통 세 속성을 합쳐서 flex: 0 0 200px; 처럼 적는다.

플렉스박스 방향

플렉스박스와 그리드는 dir=ltr dir=rtl 속성에 좌,우 영향을 받는다.

  • flex-direction: row-reverse
  • flex-direction: column-reverse

grid

2차원 레이아웃

<style>
.container {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
/* grid-column-gap, grid-row-gap 축약 */
grid-gap: 20px;
/* 가장 최근 명세에서는 `grid-` prefix 가 빠졌다 */
gap: 20px;
}
</style>
<div class="container">
<div class="item">
<h2>grid 1</h2>
</div>
<div class="item">
<h2>grid 2</h2>
</div>
<div class="item">
<h2>grid 3</h2>
</div>
<div class="item">
<h2>grid 4</h2>
</div>
<div class="item">
<h2>grid 5</h2>
</div>
<div class="item">
<h2>grid 6</h2>
</div>
</div>

fr

  • fraction
  • 유연한 너비를 나타내는 단위이다.

그리드 트랙

  • 그리드의 열과 행을 나타낸다.

그리드 배치

프로그래머의 수는 0부터 시작이지만 그리드 배치에서는 1부터여야한다.

  • LTR 의 경우는 왼쪽 끝이 1 이다.
  • RTL 의 경우는 오른쪽 끝이 1 이다.
  • 반대편 끝은 -1 이다.
.container {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-gap: 20px;
}

.item1 {
grid-column: 1 / 3;
grid-row: 1;
}
.item2 {
grid-column: 3;
grid-row: 1;
}
.item3 {
grid-column: 1;
grid-row: 2 / 4;
}
.item4 {
grid-column: 2 / 4;
grid-row: 2;
}
.item5 {
grid-column: 2 / 4;
grid-row: 3;
}

grid-column: auto / span 2; 처럼 시작 위치를 auto로 잡고 끝 위치를 span 2로 잡으면 자동 배치 기능에 의해 그리드 아이템 위치는 자동으로 정해지고 너비는 항상 컬럼 두 개만큼 확장한다.

그리드 정렬

  • justify-items로 설정하며 각 영역 안에서 정렬된다.
  • 초기값은 stretch이다.

named area

  • 그리드에 이름을 직접 지정할 수도 있다.
  • .은 공백을 나타낸다.
  • 영역은 반드시 사각형이여야한다.
  • align-items, justify-items 속성 변경시 반복이 무시된다. (stretch가 아니므로)
.container {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-gap: 20px;
grid-template-areas:
"a a b"
". d d"
"c e e";
}

.item1 {
grid-area: a;
}
.item2 {
grid-area: b;
}
.item3 {
grid-area: c;
}
.item4 {
grid-area: d;
}
.item5 {
grid-area: e;
}

반응형 그리드

  • auto-fill: 너비가 허용하는만큼 최대한 많이, 다만 아이템 갯수가 부족하면 빈 공간을 남긴다.
  • auto-fit: 아이템 갯수가 부족하면 남은 공간은 균등하게 분배된다.
  • minmax: 너비의 최소, 최대크기를 지정할 수 있다.
    • 아래 예시라면 200px 의 컬럼이 몇 개 들어가는지 계산한 뒤, 남은 공간을 컬럼에 균등하게 분배된다.
.container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
/* 컨텐츠에 따라 그리드가 길어진다. */
grid-auto-rows: minmax(150px, auto);
}

그리드 흐름

  • grid-auto-flow 속성으로 조절한다.
    • row: 기본값이며 왼쪽 위부터 오른쪽으로 칸을 채워나간다.
    • column: 줄을 채우고 다음 컬럼을 채운다.
    • dense: 빈 공간에 채워넣는다.
    • sparse: 기본값이며 빈 공간을 내버려둔다.

이 외에 order 속성으로 직접 순서 제어가 가능하다. 다만 denseorder는 탭 순서까지 변경해주지 않아 접근성을 벗어난다.

supports

구 브라우져와의 호환을 위해 @supports (display: grid) 처럼 서포트 피쳐쿼리를 사용할 수 있다.

NodeJS에서 커맨드 파싱하기

· One min read

arg

zeit/arg 패키지를 이용하면 된다.

사용법

arg 함수 하나로 파싱이 가능하다.

const arg = require("arg");

// `options` is an optional parameter
const args = arg(
spec,
(options = { permissive: false, argv: process.argv.slice(2) }),
);

세부적인 사용방법은 다음과 같다.

  1. 타입을 정하고
  2. 옵션과 축약 옵션을 정하고
  3. 검증을 넣는다.

소스

// 계정정보를 받는 스크립트라면
const help = () => {
console.log(`usage => ...`);
};

let args = {};
try {
args = arg({
"--help": Boolean,
"--user": String,
"--password": String,
"--verbose": arg.COUNT,
"--test": Boolean,

"-h": "--help",
"-u": "--user",
"-p": "--password",
"-v": "--verbose",
});
} catch (err) {
if (err.code === "ARG_UNKNOWN_OPTION") {
help();
process.exit(1);
}
}

if (!(args["--user"] && args["--password"])) {
help();
process.exit(1);
}

if (args["--test"] === true) {
process.env.TEST = 1;
}

Workbox5 버전의 주요 변경사항

· One min read

Workbox5 버전

잘 돌던 workbox-cli 가 5버전 릴리즈 후부터 돌지 않아서 확인해보았다. 전체 문서는 여기서 확인 가능하다.

injectManifest

self.__WB_MANIFEST 를 주입받는 방식으로 변경되었다.

// v4:
precacheAndRoute([]);

// v5:
precacheAndRoute(self.__WB_MANIFEST);

blacklist, whitelist 에서 denylist, allowlist 로 키 명이 변경되었다.

BroadcastChannel

broadcast-update 가 자체 API 에서 postMessage()로 변경되었다. 이벤트 리스너가 많아져 복잡해지고, 기존 API에서는 메세징의 버퍼 기능이 없었기 때문이다.

// v4:
const updatesChannel = new BroadcastChannel("api-updates");
updatesChannel.addEventListener("message", (event) => {
const { cacheName, updatedUrl } = event.data.payload;
// ... your code here ...
});

// v5:
// This listener should be added as early as possible in your page's lifespan
// to ensure that messages are properly buffered.
navigator.serviceWorker.addEventListener("message", (event) => {
// Optional: ensure the message came from workbox-broadcast-update
if (event.meta === "workbox-broadcast-update") {
const { cacheName, updatedUrl } = event.data.payload;
// ... your code here ...
}
});

Pythonic - 핸들링

· 3 min read

Handling

파일

  • File pointer 여는 법은 f.open(‘test.txt’, ‘w’)
  • 이 경우 f.write 후에 f.close 해줘야한다.
  • with 사용시 close 를 생각하지 않아도 된다.
  • with 구문에서 선언된 변수는 바깥에서 사용가능
  • f.seek(5) 처럼 위치로 이동 가능하다.
  • string.Template 과 함께 뷰 파일에서 사용할 수 있다.
## 예시 1
with open('test.txt', 'w') as f:
f.write('Test')

## 예시 2
with open('test.txt', 'r') as f:
# print(f.read())
while True:
chunk = 2
line = f.read(chunk)
print(line)

if not line:
reak

## 예시 3
## w+ 로 열면 파일이 초기화 됨
with open('test.txt', 'w+') as f:
f.write(s)
# 쓰기 후에 읽기위해 0번째로 이동
f.seek(0)
print(f.read())

## 예시 4
import string

with open('view/mail.tpl', 'r') as f:
t = string.Template(f.read())

## $name, $contents
contents = t.substitute(name='gracefullight', contents='Thanks')
print(contents)

파일 확인

  • os.path.exsists
  • os.path.isfile
  • os.path.isdir

파일 제어

  • os.rename
  • os.symlink: 심볼릭
  • shutil.copy: 복사
  • pathlib.Path('TouchFilePath').touch(): 터치 파일

폴더 제어

  • os.mkdir
  • os.rmdir: 빈 디렉토리만 가능
  • shutil.retree: recursive

CSV

  • csv.DictWriter
  • csv.DictReader

임시파일

import tempfile

## 삭제 됨
with tempfile.TemporaryFile(mode='w+') as t:
t.write('hello')

## 삭제 안 됨
with tempfile.NamedTemporaryFile(delete=False) as t:
# print(t.name)
with open(t.name, 'w+') as f:
f.write('hello')


## 삭제되는 디렉토리
with tempfile.TemporaryDirectory() as td:
print(td)

## 삭제 안 되는 디렉토리
temp_dir = tempfile.mkdtemp()

압축

tar

import tarfile

with tarfile.open('test.tar.gz', 'w:gz') as tr:
tr.add('dir')

with tarfile.open('test.tar.gz', 'r:gz') as tr:
tr.extractall(path='dir')

with tr.extractfile('tarball') as f:
print(f.read())

zip

import zipfile

with zipfile.ZipFile('test.zip', 'w') as z:
# 하나의 폴더만 가능
z.write('dir')

# 하위 전체 압축
for f in glob.glob('dir/**', recursive=True):
z.write(f)

ini

  • configparser.ConfigParser()

yaml

  • pip install pyyaml

로깅

  • logging.critical
  • error
  • warning
  • info
  • debug
import logging

## basicConfig 에 format 을 정의 가능
## doc 확인
logging.basicConfig(filename='test.log', level=logging.INFO)
logging.info('info %s %s', 'test', 'test2') # === logging.info('info {}'.format('test')

## 해당 파일에서 로그명 재정의
logger = logging.getLogger(__name__)

## 로그 레벨 재정의
logger.setLevel(logging.DEBUG)

## 전체 설정 변경
logging.config.fileConfig(...)
logging.config.dictConfig(...)

로깅 핸들러

여러 핸들러를 쉽게 붙힐 수 있다. 문서

  • handler = logging.FileHandler(...)
  • logger.addHandler(handler)

로깅 필터

로그의 출력을 필터를 사용해 쉽게 가공 가능하다. 문서

메일

  • smtplib.SMTPfrom email import message 패키지로 가능하다.
  • 파일 첨부는 email.mime 의 multipart와 text 패키지로 가능하다.
    • 파일 추가 시에는 헤더를 Content-Disposition: attachment로 줘야한다.
  • logger.handelrs.SMTPHandler 로 메일로 로그를 받을 수 있다.

서브프로세스

import subprocess

subprocess.run(['ls', '-al'])

커맨드 체이닝

여러 커맨드를 한 번에 실행시킬 경우 인젝션 방어를 위해 다음과 같이 처리하는 것이 좋다.

process1 = subprocess.Popen(['ls', '-al'], stdout=subprocess.PIPE)
process2 = subprocess.Popen(['grep' , 'test'], stdin=p1.stdout, stdout=subprocess.PIPE)
process1.stdout.close()

output = process2.communicate()[0]

커맨드 파싱

argparse를 사용하자.

날짜

파이썬에는 날짜를 timestamp 로 변환해 출력하는 기능은 없다.

초기화

import datetime
import time


now = datetime.datetime.now()
print(now)
print(now.isoformat())
print(now.strftime('%Y-%m-%d %H:%M:%S.%f'))

## 날짜만
today = datetime.date.today()
print(today) # 2020-02-02
print(today.isoformat()) # 2020-02-02
print(today.strftime('%Y-%m-%d'))

## 시간만
t = datetime.time(hour=1, minute=10, second=5, microsecond=100)
print(t) # 01:10:05.000100
print(t.isoformat()) # 01:10:05.000100
print(t.strftime('%H:%M:%S')) # 01:10:05

## timestamp
print(time.time()) # 1580617313.843047

연산

now = datetime.datetime.now()
print(now)

d = datetime.timedelta(weeks=1)
print(now - d)

쿠버네티스 리눅스 커널 튜닝하기

· 2 min read

변경가능한 커널 설정

노드 레벨의 sysctl과 네임스페이스 sysctl과 같은 커널 파라미터를 sysctl 인터페이스로 변경할 수 있다. 변경 가능한 파라미터는 다음과 같다.

  • abi: 실행 도메인 특성
  • fs: 특정 파일 시스템, 파일 핸들링, inode, dentry, 쿼터 조정
  • kernel: 전역 커널 설정 변경
  • net: 네트워킹
  • sunrpc: SUN rpc 호출
  • vm: 메모리 조정, 버퍼 및 캐시 관리
  • user: 사용자별 네임스페이스 제한

taint, toleration 을 같이 사용해 사이드이펙을 방지하라고 권하고 있다.

ARP 캐시

neighbour: arp_cache: neighbor table overflow!

쿠버네티스가 대량의 IP를 소비하면서 ARP 캐시 공간을 모두 사용할 경우 ARP 캐시 관련 변수 조절이 가능하다. 대규모 HPC 클러스터에서는 흔한 일이며 쿠버네티스의 주소 소진을 방지할 수 있다. 이 오류는 nodes with 40+ cores && more than 16 segments in each node 정도에서 발생하는 듯 하다.

  • net.ipv4.neigh.default.gc_thresh1: gc_thresh1 represents the minimum number of entries that may be in the ARP cache. Garbage collection will not be triggered if the number of entries is below this setting.
  • net.ipv4.neigh.default.gc_thresh2: gc_thresh2 represents the soft maximum number of entries that may be in the ARP cache. This setting is arguably the most important, as ARP garbage collection will be triggered ~5s after reaching this soft maximum.
  • net.ipv4.neigh.default.gc_thresh3: gc_thresh3 represents the hard maximum number of entries in the ARP cache.
net.ipv4.neigh.default.gc_thresh1 = 80000
net.ipv4.neigh.default.gc_thresh2 = 90000
net.ipv4.neigh.default.gc_thresh3 = 100000

예시

apiVersion: v1
kind: Pod
metadata:
name: sysctl-example
spec:
securityContext:
sysctls:
- name: kernel.shm_rmid_forced
value: "0"
- name: net.core.somaxconn
value: "10000"
- name: kernel.msgmax
value: "65536"
- name: fs.file-max
value: "2097152"
- name: net.ipv4.ip_local_port_range
value: "1024 65536"

여담

  • 파면 팔수록 리눅스부터 다시 정리해야되겠다는 느낌이 든다.

참조

서비스워커로 POST Request 캐싱하기

· 2 min read

앞서

서비스워커로 Navigation Request 나 Static Assets 에 대한 리소스 캐시는 쉽다. (이전 포스팅 참조)

하지만 POST Request 에 대한 레퍼런스는 찾기 힘들어 결국 만들어버렸다. 복잡한 로직이지만 Request Body 를 SHA1로 해싱해 키로 IndexedDB 에 저장하고 그 키가 맞으면 꺼내주는 방식이다.

소스

// IndexedDB 는 Promisify 되어있지 않아서 라이브러리가 필요하다.
importScripts(
"https://cdn.jsdelivr.net/npm/[email protected]/dist/localforage.min.js",
);

// 캐시하고 싶은 POST 엔드포인트
const ENDPOINT = "https://your-domain/post-request";

const bin2Hex = (buffer) => {
let digest = "";
const dataView = new DataView(buffer);
for (let i = 0, len = dataView.byteLength; i < len; i += 4) {
const value = dataView.getUint32(i);
// hex 로 바꾸면 패딩비트 0 이 제거된다.
const hex = value.toString(16);
// uint32 는 4bytes 로 나온다.
const padding = "00000000";
// 패딩을 더해서 뒤에서 잘라준다.
const paddedValue = (padding + hex).slice(-padding.length);
digest += paddedValue;
}

return digest;
};

const postRequestFetchListener = (fetchEvent) => {
const requestUrl = fetchEvent.request.url;
const method = fetchEvent.request.method.toUpperCase();
// 맞는 엔드포인트인지 확인
if (!(method === "POST" && requestUrl === ENDPOINT)) {
return;
}

fetchEvent.respondWith(
fetchEvent.request
.clone()
.arrayBuffer()
.then((buffer) => {
const requestBody = String.fromCharCode.apply(
null,
new Uint8Array(buffer),
);
// request body 에 원하는 조건만 캐시처리할 수 있게 한다.
if (requestBody.includes("cache=1")) {
// 속도면에서 다른 해싱 알고리즘을 사용해도 무방하다.
return crypto.subtle.digest("SHA-1", buffer);
}

return Promise.reject();
})
.then((sha1Buffer) => {
const sha1Hash = bin2Hex(sha1Buffer);
console.log("SHA1 Hash => ", sha1Hash);

// IndexedDB 에서 캐시된 키를 찾는다.
return localforage.getItem(sha1Hash).then((cachedResponse) => {
if (cachedResponse) {
console.log("Cached repsonse => ", cachedResponse);
return new Response(cachedResponse, {
// 여기서 statusCode 를 304 로 내보내고 싶었으나, Body 를 반환할 수 없었다.
status: 200,
statusText: "OK",
headers: {
"Content-Length": cachedResponse.length,
"Content-Type": "application/json",
// 그래서 커스텀 헤더를 추가했다.
"X-SW-Cache-Hit": 1,
"X-SW-Cache-Type": "POST",
},
});
}

// 캐시된 데이터가 없을 경우 새로 요청한다.
return fetch(fetchEvent.request).then((response) => {
console.log("Fetching response => ", response.clone());

// 정상적일 경우만 IndexedDB 에 저장한다.
if (200 <= response.status && response.status < 400) {
// 이 작업은 비동기지만 굳이 기다리지 않아도 된다.
response
.clone()
.text()
.then((textResponse) => {
console.log("Caching response => ", textResponse);
return localforage.setItem(sha1Hash, textResponse);
});
}

return response;
});
});
})
.catch(() => fetch(fetchEvent.request)),
);
};

// 리스너를 등록해준다.
self.addEventListener("fetch", postRequestFetchListener);

여담

  • WorkBox 를 사용할 수 있다면 CacheableResponse와 CacheFirst 정책으로 단번에 처리 가능할 것이다.
  • 굳이 해시를 키로 사용하지 않아도 된다. RequestBody 의 Serialize 를 키로 써도 된다. (만들면서 crypto 라이브러리를 사용해보고 싶었을 뿐)

Dockerfile의 모든 것

· 4 min read

인스트럭션

  • FROM: 빌드하는 이미지의 기반 이미지 지정
  • RUN: 이미지 빌드 시 컨테이너에서 실행할 명령어 정의
  • COPY: 호스트에서 컨테이너로 파일 및 디렉토리 복사
  • ADD: COPY + 압축 해제 + URL 다운로드
    • 운영 체제를 담은 기반 이미지를 만드는 경우처럼 특수한 경우에만 사용하면 된다.
    • 안정성 보장이 되지 않으므로 COPY 를 사용하자.
  • CMD: 컨테이너에서 foreground로 실행할 명령어 정의
  • ENTRYPOINT: 컨테이너를 실행 가능 파일로 사용할 때 정의하는 명령
    • CMD 와 ENTRYPOINT 둘 다 사용 가능
  • ARG: docker image build를 실행할 때 사용하는 변수
  • ENV: 컨테이너 안의 환경변수 정의
  • EXPOSE: 컨테이너가 노출하는 포트
  • VOLUME: 호스트나 다른 컨테이너에서 마운트할 수 있는 포인트 생성
  • LABEL: 이미지에 추가하는 메타데이터
  • STOPSIGNAL: 컨테이너에 전달되면 컨테이너를 종료하는 시스템 시그널 설정
  • HEALTHCHECK: 컨테이너 안에서 명령을 실행 후 결과를 헬스 체크에 사용
  • USER: 컨테이너 실행 시 컨테이너 사용자
    • 이미지 빌드시 USER 뒤에 나오는 RUN 인스트럭션도 해당 사용자의 권한으로 실행된다.
  • WORKDIR: 컨테이너의 작업 디렉토리
  • ONBUILD: 컨테이너 안에서 실행되는 명령 정의, 이미지에서 실행되지 않는다.
    • ONBUILD 를 정의한 이미지를 기반 이미지로 삼아 다른 이미지를 빌드할 때 실행된다.

이미지

린팅

hadolint 를 설치해 Dockerfile best practices 에 기반해 이미지를 생성했는지 검증하자.

기반 이미지

  • scratch: 아무 것도 없는 이미지
    • https 통신이 필요한 경우 cacert.pem 을 /etc/ssl/certs 에 추가해야한다.
    • 디버깅도 힘들다
  • busybox: 기본 유틸리티 (echo, ls 등) 이 있는 이미지
    • 패키지 관리자가 없다.
    • 디버깅은 좀 낫다.
  • alpine: busybox 기반으로 4MB 지만 apk 패키지 매니저가 있다.
    • glibc 대신 musl을 쓴다.
    • apk add --no-cache package
    • apk add --no-cache --virtual=ailas package && apk del --no-cache ailas

멀티스테이지 빌드

golang과 같은 빌드가 필요한 이미지에서는 멀티스테이지 빌드를 이용해 빌드 환경과 프로덕션 환경을 다르게 가져갈 수 있다.

FROM golang:1.9 AS build

WORKDIR /
COPY . /go/src/github.com/...
RUN go get gokpg.in/gorp.v1
RUN cd /go/src/github.com/... && go build -o bin/start main.go

FROM alpine:3.7
COPY --from=build /go/src/.../bin/start /usr/local/bin/
CMD ["start"]

distroless 이미지

  • 운영체제 기능은 없이 언어에 중점을 둔 이미지이다.
  • distroless 에서 확인 가능하며 주로 구글이 배포한다.
  • gcr.io/distroless/base 이미지는 glibc 기반이며 컴파일 애플리케이션을 실행하는 데에 적합하다. (Go)
  • ca-certificates 및 TLS/SSL 관련 라이브러리 등 최소한의 라이브러리만 있다.
  • CVE 취약점도 업데이트 된다고 한다.
FROM node:10.17.0 AS build-env
ADD . /app
WORKDIR /app

FROM gcr.io/distroless/nodejs
COPY --from=build-env /app /app
WORKDIR /app
CMD ["hello.js"]

chucksum 검증

ADD 인스트럭션으로 추가 된 파일은 해시기반 체크섬 검증을 해주는 것이 좋다.

ADD library.zip .
ADD library_SHA256 .
ADD library_SHA256.sig .

## Import PGP public key
RUN curl https://.../pgp_keys.asc | gpg --import

## 라이브러리 전자 서명 검증
RUN gpg --verify library_SHA256.sig library_SHA256

## Verify checksum
RUN cat library_SHA256 | grep linux_amd64 | sha256sum -cs
RUN unzip libary.zip
RUN mv library /usr/local/bin
## 실행

dockerigonore

  • Dockerfile 빌드 시에 따라 들어가지 않게 된다.
  • Dockerfile 과 같은 레벨 디렉토리에 있어야한다.
.dockerignore
.git
.idea
.vscode
.github
*.log

이미지 테스트

빌드 후의 이미지 내부에 상태가 적절한지 테스트하기 위해 아래 두 가지 yaml 기반의 테스트 툴을 사용할 수 있다.

이 중 goss는 실제 포트 및 서비스가 서빙 중인지 확인이 가능해 더 유용할 것으로 보인다.

이미지 보안

user

호스트의 리소스를 컨테이너에서 공유하는 Docker는 사용자 UID도 0으로 같이 공유되므로 같은 권한을 갖게 된다. 이 문제를 방지하기 위해 useradd 로 어플리케이션 실행 유저를 만들어 주고 USER 인스트럭션을 사용해 실행을 해줘야한다.

FROM golang:1.10

RUN mkdir /app
COPY main.go /app

RUN useradd gracefullight
USER gracefullight

CMD ["go", "RUN", "/app/main.go"]

secret

dockerd 튜닝

  • max-concurrent-downloads: 기본값은 3이며, docker image pull 로 한 번에 다운로드 되는 이미지 스레드 수를 증가시켜준다.
  • max-concurrent-uploads: 기본값은 5이며, docker image push 시에 이미지 업로드 스레드 수를 증가시켜준다.
  • registry-mirrors: Docker hub의 미러 레지스트리를 만들어 트래픽 향상에 이점을 줄 수 있다.

private registry

빠른 이미지 푸쉬/풀과 소스 때문이라도 private registry 는 필수적이다. docker 에서 제공하는 registry 이미지를 사용하면 된다.

GUI 기반으로 확인할 수 있는 툴은 아래와 같다.

  • Harbor: 프라이빗 레포지토리를 위한 모든 기능이 다 있다. 쓰자.
  • Portus: 인증 포함, 하지만 루비라 소스 개선이 힘들듯
  • docker-registry-ui: 20년 최근까지 개선 중

쿠버네티스 로그 아키텍쳐

· One min read

관리 원칙

  • 어플리케이션 로그는 모두 stdout 으로 출력해야한다.
    • 컨테이너로 운영하는 것을 전제로 한다면 파일 출력 자체가 불필요하다.
  • Nginx 등의 미들웨어에서는 로그가 stdout 으로 출력되도록 이미지를 빌드한다.
  • stdout 으로 출력되는 로그는 모두 JSON 포맷으로 출력해 각 속성을 검색할 수 있게 한다.
  • 쿠버네티스 환경에서는 fluentd-kubernetes-daemonset 을 포함하는 파드를 DaemonSet을 사용해 각 호스트에 배치한다.
  • 쿠버네티스 리소스에서는 적절히 레이블을 부여해 로그를 검색할 수 있게 한다.

Brew, 맥 추천 패키지

· 3 min read

여러 맥 기기의 환경을 구성하다보니 적어 놓는 게 나을 듯 싶었다.

brew

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

formulae

main formulae

brew install fzf \
git \
go \
kubernetes-cli \
mas \
node \
python \
stern \
tree \
wget
  • fzf
  • git
  • go
  • java
  • kubernetes-cli
  • mas
  • node
  • python
  • stern
  • tree
  • wget

sub formulae

docker for mac

brew tap spring-io/tap
brew install git \
hadolint \
helm \
kubectx \
k9s \
spring-boot \
thefuck \
volta \
youtube-dl \
zsh-autosuggestions \
zsh-syntax-highlighting
  • git
  • helm
  • kubectx
  • springboot
  • thefuck
  • volta
  • youtube-dl
  • zsh-autosuggestions
  • zsh-syntax-highlighting

cask

brew tap homebrew/cask-fonts
brew cask install adguard \
appcleaner \
bitwarden \
cap \
dbeaver-community \
firefox \
font-cascadia \
font-hack-nerd-font \
fork \
gather \
google-chrome \
hiddenbar \
iina \
iterm2 \
java \
jetbrains-toolbox \
keepingyouawake \
keka \
keycastr \
macs-fan-control \
monitorcontrol \
microsoft-edge \
orange \
pika \
podman-desktop \
postman \
rectangle \
rocket \
slack \
telegram-desktop \
visual-studio-code \
yt-music \
zoom \
zulu
  • adguard: 💰
  • appcleaner: 앱 클리너
  • bitwarden: 비밀번호 관리
  • cap: 화면 녹화 및 편집
  • dbeaver-community: DBMS 클라이언트
  • firefox: 파이어폭스
  • font-cascadia: Cascadis Code 폰트
  • font-hack-nerd-font: iTerm2 용 터미널 폰트
  • fork: 무료 중 최고의 git client
  • gather: 게더타운
  • google-chrome: 크롬
  • hiddenbar: 작업표시줄의 프로그램 숨기기
  • iina: 깔끔한 인터페이스의 미디어 플레이어
  • iterm2: 터미널
  • jetbrains-toolbox: jetbrains IDE 버전 관리
  • keepingyouawake: 잠자기 모드 제어
  • keka: 압축 프로그램
  • keycastr: 키 입력 녹화
  • macs-fan-control: 팬 조절
  • monitorcontrol: 외장모니터 제어
  • microsoft-edge: edge
  • orange: orange3 데이터 분석
  • pika: 색상 선택기
  • podman-desktop: docker desktop 대체
  • postman: 포스트맨
  • rectangle: 창 크기 조절
  • rocket: 이모지 선택기
  • slack: 슬랙
  • telegram-desktop: 텔레그램
  • visual-studio-code: vscode
  • yt-music: 유튜브 뮤직 플레이어
  • zoom: 줌
  • zulu: 자바

App store

mas install 497799835 \
1355679052 \
869223134 \
1295203466 \
1471801525 \
897118787 \
425424353 \
1475628500
  • xcode: 497799835
  • dropover: 1355679052 드래그 후 흔들면 가상폴더로 저장
  • kakaotalk: 869223134 카카오톡
  • microsoft remote desktop: 1295203466 윈도우 원격
  • polyglot: 1471801525 사파리 번역기
  • shazam: 897118787 음악 찾기
  • unicorn https: 1475628500 Encrypt DNS query

Bunblefile

bundle

brew bundle install

Brewfile
tap "homebrew/bundle"
tap "homebrew/cask"
tap "homebrew/cask-drivers"
tap "homebrew/cask-fonts"
tap "spring-io/tap"

cask_args appdir: "~/Applications", require_sha: true

brew "fzf" # fuzzy finder
brew "gh" # github clone
brew "git" # git
brew "hadolint" # dockerfile linter
brew "helm" # helm chart
brew "k9s" # k8s dashboard
brew "kubectx" # k8s context switcher
brew "mas" # app store
brew "mkcert" # local certification
brew "skaffold" # k8s deploy
brew "thefuck" # fix typo
brew "tree" # tree
brew "volta" # nodejs version manager
brew "wget" # wget
brew "zsh" # zsh
brew "zsh-autosuggestions"
brew "zsh-syntax-highlighting"
brew "spring-io/tap/spring-boot" # spring cli

cask "appcleaner" # 앱 클리너
cask "bitwarden" # 비밀번호 관리
cask "dbeaver-community" # dbms client
cask "firefox" # 파이어폭스
cask "font-cascadia-code" # ms 개발 폰트
cask "font-hack-nerd-font" # 터미널 폰트
cask "fork" # git gui
cask "gather" # gathertown
cask "hiddenbar" # 작업표시줄 숨기기
cask "iina" # 플레이어
cask "iterm2" # 터미널
cask "jetbrains-toolbox"
cask "keepingyouawake" # 잠자기 해제
cask "keka" # 압축 프로그램
## cask "logitech-options" # 로지텍 사용자만
cask "macs-fan-control" # 팬 조절
cask "microsoft-edge" # edge
cask "monitorcontrol" # 외장 모니터 조절
cask "orange" # orange3
cask "pika" # 색상 선택기
cask "podman-desktop" # docker desktop 대체
cask "postman" # postman
cask "rectangle" # 창 조절
cask "rocket" # 이모지
cask "slack" # slack
cask "telegram-desktop" # telegram
cask "visual-studio-code" # vscode
cask "zoom" # zoom
cask "zulu" # zulu jdk

mas "Dropover", id: 1355679052
mas "Microsoft Remote Desktop", id: 1295203466 # remote desktop
mas "Polyglot", id: 1471801525 # translator
mas "Shazam", id: 897118787 # music finder
mas "Xcode", id: 497799835 # xcode
mas "카카오톡", id: 869223134 # kakaotalk

쿠버네티스 롤링 업데이트와 배포

· 3 min read

Deployment

Deployment 의 파드 교체 전략에는 RollingUpdateRecreate 가 있다. 기본 값은 RollingUpdate 이며 간단한 설정이 적용된 디플로이먼트는 아래와 같을 것이다.

apiVersion: apps/v1
kind: Deployment
metadata:
name: test-rolling-update
label:
app: test
spec:
replicas: 4
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 3
maxSerge: 4
selector:
matchLabels:
app: test
template:
metadata:
labels:
app: test
spec:
containers:
- name: test
image: echo
ports:
- containerPort: 8080

여기서 롤링업데이트 속성을 좀 더 자세히 알아보자.

RollingUpdate

maxUnavailable

  • 롤링 업데이트 중 동시에 삭제할 수 있는 파드의 최대 갯수
  • 기본 값은 replicas의 25% 이다.
  • replicas: 4의 경우 1개 파드 삭제, replicas: 8의 경우 2개 파드 동시 삭제
  • 퍼센트 및 직접 지정이 가능하다.
  • 위의 예시 디플로이먼트에서 롤링업데이트 시작 시 파드 3개가 바로 죽는다.
  • 이 값을 높게 설정하면 동시에 교체되는 파드가 늘어나므로 롤링 업데이트 시간이 줄어든다.
    • 하지만 롤링업데이트 중에 남아 있는 파드에 요청 수가 몰릴 수 있다.
    • 따라서 1로 설정해 파드를 하나씩 교체하는 것이 안전할 수 있다.

maxSurge

  • 롤링 업데이트 중 동시에 생성하는 파드 갯수
  • 기본 값은 replicas의 25% 이다.
  • replicas: 4면서 maxSurge: 4면 롤링 업데이트 시작 시 새 버전의 파드가 4개 추가로 생성된다.
  • 이 값을 높게 설정하면 필요한 파드를 빨리 생성하므로 파드 교체 시간이 단축된다.
    • 하지만 필요한 시스템 자원이 급증할 수 있다.

Probe

세부 설정은 Docs를 참조하자

livenessProbe

  • 애플리케이션 헬스 체크 기능
  • 애플리케이션이 의존하는 컨테이너 안의 파일의 존재여부 확인
  • Unhealthy 상태의 경우 파드 재시작

readinessProbe

  • 컨테이너 외부에서 HTTP 등의 트래픽을 발생시켜 처리할 수 있는 상태인지 확인
  • tcpSocket으로 포트 지정도 가능하다.

응답 중인 파드 교체

응답 중인 파드가 교체되는 경우 SIGTERM 신호를 보내 파드가 삭제되는데, Graceful Shutdown 상태로 만들기 위해서 종료 처리가 오래 걸리는 파드엔 terminationGracePeriodSeconds 를 설정해주는 것이 좋다.

spec:
# 기본값 30
terminationGracePeriodSeconds: 40
containers:
- name: maria
image: maria:latest

Nginx처럼 SIGTERM 시그널을 받고 바로 종료되는 어플리케이션이 있는 파드라면, 라이플사이클 훅을 활용해 안전하게 종료시키는 것이 중요하다.

spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
lifecycle:
# 파드 종료 전 훅
preStop:
exec:
command: ["/usr/sbin/nginx", "-s", "quit"]

파드 1:1 교체

그렇다면 replicas: 1인 파드를 1:1 교체 시에는 어떤 전략을 가져가야할까 답은 아래와 같다.

strategy:
type: RollingUpdate
rollingUpdate:
# 롤링 업데이트 시 삭제 되는 파드 수
maxUnavailable: 0
# 롤링 업데이트 시 새로 생성되는 파드 수
maxSurge: 1

여기에 파드가 트래픽을 받을 수 있는지 readinessProbe를 추가해주면 된다.

readinessProbe:
httpGet:
path: /ping
port: 80
initialDelaySeconds: 5
periodSeconds: 5
successThreshold: 1
failureThreshold: 3
timeoutSeconds: 3

5초 후에 5초마다 /ping 을 보내 성공여부를 확인한다. 타임아웃은 3초며 3번까지 재시도한다.

Blue/Green

  • 디플로이먼트를 2개 만들고, 서비스의 selector 값을 라벨에 따라 변경하자.
  • 서비스메시를 연동하면 카나리아 배포 방식도 가능하다.