20200323のdockerに関する記事は16件です。

モダンなLinuxコマンド(Rust製)を Docker上で試す

ls よりも exa を使おう!モダンな Linux コマンド達を紹介」の記事を読んで、他にも色々コマンドあるんじゃないかと思って片っ端からインストールしてみました。
※当記事はdockerがインストールされている前提です。また、Windows10環境で動作確認しています。

docker-compose.yml

version: '2'
services:
  modern_linux:
    build: .
    container_name: modern_linux
    volumes:
      - ./home:/home
    tty: true

Dockerfile

From ubuntu:19.10

RUN apt-get update && \
    apt-get install -y software-properties-common && \
    add-apt-repository ppa:dawidd0811/neofetch
RUN apt-get update &&\
    apt install -y curl \
    gcc \
    git \
    neofetch \
    bat \
    fd-find \
    ripgrep
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
RUN $HOME/.cargo/bin/cargo install exa \
    hexyl \
    procs \
    tokei \
    dutree \
    lsd

RUN echo $'\n\
alias cargo="$HOME/.cargo/bin/cargo"\n\
alias exa="$HOME/.cargo/bin/exa"\n\
alias hexyl="$HOME/.cargo/bin/hexyl"\n\
alias procs="$HOME/.cargo/bin/procs"\n\
alias tokei="$HOME/.cargo/bin/tokei"\n\
alias dutree="$HOME/.cargo/bin/dutree"\n\
alias lsd="$HOME/.cargo/bin/lsd"\n\
' >> /root/.bashrc

コマンドを実行する

docker-compose up -d
docker-compose exec modern_linux /bin/bash
#コンテナ内部に入るので、インストールされたコマンドを試してみる。
# 終わったら、exit
docker-compose down

まとめ

Rust製のコマンドはカラフルなものが多くていいですね!
あと何と言ってもちょっと試してみたいときにごにょごにょインストールして、コマンド実行して、終わったら消せるDockerって素晴らしいね。。

参考url

https://qiita.com/navitime_tech/items/c249269a3b47666c784b

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Visual Studio Code Remote Development で ~/.bash_profile (~/.profile) が読み込まれなかった

先日、個人的なPython学習のための環境を作ろうと思った。

自分のマシンの環境を汚したくないし、Dockerで環境を作って、Visual Studio CodeのRemote Developmentでコードを書くことにした。

せっかくPythonの環境を作るのだからパッケージマネージャーのPoetryでライブラリをインストールしてやろうと考えた。

しかし、DockerコンテナにRemote Developmentで接続後にVisual Studio Codeのコンソールでpoetryを実行するとpoetry: command not foundが表示される。

困った。

結論

先に結論を書いておくとVisual Studio CodeでLinux環境のターミナルを起動した時、デフォルト設定のままではログインシェルを起動しないので
~/.bash_profile~/.profileが読み込まれない。

ログインシェルを起動したいのであれば"terminal.integrated.shellArgs.linux": ["-l"]settings.jsonに記述しなければいけない。

何がやりたかったか

「Docker使うのに何でPoetry使うの?」と言われそうなのでPoetryを使う理由を先に書いておく。

残念ながら私が身を置いている環境ではDockerがまったくと言っていいくらいに使われていない。
もし、業務でアプリケーションを開発する際にPythonを使う機会があってもDockerを使える可能性は低いと考えられる。

この理由に基づいてPythonと一緒にパッケージマネージャーの使い方も学んでやろうと考えて、
PoetryをDockerコンテナの中で使うことにしました。

問題の仮想環境構成ファイル

Remote Development用に作成したファイルは、関連個所を抜粋すると大体以下のような内容だった。

.devcontainer/devcontainer.json
{
    "name": "Python 3",
    "dockerComposeFile": [
        "../docker-compose.yml",
        "./devcontainer.extend.yml"
    ],
    "service": "python",
    "workspaceFolder": "/workspace",
    "settings": {
        "terminal.integrated.shell.linux": "/bin/bash",
        "python.pythonPath": "/usr/local/bin/python",
        "python.linting.pylintPath": "/usr/local/bin/pylint",
        "python.linting.pylintEnabled": false,
        "python.linting.flake8Enabled": true,
        "python.linting.mypyEnabled": true,
        "editor.formatOnSave": true,
        "python.linting.lintOnSave": true
    },
    "extensions": [
        "ms-python.python"
    ],
    "shutdownAction": "stopCompose"
}

docker-compose.yml
version: "3"

services:
  python:
    image: python
    container_name: python
    build:
      context: ./docker/python
      dockerfile: Dockerfile
.devcontainer/devcontainer.extend.yml
version: "3"

services:
  python:
    volumes:
      - ./:/workspace:cached
