DevelopBuf

開発関連雑多メモ

docker-composeでsecretsを設定してcontainerで利用する

概要

docker-composeでビルドプロセスにてsecretsを利用する際にうまく設定できなくてハマったのでメモ

詳細

1. ハマりポイント1 environmentを利用する場合はまずdocker-compose buildするホストで環境変数として設定しておき同じ名前を設定する必要がある。

以下の例だとホスト側の環境変数DB_PASSWORD_OTHER を設定しておく必要がある。 書き方は公式のサンプルと合わせた。 https://docs.docker.com/compose/use-secrets/#advanced

version: '3.8'

services:
  my_app:
    build: 
      context: ./my_app
      secrets:
        - db_password_env
secrets:
  db_password_env:
    environment: DB_PASSWORD_ENV

2. ハマりポイント2 secretsの定義をbuildセクションに記載しないといけない

buildセクションと同階層に記載してしまうとランタイムでのみsecretsが有効になる。buildセクションでネストになるように定義しないといけない。

github.com

成果物

github.com

Go,GORM,MySQL,Dockerで開発環境を構築する

概要

Go,MySQLでバックエンドを構築する際にDockerで完結するような開発環境を作りたいなと思い調べつつ実装してみました。

要件としては下記の通り。

  • Go,MySQLの開発環境がdocker-composeで構成されていること
  • ORMのGORMを用いてDBにread,writeできること
  • データが永続化されること(次回のコンテナ起動時に前回データが残っている状態)

環境

$ go version
go version go1.14.2 darwin/amd64

$ docker version
Client: Docker Engine - Community
 Version:           19.03.8
 API version:       1.40
 Go version:        go1.12.17
 Git commit:        afacb8b
 Built:             Wed Mar 11 01:21:11 2020
 OS/Arch:           darwin/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.8
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.12.17
  Git commit:       afacb8b
  Built:            Wed Mar 11 01:29:16 2020
  OS/Arch:          linux/amd64
  Experimental:     true
 containerd:
  Version:          v1.2.13
  GitCommit:        7ad184331fa3e55e52b890ea95e65ba581ae3429
 runc:
  Version:          1.0.0-rc10
  GitCommit:        dc9208a3303feef5b3839f4323d9beb36df0a9dd
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683

実装

APIのベースとルーティング実装

まずはGo Modulesでライブラリ管理してhttpリクエストをルーティングするような処理をかいていきます。

# アプリケーションのディレクトリ作成
$ mkdir sample-go-mysql-docker
$ cd sample-go-mysql-docker
$ touch main.go

# moduleを管理するファイル作成
$ go mod init

# routerをgetしてくる
$ go get -u "github.com/gorilla/mux"

endpointを定義して疎通確認します。

main.go

package main

import (
    "log"
    "net/http"

    "github.com/gorilla/mux"
)

func main() {
    router := mux.NewRouter()
    // 商品を追加するendpoint
    router.HandleFunc("/products", createProduct).Methods("POST")
    // 最新の商品情報を取得するendpoint
    router.HandleFunc("/products/latest", getProductLatest).Methods("GET")

    if err := http.ListenAndServe(":8080", router); err != nil {
        log.Fatal(err)
    }
}

func createProduct(w http.ResponseWriter, r *http.Request) {
    // TODO 商品を追加するような処理をかく
    w.Header().Set("Content-Type", "application/json")
    w.Write([]byte("success create product"))
}

func getProductLatest(w http.ResponseWriter, r *http.Request) {
    // TODO 最新の商品を取得するような処理をかく
    w.Header().Set("Content-Type", "application/json")
    w.Write([]byte("success get product latest"))
}

curlして下記のように返って来ればok

$ go run main.go
$ curl -X POST http://localhost:8080/products
success create product%
$ curl http://localhost:8080/products/latest
success get product latest%

DB構築

docker-composeでDBを構築していく。

$ touch docker-compose.yml

mysqlのcontainerを定義します。

docker-compose.yml

version: '3'

services:
  db:
    image: mysql:8.0
    ports:
      - '3306:3306'
    volumes:
      - ./docker/db/init:/docker-entrypoint-initdb.d
      - ./docker/db/my.cnf:/etc/mysql/conf.d/my.cnf
      - ./tmp/db/mysql_data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_USER: user
      MYSQL_PASSWORD: password

dbを初期化するためのsql./docker/db/init 配下に作成します。 ここで注意するのが docker-entrypoint-initdb.d が毎回実行されるのではなく /var/lib/mysql が存在しないようなケース(初回起動時)などに実行されるので頻繁にinit用のsqlを書き換えたりしていると反映されなくてハマる。その場合は一度コンテナを削除するなどする。

docker/db/init/init_db.sql

SET CHARSET utf8mb4;
DROP DATABASE IF EXISTS sample;
CREATE DATABASE IF NOT EXISTS sample DEFAULT CHARACTER SET utf8mb4;

