AWS CodeDeploy で ELB のターゲットになっていない EC2 でも勝手に追加されてしまう

AWS CodeDeployを使っていて少しハマったのでメモとして残しておきます。

CodeDeployでデプロイする場合、どこのコードを使ってどんな方法でどこにデプロイするか色々なパターンがあります。

  • EC2 or オンプレ
  • GitHub or S3
  • インプレース or Blue/Green
  • Elastic Load Balancing タイプ or そのまま

このうち、ELBタイプは、デプロイの際にデプロイ対象のインスタンスをELBのターゲットから外した上でインストール等を行い完了したらターゲットへ戻すような方法です。
この方法によって、ユーザアクセスの瞬間にアプリケーションが壊れていたり不完全だったりといったことがなく、安全にデプロイできます。

さて、そのELBタイプのデプロイですが、デプロイグループの対象のインスタンスではあるけれど、ELBのターゲットにはなっていないインスタンスがあったときに、どうなるのか?ということがありました。
さすがに、ELBのターゲットに入っていなければ、インスタンスにとってトラフィックのブロックも何も無いわけで、デプロイの対象にならないか、もしくはただアプリケーションのインストールが行われて終わるかのどちらかかと考えていました。
そう思って実行したところ、ELBのターゲットに入っていないにも関わらず、普通にトラフィックのブロックが行われ、インストールが行われ、最後にはELBのターゲットに勝手に追加されてしまいました!
そういうものなのか微妙な感じもしますが、少しハマってしまいました。

これに関して特に記述は無いような docs.aws.amazon.com

とりあえずAuto Scaling使いますか...

AWSのAuroraからGCPのBigQueryにEmbulkを使ってデータを転送する

AWSのAuroraからGCPのBigQueryにデータを転送する必要があり、最終的にEmbulkを使うことに落ち着いた話です。

最初は、AWS Data Pipelineを使ってAuroraからS3にエクスポートしてBigQuery Data Transfer Serviceを使ってS3からBigQueryにインポートするフローにする予定でしたが、全体的に少し辛かったので別の方法を検討していました。
そうしたら、Embulkを使った方法があまりにも簡単すぎたので、こちらを採用することにしました。

Embulk

Embulkとはデータ転送のためのツールで、様々なストレージやDBやクラウドサービス間でデータ転送を実現できます。

環境構築

基本的に、公式通りにやりました。GitHub - embulk/embulk: Embulk: Pluggable Bulk Data Loader.

Ubuntu 18.04で動作確認済みです。

Embulkの動作に必要なJava8とEmbulk自身をインストールします。

$ sudo apt install openjdk-8-jdk
$ sudo curl -o /usr/local/bin/embulk -L "https://dl.embulk.org/embulk-latest.jar"
$ sudo chmod 755 /usr/local/bin/embulk

今回はAuroraからBigQueryに転送したいので、それぞれのプラグインを用意します。

$ cd ~
$ embulk gem install embulk-input-mysql embulk-output-bigquery

以上です。

BigQueryの設定

GCP上でEmbulk実行用のサービスアカウントを作成し、そのアカウントに「BigQuery データ編集者」「BigQuery ジョブユーザー」のロールを与えます。
そして、そのサービスアカウントのJSON鍵をダウンロードしておきます。

設定ファイル作成

今回は全更新と差分挿入のいずれも必要なので両方に対応しました。

まず、全体として同じような設定を共通化します。
サンプルとして、本当に必要最低限の設定にしています。

$ cat common/_in_mysql.yml.liquid
in:
  type: mysql
  host: {{ env.DB_HOST }}
  user: {{ env.DB_USER }}
  password: {{ env.DB_PASS }}
  database: {{ env.DB_DATABASE }}
  default_timezone: Asia/Tokyo

$ cat common/_out_bigquery.yml.liquid
out:
  type: bigquery
  auth_method: service_account
  json_keyfile: {{ env.GCP_CREDENTIALS }}
  dataset: {{ env.BIGQUERY_DATASET }}
  default_timezone: Asia/Tokyo

続いて、データ転送対象のテーブルの設定をします。
指定の方法は何通りかありますが、特別なwhere句が必要ない場合は in > select で対象のカラムを抽出するようにして、差分挿入などwhere句が必要な場合は in > querySQLをそのまま記述しています。

$ cat replace_table.yml.liquid
in:
  {% include 'common/in_mysql' %}
  table: replace_table_name
  select: id,title,created_at
