msawady’s learning memo

フルスタックエンジニアを目指して、学んだことをログっていくところ

【Python】運用スクリプトはshellじゃなくてPythonで書こうよ、という話

はじめに

  • 自分のプロジェクトではベースとなるシステムがPythonで書かれていることもあり、インフラ/運用系スクリプトも基本的にPythonで書かれています。
  • 一方で、社内の他のプロジェクトではやはりshellが主流です。。。先日もジュニアなエンジニアが先人の残した"芸術的な"シェルとにらめっこしているところを目撃しました....
  • どちらも経験した自分としては、圧倒的にPythonで書くほうが良いというのが正直な感想です。
  • 今回の記事ではPythonを利用するメリット、導入に向けた準備について書いて行きます。

Pythonを利用するメリット

大きく、以下のようなメリットがあると思います。

言語機能的な問題

shell よりも Python の方が言語として優れている!なんて一元的かつ宗教的なことは言いたくないですが、とはいえ、Pythonの方が優れている部分は多くあります。

オブジェクト指向的に書きやすい

正直、shell はいわゆるアプリの開発者にとっては中々とっつきづらいものかと思います。

プログラマーの君! 騙されるな! シェルスクリプトはそう書いちゃ駄目だ!! という話 - Qiita

上のリンクにも有るように、そもそもデータの扱い方がJavaPythonのようなオブジェクト指向言語とは発想が全く違います。

  • Python: コマンドの実行結果を変数(構造体)に格納→ロジック(if, for)を用いて変数を加工→次のコマンドのインプットに変数を渡す...
  • shell: コマンドの実行結果をパイプする→インプットをフィルタ(grep)、加工(awk, sed)してパイプ→インプットを用いて次のコマンドを実行(xargs)...

shell はどちらかと言うと関数型に近い発想で書く必要があり、複雑な処理を綺麗に書くには中々のスキルが必要になります。 そして大抵のケースでは"ステキな正規表現によるgrep"と"趣深いawkでのデータ加工"を組み合わせた"芸術的ワンライナー"が行われ、ジュニアなエンジニアは"勉強"が必要となっているように思います....

Pythonオブジェクト指向的に書けるので、ジュニアなアプリエンジニアでも少ないコストでキャッチアップできます。 特にデータ加工をif, forなどの馴染み深いやり方で行えるので、ロジックを追いやすくなりデバッグや修正も楽になります。

json, yaml の扱いが楽

AWSを使っていると、あらゆるデータがjson, yaml形式で記述されます。

shell でも jq, yq といったライブラリを使って処理を行うことは出来ますが、これも結局は上記のパイプを基本とした処理になるので、"芸術的ワンライナー"ルートに入りやすくなります。

Pythonでは組み込みライブラリのjson, yaml を使うことでdictとして扱うことができます。dictの変数になってしまえば、if, forといった手続き的なロジックを用いてデータの処理を出来るため、ロジックを追いやすくなります。

IDEの補助を受けられる

shell にも様々なIDEプラグインがあるものの、やはりPythonの方がメソッド補完、型補完、文法チェックなどの機能が充実しているように思います。

シニアなエンジニアであれば vim などのシンプルなエディタでもガリガリと書くことが出来ますが、ジュニアなエンジニアにとっては「このクラスにこのメソッド有ったっけ」「このメソッドの引数なんだっけ」などを覚える/調べる手間を省いてくれるIDEの補助は有り難いものです。 また、コーディングルールを揃えることも容易なので、プロジェクト全体的なコードの質を担保するという点においても、IDEの補助を受けられるというのは無視できないメリットです。

テストが書ける

正直、これが一番のメリットだと思います。

pytest などを利用した単体テストを書くことが出来るので、「プロダクションのコードはちゃんとテストされているのに、運用系スクリプトは全然テストされていない(Test In Productionな状態)」を回避することができます。

もちろん、運用系スクリプトはファイル/ネットワークのI/Oが多かったり、前提条件が様々だったりとテストが書きづらいものではありますが、

  • データの取得部分とフィルタ・加工部分のメソッドを分けてフィルタ・加工部分のテストを書く
  • 静的文法チェック(Lint)を走らせる
  • dry_run オプションを使って実環境での仮実行を行う

など、様々なやり方を使ってロジックの正常性を担保することができます。また、単体テストであればCIツールと組み合わせることも可能なので、継続的に品質を担保できるとともに、リファクタや追加改修の心理障壁を下げる効果も期待できます。

ログ、標準出力をコントロールしやすい

