삽질특기생

다음에 삽질 덜하려고 만든 블로그

0%

k8s의 pod에 대해 조사를 해야하는 일이 생겨서 개인 컴퓨터에 k8s를 설치해보았다.
원래 k8s는 세 대 이상의 클러스터에 구성할 것을 권장하는 것으로 알고 있지만 나는 테스트용이므로 한 대로만 구성하였다.
k8s가 무엇인지 간략하게 알아보고, 설치 과정에 대해 기술하도록 하겠다.

kubernetes란

kubernete(k8s)는 docker orchetration tool로, application container의 deploy, scaling, operating 자동화를 위한 플랫폼이다.
쉽게 설명하자면 k8s는 사용자가 기대하는 상태로 동작하도록 application을 생성하고 실행 상태를 유지하는 것이 목적이라고 할 수 있다.
k8s는 여러 노드를 하나의 클러스터로 관리하는데, 하나의 master node와 그 외 여러 일반 node로 구성된다. 일반 node는 minions라고도 불리는 모양이다.(미니언즈라니…귀여워…)
master node에는 클러스터를 관리하는 목적의 controller pod들이 실행되어야 한다.
일반 node들은 kubelet 데몬과 docker 데몬이 실행되고 있어야 하며, container의 묶음인 pod들이 실행되는 형태이다.

20/04/06 수정
minion이라고 불렸던 것은 아주 오래전의 얘기인 것 같다. 이 github issue에서 minion이라는 용어가 헷갈리므로 변경하자는 의견이 오고 갔다.

k8s 설치 방법

docker 설치

우선 k8s가 docker 기반의 container를 관리하므로, docker를 설치해야한다.
나는 이미 설치되어있어서 해당 과정을 생략했는데, docker 공식 홈페이지의 설치 방법을 참고하여 설치하면 된다.

k8s 설치

1
2
3
4
5
$ curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add
$ vi /etc/apt/sources.list.d/kubernetes.list
deb http://apt.kubernetes.io/ kubernetes-xenial main
$ apt-get update
$ apt-get install --no-install-recommends kubelet kubeadm kubernetes-cni

패키지 설치 시 –no-install-recommends 옵션은 해당 패지키가 실행되는 데 필요한 패키지만을 설치하겠다는 옵션이다.
잡다한 패키지까지 설치하는 게 싫어서 나는 항상 이 옵션을 주고 설치하는데, 저 옵션 없이 설치해도 무방하다.

kubelet 설정

swap 설정을 해주어야 한다.

1
2
3
$ sed -i '9s/^/Environment="KUBELET_EXTRA_ARGS=--fail-swap-on=false"\n/' /etc/systemd/system/kubelet.service.d/10-kubeadm.conf
$ systemctl daemon-reload
$ systemctl restart kubelet

master init

kubeadm init

kubeadm을 init해야한다.

1
$ kubeadm init --pod-network-cidr=10.244.0.0/16 --ignore-preflight-errors Swap

–pod-network-cidr=10.244.0.0/16은 flannel을 사용하기 위한 옵션이고, –ignore-preflight-errors Swap은 swap 에러를 무시하기 위한 옵션이다.
위 커맨드를 실행하면 To start using your cluster라며 아래와 같은 커맨드가 나열될 것이다.
하라는 대로 따라 치면 되겠다.

1
2
3
$ mkdir -p $HOME/.kube
$ cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ chown $(id -u):$(id -g) $HOME/.kube/config

flannel 적용

보통 pod의 network로 flannel을 많이 사용한다.

1
$ kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/v0.9.1/Documentation/kube-flannel.yml

master node에 schedule 가능하도록 설정

원래 master node에는 schedule하지 않는 것이 default 설정이다. 즉 master node에는 별도의 설정이 없으면 pod이 뜨지 않는다.
현재 나의 테스트 환경은 single-node cluster이므로, master에 schedule이 가능하도록 설정을 바꿔야 한다.

1
$ kubectl taint nodes --all node-role.kubernetes.io/master-

결과로 node ‘<hostname>‘ untainted가 나오면 성공이다.

trouble shooting

reboot시 node not ready 에러 발생

reboot 시 node not ready 에러가 발생했다.
에러는 다음과 같은 커맨드로 node 정보를 조회하여 확인하였고, 여기서 dev는 node 이름이다.

