20200703のdockerに関する記事は8件です。

【Docker】プロセスの分離とは

はじめに

Dockerを使用するに当たって、Dockerの特徴でもあるコンテナによるプロセスの分離について学んだのでまとめます。

仮想マシンvsコンテナ

仮想化ソフトウェアには「ホスト型仮想化ソフトウェア」や「ハイパーバイザー型仮想化ソフトウェア」、「コンテナ型仮想化ソフトウェア」などの種類があります。
仮想化ソフトウェアの種類

ハイパーバイザー型仮想化

ハイパーバイザー型の仮想化ソフトウェアはハイパーバイザーと呼ばれるソフトウェアが、仮想的なハードウェアである「仮想マシン」を提供し、それによってゲストOSからはあたかも物理的なマシンで稼働しているかのように見えています。
ハイパーバイザー型の仮想化ソフトウェアはハードウェアをエミュレーション(ある装置やソフトウェア、システムの挙動を別のソフトウェアによって模倣し代替として動作させること)しています。

コンテナ環境

コンテナ環境は名前空間(namespace)とcgroupと呼ばれる資源管理の仕組みを使うことで単一のOS内で複数のコンテナがプロセス(Linux上で動いているプログラム)として稼働しています。
Linuxの「プロセス」って何だろう?

名前空間とは?

それでは、本題に入ります。
上記のハイパーバイザー型仮想化では、ハイパーバイザーにより仮想化を実現していましたが、Dockerによるコンテナ仮想化はどのようにして分離された空間を作り出しているのでしょうか?

Dockerエンジンによるコンテナ仮想化を実現する上で重要なのが名前空間です。
広義での名前空間とは以下のようなものです。

名前空間(なまえくうかん)はNamespaceの訳語で、名前の集合を分割することで衝突の可能性を低減しつつ参照を容易にする概念である。

名前空間Wiki

名前空間とは何かを分離するものという概念を表しているようです。

Dockerにおける名前空間

単一のOS環境に置いて、Dockerは複数の分離された空間を作成できます。その分離された空間を実現しているのが名前空間(namespace)です。
名前空間を使うと、ファイルシステムやネットワーキング無堂、各コンテナのシステムリソースを分離できます。
例えば、ある分離された空間Aのプロセスは別の分離された空間Bには見えないといった制御をします。
また、名前空間はプロセスだけでなくファイルシステムへのアクセスの分離を実現することもできます。

Dockerがコンテナを作成する際に作られる名前空間

Dockerがコンテナを作成する際、以下のような名前空間が作成されます。

  • ipc名前空間:Inter Process Communication名前空間と呼ばれる内部的なプロセス間通信を分離する
  • mnt名前空間:プロセスから見えるファイルシステムのマウント(外部の記憶媒体を扱うための方法)情報を分離し、chrootコマンドに近い動きをする
  • net名前空間:ネットワークの制御を行う名前空間。net空間ごとにネットワークインターフェースを持つことができるため、複数のコンテナとホスト間でネットワーク通信を行うことができる
  • pid名前空間:プロセスの分離に使用する。カーネルが制御しており、親PIDによる子PIDの制御などに利用される
  • user名前空間:ユーザーIDとグループIDを分離する。user名前空間ごとに個別のユーザーIDとグループIDを保持できる
  • uts名前空間:UTS(UnixTime-SharingSystem)名前空間と呼ばれる。ホスト名やNiSドメイン名(NISという仕組みで登場するコンピュータに付けた管理上の名前)などの分離に使用する

Linuxのマウントについて
NISドメイン名とは

pid名前空間の分離

前述の通り、Dockerコンテナを実行するとDockerエンジンはそのコンテナに対する名前空間を作成します。
各コンテナは別々の名前空間で動作しており、アクセスはその名前空間に限定されます。
これによって、ホストOSからは複数の名前空間に属するプロセスが一様に動いているよう見えるのに対して、個々の名前空間の中ではその名前空間に所属するプロセスしか見えていません。
物理基盤におけるPID名前空間とコンテナ基盤によるPID名前空間を図に表すとそれぞれ以下のようになります。

  1. 物理基盤におけるPID名前空間
           real_pid.png

  2. コンテナ基盤におけるPID名前空間
    virtual_pid-2.png

コンテナを使用した名前空間分離の例

例えば、Dockerエンジンが稼働するホストOSによって、Webサーバーのhttpdサービスが稼働するコンテナとFTPサーバーのvsftpdサービスが稼働するコンテナの2つが起動しているとします。
ホストOSはhttpdデーモンにプロセスID(PID)として1000番を割り当て、vsftpdデーモンにPIDとして2000番を割り当てたとすると、ホストOSからはhttpdとvsftpdがプロセスとして稼働しているように見えています。
この時、httpdサービスが稼働するコンテナ内ではhttpdにPIDの1番が割り当てられ、vsftpdサービスが稼働するコンテナ内ではvsftpdにPIDの1番が割り当てられます。
ここでhttpdが稼働するコンテナとvsftpdが稼働するコンテナは別々のPID名前空間であるためそれぞれのコンテナ内ではお互いのプロセスを見ることができず分離された空間となっています。

最後に

今回はdockerの名前空間による分離の中でも特にプロセスidの分離について調べましたが、その他にもファイルシステムの名前空間も同様にコンテナとして分離されます。また、ハードウェアのリソースの制限はcgroupsという仕組みで行っています。
Dockerを使用する前に、これらのDockerの仕組みを学ぶことで、実際にコマンドを叩く際にも内部で起こっていることのイメージもつきやすくなりました。