Python には logging というログ出力のライブラリが有るため、実行時の標準出力をコントロールしやすいというメリットが有ります。

  • 普段の実行時にはファイルにのみ出力する
  • 手動実行時には標準出力にも出力する
  • -v, --verbose オプションを用意してdebugしやすくする
  • debug のときでもライブラリの出力は抑止する(boto がとにかくうるさい...!)

などなど。こうした柔軟な出力制御を柔軟に出来るのは開発時、運用時ともに地味に効いてきます。

Python 導入に向けた準備

Python のメリットを書いてきましたが、導入時にやらなければいけないことはそれなりにあります。が、それほど難しいことは無いと思います。

  • 環境をどう揃えるか
  • Pythonへのキャッチアップ
  • コーディングルール

環境をどう揃えるか

基本的なLinuxサーバにはPython2系がデフォルトでインストールされています...がPython3系を使う方がベターです(詳しくは割愛)。 また、各種ライブラリやバージョンも開発環境、ビルド環境、本番環境で揃えたいですよね。

Pythonのバージョンはそれほど難しい問題ではありません。AWSであればcloud-initを用いてインスタンス作成時にインストールすれば良いですし、puppet/ansible などを使うのも良いでしょう。どちらにしろ、簡単に自動化出来る部分です。

ライブラリについてはpip, venv を利用するのが良いでしょう。本番環境で利用するrequirements.txt を作成し、ビルド環境や開発環境ではそれを利用したvenvを作成すればライブラリを揃えることが出来ます。これも大きな問題にはならないでしょう。

Python へのキャッチアップ

Python 自体がそれほど難しい言語では無いので、特にアプリエンジニアにとっては、キャッチアップのコストはあまり問題にはなりません。
むしろ、上で書いたように shell が中々難しい言語なので、キャッチアップへのコストという面では大きく変わらないと思います。

とはいえ、最低でも1人はエバンジェリストがいると良いですね。エバンジェリストがメンバーにトレーニングを行い、その内容をドキュメントに残して、New Comer へのトレーニングはメンバーが行う...という流れを作ることが理想的ですね。

// というか「shellは出来て当たり前」みたいな雰囲気の中、ロクにトレーニングが行われないことのほうが多いし、大きな問題では...???

コーディングルール

Python には pep8, pylint などのというコーディングルールが有るので、それを利用して適宜カスタマイズしていけば良いでしょう。

Pythonのスタイルガイドとそれを守るための各種Lint・解析ツール5種まとめ! - SideCI Blog

  • 変数名は snake_case, クラス名は CamelCase
  • 1行 80 字以内
  • 1メソッドの中のローカル変数は20個まで

などのルールが定義されています。デフォルトでは中々煩いルールも有る(変数名は3文字以上、メソッドドキュメント必須など)ので、プロジェクトのサイズやメンバーのレベルに合わせて適宜カスタマイズすることをおすすめします。 また、コーディングルールに準拠しているかどうかのテストはCIと組み合わせることも容易なので、「コーディングルールに準拠していなければmasterへのマージが出来ない」などの仕組みを作ることも出来ます。

おわりに

  • 正直、自分がshellが苦手ということも大きいのですが、難しいことを無理やりshellでやろうとして、保守性の低い"芸術的ワンライナー"が量産されることへの疑問が有ったのでこの記事を書きました
  • それなりにロジックを必要とする処理を書くのであればPythonの方が書きやすいし読みやすい→どうせなら全部Pythonで書けば良いんじゃない?というのが自分の意見です。
  • Python も shell もそれぞれ難しさはあります。が、プロジェクトとして採用するならPythonの方かと思います。
    • Pythonは「環境構築やコーディングルール」といった仕組みの部分が難しい→仕組みさえ作れば後は簡単。
    • shell は「可読性を高く保つこと、ミスなく書くこと」が難しい→個人のスキルに依存し続ける

長くなりましたが、以上です。実際に運用スクリプトを書く時のargparselogging の使い方についても記事を書きたいと思います。

【読書メモ】アナタはなぜチェックリストを使わないのか?

ミスを減らし、良い決断をするためのチェックリスト

  • 上司からオススメされた「アナタはなぜチェックリストを使わないのか? 」という本が非常に面白かったので共有します。
  • チェックリスト作りのHow to が書かれているようなタイトルですが、中身としては「ミスを減らし」「良い決断をする」道具としてチェックリストが有用であるということが書かれています。
  • 結果としてチェックリストの作成・運用を成功させるためのエッセンスを得ることが出来ます。巻末にある「チェックリストを作るためのチェックリスト」は非常に興味深いです。

何が問題なのか?

