2020年11月30日月曜日

Ruby の tap を使ってみる

概要

あまり使っていなかった tap の使い方を簡単にまとめてみました

環境

  • macOS 10.15.7
  • Ruby 2.7.2p137

メソッドチェインとして使う

メソッドの返り値を self にすることで自信を返却しメソッドチェインできます
self ではなく self.tap にしても同じです

class MyClass
  def initialize
    @name = "hawk"
  end

  def hello
    print("hello")
    self
  end

  def world
    puts("world")
    self
    # self.tap {}
  end

  attr_reader :name
end

cls = MyClass.new
cls.hello.world

=> helloworld

オブジェクトを tap するとデバッグ的なことができる

tap 自体は self を引数に取って self を return するメソッドです
つまり tap をメソッドチェインすることができます

class MyClass
  def initialize
    @name = "hawk"
  end

  def hello
    print("hello")
    self.tap {}
  end

  def world
    puts("world")
    self
  end

  attr_reader :name
end

cls = MyClass.new
cls.
  tap {|c| puts c.name }.
  tap {|c| c.hello.world }.

=> hawk,helloworld

返り値がほしい場合

tap の最後で break を使うと返り値がもらえます

class MyClass
  def initialize
    @name = "hawk"
  end

  def hello
    print("hello")
    self.tap {}
  end

  def world
    puts("world")
    self
  end

  attr_reader :name
end

cls = MyClass.new
ret = cls.
  tap {|c| puts c.name }.
  tap {|c| c.hello.world }.
  tap {|c| break "break" }
puts ret

=> hawk,helloworld,break

ちなみに break したあとにまた tap することができます
上記の場合は String のオブジェクトが渡されます

最後に

使えるようになると結構便利な機能な気がします
可読性という意味だと tap に慣れていない人だと読みづらくなる場合もあるかもしれません

2020年11月27日金曜日

Python で制御文字 (control character) を削除する方法

概要

制御文字は例えば改行やタブ文字のことを指します
Python で制御文字を操作する場合は unicodedata が便利です

環境

  • macOS 10.15.7
  • Python 3.8.5

サンプルコード

import unicodedata

def remove_control_characters(s):
    return "".join(ch for ch in s if unicodedata.category(ch)[0]!="C")

print('\r' + "return")
print('\t' + "tab")
print(remove_control_characters('\r') + "return")
print(remove_control_characters('\t') + "tab")

結果

こんな感じですべてブランクで置換してくれます

return
        tab
return
tab

参考サイト

2020年11月26日木曜日

cheat.sh を使ってみる

概要

cheat.sh は様々なプログラミング言語やツールのスニペットやショートカットコマンドを簡単に検索、表示することができるツールです
今回はインストールして使ってみました

環境

  • Ubuntu 18.04
  • cheat.sh

インストール

  • curl https://cht.sh/:cht.sh | sudo tee /usr/local/bin/cht.sh
  • chmod +x /usr/local/bin/cht.sh

使ってみる

  • cht.sh ruby

これで実行方法や REPL、ワンライナーのコマンドなどが表示されます
cht.sh は https://cht.sh/ に対してアクセスして情報を取得しています

サポートされている言語の一覧を表示する

  • cht.sh :list

ページの一覧を表示する

  • cht.sh ruby/:list
:learn
:list
Comments
case
classes
control_flow
hello
rosetta/

class の使い方を確認する

  • cht.sh ruby/classes

上記の /:list の結果から classes を指定して URI 形式で指定するだけです

TAB 補完をする

  • curl https://cheat.sh/:bash_completion >> ~/.bashrc

emacs で使う

  • cht.sh /:emacs

で表示される el 情報を .emacs なりに記載すれば OK です
package.el を使って cheat-sh というパッケージをインストールしても OK です

cheat-sh という関数が呼べるようになっているのでコマンドと同じように ruby/:list などをコールすると emacs のバッファで同じ情報が確認できます

Web で確認する

https://cht.sh にアクセスすればここでコマンドと同じ情報を取得することができます

最後に

cheat.sh を使ってみました
ふと言語の文法やオプションなどを確認したいときに使えるかなと思います

2020年11月25日水曜日

emacs で「error in process filter: Could not create connection to melpa.org:443」

概要

Mac 版の emacs で Homebrew でインストールした emacs で発生しました
http のリポジトリを設定すれば OK という記事をよく見かけるのですが
それだと対応できないケースがあるのでそういった場合の対処方法を紹介します

環境

  • macOS 10.15.7
  • emacs 25.3 -> 27.1

emacs を再インストールする

SSL のライブラリがうまくリンクしていないために https 通信がうまく行っていないのだと思います
再インストールするすることでリンクさせましょう

  • xcode-select --install
  • brew reinstall emacs


ただバージョンアップしてしまうのでバージョンを固定したい場合はバージョンを指定して brew install してください

動作確認

emacs を再度開いて package-list-packages などをコールしてみましょう

また brew info emacs で確認すると gnutls がちゃんとチェックになっているのが確認できると思います

