20200709のdockerに関する記事は17件です。

Ubuntu18.04のDockerイメージを起動する

目的

Ubuntu 18.04 LTS にdockerをインストールするの続き〜Ubuntu18.04のDockerイメージを起動するまでの備忘録です

準備

Dockerfile、docker-compose.yml、起動時に実行するスクリプト(start.sh)を準備する

Dockerfile
FROM ubuntu:18.04

# コンテナ起動時に実行する
ADD  start.sh  /
RUN  chmod +x /start.sh
CMD  ["/start.sh"]
docker-compose.yml
version: '2.0'

services:
  myproject:
     image: mycontainer:latest
     container_name: mycontainer
start.sh
#!/bin/bash
echo "test" > test.txt

#コンテナを起動し続ける
tail -f /dev/null

コンテナ起動

以下を実行してコンテナ起動すればOK

# build
docker build -t mycontainer .
# run
docker-compose up -d
# exec
docker exec -it mycontainer /bin/bash

docker composeのインストール

Install Docker Composeを参照

# sudo curl -L "https://github.com/docker/compose/releases/download/1.26.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
# chmod +x /usr/local/bin/docker-compose
# docker-compose -v
docker-compose version 1.26.2, build eefe0d31

docker-compose up 後に、コンテナが(container name) exited with code 0 となる場合

$ docker-compose up
Creating ubuntu-container ... done
Attaching to ubuntu-container
ubuntu-container exited with code 0

正常終了しているので、コンテナ起動後に以下を実行できるようにする。

#コンテナを起動し続ける
tail -f /dev/null

参考

Ubuntu 18.04 の Docker イメージの作成(Ubuntu 上)
【Linux入門】DockerでUbuntu18.04を構築する方法
Install Docker Compose
docker run -it の「-it」とはなにか
docker-compose upするとコンテナが一瞬でexited with code 1する話
docker コンテナ起動時のシェル実行について
dockerコンテナ起動時にシェルを実行する

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

【AWS】 CircleCI/CD 自動デプロイでハマったエラーの解消【Capistrano 】

Railsで作成したポートフォリオをAWS EC2にデプロイし、最終段階でCicleCIによるCD(自動デプロイ)を導入していて、大きくハマった点があった為、備忘録かつ誰かの一助になればと思い、記します(理解が誤っている場合がありますので、修正があれば都度修正します)。

自動デプロイの記事について、先人の記事を見ながら実装を進めていたのですが、ssh keysのインストールはできており、最終段階の「Capistrano deploy」で以下のようなエラーがおき、長らくハマっていました。

スクリーンショット 2020-07-04 18.48.16.png

Net::SSH::AuthenticationFailed: Authentication failed for user エラー

文面からSSH key関連のエラーであることはわかります。しかし、前タスクでInstalling additional ssh keysはSuccessしているしなぜ?と思っていました。ググっているとここでハマる方が多いようでした。

結論としては、

  • CircleCIに登録したKeyが間違っていた
  • その結果、Fingerprintとしてconfig.ymlに記載していた値が違う為、デプロイできなかった
config.yml
   - add_ssh_keys:
          fingerprints:
            - "XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX" # こちらの値

ということです。

最初にCircleCIに登録していた鍵は、AWS EC2を構築しているときにダウンロードした、「hoge.pem」でした。

先人の記事を参考に進めていて、書いてあるとおり何も疑わずにEC2からダウンロードしていたhoge.pemをcatしてコピーし、CircleCIに登録していました。

最終的に、自分のケースでは「pem形式の鍵をCircleCIに登録する」という点はあっていたのですが、catする鍵が違っていたようです。

CircleCIに登録すべき鍵は、ssh-keygenして作成した「~rsa」(公開鍵ではない)で、ローカルからEC2にログインする際に使用する秘密鍵でした。

EC2にSSH接続する際、初期の方で、EC2にログイン→.sshに移動→authorized_keys(中身はssh-keygenして作成した~rsa.pub)に対応する秘密鍵を、 ローカルの.sshに置いて~rsaでログインしていると思うのですが、CircleCIに登録すべきSSH keyはそちらでした。

しかし、ssh-keygenして生成した~rsaは、OPENSSH形式であり、そのままではCircleCIに登録できません。実際に登録しようとすると怒られて登録できませんでした。
※CircleCIはOPENSSH形式の鍵に現時点では対応していないとメンターさんから伺いました。

登録できない?じゃあ結局どの鍵を登録すればいいんじゃ!とそこでもハマっていたのですが、
結果的にどうするのかというと、

rsa鍵(OPENSSH形式)をpem形式(catするとBEGIN RSA PRIVATE KEY---で始まる)の鍵に変換する」というシンプルな解決方法でした。

こちらの記事を参考に、
SSH KeyをOpenSSH形式化からPEM形式に変換

local
ssh-keygen -p -m PEM -f hoge_key_rsa

こちらのコマンドで、rsaをpem形式に変換できると思います。
OPENSSH形式ではなくなっていますが、今までのように