高度で複雑な問題を解決する必要のある職業・プロジェクト(e.g: 外科手術、高層ビルの建築、一流レストラン...)では以下の問題が共通して存在します。

  1. うっかりミス
  2. 問題が複雑であり、専門分化が進んでいる
  3. 常に予測不可能なことが起こる

うっかりミス

うっかりミス、というのは残念ながらよく起こります。人間の記憶力は有限かつ脆弱なので「やらなきゃいけないことが漏れる」「やり方を間違える」リスクは常に存在します。

さらに人間は怠惰なので「手順を省略する誘惑」が付きまといます。「慣れている作業だから」「急いでいるから」といった様々な理由(言い訳)で本来やるべきことを省略してしまい、失敗に終わってしまうケースも多いのです。

特に「権力を持ったキーマン(e.g: 外科手術における外科医)」がいて、その人を引き止める権限を誰も持っていない場合は非常に危険です。キーマンが誘惑に負けてしまった場合、チームとしてミスを止めることが不可能になるからです。

問題が複雑であり、専門分化が進んでいる

複雑かつ多様な問題が発生する業界では、一人の人間があらゆる分野に精通し、全ての問題を解決することは不可能になっています。そこで、分野を細かく分け各々がそれぞれの専門分野を深く理解する「スペシャリスト」になり、スペシャリスト同士が協力しながら問題を解決していく必要があります。

そこで問題になるのは「コミュニケーション」です。それぞれのスペシャリストが漏れなく、実りのあるコミュニケーションを取ることが出来なければ、ミスが起こったり、誤った決断をしてしまったりします。

常に予測不可能なことが起こる

高度な問題に取り組む時に「前もって予定していた通りに全てが上手くいく」ことは多くない、というかむしろ極めて稀です。大抵は実行している中で想定外の事象が発生し、それに対する対処が必要となります。

想定外の事象に対処するにはコミュニケーションが非常に重要になってきます。限られた時間の中で対処法を模索するには、メンバーのコミュニケーションが円滑に行われる必要があります。さらに言えば「円滑にコミュニケーションを行える」ための土台が「事前に」作られている必要があるのです。

チェックリストの効果

ミスを減らす

あらかじめ必要な手順を記したチェックリストを利用することでミスを減らすことが出来ます。そして、それ以上に重要なことはチェックリストを利用して「権力を再配分できる」点です。

「チェックする人」を「権力を持ったキーマン」と分けることにより、手順を省略する誘惑にチームとして戦うことが出来るからです。あらかじめ「チェックする人の権限」を保証することにより、キーマンによるミスを止めることが出来るのです。 本の中では「手術室の看護師」がメスの上にカバーを置き、チェックリストが埋まってからカバーを取り外し、外科医が手術を開始するというルールを保証するという例が挙げられています。

コミュニケーションを円滑にする

「チェックリストを埋める」という儀式自体がまずコミュニケーションになるので、その後のコミュニケーションを円滑にする役割を果たします。特に、「予測不可能なことが起こることが予測される(Known Unknown)」状況では事前にチェックリストを読み合わせてコミュニケーションを円滑にしておくことで、実際に予測不可能なことが起きた時のチームとしての対処能力を高めることが出来ます。

また「コミュニケーションを取る」という項目をチェックリストに入れることでコミュニケーションが漏れることを防ぐことが出来ます。 高層ビルの建築では「各工程における手順のチェックリスト(ミスを減らすためのチェックリスト)」とは別に「何時までにXという工程について誰と誰が議論する」という「コミュニケーションのためのチェックリスト」が作成され、コミュニケーションが漏れたり遅れたりすることを防いでいます。

チェックリスト作成時に気をつけること

いつ使うかを定義する

チェックリストには「行動、のち読む」「読む、のち行動」の2種類があり、状況によって使い分けます。本の中では「一時停止点(Pause Point)」という単語が使われていますが、予め一時停止点を共有しチェックリストの運用が確実に行われることを保証する必要が有ります。

項目を増やしすぎない

項目が多くなるほど、作業効率を下げてしまうとともに「省略する誘惑」が湧きやすくなります。具体的には「一時停止点あたり9項目まで」と定められています。本当に必要な項目に絞ってチェックリストを作成することで、作業を過剰に止めることなくミスを防ぐことが出来ます。

実運用の中で検証・更新する

チェックリストを作成したら実運用の中でテストをして、「項目に過不足がないか」「タイミングは適切か」「所要時間は適切か」などを検証します。また、時間とともにチェックすべき内容は変化するので、定期的にテストを行って項目を更新していくことも大切です。

// 当たり前といえば当たり前ですが、実際には行われていないケースが多いように思います。 // 現場にそぐわない膨大な項目のチェックリストが作成され、面倒になって利用されなくなって腐る...みたいな。

