かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot + Spring Integration でいろいろ試してみる ( 番外編 )( Docker for Windows では host networking driver は使えない )

概要

記事一覧はこちらです。

Spring Boot + Spring Integration でいろいろ試してみる ( その30 )( Docker Compose でサーバを構築する、SMTP over SSL+POP over SSLサーバ編 ) において、おそらく Docker のネットワークかポートフォワードあたりが原因で POP over SSL のサーバを Docker で構築できなかったのですが、Docker の network には bridge 以外に host と呼ばれるものがあったことを思い出して試してみたが結論としては使えないことが分かった、という記事です。

Docker の Document の Use host networking を見れば The host networking driver only works on Linux hosts, and is not supported on Docker for Mac, Docker for Windows, or Docker EE for Windows Server. と書いてあるので Docker for Windows で使えないことがすぐに分かるのですが、そんなことが最初から分かる訳もなく使えると思って頑張って調べたんですよね。。。

あとなぜ同じ Unix 系の OS のはずなのに Docker for Mac まで使えないのでしょうか? LinuxMac では使えるが Windows では使えないというのがよく見かけるパターンだと思っていたので、正直これは以外でした。

参照したサイト・書籍

  1. DockerのHost networking機能
    https://deeeet.com/writing/2014/05/11/docker-host-networking/

  2. Docker compose, running containers in net:host
    https://stackoverflow.com/questions/35960452/docker-compose-running-containers-in-nethost

  3. Compose file version 3 reference
    https://docs.docker.com/compose/compose-file/

  4. Amazon ECS における Docker の基本
    https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/docker-basics.html

  5. Use host networking
    https://docs.docker.com/network/host/

目次

  1. Docker for Windows で host networking driver を試してみる
  2. AWS で EC2 インスタンスを立ち上げて試してみる
  3. そして、最後に。。。

手順

Docker for Windows で host networking driver を試してみる

docker network ls コマンドを実行すると DRIVER が bridge, host, null の3つのネットワークが表示されますが、host のネットワークを使えばホストのネットワークインターフェースをそのまま使うと聞いて、もしかするとこれを使えば POP over SSL のサーバの件も解決するかな?と思い試してみることにしました。

f:id:ksby:20181216145816p:plain

ksbysample-eipapp-dockerserver プロジェクトの docker-compose.yml ではなく、D:\tmp ディレクトリを作成してその下に docker-compose.yml を新規作成し、以下の内容を記述します。

version: '3'

services:
  nginx:
    image: nginx:latest
    container_name: nginx
    network_mode: host
  • Docker Compose で host netwoking driver のネットワークを使用したい場合には network_mode: host を記述します。
  • port は Docker Image 作成時に Dockerfile に EXPOSE で記述されたものが設定されます(docker-compose.yml で別のポート番号には変更できません)。

docker-compose up -d コマンドを実行すると、エラーは出ずにコンテナが作成されます。

f:id:ksby:20181216151947p:plain f:id:ksby:20181216152124p:plain

ただし netstat -an コマンドを実行して LISTEN 状態にあるポート番号を見てみると 80番が表示されません。127.0.0.1 や別の IP アドレスで表示されているのを見落としている訳でもありません。

f:id:ksby:20181216152308p:plain

EXPOSE で書かれていたものが自動で使用可能になるのではないのかな?と思い、今度は docker run -d --name nginx --net host nginx コマンドで試してみることにしました。

f:id:ksby:20181216153727p:plain

再度 netstat -an コマンドを実行してみましたが、やっぱり 80番が表示されません。

f:id:ksby:20181216153854p:plain

docker ps コマンドを実行してみると PORTS には何も表示されておらず、

f:id:ksby:20181216154321p:plain

docker inspect nginx コマンドを実行して NetworkSettings の設定を見ても Ports には何も表示されませんでした。

f:id:ksby:20181216154505p:plain

ちなみに docker run -d -p 80:80 --name nginx nginx コマンドでコンテナを作成すると、docker psdocker inspect nginx コマンドは以下のように表示されて、ホストの 80番へのアクセスがコンテナの 80番にフォワードされていることが分かります。

f:id:ksby:20181216154849p:plain f:id:ksby:20181216154957p:plain

AWS で EC2 インスタンスを立ち上げて試してみる

他の環境でどうなるのかを知りたかったので、AWS で EC2 インタンスを立ち上げて docker をインストールして host networking driver を試してみることにします。

EC2 インタンスは CloudFormation の YAML ファイルを作成して立ち上げることにします。D:\tmp の下に sample.yaml を作成して以下の内容を記述します。EC2 起動時に amazon-linux-extras install -y docker で docker をインストールします。

AWSTemplateFormatVersion: 2010-09-09
Description: Single EC2 for docker host network test

Parameters:
  ServiceName:
    Type: String
    Default: ksby
    Description: Service Name
  KeyPair:
    Type: AWS::EC2::KeyPair::KeyName
    Description: KeyPair Name

Mappings:
  StackConfig:
    VPC:
      CIDR: 10.0.0.0/16
    PublicSubnetA:
      CIDR: 10.0.0.0/24
      AvailabilityZoneIndex: 1  # 東京リージョンでは ap-northeast-1b
    EC2:
      InstanceType: t3.nano
      ImageId: ami-0a2de1c3b415889d2    # Amazon Linux 2 AMI (HVM), SSD Volume Type

Resources:
  #########################
  # VPC
  #########################
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !FindInMap [ StackConfig, VPC, CIDR ]
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Join [ "-", [ !Ref ServiceName, "vpc" ] ]

  #########################
  # Subnet
  #########################
  PublicSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select
        - Fn::FindInMap: [ StackConfig, PublicSubnetA, AvailabilityZoneIndex ]
        - Fn::GetAZs: !Ref "AWS::Region"
      CidrBlock: !FindInMap [ StackConfig, PublicSubnetA, CIDR ]
      Tags:
        - Key: Name
          Value: !Join [ "-", [ !Ref ServiceName, "a-subnet-public" ] ]

  #########################
  # InternetGateway
  #########################
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Join [ "-", [ !Ref ServiceName, "inetgw" ] ]
  AttatchGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC

  #########################
  # RouteTable
  #########################
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Join [ "-", [ !Ref ServiceName, "routetable-public" ] ]
  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: AttatchGateway
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref InternetGateway
  AssociatePublicRouteTableToPublicSubnetA:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnetA

  #########################
  # EC2 Web
  #########################
  WebSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Join [ "-", [ !Ref ServiceName, "sg-web" ] ]
      GroupDescription: Web EC2 Security Group
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - Description: ssh
          IpProtocol: tcp
          CidrIp: 0.0.0.0/0
          FromPort: 22
          ToPort: 22
        - Description: http
          IpProtocol: tcp
          CidrIp: 0.0.0.0/0
          FromPort: 80
          ToPort: 80
      Tags:
        - Key: Name
          Value: !Join [ "-", [ !Ref ServiceName, "sg-web" ] ]
  EC2WebA:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !FindInMap [ StackConfig, EC2, InstanceType ]
      KeyName: !Ref KeyPair
      ImageId: !FindInMap [ StackConfig, EC2, ImageId ]
      NetworkInterfaces:
        - AssociatePublicIpAddress: true
          DeviceIndex: 0
          GroupSet:
            - !Ref WebSecurityGroup
          SubnetId: !Ref PublicSubnetA
      UserData:
        Fn::Base64:
          !Sub |
            #!/bin/bash
            yum update -y
            timedatectl status
            timedatectl set-timezone Asia/Tokyo
            localectl status
            localectl set-locale LANG=ja_JP.UTF-8
            localectl set-keymap jp106

            amazon-linux-extras install -y docker
            systemctl start docker
            systemctl enable docker
            usermod -a -G docker ec2-user
      Tags:
        - Key: Name
          Value: !Join [ "-", [ !Ref ServiceName, "a-web-ec2" ] ]

以下のコマンドを実行して、S3 へのアップロード ~ Stack 作成まで行います。

> aws s3 cp sample.yaml s3://<S3バケット名>
> aws cloudformation validate-template \
    --template-url https://s3-ap-northeast-1.amazonaws.com/<S3バケット名>/sample.yaml
> aws cloudformation create-stack \
    --stack-name <stack名> \
    --template-url https://s3-ap-northeast-1.amazonaws.com/<S3バケット名>/sample.yaml \
    --parameters ParameterKey=KeyPair,ParameterValue=<作成済のKeyPair名>

この後、以下のコマンドを何度も実行し、"StackStatus": "CREATE_COMPLETE" が出力されるまで待ちます。

> aws cloudformation describe-stacks --stack-name <stack名>

AWS のコンソールから起動した EC2 インスタンスのパブリックDNSを確認した後、RLogin を使用して ssh でログインします。

docker version コマンドを実行すると 18.06.1-ce がインストールされていました。

f:id:ksby:20181216161602p:plain

docker コマンドは問題なく使えることが確認できたので、最初に netstat -an コマンドで 80番ポートで LISTEN されていないことを確認してから、

f:id:ksby:20181216161820p:plain

docker run -d --name nginx --net host nginx コマンドを実行します。

f:id:ksby:20181216162014p:plain

再度 netstat -an コマンドを実行すると、今度は 80番ポートで LISTEN されていることが確認できました。

f:id:ksby:20181216162130p:plain

以下のコマンドを実行して起動した EC2 インスタンスを削除します。

> aws cloudformation delete-stack --stack-name <stack名>

そして、最後に。。。

Linux 上だと host networking driver は問題なく使えて Docker for Windows だと使えないらしいというのは何となく分かったのですが、再度 Web で検索してみたら Docker の Document の Use host networking を見つけて、やっぱり使えないことが分かったという次第です。

エラーメッセージが出て欲しい。。。

履歴

2018/12/16
初版発行。

Spring Boot + Spring Integration でいろいろ試してみる ( その30 )( Docker Compose でサーバを構築する、SMTP over SSL+POP over SSLサーバ編 )

概要

記事一覧はこちらです。

Spring Integration のアプリケーションで使用するサーバを Docker Compose で構築します。

また動作確認のために Webmail クライアントの hardware/rainloop を利用します。Rainloop を利用するためには IMAP が必要なので、mail-server コンテナの IMAP の設定も利用可能に設定します(認証あり、SSLあり、ポート番号は993番を使用)。

