「Programming」カテゴリーアーカイブ

Operator SDK をインストールしてOperatorを作成する(1/2)

Operator SDKをMacOSにインストールしてgolangでOperatorを作成します。長くなってしまうので、2回に分けます。

  • Operator SDKのインストール
  • Operatorを作成する
    • プロジェクトを作成する
    • CRDを作成する
    • Controllerを追加する

Operator SDKのインストール


MacOSにインストールする方法は、以下の二通りあります。今回はGitHub releaseからのインストールを実施します。インストール方法は、こちらを参考にしています。

  • GitHub releaseからのインストール
  • brewコマンドからのインストール

以下のコマンドでOperator SDKコマンドをダウンロードします。

$ RELEASE_VERSION=v0.8.0
$ curl -OJL https://github.com/operator-framework/operator-sdk/releases/download/${RELEASE_VERSION}/operator-sdk-${RELEASE_VERSION}-x86_64-apple-darwin
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   652  100   652    0     0    860      0 --:--:-- --:--:-- --:--:--   859
100 68.0M  100 68.0M    0     0   997k      0  0:01:09  0:01:09 --:--:-- 1196k
curl: Saved to filename 'operator-sdk-v0.8.0-x86_64-apple-darwin'

Operator SDKのコマンドを/usr/local/bin配下にコピーします。

$ chmod +x operator-sdk-${RELEASE_VERSION}-x86_64-apple-darwin
$ sudo cp operator-sdk-${RELEASE_VERSION}-x86_64-apple-darwin /usr/local/bin/operator-sdk

これでOperator SDKコマンドが利用できるようになりました。

$ operator-sdk version
operator-sdk version: v0.8.0, commit: 78c472461e75e6c64589cfadf577a2004b8a26b3

Operatorを作成する


CRの値に応じてデプロイするイメージを切り替えるhello-operatorを作成します。helloconfigの値にhelloaもしくはhellobが指定されると、該当のイメージをデプロイするOperatorです。実用的ではないですが、まずは動くものを作っていきたいと思います。

helloaとhellobのコンテナイメージは、MicroK8sでIstioによるABテストを試してみる(1/2)で作成しています。よろしければご参照ください。

プロジェクトを作成する


以下のコマンドでプロジェクトを作成します。コマンドを実行すると、Operatorを作成するための雛形が作成されます。

$ operator-sdk new hello-operator
~~省略~~
INFO[0073] Project creation complete.

CRDを作成する


以下のコマンドを実行して、CRD用のAPIを作成します。

$ cd hello-operator/
$ operator-sdk add api --api-version=hello.example.com/v1alpha1 --kind=Hello

pkg/apis/hello/v1alpha1/hello_types.goが作成されるので、CRで設定できる設定項目を定義します。今回は、6行目にHelloConfig string `json:”helloconfig”`を加えただけになります。

~~省略~~
type HelloSpec struct {
    // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
    // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file
    // Add custom validation using kubebuilder tags: https://book.kubebuilder.io/beyond_basics/generating_crd.html
    HelloConfig string `json:"helloconfig"`
}
~~省略~~

変更を加えたら、以下のコマンドで必要なコードを再生成します。

$ operator-sdk generate k8s
INFO[0001] Running deepcopy code-generation for Custom Resource group versions: [hello:[v1alpha1], ] 
INFO[0006] Code-generation complete.

Controllerを追加する


以下のコマンドを実行して、Controllerのテンプレートを作成します。作成が完了すると、pkg/controller/hello/hello_controller.go が作成されています。

$ operator-sdk add controller --api-version hello.example.com/v1alpha1 --kind Hello
INFO[0000] Generating controller version hello.example.com/v1alpha1 for kind Hello. 
INFO[0000] Created pkg/controller/hello/hello_controller.go 
INFO[0000] Created pkg/controller/add_hello.go          
INFO[0000] Controller generation complete.

hello_controller.goは、定義したCRに対してどのような制御を行うか記述します。以下の部分を編集しました。HelloConfigの値を取得し、イメージ名に代入しているだけになります。