docker/python/Dockerfile
FROM python:3.8.1-slim

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
    apt-utils \
    gcc \
    build-essential \
    curl \
    libpq-dev \
    && apt-get autoremove -y \
    && apt-get clean -y \
    && rm -rf /var/lib/apt/lists/*

RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python

このような環境でRemote DevelopmentでDocker Compose upしていたが、
Visual Studio Codeのターミナルでpoetryを実行しようとするとcommand not foundが表示される。

この時のPoetryはバージョン 1.0.5で、公式推奨のインストール方法に従って
curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | pythonを実行していた。

get-poetry.pyPATHpoetryを追加しているのだろうと中身を確認したところ
~/.bash_profile~/.profilePATH~/.poetry/binを追加していた。

ターミナルでcat ~/.profileして中身を確認すると、export PATH="$HOME/.poetry/bin:$PATH"はちゃんと書き込まれている。

「なんでpoetrycommand not foundになるんだ?」と考えながらsource ~/.profileを実行すると、
それ以降poetryコマンドは実行できてしまう。

手順に間違いがあるのだろうと考え、比較のために非推奨のpip install --user poetryでインストールを行うも、
こちらもpoetryコマンドは正常に実行できてしまう。

ここから「どこかに間違いや勘違いがあるからPATHが通っていない」と思いながら四苦八苦を開始する。
この時、自分が使い慣れていないDocker周りに勘違いがあるのだろうという思い込みの元、
DockerfileRUN . ~/.profileを書いてみたりするなど試して間違いを重ねて時間を浪費する。

何が間違っているか分からないまま小一時間が経過し、嫌になってきたので「PATH通せばいいや」と考え以下の変更を追加。

Dockerコンテナビルドして、poetryコマンドが実行できることを確認。

docker/python/Dockerfile
FROM python:3.8.1-slim

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
    apt-utils \
    gcc \
    build-essential \
    curl \
    libpq-dev \
    && apt-get autoremove -y \
    && apt-get clean -y \
    && rm -rf /var/lib/apt/lists/*

RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python
+ # The path of poetry is added to PATH.
+ ENV PATH="/root/.poetry/bin:${PATH}"

普段からDockerをバリバリ使っているわけでは無いし、時間もあまり使いたくなかった(この時点で一時間使っているけど)。

原因究明は一旦あきらめて、Poetry周りの環境構築はとりあえずOKということにしておいた。

数日後

作業中にふと閃く、「cronでありがちなログインシェルを経由していないせいで発生する~/.bash_profile経由しないとコマンドが動作しない現象と同じだ・・・」。

「Visual Studio CodeがRemote Developmentしている時のシェル設定はどこだ」と思ったが、
自分で.devcontainer/devcontainer.json"terminal.integrated.shell.linux": "/bin/bash"を書いていることに気が付く。

しかし、該当設定値はシェルの指定だけのようで/bin/bashを指定しているが、-lオプションの指定は別の設定の様だった。
Visual Studio CodeのSettingsを開くと、"terminal.integrated.shellArgs.linux": []が見つかる。

「デフォルト設定でログインシェル読み込まないのか」と思っていると、
Mac OSは"terminal.integrated.shellArgs.osx": ["-l"]がデフォルトになっている。

念のためにドキュメントを確認すると__親切に書いてる。

Integrated Terminal in Visual Studio Code

You can pass arguments to the shell when it is launched.

For example, to enable running bash as a login shell (which runs .bash_profile), pass in the -l argument (with double quotes):

// Linux
"terminal.integrated.shellArgs.linux": ["-l"]

.devcontainer/devcontainer.json
terminal.integrated.shellArgs.linux": ["-l"]を追加し、
Dockerfileに追記したENV PATH="/root/.poetry/bin:${PATH}"を削除。

DockerコンテナリビルドしてRemote Development起動し、poetryコマンドにPATHが通っている事を確認。

そして、ここまで事実確認ができたことで、これはLinux OSでVisual Studio Codeのターミナル使ったときに、
いつでも発生する問題なのでは無いかと思い当たり、検索をかけるといくつも事例が出てくる。

完全に自分の見落としだった。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kubernetesについて調べてみた!!!

Kubernetes(クバネティス・クーベネティス)とはなんなのか?

コンテナの管理を自動化するためのソフトウェアで、コンテナ・オーケストレーション・ツールと呼ばれています。Kubernetesは、Googleが開発しました。Googleは、以前からインターネットサービスを提供するためにマイクロサービスの基盤を利用していました。Kubernetesは、このマイクロサービスの技術をオープンソースソフトウェアとしてまとめて公開したものです。現在はCNCF(Cloud Native Computing Foundation)がKubernetesとその周辺ツールの開発、管理を行っています。

昨今、注目されるようになったコンテナ技術のディファクトスタンダード(事実上の標準)となったDocker
をさらにコンテナ管理するソフトウェアのディファクトスタンダードとなったものがKubernetesです。

ここから、出来るだけ噛み砕いていきます。

始まりは、クラウド技術です。
最近はAWSやAzure・GCPというクラウドサービスの提供事業の利用が主流となってきました。

クラウドといっても
・Saas・Paas・Iaas

がありますが、ここではIaas(サービスとしてインフラストラクチャ)が該当すると思います。
クラウドのメリットは、費用が安かったり、サービス拡張に伴い増設が用意であったり、可用性・信頼性・保守性という部分もサービス事業者(AWSとか)が担保してくれるので、それ以外の部分に集中できるという様々なメリットがあると思います。
詳しくは、こちら

ほとんどのサービス提供がクラウドでの開発かクラウドへの移行が行われています。

そうなってくると、今度はクラウドでの開発を前提としての設計思想が推奨されるようになってきました。
それが、クラウドネイティブというものです。
クラウドネイティブについて

クラウドネイティブたる要素はいくつかあります。

  • コンテナ
  • マイクロサービス
  • オーケストレーション
  • サービスメッシュ 等の技術です。 これはCNCF(Linuxとか表題のKubernetesとかを管理している団体です)が大体こんな感じだよ!的な定義を出しています。

これらは、特定のソフトウェアを指しているものではありません。コンテナ技術といったらDockerというイメージですが、Docker以外もコンテナ技術は沢山ありました。そんな覇権争いを勝ち抜き、オーケストレーションといったらに該当するのがKubernetesです。

どんな技術?

コンテナは、アプリケーション層での仮想環境でプログラムを動作させる技術です。
一つのOS上で動作できるため、非常に軽量かつCPUやメモリのリソースを抑えられます。
また、dockerのコンテナはDockerイメージというものがベースとなります。DockerイメージにはOSのバージョンやミドルウェアというものがあらかじめ定まっている為、開発環境・テスト環境・本番環境による差異が生まれにくく、アプリケーションがいざ本番環境だと動かない!!!的な問題が起きにくいです。

マイクロサービスとは、クラウドの拡張性を前提に、アプリケーションを構成要素ごとに分割して、設計し各コンテナ上に実行環境を構築しようという考え方です。マイクロサービスはソフトウェアではなくアーキテクチャ(設計思想)に当たると思います。機能ごとに細分化するので、追加機能を増やしり、一つの機能がダメになった際に全部のシステムが止まってしまうということを避けられます。

オーケストレーションは、マイクロサービスを前提としてコンテナがどんどん増えていくと、それを管理するのが大変になっていきます。。
ですので、あらかじめコンテナやノードの理想の状態を人間が定義して、あとは自動でその状態を維持してくるものがオーケストレーションツールです。今回のKubernetesがこれになります。

サービスメッシュとは、各機能に分割されたアプリケーションを紐付け、一つのサービスとして管理してくるものです。

これらの技術で、クラウドサービスは成り立っているようです。

参考:https://www.sbbit.jp/article/cont1/35564

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laradockで簡単にLaravel環境構築

前提

  • Mac環境で進めていきます
  • Dockerが既にインストールされている
  • gitがインストールされている

フォルダ作成

まずは適当なファイルを作りましょう

$ mkdir docker_laravel 

次に作ったフォルダに移動します

$ cd docker_laravel

このフォルダにLaradockを構築していきます

Laradockをcloneする

では早速Laradockを先ほど作ったフォルダにcloneします
下記を実行してください

$ git clone https://github.com/LaraDock/laradock.git

上記でdoker_laravellaradockフォルダが作成されます
laradockフォルダに移動します

$ cd laradock

laradockに.envファイルを作成します

$ cp env-example .env

これで、laradockフォルダに.envが作成されました

Laravelの作成

次にLaravelプロジェクトを作成していきます
workspaceコンテナを動かします
このworkspaceコンテナにcomposerやlaravelなどが含まれており
artisanコマンドやnpmなども実行できます
workspaceコンテナを動かしコンテナ内に入ることで上記コマンドも実行できます

コンテナを動かす

$ docker-compose up -d workspace

コンテナに入る

$ docker-compose exec --user=laradock workspace bash

コンテナに入ったらLaravelプロジェクトを作成

$ composer create-project laravel/laravel sample

上記までを無事に実行できれば
docker_laravelフォルダ内にはlaradocksampleフォルダが入っているはずです

コンテナから出ます

$ exit

コンテナを一旦止めます

$ docker-compose stop

.envの編集(laradock側)

laradock内の.envファイルを下記のように修正してください

変更前
APP_CODE_PATH_HOST=../

変更後
APP_CODE_PATH_HOST=../sample
変更前
DATA_PATH_HOST=~/.laradock/data

変更後
DATA_PATH_HOST=.laradock/data
変更前
MYSQL_VERSION=latest

変更後
MYSQL_VERSION=5.7

.envの編集(sample側)

変更前
APP_NAME=Laravel

変更後
APP_NAME=Sample
変更前
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=

変更後
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=sample
DB_USERNAME=root
DB_PASSWORD=secret

これで準備は完了です

コンテナを立ち上げる

それでは、コンテナを立ち上げてLaravelのwelcomeページにアクセスしましょう

$ docker-compose up -d workspace nginx mysql

これでこちらのURLにアクセスしてwelcomeページが表示されればOKです→こちら

DBにアクセスする

ターミナルにてlaradockに移動してください
laradockにて下記コマンドを実行してください

$ docker-compose exec mysql bash

これでmysqlコンテナに入れました
さらに下記のコマンドを入力してください

$ mysql -u root -p

上記を入力するとパスワードを求められますので下記を入力

$ secret

これでmysqlに入れたかと思います

マイグレーションの実行

実際にDBの接続がきちんとできているかを確かめるために
マイグレーションを実行して、テーブルを作成してみましょう
ターミナルにてlaradockに移動し下記コマンドを実行してworkspaceコンテナに入ります

$ docker-compose exec workspace bash

これでコンテナに入れました
マイグレーションを実行します

$ php artisan migrate

上記を実行し無事にマイグレーションが実行され
DBにテーブルが追加されていれば完了です

終わりに

以上がLaradockを使ってLaravelの環境を構築と
マイグレーションの実行やDBの操作方法になります

私はDockerにあまり詳しくないのですが
そんな自分でも簡単に環境を用意することができました

Dockerにそこまで詳しくないけど
Dockerで開発してみたいと言う方は是非、挑戦してみてください

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker勉強メモ③ Dockerfileを作ってDockerイメージ作成からコンテナ起動までやってみる

はじめに

Docker勉強メモ
- Docker勉強メモ① DockerインストールからHelloWorld
- Docker勉強メモ② Dockerイメージ作ってみる
- Docker勉強メモ③ Dockerfileを作ってDockerイメージ作成からコンテナ起動までやってみる ←今ここ

やること

Dockerファイルを作ってDockerイメージを作成する

参考
Docker入門(第四回)~Dockerfileについて~

手順概要
 centosのDockerイメージをDockerHubから取得
 tomcatのtar.gzをコピーしたcentosDockeイメージを作成( -> tomcat:7)
 tomcat:7 を使いDockerイメージを作るDockerfileを作る
 DockerfileでDockerイメージをビルド
 ビルドしたDokcerイメージでコンテナ起動

おさらい

centos7 (ami-045f38c93733dd48d)にDockerインストール

ホスト
$ sudo yum-config-manager --enable Extra
$ sudo yum install container-selinux
$ sudo yum install -y yum-utils device-mapper-persistent-data lvm2
$ sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
$ ll /etc/yum.repos.d/
$ sudo yum install docker-ce
$ sudo yum list installed | grep docker-ce
$ sudo systemctl start docker
$ sudo systemctl status docker

centosのDockerイメージをpull

ホスト
$ docker pull centos:7
$ mkdir -p /root/tomcat-container/logs
$ docker run -it -d -p 18080:8080 -v /root/tomcat-container/logs:/share/logs --name tomcat centos:7

1.準備

1-1.Dockerファイルを作る

Dockerfileを作成する

nanoを使いたのでインストール

yum install nano

Dockerfileを作る

mkdir cent-tomcat
cd cent-tomcat
touch Dockerfile
sudo nano Dockerfile

Dockerfileの中身

Dockerfile
FROM centos:7
RUN yum install -y java
ADD files/apache-tomcat-9.0.31.tar.gz /opt/
CMD [ "/opt/apache-tomcat-9.0.31/bin/catalina.sh", "run" ]

1-2.apacheファイルを用意する

Dockerファイルと同じ階層に files フォルダを作成し、apacheファイルを格納する

ファイル構造はこーなる

ホスト
[centos@ip-172-31-0-62 cent-tomcat]$ tree
.
├── Dockerfile
└── files
    └── apache-tomcat-9.0.31.tar.gz

2.Dockerファイルを私用して、Dockerイメージ作成

$ cd <Dockerfileが存在するディレクトリ>
$ docker build -t tomcat:1 .
$ docker images

Dockerイメージが作成されていれば成功

[centos@ip-172-31-5-50 cent-tomact]$ sudo docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
tomcat              1                   1a24f3cd25e8        17 seconds ago      502MB
centos              7                   5e35e350aded        3 months ago        203MB

ベースにしたcentosのイメージも作成された

※余談※
ベースにしたcentosのDockerイメージだけ削除はできない。依存関係があるんだ、へぇー

[centos@ip-172-31-5-50 cent-tomact]$ sudo docker images
REPOSITORY          TAG                 IMAGE ID            CREATED              SIZE
tomcat              1                   44f9a4e385f1        About a minute ago   502MB
centos              7                   5e35e350aded        3 months ago         203MB
[centos@ip-172-31-5-50 cent-tomact]$ sudo docker rmi 5e35e350aded
Error response from daemon: conflict: unable to delete 5e35e350aded (cannot be forced) - image has dependent child images

コンテナ起動する

ホスト
$ docker run -it -d --name tomcat-1 -p 18083:8080 tomcat:1
ホスト
[centos@ip-172-31-0-62 cent-tomcat]$ docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                           PORTS                     NAMES
e9814858bec8        tomcat:1            "/opt/apache-tomcat-…"   7 seconds ago       Up 6 seconds                     0.0.0.0:18083->8080/tcp   tomcat-1
94d9fd8f516a        centos:7            "/bin/bash"              2 hours ago         Exited (137) About an hour ago                             tomcat
ホスト
$ docker exec -it tomat-1 bash

ホストで以下のサイトが開けば成功

http://localhost:18083/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker勉強メモ② Dockerイメージ作ってみる

はじめに

Docker勉強メモ
- Docker勉強メモ① DockerインストールからHelloWorld
- Docker勉強メモ② Dockerイメージ作ってみる ←今ここ
- Docker勉強メモ③ Dockerfileを作ってDockerイメージ作成からコンテナ起動までやってみる

やること

前回はDocker HubにあるDockerイメージをそのまま使った
今度はDockerイメージを作る

※参考※
Docker入門(第三回)~各種dockerコマンドとDockerイメージ作成について~

1.Dockerイメージを作る

1-1.CentOSコンテナ起動

Docker HubからCentOSのDockerイメージを取得し、CentOSコンテナを起動する

ホスト
$ docker pull centos:7
$ mkdir -p /root/tomcat-container/logs
$ docker run -it -d -p 18080:8080 -v /root/tomcat-container/logs:/share/logs --name tomcat centos:7

※「/root/tomcat-container/logs」は任意のディレクトリ

オプションの意味
 -it : コンソールに結果を出力
 -d : バックグラウンド実行
 -p : ポートフォワーディング
 -v : ディレクトリ共有
 --name : コンテナ名

1-2.CentOSコンテナにTomcatインストール

起動したCentOSコンテナにTomcatをインストールする

流れの概要はこんな感じ
ホストでtomcatのtar.gzを取得
ホスト→コンテナにtar.gzをコピー
CentOSコンテナにログイン
CentOSコンテナ内でTomcatインストール

1-2-1.ホストでtomgatのtar.gzを取得

wgetをインストール

ホスト
$ yum -y install wget

tomcatのtar.gz のURLを確認
http://www-eu.apache.org/dist/tomcat/ 開き、使うバージョンのtar.gzを探す
→今回:http://ftp.riken.jp/net/apache/tomcat/tomcat-9/v9.0.31/bin/apache-tomcat-9.0.31.tar.gz

tomcatのtar.gzを取得

ホスト
wget http://ftp.riken.jp/net/apache/tomcat/tomcat-9/v9.0.31/bin/apache-tomcat-9.0.31.tar.gz
1-2-2.ホスト→コンテナにtar.gzをコピー

ホスト側からコンテナ内にファイルをコピー 

ホスト
[centos@ip-172-31-3-246 ~]$ pwd
/home/centos
[centos@ip-172-31-3-246 ~]$ ls
apache-tomcat-9.0.31.tar.gz
[centos@ip-172-31-3-246 ~]$ sudo docker cp /home/centos/apache-tomcat-9.0.31.tar.gz tomcat:opt/

※ no such file と出たのでフルパス指定で回避

1-2-3.CentOSコンテナにログイン
ホスト
$ docker exec -it tomcat bash
tomcatコンテナ
[centos@ip-172-31-5-50 ~]$ sudo docker exec -it tomcat bash
[root@00d08555ba6b /]# 
1-2-4.CentOSコンテナ内でTomcatインストール
tomcatコンテナ
yum install -y java
cd /opt/
tar zxf apache-tomcat-9.0.31.tar.gz
cd apache-tomcat-9.0.31
./bin/startup.sh

これでTomcatが動いた

コンテナから出て(exit)、Webサイトが開くか確認する

curl http://localhost:18080/
curl http://172.31.5.50:18080/

2.ホストとディレクトリ共有

Dockerコンテナを削除してもDockerコンテナ上のログは残したい、みたいなときに使う

2-1.Tomcatログ設定変更

Tomcatログはデフォルトのディレクトリ(/opt/apache-tomcat-9.0.31/logs)に出力されているので、コンテナにログインして、ログ出力先を変える

CentOSコンテナにログイン

ホスト
$ sudo docker exec -it tomcat bash

Tomcatのログ設定を変更しTomcatサービス再起動

tomcatコンテナ
# cd /opt/apache-tomcat-9.0.31/
# sed -i -e "s/\${catalina.base}\/logs/\/share\/logs/g" ./conf/logging.properties
# ./bin/shutdown.sh
# ./bin/startup.sh
# ls -la /share/logs/

ホストに戻って、以下コマンドでログが表示されたら成功

ホスト
$ ls -la /root/tomcat-container/logs/
$ cat /root/tomcat-container/logs/catalina.2020-03-08.log

2-2.Dockerコンテナ停止後もTomcatログ見れるか確認

CentOSコンテナを停止

ホスト
$ docker ps -a
$ docker stop tomcat
$ docker ps -a

※実行時の出力

ホスト
[centos@ip-172-31-5-50 ~]$ sudo docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                     NAMES
04d08555aa6b        centos:7            "/bin/bash"         35 minutes ago      Up 35 minutes       0.0.0.0:18080->8080/tcp   tomcat
[centos@ip-172-31-5-50 ~]$ sudo docker stop tomcat
tomcat
[centos@ip-172-31-5-50 ~]$ sudo docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                       PORTS               NAMES
04d08555aa6b        centos:7            "/bin/bash"         35 minutes ago      Exited (137) 6 seconds ago                       tomcat

STATUSが UP -> Exited に変わたことを確認する

ログ残っていてログの中身が見れたら成功

ホスト
$ ls -la /root/tomcat-container/logs/

3.CentOSコンテナからDockerイメージ作る

コンテナからイメージをつくることができる
今のDockerコンテナとDockerイメージはこーいう状態

ホスト
[centos@ip-172-31-5-50 cent-tomact]$ sudo docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                     PORTS               NAMES
04d08555aa6b        centos:7            "/bin/bash"         3 hours ago         Exited (137) 3 hours ago                       tomcat
[centos@ip-172-31-5-50 cent-tomact]$ sudo docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
centos              7                   5e35e350aded        3 months ago        203MB

では、Dockerイメージを作成する
コンテナを起動してたら停止する

ホスト
$ docker stop tomcat

Dockerイメージにする対象のコンテナ名を指定し、Dockerイメージを作成

ホスト
$ docker commit tomact tomcat-image

※ 既存のDockerイメージ tomcat から tomcat-image を新規作成してる

これでDockerコンテナとDockerイメージはこーいう状態
tomcat-image というDockerイメージができてる

ホスト
[centos@ip-172-31-5-50 cent-tomact]$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                     PORTS               NAMES
04d08555aa6b        centos:7            "/bin/bash"         3 hours ago         Exited (137) 3 hours ago                       tomcat
[centos@ip-172-31-5-50 cent-tomact]$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
tomcat-image        latest              173543bffdb6        16 seconds ago      529MB
centos              7                   5e35e350aded        3 months ago        203MB

4.作成したDockerイメージを使用してコンテナ起動

ちゃんと動くか試す

ホスト
$ sudo mkdir -p /root/tomcat-container/logs2
$ docker run -it -d -p 18082:8080 -v /root/tomcat-container/logs2:/share/logs --name tomcat2 tomcat-image

tomcat2 というDockerコンテナが起動してたら成功

ホスト
[centos@ip-172-31-5-50 ~]$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED              STATUS                     PORTS                     NAMES
7f9xxx5dcf85        tomcat-image        "/bin/bash"         About a minute ago   Up About a minute          0.0.0.0:18082->8080/tcp   tomcat2
04dxxx95aa6b        centos:7            "/bin/bash"         3 hours ago          Exited (137) 3 hours ago                             tomcat

tomcat2コンテナに入りTomcatを起動してみる

tomcat2コンテナに入る

ホスト
$ docker exec -it tomcat2 bash
tomcat2コンテナ
# cd /opt/apache-tomcat-9.0.31
# ./bin/startup.sh 

※ Dockerコンテナなら抜けるときは exit

ホストからWebサイトにアクセスできたら成功

http://localhost:18082/

5.後片付け

※注意※ Docker勉強メモ③に進むならこの作業はしない

作ったDockerイメージを削除します。
(AWS利用料を抑えるには、EC2インスタンスの削除が必要)

  • Dockerコンテナ確認 docker ps -a
  • Dockerコンテナ停止 docker stop
  • Dockerコンテナ削除 docker rm
  • Dockerイメージ確認 docker images
  • Dockerイメージ削除 docker rmi

Dockerコンテナ停止

$ docker stop tomcat2

Dockerコンテナ削除

$ docker rm tomcat2
$ docker rm tomcat

Dockerイメージ削除

sudo docker rmi <REPOSITORY>
sudo docker rmi <centosのIMAGE ID>
sudo docker rmi <REPOSITORY>:<TAG>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[初心者向け] GoogleAppsScript(GAS)の開発環境をインクリメンタルに構築

本記事について

GoogleAppsScript(以下GAS)の開発環境を初心者でも分かるように順番に整えていきます。
https://developers.google.com/apps-script

この記事の内容と同じ開発工程をGithubに残してあります。
Dockerを使って構築したので読者の方々の環境にあまり依存せずに構築出来ると思います。

対象

  • GASは開発環境が悪いから触りづらいと思ってる方
  • GASでGit使いたい、好きなエディタ使いたい、TypeScript使いたい等の方
  • GASで環境を作ろうとして挫折した方
  • 難しい事はいいからとっととソースコードよこせな方

出てくるもの

  • GAS
  • Docker
  • TypeScript
  • WebPack

各段階のゴール

以下の順番で実現していきます。
気になるところだけでもどうぞ。

  • ローカル開発
    • Gitや好きなエディタが使えるようになります
  • TypeScript利用
    • 型が使えるようになります
  • Module利用
    • 複数ファイルに分割して開発しやすくなります
  • 最新のES構文をGASで使えるように
    • GASで使えない最新の構文を使えるようになります

前提知識

GASを既に利用している方向けです。

また、Dockerを利用するのでどんなものかだけでも知っていると読みやすいと思います。
とりあえずDockerをインストールして同じように記述すれば同じ環境が作れるはずです。

ソースコード

この記事のソースコードは下記のリポジトリで公開しています。
cajonito/qiita_gas_local_develop

環境

macOS Catalina 10.15.3
Docker on mac 2.2.0.4

ローカル開発

ローカル開発環境を整える事で色々なカスタマイズを行うことが可能になります。
特にGitが使えるようになるのは嬉しいですね。

Claspの導入

Google公式ツールのClaspを導入すると簡単にGASとローカル環境間でコード転送を行えます。
Command Line Interface using clasp
google/clasp

Claspのインストール

今後環境構築は全てDockerを利用していきます。
環境をコードとして残せますし、利用も簡単です。

まずDockerfileを作成します。
今回は構築当時最新だったnode:13.7-alpineというコンテナを利用しました。

Dockerfile
FROM node:13.7-alpine
WORKDIR /workdir
RUN yarn global add @google/clasp

Docker Composeも使います。
ボリュームの設定とか書いておけるので便利です。
コンテナは立ち上げっぱなしにして利用します。
VSCodeでコンテナに入って開発すると色々便利です。

docker-compose.yml
version: "3"
services:
  gas:
    build: .
    volumes:
      - .:/workdir
    working_dir: /workdir
    command: tail -f /dev/null

それではdocker-compose.ymlのあるディレクトリでコンテナを起動してClaspを使えるか確認してみましょう。

$ docker-compose up -d --build
(実行内容省略)

$ docker-compose exec gas clasp -v
2.3.0

これでDocker経由でClaspを利用出来るようになりました。

ここまでのディレクトリ構造

.
├── .git
├── docker-compose.yml
├── Dockerfile
└── README.md

Claspの認証

ClaspでGAS上のプロジェクトと連携するためにはアカウント認証が必要です。
認証情報はコンテナ上の/root/.clasprc.jsonに配置される事を踏まえてdocker-compose.ymlを更新します。

docker-compose.yml
version: "3"
services:
  gas:
    build: .
    volumes:
      - .:/workdir
+     - ./.clasprc.json:/root/.clasprc.json
    working_dir: /workdir
    command: tail -f /dev/null

また、ボリュームをマウントする際に存在しないファイルはディレクトリとして新規作成されるので、予めファイルを作成しておきます。
この後の認証でファイルがうまく作成出来ない場合は同名のディレクトリが作られていないかご確認ください。

$ touch .clasprc.json

それでは早速認証をしてみましょう。

$ docker-compose exec gas clasp login --no-localhost
Warning: You seem to already be logged in *globally*. You have a ~/.clasprc.json
Logging in globally...
? Authorize clasp by visiting this url:
https://accounts.google.com/o/oauth2/v2/auth...(省略)

Enter the code from that page here:(入力)
Authorization successful.

Default credentials saved to: ~/.clasprc.json (/root/.clasprc.json).

既に.clasprc.jsonがあると言われてますが、認証後上書きされるだけなので無視して大丈夫です。
表示されたURLにブラウザからアクセスし、GASのアカウントでログインして権限を許可します。
ログイン後表示されたコードをコピーしてEnter the code from page there:に入力します。
問題なければ.clasprc.jsonに認証内容が書き込まれています。

clasprc.json
{"token":{"access_token":"...(省略)

今後省略しますが.clasprc.json等Gitで取り扱いたくないものは適宜.gitignoreに追加していきます。

ここまでのディレクトリ構造

.
├── .clasprc.json
├── .git
├── .gitignore
├── docker-compose.yml
├── Dockerfile
└── README.md

Clasp clone

認証ができたので早速GAS上のコードをローカルにcloneしましょう。
--rootDirを利用するとソースコードを取り扱うディレクトリを決定出来ます。

$ docker-compose exec gas clasp clone --rootDir ./src
? Clone which script? 

(GAS上のプロジェクトを選ぶ)

Warning: files in subfolder are not accounted for unless you set a '.claspignore' file.
Cloned 2 files.
└─ ./src/appsscript.json
└─ ./src/コード.js
Not ignored files:
└─ src/appsscript.json
└─ src/コード.js

Ignored files:

cloneすると.clasp.jsonというファイルが作成されます。
中に対応するスクリプトのIDなどが書かれています。

clasp.sjon
{"scriptId":"(省略)","rootDir":"./src"}

src/コード.jsに何か書き足してみます。

src/コード.js
function myFunction() {
  // コード追加 
}

次はGASにローカルの変更を反映させるためにpushしてみましょう。

$ docker-compose exec gas clasp push
└─ src/appsscript.json
└─ src/コード.js
Pushed 2 files.

GAS上で確認してみるとローカルでの変更が反映されています。
スクリーンショット 2020-03-22 17.13.40.png

注意点としては、claspはGAS上とローカルの競合の解決まで行いません。
GAS上のコードの上書きだけでなく、ローカルに存在しないファイルもpushの時点で削除されるのでご注意ください。

ここまでのディレクトリ構造

.
├── .clasp.json
├── .clasprc.json
├── .git
├── .gitignore
├── docker-compose.yml
├── Dockerfile
├── README.md
└── src
   ├── appsscript.json
   └── コード.js

さあこれでローカル環境での開発が可能になりました。
claspには他にも色々な機能があるのでご興味があればGithubの公式リポジトリを御覧ください。
google/clasp

TypeScript導入

TypeScript: JavaScript For Any Scale.
TypeScriptを使って開発したいニーズも多いと思います。
私は型によるデバッグの簡単さとコード補完が欲しくて導入しました。

実はClaspはTypeScriptに対応しており、特に新しいツールを導入せずともこの時点でTypeScriptは利用可能です。
clasp/typescript.md at master · google/clasp

早速試してみましょう。
既存のコード.jsは削除して、claspの公式サンプルコードをpushしてみます。

src/main.ts
const greeter = (person: string) => {
  return `Hello, ${person}!`;
};

function testGreeter() {
  const user = "Grant";
  Logger.log(greeter(user));

  const age = 30;
  Logger.log(greeter(age)); // Argument of type '30' is not assignable to parameter of type 'string'.ts(2345)
}

下記の通りsrc/main.tsはmain.gsに変換されました。

スクリーンショット 2020-03-22 18.11.50.png

ただし、ご覧の通り引数personの型指定は無視されてしまいました。
claspはtsconfig.jsonの内容を反映させるらしいので、カスタマイズすることでpush時に型チェックされたりするのかな?
TypeScriptに対応したエディタを使っていればエディタ側で注意されるのでこの状態でも十分かもしれません。

Module利用

下記のように現在GASはModule機能をサポートしておりません。

Currently, Google Apps Script does not support ES modules.
Modules, exports and imports - clasp/typescript.md at master · google/clasp

GASにはプロジェクト内に複数のスクリプトを作成出来ますが、スコープは共有されています。
Module機能を使って開発したい場合は、ローカル環境で構築する必要があります。

webpack導入

私は最初はBrowserifyを使っていましたがwebpackの方が簡単だったのでこちらを紹介します。
webpack

webpackでModuleの解決も行いつつ、TypeScriptのコンパイルもやってしまい、生成されたjsファイルをclasp pushする形になります。

Node.jsパッケージ取り扱いのための準備

今後webpackを含め様々なNode.jsパッケージを導入するのでそのための準備をします。
まずはpackage.jsonとyarn.lockを作成しておきましょう。
インストールしたパッケージはpackage.jsonとyarn.lockに記述されていきます。
パッケージを削除してもこの2つのファイルがあればyarn installで復元出来ます。

package.jsonはyarn initすると対話的に作成出来ます。

$ docker-compose exec gas yarn init                                                                                                                      master
yarn init v1.21.1
question name (workdir): qiita_gas
question version (1.0.0):
question description:
question entry point (index.js):
question repository url:
question author:
question license (MIT):
question private:
success Saved package.json
Done in 8.91s.

$ touch yarn.lock

Dockerfileを修正し、ビルド時にpackage.jsonとyarn.lockを用いてパッケージをインストールするようにしましょう。
こうする事で、色々インストールしたコンテナを削除してしまってもビルド時に全パッケージをバージョンを含めて再構築することが出来ます。
Dockerのキャッシュはpackage.jsonの中身の更新まで認識してくれないのでビルド時にはキャッシュを使わないようにご注意ください。

Dockerfile
FROM node:13.7-alpine
WORKDIR /workdir
RUN yarn global add @google/clasp
+ COPY package.json yarn.lock /workdir/
+ RUN yarn install

Node.jsパッケージはnode_modulesディレクトリに置かれます。

筆者はVSCodeでコンテナに入ってコーディングするため、コンテナ上のnode_modulesにのみパッケージを置いて、ローカルマシンのnode_modulesは空にしようと思います。
現在はルートディレクトリを全てコンテナ上にマウントしていますが、このままだとビルド時に作成されたnode_modulesディレクトリをローカルの空のnode_modulesで上書きしてしまいます。
これを回避するためにdocker-compose.ymlを修正します。

docker-compose.yml
version: "3"
services:
  gas:
    build: .
    volumes:
      - .:/workdir
      - ./.clasprc.json:/root/.clasprc.json
+     - /workdir/node_modules
    working_dir: /workdir
    command: tail -f /dev/null

このように記述するとnode_modulesには毎回空のVolumeがマウントされます。
空のVolumeをマウントする場合、DockerはDockerImage上で既に存在していたファイルを維持するようです。
既に中身が存在するVolumeをマウントする場合はその内容で上書きするので要注意です。

これについては以下の記事が参考大変参考になりました。
node_modulesを隔離したい - Qiita

docker-compose.ymlの変更を反映させましょう。
試しにコンテナ上のnode_modulesディレクトリ内にファイルを作成しても、ローカル環境には現れないことが確認出来ます。

webpackのインストール

準備が出来たのでwebpackをインストールしていきます。
今回はTypeScriptを取り扱うのでtypescriptとts-loaderと型定義ファイルもインストールします。

$ docker-compose exec gas yarn add --dev webpack webpack-cli typescript ts-loader @types/google-apps-script
package.json
{
  "name": "qiita_gas",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
+ "devDependencies": {
+   "@types/google-apps-script": "^1.0.11",
+   "ts-loader": "^6.2.1",
+   "typescript": "^3.8.3",
+   "webpack": "^4.42.0",
+   "webpack-cli": "^3.3.11"
+ }
}

TypeScript | webpack
上記公式ドキュメントを参考にwebpack.config.jsを作成します

webpack.config.js
const path = require('path');

module.exports = {
  mode: 'production',
  entry: './src/main.ts',
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },
  output: {
    filename: 'main.gs',
    path: path.resolve(__dirname, 'dist'),
  },
}

tsconfig.jsonも作成します。

tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es5",
  }
}

webpackのモジュールをまとめる挙動を確認するためにコードを2つに分けてみます。

main.ts
import { greeter } from "./module";

function testGreeter() {
  const user = "Grant";
  Logger.log(greeter(user));
}
module.ts
export const greeter = (person: string) => {
  return `Hello, ${person}!`;
};

ここまででディレクトリ構成は以下のようになっています。

.
├── .clasp.json
├── .clasprc.json
├── .git
├── .gitignore
├── docker-compose.yml
├── Dockerfile
├── node_modules
├── package.json
├── README.md
├── src
│  ├── appsscript.json
│  ├── main.ts
│  └── module.ts
├── tsconfig.json
├── webpack.config.js
└── yarn.lock

webpackを実行してみましょう。

$ docker-compose exec gas yarn webpack
yarn run v1.21.1
$ /workdir/node_modules/.bin/webpack
Hash: 87d76effee625fe5918e
Version: webpack 4.42.0
Time: 2883ms
Built at: 03/22/2020 12:04:56 PM
  Asset      Size  Chunks             Chunk Names
main.gs  4.03 KiB       0  [emitted]  main
Entrypoint main = main.gs
[0] ./src/main.ts 204 bytes {0} [built]
[1] ./src/module.ts 155 bytes {0} [built]
Done in 4.76s.

webpack.config.jsの設定に則ってdist/main.gsさ生成されているはずです。

dist/main.gs
/******/ (function(modules) { // webpackBootstrap
/******/        // The module cache
/******/        var installedModules = {};

(省略)

今後GASにアップロードしたいのはコンパイル後のdist/main.gsです。
clasp pushでdist内のファイルをアップロードするように変更していきます。
.clasp.jsonを下記のように編集します。

.clasp.json
- {"scriptId":"(省略)","rootDir":"./src"}
+ {"scriptId":"(省略)","rootDir":"./dist"}

次に、src/appsscript.jsonをdistディレクトリに移動させます。

$ mv src/appsscript.json dist

これでclasp pushでdistディレクトリの内容をpushできるようになりました。

$ docker-compose exec gas clasp push
└─ dist/appsscript.json
└─ dist/main.gs
Pushed 2 files.

スクリーンショット 2020-03-22 21.12.47.png

ここまでのディレクトリ構成は以下のようになっています。

.
├── .clasp.json
├── .clasprc.json
├── .git
├── .gitignore
├── dist
│  ├── appsscript.json
│  └── main.gs
├── docker-compose.yml
├── Dockerfile
├── node_modules
├── package.json
├── README.md
├── src
│  ├── main.ts
│  └── module.ts
├── tsconfig.json
├── webpack.config.js
└── yarn.lock

webpackでアップしたファイルをGAS上で利用できるようにする(重要)

実はこの時点ではGAS上から関数を実行出来ません。
webpackを利用するとwebpackのスコープ内に関数が含まれる形になりますが、GASではglobalスコープの関数のみを実行できます。
gas-webpack-pluginを導入することでこれを解決することができます。
fossamagna/gas-webpack-plugin: Webpack plugin for Google Apps Script

早速インストールします。

$ docker-compose exec gas yarn add --dev gas-webpack-plugin
package.json
{
  "name": "qiita_gas",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {
    "@types/google-apps-script": "^1.0.11",
+   "gas-webpack-plugin": "^1.0.2",
    "ts-loader": "^6.2.1",
    "typescript": "^3.8.3",
    "webpack": "^4.42.0",
    "webpack-cli": "^3.3.11"
  },
}

webpack.config.jsを編集してgas-webpack-pluginを導入します。

webpack.config.js
const path = require('path');
+ const GasPlugin = require("gas-webpack-plugin");

module.exports = {
  mode: 'production',
  entry: './src/main.ts',
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },
  output: {
    filename: 'main.gs',
    path: path.resolve(__dirname, 'dist'),
  },
+ plugins: [
+   new GasPlugin()
+ ]
}

main.tsを編集してGASから呼び出したい関数をglobalのメソッドにします。

main.ts
import { greeter } from "./module";

declare var global: any;

global.testGreeter = () => {
  const user = "Grant";
  Logger.log(greeter(user));
};

再度webpackをして結果を確認します。

dist/main.gs
function testGreeter() {
}/******/ (function(modules) { // webpackBootstrap
/******/    // The module cache
/******/    var installedModules = {};

(省略)

先頭にtestGreeter()関数が追加されました。
これでGAS上からtestGreeter()関数を実行することができます。

gas-webpack-pluginが上手く動かない場合

もし上手く行かなかったら以下の点を確認してください。

  • webpackで吐き出すファイルの拡張子が".gs"である必要がある
  • productionモードである必要がある

最新のES構文をGASで使えるように

最近のアップデートでV8ランタイムを使うようになったことでGASでもモダンな書き方が出来るようになりました。
V8 Runtime Overview  |  Apps Script  |  Google Developers

V8以前はletやconstすら使えなかったので、トランスパイルする必要がありました。
V8になって殆ど気にならなくなりましたが、最新の構文をGASで使いたいというニーズは今後もあると思うので、トランスパイルの方法をご説明します。

今回は例のためにあえてV8を無効にして古いGASの記述に合わせる形で検証していこうと思います。

先程から使っているsrc/main.tsをサンプルに使います。

src/main.ts
import { greeter } from "./module";

declare var global: any;

global.testGreeter = () => {
  const user = "Grant";
  Logger.log(greeter(user));
};

tsconfig.jsonのtargetを指定する事でコンパイル後のバージョンを指定する事が出来ます。
まずはes2019にしてみましょう。

tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es2019"
  }
}

スクリーンショット 2020-03-23 01.17.09.png

V8を無効にするとエラーになってしまいました。
当時はアロー演算子が使えなかったのです。

次はtargetをes5にしてみます。

tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
-   "target": "es2019"
+   "target": "es5"
  }
}