2020年11月24日火曜日

Ubuntu18.04 で SSH デーモンを複数起動する方法

概要

異なるポートで複数の sshd を起動する方法を紹介します
単純に設定ファイルを複製しポート番号を書き換えてサービス起動するだけです

環境

  • Ubuntu 18.04

sshd_config の作成

  • cp /etc/ssh/sshd{,-second}_config

ポート番号の変更

  • vim /etc/ssh/sshd-second_config
Port 10022

サービスファイルの作成

  • cp /lib/systemd/system/ssh.service /lib/systemd/system/ssh-second.service

起動する際のコンフィグファイルを変更

  • vim /lib/systemd/system/ssh-second.service
ExecStart=/usr/sbin/sshd -D -f /etc/ssh/sshd-second_config $SSHD_OPTS

サービス登録される際の Alias も変更します

Alias=sshd-second.service

ついでに Description も変更します

Description=OpenBSD Secure second Shell server

systemd に反映させる

  • systemctl daemon-reload

サービス起動

  • systemctl enable ssh-second.service --now
  • systemctl status ssh-second.service

動作確認

  • ssh user@hostname
  • ssh -p 10022 user@hostname

最後に

設定ファイルが別々にあるので片方は鍵認証あり片方は鍵認証なしなども可能です
ポートによる IP 制限は ufw などを使えば良いかなと思います

参考サイト

2020年11月20日金曜日

certbot renew で証明書の自動更新メモ

概要

自分用のメモです
standalone プラグインを使って証明書を取得している場合はかなり簡単に自動更新できます
--pre-hook--post-hook を使って 80 を一瞬停止するのがポイントです

環境

  • Ubuntu18.04
  • certbot 1.9.0

cron 設定

2 ヶ月に 1 回 24:30 に更新する設定です
Lets’s Encrypt は 90 日で期限切れで残り 30 日から更新可能です

80 番はコンテナで動作しているので docker コマンドで停止しています
ここは自身の環境にあったコマンドに変更すれば OK です

30 15 25 */2 * cd /home/hawksnowlog/app; sudo certbot-auto renew --pre-hook "docker-compose stop proxy" --post-hook "docker-compose start proxy"

他にファイアウォールなどで 80 番ポートを制御している場合はそれも一瞬開放する必要があります

2020年11月19日木曜日

docker 上で squid を動かす

概要

前回 Ubuntu 上に squid を構築しました
今回は docker 上に構築してみたいと思います

環境

  • Ubuntu 16.04
  • docker 19.03.12

キャッシュ用のディレクトリ作成

squid がキャッシュした情報を永続化するためにホスト側にマウントします
ディレクトリは何でも OK です

  • mkdir /home/vagrant/cache

コンテナ起動

ではコンテナを起動します
先程作成したキャッシュ用のディレクトリを /var/spool/squid でマウントします

docker run --name squid -d --restart=always \
  -p 3128:3128 \
  -v /home/vagrant/cache:/var/spool/squid \
  sameersbn/squid:3.5.27-2

設定ファイルを使う

デフォルトだと http/https プロキシとして使えないので設定ファイルを変更します

  • mkdir /home/vagrant/config
  • vim /home/vagrant/squid.conf
http_access allow all
docker run --name squid -d --restart=always \
  -p 3128:3128 \
  -v /home/vagrant/config/squid.conf:/etc/squid/squid.conf \
  -v /home/vagrant/cache:/var/spool/squid \
  sameersbn/squid:3.5.27-2

使う

あとは普通に使うだけです
curl の場合は以下のように使えます

  • http_proxy=192.168.100.10:3128 https_proxy=192.168.100.10:3128 curl https://kaka-request-dumper.herokuapp.com/

設定ファイルをリロードするには

SIGHUP を送信します

  • docker kill -s HUP squid

おまけ: docker-compose で起動する

docker-compose で管理したい場合はこれを使いましょう

  • vim docker-compose.yml
version: '3'
services:
  squid:
    image: sameersbn/squid:3.5.27-2
    ports:
      - "3128:3128"
    volumes:
      - /home/vagrant/cache:/var/spool/squid
      - /home/vagrant/config/squid.conf:/etc/squid/squid.conf
    restart: always
  • docker-compose up -d

最後に

docker 上で squid コンテナを立ててみました
バージョンを上げたい場合はイメージを変更すれば動作すると思います

参考サイト

2020年11月18日水曜日

Ansible で環境変数を使って特定の環境ごとにタスクを切り替える方法

概要

例えばデプロイするファイルを動的に切り替えたい場合に毎回 playbook を変更するのは面倒です
また group_vars などで変数で定義してもいいですがそれも毎回変更するのは面倒です
そんなときは環境変数で playbook を実行するたびに別の値を設定しそれを参照するようにできます

環境

  • macOS 10.15.7
  • ansible 2.9.10

サンプルコード