~~省略~~
func newPodForCR(cr *hellov1alpha1.Hello) *corev1.Pod {
    helloconfig := cr.Spec.HelloConfig
    labels := map[string]string{
        "app": cr.Name,
    }
    return &corev1.Pod{
        ObjectMeta: metav1.ObjectMeta{
            Name:      cr.Name + "-pod",
            Namespace: cr.Namespace,
            Labels:    labels,
        },
        Spec: corev1.PodSpec{
            Containers: []corev1.Container{
                {
                    Name:    "hello",
                    Image:   "localhost:32000/" + helloconfig,
                },
            },
        },
    }
}
~~省略~~

今日はここまでとして、続きのビルドとデプロイは明日実施します。

以上です!

golangでcssファイルを読み込む

今日はgolangでcssファイルを読み込む方法について紹介します。

以下の記事で作成したwebコンテナのindex.htmlからcssファイルを利用する方法について紹介します。

GoとMongoDBのコンテナでフロントとバックエンドを分けてみる

  • cssファイルを作成する
  • htmlファイルを編集する
  • main.goを編集する
  • アプリケーションを起動して確認する

cssファイルを作成する


オレンジ色のボタンのフォーマットをcssファイル(button.css)で作成します。cssファイルが主役ではないので、説明は省略します。

.btn-push {
  display: inline-block;
  width: 180px;
  text-align: center;
  background-color: #ffa300;
  font-size: 16px;
  color: #FFF;
  text-decoration: none;
  font-weight: bold;
  padding: 10px 24px;
  border-radius: 4px;
  border-bottom: 4px solid #d37800;
}

.btn-push:active {
    transform: translateY(4px);
    border-bottom: none;
}

以下のディレクトリに配置します。

$ tree web
web
├── Dockerfile
└── src
    ├── main.go
    ├── public
    │   └── css
    │       └── button.css
    └── templates
        ├── index.html
        └── layout.html

htmlファイルを編集する


button.cssファイルを読み込んで、配列データにボタンフォーマットを適用するindex.htmlを作成します。

linkタグでcssをリンクしている訳ですが、ファイルパスが/static/css/button.cssであることに注意してください。配置したパスと異なっています。これはmain.goの記述で変更することができます。今回は分かりやすいようにパスを変えてみました。

{{ define "content" }}
  <link href="/static/css/button.css" rel="stylesheet">
  {{ range .Categories }}
    <a href="index.html" class="btn-push" style="margin: 10px 20px">{{ .Name }}</a>
  {{ end }}
{{ end }}

main.goを編集する


webコンテナのmain.goを以下に編集します。気づいた方もいらっしゃると思いますが、gorilla/muxをやめました。ビルドする度にパッケージをインストールするのを省略するためです。

大きな変更点は、http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("public/"))))を追加した点です。publicディレクトリ配下の静的ファイルを読み込んで、/static/で公開しています。従って、htmlファイルでcssをリンクするときは、公開するパスになります。

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "io/ioutil"
    "os"
        "html/template"
)

type category struct {
    Categories []struct{
      Name string `json:"name"`
    }`json:"categories"`
}

func main() {
    //public配下の静的ファイルを読み込み
    http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("public/"))))
    //ハンドラーにindexを登録
    http.Handle("/", index)

    //サーバー起動
    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatal("ListenAndServe:", nil)
    }
}

var index = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    url := "http://searcher:8082/public"
    req, _ := http.NewRequest("GET", url, nil)
    client := new(http.Client)
    resp, _ := client.Do(req)
    defer resp.Body.Close()
    byteArray, _ := ioutil.ReadAll(resp.Body)
    var categories category
    if err := json.Unmarshal(byteArray, &categories); err != nil {
            log.Fatal(err)
            os.Exit(1)
    }
  generateHTML(w, categories, "layout", "index")
})

func generateHTML(writer http.ResponseWriter, data interface{}, filenames ...string) {
    var files []string
    for _, file := range filenames {
        files = append(files, fmt.Sprintf("templates/%s.html", file))
    }

    templates := template.Must(template.ParseFiles(files...))
    templates.ExecuteTemplate(writer, "layout", data)
}

