2021年6月30日水曜日

ERROR: Could not build wheels for cryptography which use PEP 517 and cannot be installed directly

ERROR: Could not build wheels for cryptography which use PEP 517 and cannot be installed directly

概要

macOS 上に paramiko をインストールしようとしたらタイトルのエラーに遭遇しました 対応方法を紹介します

環境

  • macOS 11.4
  • Python 3.8.3
  • paramiko 2.7.2

原因

どうやら全然関係ないところでエラーになっているようです paramiko は cryptography というライブラリに依存しているのですがそのライブラリがデフォルトで Rust のコンパイラを使っています

Rust のコンパイラがすでにインストールされていれば問題なくインストールできるかもしれないのですがインストールされていない場合は今回のエラーに遭遇するようです

エラー文の途中を見ると「error: can’t find Rust compiler」という文が表示されているのが確認できると思います

対策1: cryptography だけインストールしてみる

Rust コンパイラを使わないでビルドする方法もあるようです 以下の環境変数をセットして cryptography だけインストールできるか確認してみましょう

  • export CRYPTOGRAPHY_DONT_BUILD_RUST=1
  • pipenv install cryptography

対策2: openssl のビルドオプションを追加する

openssl も使っているので以下のように openssl のヘッダファイルなどのパスも指定してあげます 環境によってことなるので brew info openssl で確認しましょう

  • export LDFLAGS="-L/usr/local/opt/openssl@1.1/lib"
  • export CPPFLAGS="-I/usr/local/opt/openssl@1.1/include"
  • export PKG_CONFIG_PATH="/usr/local/opt/openssl@1.1/lib/pkgconfig"

再度 paramiko をインストールする

上記が成功したら再度 paramiko をインストールしてみましょう

  • pipenv install paramiko

参考サイト

2021年6月29日火曜日

paramiko を monkeypatch する方法

paramiko を monkeypatch する方法

概要

Python の SSH クライアントライブラリ paramiko を pytest の monkeypatch でモックする方法を紹介します with - as 句を使うので __enter____exit__ を実装する必要があります

環境

  • macOS 11.4
  • Python 3.8.3
  • paramiko 2.7.2

実行コード

import paramiko

def execute_cmd():
    with paramiko.SSHClient() as ssh:
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        ssh.connect(hostname='192.168.100.99',
                    port=22,
                    username='user1',
                    password='xxxxxxxxxxx')
        stdin, stdout, stderr = ssh.exec_command('hostname')
        stdin.close()
        return stdout.read()

テストコード

import paramiko

from test import execute_cmd

class DummySSHClient():
    def __init__(self):
        self

    def set_missing_host_key_policy(self, policy):
        self.policy = policy

    def connect(self, hostname=None, port=None, username=None, password=None):
        return None

    def exec_command(self, cmd):
        return DummyStdin(), DummyStdout(), ""

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

    def close(self):
        return None

class DummyStdout():
    def read(self):
        return "testhost1"

class DummyStdin():
    def close(self):
        return None

class DummyAutoAddPolicy():
    def __init__(self):
        pass

def test_execute_cmd(monkeypatch):
    monkeypatch.setattr(paramiko, "SSHClient", DummySSHClient)
    monkeypatch.setattr(paramiko, "AutoAddPolicy", DummyAutoAddPolicy)
    result = execute_cmd()
    assert (result == "testhost1")

解説

paramiko.SSHClient をダミーの DummySSHClient でモックします paramiko.AutoAddPolicy もモックが必要なので DummyAutoAddPolicy を作成します

stdout と stdin のオブジェクトも使う場合にはそれぞれモックします 使用しない場合はモック不要です

あとは monkeypatch で setatter で SSHClient と AutoAddPolicy を持っくすれば OK です

scp の場合

paramiko + scp でファイルをアップロードする場合も紹介します

import paramiko
import scp

def upload_file():
    with paramiko.SSHClient() as ssh:
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        ssh.connect(hostname='192.168.100.99',
                    port=22,
                    username='user1',
                    password='xxxxxxxxxxx')
        with scp.SCPClient(ssh.get_transport()) as scp:
            scp.put('hoge.txt', '/tmp/hoge.txt')

scp のテストコード

import paramiko
import scp

from test import execute_cmd, upload_file