HOGE という環境変数を設定することを想定しています
copy のファイル名や when の条件にも使えます
環境変数の設定を必須にしたい場合は playbook の冒頭で変数のチェックをして空であれば fail にするような処理を入れたほうが良いかなと思います

---
- hosts: localhost
  gather_facts: no
  vars_files:
    - vault.yml
  tasks:
    - name: Check HOGE env
      fail: 
        msg: The HOGE variable is an empty or None.
      when: lookup('env', 'HOGE') == ""

    - name: Print HOGE env
      debug:
        msg: "HOGE = {{ lookup('env', 'HOGE') }}"

    - name: Put a file
      copy:
        src: files/test_{{ lookup('env', 'HOGE') }}.txt
        dest: ~/Downloads/test.txt
        owner: hawksnowlog
        group: staff
        mode: '0644'

実行する場合はコマンドの前に環境変数をセットしてから実行しましょう

  • HOGE=fuga ansible-playbook -i hosts site.yml

特定の値が環境変数に設定されているか確認する場合は

サンプルコードは空の場合にエラーにしました
特定の値が入っていない場合はエラーにしたい場合は when を以下のように変更しましょう

when: lookup('env', 'HOGE') not in ["hoge", "fuga", "foo"]

参考サイト

2020年11月17日火曜日

Gitlab CI で dind 超入門

概要

Gitlab CI で docker in docker (dind) を使ってプロジェクト内のソースを docker build してみました
手順や流れを紹介します

環境

  • macOS 10.15.7
  • docker 19.03.13
  • Gitlab 13.5.1-ee

事前準備: Gitlab の準備

コンテナレジストリを有効にした Gitlab を用意してください
過去の記事を参考にしてローカルに構築しても OK です

事前準備: Gitlab Runner を構築する

過去に Specific Runner を構築する方法を紹介しているのでそれを参考にしても OK です

だたポイントとして以下の条件を満たす Runner をインストールしてください

  • executor は docker を選択
  • Runner 上のマシンで docker が動作する
  • Runner に --docker-privileged の権限を付与

プロジェクトを作成する

プロジェクト名は test として進めます
まずはプロジェクトに push するためのアプリケーションを作成します
何でも OK ですが今回は Web アプリケーションにしてみます

  • bundle init
  • vim Gemfile
gem "sinatra"
  • vim app.rb
require 'sinatra'

get '/' do
  'ok'
end

ローカルで動作確認する場合は

  • bundle exec ruby app.rb -o 0.0.0.0
  • curl localhost:4567

で「ok」が返ってくることを確認します

一応 .gitignore を作成しておきましょう

Dockerfile を作成する

次に Dockerfile を作成します
dind を使った CI ではこの Dockerfile を元に docker build します

  • vim Dockerfile
FROM ruby:2.7

ADD . /app
WORKDIR /app

RUN bundle install

EXPOSE 4567

CMD ["bundle", "exec", "ruby", "app.rb", "-o", "0.0.0.0"]

ローカルで動作確認する場合は

  • docker build -t test .
  • docker run -d --name test -p 4567:4567 test
  • curl localhost:4567

で「ok」が返ってくることを確認します

.gitlab-ci.yml 作成する

では dind を使った docker build を行う CI のルールを作成します
今回の肝になります

使用するベースイメージは docker:19.03.13 ですが実際にビルドするイメージは docker:19.03.13-dind を指定しましょう
CI_REGISTRY_USER, CI_REGISTRY_PASSWORD, CI_REGISTRY は gitlab が用意してくれている変数なのでこれを使って docke login や push を行います

今回は Gitlab の Container Registry に push するのでこのような指定にしていますが dockerhub などの場合には URL や ID/PW を変更してください

  • vim .gitlab-ci.yml
image: docker:19.03.13

stages:
  - build

build_image:
  stage: build
  services:
    - name: docker:19.03.13-dind
  variables:
    DOCKER_HOST: tcp://docker:2375
    DOCKER_TLS_CERTDIR: ""
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker build -t $CI_REGISTRY_IMAGE:latest .
    - docker push $CI_REGISTRY_IMAGE:latest

push して CI が成功するか確認する

  • git init
  • git add .
  • git commit -m "First commit"
  • git remote add origin https://gitlab.example.com/root/test.git
  • git push -u origin master


これで Gitlab のプロジェクトの CI/CD を確認してみましょう

ちゃんと Gitlab の Container Registry にも push されているのが確認できると思います

最後に

Gitlab の CI/CD で docker in docker を使ってイメージビルドしてみました
Runner 側で docker executor の設定がいろいろと必要なのがポイントかなと思います

push する際のイメージ名は latest 以外にもコミットハッシュなどが変数として使えるので状況に応じて変更すると良いかなと思います

参考サイト

2020年11月16日月曜日

flask + python の print が docker logs で表示されない場合の対処方法

概要

どうやら python の print はデフォルトだとバッファリングしているようで、そのままだと docker logs の標準出力に出ないようです
簡単な対象方法を紹介します

環境

  • macOS 10.15.7
  • Python 3.8.5
  • docker 19.03.13