アプリケーションを起動して確認する


ここからアプリケーションを起動します。

1. Dockerfileを編集する

gorilla/muxのパッケージインストール行を削除しています。

FROM golang:latest

# コンテナ作業ディレクトリの変更
WORKDIR /go/src/web
# ホストOSの ./src の中身を作業ディレクトリにコピー
COPY ./src .
RUN go build -o web
# ウェブアプリケーション実行コマンドの実行
CMD ["./web"]

2. コンテナイメージを作成する

$ docker build -t web:latest web/

3. コンテナを起動する

docker-composeコマンドでアプリケーションを起動します。searcherとdbのイメージはGoとMongoDBのコンテナでフロントとバックエンドを分けてみるを参照してください。

$ docker-compose up -d
Creating network "wishy_default" with the default driver
Creating db ... done
Creating searcher ... done
Creating web      ... done

4. ブラウザで確認する

http://localhost:8080をブラウザに直接入力すると、以下のように配列データがボタンとして表示されます。

以上です!

GoとMongoDBのコンテナでフロントとバックエンドを分けてみる

GoとMongoDBを使って、DBのデータを表示する簡単なWebアプリケーションを作成します。本記事は、過去に投稿したコンテナでフロントとバックエンドを分けてみるで作成したWebアプリケーションのデータ格納先をMongoDBにしたものです。

以下の流れで紹介します。

  • dbコンテナを作成する
  • searcherコンテナを作成する
    • ソースコードを作成する
    • コンテナイメージを作成する
  • webコンテナを作成する
  • アプリケーションを起動する

dbコンテナを作成する


以下の記事で作成したMongoDBのコンテナを利用します。

mongodbコンテナの起動時に初期データを登録してみる

コンテナ起動時に、recommendデータベースcategoriesコレクションに以下のデータが保管されています。

[
  { "name": "スポーツ" },
  { "name": "文化" },
  { "name": "娯楽" },
  { "name": "食事" },
  { "name": "お酒" },
  { "name": "イベント" },
  { "name": "ドライブ" },
  { "name": "文化" }
]

searcherコンテナを作成する


dbコンテナからデータを取得し、json形式で出力するsearcherコンテナを作成します。

ソースコードを作成する


MongoDBからデータを取得し、json形式で出力するgolangを作成します。
MongoDBからデータを取得する方法は、mongodbからデータを抽出するgolangを書くで解説しています。

package main

import (
    "fmt"
    "log"
    "net/http"
    "encoding/json"
    "time"
    "context"
    "os"
    "github.com/gorilla/mux"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
    "go.mongodb.org/mongo-driver/bson"
)

type category struct {
    Categories []struct{
      Name string `json:"name"`
    }`json:"categories"`
}

func main() {
    r := mux.NewRouter()
    // localhost:8082/publicでpublicハンドラーを実行
    r.Handle("/public", public)

    //サーバー起動
    if err := http.ListenAndServe(":8082", r); err != nil {
        log.Fatal("ListenAndServe:", nil)
    }
}

var public = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    //MongoDBの認証情報
    credential := options.Credential{
      Username: "root",
      Password: "password",
    }

    //MongoDBへの接続
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    connect, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://db:27017").SetAuth(credential))
    defer connect.Disconnect(ctx)

    //collectionの取得
    collection, err := connect.Database("recommend").Collection("categories").Find(context.Background(), bson.M{})
    if err != nil { log.Fatal(err) }
    var results []bson.M
    if err = collection.All(context.TODO(), &results); err != nil {
      log.Fatal(err)
    }

    //取得したコレクションの_idエントリを削除
    for _, entry_map := range results {
      delete(entry_map, "_id")
    }

    //bson.Mのデータをjson形式に変換
    json_result, err := json.Marshal(results)
    if err != nil {
        fmt.Println("JSON marshal error: ", err)
        return
    }

    //category構造体にJson形式の値を代入
    var output category
    if err := json.Unmarshal(json_result, &output.Categories); err != nil {
        log.Fatal(err)
        os.Exit(1)
    }

    //読み込んだJsonの表示
    json.NewEncoder(w).Encode(output)

})