out:
  {% include 'common/out_bigquery' %}
  table: replace_table_name
  mode: replace

$ cat insert_table.yml.liquid
in:
  {% include 'common/in_mysql' %}
  query:
    SELECT
      id,title,created_at
    FROM
      insert_table_name
    WHERE
      DATE_FORMAT(created_at, '%Y-%m-%d') = '{{ env.TARGET_DATE }}'
out:
  {% include 'common/out_bigquery' %}
  table: insert_table_name
  mode: append

実行

まずは設定の際にenvに記述したものを、以下の方法などで環境変数に入れておく必要があります。

$ export DB_HOST=xxxxxxxxxxxxxx.rds.amazonaws.com
$ export DB_USER=db_user
$ export DB_PASS=db_password
$ export DB_DATABASE=database
$ export GCP_CREDENTIALS=/home/user/key/bigquery-key.json
$ export BIGQUERY_DATASET=bigquery_dataset
$ export TARGET_DATE=`date -d yesterday "+%Y-%m-%d"`

そして、実行します。

$ /usr/local/bin/embulk run replace_table.yml.liquid
$ /usr/local/bin/embulk run insert_table.yml.liquid

デフォルトだとBigQuery側にテーブルを用意していなくても自動で作成してくれるので、このように実行するだけで完了です。

簡単すぎてビビりました。

AWSのData PipelineでAuroraを使ったらDriverClass not foundが出るときの解消

AWSのData Pipelineを使って、AuroraからS3でテーブルをまるっとエクスポートしようというときがありました。
テンプレートとして「Full copy of RDS MySQL table to S3」が用意されているので、さくっとできるかと思っていましたが、そうでもありませんでした。

必要な値を入れてPipelineを作り、実行したら、以下のエラーが出て失敗になりました。

DriverClass not found for database:aurora

このエラー文言でググると以下のAWSのサポートページがヒットします。 aws.amazon.com

このページの通り、Auroraを使う場合にはJDBCドライバーを用意する必要があるようです。
素直に言われた通りに設定すれば特に問題なく実行できるようになるのですが、一点だけ少し詰まったので記録しておきます。

サポートページには「MySQLJDBC ドライバーをダウンロードします。」と書かれていて、現時点(2019/09/30)ではそのリンク先は「Connector/J 8.0.17」になっています。
そのページでPlatform Independentを選び、ダウンロードして、AWSのサポートページ通りに進めて実行すると以下のエラーが出ました。

com/mysql/jdbc/Driver : Unsupported major.minor version 52.0

よくよく調べるとJDBCドライバーのバージョンが良くないようで、https://dev.mysql.com/downloads/connector/j/5.1.htmlなら大丈夫でした。

GUIでクリックを自動化したかったのでPythonのPyAutoGUIを使ってみた

とある理由でPC上でのクリックを自動化したかったため、色々ツールを調べていました。
Windowsだといい感じのツールはあるようですが、Macだと手軽に使えて良い感じのツールは無さそうです。

ということで、今回はクリック自動化に求める内容もすごく単純なものだったため、プログラムで簡単に書いてみました。

使ったのはこちらです。
pyautogui.readthedocs.io

言われた通りにインストールするだけで使えます。

pip3 install pyobjc-core pyobjc pyautogui

例えば、座標 (100, 200) のポイントをクリックしようと思ったら、以下のように記述します。
座標はスクリーンの左上が原点です。

import pyautogui
pyautogui.click(x=100, y=200)

今回必要だった処理はひたすらクリックし続けるものなので、以下のように書きました。
一応sleepを挟んでいます。

import time
import pyautogui

time.sleep(2)

while True:
    pyautogui.click(x=100, y=200)
    time.sleep(0.1)

これで、0.1秒間隔に座標 (100, 200) をひたすら連打し続けるプログラムができました。
最初の time.sleep(2) は、このプログラム実行後に操作したい画面を開いて準備するための猶予時間として設けてあります。
クリック連打がこれで簡単に実現できました。

ただ、こいつには一つだけ問題があります。
プログラムを止めようと思って Ctr+C を押そうと思っても、 Ctr の直後にクリック判定が入ってしまうと、 Ctr+CLICK つまり右クリックになってしまいます。
その結果、ずっと右クリックメニューが開いたり閉じたりするという、絶妙にめんどくさい感じになっています。