日本語を扱う場合は4バイト文字列を扱ったりする可能性があるので文字コードの設定を変更しておかないといけない。そのためにcharsetをutf8mb4にしてcollationはutf8mb4_general_ciにしたmy.cnfをマウントして反映しておく。

docker/db/my.cnf

[mysqld]
# character
character-set-server=utf8mb4
collation-server=utf8mb4_general_ci

[client]
default-character-set=utf8mb4

docker-composeからコンテナ起動してpsでプロセス確認するとmysqlが起動できていることがわかります。

$ docker-compose up -d
$ docker ps

APIサーバーのdocker定義追加

先ほどは go run コマンドを使ってアプリケーションを起動しましたがdocker-composeコマンドを使って起動できるようにします。

apiのDockerfileを定義します。

docker/api/Dockerfile

FROM golang:1.14.2

ENV GO111MODULE=on
ENV APP_DIR=/go/src/github.com/konchanxxx/sample-go-mysql-docker

WORKDIR $APP_DIR

# APIサーバーからDBを参照する際のデバッグ等に使えるのでclientを入れておく
RUN apt-get update && apt-get install -y default-mysql-client

COPY . .

RUN go mod download

docker-composeファイルのservicesにapiを追加します。この際よくlinksを定義する記事を見かけたりするのですがdocker-composeで記述した各サービスはデフォルトで一つのnetworkに設定されるため書かなくて良いのとlinksは廃止される見込みのため記述していません。

version: '3'

services:
  api:
    build:
      context: .
      dockerfile: docker/api/Dockerfile
    tty: true
    depends_on:
      - db
    ports:
      - '8080:8080'
    command: sh ./bin/run.sh
  db:
    image: mysql:8.0
    ports:
      - '3306:3306'
    volumes:
      - ./docker/db/init:/docker-entrypoint-initdb.d
      - ./docker/db/my.cnf:/etc/mysql/conf.d/my.cnf
      - ./tmp/db/mysql_data:/var/lib/mysql
    environment:
      MYSQL_DATABASE: sample
      MYSQL_ROOT_PASSWORD: password
      MYSQL_USER: user
      MYSQL_PASSWORD: password

run.shはmysqlが起動するまでwaitします。

bin/run.sh

# !/bin/bash

until mysqladmin ping -h db --silent; do
  echo 'waiting for connection mysql...'
  sleep 2
done

echo 'Database is launched!!'
exec go run main.go

APIサーバーとDBの繋ぎこみ

APIサーバーとDBを繋いでいきます。ORMはGORMを利用します。 エラーハンドリングは簡易的にpanicで処理しているので実際にアプリケーションを実装する際はエラーレスポンス等を返す必要があります。

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"

    "github.com/gorilla/mux"

    _ "github.com/go-sql-driver/mysql"
    "github.com/jinzhu/gorm"
)

const (
    dbDialect = "mysql"
)

func init() {
    conn, err := getConn()
    if err != nil {
        panic(err)
    }
    defer conn.Close()

    // GORMのauto migrationを利用する。rollbackできないので本番環境等は別でmigrationの仕組みを入れる必要がある
    conn.AutoMigrate(&Product{})
}

func main() {
    router := mux.NewRouter()
    // 商品を追加するendpoint
    router.HandleFunc("/products", createProduct).Methods("POST")
    // 最新の商品情報を取得するendpoint
    router.HandleFunc("/products/latest", getProductLatest).Methods("GET")

    if err := http.ListenAndServe(":8080", router); err != nil {
        log.Fatal(err)
    }
}

// Product は商品
// GORMのカラム名は指定しなければ自動でスネークケースで生成してくれる
type Product struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Price int    `json:"price"`
}

func createProduct(w http.ResponseWriter, r *http.Request) {
    conn, err := getConn()
    if err != nil {
        panic(err)
    }
    defer conn.Close()

    err = conn.Create(&Product{Name: "switch", Price: 29980}).Error
    if err != nil {
        panic(err)
    }

    w.WriteHeader(http.StatusCreated)
    w.Header().Set("Content-Type", "application/json")
}

func getProductLatest(w http.ResponseWriter, r *http.Request) {
    conn, err := getConn()
    if err != nil {
        panic(err)
    }
    defer conn.Close()

    p := &Product{}
    err = conn.Last(p).Error
    if err != nil {
        panic(err)
    }

    res, err := json.Marshal(p)
    if err != nil {
        panic(err)
    }

    w.Header().Set("Content-Type", "application/json")
    w.Write(res)
}

func getConn() (*gorm.DB, error) {
    // hostはdocker-composeで指定したサービス名になる
    connectionString := fmt.Sprintf(
        "%s:%s@tcp(%s:%s)/%s?parseTime=true&loc=Local&charset=utf8mb4",
        "user",
        "password",
        "db",
        "3306",
        "sample",
    )

    return gorm.Open(dbDialect, connectionString)
}