local
$ ssh hoge_key_rsa
Last login: ------- 2020 from ec2-xx-xx-xx-xx.compute-1.amazonaws.com

       __|  __|_  )
       _|  (     /   Amazon Linux 2 AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-2/

EC2にSSH接続は可能です。

pem形式に変換されたhoge_key_rsaをcatすると、

hoge_key_rsa
-----BEGIN RSA PRIVATE KEY-----
省略
==
-----END RSA PRIVATE KEY-----

と出てくると思います。こうなっていればOKで、ハイフンを含めてBEGINからENDまで全てをコピーし、CircleCIに追加します。

ちなみにOPENSSH形式のままの鍵(rsa)を開くと、

local
-----BEGIN OPENSSH PRIVATE KEY-----
...

と出てきますが、このままではCIrcleCIに登録できないとういう訳です。

先ほどのcatしてコピーしたコードを、CircleCIに登録します。
そして生成されたFingerprintを、localのCircleCIの設定ファイルであるconfig.ymlに記載することで、エラーが解消し、無事デプロイが完了しました。

随時加筆・修正していきたいと思います!

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

【入門】Dockerfileを記述するときの最低限知識

 はじめに

Docker は Dockerfile というテキストファイルから命令を読み込んで、自動的にイメージをビルドしますねdocker buildを実行すると、順次コマンドライン命令を自動化した処理が行われて、ビルド結果となるイメージが得られます。
初心者がまず、dockerfileを記述するときのコマンドをまとめました。

FROM

ベースとなるDockerのimageを設定します。
FROM <image名>[:<tag>]
ubuntuベースの場合はこちら。:の後にtagを指定することもできます。
デフォルトはlatest

FROM ubuntu:latest

RUN

ビルド後にコンテナに対してLinaxのコマンドを実行する。

FROM ubuntu
RUN mkdir hoge_dir
RUN echo 'hogehoge' > hoge.txt

このように書くことで、できたコンテナ内にhoge_dirディレクトリを作り、hogeテキストファイルを作成し、hogehogeと書き込んでくれます。

ただ、注意しなければならないのは、RUNごとにlayerができてしまう点。一つのコマンドごとにRUNを指定しているとlayerが積み重なってdocker imageが重くなってしまうので

FROM ubuntu
RUN mkdir hoge_dir && echo 'hogehoge' > hoge.txt

&&でつなぎましょう。

CMD

大抵はDockerfileの最後に記述してあり、こちらもデフォルトのLinaxコマンドを設定するという意味では、RUNと似たようなものです。

ただ、CMDはlayerを作らないという違いがあります。
どうやって使い分ければ良いのかと言えば、自分はざっくりとRUNは残しておきたいものでCMDはそうじゃないものに使うという認識でいいのかなと思ってます。

RUN apt-get update
CMD ["/bin/bash"]

上の場合は一度ビルドしてupdateしたら、二度同じことすると時間がかかるので、layerに残して置きたいですよね。
逆に下の場合はbash使うよという指示だけなんで、layerに残す必要はないです。

また、似たものでENTRYPOINTというものもあります。
CMDは設定したコマンドをrunの時に上書きできてしまいますので、そういった場合はENTRYPOINTで設定しましょう。

COPYとADD

どちらもローカルからコンテナにファイルをコピーできるものです。
ADDの方ができることが多いです。

COPY <src> <dist>
ADD  <src> <dist>

公式でも基本はCOPYを使うことを推奨しています。
コピー元がファイルでtar形式なんかだとADDが勝手に展開してくれます。
自分はADDは大きなファイルを圧縮してコンテナに持っていきたい時に使っています。

WORKDIR

コンテナ内での実行ディレクトリを変更してくれるものです。例えばdockerfileに

RUN mkdir test && cd test
RUN touch hoge.txt  

としてもhoge.txtはroot直下に作られてしまいます。まあ &&で繋げればいい話なんですけど、それじゃdockerfileに書き足していくときに毎回移動しないといけないんでWORKDIRを使うみたいです。

WORKDIR <絶対パス>

絶対パスにしてくださいね。

ENV

ENVは環境変数の設定をしてくれます。

ENV <key> <value>

例えば自分はENV PATH /anaconda3/bin:$PATH
のようにPythonのPATHを通すときに使いました。

最後に

自分がとりあえず必要になって触れたコマンドをまとめてみました。
ENTRYPOINTとかLABELとかは必要になってからでいいかなあ。

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

Docker。自分がよく使うコマンドをaliasに設定「説明付き」

はじめに

私が思うよく使うDockerコマンドは、bash_profileや、bash_rcでaliasに設定して使っています。

自分の誹謗録ようなものですが、興味のある方はお試しください。
そして、もし有用なaliasを使っているならぜひ共有頂けると嬉しいです。

自分も勉強目的で書いているので、間違った情報があるかもしれません。
そこはご指摘頂けるとありがたいです。

alias設定内容

~/.bash_profile

以下の構文を追加

if [ -f ~/.myalias ]; then
  source ~/.myalias
fi

~/.myalias

● gist
https://gist.github.com/genie-oh/d73a224e7cb3cffab2868182eb79ccad

# show full command & execute 
alias al='_(){ CMD=$(alias | grep "alias $1=" | cut -d = -f 2- | sed "s:^.\(.*\).$:\1:"); ARG=$(echo $@ | sed "s/^$1//"); CMD="${CMD}${ARG}"; echo "execute : ${CMD}"; echo " "; bash -c "${CMD}"; };_'

# docker
alias al-dock='cat ~/.myalias | grep dock | sed "s/=/      \t\= /"'
alias dock='docker'
alias docki='docker images'
alias dockps='docker ps -a'
alias dockrrm='docker run --rm'
alias dockeit='docker exec -it'
alias dockrm='docker rm -f'
alias dockrmi='docker rmi -f'
alias dockrma='docker rm -f $(docker ps -aq)'
alias dockrmia='docker rmi -f $(docker images -aq)'
alias dockins='docker inspect'
alias dockip='docker inspect --format="{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}"'
alias docklogs='dock logs --tail 50 --follow --timestamps'
alias dockc='docker-compose'
alias dockcb='docker-compose build'
alias dockcu='docker-compose up -d'
alias dockcd='docker-compose down'
alias dockccl='grep container_name docker-compose.yml'

※説明

al-dock

Docker関連aliasのリストを表示

$ al al-dock
execute : cat ~/.myalias | grep dock | sed "s/=/      \t\= /"

# docker
alias al-dock           = 'cat ~/.myalias | grep dock | sed "s/=/      \t\= /"'
alias dock              = 'docker'
alias docki             = 'docker images'
alias dockps            = 'docker ps -a'
alias dockrrm           = 'docker run --rm'
alias dockeit           = 'docker exec -it'
alias dockrm            = 'docker rm -f'
alias dockrmi           = 'docker rmi -f'
alias dockrma           = 'docker rm -f $(docker ps -aq)'
alias dockrmia          = 'docker rmi -f $(docker images -aq)'
alias dockins           = 'docker inspect'
alias dockip            = 'docker inspect --format="{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}"'
alias docklogs          = 'dock logs --tail 50 --follow --timestamps'
alias dockc             = 'docker-compose'
alias dockcb            = 'docker-compose build'
alias dockcu            = 'docker-compose up -d'
alias dockcd            = 'docker-compose down'
alias dockccl           = 'grep container_name docker-compose.yml'

dockc, dockcb, dockcu, dockcd

docker-compose関連
docker-composeのbuild,up,down

alias dockc             = 'docker-compose'
alias dockcb            = 'docker-compose build'
alias dockcd            = 'docker-compose down'
alias dockcu            = 'docker-compose up -d'

dockccl

docker-compose.yml内のコンテナーネームのリストを出力

alias dockccl           = 'grep container_name docker-compose.yml'
$ al dockccl
execute : grep container_name docker-compose.yml

    container_name: lamp-web
    container_name: lamp-php
...省略

docki

docker imageのリストを出力

alias docki             = 'docker images'
$ al docki
execute : docker images

REPOSITORY               TAG                 IMAGE ID            CREATED             SIZE
centos                   7                   b5b4d78bc90c        8 weeks ago         203MB
docker-lamp-test_php     latest              e6d67d8f48cf        17 minutes ago      666MB
...省略

dockps

dockerのコンテナーリストと実行状態を表示

alias dockps            = 'docker ps -a'
$ al dockps
execute : docker ps -a

CONTAINER ID        IMAGE                    COMMAND                  CREATED             STATUS                          PORTS                     NAMES
6116324251dd        docker-lamp-test_php     "docker-php-entrypoi…"   17 minutes ago      Up 17 minutes                   0.0.0.0:32769->9000/tcp   lamp-php
...省略

dockeit

実行中のコンテナーで、interactive&ttyモードでコマンド実行
docker exec --interective --tty

alias dockeit           = 'docker exec -it'
$ al dockeit lamp-php bash
execute : docker exec -it lamp-php bash

root@37bcdbd02f92:/var/www/html# ls
config  gulpfile.js  home_root  home_sub  nodeapp

dockrrm

コンテナーでコマンドを実行後、即時にコンテナーを終了する
コンテナー作成▶コマンド実行▶コンテナー終了

alias dockrrm           = 'docker run --rm'
$ al dockrrm composer php -v
execute : docker run --rm composer php -v

PHP 7.4.7 (cli) (built: Jun 11 2020 18:58:32) ( NTS )
...省略

dockins

コンテナーの状態を確認

alias dockins           = 'docker inspect'
$ al dockins lamp-php
execute : docker inspect lamp-php

[
    {
        "Id": "6116324251ddffc8090cc605d391f89a951aeb46d32636dd62476a225a894c51",
        "Created": "2020-07-01T13:30:27.11145Z",
...省略

dockip

コンテナーの状態で、IPだけを取得

alias dockip            = 'docker inspect --format="{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}"'
$ al dockip lamp-php
execute : docker inspect --format="{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" lamp-php

172.30.0.5

docklogs

コンテナーのログを確認

alias docklogs          = 'dock logs --tail 50 --follow --timestamps'
$ al docklogs lamp-php
execute : docker logs --tail 50 --follow --timestamps lamp-php

2020-07-01T13:30:28.338589500Z [01-Jul-2020 22:30:28] NOTICE: fpm is running, pid 1
2020-07-01T13:30:28.340324600Z [01-Jul-2020 22:30:28] NOTICE: ready to handle connections

dockrm, dockrmi

特定のコンテナー、またはイメージを削除

alias dockrm            = 'docker rm -f'
alias dockrmi           = 'docker rmi -f'

dockrma, dockrmia

すべてのコンテナー、またはイメージを削除

alias dockrma           = 'docker rm -f $(docker ps -aq)'
alias dockrmia          = 'docker rmi -f $(docker images -aq)'
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

docker エラー (bad flag syntax: -----END)

よくAWSにふれるものです。

docker build(run)する際に次のようなエラーにハマりました。

問題

以下のようなコマンド

$ docker build --build-arg PRIVATE_KEY=$PRIVATE_KEY
bad flag syntax: -----END
See 'docker build --help'.

ちなみにPRIVATE_KEYはpem形式です。

-----BEGIN PRIVATE KEY-----
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
...
-----END PRIVATE KEY-----

解決

pem形式はスペースを含むので、環境変数には引用符をつけなくてはなりません。

$ docker build --build-arg PRIVATE_KEY="$PRIVATE_KEY"

以上で解決。

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

Dockerとは何か?[入門編]

Dockerについて

コンテナ仮想化を用いてアプリケーションを開発・配置・実行するためのオープンソフトウェアあるいはオープンプラットフォームである。(wikipediaより)

コンテナ?仮想化?何それ状態だと思います。次にそれらについて解説します。

コンテナとは

OS上に区画を作り、アプリケーションを動作させるために必要なライブラリやアプリケーションなどを一つにまとめたものである。

仮想化とは

コンピュータリソースを抽象化することである。
コンピュータそのものをハードウェアからソフトウェア化することを目指す。

Dockerのメリット

まずよく比較されるサービスとしてVMwareVirtualBoxなどの仮想マシンがあります。
こちらがメリットです。

  1. 簡単に環境開発
  2. 軽量でスピーディーな開発
  3. ハードウェアの資源削減
  4. 共有化されたシステム
  5. イミュータブル・インフラストラクチャである点
  6. 物理サーバの考慮を後回しにできる点
  7. モダンなエコシステム

Dockerのデメリット

  1. 同一のOSを利用しなければならない
  2. 提供できるホストの種類が少ない
  3. Windowsでの利用ハードルの高さ

仮想マシン(VMware・VirtualBox)について

ここからしばらくDockerから少し離れるので気になる方以外は飛ばしてください。

仮想マシン

仮想マシン技術による仮想化の方式としてホストOS型ハイパーバイザー型があります。

ホストOS型

本来入っているOS(WindowsやMac・Linux)上で仮想化ソフトを使い、その上で仮想マシンを立ち上げる方法。

メリット

・ホストOSが対応していれば、ハードウェア環境にほぼ依存しない。
・管理マシンを必要としない。
・ハイパーバイザー型と比べて手軽に環境構築が可能。

デメリット

・ホストOSが必要な点
・物理リソースが必要な点

ハイパーバイザー型

サーバへ直接インストールし仮想マシンを稼働させる方法

メリット

・ リソースを有効活用できる点
・システムにかかるコストを削減

デメリット

・ 運用コストが割高になる可能性
・ 種類によっては知識や技術が必要な点
(高度な管理を実現するためのツールが標準装備されていない可能性があるため。)

デメリット

見にくいと思うので、表にしてまとめてみます。

仮想マシン コンテナ技術
起動 遅い(パフォーマンスによる) 早い
 OS 各仮想マシンに付き一つ 一つで複数稼働可能
コスト 大きい 小さい
構成 ハードウェアに依存 ハードウェアに依存しない
メモリ消費 大きい 小さい

続けてDockerでよく使われる言葉について説明します。

Dockerでよく使われるワード

Dockerイメージ

OSやアプリケーションを含んだテンプレートのベースイメージ。読み取り専用。
構築(build)コンポーネント

Dockerコンテナ

分離された名前空間とアプリケーションの実行環境
実行(run)コンポーネント

DockerHub(Dockerレジストリ)

公開されているDockerイメージを提供しているプラットフォーム
配布(distribution)コンポーネント

Dockerクライアント

ユーザがコマンドを反抗し、Dockerデーモンと通信を行う

Dockerデーモン

ホストマシン上で動いている。直接ユーザとはやりとりはしない。管理者的な役割を果たす。

次回は実際にDockerを動かしていきます。

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

CentOS 7に「Docker」をインストールしてDocker Swarm Modeを設定します。

Docker Swarmは、Dockerコンテナのネイティブクラスタリングツールで、Dockerノードのクラスタを1つの仮想システムとして管理することができます。
このチュートリアルでは、CentOS 7上で3ノードのDocker Swarmクラスタを構成する方法をステップバイステップで説明します。

本ブログは英語版からの翻訳です。オリジナルはこちらからご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。

必要条件

  • CentOS 7をインストールした3台のAlibabaクラウドインスタンス。そのうち1台のサーバーがManagerノードとして動作し、2台のサーバーがWorkerノードとして動作します。
  • すべてのインスタンスに固定IPアドレスを設定しています。ここでは、Managerノードに192.168.0.102、Workerノード1に192.168.0.103、Workerノード2に192.168.0.104のIPアドレスを使用します。

アリババクラウドECSインスタンスの起動

まず、https://ecs.console.aliyun.com/?spm=a3c0i.o25424en.a3.13.388d499ep38szxAlibaba Cloud ECS Consoleにログインします。新しいECSインスタンスを作成し、少なくとも2GBのRAMを搭載したオペレーティングシステムとしてCentOS 7を選択します。ECSインスタンスに接続し、rootユーザーとしてログインします。

CentOS 7インスタンスにログインしたら、以下のコマンドを実行して、ベースシステムを最新の利用可能なパッケージでアップデートします。

yum update -y

はじめに

始める前に、各ノードがホスト名で通信できるように、各ノードの/etc/hostsファイルを設定しておく必要があります。

以下のように各ノードの/etc/hostsファイルを更新します。

nano /etc/hosts

192.168.0.102  managernode
192.168.0.103  workernode1
192.168.0.104  workernode2

保存して終了したらファイルを閉じます。

次に、/etc/hostsファイルの通りに各ノードのhostnameを設定する必要があります。

以下のコマンドを各ノードで1つずつ実行することで行うことができます。

管理者ノード

hostnamectl set-hostname managernode

Worker node1

hostnamectl set-hostname workernode1

Worker node2

hostnamectl set-hostname workernode2

Dockerエンジンのインストール

次に、すべてのノードにDocker Community Editionをインストールする必要があります。デフォルトでは、最新版のDocker CEはCentOS 7のリポジトリにはありません。そのため、Docker CEのリポジトリをシステムに追加する必要があります。

これは以下のコマンドを全ノードで実行することで行うことができます。

wget https://download.docker.com/linux/centos/docker-ce.repo - O /etc/yum.repos.d/docker.repo

Dockerリポジトリをインストールしたら、以下のコマンドを実行してDocker CEをインストールします。

yum install docker-ce -y

次にDockerサービスを起動し、以下のコマンドで起動時に起動できるようにします。

systemctl start docker
systemctl enable docker

ファイアウォールの設定

次に、スウォームクラスタが正常に動作するためには、ファイアウォール上のポート7946、4789、2376、2377、および80を開く必要があります。

すべてのノードで以下のコマンドを実行します。

firewall-cmd --permanent --add-port=2376/tcp
firewall-cmd --permanent --add-port=2377/tcp
firewall-cmd --permanent --add-port=7946/tcp
firewall-cmd --permanent --add-port=80/tcp
firewall-cmd --permanent --add-port=7946/udp
firewall-cmd --permanent --add-port=4789/udp

最後に、ファイアウォールとDockerサービスをリロードして、すべての変更を適用します。

firewall-cmd --reload
systemctl restart docker

Swarmを作成する

次に、Managerノードで Swarmを初期化する必要があります。これはdocker swarm initコマンドを実行することで行うことができます。このコマンドを実行することで、ノードをManagerノードにしてIPをアドバタイズします。

docker swarm init --advertise-addr 192.168.0.102

以下のような出力が表示されるはずです。

Swarm initialized: current node (viwovkb0bk0kxlk98r78apopo) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-3793hvb71g0a6ubkgq8zgk9w99hlusajtmj5aqr3n2wrhzzf8z-    1s38lymnir13hhso1qxt5pqru 192.168.0.102:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

注: 上記の出力のトークンを覚えておいてください。これは、後でworkerノードをManagerノードに結合するために使用します。

Swarmクラスタの状態を確認するには、以下のコマンドを使用します。

docker info

出力します。

Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 0
Server Version: 17.12.0-ce
Storage Driver: devicemapper
 Pool Name: docker-253:0-618740-pool
 Pool Blocksize: 65.54kB
 Base Device Size: 10.74GB
 Backing Filesystem: xfs
 Udev Sync Supported: true
 Data file: /dev/loop0
 Metadata file: /dev/loop1
 Data loop file: /var/lib/docker/devicemapper/devicemapper/data
 Metadata loop file: /var/lib/docker/devicemapper/devicemapper/metadata
 Data Space Used: 11.8MB
 Data Space Total: 107.4GB
 Data Space Available: 3.817GB
 Metadata Space Used: 581.6kB
 Metadata Space Total: 2.147GB
 Metadata Space Available: 2.147GB
 Thin Pool Minimum Free Space: 10.74GB
 Deferred Removal Enabled: true
 Deferred Deletion Enabled: true
 Deferred Deleted Device Count: 0
 Library Version: 1.02.140-RHEL7 (2017-05-03)
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins:
 Volume: local
 Network: bridge host macvlan null overlay
 Log: awslogs fluentd gcplogs gelf journald json-file logentries splunk syslog
Swarm: active
 NodeID: viwovkb0bk0kxlk98r78apopo
 Is Manager: true
 ClusterID: ttauawqrc8mmd0feluhcr1b0d
 Managers: 1
 Nodes: 1
 Orchestration:
  Task History Retention Limit: 5
 Raft:
  Snapshot Interval: 10000
  Number of Old Snapshots to Retain: 0
  Heartbeat Tick: 1
  Election Tick: 3
 Dispatcher:
  Heartbeat Period: 5 seconds
 CA Configuration:
  Expiry Duration: 3 months
  Force Rotate: 0
 Autolock Managers: false
 Root Rotation In Progress: false
 Node Address: 192.168.0.102
 Manager Addresses:
  192.168.0.102:2377
Runtimes: runc
Default Runtime: runc
Init Binary: docker-init
containerd version: 89623f28b87a6004d4b785663257362d1658a729
runc version: b2567b37d7b75eb4cf325b77297b140ea686ce8f
init version: 949e6fa
Security Options:
 seccomp
  Profile: default
Kernel Version: 3.10.0-693.11.1.el7.x86_64
Operating System: CentOS Linux 7 (Core)
OSType: linux
Architecture: x86_64
CPUs: 1
Total Memory: 1.102GiB
Name: centOS-7
ID: DN4N:BHHJ:6DJ7:SZPG:FJJC:XP6T:23R4:CESK:E5PO:SJ6B:BOST:HZQ5
Docker Root Dir: /var/lib/docker
Debug Mode (client): false
Debug Mode (server): false
Registry: https://index.docker.io/v1/
Labels:
Experimental: false
Insecure Registries:
 127.0.0.0/8
Live Restore Enabled: false

また、以下のコマンドでクラスタ内のノードの一覧を確認することができます。

docker node ls

出力します。

ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS
viwovkb0bk0kxlk98r78apopo *   centOS-7            Ready               Active              Leader

Worker ノードを Manager ノードに結合する

これでManagerノードの準備は完了です。次に、WorkerノードをManagerノードに追加する必要があります。

以下のように両方のWorkerノードでdocker swarm joinコマンドを実行することで行うことができます。

docker swarm join --token SWMTKN-1-3793hvb71g0a6ubkgq8zgk9w99hlusajtmj5aqr3n2wrhzzf8z-1s38lymnir13hhso1qxt5pqru 192.168.0.102:2377

出力します。

This node joined a swarm as a worker.

Managerノードで、以下のコマンドを実行して、ノードがアクティブかどうか、ノードの状態を確認します。

docker node ls

すべてがうまくいった場合は、以下のような出力が表示されるはずです。

ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS
viwovkb0bk0kxlk98r78apopo *   managernode            Ready               Active              Leader
yf6nb2er69pydlp6drijdfmwd     workernode1            Ready               Active              
yyavdslji7ovmw5fd3v7l62g8     workernode2            Ready               Active              

いつでも参加トークンを失った場合。Managerノードで以下を実行することで取得できます。

docker swarm join-token manager -q

Docker Swarmモードでサービスをデプロイする

Docker Swarmクラスタの準備が整いました。いよいよSwarm Modeでサービスをデプロイします。ここでは、Docker Swarm Modeで3つのコンテナでWebサーバサービスをデプロイします。

Managerノードで以下のコマンドを実行して、Webサーバサービスを起動します。

docker service create -p 80:80 --name webservice --replicas 3 httpd

出力します。

bky6uhg2agdxeqgc2b1a5tcsm
overall progress: 3 out of 3 tasks 
1/3: running   [==================================================>] 
2/3: running   [==================================================>] 
3/3: running   [==================================================>] 
verify: Service converged 

上記のコマンドでwebserviceという名前のサービスを作成し、dockerイメージのhttpdからコンテナを起動します。

ここで、以下のコマンドでサービスの状態を一覧表示して確認することができます。

docker service ls

出力します。

ID                  NAME                MODE                REPLICAS            IMAGE               PORTS
bky6uhg2agdx        webservice          replicated          3/3                 httpd:latest        *:80->80/tcp

docker service ps webservice

出力します。

ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE       ERROR           PORTS
xsa5wb0eg2ln        webservice.1        httpd:latest        managernode            Running             Running about a minute ago                                       
3sbscs7m9lnh        webservice.2        httpd:latest        workernode2            Running             Running about a minute ago                                      
yao2m5wi54kz        webservice.3        httpd:latest        workernode2            Running             Running about a minute ago                                 

ApacheのWebサービスは3つのノードに分散されていますので、以下のように好きなWebブラウザを使ってWorkerノードとManagerノードのいずれかにアクセスすることで、Webサーバにアクセスすることができます。

http://192.168.0.102
http://192.168.0.103
http://192.168.0.104

コンテナのセルフヒーリング

docker スウォームモードの重要な機能の一つに、コンテナの自己修復があります。コンテナがダウンした場合、同じノードでも別のノードでも自動的に再起動されます。

コンテナのセルフヒーリング機能をテストするために、workernode2からコンテナを外して、新しいコンテナが起動するかどうかを見てみましょう。

始める前に、コンテナを削除するにはコンテナIDが必要です。Workernode2上で以下のコマンドを実行することでコンテナIDをリストアウトすることができます。

docker ps

出力します。

CONTAINER ID        IMAGE               COMMAND              CREATED             STATUS              PORTS               NAMES
0dfc71537e18        httpd:latest        "httpd-foreground"   9 minutes ago       Up 9 minutes        80/tcp              webservice.3.yao2m5wi54kzs7iskefupuq6a
9b01b0a55cb7        httpd:latest        "httpd-foreground"   9 minutes ago       Up 9 minutes        80/tcp              webservice.1.xsa5wb0eg2ln5bud1sbjf9e9e

では、以下のコマンドを実行してID 9b01b0a55cb7のコンテナを削除します。

docker rm 9b01b0a55cb7 -f

ここで、Service from Managerノードを確認し、新しいコンテナが起動されているかどうかを確認します。

docker service ps webservice

下図のように、1つのコンテナが失敗し、別のコンテナがworkernode2上で起動していることがわかるはずです。

ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE              ERROR                              PORTS
z1spatkk1jj7        webservice.1        httpd:latest        workernode2            Running             Preparing 29 seconds ago                                      
xsa5wb0eg2ln         \_ webservice.1    httpd:latest        workernode2            Shutdown            Failed 30 seconds ago      "task: non-zero exit (137)"        

また、要件に応じてコンテナをスケールアップしたり、スケールダウンしたりすることもできます。例えば、Managerノードで以下のコマンドを使用して、Webサービスのコンテナを3から5にスケールアップすることができます。

docker service create -p 80:80 --name webservice --replicas 5 httpd

Managerノードで以下のコマンドを実行することで、Webサービスの状態を確認できます。

docker service ps webservice

出力します。

ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE       ERROR           PORTS
xsa5wb0eg2ln        webservice.1        httpd:latest        managernode            Running             Running about 10 minutes ago                                       
3sbscs7m9lnh        webservice.2        httpd:latest        workernode2            Running             Running about 10 minutes ago                                      
yao2m5wi54kz        webservice.3        httpd:latest        workernode2            Running             Running about 10 minutes ago                                 
dfg2mswa52sf        webservice.4        httpd:latest        workernode1            Running             Running about 15 seconds ago                                 
kah1j5hs14as        webservice.5        httpd:latest        workernode1            Running             Running about 15 seconds ago                                 

上記の出力では、workernode1で2つの新しいインスタンスが起動されていることがわかります。

サーバーの保護

Dockerノードのクラスタをセットアップした後は、セキュリティの追加レイヤーを提供することでサーバを保護することをお勧めします。監視機能とファイアウォール機能の両方を備えたセキュリティソリューションを導入するのが良いでしょう。

Alibaba Cloud Web Application Firewall(WAF)は、SQLインジェクション、クロスサイトスクリプティング(XSS)、悪意のあるボット、コマンド実行の脆弱性、その他の一般的なWeb攻撃を含むWebベースの攻撃からの保護を提供するために使用することができます。WAFは、多数の悪意のあるアクセス試行をフィルタリングし、サーバー上のハイパーテキスト転送プロトコル(HTTP)/HTTPセキュア(HTTPS)フラッド攻撃によるパフォーマンスへの影響を緩和します。

Alibaba CloudのCloudMonitorを使用して、お客様のクラウド展開の詳細な洞察を提供することができます。CloudMonitorは、中央処理装置(CPU)の使用率やレイテンシなどの重要な指標について高度な分析を提供し、ビジネス要件に応じてパラメータをカスタマイズすることもできます。

アリババクラウドは日本に2つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
アリババクラウドジャパン公式ページ

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

CentOS 7に「Docker」をインストールしてDocker Swarm Modeを設定

Docker Swarmは、Dockerコンテナのネイティブクラスタリングツールで、Dockerノードのクラスタを1つの仮想システムとして管理することができます。
このチュートリアルでは、CentOS 7上で3ノードのDocker Swarmクラスタを構成する方法をステップバイステップで説明します。

本ブログは英語版からの翻訳です。オリジナルはこちらからご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。

必要条件

  • CentOS 7をインストールした3台のAlibabaクラウドインスタンス。そのうち1台のサーバーがManagerノードとして動作し、2台のサーバーがWorkerノードとして動作します。
  • すべてのインスタンスに固定IPアドレスを設定しています。ここでは、Managerノードに192.168.0.102、Workerノード1に192.168.0.103、Workerノード2に192.168.0.104のIPアドレスを使用します。

アリババクラウドECSインスタンスの起動

まず、https://ecs.console.aliyun.com/?spm=a3c0i.o25424en.a3.13.388d499ep38szxAlibaba Cloud ECS Consoleにログインします。新しいECSインスタンスを作成し、少なくとも2GBのRAMを搭載したオペレーティングシステムとしてCentOS 7を選択します。ECSインスタンスに接続し、rootユーザーとしてログインします。

CentOS 7インスタンスにログインしたら、以下のコマンドを実行して、ベースシステムを最新の利用可能なパッケージでアップデートします。

yum update -y

はじめに

始める前に、各ノードがホスト名で通信できるように、各ノードの/etc/hostsファイルを設定しておく必要があります。

以下のように各ノードの/etc/hostsファイルを更新します。

nano /etc/hosts

192.168.0.102  managernode
192.168.0.103  workernode1
192.168.0.104  workernode2

保存して終了したらファイルを閉じます。

次に、/etc/hostsファイルの通りに各ノードのhostnameを設定する必要があります。

以下のコマンドを各ノードで1つずつ実行することで行うことができます。

管理者ノード

hostnamectl set-hostname managernode

Worker node1

hostnamectl set-hostname workernode1

Worker node2

hostnamectl set-hostname workernode2

Dockerエンジンのインストール

次に、すべてのノードにDocker Community Editionをインストールする必要があります。デフォルトでは、最新版のDocker CEはCentOS 7のリポジトリにはありません。そのため、Docker CEのリポジトリをシステムに追加する必要があります。

これは以下のコマンドを全ノードで実行することで行うことができます。

wget https://download.docker.com/linux/centos/docker-ce.repo - O /etc/yum.repos.d/docker.repo

Dockerリポジトリをインストールしたら、以下のコマンドを実行してDocker CEをインストールします。

yum install docker-ce -y

次にDockerサービスを起動し、以下のコマンドで起動時に起動できるようにします。

systemctl start docker
systemctl enable docker

ファイアウォールの設定

次に、スウォームクラスタが正常に動作するためには、ファイアウォール上のポート7946、4789、2376、2377、および80を開く必要があります。

すべてのノードで以下のコマンドを実行します。

firewall-cmd --permanent --add-port=2376/tcp
firewall-cmd --permanent --add-port=2377/tcp
firewall-cmd --permanent --add-port=7946/tcp
firewall-cmd --permanent --add-port=80/tcp
firewall-cmd --permanent --add-port=7946/udp
firewall-cmd --permanent --add-port=4789/udp

最後に、ファイアウォールとDockerサービスをリロードして、すべての変更を適用します。

firewall-cmd --reload
systemctl restart docker

Swarmを作成する

次に、Managerノードで Swarmを初期化する必要があります。これはdocker swarm initコマンドを実行することで行うことができます。このコマンドを実行することで、ノードをManagerノードにしてIPをアドバタイズします。

docker swarm init --advertise-addr 192.168.0.102

以下のような出力が表示されるはずです。

Swarm initialized: current node (viwovkb0bk0kxlk98r78apopo) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-3793hvb71g0a6ubkgq8zgk9w99hlusajtmj5aqr3n2wrhzzf8z-    1s38lymnir13hhso1qxt5pqru 192.168.0.102:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

注: 上記の出力のトークンを覚えておいてください。これは、後でworkerノードをManagerノードに結合するために使用します。

Swarmクラスタの状態を確認するには、以下のコマンドを使用します。

docker info

出力します。

Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 0
Server Version: 17.12.0-ce
Storage Driver: devicemapper
 Pool Name: docker-253:0-618740-pool
 Pool Blocksize: 65.54kB
 Base Device Size: 10.74GB
 Backing Filesystem: xfs
 Udev Sync Supported: true
 Data file: /dev/loop0
 Metadata file: /dev/loop1
 Data loop file: /var/lib/docker/devicemapper/devicemapper/data
 Metadata loop file: /var/lib/docker/devicemapper/devicemapper/metadata
 Data Space Used: 11.8MB
 Data Space Total: 107.4GB
 Data Space Available: 3.817GB
 Metadata Space Used: 581.6kB
 Metadata Space Total: 2.147GB
 Metadata Space Available: 2.147GB
 Thin Pool Minimum Free Space: 10.74GB
 Deferred Removal Enabled: true
 Deferred Deletion Enabled: true
 Deferred Deleted Device Count: 0
 Library Version: 1.02.140-RHEL7 (2017-05-03)
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins:
 Volume: local
 Network: bridge host macvlan null overlay
 Log: awslogs fluentd gcplogs gelf journald json-file logentries splunk syslog
Swarm: active
 NodeID: viwovkb0bk0kxlk98r78apopo
 Is Manager: true
 ClusterID: ttauawqrc8mmd0feluhcr1b0d
 Managers: 1
 Nodes: 1
 Orchestration:
  Task History Retention Limit: 5
 Raft:
  Snapshot Interval: 10000
  Number of Old Snapshots to Retain: 0
  Heartbeat Tick: 1
  Election Tick: 3
 Dispatcher:
  Heartbeat Period: 5 seconds
 CA Configuration:
  Expiry Duration: 3 months
  Force Rotate: 0
 Autolock Managers: false
 Root Rotation In Progress: false
 Node Address: 192.168.0.102
 Manager Addresses:
  192.168.0.102:2377
Runtimes: runc
Default Runtime: runc
Init Binary: docker-init
containerd version: 89623f28b87a6004d4b785663257362d1658a729
runc version: b2567b37d7b75eb4cf325b77297b140ea686ce8f
init version: 949e6fa
Security Options:
 seccomp
  Profile: default
Kernel Version: 3.10.0-693.11.1.el7.x86_64
Operating System: CentOS Linux 7 (Core)
OSType: linux
Architecture: x86_64
CPUs: 1
Total Memory: 1.102GiB
Name: centOS-7
ID: DN4N:BHHJ:6DJ7:SZPG:FJJC:XP6T:23R4:CESK:E5PO:SJ6B:BOST:HZQ5
Docker Root Dir: /var/lib/docker
Debug Mode (client): false
Debug Mode (server): false
Registry: https://index.docker.io/v1/
Labels:
Experimental: false
Insecure Registries:
 127.0.0.0/8
Live Restore Enabled: false

また、以下のコマンドでクラスタ内のノードの一覧を確認することができます。

docker node ls

出力します。

ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS
viwovkb0bk0kxlk98r78apopo *   centOS-7            Ready               Active              Leader

Worker ノードを Manager ノードに結合する

これでManagerノードの準備は完了です。次に、WorkerノードをManagerノードに追加する必要があります。

以下のように両方のWorkerノードでdocker swarm joinコマンドを実行することで行うことができます。

docker swarm join --token SWMTKN-1-3793hvb71g0a6ubkgq8zgk9w99hlusajtmj5aqr3n2wrhzzf8z-1s38lymnir13hhso1qxt5pqru 192.168.0.102:2377

出力します。

This node joined a swarm as a worker.

Managerノードで、以下のコマンドを実行して、ノードがアクティブかどうか、ノードの状態を確認します。

docker node ls

すべてがうまくいった場合は、以下のような出力が表示されるはずです。

ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS
viwovkb0bk0kxlk98r78apopo *   managernode            Ready               Active              Leader
yf6nb2er69pydlp6drijdfmwd     workernode1            Ready               Active              
yyavdslji7ovmw5fd3v7l62g8     workernode2            Ready               Active              

いつでも参加トークンを失った場合。Managerノードで以下を実行することで取得できます。

docker swarm join-token manager -q

Docker Swarmモードでサービスをデプロイする

Docker Swarmクラスタの準備が整いました。いよいよSwarm Modeでサービスをデプロイします。ここでは、Docker Swarm Modeで3つのコンテナでWebサーバサービスをデプロイします。

Managerノードで以下のコマンドを実行して、Webサーバサービスを起動します。

docker service create -p 80:80 --name webservice --replicas 3 httpd

出力します。

bky6uhg2agdxeqgc2b1a5tcsm
overall progress: 3 out of 3 tasks 
1/3: running   [==================================================>] 
2/3: running   [==================================================>] 
3/3: running   [==================================================>] 
verify: Service converged 

上記のコマンドでwebserviceという名前のサービスを作成し、dockerイメージのhttpdからコンテナを起動します。

ここで、以下のコマンドでサービスの状態を一覧表示して確認することができます。

docker service ls

出力します。

ID                  NAME                MODE                REPLICAS            IMAGE               PORTS
bky6uhg2agdx        webservice          replicated          3/3                 httpd:latest        *:80->80/tcp

docker service ps webservice

出力します。

ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE       ERROR           PORTS
xsa5wb0eg2ln        webservice.1        httpd:latest        managernode            Running             Running about a minute ago                                       
3sbscs7m9lnh        webservice.2        httpd:latest        workernode2            Running             Running about a minute ago                                      
yao2m5wi54kz        webservice.3        httpd:latest        workernode2            Running             Running about a minute ago                                 

ApacheのWebサービスは3つのノードに分散されていますので、以下のように好きなWebブラウザを使ってWorkerノードとManagerノードのいずれかにアクセスすることで、Webサーバにアクセスすることができます。

http://192.168.0.102
http://192.168.0.103
http://192.168.0.104

コンテナのセルフヒーリング

docker スウォームモードの重要な機能の一つに、コンテナの自己修復があります。コンテナがダウンした場合、同じノードでも別のノードでも自動的に再起動されます。

コンテナのセルフヒーリング機能をテストするために、workernode2からコンテナを外して、新しいコンテナが起動するかどうかを見てみましょう。

始める前に、コンテナを削除するにはコンテナIDが必要です。Workernode2上で以下のコマンドを実行することでコンテナIDをリストアウトすることができます。

docker ps

出力します。

CONTAINER ID        IMAGE               COMMAND              CREATED             STATUS              PORTS               NAMES
0dfc71537e18        httpd:latest        "httpd-foreground"   9 minutes ago       Up 9 minutes        80/tcp              webservice.3.yao2m5wi54kzs7iskefupuq6a
9b01b0a55cb7        httpd:latest        "httpd-foreground"   9 minutes ago       Up 9 minutes        80/tcp              webservice.1.xsa5wb0eg2ln5bud1sbjf9e9e

では、以下のコマンドを実行してID 9b01b0a55cb7のコンテナを削除します。

docker rm 9b01b0a55cb7 -f

ここで、Service from Managerノードを確認し、新しいコンテナが起動されているかどうかを確認します。

docker service ps webservice

下図のように、1つのコンテナが失敗し、別のコンテナがworkernode2上で起動していることがわかるはずです。

ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE              ERROR                              PORTS
z1spatkk1jj7        webservice.1        httpd:latest        workernode2            Running             Preparing 29 seconds ago                                      
xsa5wb0eg2ln         \_ webservice.1    httpd:latest        workernode2            Shutdown            Failed 30 seconds ago      "task: non-zero exit (137)"        

また、要件に応じてコンテナをスケールアップしたり、スケールダウンしたりすることもできます。例えば、Managerノードで以下のコマンドを使用して、Webサービスのコンテナを3から5にスケールアップすることができます。

docker service create -p 80:80 --name webservice --replicas 5 httpd

Managerノードで以下のコマンドを実行することで、Webサービスの状態を確認できます。

docker service ps webservice

出力します。

ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE       ERROR           PORTS
xsa5wb0eg2ln        webservice.1        httpd:latest        managernode            Running             Running about 10 minutes ago                                       
3sbscs7m9lnh        webservice.2        httpd:latest        workernode2            Running             Running about 10 minutes ago                                      
yao2m5wi54kz        webservice.3        httpd:latest        workernode2            Running             Running about 10 minutes ago                                 
dfg2mswa52sf        webservice.4        httpd:latest        workernode1            Running             Running about 15 seconds ago                                 
kah1j5hs14as        webservice.5        httpd:latest        workernode1            Running             Running about 15 seconds ago                                 

上記の出力では、workernode1で2つの新しいインスタンスが起動されていることがわかります。

サーバーの保護

Dockerノードのクラスタをセットアップした後は、セキュリティの追加レイヤーを提供することでサーバを保護することをお勧めします。監視機能とファイアウォール機能の両方を備えたセキュリティソリューションを導入するのが良いでしょう。

Alibaba Cloud Web Application Firewall(WAF)は、SQLインジェクション、クロスサイトスクリプティング(XSS)、悪意のあるボット、コマンド実行の脆弱性、その他の一般的なWeb攻撃を含むWebベースの攻撃からの保護を提供するために使用することができます。WAFは、多数の悪意のあるアクセス試行をフィルタリングし、サーバー上のハイパーテキスト転送プロトコル(HTTP)/HTTPセキュア(HTTPS)フラッド攻撃によるパフォーマンスへの影響を緩和します。

Alibaba CloudのCloudMonitorを使用して、お客様のクラウド展開の詳細な洞察を提供することができます。CloudMonitorは、中央処理装置(CPU)の使用率やレイテンシなどの重要な指標について高度な分析を提供し、ビジネス要件に応じてパラメータをカスタマイズすることもできます。

アリババクラウドは日本に2つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
アリババクラウドジャパン公式ページ

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

同じECSインスタンスを使用して複数のウェブサイトをホスティングするDevOpsの方法

このチュートリアルでは、Docker Composeとリバースプロキシを使って、同じECSインスタンスを使って2つ以上のWebサイトを実行する方法を学びます。

本ブログは英語版からの翻訳です。オリジナルはこちらからご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです 。

なぜこのアプローチなのか?

ほとんどの場合、アリババクラウドECSインスタンスを取得したときには、1つのウェブサイトやアプリケーションにのみ使用します。しかし、家族と写真を共有したり、新しいブログを立ち上げたり、基本的にはコンテンツをホストして提供するために別のECSインスタンスを必要とするような、他のことに使用する必要がある場合もあります。このソリューションは、安定性のために推奨されていますが、時にはやりすぎてしまうこともあります。静的なサイトや小さなブログを運営するために、まったく新しいインスタンスを必要とすることはありません。代わりに、同じマシンでそれらをホストし、リソースを共有することでコストを削減することができます。

サーバーが複数のウェブサイトにサービスを提供する可能性があることをすでに知っていて、ECSインスタンスを最大限に活用したい場合は、1つの単一インスタンスECSで異なるコンテンツを処理するための複数のソリューションがあります。

1、サブフォルダ ("example.com/web1", "example.com/web2", "example.com/web3")
2、ポートベースの仮想ホスティング ("example.com:80", "example.com:8080", "example.com:8181")
3、サブドメイン(「web1.example.com」、「web2.example.com」、「web3.example.com
4、名前ベースのバーチャルホスティング ("web1.com", "web2.com", "web3.com")

名前ベースのバーチャルホスティング

それはあなたのサイトのためのよりよい名前を使用するのに役立ちますので、ここでは最高の(そしてより優雅な)解決は、名前ベースの仮想ホスティングを使用することです。マイクロソフトとグーグルの両方が私のサーバー上で自分のウェブサイトをホストすることを決定したことを一瞬想像してみてください。最初のステップは、「microsoft.com」と「google.com」の両方のDNS「A」レコードを更新して、ECSインスタンスのパブリックIPを指すようにすることです。

そして、ここで質問が出てきます。両方のウェブサイトの訪問者が同じIPに指示されている場合、どのようにしてそれぞれのリクエストを提供するページを区別するのでしょうか?ここで、HTTP ヘッダーが来ます。これは HTTP トランザクションのパラメータを定義します。以下の例を見てみましょう。

GET / HTTP/1.1
Host: microsoft.com
Connection: keep-alive
Cache-Control: no-cache
User-Agent: Chrome/66.0.3359.139
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: en-AU

ご覧のように、このHTTPリクエストのHostパラメータ(HTTP/1.1以降は必須)は "microsoft.com "を要求しています。この情報があれば、私のサーバーはどのページにサービスを提供するかを知っています。この場合、(GETリクエストによると)microsoft.comのURIです。

幸いなことに、HTTP/0.9バージョンを使用しているWebブラウザは残っていないので、リクエストはGETの部分だけをカバーし、ホストについては他には何もありません。

最も一般的には、ポートベース名前ベースバーチャルホスティングの両方は、 sites-available という名前のフォルダ内のホスト上の設定ファイルを使用して達成され、 sites-enabled でそれらをリンクしています。これらの設定ファイルは、どのようなドメイン名が使用されているか(例えば、web1.com)とウェブサイトがディスク(例えば、/var/www/html/web1.com/)に格納されている場所を記述します。

しかし、セキュリティ上の理由から、このチュートリアルではこのアプローチを使用しません。名前ベースの仮想ホストを展開するためにこのチュートリアルで使用しようとしているアプローチは、リクエストを管理するためにコンテナ化とリバースプロキシを使用しています。私にとっては、複雑さと DevOps のベストプラクティスを満たすためのステップアップです。

仮想化・コンテナ化

今日では、Dockerなどのソフトウェアは、コンテナ化とも呼ばれるオペレーティングシステムレベルの仮想化を実行しており、実行中のプログラムから見ると、実際のコンピュータのように見えます。これを使用する利点は、異なるコンテナで動作するサービス間の分離が可能になることで、あるコンテナで動作しているウェブサイトは、別のコンテナで動作しているウェブサイトとは(デフォルトでは)一切の接続を持ちません。

このようにして、複数のウェブサイトは、1つのECSインスタンス上で同時にコンテナ(オペレーティングシステム)で実行することができ、完全にお互いを認識していないことになります。彼らはホストで直接実行している場合と同じようにポート80を公開することさえあります。そして、Dockerはホスト内のランダムな利用可能なポートをゲートウェイとして割り当てます。

逆プロキシ

そして、おっしゃる通り、コンテナ化自体では、各サイトの訪問者に適切なサイトを提供するためのHTTPリクエストを処理するという問題はまだ解決していません。そこで必要になるのがリバースプロキシという、コンテナとしても動作し、リクエストをインターセプトして正しい場所に送るサービスで、バーチャルホスティングを扱うためのメインの部分になります。

次の図は、リバースプロキシがインターネットからのリクエストを受け取り、内部ネットワーク内のサーバに転送する様子を示しています。このプロキシにリクエストを出している訪問者は気づかないかもしれません。

image.png

私のお気に入りの解決策は、開発者の "jwilder "のオリジナルプロジェクトから "neilpang "が作ったフォークであるneilpang/nginx-proxyです。このフォークには “acme.sh "も含まれており、SSLを取得するためのプロセスを自動化しています。これは、あなたのサイトを保護し、同時に安全にするための本当に良いリバースプロキシであることがわかりました。

公式READMEによると、このnginx-proxyは「nginxとdocker-genを実行しているコンテナをセットアップします。これは nginx のリバースプロキシ設定を生成し、コンテナの起動時と停止時に nginx をリロードします。」つまり、基本的にはホスト上で稼働しているすべてのコンテナを追跡し、コンテナが追加されたときにリアルタイムでルールを作成します。

すべてのパーツをまとめる

OK! これでDocker上で動作し、リバースプロキシを持ち、少なくとも2つのウェブサイトが動作するような名前ベースのバーチャルホストソリューションが必要だということがわかりました。このハウツーのタイトルにあるように、マルチコンテナアプリケーションを定義して実行するための素晴らしいツールであるDocker Composeを使ってすべてをラップします。

docker-compose.yml設定ファイルの各コンテナは “service "と呼ばれ、YAML形式でサービス配列に入ります。このファイルのサービスは以下のようになります。

1、proxy
2、web1 (web1.com)
3、web2 (web2.com)
シンプルにするために、私は両方のウェブサービスでApache(php:7.2-apache)とPHP 7.2の標準的で修正されていない公式イメージを使用しています。”web1 "と “web2 "は、したがって、Apacheからの "It works!"という一般的なメッセージが表示されます。これは、コンテナにコマンドラインで docker exec -it web1 bash と入力することで変更することができます。

ECS上での実行

ホストに直接接続

すでにインスタンスを持っている場合は、公式のドキュメントに従ってDockerをインストールし、以下のような内容の「docker-compose.yml」というファイルを作成して、通常のDocker Composeプロジェクトのように実行してください。

docker-compose.yml

version: '3.6'

services:
  proxy:
    image: neilpang/nginx-proxy
    network_mode: bridge
    container_name: proxy
    restart: on-failure
    ports:
      - "80:80"
      - "443:443"
    environment:
      - ENABLE_IPV6=true
    volumes:
      - ./certs:/etc/nginx/certs
      - /var/run/docker.sock:/tmp/docker.sock:ro
  web1:
    image: php:7.2-apache
    network_mode: bridge
    container_name: web1
    restart: on-failure
    environment:
      - VIRTUAL_HOST=web1.com
  web2:
    image: php:7.2-apache
    network_mode: bridge
    container_name: web2
    restart: on-failure
    environment:
      - VIRTUAL_HOST=web2.com

Terraformで

もしまだECSインスタンスを持っていないのであれば、Alibaba Cloudのサポートが充実しているので、Terraformでやることをおすすめします。Terraform for Alicloudをまずローカルマシンで稼働させる方法については、こちらの記事をチェックしてみてください。

その記事の指示に従ってTerraformを動作させたら、いよいよmain.tfとuser-data.shを作成します。

main.tf

variable "access_key" {
  type = "string"
  default = "XXXXX"
}
variable "secret_key" {
  type = "string"
  default = "XXXXX"
}
variable "region" {
  type = "string"
  default = "ap-southeast-2"
}
variable "vswitch" {
  type = "string"
  default = "XXX-XXXXX"
}
variable "sgroups" {
  type = "list"
  default = [
    "XX-XXXXX"
  ]
}
variable "name" {
  type = "string"
  default = "multi-tenant"
}
variable "password" {
  type = "string"
  default = "Test1234!"
}

provider "alicloud" {
  access_key = "${var.access_key}"
  secret_key = "${var.secret_key}"
  region = "${var.region}"
}

data "alicloud_images" "search" {
  name_regex = "^ubuntu_16.*_64"
}
data "alicloud_instance_types" "default" {
  instance_type_family = "ecs.xn4"
  cpu_core_count = 1
  memory_size = 1
}
data "template_file" "user_data" {
  template = "${file("user-data.sh")}"
}

resource "alicloud_instance" "web" {
  instance_name = "${var.name}"
  image_id = "${data.alicloud_images.search.images.0.image_id}"
  instance_type = "${data.alicloud_instance_types.default.instance_types.0.id}"

  vswitch_id = "${var.vswitch}"
  security_groups = "${var.sgroups}"
  internet_max_bandwidth_out = 100

  password = "${var.password}"

  user_data = "${data.template_file.user_data.template}"
}

output "ip" {
  value = "${alicloud_instance.web.public_ip}"
}

user-data.sh

#!/bin/bash -v

# Create docker-compose.yml
cat <<- 'EOF' > /opt/docker-compose.yml
version: '3.6'
services:
  proxy:
    image: neilpang/nginx-proxy
    network_mode: bridge
    container_name: proxy
    restart: on-failure
    ports:
      - "80:80"
      - "443:443"
    environment:
      - ENABLE_IPV6=true
    volumes:
      - ./certs:/etc/nginx/certs
      - /var/run/docker.sock:/tmp/docker.sock:ro
  web1:
    image: php:7.2-apache
    network_mode: bridge
    container_name: web1
    restart: on-failure
    environment:
      - VIRTUAL_HOST=web1.com
  web2:
    image: php:7.2-apache
    network_mode: bridge
    container_name: web2
    restart: on-failure
    environment:
      - VIRTUAL_HOST=web2.com
EOF

apt-get update && apt-get install -y apt-transport-https ca-certificates curl software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
apt-get update && apt-get install -y docker-ce docker-compose
curl -L https://github.com/docker/compose/releases/download/1.21.2/docker-compose-`uname -s`-`uname -m` -o /usr/bin/docker-compose

cd /opt && docker-compose up -d

そして、いつものように terraform init, terraform plan, terraform apply を実行してください。Terraformから確認の連絡が来てから約4~5分後にパッケージのインストールが開始されます。

まとめ

マシン内のリソースを効率的に共有して複数のサービスを同時に実行する方法を学びました。あなたが覚えておくべき唯一のことは、サイトがより忙しくなる場合は、ECSインスタンスをアップグレードする必要がありますので、使用状況を追跡することです(余分なCPUを追加したり、RAMメモリを増加させることによって)。このソリューションは、多くのトラフィックを持つ小さなウェブサイトに最適であり、あなたは多くのホスティングの概念とその複雑さを学ぶことができます。あなたがまだ理解していない場合は、フォーラムでこの記事を参照してください。

アリババクラウドは日本に2つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
アリババクラウドジャパン公式ページ

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

Docker Install in Ubuntu

Docker on Ubuntu 覚書

Dockerを入れて2日目の初心者が次回インストールするときにも見れるように分かっていることを書く。

インストール方法 公式ガイド: https://docs.docker.com/engine/install/

今回の環境

  • OS: Ubuntu Budgie 20.04

インストールから

今回は公式リポジトリからインストールする

1. リポジトリの追加

  1. アップデート

    sudo apt -y update && sudo apt -y upgrade
    sudo apt -y install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
    
  2. 公式GPG鍵の追加

    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
    

    フィンガープリントでキーの確認

    sudo apt-key fingerprint 0EBFCD88
    

    下記の表示が出ればOK

    pub   rsa4096 2017-02-22 [SCEA]
          9DC8 5822 9FC7 DD38 854A  E2D8 8D81 803C 0EBF CD88
    uid           [ unknown] Docker Release (CE deb) <docker@docker.com>
    sub   rsa4096 2017-02-22 [S]
    
  3. リポジトリ追加 (stable 版)

    sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
    

    ※Linux Mint どでは$(lsb_release -cs)コマンドの箇所をUbuntuなど親の名前に変更する必要があるとのこと

    2. Docker Engineのインストール

  4. ここでは最新バージョンのインストールのみ記載する(ガイドにはバージョン指定もある)

    sudo apt -y update
    sudo apt -y install docker-ce docker-ce-cli containerd.io
    
  5. hello-worldで確認

    テストイメージがダウンロードされてメッセージが表示されて終了します

    sudo docker run hello-world
    

3. 古いバージョンのアンインストール

  1. ガイドの一番上に書いてあったが、まだ使わないだろうしということで下のほうに...

    sudo apt-get remove docker docker-engine docker.io containerd runc
    
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DockerでPHP7.4の環境を整えるときの問題点

DockerでPHP7.4の環境構築をした話

結論

PHPのDockerfileに以下を加える。

  • DockerでPHP7.4系以上を使うなら、ライブラリの「oniguruma」を入れる。
    • apk add --update --no-cache oniguruma-dev \を追記。
  • DockerでMySQLを使うなら、PDOドライバーを入れよう。
    • RUN docker-php-ext-install -j$(nproc) pdo_mysqlを追記。

対象

  • DockerでPHP7.4以上を使いたい方
  • DockerでMySQLの環境構築。

環境

  • OS Catalina 10.15.4
  • Docker 2.3.0.3
    • php:7.4.7-fpm-alpine

はじめに

phpenvを用いてPHPのバージョン管理すればDockerなんて使わなくていいやと思ってたんですけど、MySQLを使うことになって、5.7と8.0系で認証方式が違うとか、8.0系の方が早いとかありますけど、手元のpcにbrewで8.0から5.7系にダウングレードしたらお手上げ状態になったので、勉強も兼ねてサクッとDockerを使いました。
なお、Dockerfileのことしか主にコードを開示していないので、その他は頑張ってください。

問題点

  • php:7.4.0*の環境のDockerfileをビルド時No package 'oniguruma' foundとエラー。
  • PHPでMySQLを使おうとするとcould not find driverというエラー

上記にもあげている通り、2点問題に遭遇しました。

php:7.4.*の環境のDockerfileをビルド時No package 'oniguruma' foundとエラーがでよる。

はい、1つ目です。

Dockerfile
RUN apk upgrade --update \
  && apk add --update --no-cache oniguruma-dev \ # ここ
  && apk --no-cache --virtual .build-deps add make g++ gcc re2c autoconf \
  && apk --no-cache add gettext-dev libzip-dev curl-dev \
  && docker-php-ext-install -j$(nproc) gettext mbstring zip opcache ctype json bcmath sockets curl \
  && pecl channel-update pecl.php.net

php:7.4以降はlibonig-devというパッケージをする必要があるそうです。

https://github.com/kkos/oniguruma

上記2行目にも書いていますが、apk add --update --no-cache oniguruma-devを追加して下さい。

PHPでMySQLを使おうとするとcould not find driverというエラー

はい、2つ目です。

このエラーが出ている人はphpinfo();をphpファイルに書いてもらって、
PDO項目を見ていただくとわかると思うのですが、sqliteしかないと思われます。

つまりデフォルトでドライバーはsqliteしか入ってないらしいですね。

これには少しびっくりしたと同時に、コンテナは本当に最小限のものしか入ってないのかと思いましたね。。。

というわけでDockerfileに書きましょう。

Dockerfile
# PDO driver(MySQL)
RUN docker-php-ext-install -j$(nproc) pdo_mysql

先ほどと同じDockerfileに追記してください。

buildした後、phpinfo();のPDO driver項目を見るとmysqlが追加されていると思います。

他のデータベースを使いたい時も同じようにDockerfileにかけば使用できるでしょう。

PHPのDockerfile全体

今回はUbuntuでなく、Alpineで軽量化しています。
なので皆さんご存知のapt-getは使えず、apkというパッケージマネージャを使う必要があります。

Dockerfile
FROM php:7.4.7-fpm-alpine

ARG environment

RUN apk upgrade --update \
  && apk add --update --no-cache oniguruma-dev \
  && apk --no-cache --virtual .build-deps add make g++ gcc re2c autoconf \
  && apk --no-cache add gettext-dev libzip-dev curl-dev \
  && docker-php-ext-install -j$(nproc) gettext mbstring zip opcache ctype json bcmath sockets curl \
  && pecl channel-update pecl.php.net

# PDO(MySQL)
RUN docker-php-ext-install -j$(nproc) pdo_mysql

まとめ

  • PHP7.4以上を使う場合はパッケージonigurumaをDockerfileに追記する。
  • PDOドライバーはデフォルトでsqliteしか入ってない。

参考

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

circleci ビルドができない、、、

ビルドすることに苦戦しすぎているので、備忘録メモ。まだ完璧に解決できていないが、一歩進んだので投稿

本題の前に一言

circleciのエラー文って不親切やし、めっちゃ難しい。dockerをちゃんと勉強しろってことなのか。
改めて現役エンジニアを尊敬する。

参考URL

https://github.com/docker-library/mysql/issues/129#issuecomment-178265632

登場ファイル

  1. .circleci/cofing.yml
  2. database.yml
  3. docker-comopose.yml

今回の抱えている問題

・MySQL、コンテナの立ち上げ失敗←今回で解決できていないが、前進した。
・コンテナの立ち上げに失敗しているからdb:createも失敗
・rubocopを実行できていない(db:createをできていないから?)
・/tmp/test-resultsがあるのに、Not Foundになってしまう←今回で解決できていない。

ファイルの中身(エラー発生時)

circleci/cofing.yml
version: 2.1
orbs:
  ruby: circleci/ruby@0.1.2

jobs:
  build:
    docker:
      # specify the version you desire here
      - image: circleci/ruby:2.6.6-stretch-node
        environment:
          - RAILS_ENV: 'test'
      - image: circleci/mysql:8.0
        name: "db"
        command: mysqld --default-authentication-plugin=mysql_native_password
        environment:
          - MYSQL_PASSWORD: "password"
          - MYSQL_ROOT_HOST: '%'

      # Specify service dependencies here if necessary
      # CircleCI maintains a library of pre-built images
      # documented at https://circleci.com/docs/2.0/circleci-images/
      # - image: circleci/postgres:9.4

    working_directory: ~/アプリ名

    steps:
      - checkout

      # Download and cache dependencies
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "Gemfile.lock" }}
            # fallback to using the latest cache if no exact match is found
            - v1-dependencies-

      - run: bundle install --jobs=4 --retry=3 --path vendor/bundle
      - save_cache:
          paths:
            - ./vendor/bundle
          key: v1-dependencies-{{ checksum "Gemfile.lock" }}

      # Database setup
      - run: bundle exec rake db:create
      - run: bundle exec rake db:schema:load

# rubocop。
      - run:
          name: Rubocop
          command: bundle exec rubocop -a

# rspec
      # run tests!
      - run:
          name: run tests
          command: |
            mkdir /tmp/test-results
            TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | \
              circleci tests split --split-by=timings)"


            bundle exec rspec \
              --format progress \
              --format RspecJunitFormatter \
              --out /tmp/test-results/rspec.xml \
              --format progress \
              $TEST_FILES

      # collect reports
      - store_test_results:
          path: /tmp/test-results
      - store_artifacts:
          path: /tmp/test-results
          destination: test-results
database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: <%= ENV.fetch("MYSQL_USERNAME", "root") %>
  password: <%= ENV.fetch("MYSQL_PASSWORD", "password") %>
  host: <%= ENV.fetch("MYSQL_HOST", "db") %>

development:
  <<: *default
  database: アプリ名_development

test:
  <<: *default
  database: アプリ名_test

production:
  <<: *default
  database: <%= ENV['DB_DATABASE'] %>
  adapter: mysql2
  encoding: utf8mb4
  charset: utf8mb4
  collation: utf8mb4_general_ci
  host: <%= ENV['DB_HOST'] %>
  username: <%= ENV['DB_USERNAME'] %>
  password: <%= ENV['DB_PASSWORD'] %>


docker-comopose.yml
version: '3'
services:
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: password
    ports:
      - '3306:3306'
    command: --default-authentication-plugin=mysql_native_password
    volumes:
      - mysql-data:/var/lib/mysql
  web:
    build: .
    command: bundle exec puma -C config/puma.rb
    environment:
      RAILS_ENV: development
    volumes:
      - .:/アプリ名
      - bundle:/usr/local/bundle
      - /app/vendor
      - /app/log
      - /app/.git
    ports:
      - "3000:3000"
    depends_on:
      - db
    tty: true
    stdin_open: true
  nginx:
    build:
      context: .
      dockerfile: ./nginx/Dockerfile
    ports:
      - '80:80'
    depends_on:
      - web
  chrome:
    image: selenium/standalone-chrome
    ports:
      - "4444:4444"
    shm_size: "2g"


volumes:
  mysql-data:
    driver: local
  bundle:
    driver: local

やったこと

開発でMySQLのコンテナの立ち上げに成功しているから、docker-comose.ymlに書いてあることを.circleci/cofing.ymlでも真似すればいけるんじゃないかと考えて、.circleci/cofing.ymlを以下のように修正

circleci/cofing.yml
version: 2.1
orbs:
  ruby: circleci/ruby@0.1.2

jobs:
  build:
    docker:
      # specify the version you desire here
      - image: circleci/ruby:2.6.6-stretch-node
        environment:
          - RAILS_ENV: 'test'
      - image: circleci/mysql:8.0
        name: "db"
        command: mysqld --default-authentication-plugin=mysql_native_password
        environment:
+         - MYSQL_ROOT_PASSWORD: password    追記
-         - MYSQL_PASSWORD: "password"    削除
-         - MYSQL_ROOT_HOST: '%'        削除

      # Specify service dependencies here if necessary
      # CircleCI maintains a library of pre-built images
      # documented at https://circleci.com/docs/2.0/circleci-images/
      # - image: circleci/postgres:9.4

    working_directory: ~/アプリ名

    steps:
      - checkout

      # Download and cache dependencies
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "Gemfile.lock" }}
            # fallback to using the latest cache if no exact match is found
            - v1-dependencies-

      - run: bundle install --jobs=4 --retry=3 --path vendor/bundle
      - save_cache:
          paths:
            - ./vendor/bundle
          key: v1-dependencies-{{ checksum "Gemfile.lock" }}

      # Database setup
      - run: bundle exec rake db:create
      - run: bundle exec rake db:schema:load

