Jenkins の CodeBuild プラグイン の TIPS

はじめに

Jenkins から CodeBuild を使うためのプラグインAWS で提供されていて、このプラグインのちょっとした使い方についてです。

docs.aws.amazon.com

github.com

plugins.jenkins.io

検索してもこのプラグインの情報があまり無くて使われてないような感じがありますが Jenkins 使っている場合は便利な場合もあると思います。

使いそうなケースは以下のようなものがあると思います。

  • Jenkins で使える色々なトリガーで CodeBuild ジョブを実行したい
  • AWS マネジメントコンソールからやるより Jenkins のほうが慣れている人が多い
  • Jenkins に色々ジョブがあって、実行中に Jenkins サーバーに負荷をかけたくない

Switch Role して使いたい

CodeBuild プラグインを使うには CodeBuild 関連の IAM 権限が必要になります。

Jenkins サーバーを EC2 インスタンスで動かしている場合は EC2 インスタンスに付いているインスタンスプロファイルの IAM ロールが使えます。

しかし Jenkins では色々なジョブを動かしたりするので一つの IAM ロールにたくさん権限付けたく無いことがあります。

それから Jenkins が動いているのとは別の AWS アカウントの CodeBuild を実行したい場合も考えられます。

こういう時に Switch Role が便利です。

Switch Role についは下のドキュメントなどに書かれていました。

docs.aws.amazon.com

やり方

Jenkins の認証情報設定のところから CodeBuild Credentials を作成します。

f:id:tkaaad97:20191027011448p:plain

(画像は一部情報を書き換えています)

IAM Role ARN に Switch Role 先の IAM ロールを指定します。

インスタンスプロファイルの IAM ロールから Switch Role できるようになっていればアクセスキーなどは空のままで大丈夫でした。

CodeBuild プラグインを使うところでこの認証情報を credentialsId で指定します。

下はパイプラインジョブ中の書き方の例です。

node {
    stage('build') {
        awsCodeBuild(
            credentialsId: 'jenkins-codebuild-role-credentials',
            credentialsType: 'jenkins',
            projectName: 'hello',
            region: 'ap-northeast-1',
            sourceControlType: 'project'
        )
    }
}

AWS_PROFILE 環境変数で切り替えられるかもやってみたのですが、こちらは上手く行きませんでした。

環境変数を指定する

Jenkins ジョブをパラメーター化して実行時にパラメーターを渡し、これを CodeBuild ジョブの環境変数に渡したいときのやり方です。

なぜか独特なフォーマットになっているので注意が必要なことがあります。

フォーマットの説明は以下です。これは Jenkins 中のパイプラインシンタックスのページにあった情報です。

 Optional parameter. These environment variables will be passed to the StartBuild API and will override any project environment variables.
Example: [ { VAR, value } ]
Example: [ { VAR, value }, { VAR2, value } ]

Commas in the name or value must be escaped. For freestyle projects, escape with one backslash:
[ { VAR, value1\,value2 } ]
For pipeline projects, escape with two backslashes:
awsCodeBuild ... envVariables: '[{VAR, value1\\,value2}]'

カンマはエスケープが必要になっているようです。

また試していて環境変数の値に空文字列を渡すことができないようでした。 下のようなエラーが出ました。

[AWS CodeBuild Plugin] CodeBuild configured improperly in project settings
    > CodeBuild environment variable keys and values cannot be empty and the string must be of the form [{key, value}, {key2, value2}] (Service: null; Status Code: 0; Error Code: null; Request ID: null)

Jenkins ジョブにパラメーターを渡すところでこのプラグインの挙動を考慮するのは微妙なのでパイプラインジョブの中で書き換えて渡してやるのがいいような気がしています。 カンマのエスケープもパイプラインのコードの中で行ってもいいと思います。

node {
    stage('build') {
        def envVariables = null;
        if ("${PARAM1}" != '') {
            envVariables = "[{PARAM1,${PARAM1}}]"
        }

        awsCodeBuild(
            credentialsId: 'jenkins-codebuild-role-credentials',
            credentialsType: 'jenkins',
            projectName: 'hello',
            region: 'ap-northeast-1',
            sourceControlType: 'project',
            envVariables: envVariables
        )
    }
}

モーメントシャドウマップについて

はじめに

分散シャドウマップに続いてモーメントシャドウマップについてです。

モーメントシャドウマップの方が少し難しい感じでなかなか理解がすすまなかったんですが、論文のほかに補足的なドキュメントが出ていてそちらを見ていたら大体の流れが理解できてきた気がするのでまとめようと思います。

アルゴリズムの部分で出てきた式をなんとなく追いかける目的で書いているため色々と細かい部分は飛ばしています。

参考にしたもの

研究者の方のページに論文やスライド、発表動画、補足ドキュメントなど情報が色々載っていました。

momentsingraphics.de

cg.cs.uni-bonn.de

導出について詳しく書いてあった補足のドキュメントはこの pdf です。

http://cg.cs.uni-bonn.de/aigaion2root/attachments/MomentShadowMappingSupplementary.pdf

モーメントシャドウマップ

まずモーメントシャドウマップについて説明してみます。

分散シャドウマップと少し似ていて改良した方法になっています。

分散シャドウマップでは二次のモーメントまでを使いますが、モーメントシャドウマップではより高次のモーメント (通常は四次) まで利用します。