コンテナイメージを作成する


Dockerfileを書いてコンテナイメージをビルドします。

ディレクトリ構造は以下の通りです。

$ tree .
.
├── Dockerfile
└── src
    └── main.go

Dockerfileは以下の通りです。

FROM golang:latest

# コンテナ作業ディレクトリの変更
WORKDIR /go/src/searcher
# ホストOSの ./src の中身を作業ディレクトリにコピー
COPY ./src .
RUN go get -u github.com/gorilla/mux
RUN go get -u "go.mongodb.org/mongo-driver/mongo"
RUN go build -o searcher
# ウェブアプリケーション実行コマンドの実行
CMD ["./searcher"]

以下のコマンドで作成します。

$ docker build -t searcher:latest .

webコンテナを作成する


webコンテナは以下の記事で作成したwebコンテナを流用します。以下の記事でフロントとバックエンドを分けていたので、修正する必要がありません。

goコンテナでフロントとバックエンドを分けてみる

アプリケーションを起動する


docker-composeを記述してコンテナアプリケーションを実行します。

docker-composeは以下の通りです。

version: '3'
services:
  db:
    container_name: db
    image: mongo-db:latest
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: password
    ports:
      - "27017:27017"
  searcher:
    container_name: searcher
    image: searcher:latest
    depends_on:
      - db
    links:
      - db
    ports:
      - "8082:8082"
  web:
    container_name: web
    image: web:latest
    depends_on:
      - searcher
    links:
      - searcher
    ports:
      - "8080:8080"

以下のコマンドを実行して起動します。

$ docker-compose up -d

localhost:8080にアクセスすると以下の結果が得られます。

以上です!

MongoDBからデータを抽出するgolangを書く

以下の記事でMongoDBをコンテナとして実行したので、golangでそのデータを抽出します。

mongodbコンテナの起動時に初期データを登録してみる

  • MongoDBドライバーをダウンロードする
  • ソースコードを書く
  • 実行する

MongoDBドライバーをダウンロードする


まずは以下のコマンドでMongoDBのドライバーをダウンロードします。これはGoがサポートするドライバーで、ドキュメントはこちらになります。

以下のコマンドでダウンロードします。

$ go get -u "go.mongodb.org/mongo-driver/mongo"

ソースコードを書く


main.goを以下に記述しました。
/publicに対してhttpリクエストを受け付けると、MongoDBコンテナにアクセスして、recommendデータベースのcategoriesコレクションのデータを取得し、表示するプログラムです。
MongoDBコンテナの起動及び初期データ登録は、こちらの記事をご参照ください。

package main

import (
    "fmt"
    "log"
    "net/http"
    "time"
    "context"
    "github.com/gorilla/mux"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
    "go.mongodb.org/mongo-driver/bson"
)

func main() {
    r := mux.NewRouter()
    // localhost:8082/publicでpublicハンドラーを実行
    r.Handle("/public", public)

    //サーバー起動
    if err := http.ListenAndServe(":8082", r); err != nil {
        log.Fatal("ListenAndServe:", nil)
    }
}

var public = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    //MongoDBの認証情報
    credential := options.Credential{
      Username: "root",
      Password: "password",
    }

    //MongoDBへの接続
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    connect, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017").SetAuth(credential))
    defer connect.Disconnect(ctx)

    //collectionの取得
    collection, err := connect.Database("recommend").Collection("categories").Find(context.Background(), bson.M{})
    if err != nil { log.Fatal(err) }
    var results []bson.M
    if err = collection.All(context.TODO(), &results); err != nil {
      log.Fatal(err)
    }

    //取得したcollectionの表示
    for _, output := range results {
      fmt.Println( output["name"] )
    }
})

実行する


1. 以下のコマンドでgoを実行します。

$ go run .

2. 以下のコマンドでgoの/publicにhttpリクエストを送ります。

$ curl localhost:8082/public