起動確認

アプリケーションのファイルを修正したのでdocker-compose buildでイメージをビルドし直してからコンテナ起動する。curlでデータがDBに生成されていることがわかる。

$ docker-compose build
$ docker-compose up -d
$ docker ps
CONTAINER ID        IMAGE                        COMMAND                  CREATED             STATUS              PORTS                               NAMES
843d3bdf2feb        sample-go-mysql-docker_api   "sh ./bin/run.sh"        4 minutes ago       Up 4 minutes        0.0.0.0:8080->8080/tcp              sample-go-mysql-docker_api_1
66b6ac91f5e2        mysql:8.0                    "docker-entrypoint.s…"   26 minutes ago      Up 4 minutes        0.0.0.0:3306->3306/tcp, 33060/tcp   sample-go-mysql-docker_db_1

# productを追加
$ curl -X POST http://localhost:8080/products

# productを取得
$ curl http://localhost:8080/products/latest
{"id":1,"name":"switch","price":29980}%

成果物

github.com

ハマったところ

  • init dbするところでコンテナが初回起動済みだと変更が反映されなかったりするのでコンテナ削除する必要などがあったりする
  • MySQL: 5.7 系で最初やろうとしたがdocker storage engineのoverlay2のエラーでディレクトリマウントできなかった。こちらまだ解決できてないので時間があれば調べる。

参考

AWS CDK 事始め

はじめに

Infrastructure as Codeを実現しようとするとメジャーどころだとTerraformやCloudFormationがあるがDSLを覚えるのが大変である。職場のプロジェクトでAWS CDK(Cloud Development Kit)を触る機会があったのでGetStartedしてみた。

AWS CDKとは?

AWS クラウド開発キット (AWS CDK) は、使い慣れたプログラミング言語を使用してクラウドアプリケーションリソースをモデル化およびプロビジョニングするためのオープンソースのソフトウェア開発フレームワークです。

CDKは、yamlDSLでIaCを実現するのではなくコードベースでIaCを実現するフレームワークだ。TypeScriptやJavaを使ってインフラを管理する。

Get Started

まずは開発のためにSDKをインストールする。

$ npm install -g aws-cdk
$ cdk --version
1.32.2 (build e19e206)

作業ディレクトリを作成してHelloWorldしていく。

$ mkdir sample-aws-cdk
$ cd sample-aws-cdk

今回はTypeScriptでやってみることにした。

$ cdk init --language typescript
Applying project template app for typescript
Executing npm install...
...
# Welcome to your CDK TypeScript project!

This is a blank project for TypeScript development with CDK.

The `cdk.json` file tells the CDK Toolkit how to execute your app.

## Useful commands

 * `npm run build`   compile typescript to js
 * `npm run watch`   watch for changes and compile
 * `npm run test`    perform the jest unit tests
 * `cdk deploy`      deploy this stack to your default AWS account/region
 * `cdk diff`        compare deployed stack with current state
 * `cdk synth`       emits the synthesized CloudFormation template

このようにnpmコマンドでビルドすることができる。

$ npm run build
$ cdk ls
SampleAwsCdkStack

S3バケットのスタック(CloudFormationではスタックという単位でリソースを管理する)を追加してみる。

$ npm install @aws-cdk/aws-s3

cdk init でlib配下にスタックを記述するtsファイルが生成されているのでそこにスタックを追加する。

import * as cdk from '@aws-cdk/core'
import * as s3 from '@aws-cdk/aws-s3'

export declare class SampleAwsCdkStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    new s3.Bucket(this, 'MyFirstBucket', {
      versioned: true
    })
  }
}
$ npm run build

cdk synth コマンドを使うとCloudFormationのテンプレート形式で出力してくれる。

$ cdk synth
...
$ cdk deploy
SampleAwsCdkStack: deploying...
SampleAwsCdkStack: creating CloudFormation changeset...
...
 ✅  SampleAwsCdkStack

AWS web consoleを確認するとバケットが作成されている。

不要になったスタックはdestroyコマンドで削除すれば良い。

$ cdk destroy
Are you sure you want to delete: SampleAwsCdkStack (y/n)? y
SampleAwsCdkStack: destroying...

 ✅  SampleAwsCdkStack: destroyed

また開発する際は AWS Toolkit for Visual Studio Code を利用するとCDKリソースを簡単にVSCode上で確認することができるようになる。

所感

数行書くだけで簡単にリソースが増やせるだけでなくコードベースで管理するので余計なDSLを覚えて職人になる必要もないので非常に良さそう。コードで管理するところはCloudFirestoreのSecurityRulesに似ているなと思った。使いこなせるようになればインフラの管理が非常に楽になりそうなので引き続き触っていきたい。

Sample

github.com

参考

Getting Started With the AWS CDK - AWS Cloud Development Kit (AWS CDK)