おわりに

IT業界の話は出てきませんでしたが応用の効く内容が多いように感じました。本番作業・障害対応は外科手術や旅客機の運行から学ぶところは多いですし、プロジェクト推進については高層ビルの建築と非常に似た問題(クリティカルパスが多い)を抱えています。

繰り返しになりますが、チェックリスト作りのHow to ではなく、「ミスを減らし」「良い決断をする」道具としてチェックリストが有用であることが書かれている本です。

エンジニアには単純な技術力だけでなく「ミスを減らす」「コミュニケーションを確実に取る」といったソフトなスキルも必要で、後者の部分について考える良いキッカケになりました。若手エンジニアには非常におすすめの一冊です。

【AWS】【Python】EC2インスタンスをCPU使用率の高い順に表示するLambdaスクリプト

EC2インスタンスをCPU使用率が高い順に表示する

  • 性能テストなどを行っていて、ボトルネックとなっているインスタンスを探したい
  • cloudwatchの結果をクエリして「当該時間帯にCPU使用率が高かったインスタンス」を探す
  • tabulateを利用して良い感じに表示する

IAMロール、ポリシー

以下の2つのbuilt-inポリシーをアタッチしたロールを作成し、Lambdaにアタッチします。

  • AWSLambdaExecute
  • AmazonEC2ReadOnlyAccess

tabulate を使えるようにする

表示にはtabulateを使いたいのですが、Lambdaではpipを利用することが出来ません。以下の記事に従って、tabulateを利用できるようにします。

【AWS】Lambdaでpipしたいと思ったときにすべきこと - Qiita

テストイベントの設定

引数には調査対象の環境(env)と、時間帯(timebox)を渡します。timeboxにはISOフォーマットを利用し、timezoneを設定することも可能です。以下のサンプルではJST(+09:00)でtimeboxを指定しています。

{
  "env": "test1",
  "timebox":{
      "start": "2018-02-23T12:30:00+09:00",
      "end":"2018-02-23T13:00:00+09:00"
  }
}

コード

Python 3.6 で書きます。

import datetime

import boto3
import dateutil.parser as parser
from tabulate import tabulate


def lambda_handler(event, context):
    ec2_client = boto3.client('ec2')
    full_info = ec2_client.describe_instances(
        Filters=[
            {
                'Name': 'tag:env',
                'Values': [event.get('env')]
            }
        ]
    )
    instances = []
    for r in full_info['Reservations']:
        for i in r['Instances']:
            instances.append(i)

    cw_client = boto3.client('cloudwatch')
    timebox = event.get('timebox', {})
    if not timebox:
        now = datetime.datetime.utcnow()
        timebox['start'] = now - datetime.timedelta(seconds=600)
        timebox['end'] = now
    else:
        timebox['start'] = parser.parse(timebox['start'])
        timebox['end'] = parser.parse(timebox['end'])

    ret = []
    for ins in instances:
        metrics = cw_client.get_metric_statistics(
            Namespace='AWS/EC2',
            MetricName='CPUUtilization',
            Dimensions=[{'Name': 'InstanceId', 'Value': ins['InstanceId']}],
            StartTime=timebox['start'],
            EndTime=timebox['end'],
            Period=60,
            Statistics=['Average']
        )
        # get latest data
        latest = None
        for d in metrics['Datapoints']:
            if not latest:
                latest = d
            else:
                if latest['Timestamp'] < d['Timestamp']:
                    latest = d
        if not latest:
            continue
        ret.append({
            'name': [t['Value'] for t in ins['Tags'] if t['Key'] == 'Name'][0],
            'instance_id': ins.get('InstanceId', ''),
            'private_ip': ins.get('PrivateIpAddress', ''),
            'CPU_usage': latest['Average'],
            'timestamp': latest['Timestamp'].isoformat()
        })
    ret.sort(key=lambda d: d['CPU_usage'], reverse=True)

    print(tabulate(ret, headers="keys"))
    return ret

補足と解説

dateutil.parser を利用してISOフォーマットの時刻文字列をdatetimeに変換できます。

    timebox = event.get('timebox', {})
    if not timebox:
        now = datetime.datetime.utcnow()
        timebox['start'] = now - datetime.timedelta(seconds=600)
        timebox['end'] = now
    else:
        timebox['start'] = parser.parse(timebox['start'])
        timebox['end'] = parser.parse(timebox['end'])