参照したサイト・書籍

  1. tomav/docker-mailserver - Configure SSL
    https://github.com/tomav/docker-mailserver/wiki/Configure-SSL

  2. Using SSL/TLS with Postfix SMTP and Courier POP3/IMAP
    https://www.mad-hacking.net/documentation/linux/applications/mail/using-ssl-tls-postfix-courier.xml

  3. byeCloud: Building a mailserver with modern webmail
    https://www.davd.eu/byecloud-building-a-mailserver-with-modern-webmail/

  4. hardware/rainloop
    https://github.com/hardware/rainloop

  5. Rainloop initial configuration
    https://github.com/hardware/mailserver/wiki/Rainloop-initial-configuration

  6. Using Gmail as SMTP server from Java, Spring Boot apps
    https://sanaulla.info/2017/09/15/using-gmail-as-smtp-server-from-java-spring-boot-apps/

  7. Fixing PKIX Path Building Issues When Using JavaMail and SMTP
    http://springinpractice.com/2012/04/29/fixing-pkix-path-building-issues-when-using-javamail-and-smtp

  8. JavaSSL証明書を追加する
    https://qiita.com/nenokido2000/items/b36b6e5f0854d7d63ba6

  9. keytool を使用して証明書を削除する
    https://docs.oracle.com/cd/E19226-01/821-1299/ghleq/index.html

  10. how to ignore server cert error in javamail
    https://stackoverflow.com/questions/4894954/how-to-ignore-server-cert-error-in-javamail

  11. STARTTLSについて解説
    https://go-journey.club/archives/2326

  12. JavaMail API - SMTP Servers
    https://www.tutorialspoint.com/javamail_api/javamail_api_smtp_servers.htm

  13. JavaMail API - POP3 Servers
    https://www.tutorialspoint.com/javamail_api/javamail_api_pop3_servers.htm

  14. Networking features in Docker for Windows
    https://docs.docker.com/docker-for-windows/networking/

目次

  1. SMTP over SSL+POP over SSL サーバを構築する
    1. docker-compose.yml の mail-server コンテナの設定を変更し、rainloop コンテナの設定を追加する
    2. サーバを起動する
  2. Webmail クライアントの Rainloop で動作確認する
  3. SMTP over SSL でメールを送信するサンプルを作成する。。。が送信できませんでした
  4. 証明書の問題を解決する
    1. 【解決法その1】keytool -import コマンドでメールサーバの証明書をキーストアに追加する
    2. 【解決法その2】spring.mail.properties.mail.smtp.ssl.trust=* の設定で全ての証明書を信頼する
  5. POP over SSL でメールを送信するサンプルを作成する
  6. メモ書き
    1. mail.smtp.ssl.enable=truemail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory は何が違うのか?

手順

SMTP over SSL+POP over SSL サーバを構築する

docker-compose.yml の mail-server コンテナの設定を変更し、rainloop コンテナの設定を追加する

Rainloop のデータを保存するディレクトリ docker/rainloop/data を作成します。

docker/mail-server/config/dovecot.cf の以下の点を変更します。

# SSL を使用しない場合には以下のコメントアウトを解除し、ssl = yes をコメントアウトする
# disable_plaintext_auth = no
# ssl = no
ssl = yes

# debug したい場合には以下の行のコメントアウトを解除する
# auth_verbose = yes
# auth_debug = yes
  • disable_plaintext_auth = nossl = no の2行をコメントアウトします。
  • ssl = yes を追加します。

docker-compose.yml の以下の点を変更します。

  mail-server:
    image: tvial/docker-mailserver:latest
    container_name: mail-server
    hostname: mail
    domainname: example.com
    ports:
      - "25:25"
      - "110:110"
      - "143:143"
      - "465:465"
      - "587:587"
      - "993:993"
      - "995:995"
    volumes:
      - ./docker/mail-server/config/:/tmp/docker-mailserver/
    environment:
      # debug したい場合には以下の行のコメントアウトを解除する
      # - DMS_DEBUG=1
      - ENABLE_SPAMASSASSIN=0
      - ENABLE_CLAMAV=0
      - ENABLE_FETCHMAIL=0
      - ENABLE_FAIL2BAN=0
      - ENABLE_POSTGREY=0
      - ENABLE_POP3=1
    cap_add:
      - NET_ADMIN
      - SYS_PTRACE
    restart: always

  # Webmail クライアント
  rainloop:
    image: hardware/rainloop
    container_name: rainloop
    ports:
      - "8888:8888"
    volumes:
      - ./docker/rainloop/data/:/rainloop/data
  • mail-server コンテナの ports に以下の行を追加します。
    • - "143:143"
    • - "465:465"
    • - "587:587"
    • - "993:993"
    • - "995:995"
  • rainloop コンテナの設定を追加します。

サーバを起動する

docker-compose up -d コマンドを実行します。

f:id:ksby:20181212012711p:plain

Webmail クライアントの Rainloop で動作確認する

まずは管理画面にアクセスして mail-server コンテナに設定している mail.example.com ドメインを追加します。http://localhost:8888/?admin にアクセスしてログイン画面を表示した後、admin / 12345 でログインします。

f:id:ksby:20181212210158p:plain

ログイン後、「Language」「Language(admin)」で「日本語」を選択します。

f:id:ksby:20181212210605p:plain f:id:ksby:20181212211316p:plain f:id:ksby:20181212211527p:plain

画面左側のメニューから「ドメイン」を選択した後、画面右側に表示されるドメイン一覧の中の「gmail.com」のチェックを外してから、

f:id:ksby:20181212211829p:plain

ドメインを追加」ボタンをクリックします。

f:id:ksby:20181212212008p:plain

ドメインを追加」ダイアログが表示されるので、以下の値を入力します。

f:id:ksby:20181212212551p:plain

  • 「名前」には docker-compoyse.yml の mail-server コンテナの hostname + "." + domainname の文字列(mail.example.com)を入力します。
  • IMAP」「SMTP」どちらも、
    • 「サーバー」には mail-server を入力します。docker-compose で起動しており、ユーザ定義の bridge ネットワークにコンテナが接続されているので、rainloop コンテナから mail-server コンテナはコンテナ名でアクセスできます。
    • 「セキュリティ」には「SSL/TLS」を選択します。選択すると IMAP のポートは「993」に、SMTP のポートは「465」に自動で設定されます。

左下の「接続テスト」ボタンをクリックして「IMAP」「SMTP」の文字が緑色で表示されることを確認したら、右下の「追加」ボタンをクリックします。

f:id:ksby:20181212213402p:plain

ドメイン一覧に "mail.example.com" がチェックされた状態で表示されます。

f:id:ksby:20181212213611p:plain

今度はユーザ画面にログインしてメールを送受信してみます。http://localhost:8888/ にアクセスしてログイン画面を表示した後、tanaka@mail.example.com / xxxxxxxx でログインします。

f:id:ksby:20181212214236p:plain

ログインしたら左上の「新規作成」ボタンをクリックします。

f:id:ksby:20181212214458p:plain

メール送信用の画面が開いたら件名とメール本文を入力した後、「送信」ボタンをクリックして送信します。

f:id:ksby:20181212223531p:plain f:id:ksby:20181212223659p:plain

画面右上の人のアイコンのボタンをクリックして、メニューから「ログアウト」を選択します。

f:id:ksby:20181212223804p:plain

ログイン画面に戻ったら、suzuki@mail.example.com / yyyyyyyy でログインします。

f:id:ksby:20181212224112p:plain

受信トレイに tanaka@mail.example.com からのメールが届いています。

f:id:ksby:20181212224159p:plain

mail-server コンテナのログを見ると postfix/smtps/smtpdsmtps になっており、Anonymous TLS connection established とも出力されているので、SMTP によるメール送信は SMTP over SSL で送信できているようです。

f:id:ksby:20181212224722p:plain

Rainloop は Docker で簡単に起動できて設定も分かりやすく、画面のデザインもシンプルで見やすくていいですね。

SMTP over SSL でメールを送信するサンプルを作成する。。。が送信できませんでした

src/main/java/ksbysample/eipapp/dockerserver/flow/MailFlowConfig.java は修正する必要はありません。今は POP over SSL の受信処理はしないので、mailRecvByPop3Flow メソッドに付与した @Bean アノテーションコメントアウトしておきます。

