Kubernetesの勉強がてら勉強会
参加してきたのは以下。
SpringBootでマイクロサービスを作ってKubernetesにデプロイしてみる【実践編】 dreamarts.connpass.com
尚、当日資料は下記。
K8S とは?
コンテナオーケストレーション技術。
コンテナオーケストレーションは、
- コンテナが落ちた時に自動でコンテナを用意してアップしてくれる
- 新しいバージョンリリースの時には「新しいコンテナを作って新版デプロイ」「接続設定を新版に切り替え」「旧版コンテナの削除」を順次行ってくれる
要するにバックのサービス云々を意識せすに、サービス維持を自動化してもらうツールと言える。
特徴として
- Declarative 宣言的
- Immutable 固定化
- AutoHealing 自己修復的
この特徴のおかげで、「致命的エラーが出た?新しいコンテナ作ればいいやん」という状況が生まれる。
ただし、このアーキテクチャを採用する場合
- サーバサイドにセッション情報を保持する作りは崩壊する。
- つまりステートレスサーバを作らなければならない(破棄前提)
個人的な意見だと Playframework でやればおk!
K8s の構成
基本的に
- kubectl
コントロールサーバ。ここから K8s の - kube-apiserver
kubectl からリクエストを受ける部分。
こいつが kube-scheduler に処理を移譲する - kube-scheduler
apiserver で受けた内容を元に、Pod を操作する部分。 - Kubelet
Podを起動し管理するエージェント - Pod
コンテナ本体
という構成とのこと。
Docker
カーネルレベルの機能としてプロセスを分離し、そこで Ubuntuなどの様なコマンドセットを用意することで、それぞれのディストリビューションとして振る舞う。
という様なお話。
すみません、自分で調べてたので聞き流してしまいました(汗
Docker 周りはこの辺聞けばいいと思う。
Flask 1.0.2 入門してみた
見通しよく、REST API を書きたいと思ったのだけど、Flask-restful がなんともしっかりした REST 特化しすぎてて微妙だったので、Flask 単品でちょっと実験したメモ
Install といっても…
Python だとこれだけ。
$ pip install Flask
ルーティング機構
と言っても非常にみたまま素直な感じ。
とてもわかりやすくてよい。
from flask import Flask, request app = Flask(__name__) @app.route('/seminers', methods=['GET', 'POST']) def seminers(): print(request) print(request.data) print(request.form) print(request.args) return 'Example message' app.run(debug = True, port = 3000)
リクエスト等の受け取り
で、どうなるのか気になったのがここなので、色々上記に流し込んで試してみた。
まずは単純リクエスト
$ curl http://localhost:3000/seminers
実行結果…まぁそうね。
null になってないあたり個人的に好き
<Request 'http://localhost:3000/seminers' [GET]> b'' ImmutableMultiDict([]) ImmutableMultiDict([])
URL パラメータ突っ込むと
$ curl http://localhost:3000/seminers?welcome=data
args が反応する
<Request 'http://localhost:3000/seminers?welcome=data' [GET]> b'' ImmutableMultiDict([]) ImmutableMultiDict([('welcome', 'data')])
今度は Form POST
$ curl -F "key=value" -F "msg=ひゃっはー" http://localhost:3000/seminers
request.form
が反応
<Request 'http://localhost:3000/seminers' [POST]> b'' ImmutableMultiDict([('key', 'value'), ('msg', 'ひゃっはー')]) ImmutableMultiDict([])
ファイルを送信してみる。適当に ss.png ファイルをキャプチャで作って食わせた
$ curl -F "key=value" -F "file=@ss.png" http://localhost:3000/seminers
えーっと(汗
<Request 'http://localhost:3000/seminers' [POST]> b'' ImmutableMultiDict([('key', 'value')]) ImmutableMultiDict([])
調べてみたら files
フィールドに格納されるらしい
@app.route('/seminers', methods=['GET', 'POST']) def seminers(): print(request) print(request.data) print(request.files) print(request.form) print(request.args) return 'Example message'
書き換えたリトライ。
でたでた。form/maltipart-formdata の区別は自動でやってくれてるらしい。
<Request 'http://localhost:3000/seminers' [POST]> b'' ImmutableMultiDict([('file', <FileStorage: 'ss.png' ('application/octet-stream')>)]) ImmutableMultiDict([('key', 'value')]) ImmutableMultiDict([])
なら、JSON はどうだろう?
$ curl -H 'Content-Type:application/json' -d "{"key":"val","key2":",val2"}" http://localhost:3000/seminers
ここにきて data
が反応。
application/json
にはデフォルトでは対応していない模様。
<Request 'http://localhost:3000/seminers' [POST]> b'{key:val,key2:,val2}' ImmutableMultiDict([]) ImmutableMultiDict([]) ImmutableMultiDict([])
json
パッケージとか使えばどうとでもなりそう。
import json @app.route('/seminers', methods=['GET', 'POST']) def seminers(): print(request) print(request.data) print(request.files) print(request.form) print(request.args) if request.headers['Content-Type'] == 'application/json': json_value = json.loads(request.data) print(json_value) return 'Example message'
そして投げ直す
$ curl -H 'Content-Type:application/json' -d "{\"key\":\"val\",\"key2\":\"val2\"}" http://localhost:3000/seminers
うまく行ったけど、あまりにダサい。
if とか腹立たしいのだけど…
<Request 'http://localhost:3000/seminers' [POST]> b'{"key":"val","key2":"val2"}' ImmutableMultiDict([]) ImmutableMultiDict([]) ImmutableMultiDict([]) {'key': 'val', 'key2': 'val2'}
application/json
にしか公開しないとかねーのかと思ったのだけど、
まぁこういう振り分けこそコントローラの仕事だし仕方ないのか(汗
Pandas の練習がてら、勉強会のトレンドを探ってみた
この辺の続きです。
4月の勉強会の開催状況と、事前申し込みの状況を眺めてみたが正解。
勉強会情報を拾ってくる
前回の流れですが、2サイト以上でやるので、少しだけ汎用化を考えてみた。
調べてみたら、ATND と COMPASS のAPI仕様がほぼ同一だった。
まずは共通の定義を用意して
import requests import sys class ClassRoom: def __init__(self, title, limit, accepted, waiting): def or_zero(v): if isinstance(v, int): return v return 0 self.title = title self.limit = or_zero(limit) self.joins = or_zero(accepted) + or_zero(waiting) def __str__(self): return f'ClassRoom({self.title}, {self.limit}, {self.joins})' class ClassRoomLoader: def load_class_rooms(self, month: str) -> list: pass class AbstractCompassAtnd(ClassRoomLoader): def __init__(self, api_url: str, max_event_num: int): self.api_url = api_url self.max_event_num = max_event_num def create_request_parameter(self, target_month: str, top: int): return {"ym": target_month, "count": self.max_event_num, "format": "json", "start": top} def request_to_compass(self, target_month: str, top: int) -> requests.Response: params = self.create_request_parameter(target_month, top) query = '&'.join([f'{key}={params[key]}' for key in params.keys()]) print(f'Request: {self.api_url}?{query}') return requests.get(self.api_url + '?' + query) def convert(self, event) -> ClassRoom: try: return ClassRoom(event['title'], event['limit'], event['accepted'], event['waiting']) except: print(f'Error: {sys.exc_info()[0]}') print(f'Value: {event}') raise Exception('Cannot convert') def load_request(self, target_month: str, top: int): class_rooms = [] r_get = self.request_to_compass(target_month, top) print(r_get.status_code) respond_json = r_get.json() for ev in respond_json['events']: class_rooms.append(self.convert(ev)) return (respond_json['results_returned'], class_rooms) def load_class_rooms(self, month: str) -> list: getted = self.max_event_num current_top = 1 class_rooms = [] request = 0 while getted == self.max_event_num: # 最大数以下が返ってきている == まだ次がある request += 1 getted, tmp_rooms = self.load_request(month, current_top) current_top += self.max_event_num class_rooms.extend(tmp_rooms) print( f'Request: {request}, Return: {getted}, currentTop: {current_top}') return class_rooms
単純に読み込む。
尚、読み取った勉強会情報は、pickle でダンプしておく。
class AtndLoader(AbstractCompassAtnd): COMPASS_API = 'http://api.atnd.org/events/' WANT_EVENT_NUM = 100 def __init__(self): super().__init__(AtndLoader.COMPASS_API, AtndLoader.WANT_EVENT_NUM) def convert(self, event) -> ClassRoom: return super().convert(event['event']) def create_request_parameter(self, target_month: str, top: int): return {"ym": target_month, "count": self.max_event_num, "format": "json", "start": top} class CompassLoader(AbstractCompassAtnd): COMPASS_API = 'https://connpass.com/api/v1/event/' WANT_EVENT_NUM = 100 def __init__(self): super().__init__(CompassLoader.COMPASS_API, CompassLoader.WANT_EVENT_NUM) def create_request_parameter(self, target_month: str, top: int): return {"ym": target_month, "count": self.max_event_num, "order": 1, "start": top} # import pickle for backup dump. import pickle if __name__ == "__main__": class_rooms = AtndLoader().load_class_rooms("201904") class_rooms.extend(CompassLoader().load_class_rooms("201904")) with open('backup.pickle', 'wb') as f: pickle.dump(class_rooms, f) print("\n".join([str(s) for s in class_rooms]))続きを読む
SQLAlchemy のさわりだけ
SQLAlchemy?
Python の O/R マッパーです。
スクリプト言語だし、たかが数行試すだけならSQL 文書いても…とは思ったのだけど、せっかくなので
検証は
- python : 3.6.8 (anaconda)
- sqlite3 インストール済み
- SQLAlchemy : 1.3
インストール
$ pip install SQLAlchemy
スキーマ作成
まずはインストールして
# Importing SQLAlchemy from sqlalchemy import create_engine, Column, Integer, String, Float from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from sqlalchemy.orm.exc import NoResultFound
どうもテーブルがなければ作るという挙動らしい。
# Create Schema. Base = declarative_base() class ClassRoomInfo(Base): __tablename__ = 'class_room_info' id = Column(Integer, primary_key = True, autoincrement = True) title = Column(String(512)) limit = Column(Integer) joins = Column(Integer) def __repr__(self): return "<ClassRoomInfo(id='%s', title='%s', limit='%s', joins='%s')>" % (self.id, self.title, self.limit, self.joins) Base.metadata.create_all(engine)
セッションを作って操作
コネクションの類は全部セッションがどうにかしてくれるご様子。
# Database session start. Session = sessionmaker(bind=engine) session = Session() # ここに何かやりたいこと # End of session. session.close()
INSERT / SELECT
さすが Python 見たままやなー
# Isertion to database. session.add(ClassRoomInfo(title='ひゃっはー', limit=20, joins=10)) # 中略(500 件くらい挿入) # commit session.commit() # Selection id50 = session.query(ClassRoomInfo).filter_by(id = 50).one() print(f'ID 50 = {id50}')
Anaconda パッケージのアップデート
といっても、そんな大したコマンドではないので、あくまでメモ
$ sudo conda update --all
Python 3.6.8 までが今日時点で出てたのでサクッと入れた。
問題は pandas とかアップデート入ってたから、前に書いたサンプルとか動かなくなってそうな点…
結構 Depricate あったからなぁ(汗
AmazonLinux2 に Java11 + Tomcat9 を Ansible2.7 で突っ込む
やろうと思ったきっかけは、そんなわりかし新しい構成でサーバ作ってと頼まれたので。
そして、やってみるとわかるのだけど、2019/03/21 時点でこれをリポジトリサポートしてるLinuxがそうそう無いと言うことに気づく。
強いて言えば Ubuntu18 が JDK11 だけサポートしてた。
でも Tomcat の Java11 対応って、Tomcat9 からなので、それなりに最新を入れざるを得なかったという問題があった。
前回もそんな流れで、JDK11 だけ先行して入ってりゃどうにかなんだろーと思ったためだ。
しかし甘かった…Tomcat9 の問題が待ち構えていたのだ。
なので、今回は前回をパワーアップチャレンジした。
インストールの段階を分離
Playbook は本来やりたいことの単位で区切っておいた方が、可読性の問題から言ってもいいので、全体として 4 つの playbook を作った。
- main.yml
メインの playbook。他の platbook を呼び出すハブ
AmazonLinux2 に ansible 2.7 で JDK11 をぶち込む
VM 作ったらまずは署名認証
VM を立ち上げて、 ssh を確認(今回は 192.168.56.2 にした)したら、署名で SSH できるようにする。
とりあえず mac 側で署名署名などを作る
$ cd ~/.ssh $ ssh-keygen -t rsa -b 4096 -C "hoge@example.com" -f ~/.ssh/virtualbox Generating public/private rsa key pair. Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /Users/xxxx/.ssh/virtualbox. Your public key has been saved in /Users/xxxx/.ssh/virtualbox.pub. The key fingerprint is: SHA256:eKnjs4jiHsEktydZ7McMGULVDBQ/Wg3kHpeewVfGVmc hoge@example.com The key's randomart image is: +---[RSA 4096]----+ | .oo**o .o.. E| | o =o+ . oo o | |... = * * .. | |+. = B * = | | o+ + * S | | .o . o | | . o | |. .. o.. | |o+. . oo | +----[SHA256]-----+ $ ls known_hosts virtualbox virtualbox.pub
したら公開鍵を転送
$ scp virtualbox.pub ec2-user@192.168.56.2:~/ ec2-user@192.168.56.2's password: virtualbox.pub $ ssh ec2-user@192.168.56.2
authorized_keys に登録する
$ ssh ec2-user@192.168.56.2 $ cd .ssh/ [ec2-user@amazonlinux .ssh]$ ls -l 合計 0 -rw------- 1 ec2-user ec2-user 0 3月 19 21:22 authorized_keys [ec2-user@amazonlinux .ssh]$ cat ~/virtualbox.pub >> authorized_keys
ansible 設定
適当に ansible.cfg を作る。
曰く、ローカルディレクトリにおいとけば効くのだそうだ
[defaults] host_key_checking=False inventory=/Users/xxxx/workspace/VirtualBox/hosts private_key_file=/Users/xxxx/workspace/VirtualBox/virtualbox
なんか勢いでディレクトリ作っちゃったので、署名の類も移動するか
mv ~/.ssh/virtualbox* /Users/xxxx/workspace/VirtualBox/
hosts も作って
[develop-server] 192.168.56.2
これでもうコマンドは通るはずなので
$ ansible all -u ec2-user -m ping Enter passphrase for key '/Users/xxxx/workspace/VirtualBox/virtualbox': 192.168.56.2 | SUCCESS => { "changed": false, "ping": "pong" }
OK通ったね。
Amazon Corretto11 を DL させてインストールする
といっても、単純に Playbook を書くだけ。
- hosts: develop-server user: ec2-user sudo: yes vars: corretto: https://d3pxv6yz143wms.cloudfront.net/11.0.2.9.3/java-11-amazon-corretto-devel-11.0.2.9-3.x86_64.rpm java_home: /usr/lib/jvm/java-11-amazon-corretto tasks: - name: Update all package yum: name: '*' state: latest - name: Install wget. yum: name: wget state: latest - name: Install corret. yum: name: '{{ corretto }}' state: present - name: Alternatives. alternatives: name: '{{ item }}' link: "/usr/bin/{{ item }}" path: "{{ java_home }}/bin/{{ item }}" with_items: - java - javac
実行すると
$ ansible-playbook java11.yml [DEPRECATION WARNING]: Instead of sudo/sudo_user, use become/become_user and make sure become_method is 'sudo' (default). This feature will be removed in version 2.9. Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg. PLAY [develop-server] ****************************************************************************************************************************************************************** TASK [Gathering Facts] ***************************************************************************************************************************************************************** Enter passphrase for key '/Users/xxxx/workspace/VirtualBox/virtualbox': ok: [192.168.56.2] TASK [Update all package] ************************************************************************************************************************************************************** ok: [192.168.56.2] TASK [Install wget.] ******************************************************************************************************************************************************************* ok: [192.168.56.2] TASK [Install corret.] ***************************************************************************************************************************************************************** ok: [192.168.56.2] TASK [Alternatives.] ******************************************************************************************************************************************************************* ok: [192.168.56.2] => (item=java) ok: [192.168.56.2] => (item=javac) PLAY RECAP ***************************************************************************************************************************************************************************** 192.168.56.2 : ok=5 changed=0 unreachable=0 failed=0
問題なく終了し、
$ ssh ec2-user@192.168.56.2 ec2-user@192.168.56.2's password: Last login: Tue Mar 19 23:55:37 2019 from 192.168.56.1 __| __|_ ) _| ( / Amazon Linux 2 AMI ___|\___|___| https://aws.amazon.com/amazon-linux-2/ [ec2-user@amazonlinux ~]$ java --version openjdk 11.0.2 2019-01-15 LTS OpenJDK Runtime Environment Corretto-11.0.2.9.3 (build 11.0.2+9-LTS) OpenJDK 64-Bit Server VM Corretto-11.0.2.9.3 (build 11.0.2+9-LTS, mixed mode)
完了。
調べながらでも 2h で済むか…