ライトブリーディングという遮蔽物の境界で本来影であるはずのところに光の筋ができてしまう問題が改善されているようです。

MIPMAP やガウシアンブラーなどのアンチエイリアシング手法も同様に使用できます。

アルゴリズム

論文の Algorithm 2 にニの倍数のモーメントでの計算方法が書かれています。

先に使われている変数をいくつか紹介します。

$z$ はライトからの深度です。

$\textbf{b}$ は深度からモーメントの列ベクトルを返す関数です。

$$ \textbf{b}(z) := (z, z^{2}, \ldots, z^{n})^{T} $$

$b$ は $\textbf{b}(z)$ の平均値です。

$\hat{\textbf b}$ は0乗からn-1乗までのモーメントの列ベクトルを返す関数です。

$$ \hat{\textbf b}(z) := (1, z, z^{2}, ..., z^{n-1})^{T} $$

$\hat{b}$ は $\hat{\textbf b}(z)$ の平均値です。

Algorithm 2

  1. $$n := \frac{m}{2} + 1$$

    モーメントの次数$m$から求める深度の数$n$が決まります。

  2. $$ B := (b _ {j+k-2}) _ {j,k=1} ^ n $$

    モーメント平均値から$B$行列を作ります。

  3. $$ z _ 1 := z _ f $$

    深度のうち一つは描画する部分の深度とします。

  4. $$ c := B^{-1} \cdot \hat{\textbf b}(z_{1})$$

    $B$逆行列とベクトル $\hat{\textbf b}(z_{1})$ の積を計算します。これを$c$とします。

  5. $$ \sum _ {k=1} ^ n c _ k z ^ {k-1} = 0 $$

    $c$と深度の $n-1$ 次の方程式を解いて $n-1$ 個の深度を求めます。

  6. $$\hat{A} := (z _ i ^ {j-1}) _ {j,i=1} ^ n $$

    求めた深度を使って $\hat{A}$ 行列を求めます。この行列はヴァンデルモンドマトリックスと呼ばれ逆行列を作れます。

  7. $$ w = \hat{A}^{-1} \hat{b} $$

    $\hat{A}$ の逆行列とモーメント平均値のベクトルから各深度の比率を求めます。

  8. $$ G := \sum_{i=1,z _ i \lt z _ f} ^ n w _ i $$

    描画部分の深度よりも小さい深度の比率を足して影の濃さを計算します。

導出

モーメントシャドウマップでは$n$個の深度に集中して分布するとして深度と比率を求めて解いています。

モーメント平均値は次のように表せます。$w _ i$ は深度 $z _ i$ の分布比率です。

$$ \hat{b} = \sum _ {i=1} ^ n w _ i \hat{\textbf b}(z _ i) $$

次に $\hat{\textbf b}(x _ {i})$ の直積の平均値を考えます。 これは$B$と等しくなります。

$$ \begin{aligned} \sum _ {i=1} ^ n w _ i \hat{\textbf b}(z _ i)\otimes\hat{\textbf b}(z _ i) &= (\sum _ {i=1} ^ n w _ i z _ i ^ {j-1} z _ i ^ {k-1}) _ {j,k=1} ^ n \\ &= (\sum _ {i=1} ^ n w _ i z _ i ^ {j+k-2}) _ {j,k=1} ^ n \\ &= (b _ {j+k-2}) _ {j,k=1} ^ n \\ &= B \end{aligned} $$

変形して次の形にできます。

$$ \begin{aligned} B &= (\sum _ {i=1} ^ n w _ i z _ i ^ {j-1} z _ i ^ {k-1}) _ {j,k=1} ^ {n} \\ &= (\sum _ {i=1} ^ {n}\sum _ {l=1} ^ {n} w _ {i} \delta _ {il} z _ {i} ^ {j-1} z _ {l} ^ {k-1}) _ {j,k=1} ^ {n} \\ &= \hat{A}\text{diag}(w)\hat{A} ^ {\textrm{T}} \\ &= \begin{pmatrix} 1 & 1 & \cdots & 1 \\ z _ {1} & z _ {2} & \cdots & z _ {n} \\ \vdots & \vdots & & \vdots \\ z _ {1} ^ {n-1} & z _ {2} ^ {n-1} & \cdots & z _ {n} ^ {n-1} \end{pmatrix} \begin{pmatrix} w _ {1} & & & 0 \\ & w _ {2} & & \\ & & \ddots & \\ 0 & & & w _ {n} \end{pmatrix} \begin{pmatrix} 1 & z _ {1} & \cdots & z _ {1} ^ {n-1} \\ 1 & z _ {2} & \cdots & z _ {1} ^ {n-1} \\ \vdots & \vdots & & \vdots \\ 1 & z _ {n} & \cdots & z _ {n} ^ {n-1} \end{pmatrix} \\ \hat{A} ^ {-1} B \hat{A} ^ {-\textrm{T}} &= \text{diag}(w) \end{aligned} $$

$\hat{A} ^ {T} B ^ {-1} \hat{A}$ も対角行列となります。

$$ \hat{\textbf b}(z _ {j}) ^ {\textrm{T}} B ^ {-1} \hat{\textbf b}(z _ {k}) = \begin{cases} 0 & \text{if} & j \ne k \\ 1 / w _ {j} & \text{if} & j = k \end{cases} $$