参考

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

【Ubuntu】DockerでRuby on Railsの環境構築中にPermissionErrorが起きる

背景

https://docs.docker.com/compose/rails/

Ubuntu上から上記に従ってすすめると以下のコマンド実行時にでエラーが出る
$ docker-compose run web rails new . --force --no-deps --database=postgresql

【エラー内容】
PermissionError: [Errno 13] Permission denied: '***/myapp/tmp/db'

原因

rails new実行時に作成されるmyapp/tmp/dbの所有者がrootになっているためコマンドが正常に動作しない模様

解決策

myappディレクトリ直下で以下を実行し/tmp/dbの所有者をログイン中のユーザーへ変更
sudo chown -R $USER:$USER .

参考

https://forums.docker.com/t/permissionerror-errno-13-permission-denied-home-fred-docker-tmp-db/81168/2

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

ansible-bender でコンテナイメージを作成する

概要

Ansible と同じ書式で、コンテナイメージの作成手順を指定することができる ansible-bender を試してみた。

環境構築

手順書にしたがって、ansible-bender をインストールする。

$ sudo pip3 install ansible-bender

ansible-bender は内部的に buildah と podman を呼び出すので、手順書にしたがって、buildah と podman をインストールする。参考までに、私が使っている ansible playbook は以下の通り

最小コンテナイメージを用意する

ansible-bender は Python がインストール済みのコンテナイメージが必要。だが、DockerHub で配布されている Debian イメージには、Python がまだ含まれていないので、このイメージに対しては ansible-bender は適用できない。

そこで、Python を含む最小のコンテナイメージを用意しておくと便利である。まず、以下のような Dockerfile を用意する。

Dockerfile
FROM debian:buster
MAINTAINER Anatano Namae <namae@example.jp>
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y python3 && apt-get clean

次に、以下のコマンドを実行して、debian-python3 という名前のコンテナイメージを作成する。

$ sudo podman build -t debian-python3:buster -f Dockerfile

作成が完了すれば、以下のようにイメージ一覧に表示されるようになるはずである。

$ sudo podman images
REPOSITORY                 TAG      IMAGE ID       CREATED          SIZE
localhost/debian-python3   buster   7b80a3ed5790   42 seconds ago   199 MB

ansible-bender でコンテナイメージを作成する

コンテナイメージを作成するための手順書を、Ansible とほぼ同じ形式で用意する。

sample.yml
- name: ntpd container
  hosts: all
  vars:
    ansible_bender:
      base_image: debian-python3:buster
      target_image:
        name: debian-ntpd:buster
  tasks:
   - name: install ntpd
     apt:
       name: ntpd
       state: latest

手順書の書き方詳細については、ansible-bender のドキュメントを参照。

後は、以下のコマンドを実行すれば良い。

$ sudo ansible-bender build sample.yml

正常にコンテナイメージの作成が完了すると、以下のコマンドでイメージができていることが分かる。

$ sudo podman images
REPOSITORY              TAG      IMAGE ID       CREATED          SIZE
localhost/debian-ntpd   buster   ab26e1f28761   13 minutes ago   223 MB

なお、最新の ansible-bender はバックエンドとして buildah のみをサポートしており、docker サポートは打ち切られている。そのため、直接 docker 形式のコンテナイメージを作成することは できない

インストールするパッケージが多い場合は、構築時間短縮のために HTTP_PROXY を指定するなど細かい調整が必要になるのだが、設定方法の調査が完了していない。

作成したコンテナイメージを docker 形式に変換する

buildah と docker の両方がインストールされていれば、以下のコマンドで podman のコンテナイメージを、同一ホストの docker に転送することができる。

$ sudo buildah push debian-python3:buster docker-daemon:debian-python3:buster

その上で、以下のように docker save と docker load を組み合わせれば、他ホストに docker コンテナイメージを転送できる。

$ sudo docker save debian-python3:buster | ssh docker.example.net sudo docker load
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Arch Linux に kubeadm で開発用 Kubernetes クラスタを構築してみる

はじめに

最近お仕事で Kubernetes クラスタをぼちぼち触り始めました。そういうわけで手元にも開発用に Kubernetes クラスタがほしくなってくるわけですが、いつも使っているのは Arch Linux です。そこで、今回は Arch Linux に kubeadm を使って Master と Worker が同居する1台構成の Kubernetes クラスタを作る方法を紹介します。

インストール前の準備

もちろん Arch Linux のインストールが必要ですが、下記の点によく注意してください。

Swap の無効化

Swap が有効化されていると kubeadm は動かすことができません。最近マシンスペック向上のおかげで Swap を作る機会は減っているかもしれませんが、もし有効にしているのであれば無効化しておいてください。

ファイルシステムについて

kubeadm では btrfs をサポートしていません。もし btrfs でディスクをフォーマットしてしまっていたら、ext4 で作り直す必要があります。要するに、クリーンインストールです......

AUR について

今回紹介する方法は AUR にしか存在していないパッケージを利用します。AUR の利用を容易にするために AUR が扱えるパッケージマネージャー (yayなど) を導入しておくことをオススメします。

Docker のインストール

何はともあれコンテナ基板として Docker をインストールします。

Docker パッケージのインストール