# rubocop。
      - run:
          name: Rubocop
          command: bundle exec rubocop -a

# rspec
      # run tests!
      - run:
          name: run tests
          command: |
            mkdir /tmp/test-results
            TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | \
              circleci tests split --split-by=timings)"


            bundle exec rspec \
              --format progress \
              --format RspecJunitFormatter \
              --out /tmp/test-results/rspec.xml \
              --format progress \
              $TEST_FILES

      # collect reports
      - store_test_results:
          path: /tmp/test-results
      - store_artifacts:
          path: /tmp/test-results
          destination: test-results

結果

コンテナの作成のところで「!」マークだったのが、「ー」マークに変わった!!(ただしエラーは解決できていないっぽい。。。)「!」と「ー」の違いは何なんやろ?
それでもrubocopは実行できるようになったので、前進はできた。
スクリーンショット 2020-07-09 8.34.04.png

メモ

解決進んだら、また投稿。
/tmp/test-resultsのエラーがホンマに分からへん。デフォルト同じなはずやのに。。。

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

GASでなんちゃってORマッパー

はじめに

GASでデータを取るのがめんどくさかったのでORマッパー的なものが欲しくなったので作りました。

主な機能

  • テーブル操作
    • find,
    • select
    • discard
    • insert
    • inner
    • join
    • outer join
    • cross join
    • not
    • update
    • transaction (begin, save, commit, rollback)
    • record iterator
    • order by
    • sort
    • sub query
    • create
  • レコード操作
    • get
    • set (update)
    • validation
    • namespace
    • create