アプリ作成

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    print('hello')
    return 'Hello'

ターミナル上で確認

  • FLASK_APP=app.py pipenv run flask run
  • curl localhost:5000

=> hello が表示される

Dockerfile

FROM python:3.8-buster

ADD . /app
WORKDIR /app

RUN pip install pipenv
RUN pipenv install
ENV FLASK_APP app.py

EXPOSE 5000

CMD ["pipenv", "run", "flask", "run", "--host", "0.0.0.0"]

docker 上で確認

  • docker build -t test_app .
  • docker run -p 5000:5000 --name test_app test_app
  • docker logs test_app

=> 表示されない

対処方法1: flush=True

print 時に flush=True を指定します

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    print('hello', flush=True)
    return 'Hello'

対象方法2: PYTHONUNBUFFERED を指定する

すべての print に flush=True を入れるのは大変なので PYTHONUNBUFFERED で制御します

  • docker run -e PYTHONUNBUFFERED=1 --name test_app -p 5000:5000 test_app

もしくは Dockerfile を書き換えても OK です

FROM python:3.8-buster

ADD . /app
WORKDIR /app

RUN pip install pipenv
RUN pipenv install
ENV FLASK_APP app.py
ENV PYTHONUNBUFFERED 1

EXPOSE 5000

CMD ["pipenv", "run", "flask", "run", "--host", "0.0.0.0"]

app.logger の場合は

特に何もせずに docker logs に出力されました
素直に app.logger を使い回すのが docker との相性は良さそうです

import logging
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    app.logger.setLevel(logging.INFO)
    app.logger.info('hello')
    return 'Hello'

最後に

flask で print を使った際にバッファリングさせない方法を紹介しました
python の print はデフォルトだとバッファリングするので flask には関係なく今回の現象は発生すると思います

flask を使っている場合は素直に app.logger を使うことをおすすめします

参考サイト

2020年11月12日木曜日

Sinatra をミドルウェアとして使ってみる

概要

Sinatra をミドルウェアとして使うと before の前に処理を実行することができるようになります

環境

  • macOS 10.15.7
  • Ruby 2.7.1p83
    • sinatra 2.1.0

サンプルコード

事前に /auth にアクセスしていないと /hello にアクセスすることができません
Middleware として使用している Sinatra のモジュラークラス内で定義している session は引き続きアプリケーションクラス側でも参照することができます

require 'sinatra/base'

class MyMiddleware < Sinatra::Base
  enable :sessions

  get '/auth' do
    session['user'] = 'admin'
  end
end

class MyApp < Sinatra::Base
  use MyMiddleware

  before do
    unless session['user'] == 'admin'
      redirect '/auth'
    end
  end

  get '/hello' do
    'ok'
  end
end

動作確認

curl で確認します
curl で cookies を使う場合は -c, -b オプションを使います

  • curl -c cookie.txt localhost:9292/auth

これでセッションを保存し -b を使って cookie を使用します

  • curl -b localhost:9292/hello

これで ok が返ってくることが確認できます
cookie.txt がない状態で /hello にアクセスするとリダイレクトされることが確認できると思います

最後に

基本的には session やデータベースと組み合わせて使うことになりそうです
処理的には認証などの共通処理や事前処理で使えそうです

2020年11月11日水曜日

Sinatra のクラシックスタイルで Inline Template を使う方法

概要

Inline Template とはテンプレートの情報をアプリケーションのファイルに記載することができる機能です
本来テンプレート (ビュー) は views ディレクトリ配下に作成しますが簡単なアプリケーションなら Inline Template が使えます

環境

  • macOS 10.15.7
  • Ruby 2.7.1p83
    • sinatra 2.1.0

ポイント

クラシックスタイルの場合はクラス内で enable :inline_templates を呼び出す必要があります

サンプルコード

  • vim app.rb
require "sinatra/base"

class MyClass < Sinatra::Base
  enable :inline_templates

  get '/' do
    @message = "Hello"
    erb :home
  end
end

__END__

@@home
<html>
<head>
  <title>home</tile>
</head>
<body>
<h2><%= @message %></h2>
</body>
</html>
  • vim config.ru
require "./app"
run MyClass
  • bundle exec rackup config.ru

参考サイト

2020年11月10日火曜日

Ruby でオブジェクトを簡単に JSON シリアライズする方法

概要

Rails を使っている場合には ActiveModelSerializers があります
単体で使用している場合には使いづらいケースもあります
そんな場合にはシンプルに使える Blueprinter が使えます

環境

  • macOS 10.15.7
  • Ruby 2.7.1p83
    • blueprinter 0.25.1

準備

  • bundle init
  • vim Gemfile
gem "blueprinter"
  • bundle install

まずは普通のクラスを定義

テスト用に作成します
これを JSON にシリアライズしてみます

class User
  def initialize(name, age)
    @name = name
    @age = age
  end

  def show_profile
    puts @name
    puts @age
  end

  attr_accessor :name, :age
end