スクリーンショット 2020-03-23 01.19.54.png

今度は無事実行できました。
上記のようにアロー演算子が置き換えられています。

ただし、この状態でもArray.prototype.flat()のようなES5には存在しなかった関数は使うことが出来ません。
TypeScriptの場合、ts-polyfillを使えばtargetに存在しなかった関数の定義を埋め込む事で利用可能になります。
ts-polyfill - npm

$ yarn add --dev ts-polyfill

tsconfig.jsonのlibパラメータを設定すると利用するライブラリを指定することが出来ます。

tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es5",
+   "lib": [
+     "DOM",
+     "DOM.Iterable",
+     "ES2015",
+     "ES2016.Array.Include",
+     "ES2017.Object",
+     "ES2017.String",
+     "ES2018.AsyncIterable",
+     "ES2018.Promise",
+     "ES2019.Array",
+     "ES2019.Object",
+     "ES2019.String",
+     "ES2019.Symbol",
+     "ES2020.Promise",
+     "ES2020.String",
+     "ES2020.Symbol.WellKnown"
+   ]
  }
}
main.ts
import { greeter } from "./module";
+ import 'ts-polyfill/lib/es2019-array';

declare var global: any;

global.testGreeter = () => {
  const user = "Grant";
  Logger.log(greeter(user));
};

