簡単なRubyスクリプトをDockerで実行する

背景

データの修正のために、ちょっとしたバッチを書くことになりました。環境はEC2でAmazon Linux2でした。

rbenvをインストールしようかなと思って準備をし、いざrubyをインストールしようとしたところで、gccが入っておらずコンパイルができないことに気がつきました…

いつもならyum install -y gccなど行うところですが、パッケージの追加もあまりしてほしくないということだったので、Dockerコンテナで実行することにしました。

Dockerfileの作成

まずはDockerfileを作成します。

EC2上に適当なディレクトリを作成して、その中にDockerfileを作成します。

1
2
3
$ mkdir app
$ cd app
$ touch Dockerfile

ベースイメージの取得

それではDockerfileのFROMに記載するベースイメージを決めようと思います。

Docker image Rubyでググると、Rubyのオフィシャルイメージが見つかります。

https://hub.docker.com/_/ruby

以前Rubyコミッタの方が最新のバージョンを使うのが速度的には一番良いとおっしゃっていたので、最新の3.0.2を利用します。

Dockerfileつづき

先程のRubyのオフィシャルイメージのページの下の方を見ていくと、How to use this imageというセクションがあり、実際の利用方法が記載されています。

例に書いてあるのを倣って作成し一度ビルドしてみます。Dockerfileは以下のようになりました。

1
2
3
4
5
6
7
8
9
10
11
$ cat Dockerfile
FROM ruby:3.0.2

RUN bundle config --global frozen 1

WORKDIR /usr/src/app

COPY Gemfile Gemfile.lock ./
RUN bundle install

CMD ["bundle", "exec", "ruby", "./test.rb"]

Gemfileは今回必要となるデータベース(MySQL RDS)への接続と、その接続のための認証情報を取得するためのgemのみ記載しました。

1
2
3
4
5
$ cat Gemfile
source 'https://rubygems.org'

gem 'mysql2'
gem 'dotenv'

イメージのビルド

ではこれでビルドします

1
2
3
4
5
6
7
8
9
10
11
$ docker build -t my-ruby-app .
[+] Building 5.0s (8/11)
=> [internal] load build definition from Dockerfile 0.0s
...
(中略)
...
=> ERROR [4/7] COPY Gemfile Gemfile.lock ./ 0.0s
------
> [4/7] COPY Gemfile Gemfile.lock ./:
------
failed to compute cache key: "/Gemfile.lock" not found: not found

Gemfile.lockはなかったですね。Dockerfileの7行目からGemfile.lockを削除して再実行します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ docker build -t my-ruby-app .
[+] Building 5.4s (9/11)
=> [internal] load build definition from Dockerfile 0.0s
...
(中略)
...
=> CACHED [4/7] COPY Gemfile ./ 0.0s
=> ERROR [5/7] RUN bundle install 1.0s
------
> [5/7] RUN bundle install:
#9 1.002 The deployment setting requires a Gemfile.lock. Please make sure you have
#9 1.002 checked your Gemfile.lock into version control before deploying.
------
executor failed running [/bin/sh -c bundle install]: exit code: 16

エラーになってしまいました。調べてみると、Dockerfileの3行目にある

1
RUN bundle config --global frozen 1

で、GemfileやGemfile.lockの変更を禁止しているからのようです。

https://bundler.io/v2.2/man/bundle-config.1.html

その行を削除しても良いのですが、一般的にimageのビルドはそのビルドしたimageをデプロイして動作させるので、デプロイの一環でもあります。上記のbundlerのマニュアルにもある通り、--deploymentフラグを有効にすると、BUNDLE_FROZENtrueになるようですし、Gemfile.lockはソースコード管理されているはずなので、Dockerfileはこのままでいきます。

Gemfile.lockの作成

ではGemfile.lockはどのように作成すれば良いでしょうか。

先程のDockerHubのページに作成方法が書いてありました。

1
2
3
4
Generate a Gemfile.lock
The above example Dockerfile expects a Gemfile.lock in your app directory. This docker run will help you generate one. Run it in the root of your app, next to the Gemfile:

$ docker run --rm -v "$PWD":/usr/src/app -w /usr/src/app ruby:2.5 bundle install

イメージのバージョンだけ現在の3.0.2に合わせて実行してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ docker run --rm -v "$PWD":/usr/src/app -w /usr/src/app ruby:3.0.2 bundle install
Unable to find image 'ruby:3.0.2' locally
3.0.2: Pulling from library/ruby
...
(中略)
...
Status: Downloaded newer image for ruby:3.0.2
Fetching gem metadata from https://rubygems.org/..
Resolving dependencies...
Using bundler 2.2.22
Fetching mysql2 0.5.3
Fetching dotenv 2.7.6
Installing dotenv 2.7.6
Installing mysql2 0.5.3 with native extensions
Bundle complete! 2 Gemfile dependencies, 3 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

無事Gemfile.lockが作成できました。

再ビルド