u = User.new("hawksnowlog", 10)
u.show_profile

シリアライズできるようにする

blueprinter と json を require します
json を require しないと uninitialized constant Blueprinter::Configuration::JSON (NameError) になります
シリアライズするクラスは Blueprinter::Base を継承します

あとは JSON のフィールドとして表示するインスタンス変数を fileds で定義します
JSON として出力する場合は render メソッドをコールして生成した User クラスのオブジェクトを渡します

require 'blueprinter'
require 'json'

class User < Blueprinter::Base
  fields :name, :age

  def initialize(name, age)
    @name = name
    @age = age
  end

  def show_profile
    puts @name
    puts @age
  end

  attr_accessor :name, :age
end

u = User.new("hawksnowlog", 10)
puts User.render(u)

識別子の自動付与

identifier を使います
uuid というメソッドを定義するか identifier の引数にブロックを与えることで uuid として設定する値を決めることができます
今回は SecureRandom.uuid の値を返していますが本来はデータベースなどから識別子を取得して返却するようになると思います

require 'blueprinter'
require 'json'
require 'securerandom'

class User < Blueprinter::Base
  identifier :uuid
  fields :name, :age

  def initialize(name, age)
    @name = name
    @age = age
  end

  def uuid
    SecureRandom.uuid
  end

  def show_profile
    puts @name
    puts @age
  end

  attr_accessor :name, :age
end

u = User.new("hawksnowlog", 10)
puts User.render(u)

フィールドのデフォルト値の設定

nil などでインスタンス変数の値が初期化されていない場合に返却するデフォルトの値を設定することができます
Blueprinter 全体の設定をするには Blueprint.configure を使います
ここで config.field_default にデフォルトの値を設定することができます

require 'blueprinter'
require 'json'
require 'securerandom'

class User < Blueprinter::Base
  identifier :uuid
  fields :name, :age

  Blueprinter.configure do |config|
    config.field_default = "N/A"
    config.association_default = {}
  end

  def initialize(name, age)
    @name = name
    @age = age
  end

  def uuid
    SecureRandom.uuid
  end

  def show_profile
    puts @name
    puts @age
  end

  attr_accessor :name, :age
end

u = User.new("hawksnowlog", nil)
puts User.render(u)

最後に

簡単ですが Blueprinter の使い方を紹介しました
to_json などを実装して自分でシリアライズするメソッドを実装しても問題ないですがデフォルト値やバリデーションなどを考えるとすでにその辺りが実装されている Blueprinter が便利かなと思います

既存のクラスを拡張する必要があるのでそこがネックになる可能性もありますが新規のクラスであれば簡単に拡張できると思います

参考サイト

2020年11月9日月曜日

pyenv でインストールした python で ModuleNotFoundError: No module named 'lsb_release' が発生した場合の対処方法

概要

過去に Ubuntu に pyenv をインストールし最新版の python をインストールする方法を紹介しました
pyenv でインストールした python をメインに使っていると lsb_release というコマンドが 0 以外のステータスを返しエラーになってしまいます
そんな場合の対処方法を紹介します

環境

  • Ubuntu 16.04 (on Vagrant)
  • Python 3.8.5
  • pyenv 1.2.20

対処方法

  • cp /usr/share/pyshared/lsb_release.py /home/vagrant/.pyenv/versions/3.8.4/lib/python3.8/site-packages

pyenv でインストールすると site-packages 配下に lsb_release.py がないためエラーになっていました
lsb_release コマンド自体が動作しなくても問題はないですが他のツールで影響を受ける場合は上記の対応をしてください

Tips: site-packages 配下を表示する方法

  • python3 -c "import site; print (site.getsitepackages())"

参考サイト

2020年11月6日金曜日

Ansible で Docker Swarm 環境を構築してみた

概要

community.general.docker_swarm なるロールがあったのでこれを使って docker swarm 環境を構築してみました
なお作業するマシンは Swarm の Master 側の Ubuntu 上で行います

環境

  • Ubuntu18.04 x 2
  • docker 19.03.13
  • ansible 2.9.15

docker の 2376 ポートの開放

こちらを参考に開放してください

お互いのマシンから 192.168.100.10:2376/info にアクセスして JSON が返ってくれば OK です

お互い SSH できるようにする

  • ssh-keygen -t rsa

をお互いのマシンで実行し id_rsa.pub を作成します
そしてそれぞれのマシンの authorized_keys に記載しましょう

ロールのインストール

  • ansible-galaxy collection install community.general

playbook の作成

では playbook を作成していきます
完成形は以下のような構成にします

.
├── group_vars
│   └── test
│       └── vars.yml
├── inventory
│   └── test
│       └── hosts.yml
├── roles
│   └── swarm
│       └── tasks
│           └── main.yml
└── site.yml

inventory ファイルの作成

  • mkdir -p inventory/test
  • vim inventory/test/hosts.yml
---

test:
  hosts:
    ubuntu:
      ansible_host: "{{ worker_ip }}"
    ubuntu2:
      ansible_host: "{{ manager_ip }}"

