CircleCI で CIRCLE_SHA1 を使う場合の注意

過去に自分がミスった内容と似た事例を他にも見かけたので、今更ながらメモ。

TL; DR

  • CircleCI の ECS/ECR へのデプロイに関するドキュメントで、コンテナイメージのタグに CIRCLE_SHA1 の値が設定されているが、これには要注意
  • CircleCI における CIRCLE_SHA1 の値は、最終コミットを識別するハッシュ値だよ
  • Git上のブランチやタグに依って変わるわけではないので、ブランチ・タグによって異なるものの識別には使えないよ
  • イメージタグのような識別子を作成する場合、それが必要なユニーク性を持っているか確認することが必要だよ

CircleCI で ECR/ECS にデプロイするときのコンテナイメージタグ

CircleCI 公式のドキュメントとして、以下の記事が公開されています。 (2019/12/15現在)

AWS ECR/ECS へのデプロイ - CircleCI

上記記事より引用

version: 2.1
orbs:
  aws-ecr: circleci/aws-ecr@0.0.2
  aws-ecs: circleci/aws-ecs@0.0.3
workflows:
  build-and-deploy:
    jobs:
      - aws-ecr/build_and_push_image:
          account-url: "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com"
          repo: "${AWS_RESOURCE_NAME_PREFIX}"
          region: ${AWS_DEFAULT_REGION}
          tag: "${CIRCLE_SHA1}"
      - ...

ビルドの結果出来上がる個々の コンテナイメージを識別するものとしてタグを設定します(上記例の tag )が、ここでは、タグに設定する値は "${CIRCLE_SHA1}" となっています。

(以前のドキュメントでは、普通に docker コマンドを使用して docker push ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com:${CIRCLE_SHA1} という感じになっていたはず…)

CIRCLE_SHA1 って?

さて、この CIRCLE_SHA1 に設定される値はどのようなものでしょうか。こちらも CircleCI の公式ドキュメントで説明されています。

環境変数の使い方 - CircleCI

CIRCLE_SHA1 String 現在のビルドの最後のコミットに関する SHA1 ハッシュ。

VCS が Git であれば、 Git のコミットハッシュと同一の値になります。CircleCI でビルドを開始する際に Git リポジトリから取得した内容 (バージョン断面) を識別するものと考えていいと思います。

コンテナイメージタグに CIRCLE_SHA1 を設定すると

この CIRCLE_SHA1 をコンテナイメージタグに使うと、どうなるでしょう?

「Git リポジトリの内容が同一ならば、ビルドされるコンテナイメージも同一である」という条件が成り立つのであれば、これで問題ありません。CIRCLE_SHA1リポジトリの内容ごとにユニークであるので、そこと 1:1 に対応付けられるコンテナイメージも CIRCLE_SHA1 で識別することができます。

しかし、上記の条件が必ずしも成り立たない場合、つまりGitリポジトリが同一であっても、できあがるコンテナイメージが異なる場合がある(異なるイメージそれぞれを管理する必要がある)場合は注意が必要です。

例えば、よく見かける方式として、本番環境やステージング環境にリリースを行うために、Git 上の master から特定のブランチにマージし、それをトリガーとして CircleCI で各環境向けのビルドおよびデプロイを行う、という方式を考えます。

コンテナイメージ内に環境の情報を含める場合は、当然、リポジトリの内容が同一であっても、本番環境向けとステージング環境向けでできあがるコンテナイメージに差異が発生します。しかし、 CIRCLE_SHA1 の値は、トリガーとなったブランチに依らず。あくまでも最終コミットのハッシュ値によって決定されるため、それぞれのコンテナイメージに同一のタグが設定されることになります。

Docker および ECR の仕様上、すでに存在するタグと同一のタグを付与して Push した場合、古い方のイメージからはタグが剥がされてしまうため、例えば ステージング環境へのデプロイ後に本番環境へのデプロイを行った場合、ステージング環境の ECS クラスタにも本番環境用のコンテナイメージがデリバリされてしまうことになります。また、並行して複数環境のビルド・デプロイを行って、完了する順番が不定となるような場合、見かけ上ランダムにコンテナイメージが切り替わるため、再現検証の難しい障害となることがあります。