Dimensions で InstanceId 、StartTimeEndTimeで時間帯を指定してcloudwatch のデータをクエリします。

    for ins in instances:
        metrics = cw_client.get_metric_statistics(
            Namespace='AWS/EC2',
            MetricName='CPUUtilization',
            Dimensions=[{'Name': 'InstanceId', 'Value': ins['InstanceId']}],
            StartTime=timebox['start'],
            EndTime=timebox['end'],
            Period=60,
            Statistics=['Average']
        )

CPU使用率が高い順にデータをソートして、tabulate で綺麗にprintします。headerにはデータのkeyを指定します。

    ret.sort(key=lambda d: d['CPU_usage'], reverse=True)

    print(tabulate(ret, headers="keys"))

出力結果

tabulateを利用することで、綺麗に表示されます。(サーバー名、instance_idなどはマスクしました)

name       instance_id          private_ip      CPU_usage  timestamp
---------  -------------------  ------------  -----------  -------------------------
server1    i-1a1a1a1a1a1a1a1a1  111.1.1.11      13.234     2018-02-23T04:28:00+00:00
server2    i-2b2b2b2b2b2b2b2b2  111.1.2.22       7.854     2018-02-23T04:27:00+00:00
server3    i-3c3c3c3c3c3c3c3c3  111.1.3.33       5.342     2018-02-23T04:28:00+00:00
server4    i-4d4d4d4d4d4d4d4d4  111.1.4.44       3.47458   2018-02-23T04:29:00+00:00
server5    i-5e5e5e5e5e5e5e5e5  111.1.5.55       1.208     2018-02-23T04:28:00+00:00
server6    i-6f6f6f6f6f6f6f6f6  111.1.6.66       0.95      2018-02-23T04:28:00+00:00
server7    i-7g7g7g7g7g7g7g7g7  111.1.7.77       0.758     2018-02-23T04:27:00+00:00
server8    i-8h8h8h8h8h8h8h8h8  111.1.8.88       0.738     2018-02-23T04:27:00+00:00
server9    i-9i9i9i9i9i9i9i9i9  111.1.9.99       0.730958  2018-02-23T04:29:00+00:00
server10   i-10j10j10j10j10j10  111.1.10.110     0.6       2018-02-23T04:29:00+00:00
server11   i-11k11k11k11k11k11  111.1.11.121     0.4       2018-02-23T04:29:00+00:00
server12   i-12l12l12l12l12l12  111.1.12.132     0.38337   2018-02-23T04:26:00+00:00
server13   i-13m13m13m13m13m13  111.1.13.143     0.36428   2018-02-23T04:29:00+00:00
server14   i-14n14n14n14n14n14  111.1.14.154     0.35      2018-02-23T04:26:00+00:00
server15   i-15o15o15o15o15o15  111.1.15.165     0.324     2018-02-23T04:29:00+00:00

おわりに

  • 性能テストや障害時に「ボトルネックとなっているインスタンスを特定 -> private ip を見てssh」といった流れをスムーズに出来るようになりました。

  • 使用するメトリクスを変えることで、ネットワーク使用率やディスクI/Oなどを見ることも出来るので、中々便利なものだと思います。

  • 3rd Partyのライブラリの利用方法や、データ操作/表示のやり方を学べたので個人的にも良い勉強になりました。

【AWS】【Python】Lambdaを利用して他のAWSアカウントのEC2インスタンスを再起動する

Lambdaを利用した他アカウントの操作

やったことの流れは以下の通り。

  • IAMロール/ポリシーの設定、信頼関係の編集
  • 他のアカウントのIAMロールにSTSしてEC2クライアントを取得
  • tagの値で対象のインスタンスを抽出し、再起動

IAMロール/ポリシーの設定

Lambdaにアタッチするロールと、他のアカウント(操作対象のインスタンスを保持しているアカウント)側のIAMロールの2つを編集する必要があります。

LambdaのIAMロール/ポリシー

今回のケースで必要な権限(ポリシー)は以下の2つです。

  • AWSLambdaExecute (built-in):
    Lambda実行のための基本的な権限です。具体的には、cloudwatch logs へのフルアクセス、S3へのPut/Get

  • 他のアカウントのIAMロールへのSTS権限:
    他のアカウントのIAMロールへの一時的な認証情報を取得(STS)してインスタンスのコントロールを行うために、Lambda側には「他のアカウントのIAMロールへのSTSを行う権限」を与える必要が有ります。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "sts:AssumeRole"
            ],
            "Resource": [
                "arn:aws:iam::999999999999:role/OtherAccountRole"
            ]
        }
    ]
}

他のアカウント側のIAMロール/ポリシー

他のアカウント(操作対象のインスタンスを保持しているアカウント)では以下のポリシーを持ったIAMロールを作成します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeInstances",
                "ec2:RebootInstances"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