ここから Algorithm 2 のステップ4とステップ5の式が出てきます。

ステップ7の各深度の分布比率はモーメント平均値の式から出てきます。

$$ \begin{aligned} \hat{b} &= \sum _ {i=1} ^ {n} w _ {i} \hat{\bf{b}}(z _ {i}) \\ &= \hat{A} w \\ w &= \hat{A} ^ {-1} \hat{b} \end{aligned} $$

$m=2$ の場合

$m=2$ の場合は分散シャドウマップと同じ式になります。

(分散シャドウマップの論文に出ていたのは影にならない確率なので実際には1から引いた値と同じ)

$$ \begin{aligned} m &= 2 \\ n &= 2 \\ B &= \begin{pmatrix} 1 & b _ {1} \\ b _ {1} & b _ {2} \end{pmatrix} \\ A &= \begin{pmatrix} 1 & 1 \\ z _ {1} & z _ {2} \end{pmatrix} \\ z &= \frac{b _ {1} z _ {f} - b _ {2}}{z _ {f} - b _ {1}} \\ G &= \frac{(z _ f - \mu) ^ 2}{\sigma ^ 2 + (z _ f - \mu) ^ 2} \end{aligned} $$

$m=4$ の場合

Algorithm 3 の Hamburger 4MSM アルゴリズムというのが多分実際に実装に使われるアルゴリズムだと思います。

$B$逆行列が必要ですが計算誤差で正則でなくなってしまうことを避けるため$b$にバイアスを加えた $b'$ を使っているようです。

バイアスに使われている $\alpha$ は$0$以上の十分小さな値 (例えば $3.0 \cdot 10 ^ {-5}$ ) です。

$$ \begin{aligned} m &= 4 \\ n &= 3 \\ b' &= (1 - \alpha) b + \alpha \begin{pmatrix}0.5 \\ 0.5 \\ 0.5 \\ 0.5\end{pmatrix}\\ B &= \begin{pmatrix} 1 & b' _ 1 & b' _ 2 \\ b' _ 1 & b' _ 2 & b' _ 3 \\ b' _ 2 & b' _ 3 & b' _ 4 \end{pmatrix} \\ A &= \begin{pmatrix} 1 & 1 & 1 \\ z _ {1} & z _ {2} & z _ {3} \\ z _ {1} ^ 2 & z _ {2} ^ 2 & z _ {3} ^ 2 \end{pmatrix} \\ G &= \begin{cases} 0 & \text{if} & z _ f \leq z _ 2 \leq z _ 3 \\ \frac{z _ f z _ 3 - b' _ 1 (z _ f + z _ 3) + b' _ 2}{(z _ 3 - z _ 2)(z _ f - z _ 2)} & \text{if} & z _ 2 < z _ f \leq z _ 3 \\ 1 - \frac{z _ 2 z _ 3 - b' _ 1 (z _ 2 + z _ 3) + b' _ 2}{(z _ f - z _ 2)(z _ f - z _ 3)} & \text{if} & z _ 2 \leq z _ 3 < z _ f \\ \end{cases} \end{aligned} $$

$G$ は Wolfram alpha で同じ感じの答えが出るのが計算できました。

Simplify[Inverse[{{1,1,1},{z_f,z_2,z_3},{z_f^2,z_2^2,z_3^2}}][[2]].{1,b_1,b_2}]
Simplify[Inverse[{{1,1,1},{z_f,z_2,z_3},{z_f^2,z_2^2,z_3^2}}][[2;;3]].{1,b_1,b_2}.{1,1}]

思ったこと

最初は数か所の深度だけに分布すると仮定して計算するのは、それでいいのかなという疑問を持ちました。

ただ $m=2$ で分散シャドウマップで使われているチェビシェフの不等式と同じ結果となったのと、モーメントのみから正確な深度の分布を求めることはできないので下限の確率を求めるということがこの計算の意味なのかなと考えるとなんとなく納得できました。

ライトブリーディングが起こるような二箇所の深度に集中して分布する場合などにこの方法で対応できるということも理解できます。

ちょっと複雑だなという気はやっぱりしてしまいます。ライトブリーディングの問題が解決できれば他の方法でもいいかもとも思いました。

数式の表示

MathJax を使うようにしてみました。

同じ数式の書き方で VS Code などで Markdown 表示の確認ができて、はてなブログの Web 上で確認するよりも大分すばやく確認できて書きやすいと思います。

しかしはてなブログ特有の Markdown 記法と干渉したりして結局そのまま使えるわけではないので微妙のような気もします。

別のページに何か影響でてしまっているかとかもあまり確認してないので後で色々調整必要かもしれません。

分散シャドウマップについて

はじめに

分散シャドウマップ (Variance Shadow Maps) について調べたことのまとめです。

モーメントシャドウマップという手法の方が新しく提案されていて、これは分散シャドウマップを改良した方法のようです。

最初はモーメントシャドウマップを調べていたんですが、分散シャドウマップについて知らないと理解できなそうだったので分散シャドウマップを先に調べることにしました。

分散シャドウマップ

元の論文が Web 上で公開されていて読むことができました。

www.punkuser.net

http://www.punkuser.net/vsm/vsm_paper.pdf