回避方法

この問題をするには、以下のやり方が考えられます

  • ECR リポジトリを環境ごとに用意する
  • CIRCLE_SHA1 以外の、適切な識別文字列を用意する。
  • タグにデプロイ先環境を識別する文字列を付加する

インフラ構成や実装の手間、ユニーク性の要件、費用等に合わせた方法を取るとよいでしょう。

まとめ

CIRCLE_SHA1 (に限りませんが) を何かの識別子として使用する場合、そのユニーク性が識別する対象のユニーク性と合致するかどうか、毎回きちんと検証しましょうというお話でした。

必要最低限のrequirements.txtを作成する (virtualenv版)

2017年に書いた、「必要最低限のrequirements.txtを作成する」という記事に未だに多少のアクセスがありまして、今現在でこれを参考にされてしまうとちょっとアレだなあと思いましたので。

(当時は Docker 憶えたてでイキっていました。反省)

noisyspot.hatenablog.com

クリーンな Python 環境を得るには virtualenv がおすすめ

virtualenv は、システムの Python 環境から、任意のディレクトリ以下に最低限のライブラリや実行ファイルをコピーし、クリーンな仮想 Python 環境を構成できるツールです。

virtualenv.pypa.io

通常は、開発したいプロジェクトやプロダクトごとに仮想環境を構成し、そのプロダクトに必要なライブラリだけ仮想環境にインストールして使用する、という使い方になると思いますが、開発で試行錯誤した結果、開発環境がプロダクションに不要なライブラリで汚染されていて整理したい、なんてこともあると思います。

そんなときは慌てず、別のディレクトリに改めてクリーンな仮想環境を作って、作成したソースコードをコピーした上で、改めて必要最低限のライブラリを pip install してテストし、問題なければ pip freeze してあげれば良いです。

使い方

virtualenv が未インストールであれば、インストールします。 pip install virtualenv でインストールできますが、特定のバージョンを入れたいなどオプションについてはドキュメントを参照

仮想環境を作りたいディレクトリに移動し、環境を作成します。

$ mkdir -p ~/path/to/project
$ cd ~/path/to/project
$ virtualenv .
Using base prefix '/home/user/.pyenv/versions/3.7.5'
New python executable in /home/user/path/to/project/bin/python3.7
Also creating executable in /home/user/path/to/project/bin/python
Installing setuptools, pip, wheel...
done.

すると、このディレクトリの下に bin, include, lib というディレクトリができます。bin 以下にある activate というスクリプトを source すると、仮想環境を使用するモードになります。

$ source bin/activate
(project) $   

プロンプトに (ディレクトリ名) が付加されていれば、そのディレクトリの仮想環境に入っている状態です。

この状態で、必要なライブラリを pip install し、 pip freeze すれば、明示的にインストールしたライブラリと依存ライブラリだけのリストが出力されます。

仮想環境から出たい場合は deactivate コマンドを実行する ( activate 時に alias が設定されている ) か、シェルからログアウトしてください。

細かすぎて伝わらないJava10のvar仕様

いろいろ試したのでメモ。

初期化時に型が決まる

まあ当たり前ですが。

public class Main {
    public static void main(String... args) {
        var hoge = 1;              // hoge は int型になる
        hoge = 2147483648;         // Integer.MAX_VALUE + 1
        System.out.println(hoge);
    }
}
> javac Main.java
Main.java:4: エラー: 整数2147483648が大きすぎます
        hoge = 2147483648; // Integer.MAX_VALUE + 1
               ^
エラー1個

以下のようににすると、iはint型になるので桁溢れして無限ループになります。