+ global.testArrayFlat = () => {
+   const a = [[1, 2], 3, 4];
+   Logger.log(a.flat());
+ };

この結果、webpackで出力されたdist/main.gsの行数がとんでもないことになります。
どうやらこの中にArray.prototype.flat()の実装が含まれているようです。

スクリーンショット 2020-03-23 01.35.36.png

このように、V8以前には存在しなかったArray.prototype.flat()の恩恵に預かることができました。

まとめ

いかがでしたでしょうか。
この記事で少しでもGAS利用者の生産性が向上すれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[初心者向け] GoogleAppsScript(GAS)の開発環境をインクリメンタルに構築(TypeScript / Module / Polyfill)

本記事について

GoogleAppsScript(以下GAS)の開発環境を初心者でも分かるように順番に整えていきます。
https://developers.google.com/apps-script

この記事の内容と同じ開発工程をGithubに残してあります。
cajonito/qiita_gas_local_develop

Dockerを使って構築したので読者の方々の環境にあまり依存せずに構築出来ると思います。

対象

  • GASは開発環境が悪いから触りづらいと思ってる方
  • GASでGit使いたい、好きなエディタ使いたい、TypeScript使いたい等の方
  • GASで環境を作ろうとして挫折した方
  • 難しい事はいいからとっととソースコードよこせな方

各段階のゴール

以下の順番で実現していきます。
気になるところだけでもどうぞ。

出てくるもの

  • GAS
  • Docker
  • TypeScript
  • WebPack

前提知識

GASを既に利用している方向けです。

また、Dockerを利用するのでどんなものかだけでも知っていると読みやすいと思います。
とりあえずDockerをインストールして同じように記述すれば同じ環境が作れるはずです。

ソースコード

この記事のソースコードは下記のリポジトリで公開しています。
cajonito/qiita_gas_local_develop

環境

macOS Catalina 10.15.3
Docker on mac 2.2.0.4

ローカル開発

ローカル開発環境を整える事で色々なカスタマイズを行うことが可能になります。
特にGitが使えるようになるのは嬉しいですね。

Claspの導入

Google公式ツールのClaspを導入すると簡単にGASとローカル環境間でコード転送を行えます。
Command Line Interface using clasp
google/clasp

Claspのインストール

今後環境構築は全てDockerを利用していきます。
環境をコードとして残せますし、利用も簡単です。

まずDockerfileを作成します。
今回は構築当時最新だったnode:13.7-alpineというコンテナを利用しました。

Dockerfile
FROM node:13.7-alpine
WORKDIR /workdir
RUN yarn global add @google/clasp

Docker Composeも使います。
ボリュームの設定とか書いておけるので便利です。
コンテナは立ち上げっぱなしにして利用します。
VSCodeでコンテナに入って開発すると色々便利です。

docker-compose.yml
version: "3"
services:
  gas:
    build: .
    volumes:
      - .:/workdir
    working_dir: /workdir
    command: tail -f /dev/null

それではdocker-compose.ymlのあるディレクトリでコンテナを起動してClaspを使えるか確認してみましょう。

$ docker-compose up -d --build
(実行内容省略)

$ docker-compose exec gas clasp -v
2.3.0

これでDocker経由でClaspを利用出来るようになりました。

ここまでのディレクトリ構造

.
├── .git
├── docker-compose.yml
├── Dockerfile
└── README.md

Claspの認証

ClaspでGAS上のプロジェクトと連携するためにはアカウント認証が必要です。
認証情報はコンテナ上の/root/.clasprc.jsonに配置される事を踏まえてdocker-compose.ymlを更新します。

docker-compose.yml
version: "3"
services:
  gas:
    build: .
    volumes:
      - .:/workdir
+     - ./.clasprc.json:/root/.clasprc.json
    working_dir: /workdir
    command: tail -f /dev/null

また、ボリュームをマウントする際に存在しないファイルはディレクトリとして新規作成されるので、予めファイルを作成しておきます。
この後の認証でファイルがうまく作成出来ない場合は同名のディレクトリが作られていないかご確認ください。

$ touch .clasprc.json

それでは早速認証をしてみましょう。

$ docker-compose exec gas clasp login --no-localhost
Warning: You seem to already be logged in *globally*. You have a ~/.clasprc.json
Logging in globally...
? Authorize clasp by visiting this url:
https://accounts.google.com/o/oauth2/v2/auth...(省略)

Enter the code from that page here:(入力)
Authorization successful.

Default credentials saved to: ~/.clasprc.json (/root/.clasprc.json).

既に.clasprc.jsonがあると言われてますが、認証後上書きされるだけなので無視して大丈夫です。
表示されたURLにブラウザからアクセスし、GASのアカウントでログインして権限を許可します。
ログイン後表示されたコードをコピーしてEnter the code from page there:に入力します。
問題なければ.clasprc.jsonに認証内容が書き込まれています。

clasprc.json
{"token":{"access_token":"...(省略)

今後省略しますが.clasprc.json等Gitで取り扱いたくないものは適宜.gitignoreに追加していきます。

ここまでのディレクトリ構造

.
├── .clasprc.json
├── .git
├── .gitignore
├── docker-compose.yml
├── Dockerfile
└── README.md

Clasp clone

認証ができたので早速GAS上のコードをローカルにcloneしましょう。
--rootDirを利用するとソースコードを取り扱うディレクトリを決定出来ます。

$ docker-compose exec gas clasp clone --rootDir ./src
? Clone which script? 

(GAS上のプロジェクトを選ぶ)

Warning: files in subfolder are not accounted for unless you set a '.claspignore' file.
Cloned 2 files.
└─ ./src/appsscript.json
└─ ./src/コード.js
Not ignored files:
└─ src/appsscript.json
└─ src/コード.js

Ignored files:

cloneすると.clasp.jsonというファイルが作成されます。
中に対応するスクリプトのIDなどが書かれています。

clasp.sjon
{"scriptId":"(省略)","rootDir":"./src"}

src/コード.jsに何か書き足してみます。

src/コード.js
function myFunction() {
  // コード追加 
}

次はGASにローカルの変更を反映させるためにpushしてみましょう。

$ docker-compose exec gas clasp push
└─ src/appsscript.json
└─ src/コード.js
Pushed 2 files.

GAS上で確認してみるとローカルでの変更が反映されています。
スクリーンショット 2020-03-22 17.13.40.png

注意点としては、claspはGAS上とローカルの競合の解決まで行いません。
GAS上のコードの上書きだけでなく、ローカルに存在しないファイルもpushの時点で削除されるのでご注意ください。

ここまでのディレクトリ構造

.
├── .clasp.json
├── .clasprc.json
├── .git
├── .gitignore
├── docker-compose.yml
├── Dockerfile
├── README.md
└── src
   ├── appsscript.json
   └── コード.js

さあこれでローカル環境での開発が可能になりました。
claspには他にも色々な機能があるのでご興味があればGithubの公式リポジトリを御覧ください。
google/clasp

TypeScript導入

TypeScript: JavaScript For Any Scale.
TypeScriptを使って開発したいニーズも多いと思います。
私は型によるデバッグの簡単さとコード補完が欲しくて導入しました。

実はClaspはTypeScriptに対応しており、特に新しいツールを導入せずともこの時点でTypeScriptは利用可能です。
clasp/typescript.md at master · google/clasp

早速試してみましょう。
既存のコード.jsは削除して、claspの公式サンプルコードをpushしてみます。

src/main.ts
const greeter = (person: string) => {
  return `Hello, ${person}!`;
};

function testGreeter() {
  const user = "Grant";
  Logger.log(greeter(user));

  const age = 30;
  Logger.log(greeter(age)); // Argument of type '30' is not assignable to parameter of type 'string'.ts(2345)
}

下記の通りsrc/main.tsはmain.gsに変換されました。

スクリーンショット 2020-03-22 18.11.50.png

ただし、ご覧の通り引数personの型指定は無視されてしまいました。
claspはtsconfig.jsonの内容を反映させるらしいので、カスタマイズすることでpush時に型チェックされたりするのかな?
TypeScriptに対応したエディタを使っていればエディタ側で注意されるのでこの状態でも十分かもしれません。

Module利用

下記のように現在GASはModule機能をサポートしておりません。

Currently, Google Apps Script does not support ES modules.
Modules, exports and imports - clasp/typescript.md at master · google/clasp

GASにはプロジェクト内に複数のスクリプトを作成出来ますが、スコープは共有されています。
Module機能を使って開発したい場合は、ローカル環境で構築する必要があります。

webpack導入

私は最初はBrowserifyを使っていましたがwebpackの方が簡単だったのでこちらを紹介します。
webpack

webpackでModuleの解決も行いつつ、TypeScriptのコンパイルもやってしまい、生成されたjsファイルをclasp pushする形になります。

Node.jsパッケージ取り扱いのための準備

今後webpackを含め様々なNode.jsパッケージを導入するのでそのための準備をします。
まずはpackage.jsonとyarn.lockを作成しておきましょう。
インストールしたパッケージはpackage.jsonとyarn.lockに記述されていきます。
パッケージを削除してもこの2つのファイルがあればyarn installで復元出来ます。

package.jsonはyarn initすると対話的に作成出来ます。

$ docker-compose exec gas yarn init                                                                                                                      master
yarn init v1.21.1
question name (workdir): qiita_gas
question version (1.0.0):
question description:
question entry point (index.js):
question repository url:
question author:
question license (MIT):
question private:
success Saved package.json
Done in 8.91s.

$ touch yarn.lock

Dockerfileを修正し、ビルド時にpackage.jsonとyarn.lockを用いてパッケージをインストールするようにしましょう。
こうする事で、色々インストールしたコンテナを削除してしまってもビルド時に全パッケージをバージョンを含めて再構築することが出来ます。
Dockerのキャッシュはpackage.jsonの中身の更新まで認識してくれないのでビルド時にはキャッシュを使わないようにご注意ください。

Dockerfile
FROM node:13.7-alpine
WORKDIR /workdir
RUN yarn global add @google/clasp
+ COPY package.json yarn.lock /workdir/
+ RUN yarn install

Node.jsパッケージはnode_modulesディレクトリに置かれます。

筆者はVSCodeでコンテナに入ってコーディングするため、コンテナ上のnode_modulesにのみパッケージを置いて、ローカルマシンのnode_modulesは空にしようと思います。
現在はルートディレクトリを全てコンテナ上にマウントしていますが、このままだとビルド時に作成されたnode_modulesディレクトリをローカルの空のnode_modulesで上書きしてしまいます。
これを回避するためにdocker-compose.ymlを修正します。

docker-compose.yml
version: "3"
services:
  gas:
    build: .
    volumes:
      - .:/workdir
      - ./.clasprc.json:/root/.clasprc.json
+     - /workdir/node_modules
    working_dir: /workdir
    command: tail -f /dev/null

このように記述するとnode_modulesには毎回空のVolumeがマウントされます。
空のVolumeをマウントする場合、DockerはDockerImage上で既に存在していたファイルを維持するようです。
既に中身が存在するVolumeをマウントする場合はその内容で上書きするので要注意です。

これについては以下の記事が参考大変参考になりました。
node_modulesを隔離したい - Qiita

docker-compose.ymlの変更を反映させましょう。
試しにコンテナ上のnode_modulesディレクトリ内にファイルを作成しても、ローカル環境には現れないことが確認出来ます。

webpackのインストール

準備が出来たのでwebpackをインストールしていきます。
今回はTypeScriptを取り扱うのでtypescriptとts-loaderと型定義ファイルもインストールします。

$ docker-compose exec gas yarn add --dev webpack webpack-cli typescript ts-loader @types/google-apps-script
package.json
{
  "name": "qiita_gas",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
+ "devDependencies": {
+   "@types/google-apps-script": "^1.0.11",
+   "ts-loader": "^6.2.1",
+   "typescript": "^3.8.3",
+   "webpack": "^4.42.0",
+   "webpack-cli": "^3.3.11"
+ }
}

TypeScript | webpack
上記公式ドキュメントを参考にwebpack.config.jsを作成します

webpack.config.js
const path = require('path');

module.exports = {
  mode: 'production',
  entry: './src/main.ts',
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },
  output: {
    filename: 'main.gs',
    path: path.resolve(__dirname, 'dist'),
  },
}

tsconfig.jsonも作成します。

tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es5",
  }
}