最初は Web 上の解説記事などを読んだりしていたんですが、 この論文はそこまで難しくはなさそうだったので論文を読んだほうが確実な気がします。

分散シャドウマップについて説明してみます。

シャドウマップでの影の描画ではまずライトを視点として、ライトから一番近いポリゴンの深度をシャドウマップ用のテクスチャに書き出します。

それからカメラ視点からポリゴンを描画していき、描画するピクセル部分のライトからの距離がシャドウマップよりも遠い場合はライトとの間に遮蔽物があることがわかるので影が落ちるように明るさを変えて描画します。

シャドウマップの解像度が低い場合にエイリアシングが起こることがよくあります。

分散シャドウマップではエイリアシングが改善され、影の境界がスムーズに描画されます。

分散シャドウマップでは MIPMAP や異方性フィルタリングなど通常の画像に使われるエイリアシングの対策が有効に使えるというのも利点のようです。

描画の方法

  1. まずシャドウマップを作成します。ただし通常のシャドウマップと違ってライトからの深度と深度の二乗を出力します。

  2. 必要に応じて事前に2パスガウシアンブラーでフィルタリングを行います。

  3. シャドウマップの MIPMAP を生成します。OpenGL では MIPMAP を自動で生成する API が使えます。

  4. カメラ視点で描画を行います。フラグメントシェーダーでシャドウマップから深度と深度の二乗を取得します。シャドウマップから取得した値は局所的な平均値になっています。 描画部分のライトからの深度がシャドウマップの深度より小さい場合は影は落ちないと判定します。 描画部分のライトからの深度がシャドウマップの深度よりも大きい場合はチェビシェフの不等式を使って影の強さを計算して描画します。

チェビシェフの不等式について

分散シャドウマップで使われているのはチェビシェフの不等式の single tail バージョンです。

en.wikipedia.org

読み方がわかりませんがカンテリ (コンテリ?) の不等式と呼ばれているようです。

論文に出てくるのは下の式です。

{
P(x \geq t) \leq p_{max}(t) \equiv \frac {\sigma ^{2}}{\sigma ^{2}+(t - \mu)^{2}}
}

{t > \mu} という条件があります。

{\mu} は深度の平均値です。 {\sigma ^2} は深度の分散で {\mu} と深度の二乗の平均から求められます。

{
\sigma ^{2} = E(x ^2) - E(x) ^2
}

{P(x \geq t)} は影を作る遮蔽物の深度が t よりも大きい場合の確率です。 t は描画する部分の深度なのでこの確率は影にならない場合の確率だと思います。 t が大きくなると分母が大きくなり、影にならない確率は小さくなります。

vertex skinning の覚え書き

はじめに

3D モデルを読み込んで OpenGL で表示させようとしていてこのあたりを調べていたのでここに書いておきます。

glTF を主に調べていましたが、他のフォーマットなどでも同じ仕組みが使われてるようです。

glTF のドキュメントには vertex skinning という名前が出てましたが色々呼び方があって紛らわしいです。

ja.wikipedia.org

スケルタルアニメーションとかスキンメッシュアニメーション、ボーンアニメーションとも呼ばれるようです。

参考にしたもの

glTF のチュートリアルがわかりやすいと思います。

glTF-Tutorials/gltfTutorial_019_SimpleSkin.md at master · KhronosGroup/glTF-Tutorials · GitHub

glTF-Tutorials/gltfTutorial_020_Skins.md at master · KhronosGroup/glTF-Tutorials · GitHub

それから VSCode の glTF プラグインがあって、これでファイル内容を見つつプレビューしたりできて便利でした。

marketplace.visualstudio.com

定義へのジャンプができるため他の要素を ID で参照している部分などを追いかけるのがやりやすいと思います。

それからこちらは DirectX のフォーマットに関する記事ですが日本語で詳しいところまで解説されていて理解しやすかったです。

marupeke296.com

marupeke296.com

仕組みなど

vertex skinning は 3D モデルに色々なポーズを取らせたり、アニメーションさせて歩かせたり踊らせたりといったことに使えます。

やったことないですが Blender でモデルを作ってボーンをいれて動かすような場合も同じ仕組みが使われてると思います。

簡単に仕組みを説明してみます。

3D モデルはたくさんの三角形から成っていて、この三角形の頂点と紐付く情報を色々持っています。

頂点と紐付く情報は頂点の位置や色、テクスチャ座標などです。

vertex skinning ではモデルにボーンを埋め込んでボーンの動きに追随して各頂点が動くようになっています。

ボーンの数は三角形頂点の数に比べて少なく、三角形頂点の情報を更新することなくボーンの変形だけでモデルを動かすことができるので扱いやすくなっています。

ボーンというと棒が繋がったようなのを想像しますが実際は階層的な座標系の構造を表すようなものだと思います。

肩の座標系の下に肘の座標系があってさらにその下に手首、その下に指という感じです。

頂点の情報にはボーンのインデックスとウェイト (影響度) が追加されます。

シェーダーには頂点の情報のほかに各ボーンの変形を表す行列が渡されてボーンによる変形を反映した座標が計算されて描画されます。

glTF

glTF の構造のうち vertex skinning と関係したところを書いておきます。

ノード

ノードは階層的な構造を表すのに使われます。ボーンの構造もノードで表現されます。

仕様だとこのあたりです。