変数ファイルの作成

  • mkdir -p group_vars/test
  • vim group_vars/test/vars.yml
---

ansible_python_interpreter: /usr/bin/python3

cluster_interface: enp0s8

manager_ip: 192.168.100.11
worker_ip: 192.168.100.10
manager_host_name: ubuntu2

タスクファイルの作成

  • mkdir -p roles/swarm/tasks
  • vim roles/swarm/tasks/main.yml
---

- name: Install docker-py
  pip:
    name: docker-py

- name: Init a new swarm with default parameters
  community.general.docker_swarm:
    state: present
    advertise_addr: "{{ manager_ip }}"
  register: rezult
  when: lookup('vars', 'ansible_' + cluster_interface).ipv4.address == manager_ip                                                

- name: Add nodes
  community.general.docker_swarm:
    state: join
    advertise_addr: "{{ worker_ip }}"
    join_token: "{{ hostvars[manager_host_name]['rezult'].swarm_facts.JoinTokens.Worker }}"                                      
    remote_addrs: [ "{{ manager_ip }}:2377" ]
  when: lookup('vars', 'ansible_' + cluster_interface).ipv4.address == worker_ip

ちょっと長いので説明します

まず docker-py をインストールしています
今回使用する community.general.docker_swarm は内部的に docker-py を使って docker の操作をしているためです

次に swarm init しています
初期化するノードは manager ノードのみなので IP で実行するノードを制限しています
ここは IP ではなく inventory や group で制御しても問題ないです

最後に swarm join しています
これは worker ノードで実行する必要があるのでこれも IP で制御しています
更に join するためのトークンは manager ノード側の hostvars に格納されているのでそれを参照するようにしています
remote_addrs は docker swarm 用の管理ポートになるので開放した 2367 ではなく swarm manager が起動した際の 2377 を指定します

実行メインファイルの作成

ロールを指定して実行するだけです

  • vim site.yml
---

- name: Build swarm cluster
  hosts: test
  become: true
  roles:
    - role: "swarm"
      tags: ["swarm"]

動作確認

実行してみましょう

  • ansible-playbook -i inventory/test site.yml

成功すると docker swarm クラスタが構築されています

  • docker node ls

あたりで確認すると良いと思います

最後に

Ansible で community.general.docker_swarm を使って docker swarm を構築してみました
今回は 2 台だったのでイマイチありがたみが出てきませんでしたがこれが 10 台くらいの規模になると楽な運用ができるようになると思います

今回は単純な join だけを紹介しましたが leave や update も実行できるのでコマンドのオペレーションは一通り Ansible にすることはできそうです

参考サイト

2020年11月5日木曜日

ansible で nickjj.docker を使ってみた

概要

nickjj.docker は ansible を使って docker 環境を構築できるロールです
今回は Ubuntu 18.04 上で試してみました

環境

  • Ubuntu 18.04 (on Vagrant)
  • ansible 2.9.15

Vagrantfile

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/bionic64"
  config.vm.network "private_network", ip: "192.168.100.11"
  config.vm.provider "virtualbox" do |vb|
    vb.memory = 4096
    vb.cpus = 2
  end
end

ansible インストール

Vagrant で構築して Ubuntu 上で作業していきます

  • sudo apt -y update
  • sudo apt -y install software-properties-common
  • sudo apt-add-repository --yes --update ppa:ansible/ansible
  • sudo apt -y install ansible

ノンパスログイン

  • ssh-keygen -t rsa
  • cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
  • chmod og-wx ~/.ssh/authorized_keys

ロールインストール

  • ansible-galaxy install nickjj.docker

試す

  • vim hosts
[localhost]
localhost
  • vim site.yml
---
# site.yml
- name: Example
  hosts: localhost
  become: true
  roles:
    - role: "nickjj.docker"
      tags: ["docker"]
  • ansible-playbook -i hosts site.yml

これで docker の最新版がインストールされています
ログを見ているとわかりますが公式の手順と同様にリポジトリの追加など行っています
dockerd の設定なども playbook から変更できそうです

  • docker -v

=> 19.03.13

  • docker-compose -v

=> 1.26.2

デフォルトで docker-compose もインストールしてくれます
バージョンの指定やレジストリの URL の指定もレシピからできます

最後に

nickjj.docker を使って docker のインストールと docker-compose のインストールをしてみました
ansible 環境がすでにある場合はこれを使うと簡単にインストールできそうです

Swarm の構築までは行ってくれなさそうです

参考サイト

2020年11月4日水曜日

Python3 の urllib を使って multipart/form-data を送信してみた

概要

urllib では multipart/form-data を送信するための便利メソッドなどは用意されていません
なので今回は自力で multipart/form-data のリクエストボディを生成してリクエストしてみました

環境

  • macOS 10.15.7
  • Python 3.8.5

テキストファイルを送信するサンプル