そうすると、以下の出力が表示されます。
コレクションは_idとnameのmapが配列となっていて、その中でnameを抜き出しています。

スポーツ
娯楽
食事
お酒
文化
ドライブ
文化
イベント

以上です!今日は少し雑ですいません。。

Goの並行処理について学ぶ

Goプログラミングにおける並行処理について紹介したいと思います。以下の流れで紹介します。

  • Goの並列処理機能について知る
    • ゴルーチンとは
    • チャネルとは
  • 並列処理機能を使ってみる
    • ゴルーチンを使う
    • チャネルを使う

Goの並列処理機能について知る


並行処理とは、複数の処理を同時に実行することです。この処理を実現する機能として、Goにはゴルーチンチャネルがあります。

ゴルーチンとは


ゴルーチンは、複数の処理を同時に実行する関数です。関数を呼び出す処理の先頭にgoをつけることで、呼び出された関数はゴルーチンとして実行されます。

go 関数名

チャネルとは


チャネルは、ゴルーチン間でデータの受け渡しを行う変数の型です。キュー(FIFO)の考え方で、送信側が送った順に受信側がデータを受け取ります。言い換えると、受信側がデータを受け取らない限りデータは溜まっていきます。変数の型の前にchanをつけることで利用することができます。

chan 変数の型

並列処理機能を使ってみる


では、ゴルーチンとチャネルを使って並列処理を体感します。

ゴルーチンを使ってみる


まずはゴルーチンを使ったプログラムを作成します。

数字とアルファベットを26個ずつ表示する関数を作成し、逐次処理とゴルーチンを使った並行処理の出力結果を比較します。

package main

import "fmt"
import "time"

func createNum(){
  for i := 0; i <= 26; i++ {
    fmt.Printf( "%d, " , i )
    time.Sleep( 1000 )
  }
}

func createChar(){
  for i := 'A'; i <= 'Z'; i++ {
    fmt.Printf( "%c, " , i )
    time.Sleep( 1000 )
  }
}

func main(){
  fmt.Println( "--------逐次処理--------" )
  createNum()
  createChar()
  fmt.Println( "\n------------------------" )
  fmt.Println( "--------並行処理--------" )
  go createNum()
  go createChar()
  fmt.Println( "\n------------------------" )
}

実行してみますが、何も表示されません。。ゴルーチンの処理が実行される前にプログラム自体が終了してる可能性があります。

$ go run main.go 
--------逐次処理--------
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, 
------------------------
--------並行処理--------

------------------------

処理を待たせるためには、sleepかWaitGroupを使うのがいいとのことでした。せっかくなのでWaitGroup使って以下のようにしました。逐次処理にWaitGroup入れる意味はありませんが。。。

package main

import "fmt"
import "sync"
import "time"

func createNum(wg *sync.WaitGroup){
  for i := 0; i <= 26; i++ {
    fmt.Printf( "%d, " , i )
    time.Sleep( 1000 )
  }
  wg.Done()
}

func createChar(wg *sync.WaitGroup){
  for i := 'A'; i <= 'Z'; i++ {
    fmt.Printf( "%c, " , i )
    time.Sleep( 1000 )
  }
  wg.Done()
}

func main(){
  var wg sync.WaitGroup
  fmt.Println( "--------逐次処理--------" )
  wg.Add( 2 )
  createNum( &wg )
  createChar( &wg )
  wg.Wait()
  fmt.Println( "\n------------------------" )
  fmt.Println( "--------並行処理--------" )
  wg.Add( 2 )
  go createNum( &wg )
  go createChar( &wg )
  wg.Wait()
  fmt.Println( "\n------------------------" )
}

今度は正しく表示されました。

$ go run main.go 
--------逐次処理--------
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, 
------------------------
--------並行処理--------
0, A, 1, B, 2, C, 3, D, E, 4, F, 5, G, 6, H, 7, I, J, K, 8, L, M, 9, N, O, 10, P, 11, 12, Q, 13, 14, 15, R, 16, S, 17, 18, 19, T, U, V, W, X, 20, Y, 21, Z, 22, 23, 24, 25, 26, 
------------------------

