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

TravisCIでGitHubのTokenを暗号化する

TravisCIでGitHubのpagesをテストする際、GitHubのトークンが必要になります。.travis.ymlに直接書き込むと、自分のトークンを公開してしまうことになるので、暗号化して登録してみました。本記事では、その方法について記録します。

  • クライアント環境
  • GitHubトークンを入手する
  • Travis CLIのインストール
  • トークンの暗号化

クライアント環境


本記事では、以下の環境で実施しています。

  • MacOS Catalina バージョン10.15.5

GitHubトークンを入手する


GitHubトークンを入手します。

ユーザーアイコンの[Settings]から[Developer Settings]→[Personal access token]→[Generate new token]でトークンを作成します。必要な権限は以下の通りです。

  • repo
  • read:org
  • user:email

今回は入手したトークンを環境変数に設定します。

$ export TRAVIS_GITHUB_TOEKN=[入手したトークン]

Travis CLIのインストール


暗号化に必要なTravis CLIをインストールし、CLIからログインします。

以下のコマンドでTravisをインストールします。

$ gem instal travis

Travis CLIからログインします。

$ travis login --github-token $TRAVIS_GITHUB_TOEKN

デフォルトだとgithub.comにログインしにいくので、エンタープライズ版のGitHubとTravisを利用している場合は、以下のコマンドでエンタープライズ版のURLをエンドポイントに設定します。

. $ travis endpoint --set-default -X -e https://[エンタープライズ版TravisのURL]/api

トークンの暗号化


Travis CLIを利用してトークンの登録及び暗号化を行います 。詳細は公式ドキュメントをご参照ください。

GitHubのトークンを暗号化して、.travis.ymlに設定します。このコマンドは.travis.ymlが置かれているパスで実行する必要があります。

$ travis encrypt GITHUB_TOKEN=$TRAVIS_GITHUB_TOEKN -r [対象レポジトリ] --add

–addオプションによって、同一パス上にある.travis.ymlに以下を追記します。

env:
  global:
    secure: [暗号化されたToken]

最後に、.travis.ymlに暗号化したトークンをdeployブロックで指定して登録しておく必要があります。

deploy:
  github_token: "$GITHUB_TOKEN"

これでTravis CIからGitHubのテストを行うことができました。

以上です。

Mavenのアーキテクチャについて

本記事では、Mavenのアーキテクチャを紹介します。

  • Mavenのアーキテクチャ
  • Mavenのビルドステップ
    • コンパイル
    • テスト
    • パッケージング
    • インストール
    • デプロイ

Mavenのアーキテクチャ


Mavenのアーキテクチャは、「pom.xml」「ローカルレポジトリ」「リモートレポジトリ」の三つがあります。それぞれの役割は以下の通りです。

  • pom.xml・・・ビルドから成果物の管理までのプロセスを記述したファイル
  • ローカルレポジトリ・・・リモートレポジトリからダウンロードしたライブラリやプラグインを保存しておく場所
  • リモートレポジトリ・・・ローカルレポジトリに必要なライブラリやプラグインが存在しない場合に、参照するレポジトリ

それぞれの関係を図示すると以下の通りです。pom.xmlには、「コンパイル」「テスト」「パッケージング」「インストール」「デプロイ」のステップがあります。各ステップについては、次節で説明します。

Mavenのビルドステップ


pom.xmlに記述する各ビルドステップについて説明します。

コンパイル


Javaソースコードをコンパイルして実行可能なファイルを生成をするステップで、以下のコマンドで単独実行できます。

$ mvn compile

コンパイルステップでは、target/classesにクラスファイルが生成されます。

テスト


ユニットテストを実行するステップで、以下のコマンドで単独実行できます。

$ mvn test

デフォルトでは以下のファイル名をテストクラスとして実行します。他のファイル名で実行したい場合は、pom.xmlのbuildタグで定義する必要があります。

  • **/Test*.java
  • **/*Test.java
  • **/*Tests.java
  • **/*TestCase.java

デフォルトで以下のAppTest.javaが作成されています。assertTrueは引数の値をtrueかfalseを確認するメソッドなので、必ず成功するテストクラスです。

package com.example;

import static org.junit.Assert.assertTrue;

import org.junit.Test;

/**
 * Unit test for simple App.
 */
public class AppTest 
{
    /**
     * Rigorous Test :-)
     */
    @Test
    public void shouldAnswerWithTrue()
    {
        assertTrue( true );
    }
}

junitをimportしているので、junitのライブラリが必要になります。デフォルトのpom.xmlではdependenciesタグにjunitが定義されていて、ライブラリを取得するよう定義しています。

~~ pom.xmlより抜粋 ~~
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
~~ pom.xmlより抜粋 ~~

パッケージング


JARファイルを作成するステップで、以下のコマンドで単独実行できます。

$ mvn package

デフォルトではtarget/hello-world-1.0-SNAPSHOT.jarが作成されます。ファイル名は、pom.xmlのartifactIdversionタグに指定した名前になります。

~~ pom.xmlより抜粋 ~~
  <artifactId>hello-world</artifactId>
  <version>1.0-SNAPSHOT</version>
~~ pom.xmlより抜粋 ~~

インストール


作成したJARファイルをローカルレポジトリに登録するステップで、以下のコマンドで単独実行できます。

$ mvn install

インストールしたJARファイルは他のプロジェクトで利用することができます。

デプロイ


作成したJARファイルをリモートレポジトリに登録するステップで、以下のコマンドで単独実行できます。

$ mvn deploy

インストールとの違いは、登録したJARファイルを他のマシンでも利用できるかどうかです。デプロイステップで登録したJARファイルは、以下のように他のマシンでも利用が可能になります。

リモートレポジトリはpom.xmlで設定できます。以下のようにdistributionManagementタグで李モートレポジトリのURLを指定します。

~~ pom.xmlより抜粋 ~~
<project>
  <distributionManagement>
    <repository>
      <id>[Remote Repository ID]</id>
      <name>[Remote Repository name]</name>
      <url>[Remote Repository URL]</url>
    </repository>
  </distributionManagement>
</project>
~~ pom.xmlより抜粋 ~~

以上です。次はpom.xmlの内容について詳しく調べていきたいとお思います。

Apache MavenをMacにインストールしてHello World

前から気になっていたMavenの使い勝手を試そうと、まずはMacにインストールしてHello Worldを表示してみました。

  • Apache Mavenとは
  • Apache Mavenのインストール
    • 事前準備
    • インストール
    • Hello World

Apache Mavenとは


Apache Mavenとは、Javaソースコードをビルドするためのオープンソースソフトウェア(OSS)です。
ソースコードのコンパイルやユニットテストに加え、アーティファクト(WAR, EARなど)の生成が可能です。

Apache Mavenのインストール


事前準備


Apache Mavenのインストールを始める前に、以下の準備を行います。

  • Apache Mavenのダウンロード
  • JAVA_HOMEの設定

Apache Mavenのダウンロード


公式サイトから、Apache Mavenのモジュールをダウンロードします。本記事では、「Binary tar.gz archive」を選択します。

JAVA_HOMEの設定


インストールするには、JAVA_HOMEの設定が必要です。以下のように設定済みです。

$ echo $JAVA_HOME
/Library/Java/JavaVirtualMachines/jdk1.8.0_77.jdk/Contents/Home