また、このIAMロールの信頼済みエンティティに、上記のLambdaロールを追加する必要が有ります。IAM画面から信頼関係の編集を行い、以下のように編集します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::111111111111:role/RebootLambdaRole"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Lambda のコード、テストイベント

Python3.6で書きます。

ソースコード

'''
AWS Lambda script for reboot instances.
This scripts need STS for IAM Role on Another Account.
'''

import boto3


def lambda_handler(event, context):
    sts_client = boto3.client('sts')
    sts_res = sts_client.assume_role(
        RoleArn=event['sts_target'],
        RoleSessionName='RebootOtherAccount'
    )
    cred = sts_res['Credentials']

    ec2_client = boto3.client(
        'ec2',
        aws_access_key_id=cred['AccessKeyId'],
        aws_secret_access_key=cred['SecretAccessKey'],
        aws_session_token=cred['SessionToken'],
    )

    full_info = ec2_client.describe_instances(
        Filters=[
            {
                'Name': 'tag:need_reboot',
                'Values': ['true']
            }
        ]
    )
    
    instance_ids = []
    for r in full_info['Reservations']:
        for i in r['Instances']:
            instance_ids.append(i['InstanceId'])

    return ec2_client.reboot_instances(
        DryRun=event.get('dry_run', True),
        InstanceIds=instance_ids,
    )

テストイベント

{
  "dry_run": true,
  "sts_target": "arn:aws:iam::999999999999:role/OtherAccountRole"
}

解説と補足

  • テストイベントから取得した他アカウントのIAMロールへのSTSを行い、取得した認証情報を利用してEC2クライアントを作成します。STSを利用して他アカウントのリソースにアクセスする場合は、clientの初期化時に以下のようにする必要があります。
    cred = sts_res['Credentials']

    ec2_client = boto3.client(
        'ec2',
        aws_access_key_id=cred['AccessKeyId'],
        aws_secret_access_key=cred['SecretAccessKey'],
        aws_session_token=cred['SessionToken'],
    )
  • 対象となるインスタンスをtagの値でフィルターします。ここではneed_reboot というタグにtrueと書いてあるインスタンスを抽出しています。
full_info = ec2_client.describe_instances(
        Filters=[
            {
                'Name': 'tag:need_reboot',
                'Values': ['true']
            }
        ]
    )
  • 細かいですが、DryRunフラグはちゃんと付けておきたいですね。
    return ec2_client.reboot_instances(
        DryRun=event.get('dry_run', True),
        InstanceIds=instance_ids,
    )

終わりに

以上です。IAMロールの設定のところで結構ハマりましたが、IAMロール/ポリシー、信頼関係などについて学ぶことが出来ました。 業務ではこんな感じのLambdaを書くことが増えたので、ブログの方にもメモを残していきたいと思います。

【GitLab】【Python】GitLabにユーザーを一括で登録、グループ参加

GitLab API を使ってユーザー一括登録

必要なもの

  • Python3
  • GitLabのAdminユーザーのprivate access token

python-gitlab のセットアップ

  • pip を利用してpython-gitlab のインストール
pip install python-gitlab 
  • 以下のような設定ファイル(python-gitlab.cfg)を用意
[default]
url = https://gitlab.somewhere.com/
private_token = AdminAccessToken
timeout = 5
ssl_verify = True
api_version = 4

ソースコード

#!/usr/bin/env python3

import argparse
import csv
import gitlab


def add_user(api, csvfile):
    with open(csvfile, 'r') as cf:
        csvreader = csv.reader(cf, delimiter=',', quotechar='|')
        for row in csvreader:
            params = {}
            params['email'] = row[0]
            params['username'] = row[1]
            params['name'] = row[2]
            params['reset_password'] = True
            user = api.users.create(params)
            print('User:{} has created'.format(user))


def add_user_to_group(api, csvfile):
    all_group = api.groups.list()
    with open(csvfile, 'r') as cf:
        csvreader = csv.reader(cf, delimiter=',', quotechar='|')
        for row in csvreader:
            user = api.users.list(username=row[0])[0]
            for i in range(1, len(row)):
                group = [g for g in all_group if g.full_path == row[i]][0]
                params = {}
                params['user_id'] = user.id
                params['access_level'] = gitlab.DEVELOPER_ACCESS
                group.members.create(params)
                print('User:{0} has joined in {1}'.format(user.name, group.name))


