- 投稿日:2020-03-23T23:25:46+09:00
モダンな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: trueDockerfile
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
- 投稿日:2020-03-23T22:10:00+09:00
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.ymlversion: "3" services: python: image: python container_name: python build: context: ./docker/python dockerfile: Dockerfile.devcontainer/devcontainer.extend.ymlversion: "3" services: python: volumes: - ./:/workspace:cacheddocker/python/DockerfileFROM 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.py
がPATH
にpoetry
を追加しているのだろうと中身を確認したところ
~/.bash_profile
と~/.profile
でPATH
に~/.poetry/bin
を追加していた。ターミナルで
cat ~/.profile
して中身を確認すると、export PATH="$HOME/.poetry/bin:$PATH"
はちゃんと書き込まれている。「なんで
poetry
がcommand not found
になるんだ?」と考えながらsource ~/.profile
を実行すると、
それ以降poetry
コマンドは実行できてしまう。手順に間違いがあるのだろうと考え、比較のために非推奨の
pip install --user poetry
でインストールを行うも、
こちらもpoetry
コマンドは正常に実行できてしまう。ここから「どこかに間違いや勘違いがあるから
PATH
が通っていない」と思いながら四苦八苦を開始する。
この時、自分が使い慣れていないDocker周りに勘違いがあるのだろうという思い込みの元、
Dockerfile
にRUN . ~/.profile
を書いてみたりするなど試して間違いを重ねて時間を浪費する。何が間違っているか分からないまま小一時間が経過し、嫌になってきたので「
PATH
通せばいいや」と考え以下の変更を追加。Dockerコンテナビルドして、
poetry
コマンドが実行できることを確認。docker/python/DockerfileFROM 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のターミナル使ったときに、
いつでも発生する問題なのでは無いかと思い当たり、検索をかけるといくつも事例が出てくる。
- VSCodeのRemote-SSHでリモート先のLinuxのbash_profileを読み込む - Qiita
- vscodeのterminalで.bash_profileが読み込まれない - Qiita
- bash - VSCode Integrated Terminal Doesn't Load .bashrc or .bash_profile - Stack Overflow
完全に自分の見落としだった。
- 投稿日:2020-03-23T20:04:47+09:00
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がこれになります。サービスメッシュとは、各機能に分割されたアプリケーションを紐付け、一つのサービスとして管理してくるものです。
これらの技術で、クラウドサービスは成り立っているようです。
- 投稿日:2020-03-23T19:15:34+09:00
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_laravel
にlaradock
フォルダが作成されます
laradockフォルダ
に移動します$ cd laradocklaradockに.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
フォルダ内にはlaradock
とsample
フォルダが入っているはずですコンテナから出ます
$ 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で開発してみたいと言う方は是非、挑戦してみてください
- 投稿日:2020-03-23T13:15:17+09:00
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 dockercentosの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:71.準備
1-1.Dockerファイルを作る
Dockerfileを作成する
nanoを使いたのでインストール
yum install nanoDockerfileを作る
mkdir cent-tomcat cd cent-tomcat touch Dockerfile sudo nano DockerfileDockerfileの中身
DockerfileFROM 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.gz2.Dockerファイルを私用して、Dockerイメージ作成
$ cd <Dockerfileが存在するディレクトリ> $ docker build -t tomcat:1 . $ docker imagesDockerイメージが作成されていれば成功
[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ホストで以下のサイトが開けば成功
- 投稿日:2020-03-23T13:14:49+09:00
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 wgettomcatの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.gztomcatの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 bashtomcatコンテナ[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 bashTomcatのログ設定を変更し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.log2-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 tomcatSTATUSが 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 tomcatDockerイメージにする対象のコンテナ名を指定し、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 203MB4.作成した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-imagetomcat2 という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 tomcattomcat2コンテナに入りTomcatを起動してみる
tomcat2コンテナに入る
ホスト$ docker exec -it tomcat2 bashtomcat2コンテナ# cd /opt/apache-tomcat-9.0.31 # ./bin/startup.sh※ Dockerコンテナなら抜けるときは exit
ホストからWebサイトにアクセスできたら成功
5.後片付け
※注意※ Docker勉強メモ③に進むならこの作業はしない
作ったDockerイメージを削除します。
(AWS利用料を抑えるには、EC2インスタンスの削除が必要)
- Dockerコンテナ確認
docker ps -a
- Dockerコンテナ停止
docker stop
- Dockerコンテナ削除
docker rm
- Dockerイメージ確認
docker images
- Dockerイメージ削除
docker rmi
Dockerコンテナ停止
$ docker stop tomcat2Dockerコンテナ削除
$ docker rm tomcat2 $ docker rm tomcatDockerイメージ削除
sudo docker rmi <REPOSITORY> sudo docker rmi <centosのIMAGE ID> sudo docker rmi <REPOSITORY>:<TAG>
- 投稿日:2020-03-23T12:28:43+09:00
[初心者向け] 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/claspClaspのインストール
今後環境構築は全てDockerを利用していきます。
環境をコードとして残せますし、利用も簡単です。まずDockerfileを作成します。
今回は構築当時最新だったnode:13.7-alpineというコンテナを利用しました。DockerfileFROM node:13.7-alpine WORKDIR /workdir RUN yarn global add @google/claspDocker Composeも使います。
ボリュームの設定とか書いておけるので便利です。
コンテナは立ち上げっぱなしにして利用します。
VSCodeでコンテナに入って開発すると色々便利です。docker-compose.ymlversion: "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.mdClaspの認証
ClaspでGAS上のプロジェクトと連携するためにはアカウント認証が必要です。
認証情報はコンテナ上の/root/.clasprc.jsonに配置される事を踏まえてdocker-compose.ymlを更新します。docker-compose.ymlversion: "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.mdClasp 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/コード.jsfunction myFunction() { // コード追加 }次はGASにローカルの変更を反映させるためにpushしてみましょう。
$ docker-compose exec gas clasp push └─ src/appsscript.json └─ src/コード.js Pushed 2 files.GAS上で確認してみるとローカルでの変更が反映されています。
注意点としては、claspはGAS上とローカルの競合の解決まで行いません。
GAS上のコードの上書きだけでなく、ローカルに存在しないファイルもpushの時点で削除されるのでご注意ください。ここまでのディレクトリ構造
. ├── .clasp.json ├── .clasprc.json ├── .git ├── .gitignore ├── docker-compose.yml ├── Dockerfile ├── README.md └── src ├── appsscript.json └── コード.jsさあこれでローカル環境での開発が可能になりました。
claspには他にも色々な機能があるのでご興味があればGithubの公式リポジトリを御覧ください。
google/claspTypeScript導入
TypeScript: JavaScript For Any Scale.
TypeScriptを使って開発したいニーズも多いと思います。
私は型によるデバッグの簡単さとコード補完が欲しくて導入しました。実はClaspはTypeScriptに対応しており、特に新しいツールを導入せずともこの時点でTypeScriptは利用可能です。
clasp/typescript.md at master · google/clasp早速試してみましょう。
既存のコード.jsは削除して、claspの公式サンプルコードをpushしてみます。src/main.tsconst 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に変換されました。
ただし、ご覧の通り引数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/claspGASにはプロジェクト内に複数のスクリプトを作成出来ますが、スコープは共有されています。
Module機能を使って開発したい場合は、ローカル環境で構築する必要があります。webpack導入
私は最初はBrowserifyを使っていましたがwebpackの方が簡単だったのでこちらを紹介します。
webpackwebpackで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.lockDockerfileを修正し、ビルド時にpackage.jsonとyarn.lockを用いてパッケージをインストールするようにしましょう。
こうする事で、色々インストールしたコンテナを削除してしまってもビルド時に全パッケージをバージョンを含めて再構築することが出来ます。
Dockerのキャッシュはpackage.jsonの中身の更新まで認識してくれないのでビルド時にはキャッシュを使わないようにご注意ください。DockerfileFROM node:13.7-alpine WORKDIR /workdir RUN yarn global add @google/clasp + COPY package.json yarn.lock /workdir/ + RUN yarn installNode.jsパッケージはnode_modulesディレクトリに置かれます。
筆者はVSCodeでコンテナに入ってコーディングするため、コンテナ上のnode_modulesにのみパッケージを置いて、ローカルマシンのnode_modulesは空にしようと思います。
現在はルートディレクトリを全てコンテナ上にマウントしていますが、このままだとビルド時に作成されたnode_modulesディレクトリをローカルの空のnode_modulesで上書きしてしまいます。
これを回避するためにdocker-compose.ymlを修正します。docker-compose.ymlversion: "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を隔離したい - Qiitadocker-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-scriptpackage.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.jsconst 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.tsimport { greeter } from "./module"; function testGreeter() { const user = "Grant"; Logger.log(greeter(user)); }module.tsexport 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.lockwebpackを実行してみましょう。
$ 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.ここまでのディレクトリ構成は以下のようになっています。
. ├── .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.lockwebpackでアップしたファイルを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-pluginpackage.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.jsconst 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.tsimport { greeter } from "./module"; declare var global: any; global.testGreeter = () => { const user = "Grant"; Logger.log(greeter(user)); };再度webpackをして結果を確認します。
dist/main.gsfunction 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 DevelopersV8以前はletやconstすら使えなかったので、トランスパイルする必要がありました。
V8になって殆ど気にならなくなりましたが、最新の構文をGASで使いたいというニーズは今後もあると思うので、トランスパイルの方法をご説明します。今回は例のためにあえてV8を無効にして古いGASの記述に合わせる形で検証していこうと思います。
先程から使っているsrc/main.tsをサンプルに使います。
src/main.tsimport { 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" } }V8を無効にするとエラーになってしまいました。
当時はアロー演算子が使えなかったのです。次はtargetをes5にしてみます。
tsconfig.json{ "compilerOptions": { "module": "commonjs", - "target": "es2019" + "target": "es5" } }今度は無事実行できました。
上記のようにアロー演算子が置き換えられています。ただし、この状態でもArray.prototype.flat()のようなES5には存在しなかった関数は使うことが出来ません。
TypeScriptの場合、ts-polyfillを使えばtargetに存在しなかった関数の定義を埋め込む事で利用可能になります。
ts-polyfill - npm$ yarn add --dev ts-polyfilltsconfig.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.tsimport { 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()の実装が含まれているようです。このように、V8以前には存在しなかったArray.prototype.flat()の恩恵に預かることができました。
まとめ
いかがでしたでしょうか。
この記事で少しでもGAS利用者の生産性が向上すれば幸いです。
- 投稿日:2020-03-23T12:28:43+09:00
[初心者向け] GoogleAppsScript(GAS)の開発環境をインクリメンタルに構築(TypeScript / Module / Polyfill)
本記事について
GoogleAppsScript(以下GAS)の開発環境を初心者でも分かるように順番に整えていきます。
https://developers.google.com/apps-scriptこの記事の内容と同じ開発工程をGithubに残してあります。
cajonito/qiita_gas_local_developDockerを使って構築したので読者の方々の環境にあまり依存せずに構築出来ると思います。
対象
- GASは開発環境が悪いから触りづらいと思ってる方
- GASでGit使いたい、好きなエディタ使いたい、TypeScript使いたい等の方
- GASで環境を作ろうとして挫折した方
- 難しい事はいいからとっととソースコードよこせな方
各段階のゴール
以下の順番で実現していきます。
気になるところだけでもどうぞ。
- ローカル開発
- Gitや好きなエディタが使えるようになります
- TypeScript利用
- 型が使えるようになります
- Module利用
- 複数ファイルに分割して開発しやすくなります
- 最新のES構文をGASで使えるように
- 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/claspClaspのインストール
今後環境構築は全てDockerを利用していきます。
環境をコードとして残せますし、利用も簡単です。まずDockerfileを作成します。
今回は構築当時最新だったnode:13.7-alpineというコンテナを利用しました。DockerfileFROM node:13.7-alpine WORKDIR /workdir RUN yarn global add @google/claspDocker Composeも使います。
ボリュームの設定とか書いておけるので便利です。
コンテナは立ち上げっぱなしにして利用します。
VSCodeでコンテナに入って開発すると色々便利です。docker-compose.ymlversion: "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.mdClaspの認証
ClaspでGAS上のプロジェクトと連携するためにはアカウント認証が必要です。
認証情報はコンテナ上の/root/.clasprc.jsonに配置される事を踏まえてdocker-compose.ymlを更新します。docker-compose.ymlversion: "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.mdClasp 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/コード.jsfunction myFunction() { // コード追加 }次はGASにローカルの変更を反映させるためにpushしてみましょう。
$ docker-compose exec gas clasp push └─ src/appsscript.json └─ src/コード.js Pushed 2 files.GAS上で確認してみるとローカルでの変更が反映されています。
注意点としては、claspはGAS上とローカルの競合の解決まで行いません。
GAS上のコードの上書きだけでなく、ローカルに存在しないファイルもpushの時点で削除されるのでご注意ください。ここまでのディレクトリ構造
. ├── .clasp.json ├── .clasprc.json ├── .git ├── .gitignore ├── docker-compose.yml ├── Dockerfile ├── README.md └── src ├── appsscript.json └── コード.jsさあこれでローカル環境での開発が可能になりました。
claspには他にも色々な機能があるのでご興味があればGithubの公式リポジトリを御覧ください。
google/claspTypeScript導入
TypeScript: JavaScript For Any Scale.
TypeScriptを使って開発したいニーズも多いと思います。
私は型によるデバッグの簡単さとコード補完が欲しくて導入しました。実はClaspはTypeScriptに対応しており、特に新しいツールを導入せずともこの時点でTypeScriptは利用可能です。
clasp/typescript.md at master · google/clasp早速試してみましょう。
既存のコード.jsは削除して、claspの公式サンプルコードをpushしてみます。src/main.tsconst 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に変換されました。
ただし、ご覧の通り引数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/claspGASにはプロジェクト内に複数のスクリプトを作成出来ますが、スコープは共有されています。
Module機能を使って開発したい場合は、ローカル環境で構築する必要があります。webpack導入
私は最初はBrowserifyを使っていましたがwebpackの方が簡単だったのでこちらを紹介します。
webpackwebpackで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.lockDockerfileを修正し、ビルド時にpackage.jsonとyarn.lockを用いてパッケージをインストールするようにしましょう。
こうする事で、色々インストールしたコンテナを削除してしまってもビルド時に全パッケージをバージョンを含めて再構築することが出来ます。
Dockerのキャッシュはpackage.jsonの中身の更新まで認識してくれないのでビルド時にはキャッシュを使わないようにご注意ください。DockerfileFROM node:13.7-alpine WORKDIR /workdir RUN yarn global add @google/clasp + COPY package.json yarn.lock /workdir/ + RUN yarn installNode.jsパッケージはnode_modulesディレクトリに置かれます。
筆者はVSCodeでコンテナに入ってコーディングするため、コンテナ上のnode_modulesにのみパッケージを置いて、ローカルマシンのnode_modulesは空にしようと思います。
現在はルートディレクトリを全てコンテナ上にマウントしていますが、このままだとビルド時に作成されたnode_modulesディレクトリをローカルの空のnode_modulesで上書きしてしまいます。
これを回避するためにdocker-compose.ymlを修正します。docker-compose.ymlversion: "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を隔離したい - Qiitadocker-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-scriptpackage.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.jsconst 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.tsimport { greeter } from "./module"; function testGreeter() { const user = "Grant"; Logger.log(greeter(user)); }module.tsexport 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.lockwebpackを実行してみましょう。
$ 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.ここまでのディレクトリ構成は以下のようになっています。
. ├── .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.lockwebpackでアップしたファイルを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-pluginpackage.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.jsconst 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.tsimport { greeter } from "./module"; declare var global: any; global.testGreeter = () => { const user = "Grant"; Logger.log(greeter(user)); };再度webpackをして結果を確認します。
dist/main.gsfunction 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 DevelopersV8以前はletやconstすら使えなかったので、トランスパイルする必要がありました。
V8になって殆ど気にならなくなりましたが、最新の構文をGASで使いたいというニーズは今後もあると思うので、トランスパイルの方法をご説明します。今回は例のためにあえてV8を無効にして古いGASの記述に合わせる形で検証していこうと思います。
先程から使っているsrc/main.tsをサンプルに使います。
src/main.tsimport { 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" } }V8を無効にするとエラーになってしまいました。
当時はアロー演算子が使えなかったのです。次はtargetをes5にしてみます。
tsconfig.json{ "compilerOptions": { "module": "commonjs", - "target": "es2019" + "target": "es5" } }今度は無事実行できました。
上記のようにアロー演算子が置き換えられています。ただし、この状態でもArray.prototype.flat()のようなES5には存在しなかった関数は使うことが出来ません。
TypeScriptの場合、ts-polyfillを使えばtargetに存在しなかった関数の定義を埋め込む事で利用可能になります。
ts-polyfill - npm$ yarn add --dev ts-polyfilltsconfig.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.tsimport { 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()の実装が含まれているようです。このように、V8以前には存在しなかったArray.prototype.flat()の恩恵に預かることができました。
まとめ
いかがでしたでしょうか。
この記事で少しでもGAS利用者の生産性が向上すれば幸いです。
- 投稿日:2020-03-23T12:24:48+09:00
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 downhttps://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
を実行しただけで作成し直してくれた。コンテナを作り直してくれる条件が不明。
- 投稿日:2020-03-23T11:22:02+09:00
分子系統学演習をdockerで
分子系統学演習 データセットの作成から仮説検定まで 田辺晶史 2015/10/20
https://www.fifthdimension.jp/documents/molphytextbook/molphytextbook.ja.htmljava
最適な 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/edb5c5df4dbfc4a99ffbJDKの公式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 foundopenjdk
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/d140f519253f276b94e0DockerコミュニティがメンテしているOpenJDKイメージを使って遭遇した問題
https://matsumana.info/blog/2018/10/13/openjdk11-docker/自己参考資料(self reference)
生物系統計資料
https://qiita.com/kaizen_nagoya/items/6ebfb7bae0495424061e
- 投稿日:2020-03-23T11:22:02+09:00
分子系統学演習を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/edb5c5df4dbfc4a99ffbJDKの公式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 foundopenjdk
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/d140f519253f276b94e0DockerコミュニティがメンテしているOpenJDKイメージを使って遭遇した問題
https://matsumana.info/blog/2018/10/13/openjdk11-docker/自己参考資料(self reference)
生物系統計資料
https://qiita.com/kaizen_nagoya/items/6ebfb7bae0495424061e
- 投稿日:2020-03-23T10:53:05+09:00
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 でコーディングできる簡単環境のできあがりです.ここの
New
からNotebook: Python3
を選択すれば Notebook を開けます試す
動くかどうかのテストコードは以下で,サンプルからとってきました (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 です:
データを扱う
こういうデータを対象にしてみましょう:
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()そうすると,表に整形されたテキストが数行出力されます:
+---+------+------+---+ | 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
に応じて,
name
にgender
に応じた 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
を列として展開したい場合どうすればいいかというと,
展開する関数を更に適用すればいいです: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 イメージが参考になります:
- https://cloudfish.hatenablog.com/entry/2018/08/03/191424
- https://hub.docker.com/r/cloudfish/pyspark-notebook/dockerfile
しかしながら,MySQL は Spark では扱いづらいみたいなところもあります.
(いろいろハマりポイントがあります)実際
Spark が威力を発揮するのは:
- データがとにかくでかい
- 適用したい処理が互いに依存しない
- 各操作に副作用がなくて内部の操作で完結する (外の API への操作とかがない)
というものだと思っています.
あと Spark は複数台でクラスタを作って worker に仕事をさせるのがキモなので,
現実的には AWS におまかせするということで,Amazon EMR とか AWS Glue でやるのがよさそうですね.
ローカルだとクラスタを作らずに動くので,本気の巨大データを打ち込んでもパフォーマンスは出ずに恩恵にはあずかれないからです.メモリの限界にぶち当たったり,
節約できても処理全体にバッチ流して二週間かかりますみたいな巨大データだと,
素朴なものでも自前で分割して複数プロセスにわけて実行とかすればできるかもしれないけど,
Spark にできることなら任せるのもよさげです.
- 投稿日:2020-03-23T01:28:36+09:00
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」 押せば難しいことはない。
運用
今回は、導入なので、非常に簡単な Web API をデプロイしてみることにした。具体的には下記。
app.jsrequire('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 を最新にすれば良い。
これで 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 で都度設定するか・・・
- 投稿日:2020-03-23T01:13:12+09:00
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 frontendBackend の開発を進める
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 todossettings.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 = TodoSerializerrouter など設定できていませんが、とりあえずは 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 API
かaxios
を使う方法がありますが、
今回はaxios
を使うことにします。npm install axios --save yarn startApp.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 Productionfrontend は 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: - backendenvironment に
CHOKIDAR_USEPOLLING=true
を追加することでイメージを再ビルドすることなく
ホットリローディングしてくれるようになります。frontend に関しては node_modules が巨大であるため、これをマウントしたりコピーしたりすると
かなりの時間を要します。
したがって、.dockerignore を追加して node_modules をイメージビルドに使用しないようにしておきます(あってる?)。$ type nul > frontend\.dockerignore/node_modulesdocker-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 --buildReact のコンパイル完了には時間がかかります。
起動できたら
http://localhost:3000
にアクセスするとローカルで表示されていた内容が
再現されているはずです。そしてGKEへ
できたら追加します。
- 投稿日:2020-03-23T00:59:24+09:00
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.inidocker-compose.ymlversion: '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:/sessionsphp.ini[Date] date.timezone = "Asia/Tokyo" [mbstring] mbstring.internal_encoding = "UTF-8" mbstring.language = "Japanese"コンテナ起動 1
この時点で一度コンテナを起動してみると...
codeigniter$ docker-compose up -dAn 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.iniDockerfileFROM php:7.4-apache RUN apt-get update \ && docker-php-ext-install mysqlidocker-compose.ymlversion: '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.phpdb['default'] = array( 'dsn' => '', 'hostname' => 'mysql', -----編集 'username' => 'ユーザー名', 'password' => 'パスワード', 'database' => 'データベース名', 'dbdriver' => 'mysqli',これで先ほどのエラーは解消できました。
詳しいことは分かりませんが、コンテナ間の通信ではdocker-compose.ymlで指定したサービス名でホスト名を指定する必要があるそうです。
今回の場合だと、docker-compose.ymlでmysqlコンテナにmysqlというサービス名を付けたのでこのような記述になるというわけですね。コンテナ起動 3
そして、今度こそ3度目の正直ということで祈りながら起動...
terminalcodeigniter$ 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の知識をも深めることができたからです。この記事が僕と同じような初級プログラマーの精神的助けになれたら嬉しい限りです。
- 投稿日:2020-03-23T00:48:13+09:00
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で立ち上げたアプリケーションにアクセスできないという自分の悩みにピタっとハマるサイトを見つけた。
これにより解決。さいごに
めでたし、めでたし