webpackのモジュールをまとめる挙動を確認するためにコードを2つに分けてみます。

main.ts
import { greeter } from "./module";

function testGreeter() {
  const user = "Grant";
  Logger.log(greeter(user));
}
module.ts
export const greeter = (person: string) => {
  return `Hello, ${person}!`;
};

ここまででディレクトリ構成は以下のようになっています。

.
├── .clasp.json
├── .clasprc.json
├── .git
├── .gitignore
├── docker-compose.yml
├── Dockerfile
├── node_modules
├── package.json
├── README.md
├── src
│  ├── appsscript.json
│  ├── main.ts
│  └── module.ts
├── tsconfig.json
├── webpack.config.js
└── yarn.lock

webpackを実行してみましょう。

$ docker-compose exec gas yarn webpack
yarn run v1.21.1
$ /workdir/node_modules/.bin/webpack
Hash: 87d76effee625fe5918e
Version: webpack 4.42.0
Time: 2883ms
Built at: 03/22/2020 12:04:56 PM
  Asset      Size  Chunks             Chunk Names
main.gs  4.03 KiB       0  [emitted]  main
Entrypoint main = main.gs
[0] ./src/main.ts 204 bytes {0} [built]
[1] ./src/module.ts 155 bytes {0} [built]
Done in 4.76s.

webpack.config.jsの設定に則ってdist/main.gsさ生成されているはずです。

dist/main.gs
/******/ (function(modules) { // webpackBootstrap
/******/        // The module cache
/******/        var installedModules = {};

(省略)

今後GASにアップロードしたいのはコンパイル後のdist/main.gsです。
clasp pushでdist内のファイルをアップロードするように変更していきます。
.clasp.jsonを下記のように編集します。

.clasp.json
- {"scriptId":"(省略)","rootDir":"./src"}
+ {"scriptId":"(省略)","rootDir":"./dist"}

次に、src/appsscript.jsonをdistディレクトリに移動させます。

$ mv src/appsscript.json dist

これでclasp pushでdistディレクトリの内容をpushできるようになりました。

$ docker-compose exec gas clasp push
└─ dist/appsscript.json
└─ dist/main.gs
Pushed 2 files.

スクリーンショット 2020-03-22 21.12.47.png

ここまでのディレクトリ構成は以下のようになっています。

.
├── .clasp.json
├── .clasprc.json
├── .git
├── .gitignore
├── dist
│  ├── appsscript.json
│  └── main.gs
├── docker-compose.yml
├── Dockerfile
├── node_modules
├── package.json
├── README.md
├── src
│  ├── main.ts
│  └── module.ts
├── tsconfig.json
├── webpack.config.js
└── yarn.lock

webpackでアップしたファイルをGAS上で利用できるようにする(重要)

実はこの時点ではGAS上から関数を実行出来ません。
webpackを利用するとwebpackのスコープ内に関数が含まれる形になりますが、GASではglobalスコープの関数のみを実行できます。
gas-webpack-pluginを導入することでこれを解決することができます。
fossamagna/gas-webpack-plugin: Webpack plugin for Google Apps Script

早速インストールします。

$ docker-compose exec gas yarn add --dev gas-webpack-plugin
package.json
{
  "name": "qiita_gas",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {
    "@types/google-apps-script": "^1.0.11",
+   "gas-webpack-plugin": "^1.0.2",
    "ts-loader": "^6.2.1",
    "typescript": "^3.8.3",
    "webpack": "^4.42.0",
    "webpack-cli": "^3.3.11"
  },
}

webpack.config.jsを編集してgas-webpack-pluginを導入します。

webpack.config.js
const path = require('path');
+ const GasPlugin = require("gas-webpack-plugin");

module.exports = {
  mode: 'production',
  entry: './src/main.ts',
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },
  output: {
    filename: 'main.gs',
    path: path.resolve(__dirname, 'dist'),
  },
+ plugins: [
+   new GasPlugin()
+ ]
}

main.tsを編集してGASから呼び出したい関数をglobalのメソッドにします。

main.ts
import { greeter } from "./module";

declare var global: any;

global.testGreeter = () => {
  const user = "Grant";
  Logger.log(greeter(user));
};

再度webpackをして結果を確認します。

dist/main.gs
function testGreeter() {
}/******/ (function(modules) { // webpackBootstrap
/******/    // The module cache
/******/    var installedModules = {};

(省略)

先頭にtestGreeter()関数が追加されました。
これでGAS上からtestGreeter()関数を実行することができます。

gas-webpack-pluginが上手く動かない場合

もし上手く行かなかったら以下の点を確認してください。

  • webpackで吐き出すファイルの拡張子が".gs"である必要がある
  • productionモードである必要がある

最新のES構文をGASで使えるように

最近のアップデートでV8ランタイムを使うようになったことでGASでもモダンな書き方が出来るようになりました。
V8 Runtime Overview  |  Apps Script  |  Google Developers

V8以前はletやconstすら使えなかったので、トランスパイルする必要がありました。
V8になって殆ど気にならなくなりましたが、最新の構文をGASで使いたいというニーズは今後もあると思うので、トランスパイルの方法をご説明します。

今回は例のためにあえてV8を無効にして古いGASの記述に合わせる形で検証していこうと思います。

先程から使っているsrc/main.tsをサンプルに使います。

src/main.ts
import { greeter } from "./module";

declare var global: any;

global.testGreeter = () => {
  const user = "Grant";
  Logger.log(greeter(user));
};

tsconfig.jsonのtargetを指定する事でコンパイル後のバージョンを指定する事が出来ます。
まずはes2019にしてみましょう。

tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es2019"
  }
}

スクリーンショット 2020-03-23 01.17.09.png

V8を無効にするとエラーになってしまいました。
当時はアロー演算子が使えなかったのです。

次はtargetをes5にしてみます。

tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
-   "target": "es2019"
+   "target": "es5"
  }
}

スクリーンショット 2020-03-23 01.19.54.png

今度は無事実行できました。
上記のようにアロー演算子が置き換えられています。

ただし、この状態でもArray.prototype.flat()のようなES5には存在しなかった関数は使うことが出来ません。
TypeScriptの場合、ts-polyfillを使えばtargetに存在しなかった関数の定義を埋め込む事で利用可能になります。
ts-polyfill - npm

$ yarn add --dev ts-polyfill

tsconfig.jsonのlibパラメータを設定すると利用するライブラリを指定することが出来ます。

tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es5",
+   "lib": [
+     "DOM",
+     "DOM.Iterable",
+     "ES2015",
+     "ES2016.Array.Include",
+     "ES2017.Object",
+     "ES2017.String",
+     "ES2018.AsyncIterable",
+     "ES2018.Promise",
+     "ES2019.Array",
+     "ES2019.Object",
+     "ES2019.String",
+     "ES2019.Symbol",
+     "ES2020.Promise",
+     "ES2020.String",
+     "ES2020.Symbol.WellKnown"
+   ]
  }
}
main.ts
import { greeter } from "./module";
+ import 'ts-polyfill/lib/es2019-array';

declare var global: any;

global.testGreeter = () => {
  const user = "Grant";
  Logger.log(greeter(user));
};

+ global.testArrayFlat = () => {
+   const a = [[1, 2], 3, 4];
+   Logger.log(a.flat());
+ };

この結果、webpackで出力されたdist/main.gsの行数がとんでもないことになります。
どうやらこの中にArray.prototype.flat()の実装が含まれているようです。

スクリーンショット 2020-03-23 01.35.36.png

このように、V8以前には存在しなかったArray.prototype.flat()の恩恵に預かることができました。

まとめ

いかがでしたでしょうか。
この記事で少しでもGAS利用者の生産性が向上すれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker composeでマウントするボリュームを変更したいならコンテナ作り直そう

TL;DR

マウントするボリュームを変更したいなら以下のいずれかを実行して、コンテナを明示的に削除しないといけない

  • docker-compose up --renew-anon-volumes
  • docker-compose down && docker-compose up
  • docker container rm ${DB_CONTAINER_ID} && docker-compose up

何をしたかったのか

開発に使っているdbコンテナで実験がしたかったが、既存のデータを壊したくなかったので、docker-compose.ymlを書き換えることで新規のデータボリュームをアタッチしたかった

Before

version: ‘3.5’
services:
  db:
    image: mysql:5.6.38
    environment:
      MYSQL_ROOT_PASSWORD: password
    volumes:
      - mysql-data:/var/lib/mysql
  web:
    image: ruby
    volumes:
      - workspace:/workspace
    depends_on:
     - db
    tty: true
volumes:
  mysql-data:
  workspace:

After

version: ‘3.5’
services:
  db:
    image: mysql:5.6.38
    environment:
      MYSQL_ROOT_PASSWORD: password
    volumes:
      - mysql-data_new:/var/lib/mysql # <- ここ
  web:
    image: ruby
    volumes:
      - workspace:/workspace
    depends_on:
     - db
    tty: true
volumes:
  mysql-data_new: # <- ここ
  workspace:

何が起きたのか

以下の警告が出て