https://github.com/KhronosGroup/glTF/blob/1f78518ec26e805cd23580022ca49f4349b0fd65/specification/2.0/README.md#nodes-and-hierarchy

JSONnodes の型は配列でインデックスが各ノードの識別子として使われます。 仕様のバージョン1.0では nodes にオブジェクトを使ってキーを識別子として使っている例もあるんですがバージョン2.0では配列になっているようです。

ノードのプロパティは name, children, rotation, scale, translation, matrix, mesh, skin などがあります。

https://github.com/KhronosGroup/glTF/blob/1f78518ec26e805cd23580022ca49f4349b0fd65/specification/2.0/README.md#node

children に子ノードの識別子を指定して階層的な構造を表せます。

rotation, scale, translation, matrix はノードの変形です。

https://github.com/KhronosGroup/glTF/blob/1f78518ec26e805cd23580022ca49f4349b0fd65/specification/2.0/README.md#transformations

mesh はノードと紐付くメッシュの識別子です。skin はスキンの識別子です。スキンについては後で説明します。

vertex skinning の場合は一番上の階層のノードに meshskin が指定されるようです。

仕様が1.0から2.0に変わったときになくなっているプロパティがいくつかあるようです。

https://github.com/KhronosGroup/glTF/blob/1f78518ec26e805cd23580022ca49f4349b0fd65/specification/1.0/README.md#node

スキン

スキンもボーンの情報を持っています。

https://github.com/KhronosGroup/glTF/blob/1f78518ec26e805cd23580022ca49f4349b0fd65/specification/2.0/README.md#skins

https://github.com/KhronosGroup/glTF/blob/1f78518ec26e805cd23580022ca49f4349b0fd65/specification/2.0/README.md#skin

joints はボーンを構成するノードの識別子の配列です。ボーンのルートのノードは相対的に動かないので joints には入ってません。

inverseBindMatricesaccessor の識別子で joints の数と同じ要素数の 4x4 行列の配列を参照します。 inverseBindMatrices はボーンの変形を計算するときに必要になります。

その27 アニメーションの根っこ:スキンメッシュアニメーション(ボーン操作)

インバースバインドマトリックスは恐らくこちらの記事で書かれていたボーンオフセット行列と同じものだと思います。

例えば肩を回転させて二の腕を動かすときに、肩の座標を原点に一致させないとおかしな変形になってしまうためこの行列が必要になるのだと思います。

ボーンの変形行列の計算方法はチュートリアルに書かれていました。

https://github.com/KhronosGroup/glTF-Tutorials/blob/master/gltfTutorial/gltfTutorial_020_Skins.md#the-joint-matrices

jointMatrix(j) =
  globalTransformOfNodeThatTheMeshIsAttachedTo^-1 *
  globalTransformOfJointNode(j) *
  inverseBindMatrixForJoint(j);

まずインバースバインドマトリックス inverseBindMatrixForJoint(j) を掛けて頂点をボーンの座標系に移動させます。

それからボーンをグローバル座標系で変形させる行列を掛けます。

最後に globalTransformOfNodeThatTheMeshIsAttachedTo^-1 を掛けているのは後で使う modelViewMatrix にメッシュがアタッチされたノードの変形が含まれているからだと思います。

skeleton というプロパティが指定される場合があってこれはボーンのルートノードの指定に使われるようなんですが、ノードのプロパティで skin が指定される場合との違いがよくわかりません。

ライブラリによってはこの skeleton プロパティは無視されているようで、three.js の GLTFLoader では使ってないように見えます。

Go で Lambda 書いたときの覚え書き

はじめに

Go 言語はほぼ触ったことなかったんですがもうリリースから10年ぐらい経っているらしいです。

AWS Lambda を書くのに少し Go 言語を使ってまた使うかもしれないので覚えたことを書いておきます。

go1.12 のバージョンを使っていました。

まだ少ししか触っていないので色々間違いがあるかもしれません。

またここに書いていることは自分にとって少し分かりにくかったことなどの羅列的なものになります。

CodeCommit や GitLab で使う場合

Go では依存ライブラリを専用のサーバーではなく GitHub などから取得するようになっているようです。

GitHub の場合はあまり問題なく使えると思うんですが CodeCommit やセルフホストしている GitLab などの場合は少し独自の設定が必要なようでした。

これについて詳しく書いているところが見つからず数日はまっていた気がします。

github.com

この issue でなんとなく CodeCommit などでの使い方がわかりました。

デフォルトでは https で依存パッケージを取得するようなんですが、 メタデータを取得する API に対応している必要があって サーバーによってはこれに対応していないので https を使わないようにしてやる必要があるのだと思います。

~/.gitconfig の設定に以下を追加しました。

[url "ssh://git-codecommit.ap-northeast-1.amazonaws.com/"]
    insteadOf = https://git-codecommit.ap-northeast-1.amazonaws.com/

それからモジュール名末尾には .git をいれました。これはいらない場合もあるかもしれません。

go mod init git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/hello.git

パッケージ

下のようにソースの一行目にパッケージの指定があります。

package main

import (
    "fmt"
)

func main() {
    fmt.Println("hello, world")
}

最初は一つのファイルがパッケージと対応しているのかと思っていましたが、 これは間違いで Go のパッケージは一つのディレクトリと対応しているようです。

