Skip to main content

Production ready nodejs dockerfile

· 2 min read

구글링으로 때워버린 nodejs 이미지라면 취약점, 실행 권한, 파드 배치에 있어 치명적이다. 그렇다면 실제환경에서 사용할 수 있을만한 이미지는 무엇일까?

린팅

  • hadolint 를 사용하자.

취약점 분석

  • clair 를 사용하자.

소스

  • 모든 조건을 만족시킨 이미지는 다음과 같다.
# 컨테이너를 위해 만들어진 alpine 이미지를 사용한다.
# 외부 레파지토리에 의존적인 모듈에 대해 latest 버전은 사용하지 않아야한다.
FROM node:16.13-alpine3.12

ENV HOME /usr/src/app

# tini 는 nodejs 파드를 PID 1 로 실행시켜 정상적인 종료를 가능하게 한다.
RUN apk add --no-cache tini=0.19.0-r0 \
&& mkdir -p $HOME \
&& chown node:node -R $HOME

WORKDIR $HOME
# 패키지 의존성을 먼저 설치한다.
COPY --chown=node:node package*.json ./

USER node
# node 권한으로 설치한다.
RUN npm install && npm cache clean --force

# 앱 소스를 복사한다.
COPY --chown=node:node . .
# 앱 빌드와 후처리 쉘에 권한을 준다.
RUN npm run build \
&& chmod u+x "bin/entrypoint.sh"

EXPOSE 3000
ENTRYPOINT [ "/sbin/tini", "--", "./bin/entrypoint.sh" ]

여담

  • 빌드 환경과 실행 환경을 분리시켜 실제 환경에서는 src 폴더 없이 실행시킬 수 있다.
  • 아예 단일실행파일(vercel/pkg) 로 만들 수도 있다.
  • deno 로 더 간단한 dockerfile 을 만들 수 있을 것 같다.

참조

One true layout 만들기

· 4 min read

Header, Navigation, Aside, Section, Footer 로 이루어진 레이아웃을 만들 시에 float 을 사용해서 구성할 경우 틀어짐을 잡는 방법에 대한 내용이다.

원문으로 보이는 링크에서는 진정한 하나의 레이아웃을 찾는 과정 중 하나이며 Eqaul Height Columns - revisited로 소개되고 있다.

이슈

아래와 같은 레이아웃에는 footer 영역이 섹션에 붙어 올라온다.

<style>
body {
width: 1000px;
margin: 0 auto;
}
#aside {
float: left;
width: 200px;
}
#section {
float: left;
width: 800px;
}
</style>
<body>
<div id="header">header</div>
<div id="nav">nav</div>
<div id="wrap">
<div id="aside">
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi sed
sollicitudin mauris. Aliquam faucibus facilisis vulputate. Curabitur
condimentum placerat mattis. Duis at metus at tellus volutpat ultrices.
Cras lorem eros, cursus et risus sit amet, gravida feugiat libero.
Nullam id faucibus ipsum. Nulla a leo sed eros mattis bibendum. Nullam
et sapien in orci tempus elementum eu sed augue. Pellentesque eu
vestibulum arcu. Pellentesque vel finibus libero. Nulla facilisi.
Quisque dolor enim, ornare eget elit ac, pharetra porta ex. Vivamus
eleifend eu arcu nec consequat.
</p>
</div>
<div id="section">
<p>
Nam pulvinar dictum nibh id ullamcorper. Suspendisse justo eros, tempor
vel faucibus in, pellentesque congue enim. Proin non eleifend turpis,
vel commodo purus. Fusce vitae nisl dapibus, tincidunt elit at, cursus
lacus. Maecenas varius imperdiet sollicitudin. Nunc pharetra fringilla
enim ut facilisis. Curabitur maximus nibh non rhoncus semper. Duis
porta, purus ut tincidunt convallis, sem purus pharetra erat, eu
vestibulum tellus mi id eros. Fusce congue, erat at blandit mollis,
tellus ex semper velit, dapibus commodo ante turpis a neque. Fusce vel
ex id sem auctor accumsan. Maecenas finibus nunc sem, ut gravida felis
efficitur at. Pellentesque lobortis dui non ligula condimentum, at
auctor dui blandit.
</p>
</div>
</div>
<div id="footer">footer</div>
</body>

해결방안

overflow: hidden

래퍼에 이 속성을 넣는 것으로 해결 된다.

body {
width: 1000px;
margin: 0 auto;
}
#aside {
float: left;
width: 200px;
}
#section {
float: left;
width: 800px;
}
#wrap {
overflow: hidden;
}

clear: both

또는 래퍼를 삭제하고 구획을 나누는 부분에 sibling 노드로 clear: both 속성을 주면 된다.

<style>
body {
width: 1000px;
margin: 0 auto;
}

.clear {
clear: both;
}

#aside {
float: left;
width: 200px;
}
#section {
float: left;
width: 800px;
}
</style>
<body>
<div id="header">header</div>
<div id="nav">nav</div>
<div id="aside">
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi sed
sollicitudin mauris. Aliquam faucibus facilisis vulputate. Curabitur
condimentum placerat mattis. Duis at metus at tellus volutpat ultrices.
Cras lorem eros, cursus et risus sit amet, gravida feugiat libero. Nullam
id faucibus ipsum. Nulla a leo sed eros mattis bibendum. Nullam et sapien
in orci tempus elementum eu sed augue. Pellentesque eu vestibulum arcu.
Pellentesque vel finibus libero. Nulla facilisi. Quisque dolor enim,
ornare eget elit ac, pharetra porta ex. Vivamus eleifend eu arcu nec
consequat.
</p>
</div>
<div id="section">
<p>
Nam pulvinar dictum nibh id ullamcorper. Suspendisse justo eros, tempor
vel faucibus in, pellentesque congue enim. Proin non eleifend turpis, vel
commodo purus. Fusce vitae nisl dapibus, tincidunt elit at, cursus lacus.
Maecenas varius imperdiet sollicitudin. Nunc pharetra fringilla enim ut
facilisis. Curabitur maximus nibh non rhoncus semper. Duis porta, purus ut
tincidunt convallis, sem purus pharetra erat, eu vestibulum tellus mi id
eros. Fusce congue, erat at blandit mollis, tellus ex semper velit,
dapibus commodo ante turpis a neque. Fusce vel ex id sem auctor accumsan.
Maecenas finibus nunc sem, ut gravida felis efficitur at. Pellentesque
lobortis dui non ligula condimentum, at auctor dui blandit.
</p>
</div>
<!-- clear:both 노드 추가 -->
<div class="clear"></div>

<div id="footer">footer</div>
</body>

CSS3 레이아웃

· 10 min read

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

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

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 버전의 주요 변경사항

· 2 min read

잘 돌던 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 - 핸들링

· 4 min read

파일

  • 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)

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

· 3 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 캐싱하기

· 4 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의 모든 것

· 7 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을 사용해 각 호스트에 배치한다.
  • 쿠버네티스 리소스에서는 적절히 레이블을 부여해 로그를 검색할 수 있게 한다.