WARNING: Service "db" is using volume "/var/lib/mysql" from the previous container. Host mapping "your-project_mysql-data_new" has no effect. Remove the existing containers (with `docker-com...

既存の"your-project_mysql-data"が使われ続けた。というか、以前のコンテナが使われ続けて、以前のデータボリュームがマウントされてしまった。

どうすればいいのか

docker-compose down

https://docs.docker.com/compose/reference/down/

してコンテナを削除するか、DBコンテナだけ消したいなら

docker container rm ${DB_CONTAINER_ID}

すればいい。DBコンテナなら必要なのはデータボリュームだけだろうからコンテナをさくりと作り直すのがいい。

もしくは、 -V or --renew-anon-volumes オプションをつけて、以前のコンテナからのボリュームの再利用を止めればいい。

https://github.com/docker/compose/issues/2481#issuecomment-279082091
https://github.com/docker/compose/issues/4476

このへんをみてようやく気づけた。

なぜそうなったのか

コンテナにアタッチしたボリュームは変更できない。なのでコンテナを作り直さないといけないんだけど、webの方のコンテナは docker-container up を実行しただけで作成し直してくれた。コンテナを作り直してくれる条件が不明。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

分子系統学演習をdockerで

分子系統学演習 データセットの作成から仮説検定まで 田辺晶史 2015/10/20
https://www.fifthdimension.jp/documents/molphytextbook/molphytextbook.ja.html

java

最適な Java の Docker イメージを選びたい
https://k11i.biz/blog/2018/05/17/base-docker-images-for-java/

Java 11 リリース後のオススメ Docker イメージを考える
https://k11i.biz/blog/2018/10/29/base-docker-images-for-java11/

Javaのサポートについてのまとめ2018
https://qiita.com/nowokay/items/edb5c5df4dbfc4a99ffb

JDKの公式Dockerイメージを使って手っ取り早くjshellを使うメモ
https://qiita.com/hi5san/items/d2f2fc77683530073267

$ docker run -v /tmp/work:/tmp/work -it openjdk /bin/bash
docker: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?.
See 'docker run --help'.
# apt update; apt -y upgrade
bash: apt: command not found

openjdk

https://hub.docker.com/_/openjdk

adoptopenjdk

https://hub.docker.com/_/adoptopenjdk

adoptopenjdk/openjdk11 - Docker Hub
https://hub.docker.com/r/adoptopenjdk/openjdk11/

$ docker run -v /tmp/work:/tmp/work -it adoptopenjdk/openjdk11:alpine-slim  /bin/bash
Unable to find image 'adoptopenjdk/openjdk11:alpine-slim' locally
alpine-slim: Pulling from adoptopenjdk/openjdk11
c9b1b535fdd9: Pull complete 
c27d8e400471: Pull complete 
deee333e69e3: Pull complete 
951b04c9812e: Pull complete 
Digest: sha256:9f18e54372102ed0cd3cb782d49d147d03cc2f3a8f08f096ee95b84563685441
Status: Downloaded newer image for adoptopenjdk/openjdk11:alpine-slim
docker: Error response from daemon: OCI runtime create failed: container_linux.go:346: starting container process caused "exec: \"/bin/bash\": stat /bin/bash: no such file or directory": unknown.

参考資料(reference)

Dockerで色んなJDKを試す
https://qiita.com/kikutaro/items/d140f519253f276b94e0

DockerコミュニティがメンテしているOpenJDKイメージを使って遭遇した問題
https://matsumana.info/blog/2018/10/13/openjdk11-docker/

自己参考資料(self reference)

生物系統計資料
https://qiita.com/kaizen_nagoya/items/6ebfb7bae0495424061e

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

分子系統学演習をdockerで(作業中)

分子系統学演習 データセットの作成から仮説検定まで 田辺晶史 2015/10/20
https://www.fifthdimension.jp/documents/molphytextbook/molphytextbook.ja.html

をdockerで動かそうと悪戦苦闘中(作業中)

java

最適な Java の Docker イメージを選びたい
https://k11i.biz/blog/2018/05/17/base-docker-images-for-java/

Java 11 リリース後のオススメ Docker イメージを考える
https://k11i.biz/blog/2018/10/29/base-docker-images-for-java11/

Javaのサポートについてのまとめ2018
https://qiita.com/nowokay/items/edb5c5df4dbfc4a99ffb

JDKの公式Dockerイメージを使って手っ取り早くjshellを使うメモ
https://qiita.com/hi5san/items/d2f2fc77683530073267

$ docker run -v /tmp/work:/tmp/work -it openjdk /bin/bash
docker: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?.
See 'docker run --help'.
# apt update; apt -y upgrade
bash: apt: command not found

openjdk

https://hub.docker.com/_/openjdk

adoptopenjdk

https://hub.docker.com/_/adoptopenjdk

adoptopenjdk/openjdk11 - Docker Hub
https://hub.docker.com/r/adoptopenjdk/openjdk11/

$ docker run -v /tmp/work:/tmp/work -it adoptopenjdk/openjdk11:alpine-slim  /bin/bash
Unable to find image 'adoptopenjdk/openjdk11:alpine-slim' locally
alpine-slim: Pulling from adoptopenjdk/openjdk11
c9b1b535fdd9: Pull complete 
c27d8e400471: Pull complete 
deee333e69e3: Pull complete 
951b04c9812e: Pull complete 
Digest: sha256:9f18e54372102ed0cd3cb782d49d147d03cc2f3a8f08f096ee95b84563685441
Status: Downloaded newer image for adoptopenjdk/openjdk11:alpine-slim
docker: Error response from daemon: OCI runtime create failed: container_linux.go:346: starting container process caused "exec: \"/bin/bash\": stat /bin/bash: no such file or directory": unknown.

参考資料(reference)

Dockerで色んなJDKを試す
https://qiita.com/kikutaro/items/d140f519253f276b94e0

DockerコミュニティがメンテしているOpenJDKイメージを使って遭遇した問題
https://matsumana.info/blog/2018/10/13/openjdk11-docker/

自己参考資料(self reference)

生物系統計資料
https://qiita.com/kaizen_nagoya/items/6ebfb7bae0495424061e

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Apache Spark を Jupyter Notebook で試す (on ローカル Docker

以前 Spark を使ってたのですが今は使ってなくて,
そのうち忘れそうなので基本をメモしておくことにしました.

(全体的に聞きかじりの知識なので間違ってる点はコメント・編集リクエストを期待します)

使う

Jupyter + PySpark な環境が動く Docker イメージが用意されているので,ローカルで試すには便利です:
https://hub.docker.com/r/jupyter/pyspark-notebook/

PySpark とは,という話ですが,Spark 自体は Scala だけど,
Python で使えるやつがあってそれが PySpark だという話があります.

IPC でがんばってるという仕組みになっていたはずなので,
Scala <-> Python の変換のコストが結構でかいうんぬんみたいな話題もあります.

さて,使ってみましょう:

docker run -it -p 8888:8888 jupyter/pyspark-notebook

これを実行すると Terminal に 8888 番にトークンがついた URL が流れてくるので,
(To access the notebook, ... のあたり)
おもむろにアクセスすると Jupyter のページが出てきて,
Jupyter Notebook でコーディングできる簡単環境のできあがりです.

Home_Page_-_Select_or_create_a_notebook.png

ここの New から Notebook: Python3 を選択すれば Notebook を開けます

Untitled_-_Jupyter_Notebook.png

試す

動くかどうかのテストコードは以下で,サンプルからとってきました (https://jupyter-docker-stacks.readthedocs.io/en/latest/using/specifics.html#in-a-python-notebook):

from pyspark.sql import SparkSession

spark: SparkSession = SparkSession.builder.appName("SimpleApp").getOrCreate()

# do something to prove it works
spark.sql('SELECT "Test" as c1').show()

SparkSession というやつはよくわからないけど,
Spark 自体のインスタンスみたいなものという認識です.

これを実行して表がでれば OK です:

Untitled_-_Jupyter_Notebook.png

データを扱う

こういうデータを対象にしてみましょう:

id name gender age
1 サトシ male 10
2 シゲル male 10
3 カスミ female 12

入力と定義

Python で素朴にデータを定義するとこうなりますね:

from typing import List, Tuple

Trainer = Tuple[int, str, str, int]
trainers: List[Trainer] = [
    (1, 'サトシ', 'male',   10),
    (2, 'シゲル', 'male',   10),
    (3, 'カスミ', 'female', 12),
]

各行の型は Python の typing でいう Tuple[int, str, str, int] となりますね.

で,Spark でもスキーマの定義があります:

from pyspark.sql.types import StructField, StructType, StringType, IntegerType

trainers_schema = StructType([
    StructField('id',      IntegerType(), True),
    StructField('name',    StringType(),  True),
    StructField('gender',  StringType(),  True),
    StructField('age',     IntegerType(), True),
])

これで Spark 側での列のスキーマを定義できます.

Python で定義したデータを Spark の DataFrame に変換するにはこうします:

from pyspark.sql import DataFrame

trainers_df: DataFrame = spark.createDataFrame(
    spark.sparkContext.parallelize(trainers),
    trainers_schema
)

これで trainers_df という DataFrame ができました.

データソースとして CSV とか MySQL とかそういうものから読み込めるので,
実際にはコード上で定義するよりそういうデータソースから読み込むことになるでしょう.
(場合により後述する JDBC とか,Hadoop の設定が必要です)

これをダンプして確認したい場合はこうします:

trainers_df.show()

そうすると,表に整形されたテキストが数行出力されます:

Untitled_-_Jupyter_Notebook.png

+---+------+------+---+
| id|  name|gender|age|
+---+------+------+---+
|  1|サトシ|  male| 10|
|  2|シゲル|  male| 10|
|  3|カスミ|female| 12|
+---+------+------+---+

集計と出力

ダンプではなく値をもらうには .collect() すればいいです:

result = trainers_df.collect()
print(result)

CSV に書き出すときはこういう雰囲気で DataFrame を書き出します:

trainers_df.coalesce(1).write.mode('overwrite').csv("path/to/output.csv")

入力同様,他にも S3, MySQL とか Elasticsearch とかいろいろ出力先がある雰囲気です.

.coalesce(1) はパーティションごとの分割されているデータを,
1つのパーティションに coalesce するというものです.
こうしないと,分割されたまま CSV 出力されます.

Hadoop の hdfs コマンドをつかって,
分割されたものを1つにまとめて取得するという手段もあります.

基本的に遅延評価になっていて,
.collect() みたいな操作をしてはじめて評価されるようになっているので,
そんなに頻繁に集計はしないはずです.

基本

これだけではただ表示しただけでまったく意味がないので適当な操作をしてみましょう:

trainers_df.createOrReplaceTempView('trainers');

male_trainers_df = spark.sql('''
    SELECT *
    FROM   trainers
    WHERE  gender = 'male'
''')
male_trainers_df.show()

これはこういう結果を得ます:

id name gender age
1 サトシ male 10
2 シゲル male 10

DataFrame.createOrReplaceTempView(name)DataFrame を,
一時的な SQL の View として登録することができるものです.

これで spark.sql(query) で登録した View を対象に SQL の操作した結果の DF を得ることができるので,
こうすれば,全く臆することなく慣れ親しんだ SQL を使って Spark を使うことができて,
心理的障壁も学習コストも低いというマジックになっています.

View に登録しなくても,DataFrame のままコードで記述するという方法もあります:

male_trainers_df = trainers_df.filter(trainers_df['gender'] == 'male')

こっちのほうが使いやすいケースもあるのでケースバイケースですね.

応用

SQL を使うことができるのだから,基本的な操作では別に問題ないのですが,
たいてい Spark を使いたいケースというのはなにかユーザー定義の操作をしたい状況になっていそうですね.

たとえば自分が過去やりたかったケースとしては,
「記事本文を形態素解析して分かち書きする」というものがあって,
これは SQL だけでは実現しがたいですね.

ただ,Python 上であれば MeCab があるので,
MeCab のライブラリを使って形態素解析してやれば何も考えなくても分解されてやってくるので,
僕のように全然わかってなくてもとりあえず MeCab に投げればいけるという手段をとれます.

そういう操作を Spark 上で DataFrame に対して行うにはどうすればいいかというと,
UDF (User-Defined Function) を定義するといいです.

(※ DataFrame ではなく RDD というものに対しては直接 lambda を適用できるという技がありますが,
  これはパフォーマンスが悪いというのがあります).

UDF を定義するには次のような定義を行います:

from pyspark.sql.functions import udf

@udf(StringType())
def name_with_suffix(name: str, gender: str) -> str:
    return name + {'male': 'くん', 'female': 'さん'}.get(gender, '氏')

spark.udf.register('name_with_suffix', name_with_suffix)

UDF となる関数に @udf(ReturnType) デコレーターを適用することで,
その関数は UDF として定義できるようになります.
それを Spark SQL で使うには spark.udf.register(udf_name, udf) して登録すれば,
COUNT() とかと同じ用にそのまま使えます.

ちなみにデコレーターを使わなくても,udf_fn = udf(fn) すれば既存の関数を適用できます.

この例としてあげたものは gender に応じて,
namegender に応じた suffix をつけるというものです.
この関数を UDF として適用してみましょう:

dearest_trainers = spark.sql('''
    SELECT name_with_suffix(name, gender)
    FROM   trainers
''')
dearest_trainers.show()

結果はこうなります:

name_with_suffix(name, gender)
サトシくん
シゲルくん
カスミさん

今回の例であれば SQL でも CASE を駆使して書けるというご意見がありますが,そのとおりです.

やりたいことによっては便利に使えるでしょう.

UDF

さて,前述した形態素解析して分かち書きするというものですが,
これはイメージとしてこのような関数になります
(実際には MeCab をカッコよく使います):

import re

# 半角/全角スペースや約物で文字列を分割する
@udf(ArrayType(StringType()))
def wakachi(text: str) -> List[str]:
    return [
        word
        for word
        in re.split('[  !…]+', text)
        if len(word) > 0
    ]

これを適用するのも同じくそのまま使えば OK です.
サンプルコードを今一度データを変更しつつ書いてみます:

Trainer = Tuple[int, str, str, int, str]
trainers: List[Trainer] = [
    (1, 'サトシ', 'male',   10, 'ポケモン ゲット だぜ'),
    (2, 'シゲル', 'male',   10, 'このおれさまが せかいで いちばん! つよいって ことなんだよ!'),
    (3, 'カスミ', 'female', 12, 'わたしの ポリシーはね… みず タイプ ポケモンで せめて せめて …せめまくる ことよ!'),
]

trainers_schema = StructType([
    StructField('id',      IntegerType(), True),
    StructField('name',    StringType(),  True),
    StructField('gender',  StringType(),  True),
    StructField('age',     IntegerType(), True),
])

trainers_df = spark.createDataFrame(
    spark.sparkContext.parallelize(trainers),
    trainers_schema
)
trainers_df.createOrReplaceTempView('trainers');

wakachi_trainers_df = spark.sql('''
    SELECT id, name, wakachi(comment)
    FROM   trainers
''')
wakachi_trainers_df.show()

ここでポイントになるのは,
今回の UDF は str を受け取って List[str] として展開するということですね.
これを実行してみるとこうなります:

id name wakachi(comment)
1 サトシ [ポケモン, ゲット, だぜ]
2 シゲル [このおれさまが, せかいで, い...
3 カスミ [わたしの, ポリシーはね, みず...

展開されたセルはリストになっていて,
列のなかに更に列がある入れ子状態になっています.

これをそれぞれの str を列として展開したい場合どうすればいいかというと,
展開する関数を更に適用すればいいです:

https://spark.apache.org/docs/2.0.2/api/java/org/apache/spark/sql/functions.html#explode(org.apache.spark.sql.Column)

from pyspark.sql.functions import explode

wakachi_trainers_df = spark.sql('''
    SELECT id, name, explode(wakachi(comment))
    FROM   trainers
''')
wakachi_trainers_df.show()

explode という関数があるので,
これを適用すれば入れ子になった要素がそれぞれの列として展開されます:

id name col
1 サトシ ポケモン
1 サトシ ゲット
1 サトシ だぜ
2 シゲル このおれさまが
2 シゲル せかいで
2 シゲル いちばん
2 シゲル つよいって
2 シゲル ことなんだよ
3 カスミ わたしの
3 カスミ ポリシーはね
3 カスミ みず
3 カスミ タイプ
3 カスミ ポケモンで
3 カスミ せめて
3 カスミ せめて
3 カスミ せめまくる
3 カスミ ことよ

ジョイン

さらなるポイントとして DataFrame どうしの JOIN ができます.
普通の MySQL とかの JOIN と変わらずに結合につかうカラムを指定して,
それをもとに DataFrame を結合するものです.

サンプルコードを更に追加して JOIN を使ってみます:

Pkmn = Tuple[int, int, str, int]
pkmns: List[Pkmn] = [
    (1, 1, 'ピカチュウ', 99),
    (2, 1, 'リザードン', 99),
    (3, 2, 'イーブイ',   50),
    (4, 3, 'トサキント', 20),
    (5, 3, 'ヒトデマン', 30),
    (6, 3, 'スターミー', 40),
]
pkmns_schema = StructType([
    StructField('id',         IntegerType(), True),
    StructField('trainer_id', IntegerType(), True),
    StructField('name',       StringType(),  True),
    StructField('level',      IntegerType(), True),
])
pkmns_df = spark.createDataFrame(
    spark.sparkContext.parallelize(pkmns),
    pkmns_schema
)
pkmns_df.createOrReplaceTempView('pkmns');

trainer_and_pkmns_df = spark.sql('''
    SELECT     *
    FROM       trainers
    INNER JOIN pkmns
          ON   trainers.id = pkmns.trainer_id
''')
trainer_and_pkmns_df.show()
id name gender age comment id trainer_id name level
1 サトシ male 10 ポケモン ゲット だぜ 1 1 ピカチュウ 99
1 サトシ male 10 ポケモン ゲット だぜ 2 1 リザードン 99
3 カスミ female 12 わたしの ポリシーはね… みず タ... 4 3 トサキント 20
3 カスミ female 12 わたしの ポリシーはね… みず タ... 5 3 ヒトデマン 30
3 カスミ female 12 わたしの ポリシーはね… みず タ... 6 3 スターミー 40
2 シゲル male 10 このおれさまが せかいで いちばん... 3 2 イーブイ 50

ちなみに INNER JOIN, OUTER JOIN の他に種類がいっぱいあります.
こちらの記事がわかりやすいので引用します:

https://qiita.com/ryoutoku/items/70c35cb016dcb13c8740

これで集合操作ができるので便利という感じです.

JOIN の概念はこのページのベン図がわかりやすいので引用します:

https://medium.com/@achilleus/https-medium-com-joins-in-apache-spark-part-1-dabbf3475690

ポイントとしてやはり JOIN はコストがかかっていて遅いです.
クラスタを組んでたとしたら,各所に分散したデータから見つけて JOIN して戻してとかそういう操作が行われているようです.

ので,後述するパフォーマンスチューニングが必要になってきます.

パフォーマンス

現実のケースとして,膨大なデータセットと格闘するのはそうとう辛いものがあります.
というのも,4時間とかかかるものだったら,最後の方で落ちたらまたやり直しかとなって,
二回ミスると一日の業務時間を捧げたことになってしまって残業が確定します.

なので,そういうパフォーマンスを改善するために,JOIN の効率を上げるようにデータを削減したり,
パーティションの区切りかたを変えたり,
パーティションをなるべくクラスタ上に小さく断片化させないような工夫が必要です.

Broadcast Join というもので,あえて全クラスタにデータセットを重複して配置することで,
JOIN 時にデータセットの検索のコストを下げるとかそういうものもあります.

重要なテクニックとして,
各チェックポイント的なところで適宜 DataFrame を .cache() しておくことで,
パフォーマンスが劇的に改善されるというものもあります.

パフォーマンスについての公式ページを見るとそういうテクニックがあって参考になります:

https://spark.apache.org/docs/latest/sql-performance-tuning.html#broadcast-hint-for-sql-queries

MySQL

さて,よくあるのが MySQL のデータベースから読み込んでうんぬんしたいというのがあります.
このケースでは MySQL を扱うために JDBC の MySQL コネクタを用意する必要がありますが,
こちらのかたのエントリと,その Docker イメージが参考になります:

しかしながら,MySQL は Spark では扱いづらいみたいなところもあります.
(いろいろハマりポイントがあります)

実際

Spark が威力を発揮するのは:

  • データがとにかくでかい
  • 適用したい処理が互いに依存しない
  • 各操作に副作用がなくて内部の操作で完結する (外の API への操作とかがない)

というものだと思っています.

あと Spark は複数台でクラスタを作って worker に仕事をさせるのがキモなので,
現実的には AWS におまかせするということで,Amazon EMR とか AWS Glue でやるのがよさそうですね.
ローカルだとクラスタを作らずに動くので,本気の巨大データを打ち込んでもパフォーマンスは出ずに恩恵にはあずかれないからです.

メモリの限界にぶち当たったり,
節約できても処理全体にバッチ流して二週間かかりますみたいな巨大データだと,
素朴なものでも自前で分割して複数プロセスにわけて実行とかすればできるかもしれないけど,
Spark にできることなら任せるのもよさげです.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Fargate 導入概要 メモ

Fargate のメモ

Fargate

AWS で簡単に Docker コンテナを動かせるサービス。
ただし、独自の概念が多く、慣れる必要がある。

筆者は以前に導入を試みて、挫折した。今日(2020/03/23)試してみたら、ドキュメントが良くなったのか、オンボーディングが良くなったのか、意外とすんなり理解できたので、記録しておく

なぜ Fargate なのか

前提として

  • PV 換算すると 月20万PVほどのアクセスのWebサービスを運用している
  • インフラエンジニアが不在なので、あまり煩わしい作業はしたくない
  • かといって、 EC2 はプロビジョニングとか面倒だし、ElasticBeanstalk はデプロイに時間かかるし小回りが効かない(経験談)
  • とりあえず動けばOKなサービスに適用する(本サービスではなく、LP + アルファな感じ。Next.js利用)

AWS には EKS など魅力的なサービスがあるが、意外と高い。コントロールパネルを動かしているだけで、15000円くらいかかるらしい(と、AWS の方に言われた)

さくっとサービス動かすだけなら、 Fargate の方が簡単で安いらしい(と、AWS の方に言われた)

値下げされたのも大きい。
EKS でも、 EKS + Fargate とかあるので、どうせならシンプルな方にしたい。

ので、再入門した。

独自概念の説明

登場人物は4人。

  • Cluster
  • Service
  • Task
  • Task Definition
  • Elastic Container Registry (ECR)

色々あって戸惑うが、このうち、 Service と Task Definition だけ考えれば良い(多分)

Task Definition

多分これが大事で、 Task Definition は Docker Image の定義だと考えれば良い。 Docker Image だけで完結できれば楽なのだが、バージョニングとかの関係でこうなっていると思う。

オブジェクト指向的に考えると、 Task Definition はクラス、 Task はクラスのインスタンス、である。
AWS EC2的に考えると、 Task Definition は AMI、Task はEC2 Instance、 である。

Service

基本的には、 Task Definition を定義して、 Service にその Task Definition を適用する。
Task Definition を Service に適用すると、 Task が作成される。つまり、 Private/PublicIP アドレスなどは Task に割り当てられる

ECR

ECR はただ Docker Image をプッシュするだけなので、特に難しいことは一切ない。ログインが必要なくらいだが、 「View push commands」 押せば難しいことはない。

image.png

運用

今回は、導入なので、非常に簡単な Web API をデプロイしてみることにした。具体的には下記。

app.js
require('http')
  .createServer((req, res) => {
    console.log(req.headers)
    res.end('ok')
  })
  .listen(3000)
FROM node:12.16.1-alpine
WORKDIR /app
ADD app.js /app
CMD node app.js

環境構築

Cluster + Service を作成し、 Task Definition を定義して、 ALB や Route 53 の設定をすれば、意外と素直に動いてくれる。

なお、Task に割り当てられた Public IP address を叩いても、動く。 Fargate 公式デモの Nginx のクラスタも、この方式で動く。 ALB の設定等で、時間を取らせたくないのだろう。

特に設定しなくても、ログが管理コンソール上で見れるのは嬉しい。

デプロイ

デプロイに若干はまった。

はまりどころとしては、イメージの更新がある。

まず、 ECR に新しい Docker Image をプッシュする。
次に、 Task Definition を update して、 Docker Image を更新する。ここまでは良い。

次に、単純に考えると、 Task を新しい Task Definition に Update と思うが、実際には Task ではなく Service をアップデートする 。多分、一度作成した Task は更新できないようになっているのだろう。 Immutable Infrastructure 的な。

管理コンソールでは、 Service を Update しようとすると下記のような画面が出てくるので、 Task Definition を最新にすれば良い。

image.png

これで Service を更新すると、自動的に新しい Task Definition を搭載した Task が作成され、デプロイされる。

今後

管理コンソールでサービス起動/更新するのは、割と簡単にできたが、以下の4点を調べる必要があるので、引き続き調査していく。

  • 1. Terraform で、どこまで/どうやって管理するのか。 ecs-cli との棲み分けは?
  • 2. 自動的なデプロイの仕組み。多分、上記で手動でやった ECR にプッシュ → Task Definitionのバージョン作成 → Serviceの更新 という流れは変わらないと思われるので、 ecs-cli でやればいけそう。
  • 3. Private IP Address が毎回変わるだが、これは固定する もしくは ALB Target Group に Fargate の Task を指定する方法はあるのだろうか・・・? Terraform なら設定できるのか、最悪 CLI で都度設定するか・・・
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DockerでDjango+Reactの環境を構築する

目標

将来的にDocker 環境下で Django と React を連携させて Google Kubernetes Engine にデプロイしてポートフォリオを作りたい。
完全に独学で進めていますので間違いがあれば何卒ご指摘を。。

環境

Windows10 home で Docker Toolbox を使用しています。
DjangoRestFrameworkとReactで超簡単なTodoアプリケーションを作成します。
アプリケーションのコードはDJANGO for APIsのChapter3を参考にしています。

まずはローカルで始める

まずはローカルで Django と React の連携を ToDO アプリを作成しながら考えてみます。

ディレクトリを作成する

Docker Toolbox は Virtualbox を使って docker ホストを立てています。
コンテナのボリュームのマウントはC:Users/環境下がデフォルトで設定されているので、
Docker toolbox を使用する場合は User ディレクトリ下をおススメします。

# プロジェクトフォルダの作成
mkdir gke-django
cd gke-djagno
# ディレクトリを作成する
mkdir backend
mkdir frontend

Backend の開発を進める

backend は Django-rest-framework で API を作成します。
まずは backend から環境を作成してみます。

settings.py

cd backend
# 仮想環境の作成
python -m venv venv
# 仮想環境の有効化
venv\Scripts\activate
# Pythonパッケージのインストール
python -m pip install --upgrade pip setuptools
python -m pip install django djangorestframework python-dotenv
# Djangoのプロジェクトを始める。
django-admin startproject config .

backend ディレクトリ下でdjango-admin startproject config .とすることで、
backend下にconfigというプロジェクトフォルダが作成されました。

config/settings.py を編集していきます。
まずは基本的なことだけ編集します。
SECRET_KEY は.env に追記するのでコピーしておきましょう。

# config/settings.py

"""
Django settings for config project.

Generated by 'django-admin startproject' using Django 3.0.4.

For more information on this file, see
https://docs.djangoproject.com/en/3.0/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.0/ref/settings/
"""

import os
from dotenv import load_dotenv  # 追加

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_DIR = os.path.basename(BASE_DIR)

# .envの読み込み
load_dotenv(os.path.join(BASE_DIR, '.env'))  # 追加

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('SECRET_KEY')

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ['*']


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'config.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],  # 変更
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'config.wsgi.application'


# Database
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}


# Password validation
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/3.0/topics/i18n/

LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.0/howto/static-files/

STATIC_URL = '/static/'

# 開発環境下で静的ファイルを参照する先
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]

# 本番環境で静的ファイルを参照する先
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')

# メディアファイルpath
MEDIA_URL = '/media/''

settings.py 内で参照している.envを作成します。

# .envファイルの作成
type nul > .env

# .envにコピペしておいたSECRET_KEYを追加する
SECRET_KEY = '+_f1u^*rb8+%cn-4o*kjn_(15*wspz0*!c)@=ll08odexo88a4'

todo アプリを始める

python manage.py startapp todos

settings.py にアプリケーションを追加します。

# conig/settings.py

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'todos.app.TodosConfig'  # 追加
]

model をつくって migration して admin に登録します。

# todos/models.py
from django.db import models


class Todo(models.Model):
    title = models.CharField(max_length=200)
    body = models.TextField()

    def __str__(self):
        return self.title
$ python manage.py makemigrations todos
Migrations for 'todos':
  todos\migrations\0001_initial.py
    - Create model Todo

$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, todos
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying sessions.0001_initial... OK
  Applying todos.0001_initial... OK
# todos/admin.py
from django.contrib import admin
from .models import Todo


admin.site.register(Todo)

管理ユーザーを作成して admin にログインして todo を 3 つほど登録します。

$ python manage.py createsuperuser
ユーザー名 (leave blank to use 'yourname'): yourname
メールアドレス: youraddress@mail.com
Password:
Password (again):
Superuser created successfully.

$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
March 10, 2020 - 23:41:26
Django version 3.0.4, using settings 'config.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

http://127.0.0.1:8000/adminにアクセスすると django-admin のログインページが開かれるので
createsuperuser で登録した内容でログインしてみましょう。
Todos を 3 つほど登録しておきましょう。

djangorestframework をはじめる

最初にpipでインストールしたrestframeworkを使用できるように config/settings.py を更新します。

# config/settings.py

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    # 3rd party
    'rest_framework',

    # Local
    'todos.apps.TodosConfig',
]

# 追加
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ]
}

rest_framework.permissions.AllowAnyは django-rest-framework が暗黙的に決めているデフォルトの設定'DEFAULT_PERMISSION_CLASSES'を解除するためのものです。
この設定はまだよくわかってないのですがとりあえず前に進みます。

todos/urls.py, todos/views.py, todos/serializers.pyを作成します。

URLs

config/urls.pyから各アプリケーションのurls.pyを追加します。

# config/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('todos.urls'))
]

todos/urls.pyを追加します。

$ type nul > todos\urls.py
# todos/urls.py
from django.urls import path
from .views import ListTodo, DetailTodo

urlpatterns = [
    path('<int:pk>/', DetailTodo.as_view()),
    path('', ListTodo.as_view())
]
Selializers

モデルインスタンスを json 形式へ変換するためのserializers.pyを追加します。

type nul > todos\serializers.py
# todos/serializers.py
from rest_framework import serializers
from .models import Todo


class TodoSerializer(serializers.ModelSerializer):
    class Meta:
        model = Todo
        fields = ('id', 'title', 'body')

fields = ('id', 'title', 'text')でのidは PrimaryKey を指定しない場合、
Django によって自動的に追加されます。

Views

Django Rest Framework でviews.pyを作成する場合はrest_framework.genericsの APIView を継承します。

# todos/views.py

from django.shortcuts import render
from rest_framework import generics
from .models import Todo
from .serializers import TodoSerializer


class ListTodo(generics.ListAPIView):
    queryset = Todo.objects.all()
    serializer_class = TodoSerializer


class DetailTodo(generics.RetrieveAPIView):
    queryset = Todo.objects.all()
    serializer_class = TodoSerializer

router など設定できていませんが、とりあえずは Todo アイテムを API として使用できるようになりました。
開発サーバーでhttp://127.0.0.1:8000/api/にアクセスすると APIview を確認することができます。

ここまでは Django でよくあるローカル環境での開発です。

CORS

CORS(Cross-Origin Resource Sharing)は React と Django を連携させる場合、React を起動したlocalhost:3000は Django の API サーバーlocalhost:8000
json のやり取りを行わせる必要があります。
django-cors-headersをインストールしましょう。

python -m pip install django-cors-headers

config/settings.pyを更新します。

# config/settings.py

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    # 3rd party
    'rest_framework',
    'corsheaders',

    # Local
    'todos.apps.TodosConfig',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'corsheaders.middleware.CorsMidddleware',  # 追加
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

##################
# rest_framework #
##################

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ]
}

CORS_ORIGIN_WHITELIST = (
    'http://localhost:3000',
)
Tests

テストを書きます。

# todos/test.py

from django.test import TestCase
from .models import Todo


class TodoModelTest(TestCase):

    @classmethod
    def setUpTestData(cls):
        Todo.objects.create(title="first todo", body="a body here")

    def test_title_content(self):
        todo = Todo.objects.get(id=1)
        excepted_object_name = f'{todo.title}'
        self.assertEqual(excepted_object_name, 'first todo')

    def test_body_content(self):
        todo = Todo.objects.get(id=1)
        excepted_object_name = f'{todo.body}'
        self.assertEqual(excepted_object_name, 'a body here')

$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.007s

OK
Destroying test database for alias 'default'...

うまくいったようです。

Frontend の開発を進める

Nodejs は予めインストールしておきましょう。

$ cd frontend
$ npx create-react-app .
$ yarn start

yarn run v1.22.0
$ react-scripts start
i 「wds」: Project is running at http://192.168.56.1/
i 「wds」: webpack output is served from
i 「wds」: Content not from webpack is served from
C:\--you-path--\gke-django-tutorial\frontend\public
i 「wds」: 404s will fallback to /
Starting the development server...
Compiled successfully!

You can now view frontend in the browser.

  Local:            http://localhost:3000
  On Your Network:  http://192.168.56.1:3000

Note that the development build is not optimized.
To create a production build, use yarn build.

フロントエンドのプロジェクトを React で開始することができました。
ブウラウザーでhttp://localhost:3000にアクセスすると React の Welcome ページが確認できます。

App.js

api のエンドポイントは以下のような形で api を返してくるので、これを意識しておきましょう。
まずは mock となるデータで試してみます。

[
  {
    "id": 1,
    "title": "test_title",
    "body": "body of test_title"
  },
  {
    "id": 2,
    "title": "test_title2",
    "body": "body of test_title2"
  },
  {
    "id": 3,
    "title": "test_title3",
    "body": "body of test_title3"
  }
]
// src/App.js

import React, { Component } from "react";
import axios from "axiso";
import "./App.css";

const list = [
  {
    id: 1,
    title: "test_title",
    body: "body of test_title"
  },
  {
    id: 2,
    title: "test_title2",
    body: "body of test_title2"
  },
  {
    id: 3,
    title: "test_title3",
    body: "body of test_title3"
  }
];

class App extends Component {
  constructor(props) {
    super(props);
    this.state = { list };
  }

  render() {
    return (
      <div>
        {this.state.list.map(item => (
          <div key={item.id}>
            <h1>{item.title}</h1>
            <p>{item.body}</p>
          </div>
        ))}
      </div>
    );
  }
}

export default App;

http://localhost:3000にアクセスするとモックデータが表示されました。
これを backendから取得したデータで表示させたいです。

axios

frontend でリクエストを叩くには build-in の Fetch APIaxios を使う方法がありますが、
今回は axios を使うことにします。

npm install axios --save
yarn start

App.js を書き換えます。

// src/App.js

import React, { Component } from "react";
import axios from "axios";
import "./App.css";

class App extends Component {
  state = {
    todos: []
  };

  componentDidMount() {
    this.getTodos();
  }

  getTodos() {
    axios
      .get("http://127.0.0.1:8000/api/")
      .then(res => {
        this.setState({ todos: res.data });
      })
      .catch(err => {
        console.log(err);
      });
  }

  render() {
    return (
      <div>
        {this.state.todos.map(item => (
          <div key={item.id}>
            <h1>{item.title}</h1>
            <p>{item.body}</p>
          </div>
        ))}
      </div>
    );
  }
}

export default App;

これでローカル環境で frontend から backend へ api をたたいて todo リスト一覧を表示させることができました。

超超単純な形ですが一応は Django と React の連携が取れました。

次はこれを Docker 化していきたいと思います。

Docker 化を進める

frontend, backend それぞれに Dockerfile を作成して backend コンテナ、frontend コンテナを作成してみます。

まずは Docker-compose で立ち上げられるところまでを考えていきます。

Backend の Docker 化

Dockerfile を書く前に Django 側でやっておきたいことがいくつかあります。

# 静的ファイ用のディレクトリ
$ mkdir backend\static
# 静的ファイルを全部集めてstaticifilesディレクトリに集められ
$ python manage.py collectstatic

本来であればデータベースの内容や settings.py を local と production で分けたりしますが、
まずは現在の形をそのまま Docker 化できることを考えます。

Dockerfile を backend ディレクトリ内に作成します。

$ type nul > backend\Dockerfile
# backend/Dockerfile

# set base image
FROM python:3.7

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# set work directory
RUN mkdir /code
WORKDIR /code

# install dependencies
COPY requirements.txt /code/
RUN python3 -m pip install --upgrade pip setuptools
RUN pip install -r requirements.txt

# Copy project
COPY . /code/

EXPOSE 8000

次にプロジェクトディレクトリに docker-compose.yml を設置して
docker-compose up で backend コンテナを起動できるようにしてます。

# docker-compose.yml
version: "3.7"

services:
  backend:
    build: ./backend/.
    command: python /code/manage.py runserver 0.0.0.0:8000
    volumes:
      - ./backend:/code
    ports:
      - "8000:8000"
$ docker-compose up

これでhttp://localhost:8000/api/にアクセスすると backend コンテナの DRF ビューにアクセスすることができました。
DockerToolbox を使っている場合は docker ホストの IP アドレスでアクセスしてください。

Frontend の Docker 化

続いて frontend 側を Docker 化していきます。

参考ページ
Dockerizing a React App
Creating an app with Docker Compose, Django, and Create React App
Using Docker for Node.js in Development and Production

frontend は React で構築しています。これを Docker 化するにはどうしたら良いでしょうか。
backend と同じように frontend ディレクトリに Dockerfile を作成します。

type nul > frontend\Dockerfile
# frontend/Dockerfile
FROM node:12.2.0-alpine

RUN mkdir /code
WORKDIR /code

# Install dependencies
COPY package.json /code/
COPY package-lock.json /code/
RUN npm install

# Add rest of the client code
COPY . /code/

EXPOSE 3000

これで node コンテナ内に package.json を使って同じ環境を構築することができます。
docker-compose.yml に frontend サービスを追加します。

# docker-compose.yml
version: "3.7"

services:
  backend:
    build: ./backend/.
    volumes:
      - ./backend:/code
    ports:
      - "8000:8000"
    stdin_open: true
    tty: true
    command: python /code/manage.py runserver 0.0.0.0:8000
    environment:
      - CHOKIDAR_USEPOLLING=true
  frontend:
    build: ./frontend/.
    volumes:
      - ./frontend:/code
      - /code/node_modules
    ports:
      - "3000:3000"
    command: npm start
    stdin_open: true
    tty: true
    environment:
      - CHOKIDAR_USEPOLLING=true
      - NODE_ENV=development
    depends_on:
      - backend

environment にCHOKIDAR_USEPOLLING=trueを追加することでイメージを再ビルドすることなく
ホットリローディングしてくれるようになります。

frontend に関しては node_modules が巨大であるため、これをマウントしたりコピーしたりすると
かなりの時間を要します。
したがって、.dockerignore を追加して node_modules をイメージビルドに使用しないようにしておきます(あってる?)。

$ type nul > frontend\.dockerignore
/node_modules

docker-compose up する前に

これで docker-compose up する準備が整いました、が、docker-toolbox を使っている場合は
ポートフォワーディングしているホスト名がlocalhostではありません。これをホスト IP に書き換える必要があります。
docker-machine ls コマンドを使って使用しているホスト IP を確認します。

backend/settings.py

手元のブラウザから frontend コンテナ ⇒backend コンテナにアクセスするため、
CORS_ORIGIN_WHITELISTに docker ホスト IP を追加する必要があります。

# backend/settings.py

CORS_ORIGIN_WHITELIST = (
    'http://localhost:3000',
    'http://192.168.99.100:3000',  # 追加
)

frontend/src/App.js

api のエンドポイントは docker ホスト IP になります。ここでは192.168.99.100:8000としています。

// src/App.js

import React, { Component } from "react";
import axios from "axios";
import "./App.css";

class App extends Component {
  state = {
    todos: []
  };

  componentDidMount() {
    this.getTodos();
  }

  getTodos() {
    axios
      .get("http://192.168.99.100:8000/api/") //変更
      .then(res => {
        this.setState({ todos: res.data });
      })
      .catch(err => {
        console.log(err);
      });
  }

  render() {
    return (
      <div>
        <h1>mother fucker!!?? </h1>
        {this.state.todos.map(item => (
          <div key={item.id}>
            <h1>{item.title}</h1>
            <p>{item.body}</p>
          </div>
        ))}
      </div>
    );
  }
}

export default App;

docker-compose up

docker-compose.yml のあるディレクトリ下で docker-compose up します。

$ docker-compose up --build

React のコンパイル完了には時間がかかります。

起動できたらhttp://localhost:3000にアクセスするとローカルで表示されていた内容が
再現されているはずです。

そしてGKEへ

できたら追加します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Codeigniter3のローカル環境をMAMPからDockerへ移した話

タイトルの通りですが、その際色々とつまづいたのでこちらに学習メモとして残しておきます。

最初に筆者のスペックを簡単に

・CodeigniterもPHPも初級者
・MAMP歴1週間、Docker利用は今回が初めて
・Qiita初投稿

てな状況でQiitaに投稿するなんて無謀かなーとは思いましたが、アウトプットの一環ということで?
流れを再現しつつ解決方法を書いていきます。

使用したものとバージョン

  • PHP 7.4
  • Codeigniter 3.1.6
  • Apache
  • Mysql 5.7

PHP、Apache、MysqlはMAMPの環境をそのまま持って行きたかったので特に選んだ理由はなし。
Codeigniterはcomposerを使ってインストールしたものになります。

ディレクトリ構造

codeigniter
├──application
├──bin
├──public
│   └──index.php
├──vendor
├──docker-compose.yml
└──php.ini
docker-compose.yml
version: '3'

services:
  php:
    container_name: CI-TL-L
    image: php:7.4-apache
    depends_on:
      - mysql
    volumes:
      - ./:/var/www/html/codeigniter
    working_dir: /var/www/html
    ports:
      - 8080:80
    restart: always
  mysql:
    image: mysql:5.7
    volumes:
      - ./mysql:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=「パスワード名」
      - MYSQL_DATABASE=「データベース名」
      - MYSQL_USER=「ユーザー名」
      - MYSQL_PASSWORD=「パスワード」
  phpmyadmin:
    image: phpmyadmin/phpmyadmin
    environment:
      - PMA_ARBITRARY=1
      - PMA_HOST=mysql
      - PMA_USER=root
      - PMA_PASSWORD=root
    links:
      - mysql
    ports:
      - 4040:80
    volumes:
      - ./phpmyadmin/sessions:/sessions
php.ini
[Date]
date.timezone = "Asia/Tokyo"
[mbstring]
mbstring.internal_encoding = "UTF-8"
mbstring.language = "Japanese"

コンテナ起動 1

この時点で一度コンテナを起動してみると...

codeigniter$ docker-compose up -d
An uncaught Exception was encountered
Type: Error
Message: Call to undefined function mysqli_init()
Filename: /var/www/html/codeigniter/vendor/codeigniter/framework/system/database/drivers/mysqli/mysqli_driver.php
Line Number: 135
Backtrace:
    File: /var/www/html/codeigniter/public/index.php
    
Line: 315

    Function: require_once

「定義されていないmysqli_init()が呼び出されています」というエラーが出ます。

コンテナ起動 1 解決方法

このエラーは指定したドライバーのmysqliがインストールされていないことに起因します。
解決するには
1.php-apache用のDockerfileを作成して
2.docker-compose.ymlを編集して上記のDockerfileでコンテナを起動します。

codeigniter
├──application
├──bin
├──public
│   └──index.php
├──vendor
├──docker-compose.yml
├──Dockerfile ---------追加
└──php.ini
Dockerfile
FROM php:7.4-apache

RUN apt-get update \
    && docker-php-ext-install mysqli
docker-compose.yml
version: '3'

services:
  php:
    container_name: CI-TL-L
    build: ./Dockerfile -------編集
    depends_on:
      - mysql
    volumes:
    .
    .
    .

これで先ほどのエラーは解消されたはずです。

コンテナ起動 2

ここでコンテナを再起動してみると...

codeigniter$ docker-compose down
codeigniter$ docker-compose up -d
エラー
Message: mysqli::real_connect(): (HY000/2002): No such file or directory
Filename: mysqli/mysqli_driver.php
Line Number: 203

また別のエラーが出ました。
mysqliのエラーのようなので「mysqliはインストールしたはずなのに...」とこのエラーで2日ほど潰されました。

コンテナ起動 2 解決方法

では実際mysqliに問題があったかというと実はそうではありません。
このエラーはdatabase.phpを編集することで解消できます。
ディレクトリパスはapplication>config>database.phpですね。

database.php
db['default'] = array(
    'dsn'   => '',
    'hostname' => 'mysql', -----編集
    'username' => 'ユーザー名',
    'password' => 'パスワード',
    'database' => 'データベース名',
    'dbdriver' => 'mysqli',

これで先ほどのエラーは解消できました。

詳しいことは分かりませんが、コンテナ間の通信ではdocker-compose.ymlで指定したサービス名でホスト名を指定する必要があるそうです。
今回の場合だと、docker-compose.ymlでmysqlコンテナにmysqlというサービス名を付けたのでこのような記述になるというわけですね。

コンテナ起動 3

そして、今度こそ3度目の正直ということで祈りながら起動...

terminal
codeigniter$ docker-compose down
codeigniter$ docker-compose up -d



エラー
A PHP Error was encountered
mkdir() invalid path
drivers/Session_file_driver.php
Line number: 136

はい、出ました。エラーです。
ここまできたらサクッと解決しちゃいましょう。

コンテナ起動 3 解決方法

解決するにはconfig.phpの編集が必要になります。

config.php
$config['sess_driver'] = 'files';
$config['sess_cookie_name'] = 'ci_session';
$config['sess_expiration'] = 7200;
$config['sess_save_path'] = BASEPATH.'sessions'; -------編集
$config['sess_match_ip'] = FALSE;
$config['sess_time_to_update'] = 300;
$config['sess_regenerate_destroy'] = FALSE;

正直このエラーはなぜエラーが出たのか、なぜこれで解消できたのか、分かりませんでした。すみません。

ということでエラー解消までの流れは以上になります。

余談

当初はローカル環境を移すだけでここまで苦労するとは思いませんでした。
検索しながら解決方法を模索しているときは真っ暗な迷路を進んでいるようで、「この道は合っているのか」「この先にゴールは本当にあるのか」と本当に辛かったと記憶しています。

しかし、終わった後に振り返ってみるとゴールすることだけが全てではないと分かりました。
エラーを解決するために右往左往するうちにDockerの使い方を覚えてCodeigniterの知識をも深めることができたからです。

この記事が僕と同じような初級プログラマーの精神的助けになれたら嬉しい限りです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker上でJupyter Notebookを使用しようとしてハマったときの対処

はじめに

Dockerの勉強をぼちぼち始めてて
なんとなくわかった気になったので実践編として
Docker上で何か使ってみたいものを立ち上げてみようと思い立ち
いざやってみたら、結構ハマったので何にハマったのかと
結局何が問題だったのかを記録として残す

参考にした資料

基本的な構築手順は
Dockerを使って5分でJupyter環境を構築する
を参考にしました。

結論

これがすべて。細かい解説はあと回しで最終的にやったことはこれだけになります。

### Docker Toolboxにて
1. $ docker pull jupyter/scipy-notebook
2. $ docker run -v `pwd`:/home/jovyan/work -p 10000:8888 --name jupyter jupyter/scipy-notebook
3. $ docker-machine ip
4. (Webブラウザ)[docker-machin ipで出力したIP]:10000にアクセス + トークン認証

どこでハマったのか?

Dockerで立ち上げたアプリをローカルPCのブラウザからアクセスできない。
この一点に尽きる。

何が問題だったのか?

参考にしたサイトでは
「ブラウザを起動して、http://127.0.0.1:1000でアクセスしたらOK」と書かれていたけど
自分の環境で起動したWebブラウザではなぜか「このページを表示できません」と表示され表示されなかった。

試したこと

いろんなサイトを参考に色々試した歴史をメモ
その1:WindowsDefenderが悪さしているかもしれない疑惑
⇒停止させてみたけど、解消されず。。。

その2:我が家で導入しているアンチウイルスソフトがアクセスブロックしているかもしれない疑惑
⇒停止させてみたけど、解消されず。。。
 一応、ソフトのアクセスログを見たけどブロックしている形跡なし

その3:サイトを参考に問題の切り分け
Dockerコンテナで起動したサーバにアクセスできないときの確認と対処方法に従って問題の切り分け
 ⇒以下のことがわかった。
  ・起動したコンテナ上でjupyter-notebookのプロセスは動作している
  ・curl http://127.0.0.1:8888では応答がなかったがエラーも出なかった。
   curl http://127.0.0.1:10000ではエラーが出た
   このことから、レスポンスらしいレスポンスはないけど、8888ポートで動作していることはわかった。
  ⇒このとき進展はなかったけど、「ひょっとしてローカルからコンテナへアクセスするときのIPが違うのでは?」
   という仮説が生まれた

その4:Dockerが払い出しているIPアドレスを確認してそこに向かってアクセスする
dockerで立ち上げたアプリケーションにアクセスできないという自分の悩みにピタっとハマるサイトを見つけた。
 これにより解決。

さいごに

記念にログインに成功した時の画面を表示。
コメント 2020-03-23 003609.png

めでたし、めでたし

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む