textlintで文章の校正を行う

文章を書く時、文法的な誤りや用語の揺らぎは避けたいものです。しかし、自分だけでは気づかないところがあったり、煩雑すぎたりする場合があります。そこで、textlintというツールを使えば、文章の校正を行うことができます。本記事では、textlintについて詳しく紹介し、文章の校正に役立てる方法を解説します。

今回試したコードはhttps://github.com/t0k0sh1/emendationにあります。

textlintがチェック対象としているのは、テキストファイルやマークダウンファイルなどのテキストファイルのみです。WordやExcelなどのファイルを処理する場合はテキストを抽出するなどの工夫をする必要があります。

textlintの導入

自然言語向けのLinterであるtextlintを導入します。

ここでは、校正チェックを行うプロジェクトemendationを作成します。textlintはNode.jsで利用可能はパッケージですので、npm initコマンドで初期化しておきます。

$ mkdir emendation
$ cd emendation
$ npm init -y

textlintのインストール

textlintをインストールします。特段前提条件となるパッケージやライブラリはなく、普通にインストール可能です。

$ npm install textlint

ルールプリセットのインストール

次にルールプリセットをインストールします。技術書や技術系の記事、設計書の執筆を行っている場合は、技術書向けのルールを使用するのがよいでしょう。

$ npm install textlint-rule-preset-ja-technical-writing

lintスクリプトの設定

package.jsonlintを実行できるスクリプトを追加します。filesディレクトリ内にあるテキストファイルを対象にするようにしています。

  "scripts": {
    "lint": "textlint ./files/*.txt"
  },

ルールの適用方法

textlintで使用するルールを適用する方法には、以下の2つの方法があります。

  1. textlintコマンドに—presetオプションで使いたいルールを指定する
  2. .textlintrc.jsonに適用するルールを記述する

実用上は細かいカスタマイズを必要とするため、後者の方法でルールを適用するようにします。

まずは、textlint --initコマンドで.textlintrc.jsonファイルを作成します。

$ textlint --init
.textlintrc.json is created.

コマンドを実行すると以下のようなファイルが生成されます。

{
  "plugins": {},
  "filters": {},
  "rules": {}
}

生成時点ではルールは何も設定されていません。先ほどインストールしたルールプリセットを適用する記述を追加します。

{
  "plugins": {},
  "filters": {},
  "rules": {
    "preset-ja-technical-writing": true
  }
}

動作を確認するために、filesディレクトリに以下のようなsample.txtを作成します。

Vueで作成した確認ダイアログを表示し、OKボタンをクリックしたときだけ処理を続行する方法をボタンクリックの場合とフォームを使ったサブミットの2パターンの実装方法を見ていきました
この考え方は他のフレームワークでも適用可能です。今回はVue 3.3.4で動作確認していますが、Vue 3で実装された機能を使っていないため、Vue 2でも動作すると思います。

このファイルは1行目の「。」が漏れているので、この点が検出されると思います。

では、実際に実行してみます。

$ npm run lint

