Dockerで簡単構築！Common Lisp学習環境

この記事では、Dockerを用いてCommon Lispの学開環境を構築する方法について書きます。

次の①〜④の流れで進めます。

　① Dockerについて

　② 利用するDockerfileについて

　③ イメージの取得とコンテナの起動

　④ Common Lisp製アプリケーションの実行


## ① Dockerについて

Dockerについては、以下の記事が参考になります。

[**・Dockerについてなるべくわかりやすく説明する / kasanaxnさん**](https://qiita.com/rawHam/items/80ba13d2d2d56dba411e)

[**・今更Docker入門 - コンテナ化することで何が嬉しいか / zgmf_mbfp03さん**](https://qiita.com/zgmf_mbfp03/items/9e515124217e4f081b6f)

この記事でのDockerの使い方を要約すると、`docker pull`でイメージのダウンロード後、`docker run`でコンテナの作成と起動をして、Common Lispの学習環境を構築する流れです。コンテナを起動すると、Caveman2やLem等のCommon Lisp製アプリケーションを実行することができます。

では、Dockerをインストールして次に進んでください。OSはmacOSかUbuntuを想定しています。Windowsのユーザの方は、Virtual BoxにUbuntuを入れて進めてください。


[**Docker for Mac のインストール手順**](https://docs.docker.com/docker-for-mac/install/)
[**Docker CE for Ubuntu のインストール手順**](https://docs.docker.com/install/linux/docker-ce/ubuntu/)

## ② 利用するDockerfileについて

今回は、Alpine Linuxをベースに、Common Lispの環境構築のためにRoswell、エディタとしてLem、notebook形式の開発環境としてDarkmatter、WebアプリケーションフレームワークとしてCaveman2がインストールされたイメージを作成するDockerfileを作成します。Dockerfileはeshamsterさんの[cl-base](https://github.com/eshamster/docker-cl-base)を元に作成しました。

```Dockerfile
FROM frolvlad/alpine-glibc:alpine-3.6

ARG work_dir=/tmp/setup
RUN mkdir ${work_dir} && \
    chmod 777 ${work_dir}

# --- Roswellのインストール --- #
RUN apk add --no-cache git automake autoconf make gcc build-base curl-dev curl glib-dev libressl-dev ncurses-dev sqlite libev-dev su-exec libpq postgresql-client postgresql-dev postgresql-contrib && \
    mkdir /docker-entrypoint-initdb.d && \
    chmod +x docker-entrypoint.sh && \
    cd ${work_dir} && \
    git clone --depth=1 -b release https://github.com/roswell/roswell.git && \
    cd roswell && \
    sh bootstrap && \
    ./configure --disable-manual-install && \
    make && \
    make install && \
    cd .. && \
    rm -rf roswell && \
    ros run -q

# --- .roswell/binにPATHを通す --- #
ENV PATH /root/.roswell/bin:${PATH}

# --- Caveman, Lem, Darkmatter をインストールする--- #
RUN ln -s ${HOME}/.roswell/local-projects work && \
    ros install fukamachi/clack && \
    ros install fukamachi/caveman && \
    ros install cxxxr/lem && \
    ros install tamamu/darkmatter && \
    ros install Shinmera/dissect && \
    ros install fukamachi/qlot && \
    ros install fukamachi/utopian && \
    mv ${HOME}/.roswell/bin/lem ${HOME}/.roswell/bin/lem2 && \
    mv ${HOME}/.roswell/bin/lem-ncurses ${HOME}/.roswell/bin/lem

# --- Webアプリケーションにアクセスできるようにポートを開ける --- #
EXPOSE 8888
```

## ③ イメージの取得とコンテナの起動

では、[**このDockerfileでビルドしたイメージ**](https://hub.docker.com/r/tcool/cl-alpine/)を用いて実践に移ります。

まず、Dockerを起動してください。Dockerの起動後、`docker pull`でイメージをダウンロードします。

```bash
$ docker pull tcool/cl-alpine
```

次に、コンテナの作成と起動を行います。イメージを元にcl-alpineという名前でコンテナを作成後、ホストOSのポート8888番へのリクエストをコンテナのポート8888番につなぎます。`-it`オプションを使うことで、シェルを対話的に実行することができます。

```bash
docker run -it -p 8888:8888 --name cl-alpine tcool/cl-alpine
```

## ④ Common Lisp製アプリケーションの実行

### Roswellについて

Roswellを使うと、`ros install <Githubアカウント名/レポジトリ名>`でLispアプリケーションをインストールできます。試しに、JSONのエンコードとデコードを行うライブラリ [**Jonathan**](http://rudolph-miller.github.io/jonathan/overview.html)をインストールしてみましょう。

```ash
#\ ros install Rudolph-Miller/jonathan
Installing from github Rudolph-Miller/jonathan
To load "jonathan":
  Load 1 ASDF system:
    jonathan
; Loading "jonathan"
..To load "proc-parse":
  Load 2 ASDF systems:
    alexandria babel
(中略)
up to date. stop
$ 
```

`ros run`とすると、REPLを起動することができます。

```ash
#\ ros run
* 
```

先ほどインストールしたJonathanをREPLから利用するには、`ql:quickload`でJonathanを読み込み、Jonathanの後に`:コロン`をつけて関数を実行します。試しに、リストをjsonにエンコードする`to-json`関数を使ってみます。

```ash
* (ql:quickload :jonathan)
To load "jonathan":
  Load 1 ASDF system:
    jonathan
; Loading "jonathan"
....
(:JONATHAN)
* (jonathan:to-json '(:name "Common Lisp" :born 1984 :impls (SBCL KCL)))
"{\"NAME\":\"Common Lisp\",\"BORN\":1984,\"IMPLS\":[\"SBCL\",\"KCL\"]}"
```

REPLで`(quit)`と打つと、REPLから抜けることができます。

```ash
* (quit)
```

また、`ros use <処理系/バージョン>`とすると、Common Lispの処理系、バージョンを切り替えることができます。例えば、`ros install sbcl/1.2.3`の後`ros use sbcl/1.2.3`とすると、デフォルトでSBCLのversion 1.2.3を用いるように指定できます。

以上のように、RoswellはCommon Lispの環境構築を行うために便利なユーティリティーです。

### 1. Lem

Lemを起動します。

```ash
#\ lem
```

起動後、`M-x slime`でSlimeを起動します。`M-x`は、「escキーかOptionキーを押しながらXを押す」という意味です。次のような画面になります。
<img width="1437" alt="スクリーンショット 2018-12-07 21.40.31.png" src="https://qiita-image-store.s3.amazonaws.com/0/292473/68aecfff-f42c-4b73-fa73-e900758b653a.png">

Lemのキーバインドは、Emacsライクなものとvi-modeがあります。

vi-modeを利用される場合は、 `~/.lem/init.lisp`に次のように設定を書いてください。

```
(lem-vi-mode:vi-mode)
```


デフィルトのキーバインドは`M-x describe-bindings`で確認できます。Emacsのキーバインドに慣れている方は、ほぼそのまま利用できます。`C-x`は、`Controlを押しながらx`、`M-x`は、`Esc(またはAlt)を押しながらx`、`C-x o`は`C-xのあとキーボードから指を離して、o`を打ちます。

基本操作は、以下の通りです。

| コマンド | 機能 |
|:---|:---|
| M-x lisp-mode | Lisp編集モードに変更 |
| M-x start-lisp-repl | 同じプロセスでslimeを開始 |
| M-x slime | 別のプロセスでslimeを開始 |
| C-x C-f | 新規ファイルの作成、diredの起動 |
| C-x o または M-o | バッファの移動 |
| C-x k | バッファの削除 |
| C-@ | 選択開始 |
| C-w | カット |
| M-w | コピー |
| C-y | ペースト |

Lemの使用法についてはこちらをご参照ください。

[![IMAGE ALT TEXT HERE](http://img.youtube.com/vi/YkSJ3p7Z9H0/0.jpg)](http://www.youtube.com/watch?v=YkSJ3p7Z9H0)


### 2. Darkmatter

Darkmatterを読み込み、サーバを起動します。

```ash
#\ ros run
* (ql:quickload :darkmatter)
* (darkmatter:start :server :hunchentoot :port 8888)
```

サーバの起動後、[http://localhost:8888](http://localhost:8888)にアクセスすると、Darkmatterを利用できます。

![as.png](https://qiita-image-store.s3.amazonaws.com/0/292473/8db3d0d9-6ad6-6def-5fe1-f25c7ce4c369.png)

Darkmatterに関する詳しい情報は、[darkmatterのレポジトリ](https://github.com/tamamu/darkmatter)をご参照ください。

### 3. Caveman2

最後に、Common Lisp製のWebフレームワーク「Caveman2」を使ってみます。プロジェクト雛形の生成機能を用いて、myappという名前でプロジェクトを生成します。

```ash
#\ ros run
* (ql:quickload :caveman2)
* (caveman2:make-project #P"/path/to/myapp/"
                         :author "<Your full name>")
;-> writing /path/to/myapp/.gitignore
;   writing /path/to/myapp/README.markdown
;   writing /path/to/myapp/app.lisp
;   writing /path/to/myapp/db/schema.sql
;   writing /path/to/myapp/shlyfile.lisp
;   writing /path/to/myapp/myapp-test.asd
;   writing /path/to/myapp/myapp.asd
;   writing /path/to/myapp/src/config.lisp
;   writing /path/to/myapp/src/db.lisp
;   writing /path/to/myapp/src/main.lisp
;   writing /path/to/myapp/src/view.lisp
;   writing /path/to/myapp/src/web.lisp
;   writing /path/to/myapp/static/css/main.css
;   writing /path/to/myapp/t/myapp.lisp
;   writing /path/to/myapp/templates/_errors/404.html
;   writing /path/to/myapp/templates/index.tmpl
;   writing /path/to/myapp/templates/layout/default.tmpl
```

サーバを起動するには、`(プロジェクト名:start :port ポート番号)`とします。例えば、myappというプロジェクトで8888番ポートでサーバを起動するには、次のようにします。

```
* (myapp:start :port 8888)
```

ブラウザで[http://localhost:8888](http://localhost:8888)にアクセスをすると、"Hello, Caveman2!"と表示されます。

### デプロイ

ここまでは、ローカルマシン上のDockerで作業を行ってきましたが、次はVPS上にDockerをインストールして環境構築してみます。ここではAWSのLightsailを用います。AWSにアカウントを作成後、次の初期化スクリプトを用いて、Ubuntuのインスタンスを作成、dockerとイメージをインストールしてください。

```bash
sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates
sudo apt-key adv --keyserver hkp://ha.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D
echo "deb https://apt.dockerproject.org/repo ubuntu-xenial main" | sudo tee /etc/apt/sources.list.d/docker.list
sudo apt-get update
sudo apt-get install -y linux-image-extra-virtual
sudo apt-get install -y docker-engine
sudo service docker start
```

完了後、WebコンソールからSSH接続をして、次のコマンドを打てば、コンテナの中に入れます。

```bash
$ sudo docker pull tcool/cl-alpine
$ docker run -it -p 8888:8888 --name cl-alpine tcool/cl-alpine
```

ローカルのTerminalからSSHでサーバに接続するには、インスタンスの設定ページからpemファイルをダウンロードして、次のように接続します。

```
ssh -i <pemファイルのパス> ubuntu@http://<固定のグローバルIPアドレス>
```

### コンテナの再起動

コンテナを停止した場合、再度ashにログインするには、コンテナを再起動する必要があります。起動中のコンテナを確認するには、`docker ps`を実行します。

```bash
docker ps

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                    NAMES
```

このようにコンテナが起動していないときには、`docker ps -a`でコンテナのIDを確認します。

```bash
$ docker ps -a
CONTAINER ID     IMAGE            COMMAND          CREATED          STATUS           PORTS                  NAMES
1d5659fdb4f3     tcool/cl-alphine   "/bin/bash"      31 minutes ago   Up 31 minutes    0.0.0.0:8888->8888/tcp cl-alpine
```

その後、`docker start <コンテナID>`でコンテナを起動します。

```bash
$ docker start 1d5659fdb4f3 
```

`docker exec -it <コンテナ名> /bin/ash`でコンテナのashにログインできます。ashを起動すると、作業の続きに戻ることができます。

```bash
docker exec -it 1d5659fdb4f3 /bin/ash 
```

## まとめ

以上の方法を使うと、簡単にCommon Lispの開発環境を試すことができます。本番環境に適したDBやSSLの対応をしていないので本番運用では使えませんが、手軽にCommon Lispの学習環境を構築するには良いかと思います。

次回は、Docker Composeを用いてDBのコンテナと連携させながらLispアプリを運用したり、AWS Lambdaのカスタムランタイムを使ってCommon Lispの関数を呼び出したりしてみたいと思います。