ひとまずリポジトリからインストールして、sudo コマンドなしでも動くように設定しておきます。usermod コマンドを実行したら一度ログアウトして再ログインします。

$ yay -S docker
$ sudo usermod -a -G docker ${USER}

設定の変更

/etc/docker/daemon.json ファイルを新規生成して、以下の内容を書き込んでください。特にストレージドライバの設定は重要です。

$ sudo mkdir /etc/docker
$ sudo vim /etc/docker/daemon.json
/etc/docker/daemon.json
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m"
  },
  "storage-driver": "overlay2"
}

Docker デーモンの起動

あとはさっくりと systemd で Docker デーモンを起動しておきましょう。

$ sudo systemctl daemon-reload
$ sudo systemctl enable --now docker

Kubernetes のインストール

さて、準備が済んだのでいよいよ Kubernetes をインストールしていきます。

必要なパッケージの導入

Kubernetes そのものの動作や操作に必要な kubectl kubelet kubeadm を AUR からインストールします。また、後ほどクラスタ内ネットワークの構築に必要となる cni-plugins もここで入れておきます。

$ yay -S kubectl-bin kubelet-bin kubeadm-bin cni-plugins-bin

また、Kubernetes が必要とするネットワーク関連のパッケージを追加でインストールしてください。

$ yay -S ethtool ebtables socat conntrack-tools

kubelet の起動

ここまで終わったら kubelet を起動する設定を有効化します。

$ sudo systemctl enable --now kubelet

Master node の初期化

いよいよ Master node を初期化して、動作できるようにします。下記コマンドを流し込んで数分待つとできあがりです。なお pod-network-cidr に渡している IP アドレスですが、後ほど flannel (内部ネットワーク) を有効化する際に必要なものです。また、flannel のドキュメントによるとこの IP アドレスで固定されているようですので、他のものに変えたりはしないでください。

$ sudo kubeadm init --pod-network-cidr=10.244.0.0/16

構築が終わったら表示されたコマンドを使って管理者として kubectl コマンドを実行できるようにしていきます。

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

最後に Master node にコンテナを建てられるように設定を変更します。通常はセキュリティ面から Master node にユーザーがコンテナを建てることはできませんが、今回は手元開発用の1台構成ですので、それを許可するようにしておきます。もちろん、本番環境で使う Master node ではこの設定を有効にしないでください。

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

Flannel のインストール

クラスタ内部ネットワークを構築するため Flannel をインストールします。以下のコマンドを流し込んでください。

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

動作テスト

Nginx のコンテナを建てて、クラスタが動作しているかどうか確認してみましょう。適当なディレクトリに nginx.yaml を下記の内容で用意してください。

$ vim nginx.yaml

yamlファイルの中身をざっくり説明すると、Nginx のコンテナを2個立てて、8080番ポートを露出させるという設定になっています。

nginx.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  type: LoadBalancer
  selector:
    app: nginx
  ports:
    - name: nginx
      port: 8080
      targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80

ファイルが書けたらクラスタにデプロイしてみましょう。

$ kubectl apply -f nginx.yaml

数分待って Pod と Service の一覧を取得します。定義通り Nginx のコンテナが2つ READY 状態になっており、 service/nginx ができあがっていることが確認できます。

$ kubectl get pods,services
NAME                        READY   STATUS    RESTARTS   AGE
pod/nginx-d46f5678b-sm685   1/1     Running   0          3m18s
pod/nginx-d46f5678b-whdbn   1/1     Running   0          3m18s

NAME                 TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
service/kubernetes   ClusterIP      10.96.0.1        <none>        443/TCP          15m
service/nginx        LoadBalancer   10.111.243.255   <pending>     8080:30044/TCP   4m14s

Nginx にアクセスしたいときは localhost から PORT(S) 欄の右側に表示されている方のポート(今回であれば30044)にアクセスしてあげると繋がります。ここまで確認できればOKです。

$ curl http://localhost:30044/
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<!-- 後略 -->

おわりに

この記事では Arch Linux に kubeadm を使って Kubernetes クラスタを構築する方法を紹介してきました。kubeadm を使えばこうして簡単に開発用のクラスタも構築できます。また、公式ドキュメントではサポートされていない Arch Linux でも以外と平気で動かせてしまうということも明らかになりました。皆さんもぜひご自分の Arch Linux 環境に Kubernetes 開発環境を用意して快適な開発者生活を送ってください。

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

Golang+Docker+LINE BOT+QiitaAPI+herokuで何か作ろうとした話

記事概要

Golang+Docker+LINE BOT+QiitaAPI+herokuで何か作ろうとしてハマった事とか色々まとめるだけの記事。

筆者について

  • Golang初見。
  • linebot、herokuはPHPとRailsで触った事がある。
  • Dockerは便利すぎてハマりだしてる。

リポジトリ

https://github.com/YanaPIIDXer/QiitaLineBot
実際に動作させるherokuとLINEのアカウントは非公開で。。。
「どんなものを作るか」「設計はどうするか」はここのREADMEに。

採用したもの

  • gin
    • Webサーバライブラリ。
  • Realize
    • ホットリロード。
    • 後述。

Golangのホットリロード

「コードを書き換えると自動でビルドが走って、後はブラウザを更新するだけ」と言う機能。

導入

Realizeを使ってGoでホットリロードを実現する
上記記事を参考にRealizeを導入。
・・・が、Realize起動時のビルドは走ってるっぽいがホットリロードは機能しない。

