cobraとviperで設定ファイルの値をフラグの値で上書きする

Go言語でコマンドを作ろうとしたときに、オプションの指定に設定ファイルの読み込みと、オプションで読み込んだ値を上書きをしたかったのでcobraとviperでの実現の仕方を確認する。

ロングオプションを利用する

qiita.com

を参考に。

記事にあるように、 viper.BindPFlag() を呼び出さないと値がフラグの値で更新されない。

また、PersistentFlags().String()でデフォルト値を設定しても、configFileの指定と違い意味が無いので行わない。

package main

import (
    "github.com/spf13/cobra"
    "fmt"
    "github.com/spf13/viper"
    "os"
)

// 設定項目
type Config struct {
    ApplicationName string
    Debug bool
}

// 設定ファイル名
var configFile string

var config Config


func main() {
    rootCmd := &cobra.Command{
        Use: "app",
        Run: func(c *cobra.Command, args []string) {
            // セットされた値の取得
            fmt.Printf("configFile: %s\nconfig: %#v\n", configFile, config)
        },
    }

    // デフォルト値を設定する
    rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "default_config.toml", "config file name")

    rootCmd.PersistentFlags().String("name", "", "application name")

    viper.BindPFlag("ApplicationName", rootCmd.PersistentFlags().Lookup("name"))

    cobra.OnInitialize(func() {
        viper.SetConfigFile(configFile)
        viper.AutomaticEnv()

        if err := viper.ReadInConfig(); err != nil {
            fmt.Println("config file read error")
            fmt.Println(err)
            os.Exit(1)
        }

        if err := viper.Unmarshal(&config); err != nil {
            fmt.Println("config file Unmarshal error")
            fmt.Println(err)
            os.Exit(1)
        }
    })

    if err := rootCmd.Execute(); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }

    println(config.ApplicationName)
}

ショートオプションも追加

ヘルプオプションの実行結果

$ go run main.go  --help
Usage:
  app [flags]

Flags:
  -c, --config string   config file name (default "default_config.toml")
  -h, --help            help for app
  -n, --name string     application name

ショートオプションを使うには PersistentFlags().StringVarP() を利用する。

pflag/flag.go at master · spf13/pflag · GitHubにあるようにショートオプションには1文字のみ利用可能で、それ以上の文字を指定するとエラーになる。

package main

import (
    "github.com/spf13/cobra"
    "fmt"
    "github.com/spf13/viper"
    "os"
)

// 設定項目
type Config struct {
    ApplicationName string
    Debug bool
}

// 設定ファイル名
var configFile string

var config Config


func main() {
    rootCmd := &cobra.Command{
        Use: "app",
        Run: func(c *cobra.Command, args []string) {
            // セットされた値の取得
            fmt.Printf("configFile: %s\nconfig: %#v\n", configFile, config)
        },
    }

    // デフォルト値を設定する
    rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "default_config.toml", "config file name")

    rootCmd.PersistentFlags().StringVarP(&config.ApplicationName, "name", "n", "", "application name")

    viper.BindPFlag("ApplicationName", rootCmd.PersistentFlags().Lookup("name"))

    cobra.OnInitialize(func() {
        viper.SetConfigFile(configFile)
        viper.AutomaticEnv()

        if err := viper.ReadInConfig(); err != nil {
            fmt.Println("config file read error")
            fmt.Println(err)
            os.Exit(1)
        }

        if err := viper.Unmarshal(&config); err != nil {
            fmt.Println("config file Unmarshal error")
            fmt.Println(err)
            os.Exit(1)
        }
    })

    if err := rootCmd.Execute(); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }

    println(config.ApplicationName)

実行結果を見るとやりたかったことが実現出来ていることがわかる。

$ cat default_config.toml
ApplicationName = "DEFAULT_APP_TOML"
Debug = true