チャネルを使ってみる


チャネルを使ってデータの受け渡しをしてみたいと思います。

createNum関数から受け取った値をcreateChar関数で表示するソースコードを作成しました。

package main

import "fmt"
import "sync"

func createNum(c chan int, wg *sync.WaitGroup){
  for i := 1; i <= 5; i++ {
    c <- i
    fmt.Printf( "create %d\n" , i )
  }
  wg.Done()
}

func createChar(c chan int, wg *sync.WaitGroup){
  for i := 'A'; i <= 'E'; i++ {
    num := <-c
    fmt.Printf( "%d = %c\n" , num, i )
  }
  wg.Done()
}

func main(){
  var wg sync.WaitGroup
  c := make( chan int )
  fmt.Println( "--------並行処理--------" )
  wg.Add( 2 )
  go createNum( c, &wg )
  go createChar( c, &wg )
  wg.Wait()
  fmt.Println( "\n------------------------" )
}

実行してみると、下記のようになりました。createNumが送った値が、createChar関数で正しい順序で表示されていることがわかります。これはFIFOの性質で、どんなに並列実行しようとも最初に格納された値を抜き出しています。出力されるタイミングが前後しているのは誤差なので、あまり気にしなくて大丈夫そうです。

--------並行処理--------
create 1
1 = A
2 = B
create 2
create 3
3 = C
4 = D
create 4
create 5
5 = E

------------------------

以上です。パッケージとか色々準備する必要がなかったので、簡単に使える印象でした。皆様もお試しあれ〜。

Goコンテナでフロントとバックエンドを分けてみる

今日はGo言語で簡単なwebアプリケーションを作成します。webとdbにコンテナを分けて、webページにdbから取得したデータを表示したいと思います。以下のように、マイクロサービスを意識して、コンテナ間通信をhttpリクエストで行いたいと思います。

以下の流れで作成します。

  • dbを作成する
  • webを作成する
    • ソースコードを作成する
      • main.goを作成する
      • layout.htmlを作成する
      • index.htmlを作成する
    • Dockerfileを作成する
  • コンテナを実行する
    • コンテナイメージの作成
    • Docker Composeファイルを作成する
    • コンテナを実行する

dbを作成する

 dbコンテナは以下の記事で作成したものを利用します。ここで作成したのは、httpリクエストを行うとjsonファイルを読み込んで出力するコンテナです。本来であればデータベースも作成しますが、今回はhttpリクエストでやり取りするのをメインにしているので、データはファイルにします。データベースを使うのはまた記事書きたいと思います。

go言語でjsonファイルを読み込んで表示してみる

webを作成する

ソースコードを作成する

main.goを作成する

 ポート8080で受け付けると、dbコンテナから取得したjsonコードをhtml形式で表示するwebのソースコード(main.go)を以下に示します。

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "github.com/gorilla/mux"
    "io/ioutil"
    "os"
        "html/template"
)

type category struct {
    Categories []struct{
      Name string `json:"name"`
    }`json:"categories"`
}

func main() {
    r := mux.NewRouter()
    r.Handle("/", index)

    //サーバー起動
    if err := http.ListenAndServe(":8080", r); err != nil {
        log.Fatal("ListenAndServe:", nil)
    }
}

var index = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    url := "http://db:8082/public"
    req, _ := http.NewRequest("GET", url, nil)
    client := new(http.Client)
    resp, _ := client.Do(req)
    defer resp.Body.Close()
    byteArray, _ := ioutil.ReadAll(resp.Body)
    var categories category
    if err := json.Unmarshal(byteArray, &categories); err != nil {
            log.Fatal(err)
            os.Exit(1)
    }
    generateHTML(w, categories, "layout", "index")
})

func generateHTML(writer http.ResponseWriter, data interface{}, filenames ...string) {
    var files []string
    for _, file := range filenames {
        files = append(files, fmt.Sprintf("templates/%s.html", file))
    }

    templates := template.Must(template.ParseFiles(files...))
    templates.ExecuteTemplate(writer, "layout", data)
}

 index = http.HandlerFuncでは、dbコンテナから取得したjsonコードを構造体categoryの変数categoriesに代入し、generateHTML関数を呼び出します。generateHTMLは引数で与えられた名前のhtmlファイル、ここではlayout.htmlとindex.htmlを使ってページを生成します。