legacyの有効化

どうもRealizeをDocker(コンテナ)上で動作させるには、legacyを有効化する必要がある模様。
ここを参照。
.realize.yamlの記述を

.realize.yaml
settings:
  legacy:
   force: true
(省略)

として再起動したら無事動作。

herokuへのdeploy

ポート

ローカル環境で立ち上げている時はポート3000を使用しているが、heroku上で動作させる場合はherokuが指定したポートで処理をしなければならない。

プログラム側の対応

環境変数「PORT」に値が入っていればheroku上での動作としてそのポートを、入っていなければローカルでの動作としてポート3000を利用するようにしてみよう。

Dockerfile
# 以下の記述を追加。
ENV PORT=${PORT}
main.go
// サーバ起動部分。
var port = os.Getenv("PORT")        // heroku上で動かす場合は指定されたPortじゃないと死なますよ。
if port == "" {
    port = "3000"
}
fmt.Println("Server Port:" + port)
router.Run(":" + port)

これでdeployしたら動・・・かなかった。

コンテナ構成の変更

色々調査しながら、ふとdocker-compose.ymlを眺めると・・・

docker-compose.yml
 (省略)
    ports:
      - "3000:3000"
    command: realize start --name='server' --run
 (省略)

・・・

docker-compose.yml
    ports:
      - "3000:3000"

お 前 か 。
ここもherokuが使ってるポートにしてやらないと動かないんじゃないかな・・・?

色々調べて、以下の記事がヒット。
golang,docker,mysqlの環境をherokuにデプロイする

この記事にある通りにコンテナ構成を変更。
上記の記事ではdocker-compose.ymlをdockersディレクトリ内に配置しているが、同じようにすると動作せず。
多分どこかでパスに問題が出てるんじゃないかな・・・?未調査だけど。
面倒なのでルートディレクトリに別名(docker-compose-local.yml)で放り込むと動作するようになった。

herokuにdeployして、ログを見た感じエラーとかも無さそう。
いざアクセス・・・が、動作せず。

SSL対応

「あれ?」と思っておもむろに
heroku logs
を実行。
アクセス時にエラーになってた。
で、そのログをよく見ると・・・

protocol=https

・・・FxxK!!
こりゃまた頭の痛い問題だ・・・。
(今ここ。進展あれば更新予定。)

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

Golang+Docker+herokuで何か作ろうとした話

記事概要

Golang+Docker+herokuで何か作ろうとしてハマった事とか色々まとめるだけの記事。

筆者について

  • Golang初見。
  • herokuはPHPとRailsで触った事がある。
  • Dockerは便利すぎてハマりだしてる。

採用したもの

  • gin
    • Webサーバライブラリ。
  • Realize
    • ホットリロード。
    • 後述。

リポジトリ

https://github.com/YanaPIIDXer/QiitaLineBot
試行錯誤の経緯とかはここのコミットログを見れば大体分かる・・・かも。
ちなみに今回ブランチ分岐は無しの方向。

Golangのホットリロード

「コードを書き換えると自動でビルドが走って、後はブラウザを更新するだけ」と言う機能。

導入

Realizeを使ってGoでホットリロードを実現する
上記記事を参考にRealizeを導入。
・・・が、Realize起動時のビルドは走ってるっぽいがホットリロードは機能しない。

legacyの有効化

どうもRealizeをDocker(コンテナ)上で動作させるには、legacyを有効化する必要がある模様。
ここを参照。
.realize.yamlの記述を

.realize.yaml
settings:
  legacy:
   force: true
(省略)

として再起動したら無事動作。

herokuへのdeploy

ポート

ローカル環境で立ち上げている時はポート3000を使用しているが、heroku上で動作させる場合はherokuが指定したポートで処理をしなければならない。

プログラム側の対応

環境変数「PORT」に値が入っていればheroku上での動作としてそのポートを、入っていなければローカルでの動作としてポート3000を利用するようにしてみよう。

Dockerfile
# 以下の記述を追加。
ENV PORT=${PORT}
main.go
// サーバ起動部分。
var port = os.Getenv("PORT")        // heroku上で動かす場合は指定されたPortじゃないと死なますよ。
if port == "" {
    port = "3000"
}
fmt.Println("Server Port:" + port)
router.Run(":" + port)

これでdeployしたら動・・・かなかった。

コンテナ構成の変更

色々調査しながら、ふとdocker-compose.ymlを眺めると・・・

docker-compose.yml
 (省略)
    ports:
      - "3000:3000"
    command: realize start --name='server' --run
 (省略)

・・・

docker-compose.yml
    ports:
      - "3000:3000"

お 前 か 。
ここもherokuが使ってるポートにしてやらないと動かないんじゃないかな・・・?

色々調べて、以下の記事がヒット。
golang,docker,mysqlの環境をherokuにデプロイする

この記事にある通りにコンテナ構成を変更。(Realizeはローカルでのみ動作させるように。)
上記の記事ではdocker-compose.ymlをdockersディレクトリ内に配置しているが、同じようにすると動作せず。
多分どこかでパスに問題が出てるんじゃないかな・・・?未調査だけど。
面倒なのでルートディレクトリに別名(docker-compose-local.yml)で放り込むと動作するようになった。