もしかすると、ループのスリープをなくしたり極端に短くすると、延々に止められない(人の限界を超えてくる!)かもしれないので、やるときは自己責任でお願いします。

PyAutoGUIはもっと細かいマウス操作まで行えるだけでなく、キーボード操作や、ディスプレイボックス、スクリーンショットまで扱えるようです。

もっと凝った自動操作をやりたくなったときにまた色々試してみたいと思います。

あとから Role をアタッチしてしまって CodeDeploy に失敗する場合は codedeploy-agent を再起動すれば良い

タイトルの通りです。

EC2インスタンスを作成した後で、CodeDeployを使いたくなり、色々設定してやってみてもなかなかデプロイに成功してくれませんでした。

必要なRoleはアタッチしているはずですが、以下のようなエラーが出続けました。

The overall deployment failed because too many individual instances failed deployment, too few healthy instances are available for deployment, or some instances in your deployment group are experiencing problems.

結果的には、 codedeploy-agent を再起動するだけで解決しました。

sudo service codedeploy-agent restart

AWSのドキュメントの通りに順番にやっていれば、Roleをアタッチしたあとでエージェントをインストールする流れになっているのでハマることは無いはずなのですが、ドキュメントを無視してしまっていることが垣間見えます... docs.aws.amazon.com

google-api-php-client でどんな通信が行われているか見るために Guzzle の Middleware を使う

PHPGoogle Play Developer APIを使ってAndroidのレシートを検証したいときは、こんな感じでやればOKです。 ohshige.hatenablog.com

APIを実データで叩くだけなら問題ないのですが、モック化したいということがありました。
モック化のやり方は良いとして、そもそもgoogle-api-php-client内部でどういうデータのやり取りをしているのか、よく理解できていません。
そこで、そのデータのやり取りを覗いてみることにしました。

google-api-php-clientではAPI通信の処理でGuzzleクライアントが使われています。
しかも、そのGuzzleクライアントは自由にカスタマイズできます。

まず、google-api-php-clientで使われるGuzzleクライアントを明示的に指定する方法は以下の通りです。

$http = new \GuzzleHttp\Client();

$client = new \Google_Client();
$client->setHttpClient($http);

これだけで明示的に指定できるので、指定するGuzzleクライアント経由でリクエストとレスポンスを確認できそうです。

Guzzleクライアント経由でリクエストとレスポンスを見るために、Middlewareを使います。 docs.guzzlephp.org

以下のように、リクエストとレスポンスをver_dumpするようなコールバック関数をGuzzleクライアントに指定して、それをGoogle_Clientに設定すれば、どんな通信がなされているか簡単にわかります。

$stack = new \GuzzleHttp\HandlerStack();
$stack->setHandler(\GuzzleHttp\choose_handler());
$stack->push(function (callable $handler) {
    return function (
        \Psr\Http\Message\RequestInterface $request,
        array $options
    ) use ($handler) {
        var_dump("<============================== request start ==============================>");
        var_dump($request);
        $promise = $handler($request, $options);
        return $promise->then(
            function (\Psr\Http\Message\ResponseInterface $response) {
                var_dump($response->getBody()->getContents());
                var_dump("<============================== request end ==============================>");
                return $response;
            }
        );
    };
});

$http = new \GuzzleHttp\Client(['handler' => $stack]);

$client = new \Google_Client();
$client->setHttpClient($http);

上記の設定後、Google Play Developer APIを呼ぶための準備をして、あるサブスクリプションのレシート検証をしてみると以下のような出力があります。

$client->setAuthConfig([認証用JSONを設置したパス]);
$client->setRedirectUri('http://localhost');
$client->setAccessType('offline');
$client->setApprovalPrompt('force');
$client->addScope(\Google_Service_AndroidPublisher::ANDROIDPUBLISHER);
$client->refreshToken([リフレッシュトークン]);
 