$ go run main.go 
configFile: default_config.toml
config: main.Config{ApplicationName:"DEFAULT_APP_TOML", Debug:true}
DEFAULT_APP_TOML

$ go run main.go  -n abc
configFile: default_config.toml
config: main.Config{ApplicationName:"abc", Debug:true}
abc

filebeats でNginxのログを確認する

Ingest nodeと、FileBeatのモジュールがで利用できるようになって、とりあえず小規模でとりあえず導入するような構成ならLogstashとか、Fluentd無しのFilebeat + Elasticsearch + Kibanaの構成でなんとかなるんじゃ無いかと試してみる。
ある程度以上の規模になったり、欠損が許容されないような場合は、Logstashとか、FluentdのようなAggregatorを間に挟んだほうが当然良いと思う。

Filebeatsをインストールする

公式サイトのインストール方法を参考にインストールする。

www.elastic.co

Nginx Moduleで出力するIndexを分ける

たとえばNginx用のIndexを分ける場合は以下のような設定をする。
prospectorでfield を追加して、output側で特定の条件(fields.typeが"accesslog"の場合)にnginx-%{+yyyy.MM.dd}という indexに送信するとしている。
ただし、nginx.access.geoipのマッピングをする必要が出てくるので、特に問題がなければデフォルトで提供されているfilebeat-* のIndexを使う方が手間は少ない。

filebeat.modules:
- module: nginx
  # Access logs
  access:
    enabled: true
    var.paths:
      - /var/log/nginx/access.log
    prospector:
      fields:
        type: accesslog

  # Error logs
  error:
    enabled: true
    var.paths:
      - /var/log/nginx/error.log
    prospector:
      fields:
        type: "accesslog"


output.elasticsearch:
  enabled: true

  hosts: ["リモートホスト:9200"]
  index: "filebeat-%{+yyyy.MM.dd}"
  indices:
    - index: "nginx-%{+yyyy.MM.dd}"
      when.equals:
        fields.type: "accesslog"

Nginx ModuleとSyslog Moduleで出力するIndexを分ける

Nginx Moduleはデフォルトのfilebeat-* に出力し、Syslog Moduleの出力先のみ変更する。

filebeat.modules:
- module: system
  # Syslog
  syslog:
    enabled: true
    var.paths: ["/var/log/secure"]
    prospector:
      fields:
        app: "syslog"

filebeat.modules:
- module: nginx
  # Access logs
  access:
    enabled: true
    var.paths:
      - /var/log/nginx/access.log

  # Error logs
  error:
    enabled: true
    var.paths:
      - /var/log/nginx/error.log


output.elasticsearch:
  enabled: true

  hosts: ["リモートホスト:9200"]
  index: "filebeat-%{+yyyy.MM.dd}"
  indices:
    - index: "syslog-%{+yyyy.MM.dd}"
      when.equals:
        fields.app: "syslog"
    - index: "filebeat-%{+yyyy.MM.dd}"
      default:

結論

デフォルトで用意されているDashboardは、一からダッシュボードを作らなくて良いのでこんなことが出来るというという取っ掛かりには良い。         filebeat-* になんでも入れると、ダッシュボードが上手く動かないとか出て来るので最低限でもモジュール単位ぐらいでは Index を分けたくなるが、そのあたりを filebeat だけでやる情報が中々なくて手間取ったが、そこそこ使える感じにはなった。         比較的小規模でAWSなどでCPUクレジットがあるようなインスタンスを使っている場合は、filebeatだけで済むのでCPU負荷が低くてすむという利点もある。

Prometheusのec2 service discoveryを試す

Prometheusには ec2 service discovery機能があり、EC2インスタンスに監視用のagent(exporter) をインストールして、特定のタグを設定するだけで監視・モニタリングの対象とすることができます。

通常のノードの追加