//    @Bean
    public IntegrationFlow mailRecvByPop3Flow() {
        ..........

SMTPSSL 化及びポート番号の変更は application.properties を以下のように変更します。

spring.mail.host=localhost
#spring.mail.port=25
spring.mail.port=465
spring.mail.username=tanaka@mail.example.com
spring.mail.password=xxxxxxxx
spring.mail.properties.mail.smtp.ssl.enable=true
# メール送信処理を debug したい場合には以下の行のコメントアウトを解除する
# spring.mail.properties.mail.debug=true
  • spring.mail.port=25コメントアウトして spring.mail.port=465 を追加します。
  • SMTP 送信時に認証が必要になるので、以下の2行を追加します。
    • spring.mail.username=tanaka@mail.example.com
    • spring.mail.password=xxxxxxxx
  • SSL/TLS で通信をするために以下の1行を追加します。
    • spring.mail.properties.mail.smtp.ssl.enable=true

Tomcat を起動して送信してみますが、javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target という例外が発生して送信できません。。。

f:id:ksby:20181213215418p:plain f:id:ksby:20181213215600p:plain f:id:ksby:20181213215804p:plain

証明書の問題を解決する

原因を調べたところ、Fixing PKIX Path Building Issues When Using JavaMail and SMTP という記事を見つけました。Java がメールサーバに設定されている自己証明書を信頼できないので例外が発生しているようです。

解決方法は以下の2つ。

  • メールサーバの自己証明書をローカル PC の Java のキーストアに追加して、証明書を信頼できるようにする。
  • Java が証明書のチェックをしないようにする。

【解決法その1】keytool -import コマンドでメールサーバの証明書をキーストアに追加する

まずはメールサーバの自己証明書をファイルに保存します。https://github.com/tomav/docker-mailserver/wiki/Configure-SSL#testing-certificate に記載されている docker exec mail-server openssl s_client -connect 0.0.0.0:25 -starttls smtp -CApath /etc/ssl/certs/ コマンドを実行します。

f:id:ksby:20181213225946p:plain f:id:ksby:20181213225644p:plain

赤枠の -----BEGIN CERTIFICATE----------END CERTIFICATE----- の部分(この2行も含む)をコピーして D:\project-springboot\ksbysample-boot-integration\ksbysample-eipapp-dockerserver\mail-example-com.cer というファイルに保存します。

次にファイルに保存した自己証明書をキーストアに追加します。現在使用している Java SE が 8u192 でインストール先が D:\Java\jdk1.8.0_192 の場合、Java のキーストアは D:\Java\jdk1.8.0_192\jre\lib\security\cacerts になります。

keytool -import -alias mail.example.com -file D:\project-springboot\ksbysample-boot-integration\ksbysample-eipapp-dockerserver\mail-example-com.cer -keystore D:\Java\jdk1.8.0_192\jre\lib\security\cacerts コマンドを実行します。

f:id:ksby:20181213230937p:plain

  • 「キーストアのパスワードを入力してください:」の部分で入力待ちになりますので、changeit と入力します(これがデフォルトのパスワードです)。
  • 「この証明書を信頼しますか。 [いいえ]:」の部分で再度入力待ちになりますので、はい と入力します。

keytool -list -keystore D:\Java\jdk1.8.0_192\jre\lib\security\cacerts で登録済の証明書一覧を表示し、mail.example.com が登録されていることを確認します。

f:id:ksby:20181213233058p:plain (.....省略.....) f:id:ksby:20181213233248p:plain

これでメールサーバの証明書を信頼するようになったので、メールが送信できるようになったはずです。Tomcat を起動すると、例外は throw されずにメールが送信できています。

f:id:ksby:20181213233804p:plain

Rainloop で suzuki@mail.example.com の受信トレイを見ると、メールが届いていました。受信したメールは削除します。

f:id:ksby:20181213233943p:plain

最後に keytool -delete -noprompt -alias mail.example.com -keystore D:\Java\jdk1.8.0_192\jre\lib\security\cacerts コマンドでキーストアから登録した証明書を削除します。

f:id:ksby:20181213234932p:plain

【解決法その2】spring.mail.properties.mail.smtp.ssl.trust=* の設定で全ての証明書を信頼する

src/main/resources/application.properties に spring.mail.properties.mail.smtp.ssl.trust=* を追加します。

spring.mail.host=localhost
#spring.mail.port=25
spring.mail.port=465
spring.mail.username=tanaka@mail.example.com
spring.mail.password=xxxxxxxx
spring.mail.properties.mail.smtp.ssl.enable=true
spring.mail.properties.mail.smtp.ssl.trust=*
# メール送信処理を debug したい場合には以下の行のコメントアウトを解除する
# spring.mail.properties.mail.debug=true

これで全ての証明書を信頼してチェックしないようになったので、メールが送信できるようになったはずです。Tomcat を起動すると、例外は throw されずにメールが送信できています。

f:id:ksby:20181213235926p:plain

Rainloop で suzuki@mail.example.com の受信トレイを見ると、メールが届いていました。受信したメールは削除します。

f:id:ksby:20181214000023p:plain

POP over SSL でメールを送信するサンプルを作成する

src/main/java/ksbysample/eipapp/dockerserver/flow/MailFlowConfig.java の以下の点を変更します。

    @Bean
    public Pop3MailReceiver pop3MailReceiver() {
        Pop3MailReceiver pop3MailReceiver
                // テキストベースの POP3 を使用する場合には上の、POP over SSL を使用する場合には下の行のコメントアウトを解除する
                // = new Pop3MailReceiver("localhost", "tanaka@mail.example.com", "xxxxxxxx");
                = new Pop3MailReceiver("localhost", 995, "tanaka@mail.example.com", "xxxxxxxx");
        pop3MailReceiver.setShouldDeleteMessages(true);
        Properties javaMailProperties = new Properties();

        // テキストベースの POP3 を使用する場合には下の4行をコメントアウトする
        javaMailProperties.put("mail.pop3.connectiontimeout", "5000");
        javaMailProperties.put("mail.pop3.timeout", "5000");
        javaMailProperties.put("mail.pop3.ssl.enable", "true");
        javaMailProperties.put("mail.pop3.ssl.trust", "*");

        // debug したい場合には以下のコメントアウトを解除する
        // javaMailProperties.put("mail.debug", "true");
        pop3MailReceiver.setJavaMailProperties(javaMailProperties);

        return pop3MailReceiver;
    }
  • ポート番号 995番を使用するので、= new Pop3MailReceiver("localhost", "tanaka@mail.example.com", "xxxxxxxx");= new Pop3MailReceiver("localhost", 995, "tanaka@mail.example.com", "xxxxxxxx"); に変更します。
  • POP over SSL とは関係ありませんが、タイムアウトを設定したいので以下の2行を追加します。
    • javaMailProperties.put("mail.pop3.connectiontimeout", "5000");
    • javaMailProperties.put("mail.pop3.timeout", "5000");
  • POP over SSL を使用するために以下の2行を追加します。今回は全ての証明書を信頼するよう設定します。
    • javaMailProperties.put("mail.pop3.ssl.enable", "true");
    • javaMailProperties.put("mail.pop3.ssl.trust", "*");

これで POP over SSL の設定は完了です。ただし Tomcat を起動して試してみたのですが、以下の画像の結果になりました。

f:id:ksby:20181215213941p:plain

  • 何件かは正常に受信できるが、少し経つと org.springframework.messaging.MessagingException: failure occurred while polling for mail; nested exception is javax.mail.MessagingException: Connect failed; というエラーメッセージが表示されて全く受信できなくなる。自動で復旧する様子はなし。
  • POP over SSL での受信で上のエラーが出ると SMTP over SSL での送信も失敗する。

原因を調査してみましたが、

  • docker-compose downdocker-compose up -d でコンテナを再起動しても復旧しない。
  • mail-server コンテナへの接続だけの問題かと思ったが、Rainloop(http://localhost:8888)や ftp、sftp サーバへの接続も全て失敗する。ホストからコンテナへ接続できなくなっている模様。
  • docker exec -it mail-server /bin/sh は成功する。コマンド実行後に ls コマンド等は動作する。
  • docker ps コマンドを実行すると e1594ff0dcc5 のコンテナの PORTS には 0.0.0.0:995->995/tcp が設定されている。 f:id:ksby:20181216092754p:plain docker inspect --format '{{ .NetworkSettings }}' mail-server を実行すると 995/tcp:[{0.0.0.0 995}] が出力されている。 f:id:ksby:20181216093315p:plain
  • docker 本体を再起動すると復旧する。 f:id:ksby:20181216093820p:plain
  • SMTP over SSL でメールを送信するだけではこの現象は発生しない。 f:id:ksby:20181216094542p:plain
  • docker/mail-server/config/dovecot.cf の設定を disable_plaintext_auth = nossl = no に戻して docker-compose コマンドでコンテナを再起動した後、ksbysample.eipapp.dockerserver.flow.MailFlowConfig の pop3MailReceiver メソッドの実装を 110番ポートの SSL ではない POP3 に戻した時はこの現象は発生しない。 f:id:ksby:20181216095252p:plain
  • 110番ポートで mail.pop3.starttls.enable=truemail.pop3.starttls.required=true の設定をして STARTTLS で接続した場合も、この現象は発生する。POP over SSL で通信した時だけ発生する模様。

結論。全然原因が分かりません! ネットワークインターフェース周りとかポートフォワードあたりのように思えますが Windows でどう調べればよいのか。。。

全てのコンテナへの通信ができなくなるなら今回実装した Java の問題ではなく Docker の問題のようなので、今回は SMTP over SSL、POP over SSL の実装方法が書けたということで終わりにします。POP の実装は実際に動くテキストベースの POP3 の方にしておきます(POP over SSL の実装はコメントアウトします)。

時間が出来たら tvial/docker-mailserver 以外の Image で SMTP, POP サーバを立てて試しみることにします。

履歴

2018/12/16
初版発行。

IntelliJ IDEA を 2018.2.6 → 2018.2.7 → 2018.3.1 へバージョンアップ

IntelliJ IDEA を 2018.2.6 → 2018.2.7 へバージョンアップする

IntelliJ IDEA の 2018.3.1 がリリースされているのでバージョンアップしますが、最初に 2018.2.6 → 2018.2.7 へバージョンアップします。

※ksbysample-webapp-lending プロジェクトを開いた状態でバージョンアップしています。

  1. IntelliJ IDEA のメインメニューから「Help」-「Check for Updates...」を選択します。

  2. IDE and Plugin Updates」ダイアログが表示されます。左下に「Update and Restart」ボタンが表示されていますので、「Update and Restart」ボタンをクリックします。

    f:id:ksby:20181208234825p:plain

  3. Plugin の update も表示されました。このまま「Update and Restart」ボタンをクリックします。

    f:id:ksby:20181208234908p:plain

  4. Patch がダウンロードされて IntelliJ IDEA が再起動します。

  5. IntelliJ IDEA が起動すると画面下部に「Indexing…」のメッセージが表示されますので、終了するまで待機します。

    f:id:ksby:20181208235258p:plain

  6. IntelliJ IDEA のメインメニューから「Help」-「About」を選択し、2018.2.7 へバージョンアップされていることを確認します。

  7. Gradle Tool Window のツリーを見ると「Tasks」の下に「other」しかない状態になっている。。。と思いましたが、今回は変わっていませんでした。念の為、左上にある「Refresh all Gradle projects」ボタンをクリックして更新しておきます。

  8. clean タスク実行 → Rebuild Project 実行 → build タスクを実行して、"BUILD SUCCESSFUL" のメッセージが出力されることを確認します。

    f:id:ksby:20181209000643p:plain

  9. Project Tool Window で src/test を選択した後、コンテキストメニューを表示して「Run with Coverage」-「All Tests」を選択し、テストが全て成功することを確認します。。。が、今回は4件テストが失敗しました。

    f:id:ksby:20181209003304p:plain

    原因を調査します。まず4件中2件は以下のテストでした。

    f:id:ksby:20181209004712p:plain

    最初の testGetLibraryList_都道府県名が正しい場合() メソッドでは java.lang.AssertionError: No further requests expected: HTTP GET http://api.calil.jp/library?appkey=... というエラーが出ていました。AssertionError の例外を throw している箇所を見てみたのですが、原因がいま一つ分かりません。。。 また調査している時に気づいたのですが、テストが失敗しているこのクラスだけテストを実行するとなぜか問題なく成功します。

    さっぱり分からなかったので stackoverflow で検索してみたところ、No further requests expected while MockRestServiceServer was set to ExpectedCount.manyTimes() という QA がヒットしました。どうも MockRestServiceServer を使用するテストがある場合、そのテストクラスに @DirtiesContext アノテーションを付けておかないと他のテストに影響が出るようです。

    Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その17 )( Spock を 1.1-groovy-2.4 → 1.2-groovy-2.5 へバージョンアップする ) の記事で追加した MockRestServiceServer を使用したテストが書かれている src/test/groovy/ksbysample/webapp/lending/service/calilapi/CalilApiServiceRetryTest.groovy に @DirtiesContext アノテーションを付加します。

    f:id:ksby:20181209022415p:plain

    再度「Run with Coverage」-「All Tests」を選択すると、テストが全て成功しました。

    f:id:ksby:20181209023055p:plain

IntelliJ IDEA を 2018.2.7 → 2018.3.1 へバージョンアップする

IntelliJ IDEA の 2018.3.1 がリリースされているのでバージョンアップします。

※ksbysample-webapp-lending プロジェクトを開いた状態でバージョンアップしています。

  1. IntelliJ IDEA のメインメニューから「Help」-「Check for Updates...」を選択します。

  2. IDE and Plugin Updates」ダイアログが表示されます。左下に「Update and Restart」ボタンが表示されていますので、「Update and Restart」ボタンをクリックします。

    f:id:ksby:20181209085028p:plain

  3. Plugin の update も表示されました。このまま「Update and Restart」ボタンをクリックします。

    f:id:ksby:20181209085138p:plain

  4. Patch がダウンロードされて IntelliJ IDEA が再起動します。

  5. 「Complete Installation」ダイアログが表示されます。何も変更せずに「OK」ボタンをクリックします。

    f:id:ksby:20181209085540p:plain

  6. IntelliJ IDEA が起動すると画面下部に「Indexing…」のメッセージが表示されますので、終了するまで待機します。

    f:id:ksby:20181209090002p:plain

  7. Plugin が全て最新にアップデートされていないので先にアップデートします。IntelliJ IDEA のメインメニューから「Help」-「Check for Updates...」を選択します。

  8. IDE and Plugin Updates」ダイアログが表示されます。何も変更せずに「Update」ボタンをクリックします。

    f:id:ksby:20181209090317p:plain

    Patch がダウンロードされた後、IntelliJ IDEA を再起動します。再起動後、画面下部に「Indexing…」のメッセージが表示されますので、終了するまで待機します。

  9. IntelliJ IDEA のメインメニューから「Help」-「About」を選択し、2018.3.1 へバージョンアップされていることを確認します。

  10. Gradle Tool Window のツリーを見ると「Tasks」の下に「other」しかない状態になっている。。。はずでしたが、今回は何も表示されていませんでした。。。

    f:id:ksby:20181209091022p:plain

    左上にある「Refresh all Gradle projects」ボタンをクリックして更新してみたら、いつもと同じように元の状態に戻りました。

    f:id:ksby:20181209091601p:plain

  11. clean タスク実行 → Rebuild Project 実行 → build タスクを実行して、"BUILD SUCCESSFUL" のメッセージが出力されることを確認します。

    f:id:ksby:20181209095629p:plain

  12. Project Tool Window で src/test を選択した後、コンテキストメニューを表示して「Run 'All Tests' with Coverage」を選択し(メニューが階層構造でなくなっていました)、テストが全て成功することを確認します。アイコンも変わりましたね。

    f:id:ksby:20181209101930p:plain

  13. 最後に C:\Users\root\IntelliJIdea2018.2 を削除します。

Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( 番外編 )( gradle-docker-compose-plugin で test の前に自動で Docker コンテナを起動してみる )

概要

記事一覧はこちらです。

Redis、RabbitMQ を Docker に移行したので、Gradle の test タスクを実行したい時にはその前に docker-compose up -d コマンドを実行してコンテナを起動していないとテストが失敗します。自動で起動できる方法がないか調べてみたところ gradle-docker-compose-plugin を見つけましたので試してみます。

ただし、以下の理由から test タスクの前に gradle-docker-compose-plugin でコンテナを自動起動するようにはしませんでした。

  • 既にコンテナが起動していると gradle-docker-compose-plugin の composeUp タスクでエラーとなり、gradle の処理が終了する。開発中は先に手動でコンテナを起動しているはずなので、test を実行したい時に都度コンテナを終了させなければならないのは面倒。解消する方法があると思うのですが、分かりませんでした。。。
  • コンソールに不要な出力が出て、build の状況が分かりにくくなる。

参照したサイト・書籍

  1. avast/gradle-docker-compose-plugin
    https://github.com/avast/gradle-docker-compose-plugin

  2. com.avast.gradle.docker-compose
    https://plugins.gradle.org/plugin/com.avast.gradle.docker-compose

  3. Ability to set log level for specific task
    https://github.com/gradle/gradle/issues/1010

  4. Gradle Docker Plugin User Guide & Examples
    https://bmuschko.github.io/gradle-docker-plugin/ https://github.com/bmuschko/gradle-docker-plugin

    • 今回は Docker Compose の自動起動・停止をしたかったので gradle-docker-compose-plugin を試していますが、Spring Boot Application の Docker Image を作成する場合には、この Gradle Plugin が使えそうです。

目次

  1. build.gradle を変更する
  2. clean タスク実行 → Rebuild Project 実行 → build タスクを実行してみる

手順

build.gradle を変更する

plugins {
    ..........
    id "com.gorylenko.gradle-git-properties" version "2.0.0-beta1"
    id "com.avast.gradle.docker-compose" version "0.8.12"
}

..........

pmd {
    ..........
}

dockerCompose.isRequiredBy(test)
  • plugins block に id "com.avast.gradle.docker-compose" version "0.8.12" を追加します。
  • dockerCompose.isRequiredBy(test) を追加します。

Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。更新後に Gradle Tool Window を見ると「docker」が追加されていました。

f:id:ksby:20181208100156p:plain

clean タスク実行 → Rebuild Project 実行 → build タスクを実行してみる

clean タスク実行 → Rebuild Project 実行 → build タスクを実行してみると、以下のような画面の表示となり、最後に"BUILD SUCCESSFUL" のメッセージが出力されました。

f:id:ksby:20181208101445p:plain f:id:ksby:20181208101550p:plain f:id:ksby:20181208101657p:plain f:id:ksby:20181208101914p:plain (.....空行が結構出力されます.....) f:id:ksby:20181208102027p:plain

  • test タスクの前に composeUp タスクでコンテナが起動されて、test が終了すると composeDown タスクでコンテナが終了されます。
  • composeUp タスクでコンテナが起動される時にはポート番号がアクセス可能かチェックしてくれます。OK でないと先に進みません。com.avast.gradle.docker-compose の「Usage」に記述されている dockerCompose タスクの設定項目を見ると waitForTcpPorts = true という設定がありました。
  • 空行が妙にたくさん出力されます。

単に > Task :composeUp> Task :composeDown だけ出力されるようにしたかったのですが、どう設定してもダメでした。。。

ちなみに既にコンテナが起動している場合には、以下の画像のようになります。

f:id:ksby:20181208223407p:plain (..... Cannot create container for service のエラーが続きます .....) f:id:ksby:20181208223507p:plain

履歴

2018/12/08
初版発行。

Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その23 )( Docker Network メモ書き+Prometheus の HTTP API でデータを削除する )