インストール


ファイルの解凍


以下のコマンドでファイルを解凍します。

$ tar xzvf apache-maven-3.6.3-bin.tar

解凍後に作成されるディレクトリを/opt配下に移動します。

$ sudo mv apache-maven-3.6.3 /opt/

環境パスへの追加


コマンド実行できるように、以下のコマンドでPATH環境変数に/opt/apache-maven-3.6.3/binを追加します。

$ export PATH=/opt/apache-maven-3.6.3/bin:$PATH

mvnコマンドが実行できることを確認します。

$ mvn -v
Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
Maven home: /opt/apache-maven-3.6.3
Java version: 1.8.0_77, vendor: Oracle Corporation, runtime: /Library/Java/JavaVirtualMachines/jdk1.8.0_77.jdk/Contents/Home/jre
Default locale: ja_JP, platform encoding: UTF-8
OS name: "mac os x", version: "10.15.5", arch: "x86_64", family: "mac"

Hello World


Apache Mavenを使って、Javaプログラムのコンパイルを試します。

ソースコードの作成


以下のコマンドでJavaプログラムの雛形を作成します。

$ mkdir hello-world
$ cd hello-world/
$ mvn archetype:generate \
   -DgroupId=com.example \
   -DartifactId=hello-world

途中で質問されますが、全てEnterでデフォルトを選択すると、以下のようにBUILD SUCCESSが表示されます。

[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating project from Archetype: maven-archetype-quickstart:1.4
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: com.example
[INFO] Parameter: artifactId, Value: hello-world
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: package, Value: com.example
[INFO] Parameter: packageInPathFormat, Value: com/example
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: package, Value: com.example
[INFO] Parameter: groupId, Value: com.example
[INFO] Parameter: artifactId, Value: hello-world
[INFO] Project created from Archetype in dir: /Users/XXX/Downloads/hello-world/hello-world
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  36.709 s
[INFO] Finished at: 2020-09-28T00:53:14+09:00
[INFO] ------------------------------------------------------------------------

以下のコマンドで確認すると、ディレクトリ構造が作成されていることがわかります。

$ tree
.
└── hello-world
    ├── pom.xml
    └── src
        ├── main
        │   └── java
        │       └── com
        │           └── example
        │               └── App.java
        └── test
            └── java
                └── com
                    └── example
                        └── AppTest.java

10 directories, 3 files

App.javaを確認すると、デフォルトでHello Worldプログラムのソースコードであることがわかります。今回はこのまま使用します。

$ cat hello-world/src/main/java/com/example/App.java 
package com.example;

/**
 * Hello world!
 *
 */
public class App 
{
    public static void main( String[] args )
    {
        System.out.println( "Hello World!" );
    }
}

Jarファイルを作成する


作成したソースコードをJarファイルに圧縮します。

$ cd hello-world/
$ mvn package
[INFO] Scanning for projects...
[INFO] 
[INFO] ----------------------< com.example:hello-world >-----------------------
[INFO] Building hello-world 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
〜〜省略〜〜
[INFO] Building jar: /Users/XXX/Downloads/hello-world/hello-world/target/hello-world-1.0-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  53.662 s
[INFO] Finished at: 2020-09-28T01:07:29+09:00
[INFO] ------------------------------------------------------------------------

targetディレクトリ配下にJarファイルが作成されていることがわかります。

$ tree
.
├── pom.xml
├── src
│   ├── main
│   │   └── java
│   │       └── com
│   │           └── example
│   │               └── App.java
│   └── test
│       └── java
│           └── com
│               └── example
│                   └── AppTest.java
└── target
    ├── classes
    │   └── com
    │       └── example
    │           └── App.class
    ├── generated-sources
    │   └── annotations
    ├── generated-test-sources
    │   └── test-annotations
    ├── hello-world-1.0-SNAPSHOT.jar
    ├── maven-archiver
    │   └── pom.properties
    ├── maven-status
    │   └── maven-compiler-plugin
    │       ├── compile
    │       │   └── default-compile
    │       │       ├── createdFiles.lst
    │       │       └── inputFiles.lst
    │       └── testCompile
    │           └── default-testCompile
    │               ├── createdFiles.lst
    │               └── inputFiles.lst
    ├── surefire-reports
    │   ├── TEST-com.example.AppTest.xml
    │   └── com.example.AppTest.txt
    └── test-classes
        └── com
            └── example
                └── AppTest.class

28 directories, 13 files

実行する


作成したJarファイルを使って、Hello Worldプログラムを実行します。

$ java -cp target/hello-world-1.0-SNAPSHOT.jar com.example.App
Hello World!

以上です。とりあえず要領が分かったので、これから色々と使ってみたいと思います。

TensorFlowのチュートリアル から回帰による予測方法を学ぶ

TensorFlowのチュートリアル から回帰分析による予測について勉強したのでメモします。これから学習する人の参考になれば幸いです。

  • 回帰分析とは
    • 単回帰分析とは
    • 重回帰分析とは
  • TensorFlowのチュートリアル
    • チュートリアルの概要 
    • データの準備
    • モデルの構築
    • モデルの検証

回帰分析とは


回帰分析とは、連続するデータを関数モデルに適用し、変数からデータの値を分析・予測することです。つまり、y=f(x)というモデルを適用することで、説明変数 xから目的変数 yを予測することです。

連続するデータというのがキーワードで、機械学習の中では連続的な値を予測するために用います。例えば、価格や確率を予測するのに利用します。今回のチュートリアルでは自動車の燃費量を予測しています。

一方、分類法は目的によって離散的な値を予測することもあります。以下の「TensorFlowのチュートリアルから2クラス分類について学ぶ」で例に出した2クラス分類法はシグモイド関数を使っているので連続的な値ですが、出力が0か1(否定的か肯定的か)のみの場合は離散的な値の予測となります。

単回帰分析とは


単回帰分析とは、一つの説明変数から一つの目的変数で成り立つ回帰分析です。

例) y = ax + b

重回帰分析とは


重回帰分析とは、二つ以上の説明変数から一つの目的変数で成り立つ回帰分析です。

例)y = a1x1 + a2x2 + b

TensorFlowのチュートリアル


TensorFlowのチュートリアル 「回帰:燃費を予測する」を実施して、回帰による予測について勉強します。詳細については、実際にチュートリアルを実施するのが一番理解を深めると思いますので、あくまでも学習の補佐として読んでいただければと思います。

チュートリアルの概要


このチュートリアルでは、1970年代後半から1980年代初めの自動車の燃費を予測するモデルを構築し、自動車の排気量や馬力などから燃費を予測します。

このチュートリアルも大きく以下の流れに沿って進みます。

  • データの準備
  • モデルの構築
  • モデルの検証

データの準備


データのダウンロード


データは、UCI Machine Learning Repositoryから取得しており、それぞれの自動車に対して以下の情報が含まれています。

  • MPG・・・自動車の走行燃費量(miles per gallonの略)
  • Cylinders・・・自動車のシリンダー数
  • Displacement・・・自動車の排気量
  • Horsepower・・・自動車の馬力
  • Weight・・・自動車の重さ
  • Acceleration・・・自動車の加速性能
  • Model Year・・・自動車の製造年
  • Origin・・・自動車の製造元(1~3で区別しており、1がUSA、2がEurope、3がJapan)