設定方法は以下のように、対象となるノードの情報をPrometheusの設定ファイルに書き、設定を反映されるためPrometheusを再起動します。
これの作業はノードを追加するたびに発生します。

  - job_name: 'node'
    scrape_interval: 30s
    scrape_timeout:  15s
    static_configs:
      - targets: ['172.0.0.3:9100', '172.0.0.3:9256']
        labels:
          name: 'server1'
          stage: 'prod'

ec2 service discoveryを利用したノードの追加方法

ec2 service discoveryを利用するには設定ファイルを以下のように書きます。
ec2 service discoveryを利用することで、この例ではインスタンスのタグ(Stage)に、prodかstgを値と設定すると自動的に監視・モニタリング対象として自動的に検出します。

  - job_name: 'node'
    ec2_sd_configs:
      - region: ap-northeast-1
        access_key: APIKeyを書く
        secret_key: SECRET_KEYを書く
        port: 9100
    relabel_configs:
      - source_labels: [__meta_ec2_tag_Stage]
        regex: (stg|prod)
        action: keep
      - source_labels: [__meta_ec2_tag_Name]
        target_label: name
      - source_labels: [__meta_ec2_tag_Stage]
        target_label: stage

job_name はPrometheusでよく設定される監視対象をグルーピングするラベルです。
ec2_sd_configsにある設定項目は Configuration | Prometheus で確認できます。
ここでは最低限のAWSの設定と、Node exporter がリクエストを受け付ける port番号の設定のみをしています。

relabel_configsは以下のようになっています。

1. 最初の source_labels

監視・モニタリング対象とするかどうかの判断をします。この例ではEC2インスタンスのTag(Stage) の値が、stgかprodの場合は監視・モニタリング対象になります。

2. 2番目と3番目の source_labels

EC2インスタンスのTagの値をPrometheusのラベルに設定しています。
ここでは、EC2のタグのNameの値をPrometheusのラベルのnameに設定し、同様にStageの値をstageに設定しています。
このようにPrometheusのラベルに値を設定しておかないと、Prometheus内(Alertmanager含む)で値を利用できないためこのように設定します。

relabel_configsの設定は EC2 Discovery Relabelling - Robust Perception が参考になりました。

複数の exporter を利用する場合

監視に利用する exporter が複数の場合は、port を複数設定する必要が出てきますが、ec2_sd_configs では port は int値のみ指定可能で複数のportをまとめて書くことはできません。
そのため Node exporter と Process exporter を利用するような場合には、以下のように ec2_sd_configs を書く2つ書くことになります。

  - job_name: 'node'
    ec2_sd_configs:
      - region: ap-northeast-1
        access_key: APIKeyを書く
        secret_key: SECRET_KEYを書く
        port: 9100
    relabel_configs:
      - source_labels: [__meta_ec2_tag_Stage]
        regex: (stg|prod)
        action: keep
      - source_labels: [__meta_ec2_tag_Name]
        target_label: name
      - source_labels: [__meta_ec2_tag_Stage]
        target_label: stage

  - job_name: 'process'
    ec2_sd_configs:
      - region: ap-northeast-1
        access_key: APIKeyを書く
        secret_key: SECRET_KEYを書く
        port: 9256
    relabel_configs:
      - source_labels: [__meta_ec2_tag_Stage]
        regex: (stg|prod)
        action: keep
      - source_labels: [__meta_ec2_tag_Name]
        target_label: name
      - source_labels: [__meta_ec2_tag_Stage]
        target_label: stage

Swift3でファイルを読み込む

swiftファイルと同じ場所にある、sample.txtを読み込むコードは以下のようになる。

        let path = Bundle.main.path(forResource: "sample", ofType: "text")!
        if let data = NSData(contentsOfFile: path){
            print(String(NSString(data: data as Data, encoding: String.Encoding.utf8.rawValue)!))
        }else{
            print("データなし")
        }

Capistrano3のコードを読んでみる1 ~設定ファイルのパス設定~