class DummySSHClient():
    def __init__(self):
        self

    def set_missing_host_key_policy(self, policy):
        self.policy = policy

    def get_transport(self):
        return None

    def connect(self, hostname=None, port=None, username=None, password=None):
        return None

    def exec_command(self, cmd):
        return DummyStdin(), DummyStdout(), ""

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

class DummySCPClient():
    def __init__(self, transport):
        self

    def put(self, src, dict):
        return None

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

class DummyStdout():
    def read(self):
        return "testhost1"

class DummyStdin():
    def close(self):
        return None

class DummyAutoAddPolicy():
    def __init__(self):
        pass

def test_upload_file(monkeypatch):
    monkeypatch.setattr(paramiko, "SSHClient", DummySSHClient)
    monkeypatch.setattr(paramiko, "AutoAddPolicy", DummyAutoAddPolicy)
    monkeypatch.setattr(scp, "SCPClient", DummySCPClient)
    result = upload_file()
    assert (result == None)

解説

scp の場合もほぼ同じです DummySSHClient 側に get_transport を追加で実装してあげます

おまけ: ProxyCommand を使う場合は

import paramiko

def execute_cmd_with_proxy():
    with paramiko.SSHClient() as ssh:
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        ssh.connect(hostname='192.168.100.99',
                    port=22,
                    username='user1',
                    password='xxxxxxxxxxx',
                    sock=paramiko.ProxyCommand('nc -X connect -x 192.168.100.100:3128 192.168.100.99 22'))
        stdin, stdout, stderr = ssh.exec_command('hostname')
        stdin.close()
        print(stdout.read())

になるので connect に 1 つ変数を追加してあげれば OK です (以下途中省略)

class DummySSHClient():
    def connect(self, hostname=None, port=None, username=None, password=None, sock=None):
        return None

おまけ2: 鍵認証の場合は

class DummyRSAKey():
    @classmethod
    def from_private_key_file(cls, key_path, passphrase):
        return "key"
class DummySSHClient():
    def connect(self, hostname=None, port=None, username=None, password=None, sock=None, pkey=None):
        return None
monkeypatch.setattr(paramiko, "RSAKey", DummyRSAKey)

こんな感じです

2021年6月28日月曜日

Google SpreadSheet で Apps Script に入門してみた

Google SpreadSheet で Apps Script に入門してみた

概要

Apps Script は Google のいろいろなサービスに JavaScript でアクセスできるサービスです 当然 SpreadSheet にも触れるので今回は SpreadSheet から Apps Script に入門してみました

環境

  • macOS 11.4
  • Chrome 91.0.4472.106
  • Google SpreadSheet
  • Apps Script

Apps Script を起動する

まずは SpreadSheet にアクセスしましょう そして

ツール -> スクリプトエディタ

と選択します

すると別画面で Apps Script の Web エディタが開きます ここで SpreadSheet 用のスクリプトを JavaScript で記載することでセルの操作などができます

サンプルコード

ではスクリプトを作成します すでにいろいろ書いてありますがデフォルトだと何もしないからの function が 1 つ用意されているだけです

今回は 3 つほどサンプルの関数を用意したので貼り付けてみましょう

function hello() {
  console.log("hello macro")
}

function getCell() {
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = spreadsheet.getActiveSheet();
  var range = sheet.getRange("A2");
  var cell_a1_value = range.getValue();
  console.log(cell_a1_value);
}

function getCells() {
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = spreadsheet.getActiveSheet();
  var range = sheet.getRange("A7:A");
  var a_values = range.getValues();
  for (var row in a_values) {
    for (col in a_values[row]) {
      console.log(a_values[row][col]);
    }
  }
}

それぞれ以下のようなサンプル関数になっています

  • hello・・・ログにメッセージを出力するだけの関数
  • getCell・・・特定のセルの値をログに出力する関数
  • getCells・・・複数の特性のセルをログに出力する関数

実行してみる

サンプルの関数を貼り付けたら実行してみましょう Apps Script のツールバーに「保存ボタン」があるのでスクリプトを保存しましょう

そしてその隣に「実行」と実行する関数名を選択できるプルダウンがあるので「hello」を選択してクリックしてみます

すると画面下にログ実行ログが表示されてメッセージが出力されているのが確認できると思います

アクセスを許可する

初回実行時やアプリがアクセスしたいスコープが変更になった場合はアクセス許可をする必要があります

Apps Script は新しいアプリとして登録されています アプリから SpreadSheet にアクセスするために許可を与える必要があります また今回作成したアプリはテスト用のアプリの設定になっており以下のように警告が出ますが気にせず許可しましょう