multipart/form-data はすごい簡単に説明すると boundary という境界文字で区切った複数のコンテンツを送信する方式です
まずは適当な文字列のフォーム+テキストファイルのフォームを送信してみました

  • vim text_file_test.py
import urllib
import urllib.request

url = "http://localhost:8080/upload"
encoding = "utf-8"
boundary = "--------python"

def multipart_formdata():
    lines = []

    lines.append('--' + boundary)
    lines.append('Content-Disposition: form-data; name="message"')
    lines.append('')
    lines.append("hello")

    lines.append('--' + boundary)
    lines.append('Content-Disposition: form-data; name="uploadfile"; filename="uploadfile.txt"')
    lines.append('Content-Type: text/plain')
    lines.append('')
    lines.append("hoge")
    lines.append('')
    lines.append("--" + boundary + "--")
    lines.append('')

    value = "\n".join(lines)
    return value.encode(encoding)

def request_with_multipart_formdata():
    req = urllib.request.Request(url)
    req.add_header("Content-Type", "multipart/form-data; boundary=%s" % boundary)
    data = multipart_formdata()
    with urllib.request.urlopen(req, data) as response:
        print(response.status)

request_with_multipart_formdata()

バイナリファイルを送信するサンプル

画像などのデータを送信する方法です
受信側が Content-Transfer-Encoding: base64 に対応していれば送信できるはずですが

import urllib
import urllib.request
import base64

url = "http://localhost:8080/upload"
encoding = "utf-8"
boundary = "--------python"

f = open("image.png", "rb", buffering=0)
file_data = f.read()
f.close()

def multipart_formdata():
    value = '--' + boundary
    value += 'Content-Disposition: form-data; name="message"'
    value += ''
    value += 'hello'
    value += '--' + boundary
    value += 'Content-Disposition: form-data; name="uploadfile"; filename="image.png"'
    value += 'Content-Type: image/png'
    value += 'Content-Transfer-Encoding: base64'
    value += ''
    value += str(base64.b64encode(file_data))
    value += ''
    value += "--" + boundary + "--"
    value += ''

    return value.encode(encoding)

def request_with_multipart_formdata():
    req = urllib.request.Request(url)
    req.add_header("Content-Type", "multipart/form-data; boundary=%s" % boundary)
    data = multipart_formdata()
    with urllib.request.urlopen(req, data) as response:
        print(response.status)

request_with_multipart_formdata()

最後に

標準の urllib ではかなり辛いので素直に requests を使うのが良いと思います

参考サイト

2020年11月3日火曜日

ansible vault 超入門

概要

触ってみないことにはわからないのでこちらのチュートリアルっぽい記事を参考に自分でも試してみました

環境

  • macOS 10.15.7
  • ansible 2.9.10

とりあえず動作する playbook の作成

まずはパスワードを暗号化していない状態で正常に動作する playbook を作成します

  • vim hosts
localhost ansible_connection=local
  • vim site.yml
---
- hosts: localhost
  gather_facts: no
  vars_files:
    - vault.yml
  tasks:
    - debug: msg="password = {{ password }}"
  • vim vault.yml
---
password: 'hogehoge'

これで実況すると vault.yml のパスワードの内容がそのまま表示されるのが確認できると思います

  • ansible-playbook -i hosts site.yml

ansible vault を使って vault.yml に書かれているパスワードを暗号化する

ansible-vault encrypt コマンドを使って先程の暗号化されていない vault.yml ファイルを指定します
この際に暗号化するためのパスワードが要求されるので適当に入力しましょう

  • ansible-vault encrypt vault.yml
New Vault password: Confirm New Vault password: Encryption successful

暗号化に成功すると上記のようになります
暗号化に使用したパスワードは別途ちゃんと覚えておきましょう

ファイルの内容を確認すると今度は暗号化された内容になっているが確認できると思います

  • cat vault.yml
$ANSIBLE_VAULT;1.1;AES256 63353464303138643235653836613236623637613733626562663234363866393836653064323337 3230346232373261353165616561313831323134633338390a396666303637323336346566343836 30306662343835303261386235313064636162373064626539386539633334396530383766326438 6230386530393864330a643162386461613066653537666431636162353361303934336361626131 32646165653533346639343137356465396362613237643562363030393966663565

この状態で再度 playbook を実行しようとすると今度はエラーになります

  • ansible-playbook -i hosts site.yml
ERROR! Attempting to decrypt but no vault secrets found

暗号化されたファイルを使って playbook を流してみる

暗号化する際に入力したパスワードを使うことで暗号化されたファイルを使って playbook を実行することができます
--ask-vault-pass というオプションを使えば OK です

  • ansible-playbook -i hosts site.yml --ask-vault-pass

これで Vault password の入力が求められるので暗号化したときのパスワードを入力すれば OK です

Vault password を入力しない場合は

毎回 Vault password を入力するのが面倒という場合は ansible.cfg を使うことで解決できます

  • vim ansible.cfg
[defaults]
vault_password_file = ~/.vault_password
  • vim ~/.vaule_password