ではもう一度ビルドしてみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ docker build -t my-ruby-app .
[+] Building 11.6s (12/12) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 236B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/ruby:3.0.2 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 641B 0.0s
=> [1/7] FROM docker.io/library/ruby:3.0.2 0.0s
=> CACHED [2/7] RUN bundle config --global frozen 1 0.0s
=> CACHED [3/7] WORKDIR /usr/src/app 0.0s
=> [4/7] COPY Gemfile Gemfile.lock ./ 0.0s
=> [5/7] RUN bundle install 11.2s
=> [6/7] COPY .env ./ 0.0s
=> [7/7] COPY . . 0.0s
=> exporting to image 0.2s
=> => exporting layers 0.1s
=> => writing image sha256:a67fb078890bb0301ed294fed21fe1d349a1ab731604328470ac759 0.0s
=> => naming to docker.io/library/my-ruby-app 0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them

イメージを確認します

1
2
$ docker images | grep -i ruby-app
my-ruby-app latest a67fb078890b 40 seconds ago 906MB

ちょっとサイズが大きい気がしますが、ビルドできました。

スクリプトの実行

では実行します。なにも指定しなければ、Dockerfileで最後に記述したコマンドが実行されます。

1
$ docker run my-ruby-app

実行ログなどを出力したい場合は、Gemfile.lockを作成した時のようにログを置くディレクトリを-vでマウントします。

test.rb

1
2
3
4
5
require 'dotenv'
require 'mysql2'

log = File.open('test.log', 'w')
log.write('log message')
1
2
3
$ docker run -v "$PWD":/usr/src/app my-ruby-app
$ cat test.log
log message

無事書き込みできていました。

まとめ

環境を変更することなく実行できるコンテナの長所を利用するといろんなことができるようになると思います。

これからはなにかを行うにしても1から実行環境を整えることもなくなりそうですね。便利な世の中になりました。

Python3でxmlをparseする

背景

構造化されたデータを読み込むことになり、そのデータが特定のURLにアクセスするとxml形式で取得できることがわかりました。

Python3をあまり知らないこともあり、同じような処理をこれからも行っていく予定なので、まとめようと思います。

HTTPクライアント

特定のURLにアクセスするためにHTTPクライアントを生成する必要があります。

私の知っている言語だとHTTPクライアントはいろんなライブラリがあったりして、それぞれ一長一短があります。Python3ではどうかなと思い、入門Python3を調べてみると、標準ライブラリurllib.requestが紹介された後に、requestsが紹介されていて、ほとんどの目的ではrequestsを使った方がウェブ開発が簡単になるようだと記載されています。わたしも倣ってrequestsを使ってみようと思います。

requestsのインストール

まずはじめにインストールする必要があります。

現在インストールされているパッケージを確認しましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ docker-compose run --rm web python -m pip list
Creating network "django_default" with the default driver
Creating django_web_run ... done
Package Version
--------------------------------- ---------
asgiref 3.4.1
backports.entry-points-selectable 1.1.0
boto3 1.18.38
botocore 1.21.38
certifi 2021.5.30
distlib 0.3.2
Django 3.2.7
filelock 3.0.12
jmespath 0.10.0
pip 21.2.4
pipenv 2021.5.29
platformdirs 2.3.0
python-dateutil 2.8.2
pytz 2021.1
s3transfer 0.5.0
setuptools 57.5.0
six 1.16.0
sqlparse 0.4.1
urllib3 1.26.6
virtualenv 20.7.2
virtualenv-clone 0.5.7
wheel 0.37.0

インストールされていないようなのでインストールします。Pipfileに追記します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
Django = "==3.2.7"
boto3 = "*"
requests = "*"

[dev-packages]

[requires]
python_version = "3.9"

インストールします。

1
2
3
4
$ docker-compose run --rm web python -m pipenv install --system --skip-lock
Creating django_web_run ... done
Installing dependencies from Pipfile...
🐍 ▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉ 1/1 —

インストールされたかどうか確認します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ docker-compose run --rm web python -m pip list
Creating django_web_run ... done
Package Version
--------------------------------- ---------
asgiref 3.4.1
backports.entry-points-selectable 1.1.0
boto3 1.18.38
botocore 1.21.38
certifi 2021.5.30
charset-normalizer 2.0.6
distlib 0.3.2
Django 3.2.7
filelock 3.0.12
idna 3.2
jmespath 0.10.0
pip 21.2.4
pipenv 2021.5.29
platformdirs 2.3.0
python-dateutil 2.8.2
pytz 2021.1
requests 2.26.0
s3transfer 0.5.0
setuptools 57.5.0
six 1.16.0
sqlparse 0.4.1
urllib3 1.26.6
virtualenv 20.7.2
virtualenv-clone 0.5.7
wheel 0.37.0

無事インストールされました。

requestsの基本的な使い方

次に基本的な利用方法を見ていきましょう。試すだけなのでDjango Shellを利用します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ docker-compose run --rm web python manage.py shell
Creating django_web_run ... done
Python 3.9.7 (default, Aug 31 2021, 19:01:35)
[GCC 10.3.1 20210424] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> import requests
>>> res = requests.get('https://google.co.jp')
>>> res.status_code
200
>>> res.headers['content-type']
'text/html; charset=Shift_JIS'
>>> res.encoding
'Shift_JIS'
>>> res.text
'<!doctype html><html itemscope="" itemtype="http://schema.org/WebPage" lang="ja"><head><meta content="世界中のあらゆる情報を検索するためのツールを提供しています。さまざまな検索機能を活用して、お探しの情報を見つけてください。" name="description">...(省略)