Github

使い方

プロジェクト新規作成

$ npm run setup:create

※途中でGoogleアカウントログインを要求されます。

既存プロジェクトにアップする場合

$ npm run setup:clone

Docker起動・終了

$ npm run docker:up
$ npm run docker:down

ログイン

$ npm run login

ソース監視・自動ビルド

$ npm run watch

プッシュ

$ npm run push

サンプル用テーブル(シート)作成

Screen Shot 2020-07-09 at 7.01.04.png

global.growSeeeds = function () {
    growSeeds()
}

※スクリプトエディタ上でグローバル関数growSeeeds()を実行してください。シートとデータが作成されます。

Examples

一部サンプル

GET

declare var global: any;

const users       = McTable.use( { name: 'users' } ) // テーブル(シート)名渡す
// spreadsheetを指定すると他のスプレットシートのデータを読み込ませられる
// const users    = McTable.use( { name: 'users', spreadsheet: SpreadsheetApp.getActiveSpreadsheet() } )
const authorities = McTable.use( { name: 'authorities' } )
const countries   = McTable.use( { name: 'countries' } )

global.doGet = function ( e ) {
    const id = +e.parameter.id

    const resultTable = users
        // usersとauthoritiesをusersのauth_idとauthoritiesのidをキーに外部結合
        .leftOuterJoin( 'auth_id',          authorities, 'id' )
        // 内部結合したテーブルとcountriesをusers.country_idとcountriesのidをもとに外部結合
        .leftOuterJoin( 'users.country_id', countries,   'id' )
        // 結合されたテーブルからusersのidをキーにレコード検索
        .find( { 'users.id': id } )
        // 抽出カラム選択
        .select( [
            'users.id',
            'users.name',
            'countries.id',
            'countries.name_ja',
            'authorities.auth'
        ] )

    resultTable.each(record => {
        const countryId = record.get( 'countries.id' )

        if ( countryId === 'USA' ) {
            // 国籍変更
            record
                .set( 'countries.id', 'JPN' )
                .set( 'countries.name_ja', 'ジパング' )
        }

        const auth = record
            // いちいちauthorities.authと入力するのが面倒なのでネームスペースを設定
            .setNamespace( 'authorities' )
            // authorities.authのデータを取得
            .get( 'auth' )

        // authorities.authが'admin'の場合にレコードを上書き
        if ( auth === 'admin' ) {
            record.set( 'auth', '管理者' )
        }

        const mail = record
            // ネームスペースをリセット
            .resetNamespace()
            .get( 'users.mail' )

        // メールダドレスの登録がなければ変わりのアドレスを設定
        if ( mail === '' ) {
            record.set( 'users.mail', 'sample@_gmail.com' )
        }
    })

    const result = JSON.stringify({
        data: resultTable.hashes()
    })

    return ContentService.createTextOutput( result );
}

