Fire Engine

化学の修士号→消防士→ITエンジニア(2016年11月〜)

粒子群最適化(Particle Swarm Optimization: PSO)をGoで実装してみた

粒子群最適化とは、群知能による最適化手法の一種です。この手のバイオミメティクス(生物模倣)によるアプローチは、私の学生時代の専門である材料工学でも非常に盛んに研究されていましたが、データサイエンスでも応用されているのを知って興味を持ったので、実装しながら勉強しました。

粒子群最適化(Particle Swarm Optimazation: PSO

粒子群最適化とは、鳥や魚などの群れに見られる社会的行動のシミュレーションを基にモデル化されたヒューリスティックな最適解探索アルゴリズムです。

全体的な最適化の流れを説明します。 まず最初に探索空間内で、決められた個数の粒子を全て初期化します。初期化とは空間内の位置をランダムに決めることを指します。

それぞれの粒子は、目的関数により適合度(どれだけその解が優れているか)を算出できます。全ての粒子をある移動ルールを元に動かしながら、適合度が高い位置を探していくのがPSOの探索方法です。

ここでPSOにおいて重要な移動ルールについて説明します。

粒子 P_{i}の時刻 tから t+1の移動速度 \vec{v}_{i}(t+1)は以下の式で表されます。

f:id:hirotsuru314:20190409205526p:plain:w500

ここで重要なのが、グローバルベスト \vec{g}(t)とパーソナルベスト \vec{p}_{i}(t)です。
グローバルベストとは全ての粒子群で過去の探索も含めて最も適合度が高い位置、パーソナルベストとは特定の1粒子におけるこれまでの探索の中で最も適合度が高い位置を示します。これは繰り返しの探索の中で随時更新されていく変数です。
 Iは慣性係数と呼ばれる定数で、前の移動速度がどれだけ維持されるかを表します。 A_{g}および A_{p}は加速係数と呼ばれる定数です。

この数式からわかるように粒子群の中ではグローバルベストの情報が共有されていて、その方向に向かうように移動速度を調整しながら、より良い解の探索を繰り返します。これはあたかも餌を見つけた鳥が仲間にその情報を伝えて、仲間が餌のある位置に集まってくるようです。

作ったもの

github.com

『Evolutionary Algorithm implemented in Go』の略で名前を付けました。 進化計算全般に興味があるので、ここに実装しながら勉強したいと思っています。

最適化問題を解いてみる

今回は下のシンプルな目的関数の最小値を最適解として、PSOでパラメータを探索してみました。

f:id:hirotsuru314:20190409212421p:plain:w120

 N=2としたときの関数のグラフを描くと以下のようになります。

f:id:hirotsuru314:20190409215708p:plain

左側が3Dに描いたもので、右側が2次元に写像したもの(左のグラフを上から見たもの)です。
グラフや数式から直感的にわかるように、この関数は (x_1,x_2)=(0,0)で最小値 0を取るため、今回の探索の目的は、 (x_1,x_2)=(0,0)を見つけることになります。

サンプルコードは以下の通りです。

package main

import (
    "github.com/tsurubee/eago"
    "log"
)

func objectiveFunc(x []float64) float64 {
    return x[0] * x[0] + x[1] * x[1]
}

func main() {
    pso := eago.NewDefaultPSO()
    pso.NParticle =  5
    pso.NStep = 20
    pso.Min = -20
    pso.Max = 10

    if err := pso.Minimize(objectiveFunc, 2); err != nil {
        log.Fatal(err)
    }
}

上のコードをmain.goとして以下のように実行します。

$ go run main.go
Step   0: Fitness=8.124 Position=[-2.168 1.851]
Step   1: Fitness=8.124 Position=[-2.168 1.851]
Step   2: Fitness=7.394 Position=[-1.618 2.186]
Step   3: Fitness=6.958 Position=[1.068 2.412]
Step   4: Fitness=6.958 Position=[1.068 2.412]
Step   5: Fitness=4.809 Position=[-2.106 0.611]
Step   6: Fitness=2.215 Position=[-1.052 1.053]
Step   7: Fitness=1.535 Position=[0.580 1.095]
Step   8: Fitness=0.163 Position=[0.023 0.403]
Step   9: Fitness=0.163 Position=[0.023 0.403]
Step  10: Fitness=0.055 Position=[-0.175 0.156]
Step  11: Fitness=0.055 Position=[-0.175 0.156]
Step  12: Fitness=0.055 Position=[-0.175 0.156]
Step  13: Fitness=0.055 Position=[-0.175 0.156]
Step  14: Fitness=0.055 Position=[-0.175 0.156]
Step  15: Fitness=0.055 Position=[-0.175 0.156]
Step  16: Fitness=0.055 Position=[-0.180 0.151]
Step  17: Fitness=0.044 Position=[0.019 0.210]
Step  18: Fitness=0.013 Position=[-0.111 0.023]
Step  19: Fitness=0.008 Position=[-0.083 -0.031]

出力結果を見てみると、Stepが進むにつれて、Fitnessが 0、パラメータの解が (x_1,x_2)=(0,0)に収束しようとしていることがわかります。
粒子が速度を調整しながら最適解に収束して行く様子は下のサイトのアニメーションが非常にわかりやすかったです。

Particle swarm optimization - Wikiwand

さいごに

群知能とか生物模倣的なアプローチはもう少し勉強を続けて深掘りしていきたい。
あと、もっと実践的なデータに対して適用してみて、自分の今の領域における応用についても考えていきたい。

参考

shop.ohmsha.co.jp

遺伝的アルゴリズムを用いてコンテナの配置を最適化する論文:「Genetic Algorithm for Multi-Objective Optimization of Container Allocation in Cloud Architecture」

ふとタイトルが目に入ってきて気になって読んでみた。私自身そんなにコンテナ技術を触ってないけど、前から遺伝的アルゴリズムが気になっていた。 ちょいちょい知識が足りずに理解しきれないところ出てきたけど、とりあえず最後まで読んだのでざっくりまとめを書いてみる。

読んだ論文

pdfはダウンロードできなかったけど、ここからOnlineで読めた。

所感

先に論文を読んだ所感を書いておく。

  • コンテナのリソース割り当ての最適化問題遺伝的アルゴリズム(GA)を使って解くというコンセプトが面白かった。Related Workとして多くの論文が挙げられていた(Table 1およびTable2)ので、この辺りは追っていくと面白そう。

  • GAはパラメータの全検索が不可能と思われるくらい膨大な組み合わせがあって、かつ有効な探索方法が定かでないパターンに有効っぽい。さらに、論文内で用いているNon-dominated Sorting Genetic Algorithm Ⅱ(NSGA-Ⅱ)などの手法は、多目的最適化に有効な手法だから、けっこう応用の幅が広そうだなぁと思った。

  • いかに最適化したいものを目的関数として定式化するかっていうところが大変で重要なところなんだろうけど、それができるならば、目的は複数あっても、目的同士が相関関係にあっても、近似解をヒューリスティックに探索してくれるから、GAという手法は学んでいくといろんな問題に適用できそう。

  • 遺伝的アルゴリズムを始めとした進化的計算という分野に興味が湧いたのでやっていきたい。

概要

クラスター内の物理ノード群に対するコンテナの配置は、システム全体のパフォーマンスや信頼性、スケーラビリティに大きな影響を与えるため、システム設計において重要な要素である。Kubernetesのような既存のコンテナクラスター管理は、コンテナのオートスケールや物理マシンへのコンテナの配置に対して単純な実装しかされておらず、それらは物理的なリソースの使用率や閾値にのみ焦点を当てている。

このようなリソース最適化はNP完全問題であり、メタヒューリスティックなアプローチが有効である。中でも、GAのような進化的アプローチはリソース最適化問題に非常に有効な解法の一つである。本論文では、コンテナのリソース割り当てに対してNon-dominated Sorting Genetic Algorithm Ⅱ(NSGA-Ⅱ)というアルゴリズムで最適化している。NSGA-Ⅱという手法はMulti-Objective(多目的)な最適化に有効であるため、今回のように最適化したい目的関数が複数あって、それぞれの目的関数同士が独立でない場合でも適用可能である。

提案手法を用いて最適化したコンテナ配置は、Kubernetesによるコンテナ管理のアプローチよりも、論文中に定義した目的(後述)において、優位な結果が出ることが実験的に示されている。

遺伝的アルゴリズム(GA)

GAとは何か?という問いに対する答えとして「遺伝的アルゴリズム(Genetic Algorithm)を始めよう!」の7枚目のスライドがわかりやすかった。

生物進化における遺伝と適者生存による自然淘汰の仕組みをソフトウェア的に模すことで複雑な問題に対する最適解を探索する手法

私の印象としては、まじめに解こうとしても解けないくらい解の組み合わせの数が膨大なときに、実用的な時間スケールで近似的な解を探索できる点が大きな特徴だと思った。
また、実際に論文内の最適化で用いているNSGA-Ⅱについてもいくつか解析記事があった。

http://mikilab.doshisha.ac.jp/dia/research/mop_ga/moga/3/3-5-5.html

この辺りはこれから学んで数学的な背景からしっかり押さえていくので、また詳しくブログに書こうと思う。

システムのモデル化

最適化問題として解くためには、当然ながら何が最適化を評価できなければならない。さらに出てきた解(GA的には染色体)の候補同士に優劣をつけるために、定量的に評価できる形、つまり定式化しておく必要がある。

本論文ではコンテナのリソース割り当てに対して、以下の4つの目的を置いて定式化している。式中の変数についてはTable3を、定式化の詳細については「3.1 Optimization Objectives」を参照。

これらの定式化したものは、個体(解の候補)の適応度(fitness)を定量的に評価するために用いられる。

1. ボトルネックを回避するためにクラスタ全体でワークロードを均等に分散させる(Threshold Distance)

f:id:hirotsuru314:20190328222537p:plain

2. 新しいアプリケーションの追加を考慮した上で、クラスタのリソースをバランスよく利用する(Balanced Cluster)

f:id:hirotsuru314:20190328222715p:plain

3. クラスタ内のノードに適切にコンテナを分散させることによってマイクロサービスの信頼性を高める(System Failure)

f:id:hirotsuru314:20190328222655p:plain

4. 関連するマイクロサービスのコンテナを短いネットワーク距離の物理マシンに配置することによって、マイクロサービス間の通信のオーバーヘッドを小さくする(Network Distance)

f:id:hirotsuru314:20190328222846p:plain

実験と結果

実験については、GAで最適化したコンテナの配置とKubernetesアロケーションポリシーによるアプローチのそれぞれをサンプルアプリケーションを使ってベンチマークを取って比較している。

比較対象のKubernetesについては以下のように説明されている(私はKubernetesを触ったことないのでピンときていない)
KubernetesはコンテナやPodの配置を管理するために2つのポリシーを採用している。

  1. PodFitResources:物理マシンへのリソース要求の合計がマシンのキャパシティを超えないようにする

  2. LeastRequestPriority:コンテナは一番リソース消費が少ないマシンに配置される

Kubernetesの実装についてはよく知らないが、これを見るだけでも、論文中の目的関数4つの方が多くのことを考慮して最適化しようとしているのがわかる。
論文中では、GAの世代ごとの成長過程についても色々と議論されている。結論としてはGenerations 100、Population size 200というリーズナブルな値で最適化された解が得られたと述べられている。

Kubenetesとの比較の結果については、前述の4つの目的関数全てにおいて、提案手法のアプローチの方が優位な結果が出たと述べられている。例えば、一つの目的関数をピックアップすると以下のような感じ

f:id:hirotsuru314:20190328225640p:plain

変数として、物理マシンの台数を250・300・350・400台と降っている。こんな規模で実験やるんだなぁ。

さいごに

GAのこともKubernetesのこともよく知らないから、へーって感じだったけど、こういったインフラ周りで機械学習を適用している事例は面白いなと思った。GAについては数学的な理論からしっかり学んでいく。

Dynamic Time Warping(動的時間伸縮法)で時系列データをクラスタリングする

最近時系列データのクラスタリングに興味を持ち始めて、いくつか論文読んだり、アルゴリズムについて調べていたら、実装してみたくなったので勉強のために作ってみました。
実装の言語にはGolangを用いていて、クラスタリングアルゴリズムは、Dynamic Time Warping(以下、DTW)とk-medoids法を組み合わせたものです。

作ったもの

github.com

tsclusterはtime series clusteringの略です。 今回は作ったのは、特定のアルゴリズムのみですが、今後興味があるアルゴリズムがあれば、ここに実装していきます。

使い方

func main() {
    var dataset [][]float64
    dataset = append(dataset, []float64{1, 1, 1, 1, 1, 1})
    dataset = append(dataset, []float64{1, 1, 1, 1, 1, 1, 1})
    dataset = append(dataset, []float64{2, 2, 2, 2, 2})
    dataset = append(dataset, []float64{2, 2, 2, 2, 2, 2, 2})

    tc := tscluster.NewTscluster(tscluster.DTW) // 引数:距離関数
    labels, err := tc.Kmedoids(dataset, 2, 20)  // 引数:データ, クラスタ数, 最大反復回数
    if err != nil {
    log.Fatal(err)
    }
    fmt.Println(labels)
}

#=>
[0 0 2 2]

いくつか補足すると、与えるデータセットの型は[][]float64であり、1次元時系列データ([]float64)のスライスです。
また、出力結果labelsは、与えたデータセットのスライスに対してクラスタリングした結果のラベルを返しています。ラベルの値として、データが所属するクラスタのmedoids(後述するがクラスタの代表点のようなもの)のindex番号を返しています。
すなわち、[0 0 2 2] とは与えたデータセットの0番目と1番目は同じクラスタに属しており、datasetの0番目が代表点であるということです。

検証

それでは、もう少し実践的なデータで試してみます。
用いたデータとプログラムは、tsclusterのリポジトリexamplesディレクトにおいています。
1次元の時系列データを30個用意してグラフを描いたものが、以下になります。このデータは意図的に、上昇トレンド・停滞トレンド・下降トレンドそれぞれを持つものを10個ずつプログラムで用意しました。

f:id:hirotsuru314:20190217160343p:plain

これらのデータセットをDTW + k-medoidsでクラスタリングして、クラスタごとに色分けして描いたグラフが以下になります。

f:id:hirotsuru314:20190217160405p:plain

グラフを見るとそれぞれのトレンドごとにきれいにクラスタリングできているように見えます。
今回のような単純な線形のトレンドごとにクラスターに分けるにはもっと単純な手法もありそうです。本来DTWという距離尺度を用いていると、形状は似ているけど時間軸に対してずれているといったデータの類似性が高くなるような特徴を持つため、そういったデータをクラスタリングしてみるのも良いかもしれません。

実装したアルゴリズム

時系列クラスタリングの概要を把握するには、下のサーベイ論文が非常に情報がまとまっていました。

Time-series clustering – A decade review

Dynamic Time Warping(DTW)

DTWの特徴としては、

  • 時系列のデータ長がそろっていなくても計算可能
  • 時間・位相のずれていても形状が似ていれば類似度が高くなる

などが挙げられます。
DTWについてはこちらのブログに直感的に大変わかりやすいアニメーションがあったので、見てみるとイメージが湧きやすいと思います。

データ長がmとnの時系列データのDTW距離を求めるアルゴリズムは以下のステップで構成されます。

  1. 各データ点に対して総当たりで距離を計算し、要素がデータ点同士の距離となる行列D(m × n)を作成
  2. D[0][0] からD[m][n]までに通るパスのうち、行列の要素の合計が最小となるパスを探索する。その際のパスの要素の合計がDTW距離となる

k-medoids

k-medoidsはクラスタリングの手法で大変メジャーなk-meansと非常に似た手法です。
両者の大きな違いはクラスタの代表点をcentroid(重心)ではなくmedoidを選択するという点です。
medoidとは、クラスタ内のデータ点で、同一クラスタ内の他の全ての点までの距離の総和が最小になる点のことを言います。
Wikipediaには、k-meansと比較したk-medoidsの特徴が以下のように表現されていました。

It is more robust to noise and outliers as compared to k-means because it minimizes a sum of pairwise dissimilarities instead of a sum of squared Euclidean distances. (https://en.wikipedia.org/wiki/K-medoids より引用)

k-meansよりノイズや外れ値に強いとのことでした。また、k-medoidsはクラスタの代表点を実際のデータの中から選ぶため、一度データ同士の距離を全て計算してしまえば(距離行列を作成すれば)、k-meansのような重心の計算や重心からのデータ点までの距離の計算などといった処理がないため、計算量も少なくなると思います。

アルゴリズムとしては以下の通りです。

  1. データセットの中からk個のデータをランダムに選択する(modoid)
  2. 全てのデータを一番近いmedoidのクラスタに割り当てる
  3. それぞれのクラスタ内において、クラスタ内の他のすべての点との距離の合計が最小になる点を新たにmedoidとする
  4. 2と3を繰り返す。ただし、3においてmedoidに変化がない、もしくは指定した最大の反復回数に到達すれば終了する

さいごに

今回のようなデータのクラスタリング教師なし学習で、データがあればすぐに適用できるというメリットはあるものの、当然ながら出てきたクラスター群に対する解釈は人間が与えないといけないですし、そもそも何個のクラスターに分けるかはある程度事前知識が必要だと思います(クラスタ数を最適化する方法はあるものの)
また何か気になるものがあれば実装してみたいと思います!

k-Shapeによる時系列クラスタリングの論文:「k-Shape: Efficient and Accurate Clustering of Time Series」を読んだ

最近、時系列データのクラスタリングに興味を持っているので、k-Shapeというクラスタリング手法に関する論文を読みました。
なぜ興味を持っているかというと、サーバの各種メトリクス(CPU使用率・メモリ使用率など)を使って、似たような特徴を持っているサーバ群をクラスタリングできないかと考えいるためです。例えば、負荷の高いサーバ群と負荷の低いサーバ群などにグルーピングできると、面白いのではないかと考えています。

元論文はこちらです。

k-Shape: Efficient and Accurate Clustering of Time Series

この論文では、提案するk-Shapeのアルゴリズムの説明だけでなく、時系列データをクラスタリングする上での理論的な背景から説明されていて、時系列クラスタリング手法の全体像を把握するのに大変良い論文でした。

概要

本論文では、k-Shapeという時系列クラスタリング手法を提案しています。

k-Shapeの特徴は、

  • 時系列データの形状に着目したshape-basedクラスタリングである
  • データ間の距離尺度として、規格化した相互相関を用いている
  • 高効率かつ高精度で、幅広いデータに適用できる

などが挙げられます。
以下、本論文の内容をまとめていきますが、理論の詳細に関しては、論文をご参照ください。

Time-Series Invariances

時系列データをクラスタリングする際には、何に着目してクラスタリングするか(何をもって似ているとするのか)というのが重要になります。
例えば、形状は似ているけど、時間軸に対してずれている2つの時系列を同一クラスと捉えるかどうかは、そのときの用途によって異なると思います。

k-Shapeの場合、ScalingShiftingに対する不変性 (Invariances)に着目しています。
簡単に言うと、Scalingは軸に対してスケールした際にデータ同士の性質が似ているかどうか、Shiftingは時間軸(位相)に対してずらした場合にデータ同士の性質が似ているかどうかに着目しており、この特徴からk-Shapeはデータの形状に着目したshape-basedなクラスタリングであるとされています。

Time-Series Distance Measures

クラスタリングを行う際に、データ間の類似性は距離として表現されます。すなわち、距離が近いほど類似性が高く、距離が遠いほど類似性は低いということです。

このことから、前述の「何を持って似ているとするか」は、データ間の距離をどのように定義するかに強く依存するため、距離尺度の定義が極めて重要となってきます。論文中でも距離をどうやって決めるかが、クラスタリングアルゴリズム自体より重要だと述べらていました。

時系列データの「形状」に着目したクラスタリングをするには、振幅と位相のずれをうまく扱う距離尺度が必要となります。
k-Shapeでは、一般的によく用いられるユークリッド距離や動的時間伸縮法ではなく、独自のShape-based distance(SBD)という距離尺度を用いています。

Shape-based distance(SBD)

SBDは、規格化した相互相関を用いています。
相互相関とは、信号処理や画像処理に広く使われている信号間の類似性の尺度です。
ざっくりいうとデータをずらしながら内積を計算していき、内積が一番大きくなった位置を探す感じです。

2つの時系列データxyにおけるSBDは以下の式で表されます。

f:id:hirotsuru314:20190206220202p:plain

ただし、

f:id:hirotsuru314:20190206220243p:plain

Shape-based Time-Series Clustering

距離尺度が決まってしまえば、クラスタの重心が計算できます。(重心の計算方法は論文中の「3.2 Time-Series Shape Extraction」を参照)
あとのクラスタリングアルゴリズムはk-meansとほぼ同じような感じで、以下のように反復的な処理を行います。

  1. 時系列データを各クラスタの重心と比較し、最も近い重心をもつクラスタにデータを割り当てる
  2. クラスタに属するメンバーから重心を計算して更新する

これを繰り返したのち、クラスタのメンバーに変更がないか、反復回数の最大値に達するまで上の手順を繰り返します。

感想

手持ちのデータをクラスタリングしたいときには、どの不変性に着目して、何を似ているとしたいのかを考えて、それに適したアルゴリズムを選定するかが重要だと思いました。
今回のアルゴリズムは、時間軸のずれや、伸縮に対して頑強なアルゴリズムである印象を受けました。サーバメトリクスの場合、scalingは重要な違いを表す要素だと思うので、サーバメトリクスのクラスタリングに対してはもっと適したアルゴリズムがあるかもなーと思いました。もっといろんな手法をサーベイしていきます。

k-Shapeに関しては、論文中に擬似コードが載っていたのでスクラッチで書けそうだし、tslearnというPythonライブラリもあるようなので試したい。

Krylov部分空間を導入して特異スペクトル変換による異常検知の処理を高速化した

1年くらい前に特異スペクトル変換法による異常検知ライブラリを作ったんですが、作ったっきり放置していたので、開発当初からやりたかった計算の高速化処理を書きました。
ずっと放置してた割にはちょいちょいGitHubのスターを押してもらえてて、データサイエンスの流行を感じた。自分ももう一回ちゃんと学び直していこうという気になったので、まずは昔書いたやつの拡張からやっていく。

【目次】

特異スペクトル変換とは?

特異スペクトル変換法の特徴については以前のブログに書いているので、ぜひそちらも読んでください。

特異スペクトル変換法の全体像は以下のようになっています。

f:id:hirotsuru314:20171011214314p:plain
出典:上の図は井手剛氏の著書「入門 機械学習による異常検知―Rによる実践ガイド」のP200 図7.4を元に作成しました。

図のように過去と今のパターンを行列として取り出し、それぞれの特徴抽出をした行列(上図の主部分空間の部分)を生成します。生成した二つの行列の食い違いの大きさを変化度として異常検知を行います。

特異スペクトル変換は、特定の確率分布を仮定していないため、多様な変化に頑強に対応できる点やパラメータチューニングが容易などといった特徴が挙げられます。

一方、特異スペクトル変換の一つの課題として計算コストが高いことが挙げられます。先ほど述べた「行列からの特徴抽出」の部分で、特異値分解と呼ばれる行列計算を用いており、これがかなり計算コストが高いです。したがって、検知を行いたい時間間隔や、パラメータの最適値によっては、リアルタイム計算が厳しくなってきます。

今回は、Krylov部分空間という概念を導入することにより、特異スペクトル変換による異常検知の処理を高速化することが目的です。

Krylov部分空間の導入

理論的な詳細は下記の元論文をご参照ください。この論文は今回実装した異常検知のアルゴリズムについてはもちろんのこと、部分空間同士の距離について丁寧に書かれていて数学的にも大変勉強になりました。

Change-Point Detection using Krylov Subspace Learning

Banpeiの既存実装だと、データから取り出した密行列に特異値分解かけるので、その行列のサイズが大きくなると処理はどんどん重くなりますが、論文のアルゴリズムを用いると、次元数k(通常2~5程度)のKrylov部分空間を導入して、最終的にはk×kの固有値問題に帰着します。

上の論文や他の参考情報(最後に載せている)を元に高速化処理を書きました。

github.com

検証結果

それでは、特異値分解(SVD)を使った既存実装と、今回実装したLanczos法を使った新規実装を比較していきます。(Lanczos法はKrylov部分空間法の計算手法の一つ)

用いるデータは、Banpeiのリポジトリに置いている周波数異常データです。
まず以下のようにしてデータを読み込みます。(ライブラリのインストール方法についてはREADME参照)

import banpei
import pandas as pd

model   = banpei.SST(w=100)
raw_data = pd.read_csv('./tests/test_data/periodic_wave.csv')
data = raw_data['y']

Jupyter Notebookを使うと以下のように%timeというマジックコマンドでプログラムの実行時間を簡単に計測できます。

# 既存実装(SVD)
%time results_svd = model.detect(data, is_lanczos=False)
#出力結果=> 
CPU times: user 4.22 s, sys: 80.4 ms, total: 4.3 s
Wall time: 2.6 s

# 新規実装(Lanczos)
%time results_lanczos = model.detect(data, is_lanczos=True)
#出力結果=> 
CPU times: user 944 ms, sys: 51.2 ms, total: 995 ms
Wall time: 872 ms

用いたサンプルデータ、既存実装により計算した変化度スコア、新規実装により計算した変化度スコアを描くと以下のようになりました。

f:id:hirotsuru314:20190203093607p:plain

実行時間に関しては、4.3sから995msとなり、約4.3倍早くなりました。
この結果は完全に取り出した行列のサイズに依存しています。今回の例で用いた行列のサイズは50×100ですが、このサイズが大きくなればなるほど、新規実装の方が顕著に処理が早くなります。

次に変化度スコアのグラフについてですが、一番下の新規実装(Lanczos)は既存実装(SVD)に比べてグラフがノイジーで粗くなっているように見えます。この理由は詳細には調べていませんが、異常検知の観点でいくと、しっかり変化度スコアが立ち上がってくれているなら、多少のグラフの粗さは問題ないかと思っています。

さいごに

難解な数学的な処理をプログラムに落とすと謎の達成感があるんだけど、書いたものは使ってなんぼなので、その辺りは今後の課題としてやっていきます。
データサイエンス周りは1年間くらい追ってなかったので、基礎的なインプットも再開してやっていく!

参考

ConsulのACL Bootstrapをリセットしてもう一度やり直す方法

こんにちは、つるべーです!
先日、Consul ACLの記事を書きましたが、今回もACLのちょっとした小ネタについて書きます。
内容としては、ACLをBootstrapしたあとにMater TokenのSecret IDをなくしてしまい、Consul関連の操作が何もできなって、発狂しそうになったときの対応方法です。

前回の記事

blog.tsurubee.tech

環境

DockerでConsul ACLの検証環境を用意しています。使い方はREADMEをご覧ください。

github.com

Consulのバージョンは1.4.0です。

/ # consul version
Consul v1.4.0
Protocol 2 spoken by default, understands 2 to 3 (agent will automatically use protocol >2 when speaking to compatible agents)

ACLのBootstrap

ACLを使い始めるには、最初にBootstrap、いわゆる初期化をしてやる必要があります。
公式ドキュメントではこのあたりに説明があります。

Consulの設定ファイルでACLを有効にしたのち、serverモードのサーバでBootstrapをすると、下のようにBootstrap Token(Master Token)が払い出されます。

/ # consul acl bootstrap
AccessorID:   3cff8208-af8e-feb9-9ebb-fc75a5ddb76e
SecretID:     91118e17-445b-942d-f090-f3b34defa4bf
Description:  Bootstrap Token (Global Management)
Local:        false
Create Time:  2018-12-21 14:26:58.574906326 +0000 UTC
Policies:
   00000000-0000-0000-0000-000000000001 - global-management

ACLのBootstrapは一度しかできない

上記のACLのBootstrapは一度しかできず、二回目を叩くと下のようにエラーが出ます。

/ # consul acl bootstrap -token=91118e17-445b-942d-f090-f3b34defa4bf
Failed ACL bootstrapping: Unexpected response code: 403 (Permission denied: ACL bootstrap no longer allowed (reset index: 18))

通常は、Bootstrapをし直す必要などないのですが、例えば、Bootstrapしたあとに、Bootstrap時に出力されるMater TokenのSecret IDをなくしてしまったらどうなるでしょう?
そうです、Bootstrapした直後の時点ではMaster TokenなしではConsulコマンドが何も実行できません。(Bootstrap時にAnonymous Tokenには何もPolicyがアタッチしていないため)
したがって、Bootstrap後にMater TokenのSecret IDがわからなくなるとConsul関連の操作が何もできなくなり、にっちもさっちもいかなくなります。

Bootstrapをリセットしてやり直す方法

Consulの公式ドキュメントを探しても、Bootstrapをもう一回やり直す方法が見当たりませんでした。(2018年12月22日現在)
しかし、同じくhashicorpが開発しているNomadのドキュメントにResetting ACL Bootstrapの説明がありました。このNomadのドキュメントと同じ手順でConsul ACLのBootstrapをリセットすることができました。

実際に手順を示します。まず二回目のBootstrapを叩いた時に出力されるエラーメッセージのreset index: 18の部分に着目してください。

ACL bootstrap no longer allowed (reset index: 18)

このindexの値は固定値でなく、個々で異なる値です。(クラスタ内では共有の値)
この値を埋め込んだファイルを、acl-bootstrap-resetという名前で、Leaderノードのデータディレクトリ配下に作成します。データディレクトリとは設定ファイルのdata_dirの部分です。(ここでは仮に/tmp/dataとする)

$ echo 18 >> /tmp/data/acl-bootstrap-reset

このようにreset indexを埋め込んだacl-bootstrap-resetファイルを作成すると、Bootstrapコマンドを再度実行できるようになります。

/ # consul acl bootstrap
AccessorID:   180d0d38-424f-69be-1897-d98b41abed7e
SecretID:     32ff58d9-df47-05d0-1279-f63ae28af609
Description:  Bootstrap Token (Global Management)
Local:        false
Create Time:  2018-12-22 07:09:03.8573353 +0000 UTC
Policies:
   00000000-0000-0000-0000-000000000001 - global-management

ここで全ての権限が与えられたTokenができるため、このSecretIDでConsulの操作ができるようになります。
ちなみに作成したacl-bootstrap-resetファイルは勝手に削除されています。

/ # cat /tmp/data/acl-bootstrap-reset
cat: can't open '/tmp/data/acl-bootstrap-reset': No such file or directory

また、実は再Bootstrapをしたからといって、完全に初期化されたのでなく、1回目のBootstrap時に作成されたMater TokenとAnonymous Tokenは残ったまま、新しいMaster Tokenが作られた形になります。

/ # consul acl token list -token=32ff58d9-df47-05d0-1279-f63ae28af609
AccessorID:   180d0d38-424f-69be-1897-d98b41abed7e
Description:  Bootstrap Token (Global Management)
Local:        false
Create Time:  2018-12-22 07:09:03.8573353 +0000 UTC
Legacy:       false
Policies:
   00000000-0000-0000-0000-000000000001 - global-management

AccessorID:   3cff8208-af8e-feb9-9ebb-fc75a5ddb76e
Description:  Bootstrap Token (Global Management)
Local:        false
Create Time:  2018-12-21 14:26:58.574906326 +0000 UTC
Legacy:       false
Policies:
   00000000-0000-0000-0000-000000000001 - global-management

AccessorID:   00000000-0000-0000-0000-000000000002
Description:  Anonymous Token
Local:        false
Create Time:  2018-12-21 14:18:38.848568562 +0000 UTC
Legacy:       false
Policies:

そのため、古いMaster Tokenは消しておいてもいいですし、新しいMaster Tokenが発行された今であれば、古いMaster TokenのSecretIDを確認することもできます。

/ # consul acl token read -id=3cff8208-af8e-feb9-9ebb-fc75a5ddb76e -token=32ff58d9-df47-05d0-1279-f63ae28af609
AccessorID:   3cff8208-af8e-feb9-9ebb-fc75a5ddb76e
SecretID:     91118e17-445b-942d-f090-f3b34defa4bf
Description:  Bootstrap Token (Global Management)
Local:        false
Create Time:  2018-12-21 14:26:58.574906326 +0000 UTC
Policies:
   00000000-0000-0000-0000-000000000001 - global-management

さいごに

最初Bootstrapをもう一回やりたいとなった時、bootstrapコマンドに--forceみたいなオプションないかなぁーと思ったのですが、ありませんでした。
今回見たように再Bootstrapできたとしても、古いTokenが消えるわけではないためすぐには事故に繋がらなさそうですが、Consulでは一意なreset indexを指定のファイル名・パスに埋め込むというオペミスが起こり得ない安全な設計になっており、自分で何かしらツールを作る時にもこういった設計は参考になりそうだなぁと思いました。

consul image

Consulを使う人が知っておくべきACLを使ったセキュリティ対策

こんにちは、つるべーです!みなさん、Consul使ってますか?
ConsulはHashiCorpが開発するツールで、サービスディスカバリやヘルスチェックなど様々な機能を有しています。Consulは、ノードやサービスの状態変化を起点として、特定の処理を発火させることができるため、『状態の把握 → 変化の検知 → 動的な制御』のような流れがConsulを使って組むことができ、動的に変化するインフラの運用管理に絶大な力を発揮するツールです。

一方で、その多岐にわたる機能のため、仕組みを完全に把握することが難しく、使い方によっては思わぬ危険性を持ってしまうことがあります。今回は、Consulのセキュリティ対策について、ACLという仕組みを中心に書いていきたいと思います。

consul image

目次

セキュリティ対策の概要

Consulのセキュリティ周りの概要を掴むには、Consulの公式ドキュメントにあるSecurity Modelのページを読むのが一番良いと思います。Consulをセキュアに使うために必要な情報がまとまっています。

そのページに以下のようなことが書かれていました。

The Consul threat model is only applicable if Consul is running in a secure configuration. Consul does not operate in a secure-by-default configuration.

意訳すると、「Consulはちゃんとセキュアな設定をしたときだけ安全に使えるよ、そしてデフォルトの設定だけでは安全ではないよ」という感じの内容が書かれています。
このことから、Consulを安全に利用するためには、仕組みをちゃんと理解して、明示的に安全な設定をする必要があるということがわかります。

同記事では、Secure Configurationとして、以下の二つが必要だと挙げています。(注:これだけで十分というわけではない)

  1. ACLを有効にする
    ACLのデフォルトポリシーをdenyにする(いわゆるホワイトリスト方式)

  2. 暗号化を有効にする
    Consul agent間の通信を暗号化するためにTLSを利用する

今回はこのうち、「1. ACLを有効にする」について書きます。

Consul HTTP APIに内在する危険性

Consulは主要なインターフェースの一つとして、RESTful HTTP APIを有しており、ノードやサービスに対するCRUD処理を行うことができます。前述のACLは、このHTTP APIに対するアクセス制限を行うためのものなので、ACLの仕組みをお話する前に、「なぜHTTP APIに対するアクセス制御が必要なのか」について書きます。

そのHTTP APIの危険性を理解するには、以下の公式ブログの記事を読むと良いです。

Protecting Consul from RCE Risk in Specific Configurations

内容としては、HTTP APIを利用したRCE (Remote Code Execution) 、すなわち遠隔からの不正なコマンド実行の危険性があるということが書かれています。これは決してConsulの脆弱性についての話ではなく、Consulの設定を正しく行わないとRCEの危険性を持ってしまうという話です。

具体的には、ヘルスチェック等に使われるチェックスクリプトをHTTP API経由で登録することができるエンドポイント(/agent/check/register)を利用して、任意のスクリプトを登録・実行するといった攻撃手法があるようです。

これは、通常8500ポートでLISTENするConsul HTTP APIを外部に公開している、かつスクリプトによるチェックを有効にしている(-enable-script-checksをtrueにしている)場合に、攻撃の危険性があります。
この攻撃手法を防ぐためには、いくつかの対応方法が考えられるのですが、この問題を非常に簡単に防ぐ方法がConsul 1.3.0からリリースされたようです。

In Consul 1.3.0, released earlier this month, a member of the Consul community contributed a patch that adds a new configuration option, -enable-local-script-checks, which allows script checks to be registered only via local configuration files, thus preventing use of the HTTP API to register malicious checks.

-enable-script-checksの設定の代わりに、-enable-local-script-checksをtrueにすると、チェックスクリプトの登録をHTTP API経由からできなくなるというものです。
この設定を入れると、前述のチェックスクリプト登録による不正コマンド実行の危険性は防ぐことができます。

ただし、これは危険性の一部に過ぎないかもしれません。なぜなら、HTTP APIのエンドポイントの全てを把握するのは困難だし(私はできていない)、これから追加されないとも言えません。問題なのは、リモートからHTTP APIに対して更新系の処理を許してしまっていることにあるのではないかと思います。そのような考えを元に、チェックスクリプト登録を防ぐということに話を絞らず、HTTP APIへのアクセス制御について書いていきたいと思います。

HTTP APIへのセキュリティ対策

1. HTTP APIを外部に公開しない

まず、一番最初にチェックした方が良い設定は、HTTP APIを外部に公開しているかどうかです。Consulが起動している環境が手元にあるのであればnetstatを叩くと即座にわかります。

/ # netstat -ant | grep 8500
tcp        0      0 :::8500                 :::*                    LISTEN

上は、外部に公開している場合の例です。これはConsulの設定の中に、"client_addr": "0.0.0.0"と書いているためです。もしConsulの設定の中でclient_addrを明示的に指定していないのであれば、デフォルトの値として127.0.0.1が採用されるため、HTTP APIが利用する8500ポートは外部から利用できなくなります。
client_addrを明示的に指定しない場合は以下

/ # netstat -ant | grep 8500
tcp        0      0 127.0.0.1:8500          0.0.0.0:*               LISTEN

どうしても外部からHTTP APIを使いたい場合を除いて、デフォルトのローカルホストのみのLISTENにしておいた方が良いでしょう。これにより外部からHTTP APIの参照・更新系の処理を実行することを防げるため、多くの危険性を未然に防ぐことができます。

2. ACLを使ってアクセス権限を制御する

すでに何度か登場しているACLですが、これを使うと、さらに細かくリソース(node、serviceなど)ごとにread・write権限を設定できます。
また、細かいアクセス制限ができるということに加えて、私がACLを使うメリットの一つだと感じたのは、ローカルホストに対するAPI実行にも制限をかけられることです。
1の方法で、APIを外部に公開しなければ、リモートからAPIを実行されることは防げますが、ローカルホストからの実行に対しては、全権限を有してしまっています。これによる危険性は、攻撃者にSSHログインを許してしまった場合はもちろんですが(SSHログインを許した場合の被害はConsulのAPIを実行されるどころじゃないと思うが)、Consulが動いているサーバ上にホストされているWebアプリケーションに何かしら脆弱性があり、そこを経由してローカルホストへのコマンド実行されるケースが想定されると思います。このような場合に備えて、ローカルホストに対するAPIの実行権限も絞っておくべきではないかと考えます。

Consul ACL

Consul ACLを理解するには下記の公式のドキュメントを読むのが一番の近道だと思います。

https://www.consul.io/docs/guides/acl.html

こちらのドキュメントにはACLの概念や仕様だけでなく、ハンズオン形式で試して行けるようになっているため、大変有用です。
ここではドキュメントから私がポイントだと思った部分をかいつまんで説明していきます。

ACLAccess Control Listという名の通り、APIやデータに対するアクセスをコントロール仕組みです。ACLには2つの主要なコンポーネントがあります。

1. ACL Policies

ACL Policies(以下、単にPolicyと呼ぶ)とは、それぞれのリソースに対するパーミッションの許可または拒否ルールの集合です。
ここでいうリソースとは、ドキュメントにあるリソース一覧のとおり、Agent APIを操作するagentリソースや、KV Store APIを操作するkeyリソースなど様々です。
これらにそれぞれ対して、read・write権限を設定することができます。(さらにprefixごとに権限を設定することもできる)

それぞれのリソースに対するルールを集めたものは、HCLの形式で以下のように書くことができ、

acl = "read"
key_prefix "_rexec" {
  policy = "write"
}
・・・

これがPolicyの設定ファイルになります。

2. ACL Tokens

ACL Tokens(以下、単にTokenと呼ぶ)は、Consul agentへのリクエストに対して、呼び出し側がそのアクションを実行する権限を持っているかを判断するために使用されます。
このtokenに前述のpolicyを割り当てることで、リクエストに対する実行権限を制御します。
ACLを有効にすると以下の2つのtokenがデフォルトで設定されます。

  • Master Token
    全権限を保持しているGlobal ManagementというPolicyがアタッチされている。そのため、Master TokenのSecret IDを使うとなんでも実行できる。

  • Anonymous Token
    tokenをつけずにリクエストを送った場合は、Anonymous Tokenが利用される。すなわちtokenなどつけずに何も意識せず、consul membersなどを叩いた時はAnonymous Tokenが利用される。このAnonymous Tokenには自分でPolicyをアタッチすることができる。

ハンズオン

それでは実際にConsul ACLを触ってみましょう。こちらにdocker-composeを利用した検証環境を用意しています。

github.com

検証にはConsul agentのバージョン1.4.0を利用しています。 上のコードをcloneしてリポジトリのトップディレクトリで下記のコマンドを叩くと、ConsulのServerモード1台、Clientモードが1台立ち上がります。

$ docker-compose up -d
Starting consul-acl-playground_consul-server_1 ... done
Starting consul-acl-playground_consul-agent_1  ... done

立ち上げたコンテナの中に入るには、

$ docker exec -it consul-acl-playground_consul-server_1 /bin/ash

コンテナ内のConsul agentのログを見るには、

$ docker logs -f consul-acl-playground_consul-server_1

といった感じのコマンドを実行します。 Consulの設定ファイル(/etc/consul.d/default.json)に以下のように設定して、ACLは有効にしてあります。

"acl": {
  "enabled": true
  ,"default_policy": "deny"
}

"default_policy": "deny"にすることで、ホワイトリスト方式でルールを追加できるので、denyにしておく方が良いです。 それでは立ち上げたコンテナの中に入って、ACLを設定を行っていきます。

ACLのブートストラップ

ServerモードのサーバでACLの初期化を行います。

/ # consul acl bootstrap
AccessorID:   8bd3c315-9155-57d7-a22f-451665f71154
SecretID:     4ec60a89-abaa-fda9-46c1-e6e174094a97
Description:  Bootstrap Token (Global Management)
Local:        false
Create Time:  2018-12-02 06:44:09.0256574 +0000 UTC
Policies:
   00000000-0000-0000-0000-000000000001 - global-management

出力されたSecretIDはMaster tokenのものであるため、どこかに記録しておいたほうが良いでしょう。
Master tokenを使うと、現在発行されているACLのリストを確認できます。

/ # consul acl token list -token=4ec60a89-abaa-fda9-46c1-e6e174094a97
AccessorID:   8bd3c315-9155-57d7-a22f-451665f71154
Description:  Bootstrap Token (Global Management)
Local:        false
Create Time:  2018-12-02 06:44:09.0256574 +0000 UTC
Legacy:       false
Policies:
   00000000-0000-0000-0000-000000000001 - global-management

AccessorID:   00000000-0000-0000-0000-000000000002
Description:  Anonymous Token
Local:        false
Create Time:  2018-12-02 06:33:20.2808862 +0000 UTC
Legacy:       false
Policies:

このようにBootstrap Token(これはMaster tokenのこと)とAnonymous Tokenが発行されていることがわかります。
また、Anonymous TokenにはデフォルトではPolicyがアタッチされていないこともわかります。
したがって、tokenをつけずにconsul membersと叩いても結果が返ってきません。

/ # consul members

Master tokenを使うと当然結果が返ってきます。

/ # consul members -token=4ec60a89-abaa-fda9-46c1-e6e174094a97
Node             Address           Status  Type    Build  Protocol  DC       Segment
consul-server-1  192.168.0.2:8301  alive   server  1.4.0  2         test-dc  <all>
consul-agent-1   192.168.0.3:8301  alive   client  1.4.0  2         test-dc  <default>

そこでまず、Anonymous Tokenに全てのリソースのread権限を与えてみましょう。

Anonymous Tokenに権限を付与

Serverモードのサーバで以下を実行していきます。
Policyは以下のように設定しています。

/ # cat /etc/consul.d/anonymous-policy.hcl
acl = "read"
agent_prefix "" {
    policy = "read"
}
event_prefix "" {
    policy = "read"
}
key_prefix "" {
    policy = "read"
}
keyring = "read"
node_prefix "" {
    policy = "read"
}
operator = "read"
query_prefix "" {
    policy = "read"
}
service_prefix "" {
    policy = "read"
    intentions = "read"
}
session_prefix "" {
    policy = "read"
}

このファイルを元にPolicyを作成します。

/ # consul acl policy create  -name "anonymous-token" -description "Anonymous Token Policy" -rules @/etc/consul.d/anonymous-policy.hcl -token=4ec60a
89-abaa-fda9-46c1-e6e174094a97
ID:           b6f332a9-9f83-2622-2fab-506f85c1e5d8
Name:         anonymous-token
Description:  Anonymous Token Policy
Datacenters:
Rules:
acl = "read"
agent_prefix "" {
    policy = "read"
}
(以下省略)

作成したPolixyをAnonymous Tokenにアタッチします。

/ # consul acl token update -id 00000000-0000-0000-0000-000000000002 --merge-policies -description "Anonymous Token - Read Only" -policy-name anonym
ous-token -token=4ec60a89-abaa-fda9-46c1-e6e174094a97
Token updated successfully.
AccessorID:   00000000-0000-0000-0000-000000000002
SecretID:     anonymous
Description:  Anonymous Token - Read Only
Local:        false
Create Time:  2018-12-02 06:33:20.2808862 +0000 UTC
Policies:
   b6f332a9-9f83-2622-2fab-506f85c1e5d8 - anonymous-token

これで、tokenなしのコマンド実行(Anonymous Token)でreadのコマンドは叩くことができます。以下のようにconsul membersの結果も返ってくるようになりました。

/ # consul members
Node             Address           Status  Type    Build  Protocol  DC       Segment
consul-server-1  192.168.0.2:8301  alive   server  1.4.0  2         test-dc  <all>
consul-agent-1   192.168.0.3:8301  alive   client  1.4.0  2         test-dc  <default>

さぁここまでの設定でMaster tokenのSecret IDを知らないものはローカルホストからの実行でさえ、参照系の処理しかできなくなり、だいぶセキュアになったと思います。
これで完璧かな?と思ったのですが、Consul agentのログをみて見ると、以下のようにエラーとワーニングが出続けています。(下はClientモード側のログ)

2018/12/02 07:00:39 [WARN] agent: Coordinate update blocked by ACLs
2018/12/02 07:01:00 [ERR] consul: "Catalog.Register" RPC failed to server 192.168.0.2:8300: rpc error making call: Permission denied

これはなぜかというと、通常Consul agent同士が協調的に動作するためには、それぞれのnodeが自分自身のnodeの情報を更新する必要があり、現在の設定だと、そのためのTokenおよび権限がないためです。これをAgent Tokenとして付与してやる必要があります。
ドキュメントにもAgent Tokenの役割として、

The acl.tokens.agent is a special token that is used for an agent's internal operations.

と記載されています。次はこれを作っていきます。

Agent Tokenの作成

先に発行したMaster Tokenを使ってAgent Tokenを発行しています。
まず、Agent Token用のPolicyを作成します。これらの操作もServerモードのサーバで行います。

/ # consul acl policy create  -name "agent-token" -description "Agent Token Policy" -rules @/etc/consul.d/agent-policy.hcl -token=4ec60a89-abaa-fda9-46c1-e6e174094a97
ID:           1e812f8f-e72d-43f3-3178-04683bc482f8
Name:         agent-token
Description:  Agent Token Policy
Datacenters:
Rules:
acl = "read"
agent_prefix "" {
    policy = "read"
}

ここでポイントなのが、Agent Token用のPolicyには最低でもnodeリソースに対するwrite権限を与えている必要があります。
上で作成したポリシーをアタッチしたAgent Tokenを発行します。

/ # consul acl token create -description "Agent Token" -policy-name "agent-token" -token=4ec60a89-abaa-fda9-46c1-e6e174094a97
AccessorID:   f604f2f2-ce21-8a64-e9ba-1ef877999742
SecretID:     65736278-e97b-6a43-9893-62f9a8574140
Description:  Agent Token
Local:        false
Create Time:  2018-12-02 07:11:32.8620954 +0000 UTC
Policies:
   1e812f8f-e72d-43f3-3178-04683bc482f8 - agent-token

この時、自動でSecretIDが払い出されます。
この時点からAgent Tokenを使ってアクセスできることがわかります。

/ # consul members -token=65736278-e97b-6a43-9893-62f9a8574140
Node             Address           Status  Type    Build  Protocol  DC       Segment
consul-server-1  192.168.0.2:8301  alive   server  1.4.0  2         test-dc  <all>
consul-agent-1   192.168.0.3:8301  alive   client  1.4.0  2         test-dc  <default>

このAgent tokenのSecret IDを下記のように設定ファイルに埋め込んで、(Serverモード・Clientモードの両方のdefault.json

"acl": {
  "enabled": true
  ,"default_policy": "deny"
  ,"tokens" : {
    "agent" : "65736278-e97b-6a43-9893-62f9a8574140"
  }
}

Consul agentをrestartすると(Dockerコンテナごとrestart)

$ docker restart consul-acl-playground_consul-server_1
consul-acl-playground_consul-server_1
$ docker restart consul-acl-playground_consul-agent_1
consul-acl-playground_consul-agent_1

エラーやワーニングが出なくなり、Consulが正常に動いていることがわかります。
以上の設定で、Tokenを知らない限り、APIに対する更新系の処理は実行できなくなりました。

さいごに

Consulは非常に便利な反面、仕様の理解が難しく、私もなかなか活用しきれてないのが現状です。ただ、非常に将来性を感じるツールですし、他のツールと組み合わせて力を発揮できるようなシーンも考えられるため、これから真剣に触れて学んでいこうと思います!正しく設定を理解してセキュアに使っていきましょう!