データのクレンジング


ここでは以下を実施しています。

  • データが欠損している自動車のデータを削除
  • 製造元の”Origin”列を”USA”, “Europe”, “Japan”に変換して0と1で表現

ラベルと特徴量の分離


ここでは目的変数である燃費量(MPG)を説明変数から削除しています。燃費量を予測したいのに、予測するデータの中に燃費量が入っていると意味ないからです。

データの正規化


 データの特徴量の範囲(min~maxの間)が広いとモデルの訓練が難しくなるそうで、( データの特徴量 – 代表値 ) / 標準偏差で正規化を行っています。

モデルの構築


krasのSequentialモデルを利用してモデルを構築しています。

  model = keras.Sequential([
    layers.Dense(64, activation='relu', input_shape=[len(train_dataset.keys())]),
    layers.Dense(64, activation='relu'),
    layers.Dense(1)
  ])

Sequentialモデルとは、層を積み重ねたモデルのことです。このモデルを利用する場合は、最初のレイヤーに入力のshapeについて情報を与える必要があります。情報を与える方法として以下の3通りあり、チュートリアルでは以下の方法でinput_shape引数にデータセットの列数を指定しています。

  • 最初のレイヤーの input_shape引数を指定する.この引数にはshapeを示すタプルを与えます(このタプルの要素は整数か Noneを取ります.Noneは任意の正の整数を期待することを意味します).

参照元:https://keras.io/ja/getting-started/sequential-model-guide/

また、ここではモデルの活性化関数(activation)にreluを利用しています。
reluは正規化線形関数で、0と説明変数を比較して大きい値を目的変数とする関数です。0以上の値が返ってくる、かつ勾配が一定になることから、勾配消失に有効な関数です。

reluを活性化関数にすることで、ニューラルネットワークのイメージは以下のようになります。

モデルのコンパイルは以下のように記述しています。

  optimizer = tf.keras.optimizers.RMSprop(0.001)

  model.compile(loss='mse',
                optimizer=optimizer,
                metrics=['mae', 'mse'])

損失関数がmse(Mean Squared Error:平均二乗誤差)で、オプティマイザーには学習率0.001のRMSpropアルゴリズムが利用されています。評価関数には、mae(Mean Absolute Error: 平均絶対誤差)とmse(Mean Squared Error:平均二乗誤差)が利用されています。

モデルの全体図は以下の通りです。

モデルの検証


モデルの訓練


訓練用データを引数にmodel.fit関数を使って、モデルを訓練しています。

チュートリアルではモデルを1000エポック訓練しますが、100エポックを過ぎてから検証スコアが悪化するので、検証スコアが10エポック以内に改善しなかったら訓練を止めるようにEary Stoppingを使っています。

# patience は改善が見られるかを監視するエポック数を表すパラメーター
early_stop = keras.callbacks.EarlyStopping(monitor='val_loss', patience=10)

history = model.fit(normed_train_data, train_labels, epochs=EPOCHS,
                    validation_split = 0.2, verbose=0, callbacks=[early_stop, PrintDot()])

最後の結論にもありますが、Early Stoppingは過学習を防止するのに有効な手法です。しかしチュートリアルでは、Early Stoppingを使うことで、40エポック辺りで訓練が終了してしまい訓練用データが少なくなってしまうという事態に陥っています。この辺りはチューニングが必要になってくると思われます。

モデルのテスト


テストデータを引数にmodel.predict関数を使って、燃費量を予測しています。誤差は少ないようですが、訓練用データが少ないために正規頒布の形にはなってないみたいですね。

以上です。

TensorFlowのチュートリアルから2クラス分類について学ぶ

TensorFlowのチュートリアルである「映画レビューのテキスト分類」から2クラス分類(二値分類とも呼ばれる)について勉強したことをメモします。これから学習する人の参考になれば幸いです。

  • 2クラス分類について
    • 2クラス分類(二値分類)とは
    • 多クラス分類とは
    • 2クラス分類と多クラス分類の違い
  • チュートリアル
    • データの準備
    • モデルの構築
    • モデルの訓練と検証

2クラス分類について


2クラス分類(二値分類)とは


2クラス分類とは、任意のオブジェクトを二つのオブジェクトに分類することです。例えば、TensorFlowのチュートリアルでは「映画のレビュー」を「肯定的レビュー」か「否定的レビュー」のどちらかに分類しています。

多クラス分類とは


チュートリアルとは関係ないですが、2クラス分類の理解をより深めるために多クラス分類についても学んでおきます。

二つよりも多いオブジェクトに分類する場合は多クラス分類と呼ばれます。例えば、フルーツの写真を「りんご」「バナナ」「いちご」に分類する場合は多クラス分類です。

2クラス分類と多クラス分類の違い


チュートリアルとは関係ないですが、ここで2クラス分類との違いについて学んでおきます。

2クラス分類の大きな特徴は、出力層のニューロンが一つであることです。出力結果を0~1の範囲にして、二つのオブジェクトのパーセンテージを表すことが多いです。TensorFlowのチュートリアルでは、0が「否定的レビュー」、1が「肯定的レビュー」として表現しています。2クラス分類の出力結果にはシグモイド関数が利用されます。

一方、多クラス分類はカテゴリ毎に出力層のニューロンが存在します。フルーツの写真の例だと「りんご」「バナナ」「いちご」毎にニューロンがあり、それぞれ0~1の範囲で出力します。ここで重要なのは、出力結果の合計が必ず1になることです。ソフトマックス関数が利用され、「りんごである確率60%」「バナナである確率10%」「いちごである確率20%」といったように、出力の合計が1になります。

チュートリアル


チュートリアルは、大きく以下の流れで進みます。

  • データの準備
  • モデルの構築
  • モデルの訓練と検証

データの準備


レビューデータについては、Internet Movie Databaseから抽出した整形済みの IMDB datasetをダウンロードして利用しています。利用時には全てのレビューを同じデータ長にしています。

モデルの構築


モデルの各層の説明については、以下の通りチュートリアルに詳しく記載されているので、割愛します。

  1. 最初の層はEmbedding(埋め込み)層です。この層は、整数にエンコードされた語彙を受け取り、それぞれの単語インデックスに対応する埋め込みベクトルを検索します。埋め込みベクトルは、モデルの訓練の中で学習されます。ベクトル化のために、出力行列には次元が1つ追加されます。その結果、次元は、(batch, sequence, embedding)となります。
  2. 次は、GlobalAveragePooling1D(1次元のグローバル平均プーリング)層です。この層は、それぞれのサンプルについて、シーケンスの次元方向に平均値をもとめ、固定長のベクトルを返します。この結果、モデルは最も単純な形で、可変長の入力を扱うことができるようになります。
  3. この固定長の出力ベクトルは、16個の隠れユニットを持つ全結合(Dense)層に受け渡されます。
  4. 最後の層は、1個の出力ノードに全結合されます。シグモイド(sigmoid)活性化関数を使うことで、値は確率あるいは確信度を表す0と1の間の浮動小数点数となります。

映画レビューのテキスト分類より抜粋

compile関数を使って上記のモデルをコンパイルしています。

model.compile(optimizer='adam',
              loss='binary_crossentropy',
              metrics=['accuracy'])