// <エントリーポイントURL>?id=1
// こんな感じで帰ってくる
{
  "data":[
    {
      "users.id":1,
      "users.name":"John",
      "countries.id":"JPN",
      "countries.name_ja":"ジパング",
      "authorities.auth":"管理者"
    }
  ]
}

レコード検索

// テーブル取得
const users = McTable.use( { name: 'users' } )

// ---------------------------------------------------------------------------------------------------------------------
// SELECT * FROM users WHERE id=1
// ---------------------------------------------------------------------------------------------------------------------

users.find( { id: 1 } )
// foundUsers.data          //=> [ [ 1, 'John', 'john@_gmail.com', 1, 'USA' ] ]
// foundUsers.cols          //=> [ 'id', 'name', 'mail', 'auth_id', 'country_id' ]
// foundUsers.colsLength    //=> 1
// foundUsers.existsSheet   //=> true
// foundUsers.existsCols    //=> true
// foundUsers.existsRecords //=> true
// foundUsers.records       //=> [ { McRecord } ]
// foundUsers.recordsLength //=> 1
// foundUsers.firstRecord   //=> { McRecord }
// foundUsers.lastRecord    //=> { McRecord }
// foundUsers.mcSS          //=> { McSS }
// foundUsers.mcSheet       //=> { McSheet }
// foundUsers.savepoints    //=> { McSheet }
// foundUsers.latest        //=> { McSheet }