左下にある「詳細」をクリックすると以下のように表示されるので更に「無題のプロジェクト ( 安全ではないページ) に移動」をクリックします

他にも便利な機能がたくさんある

  • SpreadSheet 側にボタンなどを作成して関数を紐付ける
  • サードパティのライブラリの使用
  • doGet や doPost ハンドラを実行して GCP にデプロイして Web アプリとして動作させる
  • トリガーを使って定期的に実行させる

などなどたくさんの機能があります なお .gs スクリプトは基本的には GCP 上で動作させることができるスクリプトなのでローカルアプリとして動作させるのはできません

clasp などのツールを使えばローカルで開発することはできるようにはなりますがあくまでも動作させるのは GCP 上になります

2021年6月25日金曜日

macOS で paramiko を使ってみた

macOS で paramiko を使ってみた

概要

Python の paramiko を使っていろいろな ssh 接続をしてみました 便利だけどケースによってはハマる点も多いと思います

環境

  • macOS 11.4
  • Python 3.8.3
  • paramiko

インストール

  • pipenv install paramiko scp

普通に ssh する

connect メソッドを使って各種 ssh の基本パラメータを指定するだけです

コネクションを貼るので with を使うのをオススメします

import paramiko

with paramiko.SSHClient() as ssh:
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    ssh.connect(hostname='192.168.100.10', port=22, username='user1', password='xxxxxxxxxx')
    stdin, stdout, stderr = ssh.exec_command('hostname')
    stdin.close()
    print(stdout.read())

トラブルシューティング

paramiko.ssh_exception.SSHException: Server ‘192.168.100.10’ not found in known_hosts

以下で回避できます

ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

AttributeError: ‘NoneType’ object has no attribute ‘time’

以下で回避できます

stdin.close()

もしくはコマンド実行後に 5 秒ほど wait を入れましょう 参考: https://github.com/paramiko/paramiko/issues/1617

鍵を使ってログインする

paramiko.RSAKey を使います 鍵へのパスはフルパスを指定しましょう 相対パスやカレントパスを使うとうまく鍵ファイルを読み込めない場合があります パスフレーズがある場合は一緒に指定します

import paramiko

rsa_key = paramiko.RSAKey.from_private_key_file("/path/to/key/privkey.pem", "xxxxxxxxx")

with paramiko.SSHClient() as ssh:
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    ssh.connect(hostname='192.168.100.10', port=22, username='root', pkey=rsa_key)
    stdin, stdout, stderr = ssh.exec_command('hostname')
    stdin.close()
    print(stdout.read())

scp する

scp モジュールと組み合わせることで使えます paramiko オンリーでは scp は使えません https://github.com/paramiko/paramiko/issues/150

以下はファイルをローカルからサーバにアップロードする方法になります 逆にダウンロードする場合は scp.get を使います

import paramiko
import scp

with paramiko.SSHClient() as ssh:
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    ssh.connect(hostname='192.168.100.10', port=22, username='user1', password='xxxxxxxxxx')
    with scp.SCPClient(ssh.get_transport()) as scp:
       scp.put('hoge.txt', '/tmp/hoge.txt')

鍵を使って scp する

鍵認証+scp を組み合わせるだけです

import paramiko
import scp

rsa_key = paramiko.RSAKey.from_private_key_file("/path/to/key/privkey.pem", "xxxxxxxxx")

with paramiko.SSHClient() as ssh:
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    ssh.connect(hostname='192.168.100.10', port=22, username='root', pkey=rsa_key)
    with scp.SCPClient(ssh.get_transport()) as scp:
       scp.put('hoge.txt', '/tmp/hoge.txt')

プロキシを使う

これがかなり曲者です ProxyCommand と組み合わせて使うのですが %h や %p などのテンプレート変数を paramiko は展開してくれません なので ssh コマンドで使っている ProxyCommand をそのまま使うと

paramiko.ssh_exception.ProxyCommandFailure: ProxyCommand(“nc -X connect -x 192.168.100.20:3128 %h %p”) returned nonzero exit status: Broken pipe」

と言ったエラーが発生します なお以下のサンプルは squid で ssh をプロキシした場合の ProxyCommand の設定になります

192.168.100.20 が squid プロキシで 192.168.100.10 が接続したい ssh サーバになります