同じディレクトリにおいたファイルは同一のパッケージにする必要があって、 ファイルが分かれていても同じパッケージ内であれば import せずに関数などを利用できます。

それから同じリポジトリ内であっても import は相対的パスなどではなくホスト名を含むパッケージ名を使うのが普通のようでした。

Lambda を書いたりする場合に CLI ツールとしても使えるように ライブラリのパッケージ、Lambda パッケージ、CLI ツールパッケージと分けたりすることがあるんじゃないかと思います。

例えば下のようなディレクトリ構成になります。

hello/
├── app
│   └── main.go
├── lambda
│   └── main.go
└── lib
    └── hello.go

lib/hello.go はライブラリとして他のパッケージから使われる関数などを含んでいます。 パッケージで公開される関数や定数の名前は大文字で開始するという決まりになっているようです。 これは人によって好み分かれそうですが export を別に書かなくていいのは楽だなと思いました。

package hello

import (
    "fmt"
)

func Hello() {
    fmt.Println("hello, world")
}

app/main.goCLI ツールなどのパッケージで lib/hello.go を import して使います。 import する名前にはソースリポジトリ名にパッケージのディレクトリのパスを追加して指定します。 ライブラリではなくアプリケーションとして実行するパッケージは main パッケージにして main 関数を定義します。

package main

import (
    "git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/hello.git/lib"
)

func main() {
    hello.Hello()
}

go build コマンドで実行ファイルをビルドします。

go build -o hello ./app

go run コマンドでそのまま実行もできます。

go run ./app

スライス

スライスはランダムアクセスできて要素の追加もできるコンテナです。 C++ の std::vector に近いものだと思います。 内部に要素サイズと、確保したサイズ、ポインタを保持するような実装になってるんじゃないかと思います。 std::vector のように要素をメモリ上に連続で確保するようになっていて、足りない場合は新しくメモリを確保して作り直すようです。 下のようなコードでポインタの値が変わっているのが見られました。

package main

import (
    "fmt"
)

func main() {
    a := make([]int, 2,  3)
    fmt.Println(a) // [0 0]

    a[0] = 1
    a[1] = 2
    fmt.Println(a) // [1 2]

    fmt.Printf("%v %p %p\n", a, &a, &a[0]) // [1 2] 0xc00000c060 0xc000014380

    a = append(a, 3)
    fmt.Printf("%v %p %p\n", a, &a, &a[0]) // [1 2 3] 0xc00000c060 0xc000014380

    a = append(a, 4)
    fmt.Printf("%v %p %p\n", a, &a, &a[0]) // [1 2 3 4] 0xc00000c060 0xc0000181b0
}

素数がわかる場合は指定したほうが余分なメモリ確保やコピーが減るので速くなると思います。 これは std::vector で reserve 使ったりするのと同じ感じだと思います。

マップ

マップはキーと値を持つコンテナです。 大体は C++ の std::map と似ているんですがキーはソートされていない構造のようです。 map の要素をイテレートするとき順序が保証されないことに気を付ける必要があります。

下のコードを実行すると順序がバラバラになっているのが見られました。

順序固定する場合はキーを一度取得してソートしてからキーでアクセスします。

package main

import (
    "fmt"
)

func main() {
    a := map[string]int{
        "a": 1,
        "b": 2,
        "c": 3,
        "d": 4,
        "e": 5,
    }

    fmt.Println(a)

    for i := 0; i < 10; i++ {
        for k, v := range a {
            fmt.Printf("%s %d\n", k, v)
        }
    }
}

JSON

golang.org

標準で入っている JSON のライブラリが使えます。 json.MarshalJSON への変換、 json.UnmarshalJSON から struct などの変換ができます。

struct はそのまま何もしなくても JSON にできます。

フィールドにタグを付けて少しカスタマイズすることもできます。

package main

import (
    "encoding/json"
    "fmt"
)

type Person struct {
    Name string `json:"name"`
    Age int `json:"int"`
    PhoneNumber string `json:"phone_number,omitempty"`
}

func main() {
    a := Person{
        Name: "Taro",
        Age: 3,
    }

    bytes, err := json.Marshal(a)
    if err != nil {
        panic(err)
    }

    fmt.Println(string(bytes))

    b := Person{}
    err = json.Unmarshal(bytes, &b)
    if err != nil {
        panic(err)
    }

    fmt.Println(b)
}

独自に変換処理を書きたい場合は MarshalJSON と UnmarshalJSON を定義してやればいいようです。

json - The Go Programming Language

Example (CustomMarshalJSON) の部分に例がありました。

テスト

go test のコマンドで簡単にテスト実行できるようになっています。 hoge_test.go のようなファイル名にして Test ではじまる名前の関数を定義するとテストとして実行されます。

godoc.org

テストで assert を書くのにこのライブラリが便利でした。

package main

import (
    "github.com/stretchr/testify/assert"
    "testing"
)

func TestA(t *testing.T) {
    a := []string{ "aaa", "bbb" }
    b := []string{ "aaa", "BBB", "ccc" }
    assert.Equal(t, a, b)
}

CLI

godoc.org

CLI ツールを書くときに使えるライブラリも色々あるようなんですが cobra というライブラリを少し使いました。

少し使った感じはサブコマンドも書けて、引数やオプションのキャプチャも簡単だったのでよさそうに感じました。