// ---------------------------------------------------------------------------------------------------------------------
// SELECT id, name FROM users WHERE id IN(1, 2) AND name="John"
// ---------------------------------------------------------------------------------------------------------------------

result = users
    .find( { id: [ 1, 2 ], name: 'John' } )
    .select( [ 'id', 'name' ] )

Logger.log( result.hashes() )
// [
//     {
//         name=John,
//         id=1.0
//     }
// ]

// ---------------------------------------------------------------------------------------------------------------------
// SELECT auth_id FROM (SELECT * FROM users WEHERE id NOT 1) WHERE id IN(1, 2, 3)
// ---------------------------------------------------------------------------------------------------------------------

result = users.find( { id: [ 1, 2, 3, 4 ] } ).not( { id: 1 } ).select( [ 'auth_id' ] )

Logger.log( result.hashes() )
// [
//     {
//         auth_id=1.0
//     },
//     {
//         auth_id=1.0
//     },
//     {
//         auth_id=3.0
//     }
// ]

// ---------------------------------------------------------------------------------------------------------------------
// SELECT * FROM users ORDER BY id DESC
// ---------------------------------------------------------------------------------------------------------------------

result = users.orderByDesc( 'id' )

Logger.log( result.hashes() )
// [
//     {
//         id=5.0,
//         name=pochi,
//         mail=foo@gmail.com,
//         country_id=JPN
//         auth_id=3.0,
//     },
//     {
//         id=4.0,
//         name=James,
//         mail=james@_gmail.com,
//         country_id=USA
//         auth_id=3.0,
//     },
//     {
//         id=3.0,
//         name=Emma,
//         country_id=USA,
//         mail=emma@gmail.com
//         auth_id=1.0,
//     },
//     {
//         id=2.0,
//         name=Dick,
//         country_id=USA,
//         mail=dick@_gmail.com,
//         auth_id=1.0
//     },
//     {
//         id=1.0,
//         name=John
//         country_id=USA,
//         mail=foo@_gmail.com,
//         auth_id=1.0,
//     }
// ]

レコード操作1

const users = McTable.use( { name: 'users' } )

const record1 = users.firstRecord               //=> [ 1, 'John', 'foo@_gmail.com', 1, 'USA' ] McRecordはArrayクラスを継承しているので、基本的な振る舞いはArrayクラスと同じ
record1.get( 'id' )                             //=> 1
record1.get( 'name' )                           //=> 'John'
record1.get( 'mail' )                           //=> 'foo@_gmail.com'
record1.get( 'auth_id' )                        //=> 1
record1.get( 'country_id' )                     //=> 'USA'

record1.set( 'id', 999 )                        //=> [ 999, 'John', 'john@_gmail.com',   1, 'USA' ]
record1.set( 'name', 'Smith' )                  //=> [ 999, 'Smith', 'john@gmail.com',   1, 'USA' ]
       .set( 'mail', 'smith@_gmail.com' )       //=> [ 999, 'Smith', 'smith@_gmail.com', 1, 'USA' ]
       .set( 'auth_id', 2 )                     //=> [ 999, 'Smith', 'smith@_gmail.com', 2, 'USA' ]
       .set( 'country_id', 'JPN' )              //=> [ 999, 'Smith', 'smith@_gmail.com', 2, 'JPN' ]
       .hash()                                  //=> { id: 999, name: 'Smith' , mail: 'smith@_gmail.com', auth_id: 2, country_id: 'JPN' }

レコード操作2

const users = McTable.use( { name: 'users' } )
let record

// ---------------------------------------------------------------------------------------------------------------------
// デフォルトモード(strict=false)、バリデートを通らなかったデータは''で登録
// ---------------------------------------------------------------------------------------------------------------------

record = users.Record({
    data: {
        id        : 999,
        name      : 'Smith' ,
        mail      : 'smith@_gmail.com',
        auth_id   : 2,
        country_id: 'JPN'
    },
    validators: {
        // 条件に一致しない( falseを返す )データを弾く
        id        : McRecord.validators.number,
        name      : McRecord.validators.string,
        mail      : McRecord.validators.string,
        auth_id   : McRecord.validators.string, // 数値型の値に文字列用のバリデーションを設定
        country_id: McRecord.validators.string,
    }
})

Logger.log( record )
// [999.0, Smith, smith@_gmail.com, 2.0, JPN]

Logger.log( record.hash() )
// {name=Smith, country_id=JPN, mail=smith@_gmail.com, auth_id=, id=999.0}
// auth_id=には空白が設定される

Logger.log( record.get( 'mail' ) )
// "smith@_gmail.com"



// ---------------------------------------------------------------------------------------------------------------------
// strictモード(strict=true)、バリデートを通らなかったデータがある場合にエラーになる
// ---------------------------------------------------------------------------------------------------------------------

record = users.Record({
    strict: true,
    data: {
        id        : 999,
        name      : 'Smith' ,
        mail      : 'smith@_gmail.com',
        auth_id   : 2,
        country_id: 'JPN'
    },
    validators: {
        // 条件に一致しない( falseを返す )データを弾く
        // バリデーションが設定されていない場合はデータの検証をしない
        auth_id: McRecord.validators.string,
    }
})

// Error: Invalid Col [ auth_id ] Data [ 2 ]

record = users.Record({
    strict: true,
    data: {
        id        : 999,
        name      : 'Smith' ,
        mail      : 'smith@_gmail.com',
        auth_id   : 2,
        country_id: 'JPN'
    },
    validators: {
        // 条件に一致しない( falseを返す )データを弾く
        id        : McRecord.validators.number,
    }
})

// set()内でもバリデーションを行う
record.set( 'id', 'aaa' )
// Error: Invalid Col [ id ] Data [ aaa ]


record = McRecord.create({
    strict: true,
    cols: [ 'id', 'name', 'mail', 'auth_id', 'country_id' ],
    data: {
        id        : 999,
        name      : 'Smith' ,
        mail      : 'smith@_gmail.com',
        auth_id   : 2,
        country_id: 'JPN'
    },
    validators: {
        auth_id: McRecord.validators.string,
    }
})

// Error: Invalid Col [ auth_id ] Data [ 2 ]


// ---------------------------------------------------------------------------------------------------------------------
// 関数を介した作成
// ---------------------------------------------------------------------------------------------------------------------

function createUsersRecord ( data: object ): McRecord {
    const validators = {
        // 設定されていないデータは検査しない
        id  : McRecord.validators.number,
        name: McRecord.validators.string,
        mail: McRecord.validators.string,
    }

    return users.Record( { data, validators } )
}

record = createUsersRecord({
    id        : 999,
    name      : 'Smith' ,
    mail      : 'smith@_gmail.com',
    auth_id   : 2,
    country_id: 'JPN'
})


// ---------------------------------------------------------------------------------------------------------------------
// バリデータ自作
// ---------------------------------------------------------------------------------------------------------------------

// バリデーション用関数
function validateMail ( v: any ) {
    return isString( v ) && /.*@_gmail/.test( v )
}

// バリデーション関数を登録
McRecord.setValidator( 'validateName', v => {
    return isString(v)
})


record = users.Record({
    data: {
        id        : 999,
        name      : 'Smith' ,
        mail      : 'smith@_gmail.com',
        auth_id   : 2,
        country_id: 'JPN'
    },
    validator: {
        /**
         * 条件に一致しない( falseを返す )データを弾く
         * @validator
         * @param   { * } v
         * @returns { boolean }
         */
        mail   : validateMail,
        auth_id: v => v === 1 || v === 2 || v === 3,
        name   : McRecord.validators.validateName
    }
})

トランザクション

users.begin({
    // Set users.savepoints.
    // The savepoint is updated when the instance is created and when the begin function is executed or when the save function is executed
    // users.savepoints holds a duplicate instance of an object type
    // The latest version is recorded in users.latest in numeric type

    /**
     * @param { McTable } users
     */
    handler: users => {
        users.insert( record ).commit()
        users.insert( records ).commit()

        users
            .insert( record )          // insert McRecord
            .insert( records )         // insert McRecord array
            .insert( depulicateUsers ) // insert Mctable records
            .insert({                  // insert object
                id        : 999,
                name      : 'Smith',
                mail      : 'smith@_gmail.com',
                auth_id   : 2,
                country_id: 'JPN'
            })
            .commit()

        // throw new Error( 'Error occurrence!' )
        // It rolls back the data in the sheet using the instance that was saved in the savepoint when the error occurred.
    },

    /**
     * @param { any }     e Error object
     * @param { McTable } rollbacked
     */
    exceptionHander: ( e, rollbacked ) => {
        Logger.log( e )
    }
})

テーブル更新

users.begin({
    handler ( users: McTable ) {
        users
            // レコードを一件インサート
            .insert(record1)
            // レコードを一件インサート
            .insert(record2)
            // 複数レコードをインサート
            .insert(records)
            // テーブルのレコードを纏めてインサート
            .insert(users)
            // nameがPochiのレコードを更新
            .each(record => {
                const name = record.get( 'name' )

                if ( name === 'Pochi' ) {
                    record.set( 'name', 'Tama' )
                }
            })
            // シートへ反映
            .commit()
    },

    exceptionHander ( e: any, rollbackedTable: McTable ) {
        // エラー時にuser.begin()実行時点へシートをロールバック 
        Logger.log( e )
    }
})

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

GASでORマッパー

@@ -1,546 +0,0 @@

-## はじめに

-GASでデータを取るのがめんどくさくてORマッパー的なものが欲しくなったので作りました。

-
-## 主な機能
-- テーブル操作
- - find,
- - select
- - discard
- - insert
- - inner
- - join
- - outer join
- - cross join
- - not
- - update
- - transaction (begin, save, commit, rollback)
- - record iterator
- - order by
- - sort
- - sub query
- - create
-- レコード操作
- - get
- - set (update)
- - validation
- - namespace

- - create

-Github

-## 使い方