> emendation@1.0.0 lint
> textlint ./files/*.txt


/Users/t0k0sh1/Workspace/emendation/files/sample.txt
  1:89  error  文末が"。"で終わっていません。           ja-technical-writing/ja-no-mixed-period
  2:84  error  弱い表現: "思います" が使われています。  ja-technical-writing/ja-no-weak-phrase

✖ 2 problems (2 errors, 0 warnings)

2つのエラーが検出されました。

1つは想定どおり「。」が漏れていることを検出していますが、もう1つは想定していなかったエラーです。このエラーは「〜だと思う」という弱い言い回しを検出しているようです。

ルールのカスタマイズ①不要なルールの抑止

不要なルールの抑止の説明を行うためにja-no-weak-phraseのルールを無効化してみます。以下のように記述することでルールを無効化できます。

{
  "plugins": {},
  "filters": {},
  "rules": {
    "preset-ja-technical-writing": {
      "ja-no-weak-phrase": false
    }
  }
}

では、再度実行してルールが抑止されていることを確認します。

$ npm run lint

> emendation@1.0.0 lint
> textlint ./files/*.txt


/Users/t0k0sh1/Workspace/emendation/files/sample.txt
  1:89  error  文末が"。"で終わっていません。  ja-technical-writing/ja-no-mixed-period

✖ 1 problem (1 error, 0 warnings)

先ほどまで検知されていたja-no-weak-phraseのルールが検知されなくなっていることが確認できました。

ルールのカスタマイズ②独自の辞書ルールの追加

辞書ルールを定義するファイルを作成することで、独自の辞書ルールを追加することが可能です。

ファイル名に制約はないですが、ここではprh.yamlファイルを作成します。

まずはルールを使用するように定義を修正します。

{
  "plugins": {},
  "filters": {},
  "rules": {
    "preset-ja-technical-writing": {
      "ja-no-weak-phrase": false
    },
    "prh": {
      "rulePaths": ["./prh.yml"]
    }
  }
}

次に辞書ルールを定義します。

version: 1
rules:
  - expected: View
    patterns:
      - Vue

ここでは動作を確認するために、ViewVueとタイポする想定でルールを作成しています。expectedに正しい書き方、patternsに誤った書き方を記述します。

$ npm run lint

> emendation@1.0.0 lint
> textlint ./files/*.txt


/Users/t0k0sh1/Workspace/emendation/files/sample.txt
  1:1   ✓ error  Vue => View                     prh
  1:89  error    文末が"。"で終わっていません。  ja-technical-writing/ja-no-mixed-period
  2:28  ✓ error  Vue => View                     prh
  2:49  ✓ error  Vue => View                     prh
  2:72  ✓ error  Vue => View                     prh

✖ 5 problems (5 errors, 0 warnings)
✓ 4 fixable problems.
Try to run: $ textlint --fix [file]

Vueがエラーとして検出できることを確認しました。

辞書ルールを定義することで、固有名詞の揺らぎや言い回しの不統一などを検知することが可能になります。

辞書ルール定義の発展した方法

辞書ルールが増えたときに管理が煩雑になりがちです。これを解消するために辞書ルールを分割することを考えます。

以下のように記述することで、ルールを外出しすることが可能です。

version: 1

imports:
  - ./rules/すでに.yml
  - ./rules/かつ.yml
  - ./rules/また.yml

さらに各ルールを指定パスに定義しておきます。

version: 1
rules:
  - expected: すでに
    patterns:
      - 既に
version: 1
rules:
  - expected: かつ
    patterns:
      - /且つ|且/
version: 1
rules:
  - expected: また$1
    patterns:
      - /又(、|は|は、)/
specs:
  - from: 又
  - to: また

この例は、副詞をひらく書き方を正として、ひらいていない書き方を検出するルールとなります。

以下のように例文を用意して動作確認します。

既に会議は始まっており、私たちは遅れている。且つ、プレゼンテーション資料もまだ完成していない。又は、次の会議までに資料を仕上げ、改めて説明することも一つの選択肢だ。

以下が実行結果です。

$ npm run lint

> emendation@1.0.0 lint
> textlint ./files/*.txt


/Users/t0k0sh1/Workspace/emendation/files/sample.txt
  1:1   ✓ error  既に => すでに                                                                                                  prh
  1:23  ✓ error  且つ => かつ                                                                                                      prh
  1:48  ✓ error  又は => または                                                                                                  prh
  1:75  ✓ error  一つ => 1つ
数量を表現し、数を数えられるものは算用数字を使用します。任意の数に置き換えても通用する語句がこれに該当します。  ja-technical-writing/arabic-kanji-numbers

✖ 4 problems (4 errors, 0 warnings)
✓ 4 fixable problems.
Try to run: $ textlint --fix [file]

辞書ルールで定義した箇所が検出されることが確認できます。

自動訂正を行う

textlintにはルールによって自動訂正を行うことができます。–fixオプションを使うことで、訂正可能なルールについて自動的に訂正を行うことが可能です。

package.jsonlint:fixスクリプトを追加します。

  "scripts": {
    "lint": "textlint ./files/*.txt",
    "lint:fix": "textlint --fix ./files/*.txt"
  },

先ほどチェックしたテキストに対して、lint:fixコマンドを実行することで、自動訂正されることを確認します。

まずは、修正前のテキストを再掲します。

既に会議は始まっており、私たちは遅れている。且つ、プレゼンテーション資料もまだ完成していない。又は、次の会議までに資料を仕上げ、改めて説明することも一つの選択肢だ。

再度、チェックのみを行います。

$ npm run lint

> emendation@1.0.0 lint
> textlint ./files/*.txt


/Users/t0k0sh1/Workspace/emendation/files/sample.txt
  1:1   ✓ error  既に => すでに                                                                                                  prh
  1:23  ✓ error  且つ => かつ                                                                                                      prh
  1:48  ✓ error  又は => または                                                                                                  prh
  1:75  ✓ error  一つ => 1つ
数量を表現し、数を数えられるものは算用数字を使用します。任意の数に置き換えても通用する語句がこれに該当します。  ja-technical-writing/arabic-kanji-numbers

✖ 4 problems (4 errors, 0 warnings)
✓ 4 fixable problems.
Try to run: $ textlint --fix [file]

検出したエラーのうち、チェックマークがついているものが自動訂正可能なものです。できないものもありますが、今回検出したエラーはすべて自動訂正可能です。

では、lint:fixコマンドを実行して自動訂正します。

$ npm run lint:fix

> emendation@1.0.0 lint:fix
> textlint --fix ./files/*.txt


/Users/t0k0sh1/Workspace/emendation/files/sample.txt
  1:75  ✔   一つ => 1つ
数量を表現し、数を数えられるものは算用数字を使用します。任意の数に置き換えても通用する語句がこれに該当します。  ja-technical-writing/arabic-kanji-numbers
  1:1   ✔   既に => すでに                                                                                                  prh
  1:23  ✔   且つ => かつ                                                                                                      prh
  1:48  ✔   又は => または                                                                                                  prh

✔ Fixed 4 problems

メッセージからも訂正されたことが確認できますが、修正後のテキストも確認しておきます。

すでに会議は始まっており、私たちは遅れている。かつ、プレゼンテーション資料もまだ完成していない。または、次の会議までに資料を仕上げ、改めて説明することも1つの選択肢だ。

検知された点が修正されていることが確認できました。

ルールを書くときの注意点(正規表現)

「かつ」が正しい書き方だとした場合、誤った書き方は「且つ」または「且」となります。このような場合、正規表現で長い方を先に書くようにすることで、「且つ」を「かつつ」に訂正しないようにできます。

version: 1
rules:
  - expected: かつ
    patterns:
      - /且つ|且/

また、「又」が誤った書き方で「また」に訂正したい場合、「又」が別の単語で使われていてそちらを訂正しないようにするためには、正規表現で接続詞としての「又」のみを検知しつつ、適切に訂正できるようになります。この例では、正規表現(、|は|は、)部分は$1で充当されるため、「又は」は「または」に訂正されるようになります。

version: 1
rules:
  - expected: また$1
    patterns:
      - /又(、|は|は、)/

正規表現の設定ミスなどにより思わぬ訂正が行われる可能性もあります。必ずチェックで正しく検出できているかを確認のうえ、訂正するようにしてください。

まとめ

textlintは、文章の自動校正ツールとして非常に優れています。自己チェックと併用してtextlintを使うことで、より良い文章を効率的に作成することができるようになるでしょう。ぜひ、textlintを使ってみてください。

Storybookを使ってモックアップを作成する

プロジェクト開発において、高品質なモックアップを作成することは非常に重要です。しかし、モックアップ作成プロセスはしばしば面倒で、費用がかかります。ReactなどのUIフレームワークのデザインガイドライン作成ツールとしてよく知られているStorybookを使うことで、手軽にモックアップを作成できます。この記事では、どのようにStorybookがモックアップ作成に役立つかについて紹介します。

Storybookのモックアッププロジェクトを作成する

Storybookを使ったモックアッププロジェクトを作成します。

今回は、UIフレームワークを選定していない状況で、とりあえずBootstrapとjQueryを使ってモックアップを作成する状況を想定していますので、--typeオプションでhtmlを指定します。

$ storybook-mockup
$ cd storybook-mockup
$ npm init -y
$ npx storybook@latest init --type html

本記事では、storybook-mockupというプロジェクト名で進めますが、実際に試す際はプロジェクトに合わせて命名してください。

最後のコマンドを実行すると、Storybookが自動的に起動します。

Storybookを停止したい場合はターミナルでCtrl+Cを入力して停止してください。これ以降、Storybookを実行したい場合は、package.jsonがあるディレクトリで以下のコマンドを実行してください。

$ npm run storybook

storiesディレクトリを整理する

この手順は必ずしも必要ではありませんが、モックアップ作成作業がやりやすくなるようにstoriesディレクトリを整理しておきます。

どのように整理するのかはプロジェクトの方針やメンバーの好みによって変わりますが、物理ディレクトリの構造とStorybookのサイドメニューの構造が一致している方が探しやすくなると思います。

作成直後の状態では、EXAMPLEというサイドメニュー表示されていますが、ボタンやドロップダウンリストなどのパーツを「コンポーネント(components)」、モックアップを「ページ(pages)」に分けてファイルを整理し、サイドメニューも同様に分けるようにします。

また、Storybookでは1コンポーネント1ファイルにはなりません。このまま、複数のコンポーネントやページを同じディレクトリに並べておくと見にくくなるので、コンポーネント名でディレクトリを作成して、ファイルをまとめることにしておきます。

ご使用のIDEやエディタの機能によって、ファイル移動に伴ってimport文が自動的に修正されますが、操作によっては適切に修正されない場合があります。その場合はimport文を直接修正してください。

Storybookのサイドメニューはstoriesディレクトリの構造に合わせて作成しているわけではなく、.Stories.jsファイル内に定義されているtitle属性の値で構造が決まります。

Button.stories.jsHeader.stories.jstitle属性のExampleComponentsに、Page.stories.jstitle属性のExamplePagesに修正しておきます。

すべてのコードを示すとかなりの量になるので、修正した箇所だけ説明します。コード全体を確認したい場合は、https://github.com/t0k0sh1/storybook-mockupを参照してください。

import { createButton } from "./Button";

// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
export default {
  title: "Components/Button",
  tags: ["autodocs"],
  render: ({ label, ...args }) => {

title属性を"Example/Button"から"Components/Button"に修正しています。

import "./header.css";
import { createButton } from "../Button/Button";

Header.jsからButton.jsを参照していますが、そのimport文のパスをディレクトリ構造に合わせて修正しています。

import { createHeader } from "./Header";

export default {
  title: "Components/Header",
  // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
  tags: ["autodocs"],
  render: (args) => createHeader(args),

title属性を"Example/Header"から"Components/Header"に修正しています。

import "./page.css";
import { createHeader } from "../../components/Header/Header";

Page.jsからHeader.jsを参照していますが、そのimport文のパスをディレクトリ構造に合わせて修正しています。

import { expect, userEvent, within } from "@storybook/test";
import { createPage } from "./Page";

export default {
  title: "Pages/Page",
  render: () => createPage(),

title属性を"Example/Page"から"Pages/Page"に修正しています。

StorybookにBootstrapとjQueryを導入する

StorybookにBootstrapとjQueryを導入していきます。導入方法にはいくつかの方法がありますが、ここではCDNを使って手軽に導入する方法を解説します。

Bootstrapを使用できるようにする

StorybookでCDNを使用する場合、.storybook/preview-head.htmlを新規作成し、そこにCDNにアクセスlinkタグを記述します。

<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
  integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
  crossorigin="anonymous"
/>

jQueryを使用できるようにする

こちらも同様に.storybook/preview-head.htmlにCDNにアクセスするscriptタグを記述します。

<script
  src="https://code.jquery.com/jquery-3.7.1.min.js"
  integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
  crossorigin="anonymous"
></script>

これでモックアップ作成の準備が整いました。

ButtonコンポーネントをBootstrapベースに書き換える

最後にすでに存在するButtonコンポーネントをBootstrapベースに書き換えてみましょう。

まずは、現在の実装を確認してみます。

import { createButton } from "./Button";

// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
export default {
  title: "Components/Button",
  tags: ["autodocs"],
  render: ({ label, ...args }) => {
    // You can either use a function to create DOM elements or use a plain html string!
    // return `<div>${label}</div>`;
    return createButton({ label, ...args });
  },
  argTypes: {
    backgroundColor: { control: "color" },
    label: { control: "text" },
    onClick: { action: "onClick" },
    primary: { control: "boolean" },
    size: {
      control: { type: "select" },
      options: ["small", "medium", "large"],
    },
  },
};

// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
export const Primary = {
  args: {
    primary: true,
    label: "Button",
  },
};

export const Secondary = {
  args: {
    label: "Button",
  },
};

export const Large = {
  args: {
    size: "large",
    label: "Button",
  },
};

export const Small = {
  args: {
    size: "small",
    label: "Button",
  },
};

今回注目するのは、primary属性とsize属性です。他の属性もありますが、Button.stories.jsは修正せずにいきます。

import './button.css';

export const createButton = ({
  primary = false,
  size = 'medium',
  backgroundColor,
  label,
  onClick,
}) => {
  const btn = document.createElement('button');
  btn.type = 'button';
  btn.innerText = label;
  btn.addEventListener('click', onClick);

  const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
  btn.className = ['storybook-button', `storybook-button--${size}`, mode].join(' ');

  btn.style.backgroundColor = backgroundColor;

  return btn;
};

属性に応じてclassNameに設定する値を制御します。方針は以下です。

  • primaryが指定されたらbtn-primaryを指定し、指定されなかったらbtn-secondaryを指定する
  • sizesmallならbtn-smを指定し、largeならbtn-lgを指定し、それ以外は何も指定しない

修正後のコードは以下になります。storybook-buttonクラスをbtnに修正してあります。

import "./button.css";

export const createButton = ({
  primary = false,
  size = "medium",
  backgroundColor,
  label,
  onClick,
}) => {
  const btn = document.createElement("button");
  btn.type = "button";
  btn.innerText = label;
  btn.addEventListener("click", onClick);

  const mode = primary ? "btn-primary" : "btn-secondary";
  let btnSize = size === "small" ? "btn-sm" : size === "large" ? "btn-lg" : "";
  btn.className = ["btn", btnSize, mode].join(" ");

  btn.style.backgroundColor = backgroundColor;

  return btn;

表示した結果は以下のようになります。

まとめ

以上のように、Storybookはプロジェクト開発において、手軽かつ効果的なモックアップ作成ツールであることが分かりました。Storybookを使用すると、コンポーネント化やパーツ化が手軽に推進できるので、デザインガイドラインを作成しながらモックアップを作成できます。Storybookを使うことで、モックアップ作成の開発プロセスを改善し、開発者のストレスを軽減することができます。皆さんにも一度、Storybookを使用して、モックアップ作成をスムーズに進めてみてはいかがでしょうか?

pipxでPoetryをインストールする

公式サイトでのPoetryのインストール方法の一つにpipxを使ったインストール方法があって、それが気になったので、pipxを導入してPoetryをインストールしていこうと思います。手順はmacOSの場合になります。

pipxのインストール

Homebrewを使ってpipxをインストールします。

$ brew install pipx
==> Downloading https://formulae.brew.sh/api/formula.jws.json
##O=-#      #
==> Downloading https://formulae.brew.sh/api/cask.jws.json
##O=-#      #
==> Downloading https://ghcr.io/v2/homebrew/core/pipx/manifests/1.3.3
Already downloaded: /Users/t0k0sh1/Library/Caches/Homebrew/downloads/af94290372652b3f23470aa9b2dc3e6cc6f6ac908d786fa0b2ccf9c0f53db957--pipx-1.3.3.bottle_manifest.json
==> Fetching pipx
==> Downloading https://ghcr.io/v2/homebrew/core/pipx/blobs/sha256:31547c41734fa46c13276ada25e3e8548db97281d0c513b9cdcb5268adcc74ff
Already downloaded: /Users/t0k0sh1/Library/Caches/Homebrew/downloads/a4c05e49cf7f84a6647e146f9d68d9983028ea48180d87771a41cdb1f5d27b45--pipx--1.3.3.arm64_sonoma.bottle.tar.gz
==> Pouring pipx--1.3.3.arm64_sonoma.bottle.tar.gz
==> Caveats
zsh completions have been installed to:
  /opt/homebrew/share/zsh/site-functions
==> Summary
🍺  /opt/homebrew/Cellar/pipx/1.3.3: 108 files, 697.7KB
==> Running `brew cleanup pipx`...
Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP.
Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`).

インストール後、pipx ensurepathコマンドを実行します。

$ pipx ensurepath
Success! Added /Users/t0k0sh1/.local/bin to the PATH environment variable.

Consider adding shell completions for pipx. Run 'pipx completions' for instructions.

You will need to open a new terminal or re-login for the PATH changes to take effect.

Otherwise pipx is ready to go! ✨ 🌟 ✨

上記メッセージ中に、

You will need to open a new terminal or re-login for the PATH changes to take effect.

とあるので、新しいターミナルを開くかログインし直す必要があります。今回はこのままインストールを進めたいので、設定ファイルを読み込み直します。

$ exec $SHELL -l

これでインストールは完了です。

activate-global-python-argcompleteのlinkでエラー

Homebrewでインストールする際に以下のエラーに遭遇しました。

Error: The `brew link` step did not complete successfully
The formula built, but is not symlinked into /opt/homebrew
Could not symlink bin/activate-global-python-argcomplete
Target /opt/homebrew/bin/activate-global-python-argcomplete
already exists. You may want to remove it:
  rm '/opt/homebrew/bin/activate-global-python-argcomplete'

To force the link and overwrite all conflicting files:
  brew link --overwrite python-argcomplete

To list all files that would be deleted:
  brew link --overwrite --dry-run python-argcomplete

事象としては、activate-global-python-argcompleteのsymlinkに失敗したようです。解消方法としてはいくつかありますが、ここでは一番最初に書かれている/opt/homebrew/bin/activate-global-python-argcompleteを削除する方法で進めます。

指示どおりに対応してきます。まずは、activate-global-python-argcompleteが存在するかをチェックします。

$ ls /opt/homebrew/bin/activate*
/opt/homebrew/bin/activate-global-python-argcomplete

存在していることを確認できました。これを削除します。

$ rm /opt/homebrew/bin/activate-global-python-argcomplete
$ ls /opt/homebrew/bin/activate*
zsh: no matches found: /opt/homebrew/bin/activate*

削除されたことを確認しました。では、元の手順に戻って、再度pipx installコマンドを実行します。

Poetryをインストールする

pipxを使ってPoetryをインストールします。

$ pipx install poetry
  installed package poetry 1.7.1, installed using Python 3.12.1
  These apps are now globally available
    - poetry
⚠️  Note: '/Users/t0k0sh1/.local/bin' is not on your PATH environment variable. These apps will not be globally accessible until your PATH is updated. Run `pipx ensurepath` to automatically add it, or
    manually modify your PATH in your shell's config file (i.e. ~/.bashrc).
done! ✨ 🌟 ✨

インストール後、コマンドが使えるようになっていることを確認します。

$ poetry -V
Poetry (version 1.7.1)

これでPoetryのインストールは完了しました。

まとめ

pipxのインストールで少しトラブルがありましたが、比較的簡単にインストールを進めることができました。

PYthon製のCLIをインストールする際は、pipxを使っていくのが良さそうです。

Vueで確認ダイアログ(Confirmation Dialog)を実装する

Vueで確認ダイアログを実装する簡単な方法を説明します。

axiosなどを使ってデータを送信する場合と、フォームを使ってデータを送信する場合とで実装が少し異なるため、それぞれのパターンで説明します。

プロジェクトの作成

まずは動作確認を行うプロジェクトを作成します。

$ npm create vue@latest
Vue.js - The Progressive JavaScript Framework

✔ Project name: … vue-confirm-dialog
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit Testing? … No / Yes
✔ Add an End-to-End Testing Solution? › No
✔ Add ESLint for code quality? … No / Yes

Scaffolding project in /Users/t0k0sh1/Workspace/vue-confirm-dialog...

Done. Now run:

  cd vue-confirm-dialog
  npm install
  npm run dev

設定に制約は特にありませんが、以降ではJavaScriptでコードを示します。

確認ダイアログコンポーネント

確認ダイアログコンポーネントを作成します。

<template>
  <div v-if="isVisible" class="dialog">
    <div class="dialog-content">
      <!-- ダイアログの内容 -->
      <button @click="submit" class="button ok">OK</button>
      <button @click="cancel" class="button cancel">キャンセル</button>
    </div>
  </div>
</template>

<style>
.dialog {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background-color: white;
  border: 1px solid #ccc;
  padding: 20px;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  z-index: 1000;
}

.dialog-content {
  text-align: center;
}

.button {
  margin: 10px;
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: background-color 0.3s;
}

.ok {
  background-color: #4caf50;
  color: white;
}

.ok:hover {
  background-color: #45a049;
}

.cancel {
  background-color: #f44336;
  color: white;
}

.cancel:hover {
  background-color: #d73829;
}
</style>

<script>
export default {
  props: ["isVisible"],
  methods: {
    submit() {
      this.$emit("confirm");
    },
    cancel() {
      this.$emit("cancel");
    },
  },
};
</script>

この確認ダイアログは表示/非表示の制御をv-ifで行い、その制御は親コンポーネントに委ねています。また、OKボタンを押したか、キャンセルボタンを押したかを$emitで返すようにしています。

ダイアログの表示/非表示

ダイアログの表示/非表示の制御はv-if="isVisible"で行っています。

<template>
  <div v-if="isVisible" class="dialog">
    ・・・
  </div>
</template>

isVisiblepropsに定義されており、親コンポーネントから子コンポーネントへの単方向データフローによって値が渡されます。

そのため、親コンポーネントで以下のように定義した場合、

<confirm-dialog-component :is-visible="isDialogVisible"></confirm-dialog-component>

親コンポーネントでisDialogVisible = trueとすればダイアログが表示され、isDialogVisible = falseとすればダイアログが非表示になります。propsではキャメル形式になっていますが、コンポーネント使用時に指定する名前はケバブ形式になっている点に注意してください。

OKボタンを押下したときの処理

OKボタンを押下したときの処理について見ていきます。

<template>
  ・・・
      <button @click="submit" class="button ok">OK</button>
  ・・・
</template>

OKボタンには@click="submit"が付与されていて、このボタンをクリックするとmethodsに定義されているsubmit関数を呼び出します。

submit関数では、$emit関数を使って"confirm"というイベントを発生させ、子コンポーネントから親コンポーネントへの単方向データフローによってイベントと必要であればデータを渡します。親コンポーネントがバインドしたコールバック関数を呼び出している感覚に近いイメージになります。

<script>
export default {
  ・・・
  methods: {
    submit() {
      this.$emit("confirm");
    },
    ・・・
  },
}
</script>

confirmイベントが発生すると、親コンポーネントがこのコンポーネントに設定している関数を呼び出します。

例えば、親コンポーネントがこのコンポーネントを以下のように定義している場合、confirmイベントが発生すると、親コンポーネントのhandleConfirm関数が呼び出されます。

<template>
    ・・・
    <confirm-dialog-component @confirm="handleConfirm"></confirm-dialog-component>
    ・・・
</template>

親コンポーネントではOKボタンが押下されたことを検知できるので、サーバーへのデータ送信やダイアログを非表示にする処理を実装します。

<script>
import axios from 'axios';  

export default {
  ・・・
  methods: {
    handleConfirm() {
      axios.post(・・・)
        .then(response => {
          // サーバーへのデータ送信が成功したときの処理
        })
        .catch(error => {
          // サーバーへのデータの送信が失敗したときの処理
        });
      // 確認ダイアログを非表示にする
      this.isDialogVisible = false;
    },
    ・・・
</script>

上記の例では、axiosを使ってサーバーにデータを送信しつつ、確認ダイアログを非表示にしています。

キャンセルボタンを押下したときの処理

同様にキャンセルボタンを押下したときの処理を見てみましょう。

<template>
  ・・・
      <button @click="cancel" class="button cancel">キャンセル</button>
  ・・・
</template>

クリック時に呼び出される関数が異なるだけで基本的にはOKボタン押下時と同じで、methodsに設定しているcancel関数が呼び出されます。

<script>
export default {
  ・・・ 
   cancel() {
      this.$emit("cancel");
    },
    ・・・
  },
}
</script>

キャンセルボタンを押下したときは$emit関数を使って"cancel"というイベントを発生させます。

OKボタンのときと同様に、親コンポーネントでcancelイベントを処理する関数を定義します。

<template>
  ・・・
    <confirm-dialog-component @cancel="handleCancel"></confirm-dialog-component>
  ・・・
</template>

handleCancel関数では単に確認ダイアログを閉じるだけにします。

export default {
  ・・・
  methods: {
    ・・・
    handleCancel() {
      // 確認ダイアログを非表示にする
      this.isDialogVisible = false;
    },
     ・・・
  },
}
</script>

ボタンクリックによる確認ダイアログの表示

axiosなど使ってサーバーにデータを送信するような使い方の場合はこちらが適しています。

前述のとおり、作成した確認ダイアログコンポーネントは以下のように

  • ダイアログ表示・非表示制御用のフラグ(isDialogVisible
  • ダイアログを表示する処理(handleClick
  • OKボタン押下時の処理(handleConfirm
  • キャンセルボタン押下時の処理(handleCancel

を用意することで確認ダイアログを使用することができます。。

<template>
  <input type="button" value="クリック" @click.prevent="handleClick" />
    <confirm-dialog-component
      :is-visible="isDialogVisible"
      @confirm="handleConfirm"
      @cancel="handleCancel"
    ></confirm-dialog-component>
  </form>
</template>

<script>
import ConfirmDialogComponent from "./ConfirmDialogComponent.vue";

export default {
  components: {
    ConfirmDialogComponent,
  },
  data() {
    return {
      isDialogVisible: false,
    };
  },
  methods: {
    handleConfirm() {
      axios.post(・・・)
        .then(response => {
          // サーバーへのデータ送信が成功したときの処理
        })
        .catch(error => {
          // サーバーへのデータの送信が失敗したときの処理
        });
      this.isDialogVisible = false;
    },
    handleCancel() {
      this.isDialogVisible = false;
    },
    handleClick(e) {
      this.isDialogVisible = true;
    },
  },
};
</script>

ボタンクリック時に呼び出されるhandleClick関数はダイアログ表示後にすぐに処理が終了するため、@click.preventをつけることで、preventDefault関数を呼び出したのと同様にクリック処理が発火しないようにしています。

ほとんどのコードはすでに説明済みですが、ダイアログを表示する関数だけ説明していなかったので、コードを示しておきます。といっても確認ダイアログコンポーネントに設定しているisDialogVisibleをtrueに変更してダイアログを表示するだけの処理になります。

export default {
  ・・・
  methods: {
    ・・・
    handleClick(e) {
      this.isDialogVisible = true;
    },
  },
}
</script>

サブミットによる確認ダイアログの表示

サーバーへのデータ送信をフォームで行う場合はこちらが適しています。

前述のとおり、確認ダイアログの表示中は呼び出し元関数の処理をブロックしません。また、サブミットはENTERキー押下でも行えるため、これらに対する対策が必要となります。

<template>
  <form @submit.prevent="handleSubmit">
    <input type="submit" value="サブミット" />
    <confirm-dialog-component
      :is-visible="isDialogVisible"
      @confirm="handleConfirm"
      @cancel="handleCancel"
    ></confirm-dialog-component>
  </form>
</template>

<script>
import ConfirmDialogComponent from "./ConfirmDialogComponent.vue";

export default {
  components: {
    ConfirmDialogComponent,
  },
  data() {
    return {
      isDialogVisible: false,
      resolveDialog: null,
    };
  },
  methods: {
    showDialog() {
      this.isDialogVisible = true;
      return new Promise((resolve, reject) => {
        this.resolveDialog = resolve;
      });
    },
    handleConfirm() {
      if (this.resolveDialog) {
        this.resolveDialog(true);
      }
      this.isDialogVisible = false;
    },
    handleCancel() {
      if (this.resolveDialog) {
        this.resolveDialog(false);
      }
      this.isDialogVisible = false;
    },
    async handleSubmit(e) {
      // show confirm dialog
      const confirmed = await this.showDialog();
      if (confirmed) {
        e.target.submit();
      }
    },
  },
};
</script>

ここではプロミスを使っていますが、プロミスを使わなくても実装は可能です。ただ、少しだけスマートに実装できるようになるため、プロミスを使って実装ています。

ダイアログ表示時にプロミスを作成する

まず、ダイアログを表示するときにプロミスを作成し、それを返します。このとき、resolvedataに保持しておきます。

<script>
import ConfirmDialogComponent from "./ConfirmDialogComponent.vue";

export default {
  components: {
    ConfirmDialogComponent,
  },
  data() {
    return {
      isDialogVisible: false,
      resolveDialog: null,
    };
  },
  methods: {
    showDialog() {
      this.isDialogVisible = true;
      return new Promise((resolve, reject) => {
        this.resolveDialog = resolve;
      });
    },
    ・・・
  },
};
</script>

プロミス内でdataに保存しているresolveはOKボタン、キャンセルボタンが処理結果を返すために使用します。

OKボタンを押下したときの処理

OKボタンを押下するときに呼び出されるhandleConfirm関数では、ダイアログを非表示にする処理以外にプロミス作成時にdataに保存したresolveresolveDialog)にtrueを設定しています。

export default {
  ・・・
  methods: {
    ・・・
    handleConfirm() {
      if (this.resolveDialog) {
        this.resolveDialog(true);
      }
      this.isDialogVisible = false;
    },
     ・・・
  },
}
</script>

また、フォームでデータを送信するため、handleConfirm関数ではサーバーでデータを送信する処理は行いません。

キャンセルボタンを押下したときの処理

同様にキャンセルボタンを押下したときに呼び出されるhandleCancel関数を見ていきます。

export default {
  ・・・
  methods: {
    ・・・
    handleCancel() {
      if (this.resolveDialog) {
        this.resolveDialog(false);
      }
      this.isDialogVisible = false;
    },
     ・・・
  },
}
</script>

違いはresolveresolveDialog)にfalseを設定している点でそれ以外に違いはありません。

ダイアログを開く処理

ダイアログを開く処理を見ていきます。

まず、ダイアログを開く関数はフォームに設定し、このとき使用するのは@submit.preventになります。

<form @submit.prevent="handleSubmit">

サブミットボタンではなく、フォームのsubmitイベントに関数を設定し、.preventをつけてフォームの送信処理を行わないようにする点がポイントとなります。これによりサブミットボタンをクリックしてもENTERキーを押下してもフォームの送信は行われなくなります。そのため、別の手段でフォームの送信を行う必要が出てきます。

次に@submit.preventに設定したhandleSubmit関数を見ていきます。

export default {
  ・・・
  methods: {
    ・・・
    async handleSubmit(e) {
      // show confirm dialog
      const confirmed = await this.showDialog();
      if (confirmed) {
        e.target.submit();
      }
    },
  },
}
</script>

この関数はshowDialog関数が返すプロミスの結果を受け取るためにasync関数として実装します。そして、showDialog関数をawaitをつけて同期化し、ダイアログが閉じるまで(OKボタンまたはキャンセルボタンでresolveを呼び出されるまで)待つようにします。これがダイアログが閉じるまでの間ブロックしない問題に対する対策です。

showDialog関数が返すプロミスの結果はtrue(OKボタン押下時)、false(キャンセルボタン押下時)のいずれかであるため、trueのときにe.target(これはフォームそのもの)のsubmit関数を呼び出すことでフォームの送信を行うことができます。falseが返ってきたときは何も処理をしなければpreventpreventDefault関数と同じ効果)によってフォームの送信は行われません。

前述のとおり、プロミスを使わず、async-awaitを使わずにhandleConfirm関数でフォームのsubmit関数を呼び出すことも可能ですが、フォームを探す一手間があるため、見やすくするためにプロミスとasync-awaitを使っています。

まとめ

Vueで作成した確認ダイアログを表示し、OKボタンをクリックしたときだけ処理を続行する方法をボタンクリックの場合とフォームを使ったサブミットの2パターンの実装方法を見ていきました。

この考え方は他のフレームワークでも適用可能です。今回はVue 3.3.4で動作確認していますが、Vue 3で実装された機能を使っていないため、Vue 2でも動作すると思います。

動作可能なコードははhttps://github.com/t0k0sh1/vue-confirm-dialog.gitにあります。

ab(Apache Bench)の使い方

ストレステストツールの一つであるabの基本的な使い方について解説します。

abのインストール

macOSではデフォルトでインストール済みのため、すぐに使用できます。

Ubuntuの場合は、以下のコマンドでインストールできます。

sudo apt install apache2-utils

基本的な使い方

最も重要な-nオプションと-cオプションについて説明します。

ab -n [全リクエスト数] -c [同時接続数] [URL]
  • -n:総リクエスト数を指定します
  • -c:同時に送信するリクエスト数(同時接続数)を指定します
  • [URL]:テスト対象のURL

-nで指定したリクエスト数を-cで指定した多重度で送信します。-cで指定した多重度でそれぞれが-nで指定したリクエスト数を送信するのではない点に注意してください。

例えば、総リクエスト数1,000(-n 1000)を4多重(-c 4)で実行した場合、実際に送信されるリクエストは4,000ではなく1,000になります。ただし、総リクエスト数は多重度で割り切れる必要はなく、総リクエスト数1,000に対して3多重という設定も可能です。

実行例

例えば、http://localhost:8080に対して、計1,000回のリクエストを10多重で送信する場合は以下のようにします。

$ ab -n 1000 -c 10 http://localhost:8080/
This is ApacheBench, Version 2.3 <$Revision: 1903618 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:        Apache/2.4.56
Server Hostname:        localhost
Server Port:            8080

Document Path:          /
Document Length:        49730 bytes

Concurrency Level:      10
Time taken for tests:   4.907 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      50006000 bytes
HTML transferred:       49730000 bytes
Requests per second:    203.77 [#/sec] (mean)
Time per request:       49.075 [ms] (mean)
Time per request:       4.907 [ms] (mean, across all concurrent requests)
Transfer rate:          9950.92 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       5
Processing:    35   48  12.5     46     266
Waiting:       34   45   7.8     44      88
Total:         36   48  12.5     46     266

Percentage of the requests served within a certain time (ms)
  50%     46
  66%     49
  75%     52
  80%     54
  90%     59
  95%     65
  98%     73
  99%     76
 100%    266 (longest request)

実行結果の見方

先ほど示した実行例をもとに実行結果のうち、主な項目についての見方を説明します。

Requests per secondは、1秒あたりのリクエスト数をあらわしています。実行例では203.77となっており、1秒あたり203リクエストを処理していることがわかります。

Time per requestは、1リクエストあたりの平均応答時間をあらします。実行例では2つのTime per requestがあり、最初のTime per requestは1リクエストあたりの平均応答時間で、平均49.075msで処理できていることがわかります。もう一つのTime per requestは、多重度を考慮した平均応答時間で、今回は10多重にしているため、1/10にあたる4.907msで1リクエストが処理できていることをあらわしています。

Transfer rateは、平均転送速度で、1秒あたりに転送されるデータ量をあらわします。実行例では、9950.92Kbytes/secで転送しており、1秒あたり10MB近い転送速度になっています。

Complete requestsは完了したリクエスト数をあらわし、Failed requestsは正常に処理できなかったリクエスト数をあらわします。実行例では、1,000リクエスト中、正常に処理できなかったリクエストは0になっています。負荷を上げていくとFailed requests0でなくなるタイミングがでてきます。これが性能限界をあらわしています。

例えば、実行例と同じ環境で20,000リクエストを200多重で行った結果は以下のようになりました。2リクエストはレスポンスの長さが想定と異なるエラーが発生しています。

Complete requests:      20000
Failed requests:        2
   (Connect: 0, Receive: 0, Length: 2, Exceptions: 0)

括弧内のエラーの意味は以下のようになります。

  1. Connect: サーバへの接続の際のエラー数。ネットワークの問題やサーバの過負荷、サーバのダウンなどが原因で接続できなかった場合にこの数が増えます
  2. Receive: サーバからのレスポンスの受信中に発生したエラー数。サーバからの応答が途中で切断されるなどの理由でこの数が増えることがあります
  3. Length: レスポンスの内容の長さが予期したものと異なる場合のエラー数。たとえば、予期していたHTMLページのサイズと実際に受信したサイズが異なる場合にこのエラーがカウントされます
  4. Exceptions: ab自体がエラーを捕捉した場合の数。これは通常、予期しないエラーや問題がabの実行中に発生した場合にカウントされます

Length

今回使用しているURLは固定のページを返しているため、リクエストの度にレスポンスの長さが変わることはありません。このことから、2回のLengthはエラーであるといえます。しかし、このエラー判定は同じURLが返すレスポンスの長さは常に同じという前提のもとに判定されているため、Webアプリケーションによってはリクエストごとにレスポンスの長さが変わる場合もありますので、必ずしもエラーとなりません。また、このLengthはHTTPヘッダーのContent Lengthと一致していないという判定でもない点に注意が必要です。

404エラー、500エラー

もう一つ重要な点が404エラーや500エラーを検知しない点です。負荷を上げたときにサーバーで500エラー担った場合でもレスポンスが返せているのであれば、ExceptionsやReceiveではなく、Lengthとして検知されます。

そのため、設定が誤っていて404エラーになるURLを指定してしまった場合でもエラーにはならずすべて正常に処理されます。

エラーの発生状況や正しいURLを実行できていることはアクセスログなどのサーバーログを併用して確認することが重要です。

その他の主要オプション

その他の主要オプションとして、-Aオプション、-Cオプション、-Hオプションを紹介します。

BASIC認証情報を設定する-Aオプション

BASIC認証でアクセス制限を行っている場合、-Aオプションを使って認証情報を設定します。

-A <ユーザーID>:<パスワード>

の形式で認証を情報を設定します。例えば、ユーザーID「user」、パスワード「password」で認証できるBASIC認証が設定されているサイトの場合、

$ ab -n 1000 -c 10 -A user:password http://localhost:8080/

のように指定します。

Cookieを設定する-Cオプション

認証情報をCookieで設定する場合などは-Cオプションを使用してCookieを設定します。

-C "<Cookie名>=<値>"

の形式で設定し、複数ある場合は-Cオプションを複数設定します。例えば、JWTトークン12345tokenというCookie名で送信する場合、

$ ab -n 1000 -c -C "token=12345" http://localhost:8080/

のように指定します。

HTTPヘッダーを設定する-Hオプション

HTTPヘッダーを設定する場合は、-Hオプションを使用します。

-H "<ヘッダー名>: <値>"

の形式で設定し、複数ある場合は-Hオプションを複数設定します。例えば、Accept-EncodingヘッダーとAccept-Languageヘッダーを設定する場合は、

$ ab -n 1000 -c 10 -H "Accept-Encoding: gzip, deflate, br" -H "Accept-Language: en-US,en;q=0.9,ja;q=0.8" http://localhost:8080/

のように指定します。

実行結果をファイルに出力する

実行結果をファイルに出力するオプションがいくつか用意されています。

HTML形式で出力する

テキスト形式で表示していた実行結果をHTML形式で出力するには-wオプションを使用します。このオプションを使用することでこれまでテキスト形式で出力されていた内容をHTML形式で出力できるようになります。コンソールにそのまま表示してもわかりにくいだけですので、基本的にはリダイレクトでファイルに出力して使用します。

$ ab -n 1000 -c 10 -w http://localhost:8080/ > result.html

最低限のタグ付けがされているだけですので、見た目はそれほどきれいという訳ではありません。

CSV形式で出力する

-eオプションで出力先のファイル名を指定することで、テキスト形式で表示していた内容とは別に0%から100%までのパーセンタイル情報がCSV形式で出力されます。

$ ab -n 1000 -c 10 -e result.csv http://localhost:8080/
$ head result.csv
Percentage served,Time in ms
0,34.739
1,36.607
2,37.105
3,37.471
4,37.737
5,38.012
6,38.288
7,38.449
8,38.511 

0の行が最速のレスポンス時間、50の行が中央値のレスポンス時間、100の行が最遅のレスポンス時間ををあらわします。

実行結果に表示されているPercentage of the requests served within a certain time (ms)のうち、50%が50の行、100%が100の行に対応しています。

TSV形式で出力する

最も使用することが多いのが-gオプションです。TSV形式としていますが、正確にはgnuplotフォーマットになりますが、実質的にTSVファイルとして扱って問題がありません。

$ ab -n 1000 -c 10 -g result.tsv http://localhost:8080/
$ head result.tsv
starttime	seconds	ctime	dtime	ttime	wait
Sun Oct 29 00:05:31 2023	1698505531	0	36	36	35
Sun Oct 29 00:05:29 2023	1698505529	0	36	37	36
Sun Oct 29 00:05:30 2023	1698505530	0	37	37	35
Sun Oct 29 00:05:29 2023	1698505529	0	37	37	36
Sun Oct 29 00:05:29 2023	1698505529	0	37	37	35
Sun Oct 29 00:05:30 2023	1698505530	0	36	37	35
Sun Oct 29 00:05:29 2023	1698505529	0	37	37	36
Sun Oct 29 00:05:29 2023	1698505529	0	37	37	36
Sun Oct 29 00:05:30 2023	1698505530	0	37	37	36

各カラムの意味は以下の通りです。

  • starttime: リクエストが開始された時刻。通常はエポックタイム(1970年1月1日からの経過秒数)で表される
  • seconds: リクエスト開始時間をエポックタイム(秒)
  • ctime: 接続時間。ターゲットサーバーへの接続が確立されるまでの時間(ミリ秒)
  • dtime: 処理時間。リクエストが送信されてからレスポンスが完全に受信されるまでの時間(ミリ秒)
  • ttime: 合計時間。接続の確立からレスポンスの完全な受信までの合計時間(ミリ秒)
  • wait: 応答待機時間。リクエストが完全に送信されてから最初のレスポンスバイトが受信されるまでの時間(ミリ秒)

この形式での出力ではいろいろな情報が得られます。

例えば、ctimeが長すぎる場合はサーバーが待ち受けられる接続の上限に達している可能性がありますし、waitが長すぎる場合はサーバーの処理に遅延が発生していたり、ワーカースレッドが足りない可能性があります。リクエストがエラーになっている原因やパフォーマンスが低下している原因を推測するための一助となる情報が得られる点では重要です。

abではできないこと

abはストレステストツールとして優秀ですが、シナリオテストような一連のURLを順番にアクセスするといった使い方はできません。シナリオテストで負荷をかける場合は、JMeterなどを使用してください。

まとめ

abはランディングページなど静的なサイトでのストレステストでは積極的に採用できます。ただし、クライアントで状態を管理していたり、特定の手順で負荷をかけたい場合はJMeterなど他のツールを採用する必要があります。

すばやく導入でき、複雑な設定なしで使用可能なため、サクッと負荷をかけたい場合は是非abを使ってみてください。

RabbitMQサーバーを立ち上げる

最近、MQサーバーに強い関心があり、その一つとしてRabbitMQサーバーを立ち上げてみました。

RabbitMQサーバーとは

RabbitMQは、メッセージ指向のミドルウェアで、アプリケーション間のメッセージのやり取りを仲介するためのメッセージキューサーバです。

RabbitMサーバーは、Erlang言語で書かれていて、クラスタリングとフェイルオーバーのためにOpen Telecom Platformフレームワークで構築されています。

元々はRabbit Technologies社で開発していましたが、2010年4月にSpringFrameworkの開発元であるSpringSource社に買収され、その後SpringSource社がVMware社に買収されました。そして、EMC、VMware、GEの出資で設立されたPivotal Software社がRabbitMQの開発・サポートを行っていましたが、2019年にVMware社がPivotal Software社を吸収合併し、今はVMware社がサポートを行っています。

特徴

RabbitMQは以下の特徴を持っています。

  1. 言語の中立性
    RabbitMQのクライアントは多くのプログラミング言語で利用できるため、異なる言語で書かれたアプリケーション間でもメッセージのやり取りが可能です。
  2. 耐障害性
    RabbitMQは、メッセージの永続化やクラスタリング機能を提供しており、障害時のデータの喪失リスクを低減することができます。
  3. 拡張性
    クラスタリングやフェデレーションといった機能を利用することで、大量のメッセージトラフィックを扱うことができます。
  4. 柔軟性
    トピックベースのルーティング、ワークキュー、RPCなど、さまざまなメッセージパターンをサポートしています。

主な概念

RabbitMQの主な概念は以下になります。

  1. Producer
    メッセージを生成してRabbitMQに送信するエンティティです。
  2. Consumer
    キューからメッセージを受け取って処理するエンティティです。
  3. Channel
    1つのコネクションを仮想的に分離する概念です。
  4. Queue
    メッセージが保存される場所。消費者はここからメッセージを取得します。
  5. Exchange
    プロデューサから受け取ったメッセージをキューにルーティングする役割を持ちます。RabbitMQにはいくつかの標準的な交換タイプ(direct, topic, fanout, headers)があります。

使われている用語はMQサーバー固有の用語でないため、他のMQサーバーでは少し違う用語で定義されている場合がありますので、ご注意ください。

RabbitMQサーバーの構築

RabbitMQサーバーの起動方法はいくつかありますが、ここでは手軽にDockerで起動することにします。

$ docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3.12-management

Publisherを作成する

MQサーバーにメッセージを送信するPublisherを作成します。RabbitMQでは複数のプログラミング言語をサポートしていますが、ここではPythonのpikaパッケージを使用して作成していきます。

RabbitMQサーバーへのコネクションを確立する

RabbitMQサーバーへのコネクションを確立します。ここではローカルで起動しているRabbitMQサーバーへ接続するため、host='localhost'を指定しています。

import pika

# RabbitMQサーバーへのコネクションを確立する
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))

BlockingConnectionを使用してコネクションを確立すると、同期的に処理を実行することができます。

Channelを作成する

次にChannelを作成します。

import pika

# RabbitMQサーバーへのコネクションを確立する
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))

# チャネルを作成する
channel = connection.channel()

今回の例ではPublisherは1つのキューにメッセージを送信する経路だけがあればよいので、1つだけチャネルを作成しています。

キューを定義する

メッセージを送信するキューを作成します。キューにはキューを一意に識別するための名前をつける必要があり、ここではqueue='hello'を指定しています。

import pika

# RabbitMQサーバーへのコネクションを確立する
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))

# チャネルを作成する
channel = connection.channel()

# キューを作成する
channel.queue_declare(queue='hello')

メッセージを送信する

では、実際にメッセージを送信してみましょう。

メッセージの送信方法にはいくつかありますが、ここではチュートリアルでも使われている最もシンプルな方法で実装します。

import pika

# RabbitMQサーバーへのコネクションを確立する
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))

# チャネルを作成する
channel = connection.channel()

# キューを作成する
channel.queue_declare(queue='hello')

# メッセージを送信する
channel.basic_publish(exchange='', routing_key='hello', body='Hello World!!')

Exchangeタイプを''にするとデフォルトのExchangeタイプとなり、routing_keyで指定したキューに直接メッセージを送ります。

コネクションをクローズする

最後にコネクションをクローズします。

import pika

# RabbitMQサーバーへのコネクションを確立する
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))

# チャネルを作成する
channel = connection.channel()

# キューを作成する
channel.queue_declare(queue='hello')

# メッセージを送信する
channel.basic_publish(exchange='', routing_key='hello', body='Hello World!!')

# コネクションをクローズする
connection.close()

Consumerを作成する

次にメッセージを受け取るConsumerを作成します。Consumerはあるキューにメッセージが追加されるのを待ち、メッセージが追加されたらそのメッセージを取得して処理を行います。

コネクションを確立し、キューを定義する

コネクションを確立〜キューを作成するまでの流れはPublisherと同じです。キュー名はPublisherで定義したキュー名を同じにします。

import pika

# RabbitMQサーバーへのコネクションを確立する
pika_param = pika.ConnectionParameters(host='localhost')
connection = pika.BlockingConnection(pika_param)

# チャネルを作成する
channel = connection.channel()

# キューを作成する
channel.queue_declare(queue='hello')

メッセージ受信時に実行するコールバック関数を作成する

メッセージを受信したときに実行するコールバック関数を作成します。この関数はメッセージを受信するたびに呼び出されます。

import pika

# RabbitMQサーバーへのコネクションを確立する
pika_param = pika.ConnectionParameters(host='localhost')
connection = pika.BlockingConnection(pika_param)

# チャネルを作成する
channel = connection.channel()

# キューを作成する
channel.queue_declare(queue='hello')


def callback(ch, method, properties, body):
    """ メッセージを受信したときに実行されるコールバック関数 """
    print(f" {body} Received")
    ch.basic_ack(delivery_tag=method.delivery_tag)

メッセージの受信を待ち受ける

最後にメッセージの受信を待ち受けます。待ち受けるキューのキュー名とメッセージを受信した時に実行するコールバック関数を指定します。

import pika

# RabbitMQサーバーへのコネクションを確立する
pika_param = pika.ConnectionParameters(host='localhost')
connection = pika.BlockingConnection(pika_param)

# チャネルを作成する
channel = connection.channel()

# キューを作成する
channel.queue_declare(queue='hello')


def callback(ch, method, properties, body):
    """ メッセージを受信したときに実行されるコールバック関数 """
    print(f" {body} Received")
    ch.basic_ack(delivery_tag=method.delivery_tag)


# メッセージを受信する
channel.basic_consume(queue='hello', on_message_callback=callback)
channel.start_consuming()

まとめ

今回示したのは、非常に単純な内容です。実際に業務システムに適用するには、もっと複雑な設定が必要となる場合があります。

また、MQサーバーに関する記事で「デファクト」「デファクトスタンダード」という記述を見かけますが、これまで実務で使用している限りでは「とりあえずこれを使っておけばいい」といえるデファクトスタンダードは存在していないと考えています。

どういった目的で使用するのか、動作するプラットフォームは何か、MQサーバーを管理するか/しないか、どれくらいの頻度でメッセージの送信・受信が行われるのか、どうメッセージを配信するのか、エコシステムで使用される関連するミドルウェアは何か、などの複数の要因でどのMQサーバーが最適か、どのMQサーバーは適していないか、が変わります。

そのため、複数のMQサーバーを比較・検討してどのMQサーバーを使用するかを選定する必要がありますが、MQサーバーによって用語が異なっていたり、同じ用語でも指し示しているものが少し違っていたりするため、用語を正しく理解することは非常に重要です。

マイクロサービスが浸透してくるにつれてサービス間を疎結合に保つことが一般的になってきました。また、大規模・超大規模なシステムではSOAの考え方を引き継いでいるものもあり、そういったシステムではシステム間をMQやメッセージバスによって接続している場合もあります。これからはもっとMQサーバーを利用することが多くなってくることが予想されるため、基本的な使用方法だけでも知っておいた方がよいと思います。

Kaggle CLIをアップデートする

以下のような警告が表示された場合、Kaggle CLIを最新バージョンにアップデートしましょう。

Warning: Looks like you're using an outdated API Version, please consider updating (server 1.5.15 / client 1.5.13)

Kaggle CLIをアップデートする

警告にも表示されていますが、念のため現在のバージョンを確認します。

$ kaggle --version
Kaggle API 1.5.13

次に最新バージョンを確認します。pip list --outdatedで更新が必要なパッケージをリストして最新バージョンを確認することができます。

$ pip list --outdated
Package                       Version     Latest       Type
----------------------------- ----------- ------------ -----
・・・
kaggle                        1.5.13      1.5.16       sdist
・・・

警告を見るとサーバーが1.5.15となっていたので、クライアントも1.5.15が最新版かと思いましたが、どうやら最新版は1.5.16のようです。

新しくなる分には問題ありませんので、最新版にアップデートします。

$ pip install --upgrade kaggle
Requirement already satisfied: kaggle in /Users/t0k0sh1/miniforge3/envs/datascience/lib/python3.10/site-packages (1.5.13)
Collecting kaggle
  Downloading kaggle-1.5.16.tar.gz (83 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 83.6/83.6 kB 4.5 MB/s eta 0:00:00
  Preparing metadata (setup.py) ... done
Requirement already satisfied: six>=1.10 in /Users/t0k0sh1/miniforge3/envs/datascience/lib/python3.10/site-packages (from kaggle) (1.16.0)
Requirement already satisfied: certifi in /Users/t0k0sh1/miniforge3/envs/datascience/lib/python3.10/site-packages (from kaggle) (2023.7.22)
Requirement already satisfied: python-dateutil in /Users/t0k0sh1/miniforge3/envs/datascience/lib/python3.10/site-packages (from kaggle) (2.8.2)
Requirement already satisfied: requests in /Users/t0k0sh1/miniforge3/envs/datascience/lib/python3.10/site-packages (from kaggle) (2.29.0)
Requirement already satisfied: tqdm in /Users/t0k0sh1/miniforge3/envs/datascience/lib/python3.10/site-packages (from kaggle) (4.65.0)
Requirement already satisfied: python-slugify in /Users/t0k0sh1/miniforge3/envs/datascience/lib/python3.10/site-packages (from kaggle) (8.0.1)
Requirement already satisfied: urllib3 in /Users/t0k0sh1/miniforge3/envs/datascience/lib/python3.10/site-packages (from kaggle) (1.26.15)
Requirement already satisfied: bleach in /Users/t0k0sh1/miniforge3/envs/datascience/lib/python3.10/site-packages (from kaggle) (6.0.0)
Requirement already satisfied: webencodings in /Users/t0k0sh1/miniforge3/envs/datascience/lib/python3.10/site-packages (from bleach->kaggle) (0.5.1)
Requirement already satisfied: text-unidecode>=1.3 in /Users/t0k0sh1/miniforge3/envs/datascience/lib/python3.10/site-packages (from python-slugify->kaggle) (1.3)
Requirement already satisfied: charset-normalizer<4,>=2 in /Users/t0k0sh1/miniforge3/envs/datascience/lib/python3.10/site-packages (from requests->kaggle) (3.1.0)
Requirement already satisfied: idna<4,>=2.5 in /Users/t0k0sh1/miniforge3/envs/datascience/lib/python3.10/site-packages (from requests->kaggle) (3.4)
Building wheels for collected packages: kaggle
  Building wheel for kaggle (setup.py) ... done
  Created wheel for kaggle: filename=kaggle-1.5.16-py3-none-any.whl size=110685 sha256=9d938a633a89c1157e7dcf713de3ce949e873e92ac478bc1b88a6e2b8fe8b6d0
  Stored in directory: /Users/t0k0sh1/Library/Caches/pip/wheels/43/4b/fb/736478af5e8004810081a06259f9aa2f7c3329fc5d03c2c412
Successfully built kaggle
Installing collected packages: kaggle
  Attempting uninstall: kaggle
    Found existing installation: kaggle 1.5.13
    Uninstalling kaggle-1.5.13:
      Successfully uninstalled kaggle-1.5.13
Successfully installed kaggle-1.5.16

アップデートは成功していますが、念のためバージョンを確認しておきます。

$ kaggle --version
Kaggle API 1.5.16

もう一つ、アップデートが必要なパッケージにリストされていないことも確認します。

$ pip list --outdated | grep kaggle

まとめ

Pythonのパッケージは定期的に更新しないため、バージョンが古くなりがちです。安定しているバージョンを使っているという意味では頻繁に更新する必要はありませんが、Kaggle CLIのようにサーバーとやりとりを行うCLIパッケージについては、気づいたときに最新版にアップデートすることが重要です。

mambaを使ってパッケージのインストールを高速化しよう(conda installの高速化)

conda installを何気なくやるとかなり待たされることがあります。この待ち時間を減らすためにはmambaを使用することが解決策の一つとなります。

インストールが全然始まらない

pandasparquetを扱いたくてconda installpyarrowをインストールしようとしましたが、インストールが全然始まりませんでした。

 $ conda install pyarrow
Collecting package metadata (current_repodata.json): done
Solving environment: failed with initial frozen solve. Retrying with flexible solve.
Solving environment: failed with repodata from current_repodata.json, will retry with next repodata source.
Collecting package metadata (repodata.json): | WARNING conda.models.version:get_matcher(546): Using .* with relational operator is superfluous and deprecated and will be removed in a future version of conda. Your spec was 1.8.0.*, but conda is ignoring the .* and treating it as 1.8.0
WARNING conda.models.version:get_matcher(546): Using .* with relational operator is superfluous and deprecated and will be removed in a future version of conda. Your spec was 1.9.0.
*, but conda is ignoring the .* and treating it as 1.9.0                                                                                                                           done
Solving environment: failed with initial frozen solve. Retrying with flexible solve.
Solving environment: |

conda installに時間がかかる原因としては以下が考えられます。

  1. 依存関係の解決: condaはパッケージの依存関係を非常に厳格に解決します。そのため、多くのパッケージや複雑な環境でのインストールでは、この解決プロセスが時間を要することがあります。
  2. サーバーの応答速度: 利用しているcondaのリポジトリサーバーの応答速度やネットワークの状態によっては、パッケージのダウンロードに時間がかかることがあります。
  3. インストールするパッケージのサイズ: 大きなパッケージや多数のパッケージをインストールする場合、当然ダウンロードやインストールに時間がかかります。

この問題を解決するためには以下のような対策が考えられます。

  • 特定のチャンネルを指定
  • mambaの利用
  • 環境を最小限に保つ
  • パッケージキャッシュのクリア

今回はこの中から、お手軽で高い効果のある「mambaの利用」を適用していきます。

mambaのインストール

まずはmambaをインストールします。

conda install -c conda-forge mamba

conda installにかかる時間を減らす一つのテクニックとしてチャネルを指定する方法があります。ただ、mambaをチャネル指定なしでインストールしてもそれほど時間がかからなかったので、チャネル指定がなくても大丈夫だと思います。

他にチャネルの指定する方法はあるので、conda-forgeへのリンクを貼っておきます。

condaのキャッシュをクリアする

念のためcondaのキャッシュをクリアしておきます。

$ conda clean -a
Will remove 411 (631.3 MB) tarball(s).
Proceed ([y]/n)? y

Will remove 1 index cache(s).
Proceed ([y]/n)? y

Will remove 83 (826.7 MB) package(s).
Proceed ([y]/n)? y

There are no tempfile(s) to remove.
There are no logfile(s) to remove.

何度か質問されるのでいずれの質問にもyを回答します。

mambaを使ってインストールする

では先ほど全然インストールが始まらなかったpyarrowmambaでインストールしていきます。

$ mamba install pyarrow 

Looking for: ['pyarrow']

conda-forge/osx-arm64                                6.7MB @  13.6MB/s  0.5s
conda-forge/noarch                                  12.4MB @  23.9MB/s  0.5s

・・・

Confirm changes: [Y/n] y

・・・

Downloading and Extracting Packages

Preparing transaction: done
Verifying transaction: done
Executing transaction: done

処理時間が格段に短くなるものそうですが、待ち時間が短縮されるので体感的にも早く感じられ、ストレスが軽減されるのがよいですね。

まとめ

データ分析や機械学習の作業でcondaを使ってインストール作業を行う場合はmambaコマンドを使った方が素早く作業を進められます。

外部プログラムをインストールする訳ではなく、conda installmambaをインストールできるので、ノックアウトファクターがほぼないので是非試してみてください。

テストやドキュメントで安全に使用できるドメイン名を知っておこう

テスト用に設定したメールアドレスに誤ってメールを送信してしまった、という経験はありませんか?

テストやドキュメントに使用できるドメインが予約されており、これらを活用することでメール誤送信などを防ぐことができます。

テストやドキュメントに使用できるドメインは予約されている

RFC 6761で、特別な使用のためのドメイン名として、特定のTLD(Top-Level Domain)やSLD(Second-Level Domain)が予約されています。

TLD(Top-Level Domain)

TLDとして、以下のドメインが予約されています。

  • .test: テスト用として予約されています
  • .example: ドキュメンテーションや例示に使用するために予約されています
  • .invalid: 明示的に無効なドメイン名として識別するために使用します
  • .localhost: 伝統的な意味でのlocalhostの名前解決のために予約されています

SLD(Second-Level Domain)

RFC 6761では、以下のドメインがドキュメントや例示のために予約されています。

  • example.com
  • example.net
  • example.org

ユースケース

ユースケース別にどのドメイン使用すればいいかについて見ていきましょう。

送受信の確認を行うメールアドレス

RFC 6761で予約されているドメインはメールの送受信には使用できないため、実際に送受信可能なメールアドレスを使用してください。

その際は、誤送信が起きないように十分ご確認ください。

ダミーのメールアドレス

システムのドメインがt0k0sh1.comの場合、t0k0sh1.testを使用すると良いでしょう。

書籍やドキュメント中に例示するURLやメールアドレス

URLやメールアドレスを書籍やドキュメント中に記載する場合、

  • https://www.example.com
  • test@example.com

のようにexample.comexample.netなどを使用すると良いでしょう。

example.jpなどは使用できないのか

RFC 6761にはexample.jpなどのccTLD(country code Top-Level Domain)については言及されていません。

各国で対応が異なると思いますが、日本の場合はJPRSがよくある質問で答えています。

例示に使用可能なドメイン名はありませんか?

次の文字列のJPドメイン名は、例示としてご利用いただけます。

“EXAMPLE”を用いたもの
例: EXAMPLE.JP
   EXAMPLE.CO.JP
   EXAMPLE.NE.JP

“EXAMPLE”の後に1桁の数字(””0″”から””9″”)がつく文字列を用いたもの
例: EXAMPLE1.JP
   EXAMPLE2.CO.JP
   EXAMPLE3.NE.JP

次の日本語ドメイン名

ドメイン名例.JP (日本語JPドメイン名)
XN–ECKWD4C7CU47R2WF.JP (「ドメイン名例.JP」のpunycode表記)

JPドメイン名の活用について | よくある質問 | JPRS https://jprs.jp/faq/use/#q2

example.jpexample.co.jpなどを書籍やドキュメントにおける例示に使用することは可能です。

まとめ

テスト用ドメインとしては、所有しているドメイン名を基に.testを使用することが推奨されます。一方、書籍やドキュメントで例示する場合はexample.comなどを使用すると良いでしょう。

本来の目的とは若干外れますが、テスト用のURLやメールアドレスとして、example.comなども安全に使用することもできます。

Dockerコマンドチートシート20選+α(実行例付き)

dockerコマンドの中でもよく使うコマンドを実行例付きで20個集めました。

Dockerの状態を確認する

Dockerのバージョン情報を表示

docker --version
$ docker --version
Docker version 24.0.5, build ced0996

より詳細なバージョン情報が知りたい場合は、docker versionコマンドを使用します。

docker version

こちらはクライアントおよび現在接続しているサーバーのより詳細なバージョン情報を得られます。

$ docker version
Client:
 Cloud integration: v1.0.35-desktop+001
 Version:           24.0.5
 API version:       1.43
 Go version:        go1.20.6
 Git commit:        ced0996
 Built:             Fri Jul 21 20:32:30 2023
 OS/Arch:           darwin/arm64
 Context:           desktop-linux

Server: Docker Desktop 4.22.1 (118664)
 Engine:
  Version:          24.0.5
  API version:      1.43 (minimum version 1.12)
  Go version:       go1.20.6
  Git commit:       a61e2b4
  Built:            Fri Jul 21 20:35:38 2023
  OS/Arch:          linux/arm64
  Experimental:     false
 containerd:
  Version:          1.6.21
  GitCommit:        3dce8eb055cbb6872793272b4f20ed16117344f8
 runc:
  Version:          1.1.7
  GitCommit:        v1.1.7-0-g860f061
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

Dockerの実行状態を確認

docker info
$ docker info
Client:
 Version:    24.0.5
 Context:    desktop-linux
 Debug Mode: false
・・・
WARNING: daemon is not using the default seccomp profile

イメージの操作

イメージの一覧表示

実行結果に表示されるREPOSITORYがイメージ名、TAGがタグ名です。

docker images
$ docker images
REPOSITORY                                                TAG                                                                          IMAGE ID       CREATED         SIZE
ubuntu                                                    22.04                                                                        a2f229f811bf   3 weeks ago     69.2MB
ubuntu                                                    20.04                                                                        15c9d636cadd   4 weeks ago     65.7MB

イメージの検索

キーワードをイメージ名や説明に含むイメージを検索します。

docker search <キーワード>
$ docker search nginx
NAME                                              DESCRIPTION                                      STARS     OFFICIAL   AUTOMATED
nginx                                             Official build of Nginx.                         18963     [OK]
unit                                              Official build of NGINX Unit: Universal Web …   10        [OK]
nginxinc/nginx-unprivileged                       Unprivileged NGINX Dockerfiles                   114

イメージの検索はできますが、タグを検索することはできないため、Docker Hubで探してください。

イメージの取得

イメージ名またはイメージ名とタグ名を指定してイメージを取得します。

docker pull <イメージ名>[:タグ]

タグを省略するとデフォルトタグ(多くの場合はlatestタグ)が取得されます。

$ docker pull ubuntu
Using default tag: latest
latest: Pulling from library/ubuntu
db76c1f8aa17: Pull complete
Digest: sha256:ec050c32e4a6085b423d36ecd025c0d3ff00c38ab93a3d71a460ff1c44fa6d77
Status: Downloaded newer image for ubuntu:latest
docker.io/library/ubuntu:latest

What's Next?
  View summary of image vulnerabilities and recommendations → docker scout quickview ubuntu

latestは取得タイミングによってバージョンが異なるため、基本的にはタグ(バージョン)を指定するのが推奨です。

$ docker pull ubuntu:20.04
20.04: Pulling from library/ubuntu
82d728d38b98: Pull complete
Digest: sha256:33a5cc25d22c45900796a1aca487ad7a7cb09f09ea00b779e3b2026b4fc2faba
Status: Downloaded newer image for ubuntu:20.04
docker.io/library/ubuntu:20.04

What's Next?
  View summary of image vulnerabilities and recommendations → docker scout quickview ubuntu:20.04

過去に取得したlatestタグのイメージを最新化する場合は再度docker pullしてください。

$ docker pull phpmyadmin/phpmyadmin
Using default tag: latest
latest: Pulling from phpmyadmin/phpmyadmin
faef57eae888: Pull complete
989a1d6c052e: Pull complete
0705c9c2f22d: Pull complete
621478e043ce: Pull complete
98246dcca987: Pull complete
bfed8c155cb6: Pull complete
7a7c2e908867: Pull complete
d176994b625c: Pull complete
2d8ace6a2716: Pull complete
c70df516383c: Pull complete
15e1b44fe4c7: Pull complete
65e50d44e95a: Pull complete
77f68910bc0a: Pull complete
605dd3a6e332: Pull complete
99ce27188f07: Pull complete
74d64e32c5d5: Pull complete
ef5fc9928b9f: Pull complete
163f3256e112: Pull complete
Digest: sha256:67ba2550fd004399ab0b95b64021a88ea544011e566a9a1995180a3decb6410d
Status: Downloaded newer image for phpmyadmin/phpmyadmin:latest
docker.io/phpmyadmin/phpmyadmin:latest

What's Next?
  View summary of image vulnerabilities and recommendations → docker scout quickview phpmyadmin/phpmyadmin

イメージの削除

docker rmi <イメージ名 or イメージ名:タグ or イメージID>

タグなしは:latestを指定しているのと同じです。

$ docker rmi hello-world
Untagged: hello-world:latest
Untagged: hello-world@sha256:dcba6daec718f547568c562956fa47e1b03673dd010fe6ee58ca806767031d1c
Deleted: sha256:b038788ddb222cb7d6025b411759e4f5abe9910486c8f98534ead97befd77dd7
Deleted: sha256:a7866053acacfefb68912a8916b67d6847c12b51949c6b8a5580c6609c08ae45

コンテナの操作

実行中のコンテナを一覧表示

実行中のコンテナのリストを表示します。

docker ps

一覧に表示されるCONTAINER IDがコンテナIDで、NAMESがコンテナ名です。

$ docker ps
CONTAINER ID   IMAGE     COMMAND                   CREATED         STATUS         PORTS                  NAMES
17e44cfb7902   nginx     "/docker-entrypoint.…"   4 seconds ago   Up 3 seconds   0.0.0.0:8080->80/tcp   nginx01

すべてのコンテナを一覧表示

停止しているコンテナも表示したい場合は-aオプションを使います。

docker ps -a

ステータス(STATUS)で現在の状況を確認できます。Exitedになっている場合はすでに停止しているコンテナです。

CONTAINER ID   IMAGE     COMMAND                   CREATED          STATUS                      PORTS                  NAMES
17e44cfb7902   nginx     "/docker-entrypoint.…"   6 minutes ago    Up 6 minutes                0.0.0.0:8080->80/tcp   nginx01
21dc45864d5f   busybox   "sh"                      29 minutes ago   Exited (0) 29 minutes ago                          bold_volhard

コンテナの起動

docker run <イメージ名>[:<タグ名>]
docker run <イメージID>

$ docker run nginx
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2023/09/03 03:46:40 [notice] 1#1: using the "epoll" event method
2023/09/03 03:46:40 [notice] 1#1: nginx/1.25.2
2023/09/03 03:46:40 [notice] 1#1: built by gcc 12.2.0 (Debian 12.2.0-14)
2023/09/03 03:46:40 [notice] 1#1: OS: Linux 5.15.49-linuxkit-pr
2023/09/03 03:46:40 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576
2023/09/03 03:46:40 [notice] 1#1: start worker processes
2023/09/03 03:46:40 [notice] 1#1: start worker process 29
2023/09/03 03:46:40 [notice] 1#1: start worker process 30
2023/09/03 03:46:40 [notice] 1#1: start worker process 31
2023/09/03 03:46:40 [notice] 1#1: start worker process 32
2023/09/03 03:46:40 [notice] 1#1: start worker process 33

後述の-dオプションをつけないと制御が返ってこない点に注意が必要です。Ctrl+Cでコンテナを停止されることができます。

コンテナ名を指定して起動する

コンテナ名を指定しないと適当な名前で起動します。コンテナ名を指定したい場合は--nameオプションを使用します。

docker run --name <コンテナ名> <イメージ名>[:<タグ名>]
docker run --name <コンテナ名> <イメージID>
$ docker run -d --name web nginx
$ docker ps
CONTAINER ID   IMAGE     COMMAND                   CREATED         STATUS         PORTS     NAMES
ce2fa12b11da   nginx     "/docker-entrypoint.…"   3 seconds ago   Up 3 seconds   80/tcp    web

NAMESが指定したコンテナ名になっていることが確認できます。

すでに存在するコンテナ名を指定するとエラーになりますので、使用していないコンテナ名かどうかを確認するようにしましょう。

$ docker run -d --name web nginx
docker: Error response from daemon: Conflict. The container name "/web" is already in use by container "ce2fa12b11da0fcf214221c5cb4687ed48f5ac73e6e71586d2ddcee98202df7e". You have to remove (or rename) that container to be able to reuse that name.
See 'docker run --help'.

コンテナ名は実行中でも変更可能です。

# 起動中のコンテナのコンテナ名がwebになっていることを確認
$ docker ps
CONTAINER ID   IMAGE     COMMAND                   CREATED         STATUS         PORTS     NAMES
ce2fa12b11da   nginx     "/docker-entrypoint.…"   5 minutes ago   Up 5 minutes   80/tcp    web

# コンテナIDと新しいコンテナ名を指定してリネームする
$ docker rename ce2fa12b11da web01

# コンテナ名がweb01に変更されていることを確認する
$ docker ps
CONTAINER ID   IMAGE     COMMAND                   CREATED         STATUS         PORTS     NAMES
ce2fa12b11da   nginx     "/docker-entrypoint.…"   5 minutes ago   Up 5 minutes   80/tcp    web01

ポートをマッピングしてコンテナを起動

docker run -p <ホスト側ポート番号>:<コンテナ側ポート番号> <イメージ名>[:<タグ名>]
docker run -p <ホスト側ポート番号>:<コンテナ側ポート番号> <イメージID>

nginxイメージで80ポートで公開されているポートを8080ポートでアクセスするには以下のようにします。

# -pオプションなしで80ポートが公開されていることを確認する
$ docker run -d nginx
d9fdeb14dbdbf035e12e7e6d45a24acd84c9da8cf8b8dbadc71d19baeb510ab0

$ docker ps
CONTAINER ID   IMAGE     COMMAND                   CREATED         STATUS         PORTS     NAMES
d9fdeb14dbdb   nginx     "/docker-entrypoint.…"   4 seconds ago   Up 3 seconds   80/tcp    zen_jepsen

# -pオプションをつけて8080ポートでアクセスできるようにする
$ docker run -d -p 8080:80 nginx
55378527d5381db7a3c50bc7a59ad2d8665499bee22f6a319d7e36ae44a08273

$ docker ps
CONTAINER ID   IMAGE     COMMAND                   CREATED          STATUS          PORTS                  NAMES
55378527d538   nginx     "/docker-entrypoint.…"   2 seconds ago    Up 1 second     0.0.0.0:8080->80/tcp   great_lichterman
d9fdeb14dbdb   nginx     "/docker-entrypoint.…"   37 seconds ago   Up 36 seconds   80/tcp                 zen_jepsen

# 実際にアクセスできることも確認する
$ curl http://localhost/
curl: (7) Failed to connect to localhost port 80 after 6 ms: Couldn't connect to server
$ curl http://localhost:8080/
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
・・・

-pオプションをつけるとSTATUSの表示が0.0.0.0:8080->80/tcpとなり、ホスト側が8080ポートでアクセスするとコンテナ側の80ポートにフォワードする設定になっていることがわかります。

コンテナをバックグラウンド実行で起動する

docker run -d <イメージ名>[:<タグ名>]
docker run -d <イメージID>
$ docker run -d nginx
8e715a36735862e297ddc4cec19bb6c1ae96cd4f822e3f4cd915a0d183b87e59

$ docker ps
CONTAINER ID   IMAGE     COMMAND                   CREATED          STATUS          PORTS     NAMES
8e715a367358   nginx     "/docker-entrypoint.…"   10 seconds ago   Up 10 seconds   80/tcp    pedantic_cohen

コンテナの停止

docker stop <コンテナID>
docker stop <コンテナ名>
# コンテナが起動していることを確認
$ docker ps
CONTAINER ID   IMAGE     COMMAND                   CREATED         STATUS         PORTS     NAMES
ce2fa12b11da   nginx     "/docker-entrypoint.…"   5 minutes ago   Up 5 minutes   80/tcp    web01

# コンテナを停止する
$ docker stop ce2
ce2

# コンテナがdocker psでの表示から消え、docker ps -aでのみ確認できるようになっていることを確認
$ docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
$ docker ps -a
CONTAINER ID   IMAGE     COMMAND                   CREATED          STATUS                     PORTS     NAMES
ce2fa12b11da   nginx     "/docker-entrypoint.…"   32 minutes ago   Exited (0) 7 seconds ago             web01

コンテナの起動

docker start <コンテナID>
docker start <コンテナ名>
# 停止しているコンテナを確認する
$ docker ps -a
CONTAINER ID   IMAGE     COMMAND                   CREATED        STATUS                      PORTS     NAMES
17e44cfb7902   nginx     "/docker-entrypoint.…"   16 hours ago   Exited (0) 58 minutes ago             nginx01

# コンテナのコンテナIDの一部を指定してコンテナを起動
$ docker start 17e
17e

# コンテナが起動したことを確認する
$ docker ps -a
CONTAINER ID   IMAGE     COMMAND                   CREATED        STATUS          PORTS                  NAMES
17e44cfb7902   nginx     "/docker-entrypoint.…"   16 hours ago   Up 14 seconds   0.0.0.0:8080->80/tcp   nginx01

コンテナの削除

docker rm <コンテナID>
docker rm <コンテナ名>
# 停止しているコンテナのコンテナIDを確認する
$ docker ps -a
CONTAINER ID   IMAGE     COMMAND                   CREATED        STATUS                     PORTS     NAMES
17e44cfb7902   nginx     "/docker-entrypoint.…"   16 hours ago   Exited (0) 2 seconds ago             nginx01

# コンテナIDの一部を指定してコンテナを削除する
$ docker rm 17e
17e

# コンテナが削除されたことを確認する
$ docker ps -a
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

起動中のコンテナ内でコマンドを実行

docker exec -itコマンドを使うとコンテナ内で任意のコマンドを実行できます。

docker exec -it <コンテナID> <コマンド>
docker exec -it <コンテナ名> <コマンド>
$ docker ps
CONTAINER ID   IMAGE     COMMAND                   CREATED          STATUS         PORTS     NAMES
ce2fa12b11da   nginx     "/docker-entrypoint.…"   39 minutes ago   Up 3 seconds   80/tcp    web01

# コンテナで「ls -l /var/log/nginx」を実行する
$ docker exec -it web01 ls -l /var/log/nginx
total 0
lrwxrwxrwx 1 root root 11 Aug 15 23:58 access.log -> /dev/stdout
lrwxrwxrwx 1 root root 11 Aug 15 23:58 error.log -> /dev/stderr

停止中のコンテナに実行してもエラーとなります。

$ docker stop web01
web01

$ docker exec -it web01 ls -l /var/log/nginx
Error response from daemon: Container ce2fa12b11da0fcf214221c5cb4687ed48f5ac73e6e71586d2ddcee98202df7e is not running

-itオプションにはもう一つ使い方があり、sshコマンドを使わなくてもコンテナ内にログインできるようになります。

$ docker exec -it web01 /bin/bash
root@ce2fa12b11da:/#

コンテナのセキュリティを高めるためには不要なサービスは公開せず、不要なポートも開けない方がよいため、-itオプションでのログインは非常に重要なテクニックとなります。

コンテナのログを表示

コンテナが出力するログを確認します。

docker logs <コンテナID>
docker logs <コンテナ名>

この例ではnginxイメージから作成したコンテナのログを確認しています。

$ docker ps
CONTAINER ID   IMAGE     COMMAND                   CREATED        STATUS        PORTS                  NAMES
17e44cfb7902   nginx     "/docker-entrypoint.…"   15 hours ago   Up 15 hours   0.0.0.0:8080->80/tcp   nginx01
$ docker logs 17e
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2023/08/31 22:20:56 [notice] 1#1: using the "epoll" event method
2023/08/31 22:20:56 [notice] 1#1: nginx/1.25.2
2023/08/31 22:20:56 [notice] 1#1: built by gcc 12.2.0 (Debian 12.2.0-14)
2023/08/31 22:20:56 [notice] 1#1: OS: Linux 5.15.49-linuxkit-pr
2023/08/31 22:20:56 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576
2023/08/31 22:20:56 [notice] 1#1: start worker processes
2023/08/31 22:20:56 [notice] 1#1: start worker process 29
2023/08/31 22:20:56 [notice] 1#1: start worker process 30
2023/08/31 22:20:56 [notice] 1#1: start worker process 31
2023/08/31 22:20:56 [notice] 1#1: start worker process 32
2023/08/31 22:20:56 [notice] 1#1: start worker process 33

ボリュームの操作

ボリュームの一覧表示

一覧に表示されるVOLUME NAMEがボリューム名です。

docker volume ls
$ docker volume ls
DRIVER    VOLUME NAME
local     hello

ボリュームの作成

コンテナ起動にボリュームを作成できるため、使用頻度は低いですが、ボリューム単独で作成することもできます。

docker volume creare <ボリューム名>

helloボリュームを作成する場合は以下を実行します。

$ docker volume create hello
hello

ボリュームの削除

<ボリューム名>を指定してコンテナで使用していないボリュームを削除します。

docker volume rm <ボリューム名>

helloボリュームを削除する場合は以下を実行します。

$ docker volume rm hello
hello

コンテナで使用しているボリュームを削除しようとするとエラーになります。

# ボリュームを作成
$ docker volume rm hello
hello
$ docker volume ls
DRIVER    VOLUME NAME
local     hello

# ボリュームをコンテナで使用する
$ docker run -d -v hello:/world busybox
ef22b9ed8a199057d11e9303eb153872d434b56cf098358da6c761f0aca8b201

# 削除しようとするとエラーとなり、使用しているボリュームのコンテナIDが表示される
$ docker volume rm hello
Error response from daemon: remove hello: volume is in use - [ef22b9ed8a199057d11e9303eb153872d434b56cf098358da6c761f0aca8b201]
$ docker volume ls
DRIVER    VOLUME NAME
local     hello
$ ボリュームを使用しているコンテナを削除する
$ docker rm ef22
ef22

# 再度ボリュームの削除を試み、削除できることを確認する
$ docker volume rm hello
hello
$ docker volume ls
DRIVER    VOLUME NAME

ボリュームを使用しているコンテナを調べる(応用)

xargsコマンドとjqコマンドを組み合わせた方法になりますが、以下のようにすることで指定のコンテナ名(.Name == "hello"がボリューム名を指定している箇所)を使用しているコンテナのコンテナIDを調べることができます。

docker ps -aq | xargs docker inspect | jq '.[] | select(.Mounts[]? | .Name == "hello") | .Id'
"21dc45864d5fa616e293f53b9324f1461b0e14f73adbcd57c2801c4a4ea90b31"

asdfでmacOSにTerraformをインストールする

Terraformを使う必要があったので、asdfでTerraformをインストールしました。

Terraformのインストール

他の手段もありますが、バージョンアップをすることが多いため、asdfを使ってインストールします。

Terraformプラグインの追加

plugin addコマンドでTerraformプラグインを追加します。

$ asdf plugin add terraform
updating plugin repository...HEAD is now at b03baaa feat: add asdf-oapi-codegen plugin (#864)

plugin listコマンドで追加済みのプラグインをリストし、terraformがあれば追加されています。

$ asdf plugin list
nodejs
python
terraform
yarn

Terraformをインストールする

list allコマンドでTerraformの利用可能なすべてのバージョンをリストします。

$ asdf list all terraform
・・・
1.5.0
1.5.1
1.5.2
1.5.3
1.5.4
1.5.5
1.5.6
1.6.0-alpha20230719
1.6.0-alpha20230802
1.6.0-alpha20230816

今回は1.5.6をインストールすることにします。

installコマンドでバージョンを指定してインストールします。

$ asdf install terraform 1.5.6
Downloading terraform version 1.5.6 from https://releases.hashicorp.com/terraform/1.5.6/terraform_1.5.6_darwin_arm64.zip
Skipping verifying signatures and checksums either because gpg is not installed or explicitly skipped with ASDF_HASHICORP_SKIP_VERIFY
Cleaning terraform previous binaries
Creating terraform bin directory
Extracting terraform archive

ログを見ても適切なバイナリ(darwin_arm64)が使用されていることがわかりますが、あとでバージョンを確認したときにもう一度確認しておきましょう。

今回インストールしたバージョンは普段使いするバージョンですので、globalコマンドを使ってグローバルに使用できるようにします。

$ asdf global terraform 1.5.6

複数のプロジェクトを担当していて、プロジェクトによって使用するバージョンが異なる場合は、localでプロジェクトごとにバージョンを制御できるようにしてください。

インストールできたことを確認するために-vオプションを指定してバージョンを確認します。

$ terraform -v
Terraform v1.5.6
on darwin_arm64

インストール時に指定したバージョンで、バイナリの種類も適切であることを確認できました。

まとめ

Terraformのバージョン管理の手段としては、tfenvが有名です。

こちらでもいいのですが、せっかくasdfをインストールしているので、asdfを使ってインストールしてみました。待ち時間もほとんどなくサクッとインストールすることができました。

RxJSとは Part 1

RxJSはかなりボリュームがあるので何回かに分けて取り扱います。まずは、RxJSの概要、ObservableObserverについて説明します。

RxJSとは?

RxJSは、ReactiveXプロジェクトの一部として開発されたJavaScript用のリアクティブプログラミングライブラリです。このライブラリを用いることで、非同期データストリームをシンプルかつ効果的に扱うことができます。Webアプリケーションでのユーザーインタラクションのハンドリングや外部データソースからのデータ更新の監視はもちろん、Node.jsベースのバックエンドでの開発においても有効です。

特に、Node.jsにおける利用ケースとしては、WebSocketメッセージのストリーム処理、外部システムやデータベースからのイベントドリブンアーキテクチャ、ストリームベースのファイル処理、外部APIのポーリングやマイクロサービス間の非同期通信など、非同期やデータストリーム処理が中心となるシナリオでの使用が挙げられます。

リアクティブプログラミングの概要

リアクティブプログラミングは、データストリームとそのデータストリームに対する変化の伝播に基づくプログラミングパラダイムの1つです。これは、変数の変更やイベント、ユーザーの操作など、さまざまな情報源からのデータを非同期的に扱うのに適しています。

例えば、ユーザーインターフェイスのイベント、APIからのレスポンス、または定期的に発生するイベントなど、様々なものがデータストリームとして表現されます。そして、これらのデータストリームに変更や操作を行うための処理を結合・合成することで、高度な動作やデータの流れを簡単に表現することができます。

ObservableとObserver

RxJSの中心となる概念は、ObservableObserverです。

Observable

データストリームの源となるオブジェクトです。非同期的にゼロ以上の値を生成し、それらの値をObserverに通知します。例として、ユーザーのクリックイベントやHTTPリクエストのレスポンスなどが考えられます。

Observer

Observableからのデータの通知を受け取るオブジェクトです。3つの主要なメソッドがあります。

  • next: 新しいデータがObservableから送られてきたときに呼び出される。
  • error: エラーが発生したときに呼び出される。
  • complete: データの送信が完了したときに呼び出される。

Observableを購読することで、データストリームを監視し、新しいデータやエラー、完了の通知を受け取ることができます。この購読の過程でObserverの各メソッドが関連付けられ、データの流れに応じて適切な処理が行われます。

RxJSの主要な概念

Observableの作成

Observableは、非同期なデータや複数のデータの流れを表現するオブジェクトです。RxJSでは、様々な方法でObservableを生成することができます。

of

静的な値からObservableを作成します。

import { of } from 'rxjs';
const observable = of(1, 2, 3);

from

配列やPromise、繰り返し可能なオブジェクトからObservableを作成します。

import { from } from 'rxjs';
const observable = from([1, 2, 3]);

create

カスタムのObservableを作成します。特定のロジックや外部のデータソースと結合したい場合に使用します。

import { Observable } from 'rxjs';
const observable = new Observable(observer => {
  observer.next('Hello');
  observer.next('World');
  observer.complete();
});

pipe関数

pipe関数 は、与えられたオペレータを左から右の順番で適用して、新しい Observable を返すObservableの関数です。このメカニズムにより、非同期データの変換、フィルタリング、エラーハンドリングなどの一連の操作を組み合わせることができます。

import { of } from 'rxjs';
import { map, filter } from 'rxjs/operators';

const data$ = of(1, 2, 3, 4, 5);

const processedData$ = data$.pipe(
  filter(num => num % 2 === 0),  // 偶数だけをフィルタリング
  map(num => num * 10)           // 値を10倍に変換
);

processedData$.subscribe(console.log);  // 20, 40 が出力される

Operatorsの概要

Operatorsは、Observableのデータストリームに対して操作を行い、新しいObservableを生成する関数です。これにより、非同期データのフィルタリング、変換、結合などの様々な操作を行うことができます。

mapオペレーター

map オペレーターは、Observable が発行する各アイテムに関数を適用します。入力として関数を受け取り、この関数をストリームの各アイテムに適用します。入力としての関数は、アイテムを別の形に変換するために使用されます。

import { of } from 'rxjs';
import { map } from 'rxjs/operators';
const nums = of(1, 2, 3);
const squareValues = nums.pipe(map(val => val * val));

filterオペレーター

filter オペレーターは、指定された条件に基づいて Observable のアイテムをフィルタリングします。入力として関数を受け取り、この関数は各アイテムに対して真偽値を返します。返された真偽値が true の場合、そのアイテムは出力されるストリームに含まれます。false の場合、アイテムは出力されるストリームから除外されます。

import { from } from 'rxjs';
import { filter } from 'rxjs/operators';
const nums = from([1, 2, 3, 4, 5]);
const evenNums = nums.pipe(filter(val => val % 2 === 0));

tapオペレーター

tap オペレータは、Observable の各要素に対して副作用を持たせるためのものです。つまり、データストリーム自体には影響を及ぼさず、Observable の要素に何らかの操作を適用するために使用します。これはデバッグやログ出力などの目的でよく使用されます。

import { of } from 'rxjs';
import { tap } from 'rxjs/operators';

of(1, 2, 3).pipe(
  tap(val => console.log(`Before map: ${val}`)),
  tap(val => val * 2),
  tap(val => console.log(`After map: ${val}`)) // 値は変わらない
).subscribe();

これ以外にも、数多くのOperatorsが提供されており、様々なデータ操作を容易に行えます。

まとめ

ObservableとObserverはリアクティブプログラミングの考え方に即した比較的わかりやすいクラスです。また、今回扱ったオペレータはごく一部ですが、とてもよく使用するものを挙げました。

次回はSubjectを扱います。

モバイルバージョンを終了