$publisher = new \Google_Service_AndroidPublisher($client);
$publisher->purchases_subscriptions->get([packageName], [productId], [purchaseToken]);
string(77) "<============================== request start ==============================>"
object(GuzzleHttp\Psr7\Request)#23 (7) {
  ["method":"GuzzleHttp\Psr7\Request":private]=>
  string(4) "POST"
  ["requestTarget":"GuzzleHttp\Psr7\Request":private]=>
  NULL
  ["uri":"GuzzleHttp\Psr7\Request":private]=>
  object(GuzzleHttp\Psr7\Uri)#18 (7) {
    ["scheme":"GuzzleHttp\Psr7\Uri":private]=>
    string(5) "https"
    ["userInfo":"GuzzleHttp\Psr7\Uri":private]=>
    string(0) ""
    ["host":"GuzzleHttp\Psr7\Uri":private]=>
    string(21) "oauth2.googleapis.com"
    ["port":"GuzzleHttp\Psr7\Uri":private]=>
    NULL
    ["path":"GuzzleHttp\Psr7\Uri":private]=>
    string(6) "/token"
    ["query":"GuzzleHttp\Psr7\Uri":private]=>
    string(0) ""
    ["fragment":"GuzzleHttp\Psr7\Uri":private]=>
    string(0) ""
  }
  ["headers":"GuzzleHttp\Psr7\Request":private]=>
  array(4) {
    ["User-Agent"]=>
    array(1) {
      [0]=>
      string(69) "GuzzleHttp/6.3.3"
    }
    ["Host"]=>
    array(1) {
      [0]=>
      string(21) "oauth2.googleapis.com"
    }
    ["Cache-Control"]=>
    array(1) {
      [0]=>
      string(8) "no-store"
    }
    ["Content-Type"]=>
    array(1) {
      [0]=>
      string(33) "application/x-www-form-urlencoded"
    }
  }
  ["headerNames":"GuzzleHttp\Psr7\Request":private]=>
  array(4) {
    ["user-agent"]=>
    string(10) "User-Agent"
    ["host"]=>
    string(4) "Host"
    ["cache-control"]=>
    string(13) "Cache-Control"
    ["content-type"]=>
    string(12) "Content-Type"
  }
  ["protocol":"GuzzleHttp\Psr7\Request":private]=>
  string(3) "1.1"
  ["stream":"GuzzleHttp\Psr7\Request":private]=>
  object(GuzzleHttp\Psr7\Stream)#21 (7) {
    ["stream":"GuzzleHttp\Psr7\Stream":private]=>
    resource(93) of type (stream)
    ["size":"GuzzleHttp\Psr7\Stream":private]=>
    NULL
    ["seekable":"GuzzleHttp\Psr7\Stream":private]=>
    bool(true)
    ["readable":"GuzzleHttp\Psr7\Stream":private]=>
    bool(true)
    ["writable":"GuzzleHttp\Psr7\Stream":private]=>
    bool(true)
    ["uri":"GuzzleHttp\Psr7\Stream":private]=>
    string(10) "php://temp"
    ["customMetadata":"GuzzleHttp\Psr7\Stream":private]=>
    array(0) {
    }
  }
}
string(267) "{
  "access_token": "__access_token__",
  "expires_in": 3600,
  "scope": "https://www.googleapis.com/auth/androidpublisher",
  "token_type": "Bearer"
}"
string(75) "<============================== request end ==============================>"
string(77) "<============================== request start ==============================>"
object(GuzzleHttp\Psr7\Request)#49 (7) {
  ["method":"GuzzleHttp\Psr7\Request":private]=>
  string(3) "GET"
  ["requestTarget":"GuzzleHttp\Psr7\Request":private]=>
  NULL
  ["uri":"GuzzleHttp\Psr7\Request":private]=>
  object(GuzzleHttp\Psr7\Uri)#41 (7) {
    ["scheme":"GuzzleHttp\Psr7\Uri":private]=>
    string(5) "https"
    ["userInfo":"GuzzleHttp\Psr7\Uri":private]=>
    string(0) ""
    ["host":"GuzzleHttp\Psr7\Uri":private]=>
    string(18) "www.googleapis.com"
    ["port":"GuzzleHttp\Psr7\Uri":private]=>
    NULL
    ["path":"GuzzleHttp\Psr7\Uri":private]=>
    string(278) "/androidpublisher/v3/applications/[packageName]/purchases/subscriptions/[productId]/tokens/[purchaseToken]"
    ["query":"GuzzleHttp\Psr7\Uri":private]=>
    string(0) ""
    ["fragment":"GuzzleHttp\Psr7\Uri":private]=>
    string(0) ""
  }
  ["headers":"GuzzleHttp\Psr7\Request":private]=>
  array(3) {
    ["Host"]=>
    array(1) {
      [0]=>
      string(18) "www.googleapis.com"
    }
    ["content-type"]=>
    array(1) {
      [0]=>
      string(16) "application/json"
    }
    ["User-Agent"]=>
    array(1) {
      [0]=>
      string(27) "google-api-php-client/2.2.3"
    }
  }
  ["headerNames":"GuzzleHttp\Psr7\Request":private]=>
  array(3) {
    ["host"]=>
    string(4) "Host"
    ["content-type"]=>
    string(12) "content-type"
    ["user-agent"]=>
    string(10) "User-Agent"
  }
  ["protocol":"GuzzleHttp\Psr7\Request":private]=>
  string(3) "1.1"
  ["stream":"GuzzleHttp\Psr7\Request":private]=>
  object(GuzzleHttp\Psr7\Stream)#50 (7) {
    ["stream":"GuzzleHttp\Psr7\Stream":private]=>
    resource(136) of type (stream)
    ["size":"GuzzleHttp\Psr7\Stream":private]=>
    NULL
    ["seekable":"GuzzleHttp\Psr7\Stream":private]=>
    bool(true)
    ["readable":"GuzzleHttp\Psr7\Stream":private]=>
    bool(true)
    ["writable":"GuzzleHttp\Psr7\Stream":private]=>
    bool(true)
    ["uri":"GuzzleHttp\Psr7\Stream":private]=>
    string(10) "php://temp"
    ["customMetadata":"GuzzleHttp\Psr7\Stream":private]=>
    array(0) {
    }
  }
}
string(374) "{
 "kind": "androidpublisher#subscriptionPurchase",
 "startTimeMillis": "1111111111111",
 "expiryTimeMillis": "9999999999999",
 "autoRenewing": false,
 "priceCurrencyCode": "JPY",
 "priceAmountMicros": "0000000000000",
 "countryCode": "JP",
 "developerPayload": "",
 "cancelReason": 1,
 "orderId": "XXXXXXXXXXXXXXXXXXXXXXXXXX",
 "purchaseType": 0,
 "acknowledgementState": 1
}
"
string(75) "<============================== request end ==============================>"