herokuにdeployして、ログを見た感じエラーとかも無さそう。
いざアクセス・・・が、動作せず。
SSL絡みかと思ったが、普通にhttpでアクセスしても同じ結果だった。

No web processes running

ログをみた所、「No web processes running」と言うエラーメッセージが。
これを調べた所、以下の記事に行き着いた。
【Heroku】デプロイ後にcode=H14 desc="No web processes running"

記事にある通り、heroku ps:scale web=1を叩いたところ、動作はしなかったもののエラーメッセージが変わった。
(記事では「このコマンドを叩いただけでは動作せず、heroku.ymlが必要」との事だったが、自分はRealizeをローカルでのみ動作させるようにする過程で作成済みなのでこのコマンドだけでOKだった。)

App crashed

何かクラッシュしたらしい。
このエラーメッセージで調べたところ、以下の記事がヒット。
HerokuでApplicationErrorが発生したときの対処法

この記事に書かれていたコマンドで再起動。しかし変わらず。

そこからログを少し遡ると、.env Load Failed.
「・・・これってさ、.envの読み込み失敗時にそのままエラーとしてreturnしてた時のログだよね?herokuへのdeploy作業の過程でそのエラー処理は撤去したんだけど・・・」
「何かしらの原因で過去のやつが動作しているのでは?」と言う疑いが。

再度deployしてアクセス→router.Runメソッドが走るところまでは確認したがApp crashは解消されず→再度上記記事のコマンドで再起動→ .env Load Failed.

気になる事としては、「herokuへのdeploy時、releaseから処理が返ってこない」と言う事。(毎回Ctrl+Cしてる。)
「コマンドがフォアグラウンドで走ってるとか・・・?」とか考えて、適当にENTRYPOINTをlsに変えてみても結果は同じ。
で、ENTRYPOINTがlsのものが動いてる(・・・と思い込んでいる)状態で heroku logs
普通にGoのプログラムが動いていた。
・・・まぁ、「処理が返ってこない」と言いながらCtrl+Cで強制的に抜けてるんだからそりゃそうだわな。

アプリケーションの作り直し

とりあえず heroku destroy で一旦破棄。
heroku create で作り直す。
再度pushしてアクセスすると無事動作。
app crashedの原因はよく分からないが、一旦作り直してみるのも手ではある。(今回は「過去のプログラムが動作した」と言う事から、「ゴミが残っている」と言う事が明白だったので。)

ちなみにここでdeploy方法を git push heroku master から heroku container:push webheroku container:release web にも変更している。
(自分はdocker-composeを使ったビルドしかした事が無く、docker-composeを使わずにコンテナを単体で動かす時にどうやってコンテナ名を付けるのかが分からなかった為。。。これもここで覚えた。)

最後に

「GolangをDockerでインストールし、heroku上で動作させる」まででハマったところを色々まとめてみた。
冒頭に書いたリポジトリでのアプリケーション開発活動自体はまだまだ継続するものの、今回の表題である「Golang+Docker+heroku」での動作までは進み、「後はGolang内で色々と実装するだけ」と言う段階に至ったので、ひとまずこの記事は完結とする。
(それに伴い、余計な記述も撤去。)

ここまで来るのに数日を要したが、その分多くのことをインプットできたように思える。
そこでブチ当たった様々な問題とその解決策をなるべくリアルタイムで更新してきた。
「誰かに伝える事」は正直苦手だから色々と拙い所はあると思うが、こういった記事でも役に立ってくれれば幸いである。

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

fluentdでdockerコンテナのログをファイルごとに出し分けてみたお話

fluentdとは

fluentdとはログ収集ツールです。
様々な形式のinputからログを収集でき、それを必要に応じて整形・加工してログ出力できます。

環境

  • OS:debian 10.3
  • docker:19.03.11
  • docker-compose:1.16.1
  • fluentd:1.3.2

今回の目標

今回の目標はサーバーのログを標準出力からファイル出力へ切り替えることです。
さらに、ファイル出力も全てのログを出力するログファイルと、エラーログのみを出力するエラーログファイルの2種類作成していきます。

fluentdの設定ファイルについて

デフォルトでは、 /fluentd/etc/fluent.conf が読み込まれます。

tag

fluentdではログをtagというもので管理していきます。
このtagを使って設定を任意のログのみに適用できたりします。

ディレクティブ

fluentdの設定はディレクティブと呼ばれるもので設定していきます。
ディレクティブには下記のようなものがあります。

  • source: ログの入力を設定するディレクティブ。
  • match: ログの出力を設定するディレクティブ。引数にtagを指定。
  • filter: ログに対して行う処理を設定するディレクティブ。引数にtagを指定。
  • label: @label でラベル名を設定することで、<label ラベル名> の設定にルーティングできる。

下記はmatchディレクティブの例です。

<match myapp.access>
  @type file
  path /var/log/fluent/access
</match>

myapp.access の部分がtagになります。
@type の部分が次に説明する、プラグインを指定しています。

設定ファイルの主なディレクティブの流れとしては、
< source > → < filter > や < label >など → ...... → < match >
となることを頭に入れておくと設定ファイルの読み書きがしやすくなると思います。
参考:ディレクティブ 公式ドキュメント

プラグイン

fluentdには標準のプラグインが豊富にあります。
プラグインは先ほどのディレクティブ内に @type で指定することで利用できます。
このプラグインを使うことで様々な形式でログを入出力できたり、ログを加工したりできるようになります。

