nozayasu-memo

プログラムのメモ

pyenvとpipenvによる環境構築

僕は業務上、開発と環境構築をrubypythonのどちらもやります。

そのためrubyだとこうでpythonだとこうと、頭の切り替えがよく起きるのです。

rubypythonの行き来する上で、操作感覚が似ていることはとても助かります。

pyenvのおかげで、rbenvと似た感覚でversion managementをできるようになりました。

一方アプリケーション固有の依存package managementにおいては、bundlerと似た感覚で操作できるものに出会えていませんでした。

今回はpipenvが僕の悩みをある程度解決してくれそうだったので、試してみたメモです。

pyenvによるversion management

Install & setup pyenv

 $ git clone git://github.com/yyuu/pyenv.git ~/.pyenv
 $ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile
 $ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile
 $ echo 'eval "$(pyenv init -)"' >> ~/.bash_profile
 $ source ~/.bash_profile

Install & set global version

global設定にするかは適宜変更してください

 $ pyenv install -v 任意のversion
 $ pyenv global 任意のversion

pipenvによるpackage management

Install & setup pipenv

 $ pip install pipenv
 $ pipenv install
Creating a Pipfile for this project...
Creating a virtualenv for this project...
.... 中略(~/.local/share/virtualenvs以下にvirtualenv環境を作ります)....

PipfileとPipfile.lockが増えている

 $ ls
Pipfile     Pipfile.lock

仮想環境での動作確認

動作確認様にboto3をimportするだけのsample.py作成

sample.py

 1 # -*- coding: utf-8 -*-
 2 
 3 import boto3

Pipfile内の[packages]にboto3を追加

Pipfile

  1 [[source]]
  2 url = "https://pypi.python.org/simple"
  3 verify_ssl = true
  4
  5 [packages]
  6 boto3 = "*"

再度pipenv install

 $ pipenv install
No package provided, installing all dependencies.
Pipfile found at /path/to/Pipfile. Considering this to be the project home.
Pipfile.lock out of date, updating...
Locking [dev-packages] dependencies...
Locking [packages] dependencies...
Updated Pipfile.lock!
Installing dependencies from Pipfile.lock...
[================================] 7/7 - 00:00:03
....

globalなpython使ってsample.py実行すると当然ImportError

 $ python sample.py 
Traceback (most recent call last):
  File "sample.py", line 1, in <module>
    import boto3
ImportError: No module named 'boto3'

pipenv環境下なpython使ってsample.py実行するとimportできるのでエラーなし

 $ pipenv run python sample.py

所感

rbenv -> pyenv、bundler -> pipenvと割と近い感覚で使えるようになったのは好感触

envによるpackageを切り分けていくとこを直感的にできるかをもう少し使ってみて確認したいところ

参考

github.com

github.com

qiita.com

utgwkk.hateblo.jp

TerraformとTerraformingを運用中のAWSリソースで試してみる

すでに運用中のELB/EC2でのAppサーバ構成と、同等の構成な別環境がほしくなった。

地道に作るよりどうせなら、状態管理とdry runできるTerraformを試そうと思ったのでやってみることにした。

TerraformとTerraformingをinstall

brew install terraform
gem install terraforming

Terraform用のAWS設定を作成

aws.tf として接続設定を用意しておく

provider "aws" {
  region     = "ap-northeast-1"
  access_key = "access_key"
  secret_key = "secret_key"
}

既存のリソースの状態をローカルに再現する

Terraformはステートファイル(terraform.tfstate)という仕組みを使用して、リソースの状態管理をしているのでそれをローカルに再現する。

Terraformingを利用してterraform.tfstateの作成、リソースのtfファイルを作成した。

terraform.tfstate作成

terraforming ec2 --tfstate > terraform.tfstate
terraforming eip --tfstate --merge=terraform.tfstate --overwrite
terraforming elb --tfstate --merge=terraform.tfstate --overwrite

–overwrite オプションをなくせば書き込む前にmerge後の内容を確認もできる

既存EC2関連をひとまとめにしたtfファイル作成

terraforming ec2 > ec2.tf
terraforming eip >> ec2.tf
terraforming elb >> ec2.tf

新規追加するリソースのtfファイル作成

new_ec2.tf

resource "aws_instance" "web" {
    ami                         = "ami-xxxxxx" # 任意のAMI ID
    ebs_optimized               = false
    instance_type               = "t2.small"
    monitoring                  = false
    key_name                    = "nozayasu"

    root_block_device {
        volume_type           = "gp2"
        volume_size           = 8
        delete_on_termination = true
    }

    tags {
        "Name" = "web"
    }
}

resource "aws_eip" "web" {
    instance = "${aws_instance.web.id}"
    vpc      = true
}