if __name__ == "__main__":
    parser=argparse.ArgumentParser()
    parser.add_argument('--csvfile')
    parser.add_argument('--group', action='store_true')
    args=parser.parse_args()

    api=gitlab.Gitlab.from_config(config_files=['./python-gitlab.cfg'])
    gl.session.proxies = {
          'http': 'http://yourproxy.com:9090',
          'https': 'https://yourproxy.com:8443'
    }

    if args.group:
        add_user_to_group(api=api, csvfile=args.csvfile)
    else:
        add_user(api=api, csvfile=args.csvfile)

補足と解説

  • 設定ファイルを読み込んでGitLabのAPIを初期化して、そのapiを利用してユーザー登録/グループ参加 を行います
  • Proxy環境で使いたいときは、apiにproxy設定をいれます
    api=gitlab.Gitlab.from_config(config_files=['./python-gitlab.cfg'])
    gl.session.proxies = {
          'http': 'http://yourproxy.com:9090',
          'https': 'https://yourproxy.com:8443'
    }
  • ユーザー登録時にパスワードを設定させるメールを送るようにしています
            params['reset_password'] = True

決まった初期パスワードを設定したいときは以下のようにします

            params['password'] = 'secret_p@ssw0rd'
  • Group に追加するときはDeveloper として登録されます
                params['access_level'] = gitlab.DEVELOPER_ACCESS

使い方

ユーザー登録

  • 以下のようなcsvファイルを用意する。左からメールアドレス、ログインID、表示名です。
tom.brady@patriots.com,tom.brady,Tom Brady
david.andrews@patriots.com,david.andrews,David Andrews
patrick.chung@patriots.com,patrick.chung,Patrick Chung
./add-user --csvfile filename.csv

ユーザーのグループ参加

  • 以下のようなcsvファイルを用意する。一番左にユーザーのログインID、右に登録したいグループをフルパスで並べます。
tom.brady,offence
david.andrews,offence,offence/lines
patrick.chung,defence
./add-user --group --csvfile filename.csv

終わりに

  • やっつけでつくったものですが、それなりに綺麗にかけたかなと。
  • グループ登録時の権限指定はファイルに書くようにしたいですね。
  • コードやサンプルファイルはGitHubに置きました。よろしければご利用ください。

github.com

業務ではPythonを使うことが多くなってきました。こういうことをパッと出来ると成長を感じますね。 AWSのLambdaなども書く機会が増えてきたので、Python繋がりでそういう記事も書きたいです。

2018年にエンジニアとして学びたいこと

新年の抱負的なことを書いてみる

  • 皆様あけましておめでとうございます。
  • 新年ということで今年学びたいと思っていることを書きます。
  • ざっくりな方向性としては以下です。

英語

現在のプロジェクトが多国籍チームということもあり、英語でコミュニケーションを取りたいことが多々あります。
一方で、直属の上司が日本語ネイティブ(のイギリス人!)ということもあり、あまり危機感を持って取り組めていないのも現状です。
もちろん英語を使うことは増えましたが、「英語力が上がった」というよりは「英語混じりでのコミュニケーション能力が上がった」というほうが正しい表現です。

また、直属の上司に言われたことですが、

  • そもそも「まともなエンジニア」を見つけるのがまず大変(これは日本に限らない話だそうです)
  • そして日本で「英語を話せる人」を見つけるのもなかなか大変
  • なので「英語を話せるまともなエンジニア」は外資系企業が血眼になって探している

ので、英語を話せるだけで転職市場での価値が大きく上がるとのことでした。

仕事を上手く進めるためだけでなく、自分の今後のキャリアを広げるためにも、今年こそちゃんと勉強したいと思います。 具体的には、TOEFL のスコアを上げていきたいと思います。Writing や Speaking もあるので、アウトプット能力を高めることにも繋がっていくと考えています。

コンピュータサイエンス、インフラの基礎

現在のプロジェクトではAWSを利用したインフラ/運用設計を行っています。
文系学部を卒業しアプリのエンジニアをやってきた自分としては、インフラやネットワークの知識が足りないと感じることが増えています。

また先輩からも「コンピュータサイエンスの基礎を知ってるだけでも問題を切り分けるセンスが大きく上がる」とも言われています。
今後、開発や運用の中でもう1段シニアなロールを担っていくにあたり、コンピュータサイエンスの基礎を学んでいくことが重要だと考えています。

具体的には、基本情報技術者応用情報技術者の資格を取ろうと思います。
資格が重要視される業界でもないですが、知識レベルのベンチマークとしては良いという声をよく聞くので、春に基本情報、秋には応用情報を取ることを目標にしたいと思います。

その他、各種技術

上に書いたような英語や基礎知識だけでなく、個別の技術にもキャッチアップしていきたいと思っています。
今時点で考えているのは、AWSとフロントエンドの2つです。

AWS