layout.htmlを作成する

 generateHTML関数は、layout.htmlとindex.htmlを使ってページを生成します。layout.htmlは以下の通り、body以外のheaderのみを記述しています。layoutを作成することで、それぞれのhtmlのheaderを統一し、ソースコードの重複を回避できます。

{{ define "layout" }}

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Test</title>
  </head>
  <body>
    {{ template "content" . }}
  </body>
</html>

{{ end }}

index.htmlを作成する

 このlayoutを使ったindex.htmlが以下になります。{{ define "content" }} と{{ end }}で囲んだhtml文がlayoutの{{ template "content" . }}、つまりはページの本文に記述されることになります。

{{ define "content" }}
  <center>
    {{ range .Categories }}
        {{ .Name }}<br>
    {{ end }}
  </center>
{{ end }}

Dockerfileを作成する

 コンテナイメージを作成するのに必要なDockerfileを作成します。ビルド用のディレクトリ構造は以下の通りです。

$ tree web
web
├── Dockerfile
└── src
    ├── main.go
    └── templates
        ├── index.html
        └── layout.html

 src配下に作成したソースコードを配置します。このディレクトリ構造に合わせて作成したDockerfileが以下になります。

FROM golang:latest

# コンテナ作業ディレクトリの変更
WORKDIR /go/src/web
# ホストOSの ./src の中身を作業ディレクトリにコピー
COPY ./src .
RUN go get -u github.com/gorilla/mux
RUN go build -o web
# ウェブアプリケーション実行コマンドの実行
CMD ["./web"]

コンテナを実行する

コンテナイメージを作成する

 上記で作成したwebとdbのコンテナイメージを以下のコマンドで作成します。

$ docker build -t web:latest web/
$ docker build -t db:latest db/

Docker Composeファイルを作成する

 webとdbのコンテナイメージからコンテナを実行しますが、webとdb間は通信できる必要があります。そこでdocker-compose.yamlファイルを作成します。以下にその内容を示します。

version: '3'
services:
  db:
    container_name: db
    image: db:latest
    ports:
      - "8082:8082"
  web:
    container_name: web
    image: web:latest
    depends_on:
      - db
    links:
      - db
    ports:
      - "8080:8080"

 docker-compose.yamlはservicesブロックの中に実行するコンテナ毎にブロックを作成します。今回はwebとdbコンテナだけなので、webブロックとdbブロックを作成しました。
 webのmain.goにurl := "http://db:8082/public"とurlを直接指定しました。従ってwebコンテナはホスト名dbをdbコンテナに名前解決できる必要があります。それを定義しているのがlinksです。

コンテナを実行する

 docker-compose.yamlファイルからコンテナを実行するにはdocker-composeコマンドを利用します。docker-compose.yamlを置いたディレクトリで以下のコマンドを実行します。

$ docker-compose up -d
Creating db ... done
Creating web ... done
$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED              STATUS              PORTS                    NAMES
da224d318f33        web:latest          "./web"             About a minute ago   Up About a minute   0.0.0.0:8080->8080/tcp   web
b245c4eb8320        db:latest           "./searcher"        About a minute ago   Up About a minute   0.0.0.0:8082->8082/tcp   db

 docker-compose.yamlで定義した内容で実行されていることがわかります。ブラウザでhttp://localhost:8080/にアクセスしてみましょう。jsonの中身が表示されていることがわかります。

以上です!なんとなくGo言語がわかってきたようなきてないような。。ちゃんと仕組みから理解した方が良さそうなので、またそういう記事も書きたいと思います!ソースコードが汚いのは許してください。。。

GoでJsonファイルを読み込んで表示してみる