-#### プロジェクト新規作成
-```

-$ npm run setup:create

-※途中でGoogleアカウントログインを要求されます。
-
-### 既存プロジェクトにアップする場合
-

-$ npm run setup:clone

-```

-#### Docker起動・終了
-
-$ npm run docker:up
-$ npm run docker:down
-

-#### ログイン
-
-$ npm run login
-

-#### ソース監視・自動ビルド
-
-$ npm run watch
-

-#### プッシュ
-```
-$ npm run push

-```

-### サンプル用テーブル(シート)作成

-Screen Shot 2020-07-09 at 7.01.04.png

-javascript
-global.growSeeds = function () {
- growSeeds()
-}
-

-※スクリプトエディタ上でグローバル関数growSeeeds()を実行してください。シートとデータが作成されます。

-## Examples
-- サンプル用テーブル作成
- - users
- - countries
- - authorities
- - itarator
-- テーブル操作
- - query
- - sub query
- - inner join
- - outer join
- - cross join
- - update
- - transaction
-- レコード操作
- - record
- - namespace
- - validator
-- Api

- - doGet

-### 一部サンプル

-
-#### GET
-``` javascript

-declare var global: any;

-const users = McTable.use( { name: 'users' } ) // テーブル(シート)名渡す
-// spreadsheetを指定すると他のスプレットシートのデータを読み込ませられる
-// const users = McTable.use( { name: 'users', spreadsheet: SpreadsheetApp.getActiveSpreadsheet() } )
-const authorities = McTable.use( { name: 'authorities' } )

-const countries = McTable.use( { name: 'countries' } )

-global.doGet = function ( e ) {

- const id = +e.parameter.id

  • const resultTable = users
  • // usersとauthoritiesをusersのauth_idとauthoritiesのidをキーに外部結合
  • .leftOuterJoin( 'auth_id', authorities, 'id' )
  • // 内部結合したテーブルとcountriesをusers.country_idとcountriesのidをもとに外部結合
  • .leftOuterJoin( 'users.country_id', countries, 'id' )
  • // 結合されたテーブルからusersのidをキーにレコード検索
  • .find( { 'users.id': id } )
  • // 抽出カラム選択
  • .select( [
  • 'users.id',
  • 'users.name',
  • 'countries.id',
  • 'countries.name_ja',
  • 'authorities.auth'

- ] )

  • resultTable.each(record => {

- const countryId = record.get( 'countries.id' )

  • if ( countryId === 'USA' ) {
  • // 国籍変更
  • record
  • .set( 'countries.id', 'JPN' )
  • .set( 'countries.name_ja', 'ジパング' )

- }

  • const auth = record
  • // いちいちauthorities.authと入力するのが面倒なのでネームスペースを設定
  • .setNamespace( 'authorities' )
  • // authorities.authのデータを取得

- .get( 'auth' )

  • // authorities.authが'admin'の場合にレコードを上書き
  • if ( auth === 'admin' ) {
  • record.set( 'auth', '管理者' )

- }

  • const mail = record
  • // ネームスペースをリセット
  • .resetNamespace()

- .get( 'users.mail' )

  • // メールダドレスの登録がなければ変わりのアドレスを設定
  • if ( mail === '' ) {
  • record.set( 'users.mail', 'sample@_gmail.com' )
  • }

- })

  • const result = JSON.stringify({
  • data: resultTable.hashes()

- })

  • return ContentService.createTextOutput( result ); -} - -// <エントリーポイントURL>?id=1 -// こんな感じで帰ってくる -{
  • "data":[
  • {
  • "users.id":1,
  • "users.name":"John",
  • "countries.id":"JPN",
  • "countries.name_ja":"ジパング",
  • "authorities.auth":"管理者"
  • }
  • ] -} - - -#### レコード検索 - javascript - -// テーブル取得 -const users = McTable.use( { name: 'users' } ) - -// --------------------------------------------------------------------------------------------------------------------- -// SELECT * FROM users WHERE id=1 -// --------------------------------------------------------------------------------------------------------------------- - -users.find( { id: 1 } ) -// foundUsers.data //=> [ [ 1, 'John', 'john@_gmail.com', 1, 'USA' ] ] -// foundUsers.cols //=> [ 'id', 'name', 'mail', 'auth_id', 'country_id' ] -// foundUsers.colsLength //=> 1 -// foundUsers.existsSheet //=> true -// foundUsers.existsCols //=> true -// foundUsers.existsRecords //=> true -// foundUsers.records //=> [ { McRecord } ] -// foundUsers.recordsLength //=> 1 -// foundUsers.firstRecord //=> { McRecord } -// foundUsers.lastRecord //=> { McRecord } -// foundUsers.mcSS //=> { McSS } -// foundUsers.mcSheet //=> { McSheet } -// foundUsers.savepoints //=> { McSheet } -// foundUsers.latest //=> { McSheet } - - -// --------------------------------------------------------------------------------------------------------------------- -// SELECT id, name FROM users WHERE id IN(1, 2) AND name="John" -// --------------------------------------------------------------------------------------------------------------------- - -result = users
  • .find( { id: [ 1, 2 ], name: 'John' } )

- .select( [ 'id', 'name' ] )

-Logger.log( result.hashes() )
-// [
-// {
-// name=John,
-// id=1.0
-// }

-// ]

-// ---------------------------------------------------------------------------------------------------------------------
-// SELECT auth_id FROM (SELECT * FROM users WEHERE id NOT 1) WHERE id IN(1, 2, 3)

-// ---------------------------------------------------------------------------------------------------------------------

-result = users.find( { id: [ 1, 2, 3, 4 ] } ).not( { id: 1 } ).select( [ 'auth_id' ] )

-Logger.log( result.hashes() )
-// [
-// {
-// auth_id=1.0
-// },
-// {
-// auth_id=1.0
-// },
-// {
-// auth_id=3.0
-// }

-// ]

-// ---------------------------------------------------------------------------------------------------------------------
-// SELECT * FROM users ORDER BY id DESC

-// ---------------------------------------------------------------------------------------------------------------------

-result = users.orderByDesc( 'id' )

-Logger.log( result.hashes() )
-// [
-// {
-// id=5.0,
-// name=pochi,
-// mail=foo@gmail.com,
-// country_id=JPN
-// auth_id=3.0,
-// },
-// {
-// id=4.0,
-// name=James,
-// mail=james@_gmail.com,
-// country_id=USA
-// auth_id=3.0,
-// },
-// {
-// id=3.0,
-// name=Emma,
-// country_id=USA,
-// mail=emma@gmail.com
-// auth_id=1.0,
-// },
-// {
-// id=2.0,
-// name=Dick,
-// country_id=USA,
-// mail=dick@_gmail.com,
-// auth_id=1.0
-// },
-// {
-// id=1.0,
-// name=John
-// country_id=USA,
-// mail=foo@_gmail.com,
-// auth_id=1.0,
-// }

-// ]

-```

-#### レコード操作1

-``` javascript

-const users = McTable.use( { name: 'users' } )

-const record1 = users.firstRecord //=> [ 1, 'John', 'foo@_gmail.com', 1, 'USA' ] McRecordはArrayクラスを継承しているので、基本的な振る舞いはArrayクラスと同じ
-record1.get( 'id' ) //=> 1
-record1.get( 'name' ) //=> 'John'
-record1.get( 'mail' ) //=> 'foo@_gmail.com'
-record1.get( 'auth_id' ) //=> 1

-record1.get( 'country_id' ) //=> 'USA'

-record1.set( 'id', 999 ) //=> [ 999, 'John', 'john@_gmail.com', 1, 'USA' ]
-record1.set( 'name', 'Smith' ) //=> [ 999, 'Smith', 'john@gmail.com', 1, 'USA' ]
- .set( 'mail', 'smith@_gmail.com' ) //=> [ 999, 'Smith', 'smith@_gmail.com', 1, 'USA' ]
- .set( 'auth_id', 2 ) //=> [ 999, 'Smith', 'smith@_gmail.com', 2, 'USA' ]
- .set( 'country_id', 'JPN' ) //=> [ 999, 'Smith', 'smith@_gmail.com', 2, 'JPN' ]
- .hash() //=> { id: 999, name: 'Smith' , mail: 'smith@_gmail.com', auth_id: 2, country_id: 'JPN' }

-```

-#### レコード操作2

-``` javascript

-const users = McTable.use( { name: 'users' } )

-let record

-// ---------------------------------------------------------------------------------------------------------------------
-// デフォルトモード(strict=false)、バリデートを通らなかったデータは''で登録

-// ---------------------------------------------------------------------------------------------------------------------

-record = users.Record({
- data: {
- id : 999,
- name : 'Smith' ,
- mail : 'smith@_gmail.com',
- auth_id : 2,
- country_id: 'JPN'
- },
- validators: {
- // 条件に一致しない( falseを返す )データを弾く
- id : McRecord.validators.number,
- name : McRecord.validators.string,
- mail : McRecord.validators.string,
- auth_id : McRecord.validators.string, // 数値型の値に文字列用のバリデーションを設定
- country_id: McRecord.validators.string,
- }

-})

-Logger.log( record )

-// [999.0, Smith, smith@_gmail.com, 2.0, JPN]

-Logger.log( record.hash() )
-// {name=Smith, country_id=JPN, mail=smith@_gmail.com, auth_id=, id=999.0}

-// auth_id=には空白が設定される

-Logger.log( record.get( 'mail' ) )

-// "smith@_gmail.com"

-

-// ---------------------------------------------------------------------------------------------------------------------
-// strictモード(strict=true)、バリデートを通らなかったデータがある場合にエラーになる

-// ---------------------------------------------------------------------------------------------------------------------

-record = users.Record({
- strict: true,
- data: {
- id : 999,
- name : 'Smith' ,
- mail : 'smith@_gmail.com',
- auth_id : 2,
- country_id: 'JPN'
- },
- validators: {
- // 条件に一致しない( falseを返す )データを弾く
- // バリデーションが設定されていない場合はデータの検証をしない
- auth_id: McRecord.validators.string,
- }

-})

-// Error: Invalid Col [ auth_id ] Data [ 2 ]

-record = users.Record({
- strict: true,
- data: {
- id : 999,
- name : 'Smith' ,
- mail : 'smith@_gmail.com',
- auth_id : 2,
- country_id: 'JPN'
- },
- validators: {
- // 条件に一致しない( falseを返す )データを弾く
- id : McRecord.validators.number,
- }

-})

-// set()内でもバリデーションを行う
-record.set( 'id', 'aaa' )

-// Error: Invalid Col [ id ] Data [ aaa ]

-
-record = McRecord.create({
- strict: true,
- cols: [ 'id', 'name', 'mail', 'auth_id', 'country_id' ],
- data: {
- id : 999,
- name : 'Smith' ,
- mail : 'smith@_gmail.com',
- auth_id : 2,
- country_id: 'JPN'
- },
- validators: {
- auth_id: McRecord.validators.string,
- }

-})

-// Error: Invalid Col [ auth_id ] Data [ 2 ]

-
-// ---------------------------------------------------------------------------------------------------------------------
-// 関数を介した作成

-// ---------------------------------------------------------------------------------------------------------------------

-function createUsersRecord ( data: object ): McRecord {
- const validators = {
- // 設定されていないデータは検査しない
- id : McRecord.validators.number,
- name: McRecord.validators.string,
- mail: McRecord.validators.string,

- }

  • return users.Record( { data, validators } ) -} - -record = createUsersRecord({
  • id : 999,
  • name : 'Smith' ,
  • mail : 'smith@_gmail.com',
  • auth_id : 2,
  • country_id: 'JPN' -}) - - -// --------------------------------------------------------------------------------------------------------------------- -// バリデータ自作 -// --------------------------------------------------------------------------------------------------------------------- - -// バリデーション用関数 -function validateMail ( v: any ) {
  • return isString( v ) && /.*@_gmail/.test( v ) -} - -// バリデーション関数を登録 -McRecord.setValidator( 'validateName', v => {
  • return isString(v) -}) - - -record = users.Record({
  • data: {
  • id : 999,
  • name : 'Smith' ,
  • mail : 'smith@_gmail.com',
  • auth_id : 2,
  • country_id: 'JPN'
  • },
  • validator: {
  • /**
  • * 条件に一致しない( falseを返す )データを弾く
  • * @validator
  • * @param { * } v
  • * @returns { boolean }
  • */
  • mail : validateMail,
  • auth_id: v => v === 1 || v === 2 || v === 3,
  • name : McRecord.validators.validateName
  • } -}) - - - - -#### トランザクション - javascript -users.begin({
  • // Set users.savepoints.
  • // The savepoint is updated when the instance is created and when the begin function is executed or when the save function is executed
  • // users.savepoints holds a duplicate instance of an object type

- // The latest version is recorded in users.latest in numeric type

  • /**
  • * @param { McTable } users
  • */
  • handler: users => {
  • users.insert( record ).commit()

- users.insert( records ).commit()

  • users
  • .insert( record ) // insert McRecord
  • .insert( records ) // insert McRecord array
  • .insert( depulicateUsers ) // insert Mctable records
  • .insert({ // insert object
  • id : 999,
  • name : 'Smith',
  • mail : 'smith@_gmail.com',
  • auth_id : 2,
  • country_id: 'JPN'
  • })

- .commit()

  • // throw new Error( 'Error occurrence!' )
  • // It rolls back the data in the sheet using the instance that was saved in the savepoint when the error occurred.

- },

  • /**
  • * @param { any } e Error object
  • * @param { McTable } rollbacked
  • */
  • exceptionHander: ( e, rollbacked ) => {
  • Logger.log( e )
  • } -}) - - -#### テーブル更新 - javascript -users.begin({
  • handler ( users: McTable ) {
  • users
  • // レコードを一件インサート
  • .insert(record1)
  • // レコードを一件インサート
  • .insert(record2)
  • // 複数レコードをインサート
  • .insert(records)
  • // テーブルのレコードを纏めてインサート
  • .insert(users)
  • // nameがPochiのレコードを更新
  • .each(record => {

- const name = record.get( 'name' )

  • if ( name === 'Pochi' ) {
  • record.set( 'name', 'Tama' )
  • }
  • })
  • // シートへ反映
  • .commit()

- },

  • exceptionHander ( e: any, rollbackedTable: McTable ) {
  • // エラー時にuser.begin()実行時点へシートをロールバック 
  • Logger.log( e )
  • } -}) - -```
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Jupyter・RStudioみたいにパッと画像を表示する機能を作る【Docker】