resource "aws_elb" "web" {
    name                        = "web"
    subnets                     = ["subnet-xxxxxx", "subnet-xxxxxxx"]
    security_groups             = ["sg-xxxxxx"]
    instances                   = ["${aws_instance.web.id}"]
    cross_zone_load_balancing   = true
    idle_timeout                = 60
    connection_draining         = true
    connection_draining_timeout = 300
    internal                    = false

    listener {
        instance_port      = 80
        instance_protocol  = "http"
        lb_port            = 80
        lb_protocol        = "http"
        ssl_certificate_id = ""
    }

    health_check {
        healthy_threshold   = 10
        unhealthy_threshold = 2
        interval            = 30
        target              = "HTTP:80/index.html"
        timeout             = 5
    }
}

${aws_instance.web.id} で最初に定義したaws_instance.webのinstance_idが取得できる、依存関係が必要なEIPやELBにはそれを利用して一括設定。

変更確認

terraform plan

変更箇所として、 - 表示が出ていた場合は削除扱いなので要確認

状態適応

terraform apply

所感

dry runで差分を確認しながら作業できる安心感があり幸せになった。

参考

AWSリソース(EC2/RDS/ElasticCache)を別アカウントに移行

前提条件

移行元AWSアカウント(A)と、移行先AWSアカウント(B)に接続できる状態であること

EC2の移行

  1. Aで移行したいインスタンスのAMIを作成
  2. A -> B でアカウント間プライベート共有
  3. B側でプライベート共有AMIからインスタンス立ち上げる

※ AMIは同一リージョンでないと共有できないのだけ注意が必要だった

参考

RDSの移行

  1. Aで移行したいDBのスナップショットを作成
  2. A -> B でアカウント間プライベート共有
  3. B側でプライベート共有スナップショットからDB復元

※ パラメータグループが移行はない。目視diffは厳しいのでCLIでごにょごにょ移行した

参考

ElasticCacheの移行

  1. Aで移行したいElastiCacheのバックアップを作成
  2. Aでエクスポート用のS3 bucketを作成
  3. Aでエクスポート用のS3 bucketにElastiCacheのバックアップをエクスポート
  4. aws s3 cp --profile A s3://bucketname/cache.rdb --profile B s3://bucketname/ profile使いわけて別アカウントのS3にコピー
  5. BでコピーしたElastiCacheのバックアップからElastiCache復元

※ S3のread権限与えるのを忘れて、いざ復元するときにrestore-failedになりなんで?ってなった

参考

その他

セキュリティグループの移行

前記事で書いたやつでできる

nozayasu-memo.hatenablog.com

codenize.tools piculetでAWS別アカウントにsecurity groupを移行する

事前準備

アカウントA(既存)の接続情報とアカウントB(移行先)の接続情報を用意

> cat ~/.aws/credentials 
[A]
aws_access_key_id = aws_access_key_id_for_A
aws_secret_access_key = aws_secret_access_key_for_A

[B]
aws_access_key_id = aws_access_key_id_for_B
aws_secret_access_key = aws_secret_access_key_for_B

piculet install

> gem install piculet

移行作業

piculet -e -p A -r region -o Groupfile
piculet -a -p B -r region -f Groupfile

–dry-run オプションがあるので -a 実行する時には必ず差分の確認をすると事故がない
-n オプションで、グループ名に設定してるものでフィルタリングしながらの部分適応も可能

詳しくはREADMEにて確認

codenize-tools/piculet: Piculet is a tool to manage EC2 Security Group. It defines the state of EC2 Security Group using DSL, and updates EC2 Security Group according to DSL.

所感

すでに運用済みの設定を一括でとってきて、それをコードとして運用するための作業をまるっと簡単にしてくれるのが素敵すぎた。

more_itertools chunkedの処理を読む

実際のコードは以下

def chunked(iterable, n):
    """Break *iterable* into lists of length *n*:
        >>> list(chunked([1, 2, 3, 4, 5, 6], 3))
        [[1, 2, 3], [4, 5, 6]]
    If the length of *iterable* is not evenly divisible by *n*, the last
    returned list will be shorter:
        >>> list(chunked([1, 2, 3, 4, 5, 6, 7, 8], 3))
        [[1, 2, 3], [4, 5, 6], [7, 8]]
    To use a fill-in value instead, see the :func:`grouper` recipe.
    :func:`chunked` is useful for splitting up a computation on a large number
    of keys into batches, to be pickled and sent off to worker processes. One
    example is operations on rows in MySQL, which does not implement
    server-side cursors properly and would otherwise load the entire dataset
    into RAM on the client.
    """
    return iter(partial(take, n, iter(iterable)), [])

see also
https://github.com/erikrose/more-itertools/blob/17bd5449e9872e27053e408af5832715821add36/more_itertools/more.py#L69-L90

要素を分解する

実際の処理はここだけ

return iter(partial(take, n, iter(iterable)), [])