HTTPクライアントを作成し、コンテンツを取得できました。requestsはかなり直感的でわかりやすいと思います。では実際にXMLコンテンツを取得し、parseしたいと思います。

XML parserの選定

python xml parserでググるとxml.etree.ElementTreeというライブラリが出てきます。

ドキュメント: https://docs.python.org/ja/3/library/xml.etree.elementtree.html

これは標準ライブラリのようなのでインストールは不要でimportすれば利用できます。

サンプルのXMLを使って、試してみましょう。

1
2
3
4
5
6
7
8
>>> import requests
>>> import xml.etree.ElementTree as ET
>>> res = requests.get('https://www.w3schools.com/xml/simple.xml')
>>> res.status_code
200
>>> root = ET.fromstring(res.text)
>>> root.tag
'breakfast_menu'

XMLのサンプルは

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<breakfast_menu>
<food>
<name>Belgian Waffles</name>
<price>$5.95</price>
<description>Two of our famous Belgian Waffles with plenty of real maple syrup</description>
<calories>650</calories>
</food>
<food>
<name>Strawberry Belgian Waffles</name>
<price>$7.95</price>
<description>Light Belgian waffles covered with strawberries and whipped cream</description>
<calories>900</calories>
</food>
<food>
<name>Berry-Berry Belgian Waffles</name>
<price>$8.95</price>
<description>Light Belgian waffles covered with an assortment of fresh berries and whipped cream</description>
<calories>900</calories>
</food>
<food>
<name>French Toast</name>
<price>$4.50</price>
<description>Thick slices made from our homemade sourdough bread</description>
<calories>600</calories>
</food>
<food>
<name>Homestyle Breakfast</name>
<price>$6.95</price>
<description>Two eggs, bacon or sausage, toast, and our ever-popular hash browns</description>
<calories>950</calories>
</food>
</breakfast_menu>

なので、正しくrootが取得できています。

また、先程のrootはイテレート可能な子ノードを持ちます。

1
2
3
4
5
6
7
8
>>> for child in root:
... print(child.tag, child.attrib)
...
food {}
food {}
food {}
food {}
food {}

上から構造化データを辿っていくことが可能です。

データ構造を調査する

上記の例で言うと、foodの下の階層の要素がわからない場合はどうしたらよいでしょうか?

そう言った場合は先程のrootと同じように以下のようにすると下の階層の要素のtagがわかります。

1
2
3
4
5
6
7
8
>>> foods = root.findall('food')
>>> for e in foods[0]:
... print(e.tag)
...
name
price
description
calories

そうすると、どう言ったtagを持った要素が存在するのかわかります。tagがわかればtag名でアクセス可能です。

1
2
>>> foods[0].find('name').text
'Belgian Waffles'

まとめ

Python3でHTTPクライアントとXMLパーサを触ってみました。

HTTPクライアントのrequestsはとても利用しやすく、直感的にわかる感じのパッケージでした。今後もrequestsを利用していこうと思います。

XMLパーサについては直感的に理解するのが難しく、Elementの要素に対してアクセスするのも一苦労でした。利用しながら慣れていこうと思います。

参考図書

Githubでローカルのリポジトリをリモートに作成する方法

背景

ローカルでコツコツ実装していたリポジトリを周りのメンバーに共有するためにGithubにpushしたりするときもあると思います。

たまにしか行わない作業で忘れがちなのでメモしておきます。

リモートにリポジトリを作成する

Githubにログインしてリポジトリを作成します。

わたしはだいたい~/github/リポジトリ名というディレクトリを作成しているので、このディレクトリに合わせたリポジトリ名にします。

リモートにリポジトリをpushする

リポジトリ作成後に次のアクションの例が表示されるので、それを参考にします。

ローカルになにもない場合

作成したリポジトリをクローンしてからcommit, pushします。
SSH or HTTPSでクローンします

コマンドラインで新しいリポジトリを作成する場合

クローンするのではなく、ローカルでディレクトリを作成してリポジトリとします。

1
2
3
4
5
6
7
echo "# リポジトリ名" >> README.md
git init
git add README.md
git commit -m "first commit"
git branch -M main
git remote add origin https://github.com/オーガナイゼーション名/リポジトリ名.git
git push -u origin main

既存のリポジトリをコマンドラインでpushする場合

今回のケースはこちらにあたります。

1
2
3
git remote add origin https://github.com/オーガナイゼーション名/リポジトリ名.git
git branch -M main
git push -u origin main

-Mオプションってなんでしたっけと思ったので調べてみました。

1
2
3
4
With a -m or -M option, <oldbranch> will be renamed to <newbranch>.
If <oldbranch> had a corresponding reflog, it is renamed to match <newbranch>,
and a reflog entry is created to remember the branch renaming.
If <newbranch> exists, -M must be used to force the rename to happen.

(見やすいよう改行を入れています)

既存のブランチ名をmainに変えてくれってことみたいですね。

いつからこのようになったのでしょうか?調べてみると以下の記事が出てきました。

GitHub、これから作成するリポジトリのデフォルトブランチ名が「main」に。「master」から「main」へ変更

1年くらい前からそうなってたんですね…知りませんでした…

ということで、今後はmainにしていこうと思います。

別のリポジトリからインポートする

リポジトリのURLを指定することで既存のリポジトリをインポートできるようです。

実際にやってみる