現在のプロジェクトでメインに使う分野なので、もっと習熟したいと思っています。
仕事ではシニアな人が大きな部分の設計をしてくれたりCloudFormationをゴリゴリ書いてくれたりしているのですが、自分でもそういったことをできるような知識や能力をつけていきたいです。
アプリのエンジニアをやっていくにしても、クラウドの知識は必要だと考えているので、これを機会に勉強したいと思います。

フロントエンド

フロントエンドも仕事で扱ってきましたが、これもシニアな人がベースとなる部分を作ってくれた上で開発を行ってきただけなので、もう少し深く理解をしたいと考えています。
ReactやVueといったFrameworkをいくつか理解すれば、日進月歩のフロントエンド界にキャッチアップするための足がかりを作れるのではと期待しているので、自習ベースで頑張っていきたいと思います。

おわりに

去年の夏頃にブログを初めて、やはりアウトプットの習慣がある方が良いということは実感しました。
11月頃を境に公私共に忙しくなり、更新回数が減ってしまったのは反省点です。
2018年は仕事やプライベートが忙しい中でもブログを書くという習慣を作っていきます。
勉強したいことも山ほど有る中で、ブログにアウトプットしながら精進していきたいと思います。

【Python】key と value が連なった List を Dict に変換する

Python の tips を備忘代わりに

  • プロジェクトが変わりまして、Pythonを触ることが増えました
  • 掲題の件で悩んだので、パフォーマンスの検証も含めて post しておきます

やりたいこと

こういう List を

hoge_list = ['key1',  'value1',  'key2',  'value2',  'key3',  'value3',,,,]

こういう Dict にしたい

hoge_dict = {'key1':  'value1',  'key2':  'value2',  'key3':  'value3',,,,}

これだけ。

調べて出てきたやり方

困ったときは、stackoverflow ですね。 stackoverflow.com

method1

hoge_dict = dict(zip(hoge_list[0::2], hoge_list[1::2]))

綺麗なワンライナーですね。奇数番目の要素を取り出した List と、偶数番目の要素を取り出した List を zip するというやり方です。

method2

hoge_dict = { hoge_list[i]: hoge_list[i+1] for i in range(0, len(hoge_list), 2) }

dict comprehension を使って、値を順々に key: value に入れていくというやり方です。

method3 (Python3 のみ)

i = iter(hoge_list)
hoge_dict = dict(zip(i, i))

Python3 からは zip が遅延評価になっているので、このやり方で iterator から順々に要素を取り出して zip することができます。

パフォーマンス検証

検証用コード

1万個の key と value のセットを並べたリストを、それぞれのやり方で1万回ずつ回して、実行時間を取ります。

from datetime import datetime


def method1(hoge_list):
    return dict(zip(hoge_list[0::2], hoge_list[1::2]))


def method2(hoge_list):
    return {hoge_list[i]: hoge_list[i + 1] for i in range(0, len(hoge_list), 2)}


def method3(hoge_list):
    i = iter(hoge_list)
    return dict(zip(i, i))


if __name__ == '__main__':
    hoge_list = []
    for i in range(0, 10000):
        hoge_list.append('key{}'.format(i))
        hoge_list.append('value{}'.format(i))

    start = datetime.now()
    for i in range(0, 10000):
        method1(hoge_list)
    time1 = datetime.now() - start

    start = datetime.now()
    for i in range(0, 10000):
        method2(hoge_list)
    time2 = datetime.now() - start

    start = datetime.now()
    for i in range(0, 10000):
        method3(hoge_list)
    time3 = datetime.now() - start

    print('time1: {}'.format(time1))
    print('time2: {}'.format(time2))
    print('time3: {}'.format(time3))

結果

time1: 0:00:13.092214
time2: 0:00:24.749644
time3: 0:00:09.524495

method3 < method1 <<< method2 という結果になりました。

  • method3 が method1 より速いのは納得です。「1回 key のリストと value のリストを作ってから zip する」のと、「順々に値を取り出してzipする」のでは、後者のほうがメモリコピーの回数が少ないので速くなりそうですね。
  • method2 がこんなに遅いのがちょっとビックリでした。調べたところ、Dict Comprehension は内部的に update() を呼んでいるようなので、「1回ずつupdate()を行う」のと、「zipでまとめてからdictに変換する」のでは確かに後者のほうが速そうですね。

結論

method3を使いましょう。

i = iter(hoge_list)
hoge_dict = dict(zip(i, i))

参考ページ

  • 各種 comprehension の内部的な挙動(英語)

Stupid Python Ideas: How do comprehensions work?

  • zip の内部的な挙動 (英語)

How does Python's zip function work internally? - Quora