import paramiko
with paramiko.SSHClient() as ssh:
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    ssh.connect(hostname='192.168.100.10',
                port=22,
                username='user1',
                password='xxxxxxxxx',
                sock=paramiko.ProxyCommand("nc -X connect -x 192.168.100.20:3128 192.168.100.10 22"))
    stdin, stdout, stderr = ssh.exec_command('hostname')
    stdin.close()
    print(stdout.read())

なお謎が解けなかったのは ProxyCommand で多段 ssh プロキシを使っている場合でプロキシも ssh でかつ鍵認証の場合に ProxyCommand で指定している ssh サーバの鍵のパスフレーズを指定する方法がわかりませんでした

もしかするとその場合はプロキシサーバにパスフレーズなしの鍵を登録しろってことなのかもしれません

最後に

多段プロキシで paramiko を使う場合はハマりどころがかなり多いと印象です 可能であれば多段プロキシを使わないケースで paramiko は使ったほうが良いかもしれません

参考サイト

2021年6月23日水曜日

MySQL で JSON 型を使ってみた

MySQL で JSON 型を使ってみた

概要

MySQL 5.7.8 以降で使える JSON 型を試したみました JSON データの挿入方法から特殊な JSON クエリを使った検索方法まで紹介したいと思います

環境

  • macOS 11.4
  • MySQL 8.0.25

準備

まずは JSON 型のカラムを持つテーブルの定義しましょう

  • CREATE DATABASE test;
  • USE test;
  • CREATE TABLE user (id int(11) NOT NULL AUTO_INCREMENT, profile json DEFAULT NULL, PRIMARY KEY (id));

型名にはその名の通り「json」という型があるのでこれを使います ちゃんと json 型で登録されています

  • SHOW CREATE TABLE user \G
*************************** 1. row ***************************
       Table: user
