RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

Dockerのコンテナ間通信~アプリとDBを繋ぐ~

sts-250rrです。

今回はラクス Advent Calendar 2018に投稿した記事、「開発環境を作るためにDockerを使った話」の続きになります。

qiita.com

はじめに

前回の記事ではDockerで開発環境を作ってみました。
が、このままではあまりにもチープな構成に感じます。。。

今回はコンテナ間通信を使って別コンテナに作成したDBのデータを取得できるような構成を作っていくことを目標とします。
目標とする形は以下のようなイメージです。

Dockerネットワークを作成してコンテナ間通信

コンテナ間通信を実現するために、Dockerネットワークを作成します。 Dockerネットワークはコンテナ名を指定することでアプリ用(開発環境用)のコンテナからDBコンテナに接続することができるようになります。

まずはDockerネットワークを作っていきます。

$ docker network create my-network
cbe89848f313a1b7766780903f79da2ff3a83bbe962f930c1f3caf9136fbac6f

$ docker network ls
NETWORK ID          NAME                     DRIVER              SCOPE
39d283374d44        bridge                   bridge              local
a3d0faef3da4        host                     host                local
cbe89848f313        my-network               bridge              local
7b1a7347c6b9        none                     null                local

以下のコマンドでDockerネットワークの詳細を確認することができます。

$ docker network inspect my-network
[
    {
        "Name": "my-network",
        "Id": "cbe89848f313a1b7766780903f79da2ff3a83bbe962f930c1f3caf9136fbac6f",
        "Created": "2018-12-09T02:41:41.8124636Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.21.0.0/16",
                    "Gateway": "172.21.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {},
        "Labels": {}
    }
]

作成したてのDockerネットワークなので何も登録されておらず、Containers部分が空っぽです。
コンテナ起動時にこのDockerネットワークを指定することで、同一のネットワークにコンテナが作成されるため、コンテナ間通信ができるようになります。 ※細かい部分は詰められていません。

開発環境(Go)・PostgreSQLコンテナの起動

コンテナを起動して、開発環境コンテナのGoプログラムからPostgreSQLコンテナのDBに接続出来るような環境構築を行なっていきます。
今回もdocker-composeを利用していくのでディレクトリ構成・ファイルを以下のように作成します。

<任意のディレクトリ>
|-- init
|      |-- 1_createdb.sql
|-- Dockerfile
|-- docker-compose.yml
|-- main.go

今回、main.goからPostgreSQLコンテナのDBに接続するために各設定ファイルやDBの初期構築が必要になります。
各ファイルは以下のように作成します。

docker-compose.yml
version: '3'
services:
  postgres:
    image: postgres
    environment:
      POSTGRES_USER: postgres # user
      POSTGRES_PASSWORD: postgres # pass
    ports:
      - "5432:5432"
    volumes:
      - ./db:/docker-entrypoint-initdb.d
    networks:
      - my-network
  app:
    build: .
    ports:
      - "8080:8080"
    networks:
      - my-network
volumes:
  test_db:
    external: false
networks:
  my-network:
    external: true
Dockerfile
#ベースのDockerイメージをgolangで指定
FROM golang:latest
EXPOSE 5000

#ワークディレクトリを設定する
WORKDIR /go
#ホストのディレクトリを/go配下にコピー
ADD . /go
#GOPATHの設定
ENV GOPATH $GOPATH:$HOME/go
RUN go get github.com/jinzhu/gorm
RUN go get github.com/lib/pq

#main.goを実行
CMD ["go", "run", "main.go"]
1_createdb.sql
create database testdb;
main.go

main.goはこちらを参考にさせていただきました。 実行するとDBに登録されているIDと名前を出力します。

https://qiita.com/rky_1011/items/9772d92b5fe8cb3b82b0qiita.com

package main

import (
   "fmt"

   "github.com/jinzhu/gorm"
   _ "github.com/lib/pq"
)

type User struct {
   ID int64 `gorm:"primary_key" json:"id"`
   Name string `json:"name"`
}

type Users []User

func main() {
   db, err := gorm.Open("postgres", "host=172.24.0.3 user=postgres port=5432 password=postgres dbname=testdb sslmode=disable")
   if err != nil {
      panic(err)
   }
   defer db.Close()
   db.AutoMigrate(User{})
   var user = User{Name: "testname"}
   db.NewRecord(user)
   db.Create(&user)
   db.Save(&user)

   var users = Users{}
   db.Find(&users) 
   fmt.Println(users)
}

さて、ファイルが揃ったら、docker-compose builddocker-compose upを実行すれば、main.goの実行結果が得られます。

(略)
postgres_1  | 2018-12-09 06:32:23.787 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
postgres_1  | 2018-12-09 06:32:23.829 UTC [58] LOG:  database system was shut down at 2018-12-09 06:32:23 UTC
postgres_1  | 2018-12-09 06:32:23.851 UTC [1] LOG:  database system is ready to accept connections
app_1       | [{1 testname}]
app_app_1 exited with code 0

postgres_1 |app_1 |といった形でコンテナごとの出力結果が得られています。app_1 | [{1 testname}]と出力されているのでなんだかいけてそうな気がします。