Capistranoを実行時に利用するconfig/deploy.rbとかproduction.rb、staging.rbのパスを変更できるのか気になったのでソースコードを読んでみました。
結論としては、変更可能ですがcapistrano/setupをCapfileでrequireする前にsetする必要があります。
また、Capfileファイルのパスも変えられるかが気になって見てみましたが、capfile、capfile.rb、Capfile.rbにファイル名の変更は可能という結論になりました。

/lib/capistrano/application.rb

色々省略してますが、初期化処理としてcapfileの読み込みを行います。

module Capistrano
  class Application < Rake::Application
    def initialize
      super
      @rakefiles = %w{capfile Capfile capfile.rb Capfile.rb} << capfile
    end

    private

    # allows the `cap install` task to load without a capfile
    def capfile
      File.expand_path(File.join(File.dirname(__FILE__),'..','Capfile'))
    end
  end
end

/lib/capistrano/setup.rb

load 'capistrano/defaults.rb'では、/lib/capistrano/defaults.rbで、setしなかった場合のデフォルト値が定義されています。
load:defaultsをImmutableTask(/lib/capistrano/immutable_task.rb)として定義しているので、CapistranoのタスクでもOverride出来ません。
stage_config_pathやdeploy_config_pathは/lib/capistrano/dsl/paths.rbで定義されています。
configure_backend は/lib/capistrano/configuration.rbで定義されていて、sshkitの設定を行います。

namespace :load do
  task :defaults do
    load 'capistrano/defaults.rb'
  end
end

stages.each do |stage|
  Rake::Task.define_task(stage) do
    set(:stage, stage.to_sym)

    invoke 'load:defaults'
    Rake.application["load:defaults"].extend(Capistrano::ImmutableTask)
    load deploy_config_path
    load stage_config_path.join("#{stage}.rb")
    load "capistrano/#{fetch(:scm)}.rb"
    I18n.locale = fetch(:locale, :en)
    configure_backend
  end
end

require 'capistrano/dotfile'

setup.rbの最後でrequireしているdotfile.rbの内容は以下のとおりです。

dotfile = Pathname.new(File.join(Dir.home, '.capfile'))
load dotfile if dotfile.file?

/lib/capistrano/install.rb

ファイルの中身は、以下のようになっています。

load File.expand_path(File.join(File.dirname(__FILE__),'tasks/install.rake'))

/lib/capistrano/task/install.rakeを実行しています。
install.rakeはcap install の際に実行される処理が定義されています。

/lib/capistrano/deploy.rb

require 'capistrano/framework'

load File.expand_path("../tasks/deploy.rake", __FILE__)

/lib/capistrano/framewok.rb

以下のように、framework.rakeを呼び出しています。

load File.expand_path("../tasks/framework.rake", __FILE__)
require 'capistrano/install'

task/deploy.rake

task/framework.rakeでは、Capistranoが提供するコマンド(?)のインターフェースが定義されています。

namespace :deploy do

  desc 'Start a deployment, make sure server(s) ready.'
  task :starting do
  end

  desc 'Started'
  task :started do
  end
 
  #以下、省略

また、:rollback、:deployの処理はここで定義されています。

  #以下、省略

  desc 'Rollback to previous release.'
  task :rollback do
    %w{ starting started
        reverting reverted
        publishing published
        finishing_rollback finished }.each do |task|
      invoke "deploy:#{task}"
    end
  end
end

desc 'Deploy a new release.'
task :deploy do
  set(:deploying, true)
  %w{ starting started
      updating updated
      publishing published
      finishing finished }.each do |task|
    invoke "deploy:#{task}"
  end
end
task default: :deploy

task/framework.rake

task/deploy.rake はtask/framework.rakeで定義してあったインターフェースの実装を担当しています。(全てではないですが)
また、実際の処理はinvokeで処理を委譲しています。

namespace :deploy do

  task :starting do
    invoke 'deploy:check'
    invoke 'deploy:set_previous_revision'
  end

  #以下、省略