では先程作成したGithubのリポジトリをリモートに設定します。

まず現在の設定状況を確認します

1
$ git remote -v

なにも表示されないので設定はされていません。

ではリモートを追加します。

1
$ git remote add origin https://github.com/オーガナイゼーション名/リポジトリ名.git

追加できているか確認します。

1
2
3
$ git remote -v
origin https://github.com/オーガナイゼーション名/リポジトリ名.git (fetch)
origin https://github.com/オーガナイゼーション名/リポジトリ名.git (push)

設定できました。

ではブランチ名をmainに変更します。

1
2
$ git branch
* master

今はmasterのみです。

1
2
3
$ git branch -M main
$ git branch
* main

変更できました。

最後にpushします。

1
2
3
$ git push -u origin main
remote: Repository not found.
fatal: repository 'https://github.com/オーガナイゼーション名/リポジトリ名.git/' not found

うーん、プライベートリポジトリだから認証がうまくいってないようですね。remoteのURLにユーザー名を追加してみます。

リモートを追加するには一度削除してから追加します。

1
2
$ git remote rm origin
$ git remote add origin https://ユーザー名@github.com/オーガナイゼーション名/リポジトリ名.git

もう一度pushしてみます。

1
2
3
4
5
6
7
8
9
10
11
$ git push -u origin main
Enumerating objects: 45, done.
Counting objects: 100% (45/45), done.
Delta compression using up to 8 threads
Compressing objects: 100% (39/39), done.
Writing objects: 100% (45/45), 7.56 KiB | 1.51 MiB/s, done.
Total 45 (delta 17), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (17/17), done.
To https://github.com/オーガナイゼーション名/リポジトリ名.git
* [new branch] main -> main
Branch 'main' set up to track remote branch 'main' from 'origin'.

無事pushできました。

まとめ

最初は自分だけで開発している場合ものちにGithubリポジトリにpushしてチーム開発していくことが多くあると思います。

いつでもpushできるよう準備しておくのが良さそうですね。

参考図書

複数のgitアカウントの利用方法

背景

Macのリプレースを行って(最近はこればっかり)、git関連の設定もふっとんでいます。

Macでは、会社用・プライベート用などいろんなgithubのアカウントを利用しています。複数のgitアカウントを設定する時のわかりやすい方法を調べました。

普段は1度設定したらあまり設定することのないGitの設定をまとめたいと思います。

事前準備 git: ‘secrets’ is not a git command. See ‘git –help’.

Macで一番最初にgit commitしようとしたら、エラーが発生しました

1
git: 'secrets' is not a git command. See 'git --help'.

ググってみると、git-secretsをインストールする必要があるようでした。

1
$ brew install git-secrets

こちらでエラーはなくなります。

設定の方針

設定の方針としてはメインで利用するアカウントをglobalに設定し、それをオーバーライドしたいリポジトリではlocalでアカウントを設定します。

そうすることで、よく使うアカウントがデフォルトの設定になり、手間が省けます。

よく利用するアカウントの設定

会社のPCだと、会社のGithubのアカウントを利用することが多いです。なので、会社のGithubアカウントをglobalに設定します。

まず現在の設定を確認してみます。

1
2
$ git config --global --list
fatal: unable to read config file '/Users/user/.gitconfig': No such file or directory

そもそもファイルがありませんでした…コマンドでuser.nameuser.emailを設定してみます。

1
2
$ git config --global user.name "会社のアカウント名"
$ git config --global user.email "会社のメールアドレス"

反映されていることを確認します

1
2
3
4
$ git config --list
credential.helper=osxkeychain
user.name=会社のアカウント名
user.email=会社のメールアドレス

プライベートのアカウントの設定

プライベートなGithubアカウントを設定したいリポジトリのディレクトリに移動します。

そこでgit config --local --listを実行します。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ git config --local --list
user.name=会社のアカウント名
user.email=会社のメールアドレス
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true
core.ignorecase=true
core.precomposeunicode=true
remote.origin.url=https://github.com/プライベート/xxxxxxx.git
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
branch.master.remote=origin
branch.master.merge=refs/heads/master

globalで設定した会社のメールアドレスが設定されています。ここで、git config --localを利用して、アカウント名とメールアドレスを設定してみます。

1
2
$ git config --local user.name "プライベートのアカウント名"
$ git config --local user.email "プライベートのメールアドレス"

反映されていることを確認します

1
2
3
4
5
6
7
8
9
10
11
12
13
$ git config --local --list
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true
core.ignorecase=true
core.precomposeunicode=true
remote.origin.url=https://github.com/プライベートのアカウント名/xxxxxxx.git
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
branch.master.remote=origin
branch.master.merge=refs/heads/master
user.email=プライベートのメールアドレス
user.name=プライベートのアカウント名

正しく反映されていました。

設定は~/.git/configに保存されています。確認してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ cat .git/config 
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = https://github.com/プライベートのアカウント名/xxxxxxx.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master
[user]
email = プライベートのメールアドレス
name = プライベートのアカウント名

しっかり保存されていました。

git commitして動作確認

実際にgit commitして、ログメッセージがどうなるかを確認します。