Create Table: CREATE TABLE `user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `profile` json DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
1 row in set (0.01 sec)

データ登録

次に JSON 型のデータを登録してみます

データを登録する際はシングルクオートで全体を囲って中のキーや値はダブルクオートで囲います

  • INSERT INTO user VALUES (null, ‘{“name”:“hawksnowlog”}’);
  • INSERT INTO user VALUES (null, ‘{“name”:“hawk”}’);
  • INSERT INTO user VALUES (null, ‘{“name”:“snowlog”}’);

JSON_EXTRACT

ここからは特殊な JSON 型のクエリを使っていきます

JSON_EXTRACT クエリは特定の Json ドキュメントを取得するためのクエリです

例えば name キーの値だけを取得する場合は以下のようにします

  • SELECT JSON_EXTRACT(profile, ‘$.name’) FROM user;
+---------------------------------+
| JSON_EXTRACT(profile, '$.name') |
+---------------------------------+
| "hawksnowlog"                   |
| "hawk"                          |
| "snowlog"                       |
+---------------------------------+
3 rows in set (0.00 sec)

これと WHERE 句を組み合わせて特定の JSON ドキュメントを含んだレコードのみを取得することもできます

  • SELECT * FROM user WHERE JSON_EXTRACT(profile, ‘$.name’) = ‘hawksnowlog’;
+----+-------------------------+
| id | profile                 |
+----+-------------------------+
|  1 | {"name": "hawksnowlog"} |
+----+-------------------------+
1 row in set (0.00 sec)

LIKE 句と組み合わせるとより柔軟な検索が可能になります

  • SELECT * FROM user WHERE JSON_EXTRACT(profile, ‘$.name’) LIKE ‘%hawk%’;
+----+-------------------------+
| id | profile                 |
+----+-------------------------+
|  1 | {"name": "hawksnowlog"} |
|  2 | {"name": "hawk"}        |
+----+-------------------------+
2 rows in set (0.00 sec)

JSON_SEARCH

JSON_SEARCH クエリは特定のドキュメントが存在するパスを取得することができるクエリです

2 つ目の引数には「one」or「all」のどちらかを指定します 前者は一致した最初の JSON ドキュメントのパスだけを取得します 後者は一致したすべての JSON ドキュメントのパスを取得します 今回は一致する JSON ドキュメントが 1 つしかないので one でも all でもパスは 1 つになります

  • SELECT JSON_SEARCH(profile, ‘one’, ‘hawksnowlog’) FROM user;
+--------------------------------------------+
| JSON_SEARCH(profile, 'one', 'hawksnowlog') |
+--------------------------------------------+
| "$.name"                                   |
| NULL                                       |
| NULL                                       |
+--------------------------------------------+
3 rows in set (0.00 sec)

応用して先程の JSON_EXTRACT と組み合わせることもできます

  • SELECT JSON_SEARCH(profile, ‘all’, ‘%hawk%’) FROM user WHERE JSON_EXTRACT(profile, ‘$.name’) LIKE ‘%hawk%’;
+---------------------------------------+
| JSON_SEARCH(profile, 'all', '%hawk%') |
+---------------------------------------+
| "$.name"                              |
| "$.name"                              |
+---------------------------------------+
2 rows in set (0.00 sec)

JSON_CONTAINS

JSON_CONTAINS は各レコードに特性のドキュメントが含まれているか確認することができるクエリです 含まれている場合は「1」含まれていない場合は「0」が返ってきます

2 つ目の引数に検索する値を指定するのですが文字列を指定する場合はシングルクオートで囲った上で中をダブルクオートでも囲う必要があるので注意しましょう

以下は name フィールドに hawksnowlog という値を含む JSON ドキュメントが各レコードで含まれているか確認するクエリです

  • SELECT JSON_CONTAINS(profile, ‘“hawksnowlog”’, ‘$.name’) FROM user;
+---------------------------------------------------+
| JSON_CONTAINS(profile, '"hawksnowlog"', '$.name') |
+---------------------------------------------------+
|                                                 1 |
|                                                 0 |
|                                                 0 |
+---------------------------------------------------+
3 rows in set (0.00 sec)

JSON_CONTAINS も JSON_EXTRACT と組み合わせて検索することができます

  • SELECT JSON_CONTAINS(profile, '"hawksnowlog"', '$.name') FROM user WHERE JSON_EXTRACT(profile, '$.name') LIKE '%hawk%';
+---------------------------------------------------+
| JSON_CONTAINS(profile, '"hawksnowlog"', '$.name') |
+---------------------------------------------------+
|                                                 1 |
|                                                 0 |
+---------------------------------------------------+
2 rows in set (0.00 sec)

JSON_VALUE

指定のキー (パス) の値だけを取り出すことができるクエリです

  • SELECT JSON_VALUE(profile, ‘$.name’) from user;
+-------------------------------+
| JSON_VALUE(profile, '$.name') |
+-------------------------------+
| hawksnowlog                   |
| hawk                          |
| snowlog                       |
+-------------------------------+
3 rows in set (0.01 sec)

JSON_EXTRACT に似ていますがこちらは返り値の型変換が行えたり空 or エラー時のハンドリングができます

  • SELECT JSON_VALUE(profile, ‘$.hoge’ RETURNING CHAR DEFAULT ‘default_value!’ ON EMPTY) from user;
+--------------------------------------------------------------------------------+
| JSON_VALUE(profile, '$.hoge' RETURNING CHAR DEFAULT 'default_value!' ON EMPTY) |
+--------------------------------------------------------------------------------+
| default_value!                                                                 |
| default_value!                                                                 |
| default_value!                                                                 |
+--------------------------------------------------------------------------------+
3 rows in set (0.00 sec)

ちなみに JSON_VALUE も WHERE 句に使えます

  • SELECT * FROM user WHERE JSON_VALUE(profile, ‘$.name’) = ‘hawk’;

その他クエリ

  • JSON_OVERLAPS・・・ 2 つの JSON ドキュメントを比較することができるクエリ
  • MEMBER OF・・・配列内に特定の値が含まれているかチェックするクエリ

Ruby から使ってみる

mysql2 を使って JSON 系のクエリを発行してみました サンプルコードを紹介します

require 'mysql2'
require 'json'

client = Mysql2::Client.new(:host => "localhost", :username => "root", :database => 'test')

query = "select * from user"
records = client.query(query)
records.each do |record|
  puts record["id"]
  profile = JSON.parse(record["profile"])
  puts profile["name"]
end

query = "SELECT JSON_VALUE(profile, '$.name') from user;"
records = client.query(query)
records.each do |record|
  puts record
end

結果は以下の通りです JSON 系のクエリを発行した場合は AS 句を使って結果のカラムを変更してあげると扱いやすいと思います

1
hawksnowlog
2
hawk
3
snowlog
hawksnowlog
hawk
snowlog

最後に

気になるのは性能面かなと思います 全レコードを検索するような場合に更に JSON オブジェクトを展開するとなると検索のオーダが跳ね上がるのでそこは MySQL で頑張るのかコード側で頑張るのか (もしくは別のコンポーネントを使うのか) などを考えたほうが良いかなと思います

参考サイト

2021年6月22日火曜日

pytest の monkeypatch で subprocess をパッチする方法

pytest の monkeypatch で subprocess をパッチする方法

概要

タイトルの通り Popen を使って結果を cummnicate() で取得する場合のサンプルを紹介します

環境

  • macOS 11.4
  • Python 3.8.3

サンプルコード

  • vim test_user.py
import pytest
import subprocess
from user.user import User

class DummyProcess():
    def __init__(self):
        self.returncode = 0

    def communicate(self):
        return "hello", None

class TestUser():
    def test_subprocess(self, monkeypatch):
        monkeypatch.setattr(subprocess, "Popen", lambda args, stdout: DummyProcess())
        u = User("hawk", 10)
        ret = u.execute_command()
        assert(ret == "hello")
import subprocess

class User():
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def execute_command(self):
        process = subprocess.Popen(["ls"], stdout=subprocess.PIPE)
        stdoutdata, _ = process.communicate()
        return stdoutdata

ポイント

Popen のリターンとして DummyProcess を返すのがポイントです この DummyProcess クラスに communicate() を実装することで実際にコマンドを実行した結果ではなくダミーの結果を返すことができるようになります

2021年6月21日月曜日

k8s では Service を作ればクラスタ内の Pod から簡単にアクセスできるようになる

k8s では Service を作ればクラスタ内の Pod から簡単にアクセスできるようになる

概要

今更ですが備忘録として残しておきます Service 名でアクセスできるのが嬉しい点かなと思います

環境

  • k8s v1.20.1

マニフェスト

  • vim deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  selector:
    matchLabels:
      run: my-nginx
  replicas: 2
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
      - name: my-nginx
        image: nginx:latest
        volumeMounts:
        - name: postgres-password
          mountPath: "/tmp"
          readOnly: true
        ports:
        - containerPort: 80
      volumes:
      - name: postgres-password
        secret:
          secretName: postgres.acid-gitlab.credentials.postgresql.acid.zalan.do

---

apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    run: my-nginx
spec:
  ports:
  - port: 80
    protocol: TCP
  selector:
    run: my-nginx

動作確認

  • kubectl run curl --image=radial/busyboxplus:curl -i --tty
  • cur my-nginx

最後に

あくまでも同一クラスタ内でのみ可能なので注意してください

2021年6月18日金曜日

macOS 11.4 で pyenv を使う方法

macOS 11.4 で pyenv を使う方法

概要

macOS 11 の場合 pyenv で指定のバージョンの Python をインストールする際にコンパイルエラーになるので注意が必要です

環境

  • macOS 11.4
  • pyenv 2.0.1

pyenv のインストール

  • brew update
  • brew install pyenv

pyenv の設定

  • echo 'eval "$(pyenv init --path)"' >> ~/.zprofile
  • echo 'eval "$(pyenv init -)"' >> ~/.zshrc

ターミナルのセッションを再起動

ログインするなり新しいターミナルを起動しましょう

  • pyenv --version

指定した Python のバージョンをインストール

  • pyenv install 3.8.3

implicit declaration of function ‘sendfile’ is invalid in C99 対策

インストールした Python を使う

  • pyenv versions
  • pyenv local 3.8.3
  • python --version

python が使えない場合はセッションをクリアしてみてください (再ログイン or ターミナル再起動)

元に戻す

pyenv でインストールした python が優先されるので Homebrew などでインストールした python に戻したい場合は

  • pyenv local system

で元に戻ります

参考サイト

2021年6月17日木曜日

k8s の kustomize に入門する

k8s の kustomize に入門する

概要

kustomize は k8s のマニフェストを生成、管理することができるツールです helm や helmfile に近いツールですが kubectl に標準で搭載されているので別途インストール作業は不要で使えます 今回は簡単なリソース作成を kustomize を使ってやってみました

環境

  • k8s 1.20.1
  • kubectl 1.20.4

今回の流れ

configMap で作成した情報を nginx で表示するアプリを作成してみます 最終的には kustomize.yaml で各種リソースを定義してデプロイします

profile.txt の作成

今回は configMap として登録します kustomize では configMap を作成するために configMapGenrator という機能が使えます これはファイルから configMap の定義を生成してくれる機能です

  • vim profile.txt
name=hawksnowlog

この profile.txt を使って configMap を作成するために kustomize.yaml というファイルを作成します

  • vim kustomize.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

configMapGenerator:
- name: user-profile
  files:
  - profile.txt

とりあえずこれでビルドしてみると kustomize が自動生成してくれるマニフェストの状態を確認することができます

  • kubectl kustomize .

こんな感じで kustomize.yaml にいろいろな定義をしていくことで一括でマニフェストを管理してくれるツールになります

deployment.yaml の作成

次に deployment.yaml を作成します 先程定義した configMap を使うように定義します

  • vim deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: app
        image: nginx:latest
        volumeMounts:
        - name: profile
          mountPath: /usr/share/nginx/html
      volumes:
      - name: profile
        configMap:
          name: user-profile

単純な deployment の定義になります volumeMounts で configMap を参照できます 今回はテキストファイルですが nginx のドキュメントルートに配置してあとから確認できるようにします

そしてこの deployment.yaml も kustomize.yaml で管理するので追記します 普通のマニフェストを管理する場合は resoureces ディレクティブを使って定義します

  • vim kustomize.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

configMapGenerator:
- name: user-profile
  files:
  - profile.txt

resources:
- deployment.yaml

service.yaml の作成

deploment によってデプロイされた Pods にアクセスするために service を定義します NodePort でアクセスします

  • vim service.yaml
apiVersion: v1
kind: Service
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  type: NodePort
  ports:
  - port: 80
    targetPort: 80
    protocol: TCP
  selector:
    app: my-app

selector の「app: my-app」は deployment のラベルと合わせるようにしましょう

これも kustomize.yaml に追記します

  • vim kustomize.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

configMapGenerator:
- name: user-profile
  files:
  - profile.txt

resources:
- deployment.yaml
- service.yaml

動作確認

ここまで作成できたら適用してみます kustomize を使っている場合はすべてのリソースを一回でデプロイできます

  • kubectl apply -k .

-f ではなく -k オプションなので注意しましょう

これでデプロイされると以下のように kustomize によりリソースが作成されているのが確認できると思います

  • kubectl get -k .
NAME                                       DATA   AGE
configmap/user-profile-gd9h846t5c   1      3m25s
NAME             TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
service/my-app   NodePort   10.110.195.120   <none>        80:30722/TCP   11m
NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/my-app   1/1     1            1           23s

あとは NodePort で LISTEN しているポートにアクセスすればちゃんと profile.txt の内容が確認できます

  • curl node_ip:30722/profile.txt

=> name=hawksnowlog

リソース削除

  • kubectl delete -k .

最後に

自分でマニフェストを作成して管理する際にわざわざ helm chart のテンプレートとして書きたくない場合には kustomize を使う感じかなと思います

helmfile もあるのを考えると helm に寄せるほうが良い気はしますが一概にどちらが良いかは何とも言えないかなと思います

kubectl でのオペレーションに慣れている人は kustomize にしたほうがいいし helm でのオペレーションに慣れている人は helm にしたほうがいいなどはあるかなと思います

参考サイト

2021年6月16日水曜日

Python のサブプロセスで環境変数を扱う方法

Python のサブプロセスで環境変数を扱う方法

概要

例えば環境変数でコマンドの設定をする場合などに使えます (ex: PYTHONPATH や FLASK_APP、KUBECONFIG など)

環境

  • macOS 11.4
  • Python 3.8.3

サンプルコード

import subprocess, os
my_env = os.environ.copy()
my_env["MSG"] = "hello!"

process = subprocess.Popen(["ruby", "msg.rb"], stdout=subprocess.PIPE, env=my_env)
stdoutdata, _ = process.communicate()

print(stdoutdata)
print(process.returncode)
  • vim msg.rb
puts ENV["MSG"]

ポイント

  • Python 内での環境変数への値の設定は os.environ を使う
  • subprocess に環境変数を渡す場合は env オプションを使う
  • 結果の取得は communicate() を使う

2021年6月15日火曜日

helm secrets を使ってみたけどうまく dec ができなかった話

helm secrets を使ってみたけどうまく dec ができなかった話

概要

helm secrets は sops を使って YAML ファイルを暗号/復号化する方法です Key には gpg or aws/arn, gcp/kms などが使えます 今回は gpg キーを使っています なおなぜか dec がうまく動作しなかったのでそれも紹介します

環境

  • Ubuntu 18.04
  • helm 3.5.2

helm-secrets のインストール

helm のプラグインとしてインストールします バージョンを指定するのがオススメの方法のようです

SOPS のインストール

pip があるなら pip が一番簡単だと思います

  • pip install --upgrade sops

gpg キーの登録

何でも OK です 今回は SOPS のサンプルから取得して登録します

gpg キーのハッシュ値の確認

ハッシュ値はあとで使います

  • gpg --list-secret-keys
/root/.gnupg/pubring.kbx
------------------------
sec   rsa2048 2019-08-29 [SC]
      FBC7B9E2A4F9289AC0C1D4843D16CEE4A27381B4
uid           [ unknown] SOPS Functional Tests Key 1 (https://github.com/mozilla/sops/) <secops@mozilla.com>
ssb   rsa2048 2019-08-29 [E]
sec   rsa1024 2019-08-29 [SC]
      D7229043384BCC60326C6FB9D8720D957C3D3074
uid           [ unknown] SOPS Functional Tests Key 2 (https://github.com/mozilla/sops/) <secops@mozilla.com>
ssb   rsa1024 2019-08-29 [E]

.sops.yaml ファイルの作成

暗号/複合時に使用するキーのハッシュ値を登録します AWS の場合は arn を登録します

  • vim .sops.yaml
---
creation_rules:
- pgp: >-
    FBC7B9E2A4F9289AC0C1D4843D16CEE4A27381B4,
    D7229043384BCC60326C6FB9D8720D957C3D3074

動作確認

  • vim test.yaml
name: hawksnowlog

暗号化します

  • helm secrets enc test.yaml

これで再度 test.yaml を確認すると値の部分が暗号化されているのが確認できると思います

復号化するには dec を使います

  • helm secrets dec test.yaml

本来ならこれで test.yaml.dec というファイルが新規で作成されるのですがなぜか自分は作成されずまた test.yaml に復号化されたデータが上書きされることもありませんでした 原因は不明です

しかし復号化した中身自体は view で確認できました

  • helm secrets view test.yml

もしかするとバグかもしれません

参考サイト

2021年6月14日月曜日

bitnami の PostgreSQL-ha helm chart で好きなユーザを追加する方法

bitnami の PostgreSQL-ha helm chart で好きなユーザを追加する方法

概要

初期化スクリプトを使って実現します また HA 構成の場合は pgpool 側にも認証情報の追加が必要なのもポイントです pgpool 側にもユーザを追加するための初期化スクリプトを流します

環境

  • kubernetes 1.20.5
  • helm 3.5.2
  • bitnami PostgreSQL ha helm chart 7.6.2

values.yaml

postgresql:
  password: xxxx
  repmgrPassword: xxxx
  initdbScripts:
      setup.sql: |
        CREATE USER user1 WITH PASSWORD 'xxxx';
        CREATE USER user2 WITH PASSWORD 'xxxx';
        CREATE DATABASE db1 WITH OWNER user1;
        CREATE DATABASE db2 WITH OWNER user2;

pgpool:
  adminPassword: xxxx
  initdbScripts:
      setup.sh: |
        #!/bin/bash
        USERS=("uesr1:xxxx" "user2:xxxx")
        for user in "${USERS[@]}"; do
            user_info=(${user//:/ })
            pg_md5 -m --config-file="/opt/bitnami/pgpool/conf/pgpool.conf" -u "${user_info[0]}" "${user_info[1]}"
        done

service:
  type: NodePort
  nodePort: 30432

少し解説

好きなユーザを追加したい場合は postgresql.username があるのですがこれは使わないようにしましょう 基本的には initdbScripts に SQL やシェルスクリプトを書いて追加します

まず posgresql 側には CREATE USER コマンドを発行してユーザを作成します このときパスワードも指定します

次に pgpool 側です こちらは pg_md5 コマンドを使って /opt/bitnami/pgpool/conf/pool_passwd に認証情報を追加する必要があります HA 構成の場合認証箇所が postgresql と pgpool の 2 箇所になるのでそれぞれに認証情報を登録する必要があります pgpool Pod が起動後に手動で追加しても OK です

あとは SQL などで好きなコマンドを発行して上げれば OK です postgresql.username を指定しないことでデフォルトの postgres ユーザを使ってコマンドを発行してくれるので権限の強いコマンドでも実行できるようになっています

参考サイト