aws-cli を使ったシェルスクリプトなどを書いていてオプションとか扱うのがなかなか大変なので Go で書いて aws-sdk-go 使うのもいいかもなと思いました。

Lambda や CodeBuild でも多分使えると思うので実行もしやすいと思います。

AWS SAM

aws.amazon.com

docs.aws.amazon.com

Go 言語関係無い話ですが Lambda を書くときに AWS SAM を使うと便利でした。

CloudFormation に変換されるテンプレートで Lambda や CloudWatch イベント、API Gateway、IAM ロール などのリソースを定義して AWS SAM のコマンドでパッケージ化とデプロイが簡単にできるようになっています。

ローカル環境で Docker を使って Lambda の挙動をテストすることもできます。

AWS SDK

Go 言語向けに AWS SDK のライブラリが公開されていてこれを使って AWSAPI を色々利用できます。

aws.amazon.com

aws-sdk-go-v2 というライブラリもあるようなんですがまだ Developer Preview の段階でした。

github.com

基本的に API 呼び出すためのライブラリですが一部ユーティリティの機能も追加されていてページネーションに対応した便利な関数などもあるようでした。

godoc.org

テスト用のモックも用意されているようです。

godoc.org

感想

使う前は機能が色々足りてないような印象だったんですが書いてみると意外と書きやすく感じました。

書きにくいと感じたり他の言語でできることができないために自分で書かなければいけないようなコードが増えたりもするんですが、 こういう言語だから仕方ないとあきらめて書いているとあまり悩まずに書ける気がしました。

コンパイラで静的に色々チェックされるのも安心感があって Python とか シェルスクリプトとかで色々書くのよりも自分にはあってる気がしました。

OpenGL を使うテストを CI でするときに osmesa が使えそう

はじめに

前に docker で OpenGL を使う環境を作ってそれでローカル環境では動かせていたんですが CI の環境では GPU が使えなかったり、X Server がない、privilegedのオプションが使えないなど場合もあって そのままでは使えないことが多いので CI でも使える方法を少し調べていました。

テストするときも実際に使うときと同じ環境でやったほうがいいのは間違いないですが、 CI である程度の部分だけでもテストできると嬉しいと思います。

osmesa

Linux 環境では OpenGL はほとんどの場合 GLX から使うようになっています。 同様に Windows の場合は WGL から OpenGL を使います。

GLW と WGL には OpenGL のコンテクストを作成したり、OpenGLAPI のアドレスを取得するインターフェースがあります。

Programming OpenGL in Linux: GLX and Xlib - OpenGL Wiki

CI 環境や GUI がインストールされていないサーバーだと GLX が使えない場合があります。 こうした場合に osmesa が使えるようです。osmesa はソフトウェアレンダリングによる OpenGL 実装です。

www.mesa3d.org

ただ osmesa は OpenGL の機能を完全に実装しているわけではないようで、新し目の glsl バージョンのシェーダをコンパイルできないことがありました。 なので描画に直接関係する部分のテストには使えないかなと思いますが、バッファの確保や解放などリソース管理のテストぐらいはある程度できると思います。

Ubuntu の場合は apt で osmesa を入れることができたので、これを使っています。 (apt のパッケージは libosmesa6-dev という名前でした。)

自分でビルドするともう少し新しい OpenGL バージョンに対応しているかもしれません。

LD_PRELOAD で関数を置き換える

osmesa を使うにはコンテキストを初期化する部分と OpenGL API のアドレスを取得する部分を変更する必要があります。

コンテキストを初期化する部分は独自に実装すればなんとかなりますが、 OpenGL API のアドレスを取得する部分が OpenGL バインディングライブラリ内にあって切り替えられないという問題がありました。

そこで LD_PRELOAD の環境変数を使って glXGetProcAddress を呼び出したときに OSMesaGetProcAddress が使われるようにするという方法を使いました。

動的リンクのライブラリに同じ名前の関数が複数あった場合に先に見つかった関数が使われるため、 LD_PRELOAD でライブラリを読み込ませることで上書きできるようです。

下のようなコードを書いて共有ライブラリを作成して使いました。

typedef unsigned char GLubyte;
typedef void (*genericFunctionPointer)(void);
extern genericFunctionPointer OSMesaGetProcAddress(const GLubyte *);

genericFunctionPointer glXGetProcAddress(const GLubyte* name)
{
    return OSMesaGetProcAddress((const char*)name);
}

genericFunctionPointer glXGetProcAddressARB(const GLubyte* name)
{
    return OSMesaGetProcAddress((const char*)name);
}
gcc -shared -fPIC cbits/glxoverride.c -o libglxoverride.so

GPU を使える場合

github.com

nvidia で公開されている docker コンテナでは GLX のかわりに EGL を使うようになっているようです。 GPU は使えるが X Server はないという場合は EGL が使えそうです。

CircleCI で GPU を使える環境もあるようで予算がある場合はこれもいいかもしれません。

https://circleci.com/docs/ja/2.0/gpu/

CodeDeploy を導入したときに調べたことなど

はじめに

大分前に CodeDeploy について少し調べたことがありました。

tkaaad97.hatenablog.com

その後実際に CodeDeploy を導入する機会があってもう少し色々調べたり試したりしたことがあったのですが、導入したことに満足して調べたことをまとめていませんでした。