でファイルを開いて Vault password を書いておくことで --ask-vault-pass が不要になります

  • ansible-playbook -i hosts site.yml

暗号化したファイルを復号化するには

単純に復号化してファイルの内容を確認することができます
その場合は ansible-vault decrypt を使います

  • ansible-vault decrypt vault.yml

ansible.cfg に vault_password_file が記載されていてすでに復号化用のパスワードが記載されている場合は Vault password を入力せずに復号化できます

再度暗号化する場合は ansible-vault encrypt を使います

  • ansible-vault decrypt vault.yml

最後に

ansible-vault を使ってみました
やろうと思えばこれですべての playbook を暗号化できますがそこまでやる必要はないと思います

参考サイト

2020年11月2日月曜日

Sinatra で Rack::Attack を使ってみる

概要

rack-attack は Rack ミドルウェアを使ってアクセス制御を実現できるミドルウェアです
Sinatra は Rack を実装しているので今回は Sinatra を使って Rack::Attack を使ってみました
ただ使ってみた感じだと blocklistthrottle しか動作していないように見えました
ローカルネットワークからのアクセスだったのでローカルネットワークからのアクセスは基本的には許可しているのかもしれません

環境

  • macOS 10.15.7
  • Ruby 2.7.1p83
    • sinatra 2.0.7
    • rack-attack 6.3.1

インストール

  • vim Gemfile
gem "sinatra"
gem "rack-attack"
gem "activesupport"
  • bundle install

activesupport はスロットリングの機能に使います

有効にする

基本的には config.ru のみを編集していきます

  • vim config.ru
require './app'
require "rack/attack"

use Rack::Attack
Rack::Attack.enabled = true

run MyApp

アプリは何でも OK です

  • vim app.rb
require 'sinatra/base'

class MyApp < Sinatra::Base
  get '/admin' do
    'ok'
  end
end

起動して問題なくアクセスできるか確認します

  • bundle exec rackup config.ru -o 0.0.0.0
  • curl localhost:9292/admin

アクセス可能な IP で制御する

アクセス可能な IP で制御してみます
blocklist というメソッドにブロックでアクセス拒否する条件を記載できます
request オブジェクトが受け取れるのでリクエストの情報を元に制御するのが基本になります

# coding: utf-8
require './app'
require "rack/attack"

use Rack::Attack
Rack::Attack.enabled = true

Rack::Attack.blocklist("特定の IP のみ admin パスにアクセスできる") do |request|
  !(request.ip == "127.0.0.1" && request.path == "/admin")
end

run MyApp

本来であれば safelist を使って否定構文を削除したほうが良いです
なぜか safelist がうまく動作せず blocklist を使っているためこのような書き方になっています

起動して動作確認してみましょう
ローカルからのアクセスは拒否されないことを確認しましょう

  • bundle exec rackup config.ru -o 0.0.0.0
  • curl localhost:9292/admin

=> ok

アクセス不可なパスを制御する

例えば特定のヘッダが付与されていないとアクセスできないパスを作ってみます
blocklist を使ってパスとヘッダ情報をチェックしましょう

  • vim config.ru
# coding: utf-8
require './app'
require "rack/attack"

use Rack::Attack
Rack::Attack.enabled = true

Rack::Attack.blocklist("API キーが設定されているユーザのみ admin パスにアクセスできる") do |request|
  !(request.env["HTTP_APIKEY"] == "secret-string" && request.path == "/admin")
end

run MyApp

起動して動作確認してみましょう
ちゃんとヘッダがないと 403 になることが確認できます

  • bundle exec rackup config.ru -o 0.0.0.0
  • curl localhost:9292/admin

=> Forbidden

  • curl -H "APIKEY: secret-string" localhost:9292/admin

=> ok

スロットリングしてみる

スロットリングとは時間と回数によるアクセス制御のことです
例えば 2 秒間に 5 回しかアクセスできないようなスロットリングをしてみます

# coding: utf-8
require './app'
require "rack/attack"
require "active_support"

use Rack::Attack
Rack::Attack.enabled = true
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new

Rack::Attack.throttle("2 秒間に 5 回しかアクセスできない", limit: 5, period: 2) do |request|
  request.ip
end

run MyApp

起動して動作確認してみましょう
連続でアクセスすると ok が 5 回続いた後はすべて「Retry later (429)」になることが確認できます
そして 2 秒後に再度 5 回アクセスできることが確認できると思います

  • bundle exec rackup config.ru -o 0.0.0.0
  • for i inseq 1 60; do curl localhost:9292/admin; done

=> okokokokok => Retry later

最後に

Sinatra で Rack::Attack を使ってみました
Rails で使う例はたくさん見かけたのですが Sinatra で使う方法はあまり紹介されていなかったので自分で試してみました

safelist がうまく動作していないように見えましたがもしかするとローカルネットワークからのアクセス意外であれば動作するかもしれません
スロットリングなどはパスやユーザ名などいろいろなパラメータでスロットリングを掛けることもできるので便利な機能かなと思います

参考サイト