概要

記事一覧はこちらです。

Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その22 )( Docker で RabbitMQ の環境を構築する2(RabbitMQ の Clustering 構成) ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Docker Compose で Redis と RabbitMQ の Clustering 構成を構築してみましたが、Docker の network 機能が全然分かってないな。。。と思ったので、その辺を調べてみます。
    • 試している途中で Grafana 上に不要なデータが表示されるようになったので、Prometheus の HTTP API を有効にして削除しています。

参照したサイト・書籍

  1. Networking Overview
    https://docs.docker.com/network/

  2. Networking with standalone containers
    https://docs.docker.com/network/network-tutorial-standalone/

  3. Networking in Compose
    https://docs.docker.com/compose/networking/

  4. Prometheus: Delete Time Series Metrics
    https://www.shellhacks.com/prometheus-delete-time-series-metrics/

  5. Prometheus - HTTP API
    https://prometheus.io/docs/prometheus/latest/querying/api/

  6. High Availability Prometheus and Alertmanager
    https://mkezz.wordpress.com/tag/docker/ https://github.com/mkez00/prom-alertmanager-ha

    • 記事とは無関係ですが、Vagrant で自分が知らないことをやっているサンプルを見かけたので、メモとして残しておきます。
  7. Docker Compose - docker-compose.yml リファレンス
    https://qiita.com/zembutsu/items/9e9d80e05e36e882caaa

  8. redis/5.0/alpine/Dockerfile
    https://github.com/docker-library/redis/blob/f1a8498333ae3ab340b5b39fbac1d7e1dc0d628c/5.0/alpine/Dockerfile

  9. Alpine Linux package management
    https://wiki.alpinelinux.org/wiki/Alpine_Linux_package_management

  10. Alpine Linux で軽量な Docker イメージを作る
    https://qiita.com/pottava/items/970d7b5cda565b995fe7

目次

  1. Networking with standalone containers を試してみたメモ書き
  2. docker-compose で network を指定せずにコンテナを作成した時にコンテナ名でも通信できる理由とは?
  3. コンテナ名で通信ができるなら hostname の指定は不要なのでは?
  4. Grafana に追加された rabbit@791f27a3e17b を削除するには?
  5. rabbitmq_exporter → haproxy へはコンテナ名でアクセスさせる
  6. redis-cluster-make コンテナ用の Dockerfile の FROM に指定するイメージを -alpine に変更する

手順

Networking with standalone containers を試してみたメモ書き

Docker のドキュメントに Networking with standalone containers というページがあったので試してみた時のメモ書きです。

  • docker network ls コマンドで作成されている docker network を表示できる。デフォルトで作成されているのは bridge, host, none の3つ。
  • network を指定せずに docker run コマンドを実行した場合、bridge が使用される。
  • デフォルトの bridge ではコンテナ名で通信できない。ping コマンドを実行すると bad address と表示される。IP アドレスでは通信できる。
  • docker network create --driver bridge <network名> コマンドでユーザ定義の bridge ネットワークが作成できる。
  • docker run コマンド実行時に --net オプションでユーザ定義の network を指定すれば、指定された network が使用される。
  • docker run コマンド実行後に docker network connect コマンドで別の network に接続することも可能(複数接続可能らしい)。

ということは、bridge-a-net, bridge-b-net という2つの bridge のユーザ定義 network を作成した後、

f:id:ksby:20181205211008p:plain

alpine1, alpine2, alpine3 の3つのコンテナを作成し、alpine1 は bridge-a-net へ、alpine3 は bridge-b-net へ、そして alpine2 は bridge-a-net, bridge-b-net の両方へ接続します。

f:id:ksby:20181205211315p:plain f:id:ksby:20181205211425p:plain f:id:ksby:20181205211520p:plain

alpine1 から alpine2 に ping は通るが、alpine3 には ping は通らず、

f:id:ksby:20181205211649p:plain

alpine3 から alpine2 に ping は通るが、alpine1 には ping は通らず、

f:id:ksby:20181205211942p:plain

alpine2 から alpine1, alpine2 の両方に ping が通ります。

f:id:ksby:20181205212136p:plain

ifconfig コマンド、route コマンドで各コンテナのネットワークインターフェースやルーティングテーブルを確認すると以下の通り。alpine1, alpine3 には eth0 しかありませんが、alpine2 には eth0, eth1 の2つのネットワークインターフェースがあります。

f:id:ksby:20181205212442p:plain f:id:ksby:20181205212559p:plain f:id:ksby:20181205212701p:plain

なるほど。。。

docker-compose で network を指定せずにコンテナを作成した時にコンテナ名でも通信できる理由とは?

Networking in Compose によると、デフォルトで作成されている bridge network が使われるのではなく、[projectname]_default(projectname は docker-compose.yml のあるディレクトリ名)という名前の network が作成されてそれが使われるとのことでした。

確かに docker-compose up -d コマンドを実行すると、最初に Creating network "ksbysample-webapp-lending_default" with the default driver というメッセージが出力されていました。

f:id:ksby:20181205222609p:plain

docker network inspect ksbysample-webapp-lending_default コマンドを実行すると以下の出力でした。

f:id:ksby:20181205222755p:plain f:id:ksby:20181205222937p:plain f:id:ksby:20181205223118p:plain

コンテナ名で通信ができるなら hostname の指定は不要なのでは?

docker-compose.yml では rabbitmq1 ~ rabbitmq3 のコンテナの定義に hostname: rabbitmq1 のように hostname を設定していますが、コンテナ名と hostname が同じ文字列でコンテナ名で通信できるのであれば書かなくても良い気がします。

  rabbitmq1:
    image: rabbitmq:${RABBITMQ_VERSION}-alpine
    container_name: rabbitmq1
    hostname: rabbitmq1
    environment:
    - RABBITMQ_ERLANG_COOKIE
    - RABBITMQ_DEFAULT_USER
    - RABBITMQ_DEFAULT_PASS
    - RABBITMQ_DEFAULT_VHOST

rabbitmq1 ~ rabbitmq3 の定義から hostname を削除してみます。

  rabbitmq1:
    image: rabbitmq:${RABBITMQ_VERSION}-alpine
    container_name: rabbitmq1
    environment:
    - RABBITMQ_ERLANG_COOKIE
    - RABBITMQ_DEFAULT_USER
    - RABBITMQ_DEFAULT_PASS
    - RABBITMQ_DEFAULT_VHOST
  rabbitmq2:
    image: rabbitmq:${RABBITMQ_VERSION}-alpine
    container_name: rabbitmq2
    depends_on:
    - rabbitmq1
    environment:
    - RABBITMQ_ERLANG_COOKIE
    volumes:
    - ./docker/rabbitmq/cluster-entrypoint.sh:/usr/local/bin/cluster-entrypoint.sh
    entrypoint: /bin/sh -c /usr/local/bin/cluster-entrypoint.sh
  rabbitmq3:
    image: rabbitmq:${RABBITMQ_VERSION}-alpine
    container_name: rabbitmq3
    depends_on:
    - rabbitmq1
    environment:
    - RABBITMQ_ERLANG_COOKIE
    volumes:
    - ./docker/rabbitmq/cluster-entrypoint.sh:/usr/local/bin/cluster-entrypoint.sh
    entrypoint: /usr/local/bin/cluster-entrypoint.sh

docker-compose up -d コマンドを実行するとエラーは出ずに起動し、

f:id:ksby:20181205224740p:plain

HAProxy のログを見ると rabbitmq1 の UP が認識されて(ただし後から rabbitmq2, rabbitmq3 のコンテナが停止していることに気づきました。。。)、

f:id:ksby:20181205224906p:plain

順調かな。。。と思いましたが、管理コンソールを見ると Nodes には1台しか表示されておらず、Name も rabbit@rabbitmq1 ではなく rabbit@791f27a3e17b になっていました。

f:id:ksby:20181205225033p:plain

Grafana でも RabbitMQ が1台追加されて表示されており、

f:id:ksby:20181205225535p:plain

rabbitmq1 コンテナで hostname コマンドを実行すると、rabbitmq1 ではなく 791f27a3e17b という文字列が表示されました。hostname を設定しないとこうなるようです。

f:id:ksby:20181205230103p:plain

RabbitMQ には hostname の設定が必要だったので元に戻します。単に通信するだけならば hostname の設定は不要ですが、サーバによっては hostname が指定の文字列で設定されている必要があるようです。

Grafana に追加された rabbit@791f27a3e17b を削除するには?

hostname の設定を戻してからコンテナを再起動したのですが、rabbit@791f27a3e17b が残ったままでした。。。

f:id:ksby:20181207003552p:plain

削除する方法を調べたところ、Prometheus: Delete Time Series Metrics のページを見つけたので、この方法を試してみます。

最初に Prometheus 起動時に --web.enable-admin-api オプションを追加します。prometheus/Dockerfile から ENTRYPOINT の定義をコピーして --web.enable-admin-api を追加したものを docker-compose.yml に記述します。

  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    ports:
    - "9090:9090"
    volumes:
    - ./docker/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
    - ./docker/prometheus/storage:/prometheus
    entrypoint:
    - "/bin/prometheus"
    - "--storage.tsdb.path=/prometheus"
    - "--web.console.libraries=/etc/prometheus/console_libraries"
    - "--web.console.templates=/etc/prometheus/consoles"
    - "--config.file=/etc/prometheus/prometheus.yml"
    - "--web.enable-admin-api"

docker-compose up -d コマンドを実行します。これで Prometheus の HTTP API が呼び出せるようになりました。ブラウザから http://localhost:9090/api/v1/status/flags にアクセスすると、JSON で結果が返ってきます。

f:id:ksby:20181207003949p:plain

削除するデータを特定するための文字列を取得します。Grafana 上で削除対象の「Edit」を選択した後、

f:id:ksby:20181207004411p:plain

rabbitmq_running{node="$node"} の部分をコピーして $node の部分を rabbit@791f27a3e17b に置き換えた文字列 rabbitmq_running{node="rabbit@791f27a3e17b"} を作成します。

f:id:ksby:20181207004518p:plain

コマンドラインから curl -v -X POST -g 'http://localhost:9090/api/v1/admin/tsdb/delete_series?match[]=rabbitmq_running{node="rabbit@791f27a3e17b"}' を実行します。

f:id:ksby:20181207005030p:plain

Grafana 上ですぐに削除されませんが、Dashboard を表示し直すと rabbit@791f27a3e17b が消えました。

f:id:ksby:20181207005241p:plain

--web.enable-admin-api オプションはデフォルトでは無効化されている機能なので、docker-compose.yml を元に戻しておきます。

rabbitmq_exporter → haproxy へはコンテナ名でアクセスさせる

rabbitmq_exporter が haproxy 経由で rabbitmq にアクセスする時にはホストを経由する必要はないので、コンテナ名でアクセスすることにします。docker-compose.yml の以下の点を変更します。

  rabbitmq_exporter:
    image: kbudde/rabbitmq-exporter:latest
    container_name: rabbitmq_exporter
    ports:
    - "9419:9419"
    environment:
    - RABBIT_URL=http://haproxy:15672
    - RABBIT_USER=${RABBITMQ_DEFAULT_USER}
    - RABBIT_PASSWORD=${RABBITMQ_DEFAULT_PASS}
    - RABBIT_CAPABILITIES=bert,no_sort
    - PUBLISH_PORT=9419
  • RABBIT_URL=http://${HOST_IP_ADDRESS}:15672RABBIT_URL=http://haproxy:15672 に変更します。

redis_exporter → redis も REDIS_ADDR=redis://${HOST_IP_ADDRESS}:${REDIS_CLUSTER_1_PORT},.....REDIS_ADDR=redis://redis-cluster-1:6379,..... にしてみたのですが、こちらは正常に動作しなかったので元に戻しました。

redis-cluster-make コンテナ用の Dockerfile の FROM に指定するイメージを -alpine に変更する

docker/redis/Dockerfile の FROM の記述が FROM redis:${REDIS_VERSION} となっており -alpine を付けていなかったので付けることにします。

redis/5.0/alpine/Dockerfile を参考に docker/redis/Dockerfile を以下のように変更します。

ARG REDIS_VERSION
FROM redis:${REDIS_VERSION}
RUN apk add --no-cache expect
  • FROM redis:${REDIS_VERSION}FROM redis:${REDIS_VERSION}-alpine に変更します。
  • RUN apt-get update -qq && apt-get install -y expectRUN apk add --no-cache expect に変更します。Alpine Linux だと、パッケージ管理ツールが apk になる、インストール前に update 不要、--no-cache を付けてキャッシュに残らないようにする、-y オプションは不要、になるようです。

IntelliJ IDEA の Docker Plugin から redis:5.0.2-custom のイメージを削除した後、docker-compose up -d コマンドを実行します。

f:id:ksby:20181208082347p:plain

redis-cluster-make コンテナのログを見ると yes が自動入力されており、redis-cli --cluster create コマンドも正常終了しています。

f:id:ksby:20181208082511p:plain

redis-cluster-6379 コンテナで redis-cli コマンドを起動して cluster nodes コマンドを実行すると、Resis Cluster 構成が構築できていることが確認できます。

f:id:ksby:20181208082750p:plain

履歴

2018/12/08
初版発行。

Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その22 )( Docker で RabbitMQ の環境を構築する2(RabbitMQ の Clustering 構成) )