for (var i = 0; i < 2147483648; i++) {
....

プリミティブ型も推論してくれる

public class Main {
    public static void main(String... args) {
        var i = 1;
        i.getClass().getName();
    }
}
>javac Main.java
Main.java:4: エラー: intは間接参照できません
        i.getClass().getName();
         ^
エラー1個

余計な Boxing/Unboxing は発生しないようで一安心。

匿名クラスのインスタンスで初期化すると匿名クラス型の変数になる

        var hoge = new Object() {
            int a = 1;
            String s = "Hello!";
            int func(int a, int b) {
                return a + b;
            }
        };

のようにすると、hoge は Object 型ではなくここで定義された匿名クラスの型 (Main$1とか) になります。 従って、この同じメソッドの中であれば、 hoge.ahoge.func(1, 2); のように匿名クラス内のメンバを Interface定義なしで 呼び出すことができます。おもしろいけど使い所あるかな?


以降思いついたら順次追加

無限Streamに終了条件を設定する (Java9版)

noisyspot.hatenablog.com

というのを以前書きましたが、Java9では Stream#takeWhile というメソッドが追加されてこういうケースが簡単に書けるようになりました。

    public static String getTreePathTakeWhile(Node node, String delimiter) {
        List<String> names = Stream.iterate(node, Node::getParent)
                .takeWhile(n -> n != null)
                .map(n -> n.getName())
                .collect(Collectors.toList());
        Collections.reverse(names);

        return delimiter + String.join(delimiter, names);
    }

これでSupplierを使った無限Streamがとても使いやすくなりますね。

必要最低限のrequirements.txtを作成する

シチュエーション

Pythonでつくったサーバーアプリを環境へのデプロイするために、依存モジュールのバージョンを固定したrequirements.txtを作りたいけど、開発環境のPython環境がだいぶ使い古しで、試しに導入した不要なモジュールもあるので、単純にpip freezeしちゃうと余計なモジュールが大量に混ざってしまう…なんてときの対処法。

手順


注(2019/12/08追記):

この記事では Docker を使った方法を説明していますが、 Python だけ気にするのであれば virtualenv を使用する方法のほうがシンプルかつ早いので、そちらを使用しましょう。

noisyspot.hatenablog.com


Dockerできれいな仮想環境を作る

とりあえず、pyenvでPythonをインストールできる最低限の環境を作ります。導入するモジュールによっては追加が要るかも。

以下のようなDockerfileを作る。とりあえずUbuntu14ベースで作ってますが、他のディストロならもうちょっとシンプルになるかも。

FROM ubuntu:14.04

ARG http_proxy
ARG https_proxy

ENV http_proxy=${http_proxy:-} \
    https_proxy=${https_proxy:-}

RUN apt-get update && apt-get install -y \
    build-essential python git curl zlibc zlib1g-dev \
    libssl-dev libreadline-dev libbz2-dev libsqlite3-dev \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /root

RUN git clone https://github.com/yyuu/pyenv.git .pyenv
ENV PATH /root/.pyenv/bin:$PATH
RUN echo 'eval "$(pyenv init -)"' >> .bashrc

CMD ["/bin/bash", "-i"]

これを適当なディレクトリに置いてdocker buildします。

$ cd clean_pyenv # 上記Dockerfileがおいてあるディレクトリに移動
# 直接インターネットが見られる環境の場合
$ docker build -t clean_pyenv ./
# プロキシが必要な場合
$ docker build --build-arg http_proxy=$http_proxy --build-arg https_proxy=$https_proxy -t clean_pyenv ./

仮想環境内で、pyenvを使ってターゲットバージョンのPython環境を作る

上記で作ったDockerイメージにはpyenvがインストール済みなので、これを使って必要な環境を作っていきます。 コンテナを起動してログイン、pyenv installで目的のバージョンをインストールして切り替えます。

$ docker run -it --rm clean_pyenv
$ pyenv install 2.7.13
$ pyenv global 2.7.13

アプリから直接参照しているモジュールのリストを用意してpip install -rした後、pip freezeする

例として、アプリから直接参照している外部モジュールがboto3 1.4.4だけだとします。 (モジュールがたくさんあるようであれば、列挙したテキストファイルを別のターミナルからdocker cpで送り込んであげればよいです。モジュールバージョンが最新でよければ、バージョン番号は省略でもOK)

$ echo boto3==1.4.4 > reqirements_part.txt
$ pip install -r reqirements_part.txt
$ pip freeze > requirements.txt

これで、依存モジュールも含むrequirements.txtが生成されるはずです。

途中間違えたら、コンテナからログアウトして終了してしまえば、pyenvだけのきれいな状態からやり直すことができます。もちろんDockerイメージは再利用可能。

Gitでローカル・リモート一緒にブランチ名を変更したい

Git 2.11.1で確認。

シチュエーション

hageってブランチ作って、

$ git checkout -b hage

諸々修正して、

$ git commit -a
$ git push -u origin hage

commit・pushした後に、

「あれ、ブランチ名間違えた。hogeだった」

ということで、ローカルとリモート併せて名前変えたい。

失敗例

upstreamブランチをリモートから削除して

$ git push --delete origin hage

ローカルブランチの名前を変えて

$ git checkout hage
$ git branch -m hoge

そのまま再push!

$ git push -u origin hoge
...
 * [new branch]      hoge -> hage
                             ~~~~

あれー?

$ git push --delete origin hage
$ git branch -vv
...
hoge ff0da9d [origin/hage: gone] ....
...

リモートのブランチが消えても、一度設定されたupstreamの向き先は残ってるっぽい。

解決

upstreamブランチを削除してローカルブランチ名変えた後、明示的にupstream設定を削除する必要があるようです。

$ git branch --unset-upstream

その後、再push

$ git push -u origin hoge
...
 * [new branch]      hoge -> hoge
                             ~~~~

めでたしめでたし。

atom 1.9.x + graphviz-preview の表示バグの回避

Atom を使っていて、ちょっと図を書いて整理したいなんてときに便利な graphviz-preview ですが、2016/8/27現在、graphviz-previewの更新が、対応バージョン1.7.0で止まっており、現時点の最新である1.9.8で使用するとこんなことになってしまいます。

f:id:shout_poor:20160827195503p:plain

そのうち修正されてほしいなあと思いつつ、とりあえずの回避方法。

$HOME/.atom/packages/graphviz-preview/styles/graphviz-preview.lessテキストエディタで開き、以下のwebview要素のstyleを追記します。

// The ui-variables file is provided by base themes provided by Atom.
//
// See https://github.com/atom/atom-dark-ui/blob/master/styles/ui-variables.less
// for a full listing of what's available.
@import "ui-variables";

.graphviz-preview {
  background-color: #fff;
  overflow: scroll;
  box-sizing: border-box;
  padding: 0;

  iframe {
    width: 100%;
    height: 100%;
    border: 0;
  }

  // ここから追記
  webview {
    width: 100%;
    height: 100%;
    border: 0;
  }
  // ここまで追記

}

するとこんな感じに。 スクロールバーが二重で表示されるのカッコ悪いんですが、とりあえず使うのには困らないので、これでしのぎながら公式のアップデートを待ちます…

f:id:shout_poor:20160827200504p:plain

2016/08/28 追記

.graphviz-preview クラスと webview 要素に overflow: none; を設定してやると余分なスクロールバーが消えました。実際のスクロールバーは、webview内に貼られるobjectが表示してくれるようです。

// The ui-variables file is provided by base themes provided by Atom.
//
// See https://github.com/atom/atom-dark-ui/blob/master/styles/ui-variables.less
// for a full listing of what's available.
@import "ui-variables";

.graphviz-preview {
  background-color: #fff;
  overflow: none;      // scroll -> none
  box-sizing: border-box;
  padding: 0;

  iframe {
    width: 100%;
    height: 100%;
    border: 0;
  }

  // ここから追記
  webview {
    width: 100%;
    height: 100%;
    border: 0;
    overflow: none;
  }
  // ここまで追記

}