各引数の説明は以下の通りです。

  • optimizer・・・重みの修正を行うアルゴリズムを指定します。指定できるアルゴリズムの詳細についてはこちら
  • loss・・・予測データと正解データの誤差を表す損失関数を指定します。指定できる損失関数の詳細についてはこちら
  • metrics・・・モデル全体の評価を行う関数を指定します。accuracyの場合は正解率で評価します。指定できる関数についての詳細はこちら

ニューラルネットワークのモデルを訓練する時は、予測データと正解データの誤差に従って層間の重みを修正していきます。上記のcompile関数における引数の役割を図示しました。

モデルの訓練と検証


本チュートリアルでは、上記で作成したモデルの訓練と検証まで実施しています。

訓練と検証の結果をエポック数と損失、エポック数と正解率のグラフで図示しています。エポック数は、一つの訓練データの学習回数を示します。

訓練中にエポック数が増える度に損失が減少し、正解率が上昇していることがわかります。しかし、検証では20エポック過ぎた辺りから損失も正解率も横ばいになります。

ニューラルネットワークはモデルを訓練すればするほど精度が上がるというわけではありません。学習して欲しくないパターンもあるからです。学習して欲しくないパターンを学習することによって、訓練用データでの正解率が上がる一方で、検証用データでの正解率が上がらない現象が発生します(過学習またはオーバーフッティングと呼びます)。チュートリアルでは、最適なエポック数が20程度で、それ以降は過学習ということがグラフからわかります。

以上です。

TensorFlowのチュートリアルをDockerで動かしてみる

AIOpsなどの波に押し寄せられ、機械学習について最初の一歩を恐る恐る踏み出してみました。これから始める方のために一応残しておきます。

  • TensorFlowとは
  • TensorFlowをローカルで動かす
  • TensorFlowのチュートリアルを試す

TensorFlowとは


公式ドキュメントでは、以下の通り記載があります。

TensorFlow は、機械学習向けに開発されたエンドツーエンドのオープンソース プラットフォームです。
引用元:https://www.tensorflow.org

Machine Learning(ML)を使ったアプリケーションを開発・デプロイするために作成されました。

TensorFlowをローカルで動かす


TensorFlowをローカルで動かすには、以下二通りの方法があります。本記事ではリンク先を参考に、コンテナとして実行する方法で動かします。

  • pipコマンドによるインストール
  • Dockerによるコンテナ実行

1. コンテナイメージをpullする


以下のコマンドでTensorFlowのコンテナイメージを取得します。

$ docker pull tensorflow/tensorflow:nightly-py3-jupyter

コンテナイメージにおけるタグ情報のオプションはリンク先をご参照ください。ここでは、python3のサポートとJupyterを含むナイトリービルド(最新ソースコード)のコンテナイメージを取得しています。

ちなみにJupyterとは、プログラミング言語をウェブブラウザ上で実行できるドキュメントツールです。ソースコードと説明文が混在したドキュメントを作成し、ソースコードとその実行結果をインタラクティブに表示することができます。

2. コンテナを起動する


以下のコマンドで、手順1で取得したコンテナイメージからコンテナを起動します。