iter, partial, takeという処理を組み合わせて実現してる、それぞれみてみる。

iter

https://docs.python.org/3/library/functions.html#iter

イテレータオブジェクトを返してくれる 引数の内容によって振る舞いがかわってくる

>>> iter(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object is not iterable
>>> iter("こんにちは")
<str_iterator object at 0x10dbf7c18>
>>> iter("こんにちは", "")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: iter(v, w): v must be callable
>>> iter(int, 0)
<callable_iterator object at 0x10dbf7cc0>

1つだけ引数を渡す場合、イテレートできないようなオブジェクトだとTypeErrorになる
2つ引数を渡す場合、callできないようなオブジェクトだとTypeErrorになる、callした結果が2つ目の引数と一致するまでcallし続ける

partial

https://docs.python.org/3/library/functools.html#functools.partial

任意のfunctionにおいて、特定引数のdefault値が設定された新たなfunctionを生成してくれる
次みたいなのができるになる

>>> from functools import partial
>>> def say(word):
...     print(word)
... 
>>> say("こんにちは")
こんにちは
>>> new_say = partial(say, word="こんばんは")
>>> new_say()
こんばんは
>>> new_say(word="おやすみ")
おやすみ

take

https://github.com/erikrose/more-itertools/blob/17bd5449e9872e27053e408af5832715821add36/more_itertools/recipes.py#L80-L92

イテレータオブジェクトの頭からn回処理した結果をリストとして返してくれる

def take(n, iterable):
    """Return first *n* items of the iterable as a list.
        >>> take(3, range(10))
        [0, 1, 2]
        >>> take(5, range(3))
        [0, 1, 2]
    Effectively a short replacement for ``next`` based iterator consumption
    when you want more than one item, but less than the whole iterator.
    """
    return list(islice(iterable, n))

ここでつかってるisliceはイテレータオブジェクトの頭からn回処理した結果を返す

islice: https://docs.python.org/3.6/library/itertools.html#itertools.islice

list()しない場合とでみるとわかりやすいかも

>>> from itertools import islice
>>> word = "こんにちは"
>>> islice(word, 3)
<itertools.islice object at 0x10dbf8728>
>>> list(islice(word, 3))
['こ', 'ん', 'に']
>>> 

どんなことやらせてるのか

実際の処理の部分を再掲

def chunked(iterable, n):
    return iter(partial(take, n, iter(iterable)), [])
  • iter(iterable)イテレータオブジェクトを生成してる
  • partial(take, n, iter(iterable))でtakeに対してnとiter(iterable)が与えられた状態のtake functionを生成してる
  • iter(partial(take, n, iter(iterable)), [])で新たに生成したtake functionの返りが[]となるまで繰り返すようなイテレータオブジェクトを生成している

という感じのことだということがわかった。

more_itertoolsでリストを小分けにする

大きめIDのリストを一定量の塊に分解しながら、DBに問い合わせをして後続の処理とかをしたいケースがあった

そんな時にmore_itertoolsを使ったら読みやすくなったという話

結果から、more_itertoolsのchunkedを使うと次のような書き方ができる

>>> from more_itertools import chunked
>>> ids = [0, 1, 2, 3, 4, 5, 6, 7, 8]
>>> list(chunked(ids, 3))
[[0, 1, 2], [3, 4, 5], [6, 7, 8]]

chunkedってメソッドに元となるリストと塊の量を渡す、するとその量ごとのリストに分解してくれる

なにやるのかが明示的になったので、読みやすくなった

see also
erikrose/more-itertools: More routines for operating on iterables, beyond itertools More Itertools — more-itertools 3.2.0 documentation

これ見つける前に考えたこと

最初に考えたのは、内包表記とスライスで小分けにする方法

>>> ids = [0, 1, 2, 3, 4, 5, 6, 7, 8]
>>> [ids[i:i+3] for i in range(0, len(ids), 3)]
[[0, 1, 2], [3, 4, 5], [6, 7, 8]]

rangeの3つ目の引数、stepの内容をしらないと少し見慣れないかもしれない書き方だなと思った
あと自分で書いたけどイメージするのに、少し時間かかるのでもやっとしてた

see also
4. More Control Flow Tools — Python 3.6.2 documentation

というところまできて、ぐぐったらmore_itertoolsがすぐに見つかった

more_itertoolsを見つけたのと同時に、iterとzipを組み合わせて合わせて作る方法あるよーってのも見つかった。

>>> zip(*[iter(ids)]*3)
[(0, 1, 2), (3, 4, 5), (6, 7, 8)]

ぱっとみ理解できない…
読み解くと同じイテレータオブジェクトを3つ生成して、それをZIPの引数として全て渡している
ZIPに渡す引数を同じものにすることで、自分自身を変更しながらとりだしてく
それによって渡した分だけの個数ごとに分割された状態となることが理解できた