1
2
3
4
5
6
7
8
9
$ git commit -m "git commitのテストのためにコミット"
[mastet xxxx] git commitのテストのためにコミット
1 file changed, .....
$ git log
commit xxxxxxxxxxxx (HEAD -> master)
Author: プライベートのアカウント名 <プライベートのメールアドレス>
Date: Fri Sep 10 23:23:58 2021 +0900

git commitのテストのためにコミット

アカウント名、メールアドレスともに正しく認識されています。

これでgitの複数アカウントでの設定が完了しました。

おまけ

gitの設定がされていない場合、アカウント名はMacのログイン名、メールアドレスはMacのアカウント@ホスト名となります。

まとめ

Gitの設定はスムーズに開発を行うために必須です。アカウントを正しく設定できていないと、Githubでコミットログでも正しいアカウントがヒョ次されていることが好ましいです(誰による変更かわからなくなってしまうため)

今回基本的な設定を行いましたが、これからもgitの設定を育てていきたいと思います。

参考図書

pyenvを使ってVSCodeで利用するpythonのバージョンを指定する

背景

Macのリプレースを行ったときにVSCodeの設定の引き継ぎを行わなかったので、最初から設定することになってしまいました。

Pythonのインストールとバージョンの切り替えにはpyenvを利用していたので、引き続き使っていこうと思っていましたが、VSCodeのpythonプラグインをインストールした後のpython実行ファイルの指定で少し詰まってしまったことを記載します。

pythonプラグインのインストール

VSCodeで.pyのファイルを開くと、VSCodeからpythonプラグインのインストールを勧められるので、そのままインストールします。

インタプリタの設定

プラグインのインストールが終わると、インタプリタの設定画面が表示されます。Pythonをインストールする、インタプリタを選ぶなどの選択肢がありますが、pyenvでPythonをインストールしていなかったのでまずはpyenvでPythonをインストールします。

pyenvでPythonをインストールする

以前から利用していた3.9.6をインストールします(現時点での最新版は3.9.7)

1
2
3
4
5
6
7
8
9
$ pyenv install 3.9.6
python-build: use openssl@1.1 from homebrew
python-build: use readline from homebrew
Downloading Python-3.9.6.tar.xz...
-> https://www.python.org/ftp/python/3.9.6/Python-3.9.6.tar.xz
Installing Python-3.9.6...
python-build: use readline from homebrew
python-build: use zlib from xcode sdk
Installed Python-3.9.6 to /Users/user/.anyenv/envs/pyenv/versions/3.9.6

インストールできました。パスを確認しておきます。

1
2
$ which python
/Users/user/.anyenv/envs/pyenv/shims/python

次にVSCodeでインタプリタの指定を行います。

インタプリタの指定

VSCodeのインタプリタの指定で、インタプリタを選ぶを選択し、先ほど確認したパスのファイルを指定します。

ステータスバー(画面一番下の水色のバー)にPythonのバージョンが表示されるのですが、なぜかPython2.7となっています…

もう一度バージョンとパスを確認します。

1
2
3
4
$ which python    
/Users/user/.anyenv/envs/pyenv/shims/python
$ python --version
Python 3.9.6

ん〜原因がわかりません…

ステータスバーに表示されているPythonのバージョンをクリックすると、インタプリタの選択ができます。

インタープリターの選択と書かれている右に再読み込みボタンがあるので、それをクリックしてみると、先ほどインストールした3.9.6のパスが表示されました。

表示されたバージョンをクリックすると無事3.9.6が選択されたようです。ステータスバーのバージョンも更新されていました。

まとめ

VSCodeは上手に設定しないとその便利さを引き出すことができません。設定したりするのは少し面倒な気がしますが、最初だけなので、しっかりと設定して開発効率を上げていけるようになりたいです。

参考図書

VSCodeのターミナルでctrl-pが効かない時の対処法

背景

前回から引き続きVSCodeでおかしな挙動になりました。

VSCodeのターミナルはVSCode内でシェルを使えるので重宝しています。ここでコマンドを実行したりするので、ctrl-p、ctrl-n, ctrl-rはよく利用します。

新しいMacに変えて、VSCodeのターミナルでctrl-pを入力すると、

1
^P

とだけ表示されるようになってしまいました。これはめちゃくちゃ不便…シェルをちゃんと利用したいです。ということで対応方法を記しておきます。

zshの問題かどうかの確認

まずはシェルの問題なのかどうかを確認するため、いつも使っているterminalで確認してみようと思います。

普段使っているterminalソフトはiterm2です。では入力してみましょう。

1
$ ^P^P^P

ダメですね…この問題はVSCodeの問題ではなく、シェルが変わったことによるもののようです。

ググってみると、同じような目に合っている人が何名かいらっしゃるようです。対応方法としては、.zshrc

1
bindkey -e

を実行してシェルを再起動するといけるようです。やってみましょう。

1
2
3
4
5
6
7
8
$ ^P^P^P
# この時点ではまだダメ
$ vi ~/.zshrc
# bindkey -eを追記
$ exec $SHELL -l
$
$ exec $SHELL -l
# ctrl-pで以前実行したコマンドが表示できた

VSCodeのターミナルでもシェルを再起動することでctrl-pを入力すると前のコマンドが表示されるようになります。

まとめ

zshに不慣れで、今までbashでいいって思っていたのですが、これを機にzshについてしっかりと理解していこうと思いました。

VSCodeでキーボードの長押しができなくなったときの対処方法