$ docker run -it -p 8888:8888 tensorflow/tensorflow:nightly-py3-jupyter
[I 14:54:21.897 NotebookApp] Writing notebook server cookie secret to /root/.local/share/jupyter/runtime/notebook_cookie_secret
jupyter_http_over_ws extension initialized. Listening on /http_over_websocket
[I 14:54:22.459 NotebookApp] Serving notebooks from local directory: /tf
[I 14:54:22.459 NotebookApp] The Jupyter Notebook is running at:
[I 14:54:22.461 NotebookApp] http://453c162dd9e3:8888/?token=d233277b7f66020bbc79ea155a906ed4c41fc3846634511d
[I 14:54:22.461 NotebookApp]  or http://127.0.0.1:8888/?token=d233277b7f66020bbc79ea155a906ed4c41fc3846634511d
[I 14:54:22.463 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
[C 14:54:22.471 NotebookApp] 
    
    To access the notebook, open this file in a browser:
        file:///root/.local/share/jupyter/runtime/nbserver-1-open.html
    Or copy and paste one of these URLs:
        http://453c162dd9e3:8888/?token=d233277b7f66020bbc79ea155a906ed4c41fc3846634511d
     or http://127.0.0.1:8888/?token=d233277b7f66020bbc79ea155a906ed4c41fc3846634511d

3. ブラウザからログインする


標準出力されたOr copy and paste one of these URLs配下のURLをコピーしてブラウザアクセスすると、以下の画面が表示されます。

TensorFlowのチュートリアルを試す


Dockerで動かしたJupyterを使って、TensorFlowのチュートリアル(分類問題の初歩)を試してみます。このチュートリアル自体は、機械学習のHello Worldに該当するそうです。

1. ipynbファイルを表示する


「tensorflow-tutorials」→「classification.ipynb」をクリックします。

2. 「Run」ボタンをクリックする


「Run」ボタンをクリックし、上から順にソースコードを実行していきます。それぞれの出力結果については、チュートリアルに記載されているので省略します。

チュートリアルで実施している内容をざっくり要約すると以下になります。

以上です。チュートリアルを一通りやるだけでも勉強になりました。

SwaggerEditorで生成したGo API Serverの処理を実装する

  • はじめに
  • APIの全体像
  • MongoDBの作成
    • Dockerfileの作成
    • Dockerビルド
  • Go-Serverの作成
    • APIの実装
      • CreateUserメソッドの実装
      • GetUserByNameメソッドの実装
      • go/api_user.goの全体
    • Dockerfileの作成
    • Dockerビルド
  • APIの確認
    • docker-compose.yamlの作成
    • コンテナの起動
    • curlによるAPIの確認

はじめに


SwaggerEditorはOpenAPI specを様々な言語に変換することができます。以下の記事では、SwaggerEditorを使って簡単なOpenAPI specを作成し、Go-Serverに変換するところまで紹介しました。

MacにSwaggerEditorをインストールしてOpenAPI specを実装する

しかし、あくまでもAPIの雛形であって、中身の処理が実装されているわけではありません。本記事では、上記の記事で作成したGo-Serverに処理を実装します。

APIの全体像


本記事で作成するAPIの全体像について紹介します。

APIの仕様は以下の通りです。DBにはMongoDBを利用し、apiとdbはそれぞれコンテナで起動します。

  • POST /v2/user/create・・・Context-TypeでJson形式のユーザー情報を受け付け、DBに保管します。
  • GET /v2/user/{username}・・・URLに指定したusernameとマッチするレコードをDBから取得し、表示します。

MongoDBの作成


Dockerfileの作成


MongoDBのDockerfileを作成します。

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

$ tree mongo/
mongo/
├── Dockerfile
├── init.sh
└── users.json

今回は揮発性のDBコンテナにします。コンテナ起動時に初期データ(users.json)を流し込むinit.shも併せて作成します。

1. 初期データの作成


コンテナ起動時に流し込む初期データをjson形式で作成します。

[
  { "username":	"koratta", "email": "koratta@example.com" },
  { "username":	"koratta2", "email": "koratta2@example.com" }
]

2. 初期実行ファイルの作成


1で作成したJsonファイルをMongoDBに保管するためのスクリプトinit.shを作成します。

mongoimport --authenticationDatabase admin --username root --password password --db api --collection users --drop --file /docker-entrypoint-initdb.d/users.json --jsonArray

3. Dockerfileの作成


init.shをコンテナ起動時に実行するmongoベースのDockerfileを作成します。コンテナ起動時に実行するため、/docker-entrypoint-initdb.dにコピーします。

FROM mongo:latest
COPY users.json /docker-entrypoint-initdb.d/
COPY init.sh /docker-entrypoint-initdb.d/

Dockerビルド


上記で作成したDockerfileをビルドし、コンテナイメージを作成します。

$ docker build -t api-mongo-db:latest mongo/

Go-Serverの作成


APIの実装


APIリクエストのルーティング情報は、go/routers.goに記述されています。

~~go/router.goより抜粋~~
var routes = Routes{ Route{ "Index", "GET", "/v2/", Index, }, Route{ "CreateUser", strings.ToUpper("Post"), "/v2/user/create", CreateUser, }, Route{ "GetUserByName", strings.ToUpper("Get"), "/v2/user/{username}", GetUserByName, }, }

/v2/user/createに対してPOSTされたらCreateUserメソッドを、/v2/user/{username}に対してGETされたらGetUserBynameメソッドを呼び出すようになっています。それぞれのメソッドはgo/api_user.goで定義されているので、それぞれを実装します。

CreateUserメソッドの実装


CreateUserメソッドを以下に示します。

func CreateUser(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json; charset=UTF-8")

	//ヘッダー情報の取得
	if r.Header.Get("Content-Type") != "application/json" {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	length, err := strconv.Atoi(r.Header.Get("Content-Length"))
	body := make([]byte, length)
	length, err = r.Body.Read(body)
	var insert_record User
	err = json.Unmarshal(body[:length], &insert_record)

	//DBへのInsert
	connect := getConnect()
	_, err = connect.Database("api").Collection("users").InsertOne(context.Background(), insert_record)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	w.WriteHeader(http.StatusOK)
}

9~11行目でPOSTされたJson形式のユーザー情報を取得しています。

	length, err := strconv.Atoi(r.Header.Get("Content-Length"))
	body := make([]byte, length)
	length, err = r.Body.Read(body)

12~13行目でUser構造体のinsert_recordに変換しています。

	var insert_record User
	err = json.Unmarshal(body[:length], &insert_record)

このUser構造体は、OpenAPI specから生成されていて、go/model_user.goに記述されています。

package swagger

type User struct {

	Username string `json:"username,omitempty"`

	Email string `json:"email,omitempty"`
}

16~17行目でMongoDBに保管しています。getConnectメソッドはMongoDBとの接続を確立するメソッドで、go/api_user.goの全体に載せています。

	connect := getConnect()
	_, err = connect.Database("api").Collection("users").InsertOne(context.Background(), insert_record)

GetUserByNameメソッドの実装


GetUserByNameメソッドを以下に示します。

func GetUserByName(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json; charset=UTF-8")

	//検索ユーザー情報の取得
	user := strings.Replace(r.URL.Path, "/v2/user/", "", 1)

	//user情報の取得
	var output User
	connect := getConnect()
	filter := bson.D{{"username", user }}
	err := connect.Database("api").Collection("users").FindOne(context.Background(), filter).Decode(&output)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	//Jsonの出力
	json.NewEncoder(w).Encode(output)

	w.WriteHeader(http.StatusOK)
}

5行目で検索条件となるユーザー名を取得しています。今回のAPIの仕様は、URLのパスに指定されたユーザー名の情報を表示するので、URLパスからユーザー名を取得しています。

user := strings.Replace(r.URL.Path, "/v2/user/", "", 1)

9~11行目で、URLに指定されたユーザー名を基にMongoDBから情報を取得しています。

	connect := getConnect()
	filter := bson.D{{"username", user }}
	err := connect.Database("api").Collection("users").FindOne(context.Background(), filter).Decode(&output)

go/user_api.goの全体


user_api.goの全体は以下の通りです。

/*
 * Koratta Test API
 *
 * This is sample api
 *
 * API version: 1.0.0
 * Contact: koratta@example.com
 * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
 */

package swagger

import (
	"net/http"
	"log"
	"time"
	"encoding/json"
	"context"
	"strconv"
	"strings"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
	"go.mongodb.org/mongo-driver/bson"
)

func CreateUser(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json; charset=UTF-8")

	//ヘッダー情報の取得
	if r.Header.Get("Content-Type") != "application/json" {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	length, err := strconv.Atoi(r.Header.Get("Content-Length"))
	body := make([]byte, length)
	length, err = r.Body.Read(body)
	var insert_record User
	err = json.Unmarshal(body[:length], &insert_record)

	//DBへのInsert
	connect := getConnect()
	_, err = connect.Database("api").Collection("users").InsertOne(context.Background(), insert_record)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	w.WriteHeader(http.StatusOK)
}

func GetUserByName(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json; charset=UTF-8")

	//検索ユーザー情報の取得
	user := strings.Replace(r.URL.Path, "/v2/user/", "", 1)

	//user情報の取得
	var output User
	connect := getConnect()
	filter := bson.D{{"username", user }}
	err := connect.Database("api").Collection("users").FindOne(context.Background(), filter).Decode(&output)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	//Jsonの出力
	json.NewEncoder(w).Encode(output)

	w.WriteHeader(http.StatusOK)
}

func getConnect() *mongo.Client{
	//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))
	if err != nil { log.Fatal(err) }

	return connect
}

Dockerfileの作成


Dockerfileを作成します。

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

$ tree api
api
├── Dockerfile
└── src
    ├── api
    │   └── swagger.yaml
    ├── go
    │   ├── README.md
    │   ├── api_user.go
    │   ├── logger.go
    │   ├── model_user.go
    │   └── routers.go
    └── main.go

src配下のファイルは、MacにSwaggerEditorをインストールしてOpenAPI specを実装するで作成したソースコードです。

Dockerfileは以下の通りです。

FROM golang:latest
# gouserの作成
RUN useradd -m -s /bin/bash gouser
# コンテナ作業ディレクトリの変更
WORKDIR /go/src/api
RUN chown gouser:gouser /go/src/api
# モジュールのダウンロード
RUN go get -u github.com/gorilla/mux &&\
    go get -u "go.mongodb.org/mongo-driver/mongo"
# ホストOSの ./src の中身を作業ディレクトリにコピー
COPY --chown=gouser:gouser ./src .
# go build
RUN go build -o koratta-api &&\
    chown gouser:gouser api
# gouserに変更
USER gouser
# API実行コマンドの実行
CMD ["/go/src/api/koratta-api"]

必要なモジュールのgorilla/muxとmongo-driver/mongoをインストールし、gouserに実行権限を与えてgouserで実行するようにしています。root権限でも当然実行できます。

注意点は、main.goのimportパッケージのsw "go"部分です。コンテナの場合デフォルトで$GOPATH=/go/srcなので、上記のDockerfileの場合sw "api/go"になります。

APIの確認


docker-compose.yamlの作成


MongoDBとAPIのコンテナを起動するためのdocker-compose.yamlを作成します。

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

コンテナの起動


docker-composeを使ってコンテナを起動します。

$ docker-compose up -d

curlによるAPIの確認


作成したAPIの挙動を確認します。

初期データとして保管されているレコードをAPI経由で確認すると、以下の通り、指定したユーザー名に該当する情報を取得できます。

$ curl -X GET "http://localhost:8080/v2/user/koratta"
{"username":"koratta","email":"koratta@example.com"}

保管されていないユーザーは、当然取得できません。

$ curl -X GET "http://localhost:8080/v2/user/koratta5
$

従って、APIを使ってユーザー情報を保管します。

$ curl -X POST "http://localhost:8080/v2/user/create" -H "accept: application/xml" -H "Content-Type: application/json" -d "{ \"username\": \"koratta5\", \"email\": \"koratta5@example.com\"}"

再度確認すると、ユーザー情報を確認することができます。/v2/user/createのAPI経由でDBに保管できていることが確認できました。

$ curl -X GET "http://localhost:8080/v2/user/koratta5"
{"username":"koratta5","email":"koratta5@example.com"}

以上です。

MacにSwaggerEditorをインストールしてOpenAPI specを実装する

  • Swaggerとは
  • Swaggerの利用方法
  • SwaggerEditorのインストール
  • OpenAPI specの実装
  • OpenAPI specからGo Serverの実行

Swaggerとは


Swaggerは、API開発フレームワークのオープンソースツールです。OpenAPI仕様のREST API設計・構築・ドキュメント化を行うことができます。

OpenAPIとは、REST APIを記述するためのフォーマットです。OpenAPI specとして、APIの雛形をJsonもしくはYaml形式で記述することができます。OpenAPIの仕様については、こちらに記載されています。

Swaggerには、以下のツールが含まれています。

  • Swagger Editor – ブラウザベースでOpenAPI specを実装することができるツールです。
  • Swagger UI – OpenAPI specからドキュメントをレンダリングするツールです。
  • Swagger Codegen – OpenAPI specからサーバースタブとクライアントライブラリを生成するツールです。

本記事では、Swagger Editorで簡単なOpenAPI specを記述し、Goで実行するところまで紹介します。

Swaggerの利用方法


Swaggerを利用するには、以下の方法があります。

  • オンライン利用・・・リンク先にアクセスしてSwaggerEditorを利用する方法です。
  • ローカル利用・・・SwaggerEditorをダウンロードして利用する方法で、以下の二通りがあります。
    • ローカルファイルによる実行・・・ダウンロードしたSwaggerファイルをブラウザで表示する方法です。
    • Dockerによる実行・・・DockerHubからコンテナイメージをプルして実行する方法です。手順はこちらです。

本記事では、ローカルのSwaggerEditorファイルを利用する方法を紹介します。

SwaggerEditorのインストール


1. GitHubからソースをダウンロードする


$ curl -L -O https://github.com/swagger-api/swagger-editor/archive/v3.11.7.zip

ダウンロードのバージョン情報はこちらです。

2. ファイルを解凍する


$ unzip v3.11.7.zip 

3. index.htmlをブラウザで表示する


$ open ./swagger-editor-3.11.7/index.html

以下の画面が表示されます。

OpenAPI specの実装


SwaggerEditorを利用してOpenAPI specを作成します。デフォルトで作成されているyamlファイルはボリュームがあるので、一部を抜き取って編集しました。

swagger: "2.0"
info:
  description: "This is sample api"
  version: "1.0.0"
  title: "Koratta Test API"
  contact:
    email: "koratta@example.com"
host: "localhost:8080"
basePath: "/v2"
tags:
- name: "user"
  description: "User's API"
schemes:
- "http"
paths:
  /user/create:
    post:
      tags:
      - "user"
      summary: "Create user"
      description: "This can only be done by the logged in user."
      operationId: "createUser"
      produces:
      - "application/xml"
      - "application/json"
      parameters:
      - in: "body"
        name: "body"
        description: "Created user object"
        required: true
        schema:
          $ref: "#/definitions/User"
      responses:
        default:
          description: "successful operation"
  /user/{username}:
    get:
      tags:
      - "user"
      summary: "Get user by user name"
      description: ""
      operationId: "getUserByName"
      produces:
      - "application/xml"
      - "application/json"
      parameters:
      - name: "username"
        in: "path"
        description: "The name that needs to be fetched. "
        required: true
        type: "string"
      responses:
        "200":
          description: "successful operation"
          schema:
            $ref: "#/definitions/User"
        "400":
          description: "Invalid username supplied"
        "404":
          description: "User not found"
definitions:
  User:
    type: "object"
    properties:
      username:
        type: "string"
      email:
        type: "string"
    xml:
      name: "User"

/user/createにユーザー名とメールアドレスをPOSTし、/user/[ユーザー名]でユーザー情報をGETするAPIです。

yamlを作成すると、画面右側にドキュメントが作成されます。

OpenAPI specからGo Serverの実行


上記で作成したOpenAPI specからGo Serverを実行します。

Swaggerには各言語への変換機能がついており、golangも対応しています。

1.「Generate Server」タブから「go-server」を選択する


2. ダウンロードしたファイルを解凍して実行する


$ go run go-server-server/main.go

そのまま実行すると以下のエラーが出ます。

unexpected directory layout:

main.goのインポートパッケージに相対ディレクトリが使用されているので、$GOPATHからのディレクトリに変更する必要があります。以下の場合は、$GOPATH/go-server-server/goになります。

main.goの24行目
変更前: sw "./go"
変更後: sw "go-server-server/go"

3. API用curlコマンドを生成する


swaggerはAPIを使用するためのcurlコマンドを生成することができます。

3-1. 使用したいAPIをクリックし、「Try it out」をクリックする


3-2. 使用したいAPIをクリックし、「Try it out」をクリックする


3-3. 生成されたコマンドをコピーする


同じ要領でGETのAPIについても生成しておきます。

4. APIを利用する


手順3で生成したcurlコマンドで、APIを利用します。

$ curl -X POST "http://localhost:8080/v2/user/create" -H "accept: application/xml" -H "Content-Type: application/json" -d "{ \"username\": \"koratta\", \"email\": \"koratta@example.com\"}"
$ curl -X GET "http://localhost:8080/v2/user/koratta" -H "accept: application/xml"
$ curl -X GET "http://localhost:8080/v2/user/koratta2" -H "accept: application/xml"
$

Go Server側でAPIのログが確認できます。

$ go run src/go-server-server/main.go 
2020/07/21 23:43:37 Server started
2020/07/21 23:45:20 POST /v2/user/create CreateUser 3.095µs
2020/07/21 23:48:19 GET /v2/user/koratta GetUserByName 4.464µs
2020/07/21 23:49:55 GET /v2/user/koratta2 GetUserByName 2.33µs

あれ、作成していないkoratta2も同じ結果だ。。と一瞬思いましたが、そもそもAPIの雛形を作成しただけなので、当たり前でした。CreateUserとGetUserByNameのメソッドを確認すると、以下のようにステータス200を返すだけとなっていました。ここからAPIの中身を開発していくようなイメージですかね。

~~go/api_user.goより抜粋~~
func CreateUser(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	w.WriteHeader(http.StatusOK)
}

func GetUserByName(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	w.WriteHeader(http.StatusOK)
}

以上です。なんて便利なツールなんだ。。。

MacOSにS2IコマンドをインストールしてS2Iビルドする

  • S2Iとは
  • S2Iコマンドのインストール
  • S2Iビルダーイメージのビルド
  • アプリケーションイメージのS2Iビルド

S2Iとは


S2Iとは、Source To Imageの略で、OpenShiftの特徴的な機能の一つです。その名の通り、ソースコードをコンテナイメージに変換します。S2Iビルダーイメージとソースコードからアプリケーションイメージをビルドすることによって、ビルドを自動化するだけでなく、イメージとソースコードの保守を分離することができます。

この機能における具体的なメリットは、以下の公式ドキュメントをご参照ください。
https://access.redhat.com/documentation/ja-jp/openshift_container_platform/4.4/html/builds/understanding-image-builds#build-strategy-s2i_understanding-image-builds

本記事は、S2Iツールをローカルマシン(Mac)にインストールしてローカル上で使ってみます。

S2Iコマンドのインストール


S2IコマンドをインストールしてMac PC上で操作します。

S2Iツールのパッケージは以下のURLからダウンロードできますが、Macではbrewコマンドでインストールができます。
https://github.com/openshift/source-to-image/releases/tag/v1.3.0

以下のコマンドでインストールします。

$ brew install source-to-image
$ s2i version
s2i v1.3.0-dirty

S2Iビルダーイメージのビルド


S2Iコマンドを使ってS2Iビルダーイメージをビルドしてみます。

以下のコマンドで例のようにS2Iビルダーイメージの雛形を作成することができます。指定したdirectory配下には、必要なファイルが作成されます。それぞれのファイルの解説はまた今度記事を書きたいと思いますが、自身で作成したいイメージに合わせてファイルを編集する必要があります。

$ s2i create [image名] [directory名]
例)
$ s2i create python:3.6 test-python
$ tree test-python/
test-python/
├── Dockerfile
├── Makefile
├── README.md
├── s2i
│   └── bin 
│       ├── assemble
│       ├── run
│       ├── save-artifacts
│       └── usage
└── test ←テストソースコードなので、S2Iビルダーイメージのビルドには不要です。
    ├── run
    └── test-app
        └── index.html
$ docker build -t python-s2i-builder test-python/ 
$ docker images python-s2i-builder REPOSITORY TAG IMAGE ID CREATED SIZE
python-s2i-builder latest 490710255550 3 minutes ago 426MB

今回は自分でS2Iビルダーイメージを作成するのではなく、以下のコマンドでregistry.redhat.ioのイメージを利用してアプリケーションイメージをS2Iビルドします(Red Hatのアカウントが必要になります。)。このように、OpenShiftではS2Iビルダーイメージをサポートしてくれるのが魅力の一つでもあります。

$ docker login registry.redhat.io
Username: 
Password: 
Login Succeeded
$ docker pull registry.redhat.io/rhel8/python-36
Using default tag: latest
latest: Pulling from rhel8/python-36
1a6747857d79: Pull complete 
fc5aa93e3b58: Pull complete 
dd09aac02f79: Pull complete 
8542dbe7d6e8: Pull complete 
ad98eb9d602e: Pull complete 
Digest: sha256:74057ed15aba3ba78ddf061a964b60a4e5c1d0eb8c989ad6831ea086721d9acf
Status: Downloaded newer image for registry.redhat.io/rhel8/python-36:latest
$ docker images registry.redhat.io/rhel8/python-36:latest
REPOSITORY                           TAG                 IMAGE ID            CREATED             SIZE
registry.redhat.io/rhel8/python-36   latest              77c14d37c369        4 weeks ago         781MB
$ docker tag registry.redhat.io/rhel8/python-36:latest python-s2i-builder
$ docker images python-s2i-builder
REPOSITORY           TAG                 IMAGE ID            CREATED             SIZE
python-s2i-builder   latest              77c14d37c369        4 weeks ago         781MB

アプリケーションイメージのS2Iビルド


上記で作成したS2Iビルダーイメージを使って、アプリケーションイメージをビルドします。

以下のコマンドでアプリケーションイメージをビルドすることができます。

$ s2i build [ソースコード] [S2Iビルダーイメージ名] [アプリケーションイメージ名]

registry.redhat.ioからpullしたS2IビルダーイメージとGit上のサンプルPythonソースコードを使って、python-s2iアプリケーションイメージをビルドします。

$ s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.6/test/setup-test-app/ python-s2i-builder python-s2i
$ docker images python-s2i
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
python-s2i          latest              b07c7f096566        15 seconds ago      789MB

アプリケーションイメージが実行できることを確認できました。

$ docker run --name hello -d -p 8080:8080 python-s2i
ae57680ef2705b92a51727bc0e6d67ce14fca3d6a515abdffc48c9f4f6913bf4
$ curl localhost:8080
Hello from gunicorn WSGI application!

以上です!

NAMESPACEをプロビジョニングするOPERATORを作ってみる(ユーザー権限編)

NamespaceをプロビジョニングするOperatorがあったら、マルチテナントでクラスター運用するのが楽になるかなぁと思い、Operatorを作成しています。本日はNamespaceにおけるユーザーアクセス権限のプロビジョニング機能を作成します。

  • はじめに
  •  Operatorを作成する
    • CRDを作成する
    • Controllerを追加する
  • Operatorをビルドする
  • Operatorをデプロイする
  • Operatorを確認する
    • ユーザーを作成する
    • Contextを作成する
    • CRを作成する
    • 権限を確認する

はじめに


最近、NamespaceをプロビジョニングするOperatorを作成しています。以下の関連記事にあるように、これまでResourceQuotaやNetworkPolicyをプロビジョニングするOperatorを作成しました。

本記事では、このOperatorにアクセス権限をプロビジョニングする機能を追加します。具体的には、CR(Cutom Resouce)に指定されたGroupに所属するユーザーが、Namespaceにアクセスできるように設定します。システムやプロジェクト単位でNamespaceを分割する際、関係ないユーザーが勝手に操作することを防げるので、必ずと言っていいほどインフラチームが実施する設定です。

Operatorを作成する


CRDを作成する


CRで設定できる設定項目を定義します。今回は、pkg/apis/nspro/v1alpha1/nspro_types.goのNsproSpecにAllowedGroup []string `json:”allowedGroup”`を加えて、Namespaceにアクセス可能なグループを指定できるようにします。

Operator SDKのインストール方法は、Operator SDK をインストールしてOperatorを作成する(1/2)で紹介していますので、よろしければご参照ください。

type NsproSpec 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
    NsSize string `json:"nssize"`
    Managed bool `json:"managed"`
    AllowedGroup []string `json:"allowedGroup"`
}

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

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

Controllerを追加する


ControllerのReconcile関数には、newRolePolicyForNSとnewRoleBindingForNSを呼び出してRole, RoleBindingリソースを作成する処理を記述します。

func (r *ReconcileNspro) Reconcile(request reconcile.Request) (reconcile.Result, error) {
    ~~省略~~
    role := r.newRoleForNS(instance)
    err = r.client.Create(context.TODO(), role)

    group_names := instance.Spec.AllowedGroup
    for i := range group_names {
      rb := r.newRoleBindingForNS(instance, group_names[i])
      err = r.client.Create(context.TODO(), rb)
    }
    ~~省略~~
}

newRoleforNSは以下の通りで、CRで作成するNamespaceの全権限(クラスター権限を除く)を付与します。作成方法は他のリソースと同じですが、RoleとRoleBindingの定義は、vender/k8s.io/api/rbac/v1/type.goに記述されています。

func (r *ReconcileNspro) newRoleForNS(cr *nsprov1alpha1.Nspro) *rbacv1.Role {
  nsname  := cr.Name
  role := &rbacv1.Role {
    ObjectMeta: metav1.ObjectMeta {
      Name:      nsname + "-role",
      Namespace: nsname,
    },
    Rules: []rbacv1.PolicyRule {
      {
        APIGroups: []string{ rbacv1.APIGroupAll },
        Resources: []string{ rbacv1.ResourceAll },
        Verbs: []string{ rbacv1.VerbAll },
      },
    },
  }
  controllerutil.SetControllerReference(cr, role, r.scheme)
  return role
}

newRoleBindingForNSは以下の通りで、newRoleforNSで作成したRoleをCRに指定されたGroupに付与します。

func (r *ReconcileNspro) newRoleBindingForNS(cr *nsprov1alpha1.Nspro, group_name string) *rbacv1.RoleBinding {
  nsname  := cr.Name
  rb := &rbacv1.RoleBinding {
    ObjectMeta: metav1.ObjectMeta {
      Name:      nsname + "-rb",
      Namespace: nsname,
    },
    Subjects: []rbacv1.Subject {
      {
        Kind: "Group",
        Name: group_name,
        APIGroup: "rbac.authorization.k8s.io",
      },
    },
    RoleRef: rbacv1.RoleRef {
      Name: nsname + "-role",
      Kind: "Role",
      APIGroup: "rbac.authorization.k8s.io",
    },
  }
  
  controllerutil.SetControllerReference(cr, rb, r.scheme)
  return rb
}

Operatorをビルドする


作成したOperatorのコンテナイメージをビルドします。

$ operator-sdk build 192.168.64.2:32000/nspro-operator:v1

以下のコマンドでレジストリに登録します。今回はMicroK8s上のコンテナレジストリ上に登録するので、[WorkerNodeのIPアドレス]:[NodePort]/[イメージ名]:[タグ名]のように登録しておきます。

$ docker push 192.168.64.2:32000/nspro-operator:v1

Operatorをデプロイする


Operatorをデプロイします。Operatorの前にCRDを作成しないとapiVersionが利用できないので、CRDから作成します。

$ kubectl create -f deploy/crds/nspro_v1alpha1_nspro_crd.yaml
customresourcedefinition.apiextensions.k8s.io/nspros.nspro.example.com created

続いてdeploy/配下にあるyamlを作成します。operator.yamlだけイメージ名を変更する必要があります。

$ kubectl delete -f deploy/operator.yaml 
deployment.apps "nspro-operator" deleted
$ kubectl create -f deploy/operator.yaml -n nspro-operator
deployment.apps/nspro-operator created
$ kubectl create -f deploy/service_account.yaml -n nspro-operator
serviceaccount/nspro-operator created
$ kubectl create -f deploy/role.yaml -n nspro-operator
role.rbac.authorization.k8s.io/nspro-operator created
$ kubectl create -f deploy/role_binding.yaml -n nspro-operator
rolebinding.rbac.authorization.k8s.io/nspro-operator created

デプロイすると以下のエラーが発生します。

E0701 15:06:25.610185 1 reflector.go:134] sigs.k8s.io/controller-runtime/pkg/cache/internal/informers_map.go:126: Failed to list *v1alpha1.Nspro: nspros.nspro.example.com is forbidden: User "system:serviceaccount:nspro-operator:nspro-operator" cannot list resource "nspros" in API group "nspro.example.com" in the namespace "nspro-operator"