概要

記事一覧はこちらです。

Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その21 )( Docker で RabbitMQ の環境を構築する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Docker で RabbitMQ の Clustering 環境を構築します。
    • いろいろ調べてみたところ、RabbitMQ の Clustering 環境を構築するには HAProxy を使うようです。
    • HAProxy を複数立ち上げて keepalived冗長化した上で、HAProxy の backend に RabbitMQ の Clustering 環境を配置するというのが理想なのですが、RabbitMQ 以外に HAProxy + keepalived冗長化構成まで対応していたらいつ終わるか分からないな。。。と思ったので、HAProxy(1台)+ RabbitMQ(3台)の構成にします。
    • 今回は pardahlman/docker-rabbitmq-cluster をほぼそのまま利用しましたので、この記事には変更した点だけ記載しコード全ては記述しません。分かりやすいサンプルでした。作者の方には感謝です。

参照したサイト・書籍

  1. Clustering Guide
    https://www.rabbitmq.com/clustering.html

  2. Highly Available (Mirrored) Queues
    https://www.rabbitmq.com/ha.html

  3. RabbitMQ の分散構成はどうするのが良さそうか?
    http://krdlab.hatenablog.com/entry/2016/05/14/225230

  4. Part 3: RabbitMQ Best Practice for High Availability
    https://www.cloudamqp.com/blog/2018-01-09-part3-rabbitmq-best-practice-for-high-availability.html

  5. pardahlman/docker-rabbitmq-cluster
    https://github.com/pardahlman/docker-rabbitmq-cluster

  6. Cluster RabbitMQ in Docker
    http://fellowdeveloper.se/2017/05/24/cluster-rabbitmq-in-docker/

  7. haproxy
    https://hub.docker.com/_/haproxy/

  8. HAProxy
    http://www.haproxy.org/

  9. HAProxy Documentation Converter
    https://cbonte.github.io/haproxy-dconv/

  10. 多機能プロクシサーバー「HAProxy」のさまざまな設定例
    https://knowledge.sakura.ad.jp/8084/

  11. 多機能なロードバランサとして使える多機能プロクシサーバー「HAProxy」入門
    https://knowledge.sakura.ad.jp/8078/

  12. ロードバランサーの管理
    https://access.redhat.com/documentation/ja-jp/red_hat_enterprise_linux/7/html/load_balancer_administration/

  13. OpenStack Documentation - HAProxy
    https://docs.openstack.org/ja/ha-guide/controller-ha-haproxy.html

  14. mminks/haproxy-docker-logging
    https://github.com/mminks/haproxy-docker-logging

  15. RafPe/docker-haproxy-rsyslog
    https://github.com/RafPe/docker-haproxy-rsyslog

  16. Spring BootでRabbit MQクラスタを利用する
    https://qiita.com/yoshidan/items/7ef893a7ab7ac0f2884b

目次

  1. Docker Compose で RabbitMQ の Clustering 環境を構築する
  2. 動作確認

手順

Docker Compose で RabbitMQ の Clustering 環境を構築する

.env に以下の設定を追加します。

..........

RABBITMQ_VERSION=3.7.8-management
RABBITMQ_ERLANG_COOKIE=Uzkm93w5e1Lz8AcP
RABBITMQ_DEFAULT_USER=rabbitmq
RABBITMQ_DEFAULT_PASS=12345678
RABBITMQ_DEFAULT_VHOST=/
  • 以下の行を追加します。
    • RABBITMQ_ERLANG_COOKIE=Uzkm93w5e1Lz8AcP
    • RABBITMQ_DEFAULT_USER=rabbitmq
    • RABBITMQ_DEFAULT_PASS=12345678
    • RABBITMQ_DEFAULT_VHOST=/
  • Clustering 環境の RabbitMQ 全てに同じ RABBITMQ_ERLANG_COOKIE を設定する必要があるので、.env ファイルに定義してそれを使用するようにします。
  • RABBITMQ_DEFAULT_USER, RABBITMQ_DEFAULT_PASS もついでに .env に移動します。
  • いろいろなサンプルを見ると全て RABBITMQ_DEFAULT_VHOST=/ を定義していたので、.env に追加して使用するようにします。

docker/rabbitmq ディレクトリを作成した後、その下に cluster-entrypoint.sh, haproxy.cfg を配置します。この2つのファイルは pardahlman/docker-rabbitmq-cluster にあるファイルをそのまま持ってきました。ただし改行コードは LF に変更します。改行コードを変更しないと cluster-entrypoint.sh が実行されません。

f:id:ksby:20181202103200p:plain

docker-compose.yml に RabbitMQ の Clustering 構成の設定と HAProxy の設定を追加します。

  # 起動したコンテナに /bin/sh でアクセスする場合には以下のコマンドを実行する
  # docker exec -it rabbitmq /bin/sh
  #
  # 起動したコンテナの rabbitmq に rabbitmqctl で接続して管理コマンドを実行するには以下のコマンドを実行する
  # docker exec -it rabbitmq rabbitmqctl ...
  #############################################################################
  # 単体 Redis サーバ
  # rabbitmq:
  #   image: rabbitmq:${RABBITMQ_VERSION}-alpine
  #   container_name: rabbitmq
  #   hostname: rabbitmq
  #   ports:
  #   - "5672:5672"
  #   - "15672:15672"
  #   environment:
  #   - RABBITMQ_DEFAULT_USER=rabbitmq
  #   - RABBITMQ_DEFAULT_PASS=12345678
  #
  #############################################################################
  # RabbitMQ Clustering
  rabbitmq1:
    image: rabbitmq:${RABBITMQ_VERSION}-alpine
    container_name: rabbitmq1
    hostname: rabbitmq1
    environment:
    - RABBITMQ_ERLANG_COOKIE
    - RABBITMQ_DEFAULT_USER
    - RABBITMQ_DEFAULT_PASS
    - RABBITMQ_DEFAULT_VHOST
  rabbitmq2:
    image: rabbitmq:${RABBITMQ_VERSION}-alpine
    container_name: rabbitmq2
    hostname: rabbitmq2
    depends_on:
    - rabbitmq1
    environment:
    - RABBITMQ_ERLANG_COOKIE
    volumes:
    - ./docker/rabbitmq/cluster-entrypoint.sh:/usr/local/bin/cluster-entrypoint.sh
    entrypoint: /bin/sh -c /usr/local/bin/cluster-entrypoint.sh
  rabbitmq3:
    image: rabbitmq:${RABBITMQ_VERSION}-alpine
    container_name: rabbitmq3
    hostname: rabbitmq3
    depends_on:
    - rabbitmq1
    environment:
    - RABBITMQ_ERLANG_COOKIE
    volumes:
    - ./docker/rabbitmq/cluster-entrypoint.sh:/usr/local/bin/cluster-entrypoint.sh
    entrypoint: /usr/local/bin/cluster-entrypoint.sh
  haproxy:
    image: haproxy:1.8.14-alpine
    container_name: haproxy
    volumes:
    - ./docker/rabbitmq/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
    depends_on:
    - rabbitmq1
    - rabbitmq2
    - rabbitmq3
    ports:
    - "5672:5672"
    - "15672:15672"

  rabbitmq_exporter:
    image: kbudde/rabbitmq-exporter:latest
    container_name: rabbitmq_exporter
    ports:
    - "9419:9419"
    environment:
    - RABBIT_URL=http://${HOST_IP_ADDRESS}:15672
    - RABBIT_USER=${RABBITMQ_DEFAULT_USER}
    - RABBIT_PASSWORD=${RABBITMQ_DEFAULT_PASS}
    - RABBIT_CAPABILITIES=bert,no_sort
    - PUBLISH_PORT=9419
  • 記述している内容は基本的には pardahlman/docker-rabbitmq-cluster の通りなのですが、以下の点を変更しています。
    • image は rabbitmq:${RABBITMQ_VERSION}-alpine に変更しました。RabbitMQ のバージョン番号は .env に定義した RABBITMQ_VERSION を利用し、かつ -alpine のイメージを利用します。
    • 各コンテナに container_name: ... の設定を追加しました。
    • environment の設定は =... の部分を削除しました。.env に同名の環境変数を定義しており、=... を記述しなくても環境変数の値がそのまま適用されます。
    • volumes に記述する cluster-entrypoint.sh のホスト側のパスを ./docker/rabbitmq/cluster-entrypoint.sh に変更しました。
    • haproxy の image を haproxy:1.7haproxy:1.8.14-alpine に変更しました。
  • rabbitmq_exporter の RABBIT_USER, RABBIT_PASSWORD の値は .env に定義した RABBITMQ_DEFAULT_USER, RABBITMQ_DEFAULT_PASS を使用するように変更しました。

docker-compose up -d コマンドを実行して起動します。

f:id:ksby:20181202105159p:plain

IntelliJ IDEA の Docker Plugin でログを見ると以下のように出力されていました。

f:id:ksby:20181202105510p:plain f:id:ksby:20181202105614p:plain f:id:ksby:20181202105746p:plain f:id:ksby:20181202110030p:plain

HAProxy は全然ログが出ていませんでした。ログを出したい場合には mminks/haproxy-docker-loggingRafPe/docker-haproxy-rsyslog を参考にすればよさそうです。

RafPe/docker-haproxy-rsyslog を利用してみます。docker-compose.yml に haproxy-rsyslog の設定を追加します。

  haproxy:
    image: haproxy:1.8.14-alpine
    ..........
  haproxy-rsyslog:
    image: rafpe/docker-haproxy-rsyslog
    container_name: haproxy-rsyslog
    volumes:
    - ./docker/rabbitmq/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
    depends_on:
    - haproxy

docker-compose downdocker-compose up -d コマンドを実行します。

f:id:ksby:20181202112025p:plain

IntelliJ IDEA の Docker Plugin で haproxy-rsyslog コンテナのログを見ると HAProxy のログが出ていました。

f:id:ksby:20181202112205p:plain

RabbitMQ の管理コンソールを見ると「Nodes」のところに3台表示されていました。

f:id:ksby:20181202113359p:plain

Grafana で見ると Clustering している3台のサーバが全て表示されていました。管理コンソールでもないのに自動でサーバが認識されるんですね。。。

f:id:ksby:20181202113204p:plain

動作確認

Tomcat を起動して動作を確認します。Spring Actuator の health check を見ると rabbitmq が認識されています。Redis と違い、RabbitMQ の Clustering 構成は表示は何も変わりませんでした。

f:id:ksby:20181202131705p:plain

ブラウザから http://localhost:8080/ にアクセスして「貸出希望書籍 CSV ファイルアップロード」から貸出希望書籍を登録すると結果のメールが返ってきます。

f:id:ksby:20181202131950p:plain f:id:ksby:20181202132025p:plain

RabbitMQ の管理コンソールを見るとメッセージが送受信されていることが分かります。

f:id:ksby:20181202132130p:plain

rabbitmq1 のサーバを停止してみます。管理コンソールを見ると rabbitmq1 が Node not running と表示されます。

f:id:ksby:20181202132313p:plain

HAProxy の方でも rabbitmq1 の DOWN が検知されています。

f:id:ksby:20181202132515p:plain

「貸出希望書籍 CSV ファイルアップロード」から貸出希望書籍を登録すると、エラーにならず結果のメールが返ってきました。

f:id:ksby:20181202132638p:plain

rabbitmq2, 3 の停止 → rabbitmq1 の起動を連続で行うと。。。、

  • HAProxy の方で rabbitmq2, 3 の DOWN は認識されましたが、いつまで経っても rabbitmq1 の UP が認識されません。
  • Web アプリケーションの方では、以下の TimeoutException のエラーが延々と出続けていました。画面も全く表示されません。
Caused by: java.util.concurrent.TimeoutException: null
    at com.rabbitmq.utility.BlockingCell.get(BlockingCell.java:77)
    at com.rabbitmq.utility.BlockingCell.uninterruptibleGet(BlockingCell.java:120)
    at com.rabbitmq.utility.BlockingValueOrException.uninterruptibleGetValue(BlockingValueOrException.java:36)
    at com.rabbitmq.client.impl.AMQChannel$BlockingRpcContinuation.getReply(AMQChannel.java:494)
    at com.rabbitmq.client.impl.AMQConnection.start(AMQConnection.java:315)
    at com.rabbitmq.client.ConnectionFactory.newConnection(ConnectionFactory.java:1104)
    at com.rabbitmq.client.ConnectionFactory.newConnection(ConnectionFactory.java:1054)
    at com.rabbitmq.client.ConnectionFactory.newConnection(ConnectionFactory.java:994)
    at org.springframework.amqp.rabbit.connection.AbstractConnectionFactory.createBareConnection(AbstractConnectionFactory.java:457)
    ... 9 common frames omitted
  • rabbitmq1 を停止→起動し直しただけではまだ HAProxy の方で rabbitmq1 の UP を認識せず、rabbitmq2, 3 を起動すると rabbitmq1, 2, 3 の UP が認識されました。

どうも RabbitMQ の方で rabbitmq2 か 3 の master になっているサーバが rabbitmq1 に master を引き継げないまま落ちてしまうと、rabbitmq1 のコンテナを起動してもその中の RabbitMQ サーバが起動しないようです。RabbitMQ でほぼ同時にサーバが停止して master を別のサーバに引き継げなかった場合、slave のサーバだけを起動しても RabbitMQ は復旧せず、master のサーバを起動しないとダメなのでしょう。

master がきちんと引き継がれるようにする(全サーバを1度にダウンさせない)ことに注意してサーバを DOWN、UP させながらいろいろ試してみましたが、メッセージの送受信に問題が出ることはありませんでした。

とりあえず Clustering 構成には出来たようです。Queue 間でのメッセージのコピー?等も確認してみたかったのですが、どこか別にやることにします。

履歴

2018/12/02
初版発行。

Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その21 )( Docker で RabbitMQ の環境を構築する )