1
$ kubectl describe node dev

Ready False Tue, 15 May 2018 10:35:18 +0900 Tue, 15 May 2018 10:26:25 +0900 KubeletNotReady runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:docker: network plugin is not ready: cni config uninitialized

/etc/systemd/system/10-kubeadm.conf에서 KUBELET_NETWORK_ARGS line을 삭제했더니 정상 동작하였다.
해당 과정은 kubernetes github을 참고하였다.

swap 에러 발생

master init 과정에서 다음 커맨드를 실행했을 때 swap을 disable하지 않았다며 에러가 발생했다.

1
$ kubeadm init

[ERROR Swap]: running with swap on is not supported. Please disable swap
[preflight] If you know what you are doing, you can make a check non-fatal with --ignore-preflight-errors=...

위의 kubelet 설정 방법대로 따라하고, kubeadm init시 swap error를 ignore하는 설정을 잊지 않도록 한다.

마무리

이 포스팅에서는 k8s를 설치하는 방법에 대해서 알아보았다.
시작이 반이라지만 k8s를 설치한 것만으로 반을 했다고 볼 수는 없다ㅋㅋㅋ
k8s를 제대로 사용하려면 pod도 알아야하고 service와 deployment 등등 알아야할 것이 산더미이다.
시간이 되는 대로 내가 조사한 것에 대해 포스팅을 해야겠다.

회사에서 ceph object storage를 사용 중인데, object 업로드를 위해 frontend단에서 http OPTIONS가 성공하는지를 체크한다고 한다.
이 options가 성공하려면 bucket에 대해 cors를 활성화해주어야 하고, 이를 aws cli로 하면 간단하게 성공했지만 java 코드로 성공하기까지 매우 힘들었다.
(아직도 의문인 점은, 굳이 cors를 활성화하지 않아도 object 업로드는 된다는 점이다. options만 실패할 뿐…)
aws cli로 시도했을 때 –debug로 나온 결과가 실제 aws 가이드와 약간 다르기도 했고, aws signature 버전이 올라가면서 authorization을 생성하는 과정이 매우매우 복잡해졌기 때문이었다.
혹시나 이것 때문에 골치 아플 미래의 분을 위해 내가 며칠 동안 조사하고 테스트한 내용을 기록해두고자 한다.

우선 이번 포스팅에서는 aws s3, object storage, bucket, cors 등등의 개념을 간단간단하게 설명하고,
put bucket cors를 awscli로 하는 방법에 대해 설명하도록 하겠다.
원래는 AWSv4 Authorization 생성 방법까지 다루려 했는데, 쓰다보니 너무 길어져서 이건 다음에 포스팅해야겠다.

AWS S3란

AWS(Amazon Web Service)에서 제공하는 Object Storage로, Simple Storage Service를 줄여 S3 서비스라고 한다.
Object Storage는 rest api를 사용하여 object를 key-value로 관리하는 저장소라고 생각하면 된다.
Amazon에서는 S3에 대해 rest api와 각종 sdk, command line으로 조작하는 aws cli도 제공하고 있다.
sdk는 내부적으로 rest api를 사용하므로 다를 바가 없고, cli도 –debug로 확인해보면 별반 다르지 않음을 알 수 있다.
그러니까 결론은 편의성을 위해 여러 방법이 제공되지만 모든 조작은 rest api로 가능하다는 것이다.

Bucket이란

AWS S3의 경우 user당 object storage가 만들어지고, 각 object storage당 access key와 secret key가 발급된다.
이 때 두 key는 각각 id와 pw격으로 사용되며, aws authorization string을 생성할 때 필요하다.
Object storage를 생성했다고 해서 바로 object를 업로드할 수 있는 건 아니다.
물론 object storage는 원래 object를 flat하게 관리하지만, aws에는 bucket이라는 layer가 있다.
즉, object storage에 여러 개의 bucket을 만들고, 이 bucket에 object에 업로드하는 구조이다.

CORS란

CORS(Cross-Origin Resource Sharing)은 다른 도메인에 있는 서버의 url을 호출할 수 있도록 접근을 허용하는 것이다.
그러니까 cors를 설정하지 않았다면 원래는 같은 도메인에서만 접근이 가능하고, 다른 도메인에 접근하려면 보안 이슈가 발생하는 것이다.
aws의 경우 bucket에 대해 cors 활성화를 해주어야 다른 도메인에서도 접근할 수 있다.