背景

最近会社で利用しているMacのリース期限が近づいてきたので、新しいMacにリプレースすることにしました。

VSCodeの環境はそのまま移行せずに1から綺麗な状態で作成しようと思い、初期設定を行いました。

いざ開発するとなって、Vimプラグインを入れ忘れていることに気づき、インストールしました(vscodevim)。

いざ実装しようと思って、jklhキーを使ってカーソルを移動させていたところ、長押ししてもカーソルが動かないことに気づきました。

これはとても不便です…なんとかしないといけません…その時の対応方法を記載します。

Macの設定を疑う

Macをリプレースしたこともあって、最初はMacの設定だと思っていました。

Macのキーボードの設定はシステム環境設定キーボードからキーボードタブで、リピート入力認識までの時間を一番短くしてみました。

結果、なにも変わらず、原因はここではないことがわかりました。

ググってみる

全くあてがなかったので、とりあえずググってみました。

VSCode 連打 offという謎のワードで検索したところ、以下のページが見つかりました。

VSCodeにMacVimから乗り換え!基本設定して使ったら快適だ…

このページ内で連打で検索したところ、原因がvimプラグインにあることがわかりました。プラグインのページにも記載されています。

To enable key-repeating execute the following in your Terminal and restart VS Code:

1
2
3
4
$ defaults write com.microsoft.VSCode ApplePressAndHoldEnabled -bool false         # For VS Code
$ defaults write com.microsoft.VSCodeInsiders ApplePressAndHoldEnabled -bool false # For VS Code Insider
$ defaults write com.visualstudio.code.oss ApplePressAndHoldEnabled -bool false # For VS Codium
$ defaults delete -g ApplePressAndHoldEnabled # If necessary, reset global default

今回は上の2行のみ実行しました。

再起動、動作確認

コマンド実行すると、とくにメッセージは表示されないのですが、無事実行できたようです。

最後にVSCodeを再起動します(再起動しないと反映されません)。

起動後、長押しでカーソル移動できるようになりました。

まとめ

VSCodeで初めてハマりました。いろんなプラグインがあるからか、たまにこういうことが起こるかもしれません。VSCodeでなにか起こったらまず直近にインストールしたプラグインを調べるようにしようと思います。

AWS Batchを試してみる

背景

前回Djangoのカスタムコマンドを試しに追加してみました。

今回はこのカスタムコマンドをAWS Batchで実行してみたいと思います。

AWS Batchとは?

AWS Batchは今回初めて利用するので、何なのかを知っておく必要があります。

AWSサービスの学び方としては、AWS Black Belt Online Seminarを受けるとよいとAWS認定資格試験の本に書いてあったので、見てみることにしました。

その動画はこちらになります。
[AWS Black Belt Online Seminar] AWS Batch 資料及び QA 公開

資料はこちらです。

わかったこととしては、以下のようになります。

  • 通常のバッチを実行するためではなく、ハイパフォーマンスなりソースが必要な場合に用いるサービスである。(資料P.9)
    • しかし通常のバッチ処理をしてはいけないというわけではない
  • S3 Batchというサービスもある
  • 最終的なデータのIN/OUTにはS3を用いることが多い
  • ジョブ完了ステータスからSNSを利用してSlack通知をすることができる
  • ジョブの投入方法(資料P.48)
    • ユーザーが手動で投入(マネジメントコンソール/CLI)
    • S3へのファイルアップロードをトリガーにLambdaからアップロードされたファイルを処理するジョブを投入
    • Cloud Watch Eventsにより決められた時刻にジョブを投入
  • スポットインスタンスの活用を検討すべき
  • 複雑なワークフローを実現するためにはAWS Step Functionsを利用する

なんとなくわかった気がしたので、実際にマネジメントコンソールから設定してみます。

AWS Batchの設定

ではAWS Batchの設定を行いたいと思います。

ジョブの作成には、ジョブ定義とジョブキューが必要です。なので、この二つを作成します。

しかしジョブ定義の作成にはコンテナを指定する必要があったので、まず初めにコンテナイメージの作成から行なっていきます。

コンテナイメージの作成

ECRに登録するためにローカルで作成したイメージにECR用のタグを設定します。ローカルにあるイメージのタグをtest:1.0とすると

1
$ docker tag test:1.0 xxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/test:1.0

というようにしてECR用のタグを設定します。

コンテナレジストリへの登録

ECRに登録するためECRにログインし、先ほど作成したイメージをpushします。

1
2
$ aws ecr get-login-password --region ap-northeast-1 --profile test | docker login --username AWS --password-stdin xxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com
$ docker push xxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/test:1.0

ジョブ定義の作成

コンテナイメージをECRに登録できたので、ジョブ定義を作成します。

ジョブ定義に必要な内容は

  • 名前
  • プラットフォーム(EC2 or Fargate)
  • 再試行戦略
    • ジョブの試行回数
  • 実行タイムアウト
  • コンテナプロパティ
    • イメージ
    • コマンドの構文
    • コマンド
    • vCPU
    • メモリ
    • 実行ロール
    • Fargateプラットフォームのバージョン

です。
今回はプラットフォームにFargate、コンテナプロパティのイメージには先ほどECRに登録したイメージを指定します。コンテナプロパティのコマンドにはバッチで実行するコマンドを指定します。