概要

記事一覧はこちらです。

Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その20 )( Docker で Redis の環境を構築する3(Redis を 5.0.1 → 5.0.2 にバージョンアップする+.env の環境変数を使用するよう変更する)) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • RabbitMQ の環境を Windows バイナリ(rabbitmq-server-3.7.7.exe)をインストールして構築した環境から Docker の RabbitMQ による Cluster 環境へ変更します。
    • 今回は単体の RabbitMQ サーバ環境を構築し、次回 Cluster 環境を構築する予定です。
    • RabbitMQ は管理コンソールを使用したいので、末尾に -management が付いた 3.7.8-management を使用します。

参照したサイト・書籍

  1. rabbitmq
    https://hub.docker.com/_/rabbitmq/

  2. rabbitmq/3.7/debian/Dockerfile
    https://github.com/docker-library/rabbitmq/blob/1a8fd1c6ee027baafb7144c24ad3c995ba5e0d24/3.7/debian/Dockerfile

  3. rabbitmqctl(8)
    https://www.rabbitmq.com/rabbitmqctl.8.html

  4. RabbitMQ Monitoring
    https://grafana.com/dashboards/4279

  5. kbudde/rabbitmq_exporter
    https://github.com/kbudde/rabbitmq_exporter

  6. 【超ざっくりわかる】DebianUbuntuの違いを比較しました
    https://eng-entrance.com/linux-debian-ubuntu

  7. Alpine Linux で Docker イメージを劇的に小さくする
    https://qiita.com/asakaguchi/items/484ba262965ef3823f61

  8. alpine linuxベースのdocker imageに移行したはなし
    https://techblog.zozo.com/entry/docker_image_slim_in_alpinelinux