Put bucket cors 방법

AWS cli를 사용하는 방법

  1. aws cli를 설치한다.

    1
    $ apt-get install --no-install-recommends awscli
  2. access key와 secret key를 설정한다.

    1
    2
    3
    4
    5
    $ aws configure
    AWS Access Key ID [None]: ${access-key}
    AWS Secret Access Key [None]: ${secret-key}
    Default region name [None]:
    Default output format [None]: json

    region은 설정 안해도 된다. aws configure 상에서는 공란으로 되어있지만, 실제 authorization 생성 시 default는 us-east-1로 사용한다.
    output format은 자유.

  3. bucket을 생성한다.

    1
    $ aws s3api create-bucket --endpoint-url ${endpoint-url} --bucket testbucket

    endpoint-url은 ceph의 object storage gateway ip를 적으면 된다.
    이걸 생략할 경우 s3.amazonaws.com으로 rest api를 전송한다.
    아 물론 region이 설정되어있을 경우 url이 달라질 수 있다.

  4. cors rule 지정

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    $ vi cors.json
    {
    "CORSRules": [
    {
    "AllowedOrigins": ["*"],
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["PUT", "GET", "DELETE"],
    "MaxAgeSeconds": 3000,
    "ExposeHeaders": ["x-amz-server-side-encryption"]
    }
    ]
    }

    PUT, GET, DELETE method에 대해 활성화해놓도록 지정하였다.
    object는 multipart upload가 아닌 이상 post를 사용하지 않기 때문이다.

  5. testbucket의 cors를 활성화시킨다.

    1
    $ aws s3api put-bucket-cors --bucket testbucket --cors-configuration file://cors.json --endpoint-url ${endpoint-url}

    이제 testbucket에 object를 업로드하고, 업데이트하고, 다운로드하고, 삭제할 수 있다.

AWSv4 Authorization 생성하는 방법과 이걸로 http request를 날려서 bucket cors를 활성화하는 방법은 다음 포스팅에…!
To be Continued…

코딩하다보면 무의식적으로 동일한 context root를 사용하게 될 때가 있다.
예를 들면 강의를 듣다가 예제 프로젝트를 모두 import해두고 하나씩 실행해볼 때라던가
그럴 때라던가… 그럴 때밖에 없나…
하지만 영문 모를 에러에 당황할 사람들을 위해 해결 방법을 공유해본다.

에러 내용

동일한 context root를 사용하는 웹 모듈(Project)를 서버에 동시에 올릴 경우 다음과 같은 에러가 발생한다.

Two or more Web modules defined in the configuration have the same context root (/ex). To start this server you will need to remove the duplicate(s).

여기에서 context root는 ex다.

해결 방법

Project 우클릭 > Run As > Run on Server를 클릭 후, Next를 눌러 다음 페이지로 넘어간다.

우측 목록에서 실행하기를 원하는 프로젝트만 남기고 다른 프로젝트를 Remove한다.

학생 때부터 필기를 좋아했다.
필기한 것의 90퍼센트는 나중에 다시 쳐다보지 않았지만(약간 저장강박증인 것 같기도 하다)
뭔가 듣고 남겼다는 것에 뿌듯함을 느끼곤 했다.
수업을 들으면서 갤럭시노트 10.1, 아이패드 미니에 필기를 해봤지만
결국엔 필기감이 종이를 따라올 수 없어서 A4 이면지를 애용했다.

회사 생활하면서도 이면지를 계속 쓰다가, 낱장이라는 한계에 부딪히게 되었다.
이면지가 섞이면 어느 순서로 봐야하는지도 모르겠고, 내가 언제한 필기인지도 몰라서
구석에는 항상 날짜와 몇 번째 종이인지 기록해둬야 했다. 으으 귀찮아

디지털과 아날로그의 중간 단계를 원했는데 부기보드 싱크가 바로 내가 찾던 그것이었다!

  1. 필기감이 종이에 가까운 것
  2. 휴대성이 좋은 것
  3. 저장이나 어플 연동이 되는 것
  4. 배터리는 충전식일 것