ジョブキューの作成

次にジョブキューを作成します。

ジョブキューの作成には

  • 名前
  • 優先度
  • コンピューティング環境

が必要です。

コンピューティング環境を作成していなかったので、こちらを作成します。

コンピューティング環境の作成

コンピューティング環境の作成では以下の項目を設定します。

  • コンピューティング環境設定
    • コンピューティング環境のタイプ(マネージド or not)
    • コンピューティング環境の名前
  • インスタンスの設定
    • プロビジョニングモデル(Fargateなど)
    • 最大vCPU
  • ネットワーキング
    • VPC ID
    • サブネット

では以下にそれぞれの設定内容を記載します。

コンピューティング環境のタイプ

マネージド型

コンピューティング環境の名前

任意の文字列

プロビジョニングモデル

Fargate

最大vCPU

256

VPC ID

このコンピューティング環境専用のVPCを指定します

サブネット

上記VPCに紐づいたサブネット(パブリック)を指定します。パブリックサブネットにするのはECRへのアクセスを行うためです。

コンピューティング環境の作成ができたら、ジョブキューの作成を完了します。

ジョブの投入

では実際にジョブを作成してバッチを実行してみます。

ジョブから新しいジョブを送信をクリックします。名前は任意ですが、ジョブ定義・ジョブキューは先ほど作成したものを指定します。実行タイムアウトが空欄になっているので最小値の60を指定します。それ以外の項目に関しては、ジョブ定義・ジョブキューの内容が反映されています。

では送信ボタンを押して実行しましょう。


エラー発生

少し待ってからダッシュボードで確認すると、FAILEDに1と表示されてしまいました。FAILEDの1がリンクになっていてクリックしてみると、失敗したジョブが表示されました。

失敗したジョブ名がリンクになっていたのでクリックすると、ジョブの詳しい情報が表示されました。

ステータス理由というところに、エラー内容が記載されていました。

1
ResourceInitializationError: unable to pull secrets or registry auth: pull command failed: : signal: killed

なにかしらの原因でイメージが引っ張ってこれなかったということでしょうか。エラー内容から検索してみます。

ECR接続のためのルーティング設定

エラーメッセージで調査したところ、やはりECRからコンテナイメージをpullできなかったときに表示されるエラーメッセージでした。

ECRからコンテナイメージを取得するための方法が公式ドキュメントに載っていました。
https://aws.amazon.com/jp/premiumsupport/knowledge-center/ecs-pull-container-api-error-ecr/

今回の場合はFargateを利用していて、パブリックサブネット(のつもり)だったので、サブネットの設定で自動割り当てパブリックIPが有効になっているかを確認したのですが、有効になっていました。

もう少し調べてみると、パブリックサブネットと呼べるネットワーク設定の一つに、ルーティングでインターネットゲートウェイを向いていることと書かれていました。調べてみるとデフォルトルートが指定されておらず、ローカルのルーティングしか設定されていませんでした。

VPCのルートテーブルのデフォルトルート(0.0.0.0/0)にインターネットゲートウェイを設定しました。

実行確認

再度ジョブを実行してみると、今度は無事SUCCEEDEDになりました。

まとめ

AWS BatchでDjangoのカスタムコマンドを実行することができました。

次回は今回手動で実行していたジョブをイベントをトリガーに実行するようにしてみようと思います。

参考図書

AWS認定資格試験テキスト AWS認定ソリューションアーキテクト - アソシエイト 改訂第2版

Djangoでカスタムコマンドを作成する

背景

Djangoでバッチを実行することになりました。

Railsの場合はRakeタスクがあります。Djangoの場合はどのように行うのか調べて試してみました。

既存のカスタムコマンドの確認

Djangoでコマンドを追加するには、manage.pyを使ったカスタムコマンドと呼ばれる処理を追加することになります。もともとDjangoにはmanage.pyを利用したコマンドがいくつもあります。一覧で見てみましょう。一覧で見るにはmanage.pyに引数をつけずに実行します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
$ docker-compose run --rm web python manage.py
Starting django_db_1 ... done
Creating django_web_run ... done

Type 'manage.py help <subcommand>' for help on a specific subcommand.

Available subcommands:

[auth]
changepassword
createsuperuser

[contenttypes]
remove_stale_contenttypes

[django]
check
compilemessages
createcachetable
dbshell
diffsettings
dumpdata
flush
inspectdb
loaddata
makemessages
makemigrations
migrate
sendtestemail
shell
showmigrations
sqlflush
sqlmigrate
sqlsequencereset
squashmigrations
startapp
startproject
test
testserver

[sessions]
clearsessions

[staticfiles]
collectstatic
findstatic
runserver

runserverstartprojectなど見慣れたコマンドもいくつかありますが、ほとんど知らないコマンドです。こんなにあるんですね。

[]で囲まれているのはアプリケーション名でしょうか。settings.pyINSTALLED_APPSに指定されている名前と一致しています。

カスタムコマンドを追加する

カスタムコマンドの追加方法はこちらに記載されています。正確にはカスタムマネジメントコマンドなんですね。

実行ファイルを配置する

先ほどの公式サイトには以下のようにディレクトリ階層が書かれています。