アクセス権限を付与するために、以下の記事でRBAC機能を有効化したことによってOperatorに権限がなくなったことが原因です。

MicroK8sへのユーザー追加と権限付与でハマった話

従って、OperatorにCluster権限を与える必要があります。以下のyamlを使ってnspro-operator ServiceAccountにcluster-adminを付与します。

kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: admin-for-nspro-operator
subjects:
- kind: ServiceAccount
  name: nspro-operator
  namespace: nspro-operator
roleRef:
  kind: ClusterRole
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io

Operatorを確認する


CRを作成してOperatorの挙動を確認します。

ユーザーを作成する


Operator確認用に、dev1グループに所属するdev1-user01とdev2グループに所属するdev2-user01を作成します。

basic_auth.csv に以下の行を追加します。今回はMicroK8sを利用しているので/var/snap/microk8s/current/credentials/配下にあります。環境毎にパスが異なるので注意してください。

[dev1-user01のパスワード],dev1-user01,dev1-user01,"system:authenticated,developer,dev1"
[dev2-user01のパスワード],dev2-user01,dev2-user01,"system:authenticated,developer,dev2"

Contextを作成する


~/.kube/config を編集して、dev1-user01とdev2-user01で操作する用のContextを作成します。

apiVersion: v1
clusters:
- cluster:
    insecure-skip-tls-verify: true
    server: https://127.0.0.1:16443
  name: microk8s-cluster