우리나라에서 구매하면 좀 많이 비싸서 아마존에서 해외직구로 구매하게 되었다.
첫 해외직구라 주소지가 한국이면 늦게 배송될 수도 있다는 말을 듣고 무서워서 배송대행지로 시켰는데,
지금 생각해보면 굳이 안그래도 됐었을 것 같다. 아까운 배송비…
무튼 아마존에서 부기보드는 $64.95, 세븐존 배송비 $12.5로 구매했다.

Boogie Board Sync 9.7


A4 정도의 크기에 펜을 굳이 따로 들고 다니지 않아도 부기보드에 끼우면 된다.
두께도 매우 얇아서 가볍고, 필기감이 너무 좋다.


부기보드에 필기한 모습이다.

Boogie Board Sync 앱

부기보드 싱크의 특장점! 바로 저장 및 연동이다.
상단의 SAVE버튼을 누르면 기기에 저장이 되고, 컴퓨터에 바로 연결해도 되고 어플을 설치하여 블루투스 연동할 수도 있다.


블루투스 연동으로 auto downlod 설정을 해놓으니 편하다.
그리고 어플은 드랍박스 혹은 에버노트로 추가적인 연동이 가능하다.


다운받은 노트는 NoteBooks 탭에서 확인이 가능하다.
비교적 깔끔하게 저장된 것을 확인할 수 있다.
그리고 가운데 탭(주황색 탭)을 누르면 부기보드에 필기하는 내용을 어플에서 라이브로 확인이 가능하다.

Summary

부기보드 싱크를 들고 가니 회사 사람들이 매우 신기해해서 뿌듯했다.
저장 기능이 있는 싱크 모델로 구매한 것은 정말 잘한 일이다!

부기보드들은 부분 지우기를 지원하지 않는 게 단점이다.
최근에 부분 지우기도 가능한 모델이 출시됐지만, 그 모델은 좀 크고 싱크처럼 저장 및 연동이 되지 않아서 패스.
부분 지우기가 생각보다 필요하지 않아서 나에겐 별 문제가 되진 않았다.

부기보드 싱크 살까말까 고민하고 계신다면 강추!
실제로 주변에도 추천하고 다니는 중이다ㅋㅋㅋ

환경 구성

  • JDK 8
    • 9는 지원이 끊겼고, 10으로 했을 때 eclipse 실행 시 에러가 발생했기 때문에 안전하게 8로 설치함
    • oracle 계정이 있으면 9를 설치할 수 있는 모양인데, 굳이…
  • Eclipse EE oxygen
  • Tomcat 8.5.31

에러 내용

Tomcat을 단독으로 실행했을 때는 localhost:8081에 페이지가 정상적으로 로드되었는데,
Eclipse에 server로 tomcat을 추가하여 실행하면 자꾸 다음과 같은 에러가 발생했다.

The origin server did not find a current representation for the target resource or is not willing to disclose that one exists.

해결 방법

Eclipse에서 server를 더블 클릭하면 server overview가 나온다.

이 때 다음 항목을 변경해야 한다.(HTTP 포트 번호는 굳이 변경하지 않아도 된다.)

server path를 실제 Tomcat이 설치된 경로로 사용하라고 Use Tomcat installation으로 변경하고,
deploy path를 실제 Tomcat이 설치된 경로의 webapps 디렉토리로 변경한다.

저장한 뒤 다시 start하면 다음과 같이 예쁘게 tomcat 페이지가 로드된다.

참고 사이트

https://stackoverflow.com/questions/20235909/tomcat-is-not-working-in-eclipse

ubuntu에서 sudo를 사용할 때마다 다음과 같은 문구가 뜨는 경우가 있다.

1
sudo: unable to resolve host

명령을 실행하는 데에 문제가 되는 것 같지는 않지만 매우 거슬리므로 해결해보도록 하자.

hostname

hostname은 네트워크에 연결된 장치에 부여되는 고유한 이름이다.

ubuntu desktop 버전을 설치할 때 user를 생성하다보면 계정명과 pc이름처럼 이상한 것이 결합되어 길게 hostname이 붙곤 한다.
이 hostname은 terminal의 명령행에 계속 노출되므로 길면 거슬리기 마련이라, 필자는 보통 dev로 변경하는 편이다.

/etc/hostname과 /etc/hosts