最近Go言語を使ってjsonファイルを読み込んで表示してみました。本記事ではその方法について紹介します。最近はアプリケーションをコンテナで実行しているので、コンテナベースでの実行とします。

  1. jsonファイルを作成する
  2. jsonファイルを読み込んで出力するソースコードを作成する
  3. コンテナで実行して出力を確認する

1. jsonファイルを作成する

 用意したjsonファイル(categories.json)を以下に示します。jsonらしくはない、ただの配列になってますが。。

{
  "categories": [
    { "name": "スポーツ" },
    { "name": "文化" },
    { "name": "娯楽" },
    { "name": "食事" },
    { "name": "お酒" },
    { "name": "イベント" },
    { "name": "ドライブ" },
    { "name": "文化" }
  ]
}

2. jsonファイルを読み込んで出力するソースコードを作成する

 Goのソースコード(main.go)を以下に記します。public関数がjsonファイルを読み込んで表示する処理を行います。注意点としては、jsonファイルを読み込む時に構造体を利用することです。ここでは、postという構造体を定義していて、categories.jsonの形式に合わせています。json.Unmarshal(categories, &output)が構造体postの変数であるoutputに対して、jsonファイルから取得した内容を代入しています。

package main

import (
    "encoding/json"
    "fmt"
    "searcher/data"
    "log"
    "net/http"
    "github.com/gorilla/mux"
    "io/ioutil"
    "os"
)

type post struct {
    Categories []struct{
      Name string `json:"name"`
    }`json:"categories"`
}

func main() {
    r := mux.NewRouter()
    // localhost:8082/publicでpublicハンドラーを実行
    r.Handle("/public", public)

    //サーバー起動
    if err := http.ListenAndServe(":8082", r); err != nil {
        log.Fatal("ListenAndServe:", nil)
    }
}

var public = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   //Jsonファイルの読み込み
    categories, err := ioutil.ReadFile("./data/categories.json")
    if err != nil {
        fmt.Println(err.Error())
    }
    var output post
    if err := json.Unmarshal(categories, &output); err != nil {
        log.Fatal(err)
        os.Exit(1)
    }
    //読み込んだJsonの表示
    json.NewEncoder(w).Encode(output)
})

3. コンテナで実行してJsonの表示を確認する

 ビルド用ディレクトリ構造は以下の通りです。

$ tree .
.
├── Dockerfile
└── src
    ├── categories.json
    └── main.go

 Dockerfileは以下の通りです。

FROM golang:latest

WORKDIR /go/src/searcher
COPY ./src .

RUN go get -u github.com/gorilla/mux
RUN go build -o searcher

CMD ["./searcher"]

 以下のコマンドでビルドします。

$ docker build -t test:latest .
Sending build context to Docker daemon  19.46kB
Step 1/6 : FROM golang:latest
 ---> 2421885b04da
Step 2/6 : WORKDIR /go/src/searcher
 ---> Using cache
 ---> 588d44459848
Step 3/6 : COPY ./src .
 ---> e863f9711371
Step 4/6 : RUN go get -u github.com/gorilla/mux
 ---> Running in a66f5891cc12
Removing intermediate container a66f5891cc12
 ---> 7a098828debd
Step 5/6 : RUN go build -o searcher
 ---> Running in 79cf40f885b3
Removing intermediate container 79cf40f885b3
 ---> 78cef5ba6945
Step 6/6 : CMD ["./searcher"]
 ---> Running in 03373a52a96a
Removing intermediate container 03373a52a96a
 ---> 47e4f7d48325
Successfully built 47e4f7d48325
Successfully tagged test:latest
$ docker images test
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
test                latest              47e4f7d48325        5 minutes ago       819MB

 以下のコマンドでコンテナを実行し、出力を確認します。

$ docker run -d --name test -p 8082:8082 test
8f8b34416854890b1cd527a2f40b93758d289a59db79c8c46559ad3a2d48265e

$ curl localhost:8082/public
{"categories":[{"name":"スポーツ"},{"name":"文化"},{"name":"娯楽"},{"name":"食事"},{"name":"お酒"},{"name":"イベント"},{"name":"ドライブ"},{"name":"文化"}]}