contexts:
- context:
    cluster: microk8s-cluster
    user: admin
  name: microk8s
- context:
    cluster: microk8s-cluster
    user: dev1-user01
  name: microk8s-dev1
- context:
    cluster: microk8s-cluster
    user: dev2-user01
  name: microk8s-dev2
current-context: microk8s
kind: Config
preferences: {}
users:
- name: admin
  user:
    password: [adminのパスワード]
    username: admin
- name: dev1-user01
  user:
    password: [dev1-user01のパスワード]
    username: dev1-user01
- name: dev2-user01
  user:
    password: [dev2-user01のパスワード]
    username: dev2-user01

CRを作成する


CRを作成してNamespaceをプロビジョニングします。

dev1グループがアクセスできるdev1-nsとdev2グループがアクセスできるdev2-nsを以下のyamlを使って作成します。

apiVersion: nspro.example.com/v1alpha1
kind: Nspro
metadata:
  name: dev1-ns
spec:
  nssize: large
  managed: true
  allowedGroup: ["dev1"]
apiVersion: nspro.example.com/v1alpha1
kind: Nspro
metadata:
  name: dev2-ns
spec:
  nssize: large
  managed: true
  allowedGroup: ["dev2"]
$ kubectl create -f deploy/crds/dev1-ns.yaml -n nspro-operator
nspro.nspro.example.com/dev1-ns created
$ kubectl create -f deploy/crds/dev2-ns.yaml -n nspro-operator
nspro.nspro.example.com/dev2-ns created
$ kubectl get ns dev1-ns dev2-ns
NAME      STATUS   AGE
dev1-ns   Active   82s
dev2-ns   Active   74s

権限を確認する


dev1-user01とdev2-user01のコンテキストに切り替えて、それぞれの権限を確認すると、以下のようにCRのallowedGroupに指定されたNamespaceのみアクセスできることがわかります。

$ kubectl config use-context microk8s-dev1
Switched to context "microk8s-dev1".
$ kubectl auth can-i get pods -n dev1-ns
yes
$ kubectl auth can-i get pods -n dev2-ns
no
$ kubectl config use-context microk8s-dev2
Switched to context "microk8s-dev2".
$ kubectl auth can-i get pods -n dev1-ns
no
$ kubectl auth can-i get pods -n dev2-ns
yes

以上です。段々Operatorの使い方も慣れてきました。