ubuntu의 hostname을 변경하려면 총 두 개의 파일을 수정해야한다.
결론부터 말하자면 unable to resolve host 문구가 출력되었던 이유는 이 두 파일의 내용이 달랐기 때문이었다.

에러가 발생하는 상황에서 두 파일을 비교해보면 다음과 같다.

1
2
3
4
5
6
$ cat /etc/hostname
dev
$ cat /etc/hosts
127.0.0.1 localhost
127.0.1.1 jwcheong-15U530-GH5HK
#(이하 생략)

/etc/hostname에는 dev라고 나와있는 반면, /etc/hosts에는 변경 전의 hostname이 쓰여있었다.
예전에 hostname을 바꿀 때 /etc/hostname만 변경한 모양이다.
계정 생성 단계에서 바꿨으면 쉬웠 읍읍

/etc/hosts에서 예전 hostname을 지우고 현재 hostname으로 변경해주면 더 이상 문구가 출력되지 않는다.

1
2
3
4
$ vi /etc/hosts
127.0.0.1 localhost
127.0.1.1 dev
#(이하 생략)

reboot할 때마다 script를 실행시킬 일이 생겨서 cron을 사용하게 되었는데, 삽질을 하면서 알게된 내용을 기술하고자 한다.
여러 가지가 엮여있는 내용이라 좀 장황하므로, 맨 마지막 결론만 읽어도 무방하다.

cron

cron은 crontab에 예약된 작업을 반복적으로 실행시키는 데몬이다.
cron은 ubuntu 기본 패키지이므로 별도의 설치과정이 필요없다.
다음의 명령어를 입력하여 작업을 등록할 수 있다.

1
$ crontab -e

작업을 등록할 때는 실행될 주기, 실행하고자하는 명령어나 파일을 적으면 된다.
reboot할 때마다 /root/cron/test.sh를 실행하고 싶다면 아래와 같이 작업을 등록한다.

1
@reboot /root/cron/test.sh > /root/work/cron/cron.log 2>&1

매번 껐다 켤 수 없으므로 테스트할 때는 매분 실행되도록 하였다.

1
* * * * * /root/cron/test.sh > /root/work/cron/cron.log 2>&1

*은 every의 의미로, 맨 왼쪽부터 ‘분 시간 일 월 년’을 의미한다.

문제가 되었던 cron test 과정

문제는 여기에서 발생했다.
테스트 코드가 매우 복잡했던 관계로 대략적인 뼈대만 적자면, bash script 안에서 flock으로 또 다른 명령어들을 실행하는 구조였다.

1
2
3
4
#!/bin/bash
if [[ -d /path/to/lock/dir ]];then
flock -x -o /path/to/lock/file -c 'if [[ /test/something ]]; then echo "blah"; fi;'
fi

다음과 같은 에러가 발생했다.