しばらく時間がたって忘れてるところもありますが、また使う機会もあるかもしれないのでここに書いておきます。

リビジョン作成

CodeDeploy ではデプロイするソースとして S3 か GitHub が使えます。 しかしデプロイ時に展開するものは GitHub で管理されているソースだけでは無いことが多いので S3 を使うことの方が多いと思います。 スクリプト言語の場合は依存ライブラリなども取得してデプロイする必要があります。 コンパイルする場合バイナリは Git 管理しないことが多いと思います。

S3 にファイルをアップロードしてリビジョンを作成するためのコマンドがあります。

docs.aws.amazon.com

しかしこのコマンドではあまり細かい制御ができないため結局使いませんでした。 隠しファイルを含めるかどうかというオプションがあるのですが一部だけ含めたりという指定ができませんでした。

dev.classmethod.jp

こちらの記事を参考に aws deploy push に相当する処理を行うスクリプトを作成してリビジョン作成を行うようにしました。

場合によっては CodeDeploy でのインストール処理とは別に Ansible Playbook などを実行して独自にインストール処理を行うということもあるかもしれません。 この場合は CodeDeploy でインストールするのは Ansible のファイルだったりリビジョンに含めるものは変わってくると思います。

ソース配置ディレクトリをアプリケーションによって変えたい

CodeDeploy でどのディレクトリにソースを配置するかは appspec.yml で指定します。 files の destination の部分です。

docs.aws.amazon.com

あまり普通はやらないし推奨されないと思いますが 複数のアプリケーションを同一のサーバーにデプロイする場合 ソース配置ディレクトリが同じになってしまいます。

アプリケーションごとに appspec.yml を変えるといった方法は無いようでした。 しかしリビジョン作成時に appspec.yml を書き換えるという無理やりな方法で一応は目的のことができました。

デプロイグループごとにデプロイ処理を変えたい

同じソースをデプロイするサーバーでも色々と役割が違うものがあります。 例えばウェブサーバーがあったり、分析や集計などを行うサーバーがあったりして、 役割ごとにデプロイグループを分けるのが普通だと思います。

デプロイグループごとに変えるには CodeDeploy で提供されている環境変数を使うことができます。

docs.aws.amazon.com

APPLICATION_NAME と DEPLOYMENT_GROUP_NAME の環境変数があるので これに基づいて設定ファイルを読み込んだり、AWS Systems Manager パラメータストアから設定を取得し デプロイの処理を変えることができます。

オートスケーリンググループに複数のデプロイグループを付けられるか

複数のデプロイグループに同じオートスケーリンググループを設定することはできるんですが、 試したところこれはやめたほうがいいということが分かりました。

問題はオートスケーリングでインスタンスが新しく追加されたときに 両方のデプロイグループのデプロイ処理が同時に動いて正常に動作しないという点でした。 オートスケーリングを使っていない EC2 タグでデプロイを行っている場合はそんなに問題ないかもしれません。 同時にデプロイしたりするとエラー起こりそうですが。

デプロイごとに処理を変えたい

複数デプロイグループを作ろうとしたのは同じ役割のサーバーでもデプロイを行うときに処理を変えたい場合があったからでした。 先に書いたように複数デプロイグループで分岐するのは上手くいかなかったため、 リビジョン作成時にファイルを追加してこのファイルを読んで処理を分岐するというような方法を使いました。

オートスケーリンググループに複数のロードバランサーが付いている場合

複数のロードバランサーには CodeDeploy は対応していないようです。 複数のロードバランサーを使うのは諦めるか、自前でロードバランサー関連の処理を行うことになります。

オートスケーリンググループのインスタンスは全てをロードバランサーに付けるか外すかの二択かと勘違いしていたのですが、 オートスケーリンググループ中の個々の EC2 インスタンスごとにロードバランサーから一時的に外したりということができます。

docs.aws.amazon.com docs.aws.amazon.com

インプレースデプロイの場合はアプリケーションストップ時にロードバランサーから一時的に外し、 デプロイが終わってアプリケーションスタート時にロードバランサーに付けるという感じのことができるようです。

この処理については GitHub に公開されていたサンプルが参考になりました。

github.com

Blue/Green デプロイの場合はオートスケーリンググループ自体が複製されてインスタンスも全て入れ替わるということになるらしいです。 この場合ターゲットグループやロードバランサーとの紐付けもそのまま引き継がれるのかどうか疑問ですがインプレースデプロイを使うことにしたため詳しく調べていません。

シンボリックリンクが壊れるバグ

github.com github.com

シンボリックリンクが通常のファイルに変わってしまうという問題が起こる場合があるようです。 この挙動で問題が起こる場合はデプロイ処理内でシンボリックリンクを作るようにするなどの対応が必要になりそうです。

導入してみての感想など

少し融通がきかないところがあったり、色々と疑問に思ったところがドキュメントに書かれていなかったりして 全く不満が無いわけではないですが CodeDeploy まあまあ便利だと思います。 CodeDeploy 前に使っていたデプロイ方法が色々と問題があったためそう感じるというところもあると思います。

便利と感じるところはオートスケーリングと一緒に使えて新規インスタンス追加時に色々な処理が行えるところや、 デプロイの履歴やインスタンスごとに起こったエラーなどが簡単に調べられることなどです。