下記は filterディレクティブ のプラグインの一部です。

  • record_transformer:ログにフィールドを追加したり削除したりできる。
  • grep:正規表現でマッチするログだけを通したり、マッチするログを出力しないようにしたりできる。
  • parser:ログ内のフィールドをjsonなどの形式を指定してパースすることができる。

参考:filterプラグイン 公式ドキュメント

今回使う各ファイル

ファイル階層

├── app
│   └── main.go
├── docker
│   ├── app
│   │   ├── app
│   │   └── Dockerfile
│   ├── docker-compose.yml
│   └── fluentd
│       ├── config
│       │   └── fluent.conf
│       ├── Dockerfile
│       └── log
│           ├── alert.buf
│           └── log.buf
└── Makefile

app/main.go

今回はこちらの超簡易サーバーを使っていきたいと思います。

main.go
package main

import (
    "fmt"
    "net/http"

    "github.com/sirupsen/logrus"
)

func main() {
    // server起動
    http.HandleFunc("/sample", outputQueryString)
    http.ListenAndServe(":8080", nil)
}

func outputQueryString(w http.ResponseWriter, r *http.Request) {
    log := logrus.New()
    // 処理開始のログ
    log.Infoln("Start!")

    // クエリストリングからキーが name の値を取得
    qs := r.URL.Query().Get("name")
    // キーが name の値がない場合のエラー処理
    if qs == "" {
        log.Errorln("Query string is nothing.")
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    // name があった場合の出力
    fmt.Fprintf(w, "Your name is %s", qs)
    log.Infoln("Complete!")
}

このサーバーのログをfluentdで収集します。
こちらのサーバーは、

  1. (IPアドレス):8080/sample?name=XXX にアクセスすると、「Your name is XXX」を返す
  2. (IPアドレス):8080/sample?key=XXX のようにクエリストリングのキーに name の値がない場合、エラーログを出力し、400エラーを返す

という処理内容になってます。

出力されるログは、

  1. アクセスされたときに処理開始のログ Start! を出力
  2. key が name のクエリストリングがなかった場合、エラーログ Query string is nothing. を出力
  3. 処理が正常終了した場合、処理完了のログ Complete! を出力

となっています。
なので、ログファイルには1, 2, 3のログが、エラーログファイルには2のログのみが出力されることになります。

docker/app/app

app/main.go の実行ファイル。
こちらをappのコンテナ内にコピーして実行します。

docker/app/Dockerfile

Dockerfile
FROM alpine:latest

COPY app /bin

CMD "app"

今回、appのdockerイメージはalpineを使用しています。
alpineを選択した理由はdockerイメージの容量が小さくなるからです。
詳しくはこちらの記事を参考にしてみてください。
Alpine Linux で Docker イメージを劇的に小さくする

Dockerfileの内容としては、先ほどのappの実行ファイルをコンテナ内の /bin ディレクトリにコピーして実行しているだけです。

docker/fluentd/config/fluent.conf (fluentd設定ファイル)

いよいよ、本命のfluentdの設定ファイルです。
ちょっと長いかもしれませんが、ブラウザバックするのは待ってください。
わかります。わたしも設定ファイルアレルギーの重症者ですから。
ですが、安心してください。
部分に分けて説明をしているので、どうか我慢して少し読み進めてみてください。
わからないところがあればコメントいただければできるだけ(アレルギー重症者レベルで)回答します。

fluent.conf
<source>
  @type  forward
  @label @mainstream
  port  24224
</source>

<label @mainstream>
  <match docker.**>
    @type copy
    <store>
      @type relabel
      @label @all_log
    </store>
    <store>
      @type relabel
      @label @err_log
    </store>
  </match>
</label>

<label @all_log>
  <match **>
    @type file
    path /var/log/fluent/log_*.log
    format ltsv
    buffer_type file
    buffer_path /var/log/fluent/log.buf
    symlink_path /var/log/fluent/log_current
    time_slice_format %Y%m%d
    flush_at_shutdown true
    append true
  </match>
</label>

<label @err_log>
  <filter **>
    @type grep
    <regexp>
      key log
      pattern /level=(warn|error)/
    </regexp>
  </filter>

  <match **>
    @type file
    path /var/log/fluent/alert_*.log
    format ltsv
    buffer_type file
    buffer_path /var/log/fluent/alert.buf
    symlink_path /var/log/fluent/alert_current
    time_slice_format %Y%m%d
    flush_at_shutdown true
    append true
  </match>
</label>

設定ファイルの内容説明

設定ファイルの内容を上から分けて見ていきます。

<source>
  @type  forward
  @label @mainstream
  port  24224
</source>

この sourceディレクティブでは、inputの設定を定義しています。
取得したログに対して @mainstream というラベルを設定しているので、この次は <label @mainstream> の設定に飛びます。

<label @mainstream>
  <match docker.**>
    @type copy
    <store>
      @type relabel
      @label @all_log
    </store>
    <store>
      @type relabel
      @label @err_log
    </store>
  </match>
</label>

この <label @mainstream> では、ログの複製を行っています。
@type copy でログの複製ができます。
なぜ複製しているのかというと、この後ログを、全てのログが出力されるログファイルと、エラーログだけが出力されるエラーログファイルに分けたいからです。
複製したログには @all_log@err_log というラベルを再設定しているので、
それぞれ <label @all_log><label @err_log> のディレクティブに飛びます。

<label @all_log>
  <match **>
    @type file
    path /var/log/fluent/log_*.log
    format ltsv
    buffer_type file
    buffer_path /var/log/fluent/log.buf
    symlink_path /var/log/fluent/log_current
    time_slice_format %Y%m%d
    flush_at_shutdown true
    append true
  </match>
</label>

この <label @all_log> では、ログファイルの出力設定を行っています。

  • path ではファイルの出力先を指定しています。* は後述の time_slice_format で指定したフォーマットで日付や日時が入ります。
  • buffer_type ではファイルに出力する前のバッファリングをfileに出力するか、メモリに溜めておくかを設定できます。メモリの方が処理は速いですが、fluentdが途中で落ちるとログが消失する可能性があります。
  • buffer_path ではバッファをfile出力にした際の出力先を指定しています。
  • symlink_path ではファイル出力しているバッファへのシンボリックリンクを設定しています。これにより、バッファが出力されているファイル名がどんなものであれ、同じファイル名でアクセスできるようになります。
  • time_slice_format ここで設定したフォーマットでファイル名の * が置き換えられます。デフォルトでは %Y%m%d なので日付単位でファイルが生成されますが、ここの設定を時間単位にすればファイルが1時間ごとに生成されるようになるので、ファイルの生成単位を設定しているとも言えます。
  • flush_at_shutdown では、fluentdがシャットダウンされた際に最後に1度だけバッファの内容を出力するかどうかを設定しています。trueなら出力を試します。
  • append では、既にファイル出力先ファイルがある場合、追記するか別ファイルに出力するかを設定しています。trueなら追記します。
<label @err_log>
  <filter **>
    @type grep
    <regexp>
      key log
      pattern /level=(warn|error)/
    </regexp>
  </filter>

  <match **>
    @type file
    path /var/log/fluent/alert_*.log
    format ltsv
    buffer_type file
    buffer_path /var/log/fluent/alert.buf
    symlink_path /var/log/fluent/alert_current
    time_slice_format %Y%m%d
    flush_at_shutdown true
    append true
  </match>
</label>

この <label @err_log> では、エラーログファイルの出力設定を行っています。
filterディレクティブでは、grepプラグインを使い、ログのフィルタリングを行っています。
grepプラグインでは、フィールドのキーを設定して、それに対して正規表現で通すログ通さないログを設定できます。
この設定では、 log というキーに対して level=warnlevel=error という値を持つログのみを通します。
matchディレクティブについては、上記ログファイル出力設定と重複するので割愛します。

以上が、fluent.confの設定内容です。
labelのおかげで流れを掴みやすく、他の設定ファイルより読みやすいのではと思います。

fluentdのDockerfile

Dockerfile
FROM fluent/fluentd:latest

dockerイメージで、fluentdを指定しているだけです。

アプリのcompose.yml

docker-compose.yml
version: "3.3"

services:
  app:
    build: ./app
    container_name: app
    restart: always
    ports:
      - "8080:8080"
    logging:
      driver: "fluentd"
      options:
        fluentd-address: "localhost:24224"
        tag: "docker.{{.Name}}"
    depends_on:
      - fluentd

  fluentd:
    build: ./fluentd
    container_name: fluentd
    volumes:
      - ./fluentd/config:/fluentd/etc
      - ./fluentd/log:/var/log/fluent
    restart: always
    ports:
      - "24224:24224"

appの logging というところでログ出力先の設定を行っています。
こちらのように driver に fluentd を指定することで、fluentdでログを収集できるようになります。
optionsではfluentdのコンテナへの接続とログのtagの設定をしています。

fluentdのvolumesでは、2つのディレクトリをマウントしています。
1つめの ./fluentd/config では、デフォルトのfluent.confが格納してあるディレクトリをマウントすることで、設定ファイルを上書きしています。
2つめの ./fluentd/log では、fluentdのログファイルが出力されている /var/log/fluent をマウントすることで、ログファイルをローカルの /docker/fluentd/log のディレクトリから取得できるようにしています。

実際に起動してみる

それでは、実際に起動してみます。

$cd docker/; docker-compose up -d --build
Creating network "docker_default" with the default driver
Building fluentd
Step 1/1 : FROM fluent/fluentd:latest
 ---> 9406ff63f205

Successfully built 9406ff63f205
Successfully tagged docker_fluentd:latest
Building app
Step 1/3 : FROM alpine:latest
 ---> a24bb4013296
Step 2/3 : COPY app /bin
 ---> a57975cbc6b8
Step 3/3 : CMD "app"
 ---> Running in fa1f4446976e
Removing intermediate container fa1f4446976e
 ---> 515558e9745f

Successfully built 515558e9745f
Successfully tagged docker_app:latest
Creating fluentd ...
Creating fluentd ... done
Creating app ...
Creating app ... done

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                                NAMES
b76f4a1c2bdd        docker_app          "/bin/sh -c \"app\""     57 seconds ago      Up 56 seconds       0.0.0.0:8080->8080/tcp               app
b732ea83c8e7        docker_fluentd      "/bin/entrypoint.sh …"   58 seconds ago      Up 57 seconds       5140/tcp, 0.0.0.0:24224->24224/tcp   fluentd

fluentdとサーバーのコンテナが起動できました。
では、アクセスしてみましょう。
まずは、 (IPアドレス):8080/sample?name=Qiita でアクセスしてみます。
image.png
クエリストリングで指定した Qiita がちゃんと表示されてますね。

続いて、 (IPアドレス):8080/sample?key=Qiita でアクセスしてみます。
image.png

こちらもちゃんとエラーが返されてます。サーバーは意図通り動いてそうですね。

続いて、ログについて見ていきます。
log_yyyymmdd.log には全てのログが出力されます。
alert_yyyymmdd.log にはエラーログのみが出力されます。
実施日が2020/06/29なので yyyymmdd は 20200629 になります。

log_20200629.log
source:stderr   log:time="2020-06-29T14:18:06Z" level=info msg="Start!" container_id:1b3e5a8deaaa2322bec250db7a47d67f49dd5654bc037af1730e31db1c4af74d   container_name:/app
container_id:1b3e5a8deaaa2322bec250db7a47d67f49dd5654bc037af1730e31db1c4af74d   container_name:/app source:stderr   log:time="2020-06-29T14:18:06Z" level=info msg="Complete!"
source:stderr   log:time="2020-06-29T14:18:09Z" level=info msg="Start!" container_id:1b3e5a8deaaa2322bec250db7a47d67f49dd5654bc037af1730e31db1c4af74d   container_name:/app
container_id:1b3e5a8deaaa2322bec250db7a47d67f49dd5654bc037af1730e31db1c4af74d   container_name:/app source:stderr   log:time="2020-06-29T14:18:09Z" level=error msg="Query string is nothing."
alert_20200629.log
container_id:1b3e5a8deaaa2322bec250db7a47d67f49dd5654bc037af1730e31db1c4af74d   container_name:/app source:stderr   log:time="2020-06-29T14:18:09Z" level=error msg="Query string is nothing."

log_20200629.log を見てみるとの1行目と3行目には処理開始のログが出力され、2行目と4行目にはそれぞれ正常終了時のログとエラー時のログがすべて出力されていますね。
一方、alert_20200629.log の方には log_20200629.log の4行目と同じログ(エラーログ)が出力されています。
これでエラー時のログのみ見たい場合は alert_20200629.log を見ればよくなりましたね。

おわりに

今回はfluentdの設定ファイルの概要と実際に使ってみた実例を紹介しました。
ここまで読んでいただいた方には、fluentdの設定ファイルがそこまで難解ではないというのが少しは伝わったのではないでしょうか?
設定ファイルアレルギーの重症者であるわたしでも簡単な設定ファイルについては記述できるようになりました。
みなさんもぜひこの記事をきっかけにfluentdデビューをしてスマートなログ収集ライフを!

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

docker コマンド集

Use Docker command line client

docker コンテナ実行

docker run [image]
image = docker hubなどにあるもの
e.g.) hello-world