1
/bin/sh: 1: /bin/sh: [[: not found

/bin/bash로 실행하라고 shebang(#!/bin/bash)을 넣었음에도 /bin/sh로 실행되었다는 것에 의문을 품게 되었다.

cron은 default shell로 작업을 실행시킨다

수차례 테스트한 결론부터 말하자면, cron은 default shell 즉, $SHELL -c ‘등록된 작업’을 실행시킨다.
이 때 $SHELL은 기본적으로 /bin/sh이다.
이는 비단 cron뿐 아니라 flock도 그런 것으로 보이는데, 아래의 테스트 결과에서 알 수 있다.

1
2
#!/bin/bash
flock -x -o /root/work/cron/lock -c 'ps -ajxf | grep -C3 cron'

cron이 위 스크립트를 실행하게 한 결과는 다음과 같다.(불필요한 라인은 삭제했다.)

1
2
3
4
5
6
7
8
9
root@dev:~/work/flock# vi qcreate_shell-nonshebang.log
1 892 892 892 ? -1 Ss 0 0:00 /usr/sbin/cron -f
892 4018 892 892 ? -1 S 0 0:00 \_ /usr/sbin/CRON -f
4018 4019 4019 4019 ? -1 Ss 0 0:00 \_ /bin/sh -c ~/work/cron/test.sh >> ~/work/cron/test.log 2>&1
4019 4020 4019 4019 ? -1 S 0 0:00 \_ /bin/bash /root/work/cron/test.sh
4020 4021 4019 4019 ? -1 S 0 0:00 \_ flock -x -o /root/work/cron/lock -c ps -ajxf | grep -C3 cron
4021 4022 4019 4019 ? -1 S 0 0:00 \_ /bin/sh -c ps -ajxf | grep -C3 cron
4022 4023 4019 4019 ? -1 R 0 0:00 \_ ps -ajxf
4022 4024 4019 4019 ? -1 S 0 0:00 \_ grep -C3 cron

우선 cron은 /bin/sh -c ‘명령’으로 실행시킨다.
/bin/sh은 -c의 인자로 온 ‘명령’을 새로운 프로세스를 띄워서 실행시키며, 이 때 shebang을 인식하여 알맞는 shell로 실행시킨다.
flock 역시 /bin/sh -c ‘명령’으로 실행시키는 것을 알 수 있다.

이젠 crontab에 SHELL을 /bin/bash로 등록한 뒤, 똑같은 작업을 수행해보았다.

1
2
3
$ crontab -e
SHELL=/bin/bash
* * * * * /root/cron/test.sh >> /root/cron/test.log 2>&1

아래는 실행 결과다.(역시 불필요한 라인은 삭제했다.)

1
2
3
4
5
6
7
8
   1   892   892   892 ?           -1 Ss       0   0:00 /usr/sbin/cron -f
892 4099 892 892 ? -1 S 0 0:00 \_ /usr/sbin/CRON -f
4099 4100 4100 4100 ? -1 Ss 0 0:00 \_ /bin/bash -c ~/work/cron/test.sh >> ~/work/cron/test.log 2>&1
4100 4101 4100 4100 ? -1 S 0 0:00 \_ /bin/bash /root/work/cron/test.sh
4101 4102 4100 4100 ? -1 S 0 0:00 \_ flock -x -o /root/work/cron/lock -c ps -ajxf | grep -C3 cron
4102 4103 4100 4100 ? -1 S 0 0:00 \_ /bin/bash -c ps -ajxf | grep -C3 cron
4103 4104 4100 4100 ? -1 R 0 0:00 \_ ps -ajxf
4103 4105 4100 4100 ? -1 S 0 0:00 \_ grep -C3 cron

cron도 flock도 -c 다음에 오는 명령을 이번에는 /bin/bash로 실행시켰다!

ubuntu의 default shell

똑같은 스크립트를 필자가 실행했을 때와 cron이 실행했을 때 결과가 달랐던 것은, 결국 실행하는 shell이 달랐기 때문이었다.

ubuntu의 경우 default shell이 /bin/sh이며, 별다른 수정을 하지 않았다면 dash(bourne shell)를 가리킨다.

1
2
root@dev:~/# ll /bin/sh
lrwxrwxrwx 1 root root 4 4월 10 14:43 /bin/sh -> dash*

그렇기 때문에 테스트 과정에서 cron과 flock은 ubuntu의 default shell인 /bin/sh로 명령어를 실행한 것이다.
이 때 /bin/sh -c ‘명령어’는 ‘명령어’를 실행하기 위해 새로운 프로세스를 띄우며, loader가 shebang을 확인한다.
(필자는 굳이 flock -c의 인자로 스크립트 내용을 줌으로써 삽질을 했던 것이었다.
처음부터 flock -c ‘#!/bin/bash가 추가된 스크립트 파일’을 실행하게 했으면 겪지 않았을 문제였다…!)

반면 user의 default login shell이 bash이기 때문에, 필자는 shebang을 추가하지 않고도 bash로 스크립트를 실행할 수 있었던 것이다.

1
2
root@dev:~/work/flock# cat /etc/passwd
root:x:0:0:root:/root:/bin/bash

결론

돌고 돌아왔으나, 이번 테스트로 함축되는 내용은 다음 네 가지이다.

  1. ubuntu의 default shell은 /bin/sh이며, 이는 dash를 가리킨다.
  2. 스크립트를 실행하면 loader가 맨 첫 줄의 shebang을 보고 알맞은 shell을 실행시킨다.
  3. user의 default login shell은 /bin/bash이다.
  4. cron으로 bash script를 실행하고 싶다면, 스크립트에 shebang을 추가하거나 crontab에 SHELL=/bin/bash를 추가해야 한다.(이는 cron을 떠나서 추가해주는 것이 정신건강에 좋다.)