目次

  1. 単体の RabbitMQ サーバの環境を構築する
  2. ksbysample-webapp-lending から接続する
  3. RabbitMQ のメトリックスを Prometheus + Grafana で表示してみる
  4. docker-compose up -d コマンドでコンテナが起動しないことがあるので Docker の利用可能メモリを 2GB → 4GB に変更する+一部のコンテナを "-alpine" イメージに変更する
  5. メモ書き

手順

単体の RabbitMQ サーバの環境を構築する

.env に RabbitMQ のバージョン番号を記述します。

HOST_IP_ADDRESS=172.23.136.33
REDIS_VERSION=5.0.2
REDIS_CLUSTER_1_PORT=6379
REDIS_CLUSTER_2_PORT=6380
REDIS_CLUSTER_3_PORT=6381
REDIS_CLUSTER_4_PORT=6382
REDIS_CLUSTER_5_PORT=6383
REDIS_CLUSTER_6_PORT=6384

RABBITMQ_VERSION=3.7.8-management
  • RABBITMQ_VERSION=3.7.8-management を追加します。

docker-compose.yml に rabbitmq の設定を追加します。

  rabbitmq:
    image: rabbitmq:${RABBITMQ_VERSION}
    container_name: rabbitmq
    hostname: rabbitmq
    ports:
    - "5672:5672"
    - "15672:15672"
    environment:
    - RABBITMQ_DEFAULT_USER=rabbitmq
    - RABBITMQ_DEFAULT_PASS=12345678

docker-compose up -d コマンドを実行して起動します。

f:id:ksby:20181201160151p:plain

IntelliJ IDEA の Docker Plugin からログを見てみると以下のように出力されています。エラーは出ていません。

f:id:ksby:20181201160251p:plain f:id:ksby:20181201160346p:plain

管理コンソールにアクセスしてみます。http://localhost:15672/ にアクセスするとログイン画面が表示されますので、docker-compose.yml に RABBITMQ_DEFAULT_USER, RABBITMQ_DEFAULT_PASS で設定した rabbitmq / 12345678 でログインします。

f:id:ksby:20181201160702p:plain f:id:ksby:20181201160740p:plain

無事ログインできました。問題はなさそうです。

ksbysample-webapp-lending から接続する

src/main/resources/application-develop.properties の以下の点を変更します。

spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=rabbitmq
spring.rabbitmq.password=12345678
  • 以下の2行を追加します。docker-compose.yml に RABBITMQ_DEFAULT_USER, RABBITMQ_DEFAULT_PASS で guest / guest 以外のユーザID、パスワードを設定したので、application-develop.properties にも同じユーザID、パスワードを設定します。
    • spring.rabbitmq.username=rabbitmq
    • spring.rabbitmq.password=12345678

Tomcat を起動します。Spring Actuator の health check を見ると rabbitmq が認識されています。

f:id:ksby:20181201162154p:plain

ブラウザから http://localhost:8080/ にアクセスして「貸出希望書籍 CSV ファイルアップロード」から貸出希望書籍を登録すると(ここで RabbitMQ にメッセージが送信されます)、

f:id:ksby:20181201162337p:plain

定期タスクが RabbitMQ からメッセージを受信して処理されて、結果のメールが返ってきました。

f:id:ksby:20181201162414p:plain

RabbitMQ の管理コンソールを見るとメッセージが配信されたことが確認できます。

f:id:ksby:20181201162547p:plain

rabbitmqctl コマンドでユーザ一覧を出力してみます。docker exec -it rabbitmq rabbitmqctl list_users コマンドを実行すると、デフォルトで登録している rabbitmq ユーザが出力されました。

f:id:ksby:20181201164125p:plain

RabbitMQ のメトリックスを Prometheus + Grafana で表示してみる

RabbitMQ のメトリックスを Grafana で表示させる Dashboard があるのか調べてみたところ RabbitMQ Monitoring を見つけました。これを利用して表示させてみます。

RabbitMQ → Prometheus への exporter は kbudde/rabbitmq_exporter を Docker で起動して利用します。docker-compose.yml に rabbitmq_exporter の設定を追加します。

  rabbitmq_exporter:
    image: kbudde/rabbitmq-exporter:latest
    container_name: rabbitmq_exporter
    ports:
    - "9419:9419"
    environment:
    - RABBIT_URL=http://${HOST_IP_ADDRESS}:15672
    - RABBIT_USER=rabbitmq
    - RABBIT_PASSWORD=12345678
    - RABBIT_CAPABILITIES=bert,no_sort
    - PUBLISH_PORT=9419

docker/prometheus/prometheus.yml に rabbitmq_exporter からメトリックスを収集する設定を追加します。

- job_name: 'rabbitmq_exporter'
  static_configs:
  - targets: ['172.23.136.33:9419']

docker-compose downdocker-compose up -d コマンドを実行します。

f:id:ksby:20181202002038p:plain

Grafana に RabbitMQ MonitoringDashboard を追加します。http://localhost:3000/ にアクセスした後、画面左側のメニューから「Create」-「Import」を選択します。

f:id:ksby:20181202002312p:plain

「Import」画面が表示されますので 4279 の ID を入力します。

f:id:ksby:20181202002444p:plain

「Prometheus」で「spring-actuator」を選択した後、「Import」ボタンをクリックします。

f:id:ksby:20181202002657p:plain

RabbitMQ MonitoringDashboard が表示されます(画面右上の表示間隔の設定を Last 3 hours Refresh every 5s → Last 15 minutes Refresh every 5s に変更しています)。

f:id:ksby:20181202002845p:plain f:id:ksby:20181202002943p:plain

docker-compose up -d コマンドでコンテナが起動しないことがあるので Docker の利用可能メモリを 2GB → 4GB に変更する+一部のコンテナを "-alpine" イメージに変更する

docker-compose.yml に Redis、RabbitMQ の設定を追加してから docker-compose downdocker-compose up -d コマンドを何度も実行すると、docker-compose up -d コマンドでエラーメッセージが出ないにもかかわらず実際には Redis Cluster が構築できなかったり、RabbitMQ が起動していなかったりしたことがありました。

docker images コマンドで Docker Image の一覧を表示させてサイズを確認すると、

f:id:ksby:20181202014251p:plain

  • redis:5.0.2-custom (197MB x 7 = 1379MB)
  • rabbitmq:3.7.8-management (149MB)
  • grafana/grafana:latest (236MB)
  • prom/prometheus:latest (97.7MB)

で合計が 1861.7MB もあり、Docker Engine にデフォルトで設定されている利用可能メモリのサイズ(2048MB)ぎりぎりでした。

Docker Engine が利用可能なメモリを 2048MB → 4096MB へ、Swap を 1024MB → 2048MB へ変更します。

f:id:ksby:20181202013548p:plain