docker コンテナ実行(インタラクティブモード)

docker run -t -i [image] [cmd]
e.g.) docker run -t -i ubuntu /bin/bash

docker コンテナ実行(background + Port開放 + name割当)

docker run -d -P --name hello-world-app training/webapp python app.py
---
-dはdeamon(background)
-Pはネットワークポートの開放(自動割当)
--nameは名前の割当
---
docker  ps  で確認されて割り当てられたポートで内部からアクセスできる
e.g.)curl http://localhost:<port>/

docker コンテナ実行(background + Port開放 + name割当)

docker run -d -p 80:5000 --name webapp training/webapp python app.py
---
-pオプションはポート番号を手動で割り振れる
---
docker  ps  で確認されて割り当てられたポートで内部からアクセスできる
e.g.)curl http://localhost:80/
実はこれでもう80番ポートを開放しているので外部からアクセスできる(httpは80番なので、80を省略)
e.g.)http://[このサーバーのグローバルIP]/ ←これでブラウザからアクセス可能

docker コンテナ停止

 docker stop -t 0 [name]
 nameはimage_nameではなく、docker_psでみれるname
 -t は停止までの時間。指定しなければ10, 0だと即時終了

docker コンテナ実行中確認

docker ps

docker コンテナ一覧確認(未実行含む)