これだけではピンとこないのでPostgreSQLコンテナに入ってDBを確認してみると、

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
f9a9ed9b3fc8        postgres            "docker-entrypoint.s…"   2 minutes ago       Up 2 minutes        0.0.0.0:5432->5432/tcp   app_postgres_1
$ docker exec -it app_postgres_1 bash
root@f9a9ed9b3fc8:/# psql -U postgres testdb
psql (11.1 (Debian 11.1-1.pgdg90+1))
Type "help" for help.

testdb=# select * from users;
 id |   name   
----+----------
  1 | testname
(1 rows)

PostgreSQLコンテナの内容と開発環境コンテナのmain.go実行時の内容が一致するので正しく値を取得できているようです。

最後にDockerネットワークの状態を確認します。

docker network inspect my-network
[
    {
        "Name": "my-network",
        "Id": "e77a834d8aff49e316e7ab153666232eab35f7fe4350b18be19da1d608125c4b",
        "Created": "2018-12-09T06:25:02.1986525Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.24.0.0/16",
                    "Gateway": "172.24.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "f9a9ed9b3fc8b67232edb76ec4202e0b66ec776dbd0665db7f9ee11341088571": {
                "Name": "app_postgres_1",
                "EndpointID": "6dfd276d55c4352668364de97a588ca5072bf5ee647b8b9dbe5cbdc04d8603f1",
                "MacAddress": "02:42:ac:18:00:03",
                "IPv4Address": "172.24.0.3/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]

Go側のコンテナは実行完了と同時に停止してしまうので、Containersには表示されていませんが、Postgresコンテナがmy-network内に含まれていることがわかります。

色々複雑でしたが、コンテナ間通信ができる開発環境、無事完成です。

今回のポイント

Goのパッケージインポート

ローカルでGoを書く時にも必要なことですが、軽く詰まりました。
main.goの中でGithubからパッケージをインポートしていますが、事前にGOPATHを設定したり、go getでGitリモートリポジトリをダウンロードしておかなければならなかったようです。
Dockerコンテナでもこれは同様なのでDockerfile上でENVやRUNで必要な事前処理を行っていますが、詰まったのはRUNでgo getを実行する部分です。
はじめはCMDで実行していましたが、main.go実行時にこうなります。

app_1  | main.go:6:4: cannot find package "github.com/jinzhu/gorm" in any of:
app_1  |    /usr/local/go/src/github.com/jinzhu/gorm (from $GOROOT)
app_1  |    /go/src/github.com/jinzhu/gorm (from $GOPATH)
app_1  |    /go/src/src/github.com/jinzhu/gorm
app_1  | main.go:7:4: cannot find package "github.com/lib/pq" in any of:
app_1  |    /usr/local/go/src/github.com/lib/pq (from $GOROOT)
app_1  |    /go/src/github.com/lib/pq (from $GOPATH)
app_1  |    /go/src/src/github.com/lib/pq
app_app_1 exited with code 1

パッケージ見つからないよ。という旨のエラーですね。
調べてみたらRUNとCMDでこんな違いがありました。

qiita.com

深くまで追えていませんがイメージ作成時に実行しておかないとmain.goの実行時にはDLが完了できないとかなんでしょうか?

PostgreSQLコンテナで初期DB構築を行う

PostgreSQLコンテナをただ作るのみではもちろんDBの作成はしていないので、接続に失敗します。かといって毎回コンテナを起動するたびに手動で作成するのはDockerの利点を潰してしまっていますよね。

そんなことをしなくて良いようにDockerhubの公式のイメージには便利機能がありました。

If you would like to do additional initialization in an image derived from this one, add one or more .sql, .sql.gz, or *.sh scripts under /docker-entrypoint-initdb.d (creating the directory if necessary).

/docker-entrypoint-initdb.dというディレクトリに.sql.shのファイルを配置しておけば起動時に実行してくれるみたいです。

ということで、docker-compose.ymlvolumes:init以下の1_createdb.sqldocker-entrypoint-initdb.dに配置し、createdbを実行していました。

実は・・・

必要なファイルが揃えば、さも簡単に実行できるかのように書いていますが、1度目の実行はほぼ必ず失敗します。 というのも1度目の実行ではDockerネットワーク上のPostgreSQLコンテナに割り振られるIPがわからないためです。。。 main.goの中で接続先のDBのあるIPを指定しているので、その部分でコケます。 多分何かしらいい方法があるんだと思いますが、現時点では見つからず。。。

良い方法があれば教えていただきたいです。。。

まとめ

今回はDockerネットワークを使ってGoアプリとDBを接続してみました。 アプリのコンテナとDBを分けることができ、なんだかそれらしくなってきたような気がします。 Dockerやdocker-composeがよしなにやってくれる分、上手くいかなかった時にはどこに問題があるのかを見つけるのが大変です。 その分出来上がってしまえば作り直しや複製が簡単にできてしまうので、便利であることに変わりはないですね。

個人的には、「実は・・・」でお伝えした部分はなんとか解決したいものです。。。


◆TECH PLAY
techplay.jp

◆connpass
rakus.connpass.com

Copyright © RAKUS Co., Ltd. All rights reserved.