また Redis のコンテナは全てを redis:5.0.2-custom にする必要はないので、redis-cluster-1~redis-cluster-6 コンテナは redis:${REDIS_VERSION}-customredis:${REDIS_VERSION}-alpine に変更し、redis-cluster-make コンテナだけ redis:${REDIS_VERSION}-custom を使用するように変更します。redis-cluster-1~redis-cluster-6 コンテナには volumes: - ./docker/redis/redis.conf:/etc/redis/redis.conf も追加します。rabbitmq コンテナも rabbitmq:${RABBITMQ_VERSION}rabbitmq:${RABBITMQ_VERSION}-alpine に変更します。

  # 起動したコンテナに /bin/sh でアクセスする場合には以下のコマンドを実行する
  # docker exec -it redis /bin/sh
  #
  # 起動したコンテナの redis に redis-cli でアクセスするには以下のコマンドを実行する
  # docker exec -it redis redis-cli
  #
  #############################################################################
  # 単体 Redis サーバ
  # redis:
  #   image: redis:${REDIS_VERSION}
  #   container_name: redis
  #   ports:
  #   - "6379:6379"
  #
  #############################################################################
  # Redis Cluster
  redis-cluster-1:
    image: redis:${REDIS_VERSION}-alpine
    container_name: redis-cluster-${REDIS_CLUSTER_1_PORT}
    ports:
    - "${REDIS_CLUSTER_1_PORT}:6379"
    - "1${REDIS_CLUSTER_1_PORT}:16379"
    volumes:
    - ./docker/redis/redis.conf:/etc/redis/redis.conf
    command:
    - /bin/sh
    - -c
    - |
      redis-server /etc/redis/redis.conf \
                  --cluster-announce-ip ${HOST_IP_ADDRESS} \
                  --cluster-announce-port ${REDIS_CLUSTER_1_PORT} \
                  --cluster-announce-bus-port 1${REDIS_CLUSTER_1_PORT}
  redis-cluster-2:
    image: redis:${REDIS_VERSION}-alpine
    container_name: redis-cluster-${REDIS_CLUSTER_2_PORT}
    ports:
    - "${REDIS_CLUSTER_2_PORT}:6379"
    - "1${REDIS_CLUSTER_2_PORT}:16379"
    volumes:
    - ./docker/redis/redis.conf:/etc/redis/redis.conf
    command:
    - /bin/sh
    - -c
    - |
      redis-server /etc/redis/redis.conf \
                  --cluster-announce-ip ${HOST_IP_ADDRESS} \
                  --cluster-announce-port ${REDIS_CLUSTER_2_PORT} \
                  --cluster-announce-bus-port 1${REDIS_CLUSTER_2_PORT}
    depends_on:
    - redis-cluster-1
  redis-cluster-3:
    image: redis:${REDIS_VERSION}-alpine
    container_name: redis-cluster-${REDIS_CLUSTER_3_PORT}
    ports:
    - "${REDIS_CLUSTER_3_PORT}:6379"
    - "1${REDIS_CLUSTER_3_PORT}:16379"
    volumes:
    - ./docker/redis/redis.conf:/etc/redis/redis.conf
    command:
    - /bin/sh
    - -c
    - |
      redis-server /etc/redis/redis.conf \
                  --cluster-announce-ip ${HOST_IP_ADDRESS} \
                  --cluster-announce-port ${REDIS_CLUSTER_3_PORT} \
                  --cluster-announce-bus-port 1${REDIS_CLUSTER_3_PORT}
    depends_on:
    - redis-cluster-1
  redis-cluster-4:
    image: redis:${REDIS_VERSION}-alpine
    container_name: redis-cluster-${REDIS_CLUSTER_4_PORT}
    volumes:
    - ./docker/redis/redis.conf:/etc/redis/redis.conf
    ports:
    - "${REDIS_CLUSTER_4_PORT}:6379"
    - "1${REDIS_CLUSTER_4_PORT}:16379"
    command:
    - /bin/sh
    - -c
    - |
      redis-server /etc/redis/redis.conf \
                  --cluster-announce-ip ${HOST_IP_ADDRESS} \
                  --cluster-announce-port ${REDIS_CLUSTER_4_PORT} \
                  --cluster-announce-bus-port 1${REDIS_CLUSTER_4_PORT}
    depends_on:
    - redis-cluster-1
  redis-cluster-5:
    image: redis:${REDIS_VERSION}-alpine
    container_name: redis-cluster-${REDIS_CLUSTER_5_PORT}
    volumes:
    - ./docker/redis/redis.conf:/etc/redis/redis.conf
    ports:
    - "${REDIS_CLUSTER_5_PORT}:6379"
    - "1${REDIS_CLUSTER_5_PORT}:16379"
    command:
    - /bin/sh
    - -c
    - |
      redis-server /etc/redis/redis.conf \
                  --cluster-announce-ip ${HOST_IP_ADDRESS} \
                  --cluster-announce-port ${REDIS_CLUSTER_5_PORT} \
                  --cluster-announce-bus-port 1${REDIS_CLUSTER_5_PORT}
    depends_on:
    - redis-cluster-1
  redis-cluster-6:
    image: redis:${REDIS_VERSION}-alpine
    container_name: redis-cluster-${REDIS_CLUSTER_6_PORT}
    volumes:
    - ./docker/redis/redis.conf:/etc/redis/redis.conf
    ports:
    - "${REDIS_CLUSTER_6_PORT}:6379"
    - "1${REDIS_CLUSTER_6_PORT}:16379"
    command:
    - /bin/sh
    - -c
    - |
      redis-server /etc/redis/redis.conf \
                  --cluster-announce-ip ${HOST_IP_ADDRESS} \
                  --cluster-announce-port ${REDIS_CLUSTER_6_PORT} \
                  --cluster-announce-bus-port 1${REDIS_CLUSTER_6_PORT}
    depends_on:
    - redis-cluster-1
  redis-cluster-make:
    build:
      context: ./docker/redis
      args:
      - REDIS_VERSION=${REDIS_VERSION}
    image: redis:${REDIS_VERSION}-custom
    container_name: redis-cluster-make
    command:
    - /bin/sh
    - -c
    - |
      expect -c "
      spawn redis-cli --cluster create \
                        ${HOST_IP_ADDRESS}:${REDIS_CLUSTER_1_PORT} \
                        ${HOST_IP_ADDRESS}:${REDIS_CLUSTER_2_PORT} \
                        ${HOST_IP_ADDRESS}:${REDIS_CLUSTER_3_PORT} \
                        ${HOST_IP_ADDRESS}:${REDIS_CLUSTER_4_PORT} \
                        ${HOST_IP_ADDRESS}:${REDIS_CLUSTER_5_PORT} \
                        ${HOST_IP_ADDRESS}:${REDIS_CLUSTER_6_PORT} \
                      --cluster-replicas 1
      expect \"Can I set the above configuration? (type 'yes' to accept): \"
      send \"yes\n\"
      expect eof
      "
    depends_on:
    - redis-cluster-1
    - redis-cluster-2
    - redis-cluster-3
    - redis-cluster-4
    - redis-cluster-5
    - redis-cluster-6

  redis_exporter:
    image: oliver006/redis_exporter:latest
    container_name: redis_exporter
    ports:
    - "9121:9121"
    environment:
    - REDIS_ADDR=redis://${HOST_IP_ADDRESS}:${REDIS_CLUSTER_1_PORT},redis://${HOST_IP_ADDRESS}:${REDIS_CLUSTER_2_PORT},redis://${HOST_IP_ADDRESS}:${REDIS_CLUSTER_3_PORT},redis://${HOST_IP_ADDRESS}:${REDIS_CLUSTER_4_PORT},redis://${HOST_IP_ADDRESS}:${REDIS_CLUSTER_5_PORT},redis://${HOST_IP_ADDRESS}:${REDIS_CLUSTER_6_PORT}

  # 起動したコンテナに /bin/sh でアクセスする場合には以下のコマンドを実行する
  # docker exec -it rabbitmq /bin/sh
  #
  # 起動したコンテナの rabbitmq に rabbitmqctl で接続して管理コマンドを実行するには以下のコマンドを実行する
  # docker exec -it rabbitmq rabbitmqctl ...
  rabbitmq:
    image: rabbitmq:${RABBITMQ_VERSION}-alpine
    container_name: rabbitmq
    hostname: rabbitmq
    ports:
    - "5672:5672"
    - "15672:15672"
    environment:
    - RABBITMQ_DEFAULT_USER=rabbitmq
    - RABBITMQ_DEFAULT_PASS=12345678

  rabbitmq_exporter:
    image: kbudde/rabbitmq-exporter:latest
    container_name: rabbitmq_exporter
    ports:
    - "9419:9419"
    environment:
    - RABBIT_URL=http://${HOST_IP_ADDRESS}:15672
    - RABBIT_USER=rabbitmq
    - RABBIT_PASSWORD=12345678
    - RABBIT_CAPABILITIES=bert,no_sort
    - PUBLISH_PORT=9419

docker/redis/Dockerfile は ADD redis.conf /etc/redis/ を削除します。

ARG REDIS_VERSION
FROM redis:${REDIS_VERSION}
RUN apt-get update -qq && apt-get install -y expect

redis:5.0.2-custom の Docker Image を削除してから docker-compose up -d コマンドを実行します。

f:id:ksby:20181202024148p:plain (.....途中は長いので省略.....) f:id:ksby:20181202024321p:plain

IntelliJ IDEA の Docker Plugin で見ると、コンテナは全て起動しており、Redis Cluster の構築も成功していました。

f:id:ksby:20181202024806p:plain

Grafana の Dashboard も正常に表示されて、Tomcat を起動して Web アプリケーションを実行しても特に問題はありませんでした(画面キャプチャは省略)。

再度 docker images コマンドで Docker Image の一覧を表示させてサイズを確認すると、

f:id:ksby:20181202025317p:plain

  • redis:5.0.2-custom (197MB)
  • redis:5.0.2-alpine (40.9MB x 6 = 245.4MB)
  • rabbitmq:3.7.8-management-alpine (82.8MB)
  • grafana/grafana:latest (236MB)
  • prom/prometheus:latest (97.7MB)

で合計が 858.9MB まで下がりました。

メモ書き

今回は記事を書いている途中に書いたり、最後に書いたりしたメモ書きです。

  • redis/5.0/Dockerfilerabbitmq/3.7/debian/Dockerfile もベースの OS には debian:stretch-slim を使用していました。 OFFICIAL REPOSITORY だと debian が多いのでしょうか? 1. 【超ざっくりわかる】DebianとUbuntuの違いを比較しました の記事を読んだ限りでは、堅実で下記の alpine 程ではないが少ないメモリで動くらしい。
  • それとは別に -alpine という Docker Image もありました。Alpine Linux を使用していて、軽量で docker pull、docker build コマンド等が超高速になるらしい。。。というか Docker for Windows でコンテナを多数起動する時には Docker Image のサイズが小さくなる alpine 版が用意されているのはありがたいということが分かりました。
  • Redis も RabbitMQ も管理で使うポート番号に +10000 するのは何かよく使われるルールなのでしょうか?
  • Spring Actuator の Health Check がここにきてすごい便利に感じています。Web アプリケーションから Redis や RabbitMQ がきちんと認識できているのかを簡単に確認できます! DBサーバとしか接続しない場合にはそんなに気にしないのかもしれませんが、いろいろなサーバに接続するようになるとその便利さが実感できます。

履歴

2018/12/02
初版発行。