docker ps -a

docker image 削除

docker rm [image_layer_name] 
image_layer_name = docker ps -a 出てくるもの

docker image 取得

docker image pull [image_name]
e.g.) docker image pull ubuntu

docker image 取得(特定のver)

docker image pull [image_name]:[version]
e.g.) docker image pull ubuntu:14.04

docker image一覧

docker image ls

docker image 調査

docker inspect [name]

docker image 調査(フィルター)

docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' [name]

docker exec(既に起動しているコンテナにコマンド実行)

docker exec [name] ps aux
これだと ps auxを実行させる

docker exec(既に起動しているコンテナにコマンド実行をインタラクティブモードで)

docker exec -it [name] bash
root@249476631f7d:/opt/webapp# ps aux

docker ファイルコピー

docker cp [origin] [copy]
e.g.) docker cp webapp:/opt/webapp/app.py .
この場合、webappのコンテナの中にあるapp.pyをlocalにコピー
---
e.g.)docker cp app.py webapp:/opt/webapp/
この場合、localにあるapp.pyをwebappのコンテナの指定の場所にコピー

その他

Persisted Volume

accessModes
PVのアクセスモードを指定する。アクセスモードは現在以下の3種類。

ReadWriteOnce : 単一Nodeで読み書きが可能
ReadOnlyMany : 複数Nodeで読み込みが可能
ReadWriteMany : 複数Nodeから読み書きが可能
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む