これを見ると2回の通信が行われていることがわかり、1回目はアクセストークンの取得、2回目は実際のpurchaseTokenの検証を、それぞれ実行しています。
リクエストもレスポンスもよくわかり、モック化の参考になります。

リフレッシュトークンの設定を外せば、2回目のAPIだけが呼ばれることになります。
モック化にあたっては、その2回目のAPIだけを考慮すれば良さそうです。

GuzzleのMiddlewareは便利で、単に通信を覗き見るだけでなく、ヘッダーを常に付与したり、レスポンスを変換したり、色々なことが可能になります。

AWS CodeDeploy で登録した GitHub トークンを削除したい

AWS CodeDeployは、EC2、Lambda、オンプレサーバなどへのデプロイを自動化してくれるサービスです。

デプロイのリビジョンタイプとして、アプリケーションをS3に置いておくかGitHubに置いておくか選ぶことができます。
GitHubに置いておく場合はCodeDeployがGitHubにアクセスする必要があるので、GitHubアカウントに紐付けたトークンの登録をする必要があります。

CodeDeployのデプロイ設定のページで「GitHubに接続」を選択すれば、アカウントの紐付けをするかどうかの確認のポップアップが現れるので、簡単に設定できます。

ただ、最初に試行錯誤しながら設定をしていると何回もアカウントを紐付けてしまい、トークンがいくつもできてしまうことがあります。
そのまま進めてもいいのですが、キリの良いタイミングでトークンの整理をしたいところです。

このトークンはGUIでは削除することができないため、CLIで削除する方法をまとめます。
いずれも権限が必要ですが、問題ないなら AWSCodeDeployFullAccess ポリシーをアタッチするのが楽です。

トークンの一覧表示

awsコマンドで以下のように実行すると一覧表示ができます。
実行には codedeploy:ListGitHubAccountTokenNames の権限が必要です。

$ aws deploy list-git-hub-account-token-names
{
    "tokenNameList": [
        "test",
        "testtest",
        "test-token",
        "mytest",
        "codedeploy-token"
    ]
}

トークンの削除

awsコマンドで以下のように実行するとトークンを削除することができます。
実行には codedeploy:DeleteGitHubAccountToken の権限が必要です。

aws deploy delete-git-hub-account-token --token-name test-token
{
    "tokenName": "test-token"
}

参考
docs.aws.amazon.com