概要

普段neovimでPythonやRのスクリプトを書くのですが、グラフをpngやhtmlで出力した後にいちいちファイルを開いて確認するのが面倒でした。新しいファイルが自動でパっと表示される機能がほしいなと思いdockerで作ってみました。

Seo5IDfC4BCZXHQDD0QL1594225984-1594226031.gif

初めに断っておきますが、大したものは作っていません。「ふーん、そんなこと考える人がいるんだ」くらいのテンションで読んでもらえれば嬉しいです。

使い方

まずは以下のスクリプトをdocker-compose.ymlという名前で保存1。docker-composeは使えるようにしておいてください。

docker-compose.yml
version: "3"
services:
  websocket:
    image: dr666m1/image_watcher_websocket:version-0.0
    volumes:
      - .:/work/sync
    ports:
      - "9999:9999"
  webserver:
    image: dr666m1/image_watcher_webserver:version-0.0
    volumes:
      - .:/work/sync
    ports:
      - "8888:8888"
    depends_on:
      - websocket

その後、pngやhtmlを出力する予定のディレクトリに移動し、以下のコマンドで起動・停止($FILE_PATHは先ほどのdocker-compose.yml)。

# 起動
docker-compose -f $FILE_PATH --project-directory $(pwd) up -d

# 停止
docker-compose -f $FILE_PATH --project-directory $(pwd) down

起動中にブラウザでhttp://localhost:8888/を開くと冒頭に載せたような画面になります。

仕組み

docker-compose.ymlを見ての通り、2つのdockerコンテナが動いています。以下、それぞれの役割を簡単に説明します。コードは私のgithubに載せています。

websocket (image_watcher_websocket)

WebSocketとは何か、という解説は他の記事にお任せします。このdockerコンテナの役割は以下です。

  • png・htmlファイルの作成or更新を数秒置きに検知する
  • 検知したファイルの情報をブラウザに送信する

実装にはPythonのwebsocket-serverというパッケージを利用しました。

webserver (image_watcher_webserver)

Webサーバーとは何か、という解説も他の記事にお任せします。このdockerコンテナの役割は以下です。

  • ローカルの8888番ポートでリクエストを受け付けindex.htmlを返す
  • png・htmlファイルもリクエストがあれば返す

実装にはPythonのFlaskというパッケージを利用しました。index.htmlの中ではReactを多用しています。

こだわり

表示・非表示の切り替え

ファイル名の左の「▶」「▼」で、表示・非表示の切り替えができます。

別画面での表示

ファイル名をクリックすると、そのファイルだけ別画面で表示できます。

その他

htmlファイルのscrollHeightに合わせてiframeの縦幅を自動修正したりとか、トップに戻るボタンの実装とかもちょっとしたこだわりです。

最後に

業務では分析用のPython・R・SQLくらいしか書かないので、Reactで画面を作る作業は勉強になりました。


  1. 見たことないdockerイメージが指定されていると思いますが、私が作成したものです。DockerHubで公開しているので普通にdocker pullできるはずです。 

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

curl でダウンロードしつつ任意の場所にtar展開する

ruby:2.6.5-slim-stretch のDockerイメージを使い yarn を入れるのに苦労したので記録を残しておきます。

RUN部分の抜粋。

ENV YARN_VERSION=1.22.4
RUN bash -c "curl -sL --compressed https://yarnpkg.com/downloads/${YARN_VERSION}/yarn-v${YARN_VERSION}.tar.gz | tee >(tar zx -C /usr/local/ --strip=1 --wildcards yarn*/bin --no-same-owner --no-same-permissions) | tar zx -C /usr/local/ --strip=1 --wildcards yarn*/lib --no-same-owner --no-same-permissions"

3つのコマンドをパイプで連結した文字列を用意して bash -c "curl..." で実行するようにしています。

はじめに curl を使って指定されたバージョンの yarn を標準出力に表示します。

curl -sL --compressed https://yarnpkg.com/downloads/${YARN_VERSION}/yarn-v${YARN_VERSION}.tar.gz

tee を使い出力先を分岐します。 /usr/local/bin に tar内の */bin 以下を展開します。

| tee >(tar zx -C /usr/local/ --strip=1 --wildcards yarn*/bin --no-same-owner --no-same-permissions)

同様に /usr/local/lib に tar 内の */lib 以下を展開します。

| tar zx -C /usr/local/ --strip=1 --wildcards yarn*/lib --no-same-owner --no-same-permissions

bash -c "" をしているのは、 2つ目の | tee >(tar...) にてプロセス展開を行うためです。

docker build を行う際に sh 環境で実行されるので、プロセス展開部分で、 /bin/sh: 1: Syntax error: "(" unexpected がでてしばらく悩みました…。

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

Dockerを使ったphp-fpm(+Laravel)とNginx環境の構築

背景

「LaravelってどうやってHTTPサーバと連携するんだ?」と思い、調査しましたが結構はまったので、自分用のメモも兼ねて記事を書きます。

やりたいこと

・Laravelを使ったWebアプリサーバを立てたい
・Dockerを使って、コマンド一発で楽にWebサーバとWebアプリサーバを立てたい

特にDockerを使ってWebサーバとWebアプリサーバを立てるのは、Kubernetesを使うのに必須だったりするので、鍛錬もかねて、実施しました。

システムの全体図

システムというほどのものではないですが、下記のようにDockerの環境を構築しました。
システム構成図.png

・Nginxのコンテナを立てて、80番と443番でHTTP/HTTPSのリクエストを受け付ける
・NginxのコンテナとWebアプリのコンテナは9000番で通信
・Webアプリのコンテナには、外部から直接アクセスはできない

負荷分散は今回は未実施です。
(勉強のためKubernetesやりたいので、取り組んだらまた投稿します。)

今回もdocker-compose(コンテナをまとめて管理できるツール)を使いました。

Composeファイル

ディレクトリ構造は下記のようにしました。

project/
  ├ nginx/
  │  └ dockerfile
  │  └ server.conf
  ├ php-fpm/
  │  └ dockerfile
  │  └ www.conf
  ├ web/
  │  └ Laravelアプリ/
  ├ docker-compose.yml

docker-compose.ymlは下記のようにしました。

docker-compose.yml
version: '3'
services:
    web:
        build: 
            context: ./nginx
        ports:
            - "80:80"
            - "443:443"
        volumes:
            - ./web/public:/etc/nginx/public
            - ../ssl/certs/:/etc/pki/tls/certs/
            - ../ssl/private/:/etc/pki/tls/private/
        depends_on:
            - app
        container_name: blog_web
    app:
        build: ./php-fpm
        volumes:
          - ./web:/var/www/
        container_name: blog_app

webがNginxコンテナで、appがphp-fpm+laravelのコンテナです。

Nginxコンテナは
・外部から、80番と443番を受け付けるようにし、appと通信できる
・publicディレクトリにおいてある、cssやjsを直接さわれるようにする(staticファイルはnginxから直接返す)
・sslの証明書と鍵は、アクセスの権限を調整して、マウントして見れる

php-fpm+Laravelコンテナは
・ソースをまるまるマウントする

という感じです。

Nginxのコンテナ

下記のように書くだけです。

Dockerfile
FROM nginx:latest

# 設定ファイルを指定の場所に置く
COPY ./server.conf /etc/nginx/nginx.conf


CMD ["nginx", "-g", "daemon off;"]

server.conf
user nginx;

worker_processes auto;
pid /var/run/nginx.pid;

events{
    worker_connections 2048;
    multi_accept on;
    use epoll;
}


http {
    charset UTF-8;
    # versionを表示しない
    server_tokens off;
    include /etc/nginx/mime.types;
    default_type text/plain;
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;
    ssl_protocols TLSv1.1 TLSv1.2;

    server {
            listen 80;
            server_name localhost;
            # 80番でアクセスしてきた人は、443番のリダイレクトを返す
            return 301 https://$host$request_uri;
    }
    server {
            listen 443;
            ssl on;
            server_name  localhost;
            # 証明書
            ssl_certificate /etc/pki/tls/certs/example.crt;
            # 秘密鍵
            ssl_certificate_key /etc/pki/tls/private/example.key;

            # rootディレクトリ
            root /var/www/public;

            add_header X-Frame-Options "SAMEORIGIN";
            add_header X-XSS-Protection "1; mode=block";
            add_header X-Content-Type-Options "nosniff";

            # indexファイルの指定
            index index.php index.html;

            charset utf-8;

            # アクセスしてきたパスに対応するファイルを返す
            location / {
                try_files $uri $uri/ /index.php?$query_string;
            }

            # staticファイルはnginxが直接返す

            # cssファイル
            location /css/ {
               alias public/css/;
            }
            # jsファイル
            location /js/ {
                alias public/js/;
            }
            # imageファイル
            location /images/ {
                alias public/images/;
            }
            # fontsファイル
            location /fonts/ {
                alias public/fonts/;
            }
            # faviconリクエストはログを残さない
            location = /favicon.ico { access_log off; log_not_found off; }

            # php-fpmとの連携
            location ~ \.php$ {
                fastcgi_split_path_info ^(.+\.php)(/.+)$;
                fastcgi_pass  app:9000;
                fastcgi_index  index.php;
                include fastcgi_params;
                fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
                fastcgi_param  PATH_INFO $fastcgi_path_info;
            }

            # 指定のパス以外へのアクセスを禁止する
            location ~ /\.(?!well-known).* {
                deny all;
            }

            # 証明書更新のパスへアクセスできるようにする
            location /.well-known/ {
                alias public/.well-known/;
            }

            # クローラがアクセスできるようにする
            location /robots.txt {
                alias public/robots.txt;
            }

            # サイトマップにアクセスできるようにする
            location /sitemap.xml {
                alias public/sitemap.xml;
            }
    }
}

まだわかっていない箇所もありますが、
Nginxのコンテナは、

fastcgi_pass  app:9000;

とすることで、Webアプリのコンテナのpublicディレクトリのindex.phpをたたきにいけるようにしています。
私はここの記載の仕方でちょっとはまりました。。。

cssファイルやjsファイルなどのstaticファイルは、Nginxコンテナがアプリのpublicディレクトリをマウントしていて、Nginxから直接返せるようにしています。
クローラが見に来れるようにrobots.txtとsitemap.xmlにアクセスできるようにしています。

php-fpm+Laravelコンテナ

dockerfileに入る前に、そもそもphp-fpmとは何ぞ???というのが自分の中であったので、調べました。
公式ドキュメントとか見ましたが、一番わかりやすかったのは他者様の記事

php-fpmとは

php向けのFastCGIで、メモリでキャッシュを保持することで高速にWebサーバ上でPHPを動作させるアプリケーション

ということです。
この他者様の記事ではコンテナ間の通信のことも書いてありましたが、今回Nginxとphp-fpmはTCPで通信しています。
次やる時はUNIXドメインソケットで通信しようかと思います。

さて、Dockerfileですが、下記のようにしました。

Dockerfile
FROM php:7.4.7-fpm

RUN apt update -y
RUN apt install -y libfcgi0ldbl curl git unzip wget vim

# nginxというユーザを作る
RUN useradd -m -s /bin/sh -u 1000 nginx

# 設定ファイルを指定の場所に置く
COPY ./www.conf /usr/local/etc/php-fpm.d/zzz-www.conf

# Composeインストール
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

# nodejs 12インストール
RUN apt install -y npm
RUN npm install n -g
RUN n 12

# ユーザ変更
USER nginx

# 作業ディレクトリ
WORKDIR /var/www

VOLUME ["/var/run/php-fpm"]

www.conf
[www]
user = nginx
group = nginx
listen = 9000
listen.owner = nginx
listen.group = nginx
listen.mode = 0660
request_terminate_timeout=30s

php-fpmの設定はwww.confの設定ファイルと公開されているDockerImageで設定は簡単にできました。
あとは、コンテナにComposerとnodejs12を入れたかったので、Dockerfile内でインストールするようにしました。
実際のnpmパッケージのインストールと、composerのビルドは、コンテナを立ち上げた後、コマンドで実行しています。

docker exec blog_app composer install --optimize-autoloader --no-dev
docker exec blog_app npm install
docker exec blog_app npm run production

このようにしたのは、マウント後にnpmパッケージのインストールやcomposerのビルドをしないと、ソースがない時にdockerビルドが走り、失敗してしまうからです。
dockerfile内で、ソースをとってくる処理を書いている場合は、dockerfile内でnpmパッケージのインストールやcomposerのビルドができます。

Composeのビルド

あとは、コンテナのビルド+コンテナ起動して、npmパッケージのインストールやcomposerのビルドして終了です。

docker-compose build

docker-compose up -d

ドメイン設定・SSL対応

ドメインはこちらで買いました。
DNSはAWSのものを使いたかったので、Route53を使用しました。
設定は下記です。
route53_dns.png

DNSの設定はこちらを参考しました。

最低限の設定は下記で、
・Aレコード:グローバルIPとホスト名の紐付け
・NSレコード:ドメインのDNSサーバ(ドメインを購入したサイトのネームサーバにこの値を登録します。)
・SOAレコード:上位のDNSサーバ
オプションで、
・CNAMEレコード:別ホスト名
・TXTレコード:今回はクローラの認証情報記載
を記載しています。

証明書の取得は、お金をかけずにやりたかったので、
他者様の記事を参考にbacmeでオレオレ証書を取得しました。

ホスト名とマシンの紐付けができていれば、やることは単純で、
bacmeをgit cloneしてきて、取得したホスト名で

./bacme -w /var/www/example/ example.com www.example.com

を実行します。wは、.well-knownディレクトリのパスです。
(80番でwell-knownのパスにアクセスできないと↑のコマンドは失敗します。アクセスできるようにNginxの設定を見直してみてください。)
成功すると、bacmeディレクトリ直下に証明書と秘密鍵ができますので、
Nginxのマウント先(/etc/pki/tls/certs/)に指定してください。

参考文献

nginx と PHP-FPM の仕組みをちゃんと理解しながら PHP の実行環境を構築する

調べなきゃ寝れない!と調べたら余計に寝れなくなったソケットの話

Dockerで構築したnginxをSSL化対応する

xdomain

次は、最低限のセキュリティ設定とKubernetesのことを調べます。

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