1
2
3
4
5
6
7
8
9
10
11
polls/
__init__.py
models.py
management/
__init__.py
commands/
__init__.py
_private.py
closepoll.py
tests.py
views.py

ですので、アプリケーションディレクトリの下にmanagementディレクトリを作成し、その下にcommandsディレクトリを作成して、その下に実行するファイルを置く感じのようです。

実際に作成してみます。

1
2
3
$ cd sample_application
$ mkdir -p management/commands/
$ touch management/commands/dummy.py

これで実行ファイルを配置することができました。

実行ファイルを実装する

カスタムコマンドの実行方法についても、先ほどの公式サイトに記載されています。

例ではBaseCommandCommandErrorをimportしていますが動かすだけであればBaseCommandのみでよさそうです。今回は単純なサンプルを作って実行できるところまで試したいので、BaseCommandのみ利用したいと思います。

(タスクが正常に動作しなかった場合はCommandError例外をあげて知らせるということだと思います)

ということで、dummy.pyを実装してみました

1
2
3
4
5
6
7
8
9
10
11
from django.core.management.base import BaseCommand

class Command(BaseCommand):
help = 'Dummy command'

def add_arguments(self, parser):
pass

def handle(self, *args, **options):
print("execute Dummy command")

では実行してみます。

カスタムコマンドを実行する

それでは実行してみましょう。

1
2
3
4
$ docker-compose run --rm web python manage.py dummy
Creating django_web_run ... done
Unknown command: 'dummy'
Type 'manage.py help' for usage.

おっと、実行に失敗してしまいました…どこか設定が足りないようなので、調べてみます。

INSTALLED_APPへの追加

どの設定が足りないのかを調べていたところ、公式サイトにあまり目立たない感じで以下のように書かれていました。

1
In this example, the closepoll command will be made available to any project that includes the polls application in INSTALLED_APPS.

この例の場合は、pollsINSTALLED_APPSに設定すると有効になります、的なことが書いてあるのかと思います。

ということでINSTALLED_APPSに追加します

1
2
3
4
5
6
7
8
9
10
11
12
13
$ git diff sample_application/settings.py
diff --git a/sample_application/settings.py b/sample_application/settings.py
index b2f36d7..22ccbe3 100644
--- a/sample_application/settings.py
+++ b/sample_application/settings.py
@@ -37,6 +37,7 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
+ 'sample_application'
]

MIDDLEWARE = [

では実行してみます

1
2
3
$ docker-compose run --rm web python manage.py dummy
Creating django_web_run ... done
execute Dummy command

無事実行できました!

まとめ

今回カスタムコマンドの追加方法を学びました。INSTALLED_APPSに追加していなかったところ以外はスムーズでした。通常はアプリケーションは追加されているはずなので、わざわざINSTALLED_APPSに追加するという説明はほとんど書かれていません。参考図書の実践Django Pythonによる本格Webアプリケーション開発にも、カスタムコマンドの追加に関しては3ページくらいさいてあるのですが(280ページ、管理コマンドの追加)、INSTALLED_APPSに追加しないと有効にならないとは記載されていません。わたしみたいな初心者しかここでは躓かないのかもしれませんね。

次回はこのコンテナを使ってカスタムコマンドをAWS Batchで実行しようと思います。

参考図書

Djangoで各環境ごとの設定を行う

背景

Djangoでアプリケーションを開発するための準備として情報をいろいろ集めています。

アプリケーションを稼働させる環境として、ローカルの開発環境(development)とステージング環境(staging)と本番環境(production)があります。

Djangoではどのように環境ごとの設定をするのだろう?と思ったので調べてみることにしました。

Railsの場合

RailsではRAILS_ENVという環境変数があり、RAILS_ENVに環境名を設定します。また、設定ファイルはRAILS_ENVで設定した環境名により、設定値が変えられるようになっています。また、そういった環境ごとで設定を分けるのをサポートしてくれるgemもあります。アプリケーションもサポートしてくれていますし、それをサポートするgemもあるのはすばらしいですね。

Djangoの場合

調べてみてもあまり情報は出てきませんでした。Railsよりは情報量が多くなかったです。

困っていたところ、この前購入した実践Django Pythonによる本格Webアプリケーション開発9.9 設定ファイルの分割にその方法が載っていることに気づきました。

DjangoではDJANGO_SETTGINS_MODULEという環境変数に、設定ファイル(デフォルトでsettings.py)を指定することで、環境ごとの設定を行うことができそうです。

以下のような感じで行います。

1
$ DJANGO_SETTING_MODULE=project.production_settings python manage.py runserver

Railsでは環境名でしたが、Djangoでは設定ファイル自体を指定するようですね。環境名ではなく設定ファイルなので、設定をそのファイルにすべて記載しないといけなさそうで、だいぶ大きくなってしまうイメージがありますが、実際に実装していってからその問題にあたったら考えたいと思います。

ちなみに、--settingsオプションでも指定できるようです。

1
$ python manage.py runserver --settings=project.production_settings

コンテナの利用の際などは環境変数を渡すことが多いと思いますので、環境変数の方を利用するということで大丈夫かと思います。

まとめ

Webアプリケーションフレームワークでは環境ごとの設定をサポートしているものがほとんどだと思います。Djangoでのサポート方法を調べてみました。

実際に設定ファイルを書いてみて問題が出てきたらまた共有したいと思います。

参考図書