AWS EC2にインスタンス一覧から選択してSSHできるスクリプトを作った

EC2 に SSH する度に AWS コンソールからインスタンスの詳細選んで Elastic IP をコピペしてキーファイルを指定して実行というのが怠いので便利スクリプトを作ってみた。

単に SSH

第1引数にプロファイル名を指定します。 ~/.aws/credentials に定義されている中から選択します(引数なしで実行するとプロファイルがわかります)。

$ ec2ssh profile1
0) server-dev   60.100.45.XXX  i-abcdefgx  keypair-dev
1) server-stg   60.100.45.YYY  i-abcdefgy  keypair-stg
2) server-prod  60.100.45.ZZZ  i-abcdefgz  keypair-prod
Input target EC2 number> 2
Last login: Wed Jan 20 15:19:00 2016 from xxxx.xxxx.xxx.xxx

       __|  __|_  )
       _|  (     /   Amazon Linux AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-ami/2015.09-release-notes/
[ec2-user@ip-10-0-0-1 ~]$

ファイルをEC2からローカルにコピー

EC2 の /tmp/log.tgz をローカルの ~/work/ にコピーする。 引数に get, EC2内ファイルパス, ローカルコピー先パス の順に指定する。 コピー先が未指定ならカレントディレクトリになる。

$ ec2ssh profile1 get /tmp/log.tgz ~/work/

ファイルをローカルからEC2にコピー

カレントディレクトリの batch.sh を EC2 の /tmp にコピーする 引数に put, ローカルファイルパス, EC2内コピー先パス の順に指定する。 コピー先が未指定なら ec2-user のホームディレクトリになる。

$ ec2ssh profile1 put batch.sh /tmp

スクリプト

https://gist.github.com/tilfin/4cdca4311b2585ed91e8

  • AWS CLIjq に依存しています。
  • 各キーペアpemファイルは、AWSコンソールから取得したときのファイル名のまま ~/.ssh/ に置く。
  • ~/.ec2ssh-pre を置くと ssh, scp 前に任意のコマンドを実行できます。
  • ~/.ec2ssh-post を置くと ssh, scp 後に任意のコマンドを実行できます。
#!/bin/bash
#---------------------------------------------
# EC2 SSH
#
# select target from instace list
#---------------------------------------------
#
#  ssh example) $ ec2ssh profile1
#
#  download file example)
#  $ ec2ssh profile1 get /tmp/log.tgz ~/work/
#
#  upload file example)
#  $ ec2ssh profile1 put batch.sh /tmp
#
#
#  If you want to hook pre and post command,
#  put ~/.ec2ssh-pre or ~/.ec2ssh-post
#  pre|post script is called with arguments:
#     $1 = EC2 IP Address
#     $2 = profile
#     $3 = action
#     $4 = source path
#     $5 = destination path
#
#---------------------------------------------

EC2USER=ec2-user
ALIVEINTERVAL=30


profile=$1
action=$2
src=$3
dest=$4
if [ "$profile" == "" ]; then
  name=`basename $0`
  echo "Usage: $name <aws profile> [action] [src] [dest]"
  echo -n "  aws profiles)"
  for pf in `grep -e "\[[a-z\-]*\]" ~/.aws/credentials | sed 's/\[//;s/\]//'`
  do
    echo -n " $pf"
  done
  echo -e "\n  action) get put"
  exit 1
fi


iplist=()
keylist=()
index=0

IFS=$'\n'
for line in `aws ec2 describe-instances --profile $profile \
  | jq '.Reservations[].Instances[] | {InstanceId, PublicIpAddress, PrivateIpAddress, KeyName, InstanceName: (.Tags[] | select(.Key=="Name").Value)}' 2> /dev/null \
  | jq 'sort_by(.InstanceName) | .[]' --slurp \
  | jq -r '[.PublicIpAddress, .InstanceId, .InstanceName, .KeyName] | @tsv'`
do
  IFS=$'\t'
  item=( $line )
  IFS=$'\n'

  iplist+=( ${item[0]} )
  keylist+=( ${item[3]} )
  echo -e "$index) ${item[2]}\t${item[0]}\t${item[1]}\t${item[3]}"

  index=$((index + 1))
  count=$index
done

if [ $count -eq 0 ]; then
  echo "Not found EC2 instances"
  exit 1
fi

echo -n "Input target EC2 number> "
read INPUT
index=$INPUT

if [ "$index" != "" ] && [ $index -lt $count ]; then
  hostip=${iplist[$index]}
  pemfile=${keylist[${index}]}

  if [ -f ~/.ec2ssh-pre ]; then
    . ~/.ec2ssh-pre $hostip $profile $action $src $dest
  fi

  if [ "$action" == "get" ]; then
    if [ "$dest" == "" ]; then
      dest="."
    fi

    scp -o StrictHostKeyChecking=no -i ~/.ssh/${pemfile}.pem $EC2USER@$hostip:$src $dest
  elif [ "$action" == "put" ]; then
    if [ "$dest" == "" ]; then
      dest="~"
    fi

    scp -o StrictHostKeyChecking=no -i ~/.ssh/${pemfile}.pem $src $EC2USER@$hostip:$dest
  else
    ssh -o ServerAliveInterval=$ALIVEINTERVAL \
        -o StrictHostKeyChecking=no \
        -l $EC2USER -i ~/.ssh/${pemfile}.pem $hostip
  fi

  if [ -f ~/.ec2ssh-post ]; then
    . ~/.ec2ssh-post $hostip $profile $action $src $dest
  fi
fi

Qiitaにクロスポストしてみるか。

Mongoose で完全なる ES6 Promise を使う

現行の Mongoose 4.2.4 ではネイティブ Promise がサポートされていない。Query オブジェクトの exec メソッドを引数無しで呼び出すと、Mongoose 独自の Promise が返るがチェインの中で利用すると上手く動かなかった。

http://mongoosejs.com/docs/api.html#promise_Promise

リンク先に Mongoose 5.0 でネイティブ Promise がサポートされるとあるが今使いたい。そこで Query オブジェクトに execPromise メソッドを拡張実装してみる。

var mongoose = require('mongoose');

mongoose.Query.prototype.execPromise = function() {
  var self = this;
  return new Promise(function(resolve, reject){
      self.exec(function(err, data){
        if (err) reject(err);
        else resolve(data);
      });
    });
}

以下のようにチェインで使うことができる。

// fetch an author who writes an article
var promise = Article.find({ id: 1 }).execPromise();

promise.then(function(article){
  return Author.find({ id: article.author }).exePromise();
})
.then(function(author){
  console.log(author);
})
.catch(function(err){
  console.log(err);
});

Elastic Beanstalk へ CI からデプロイ時に特定コンフィギレーションをスキップするには

CI サービスから自動的に AWS Elastic Beanstalk にアプリケーションをデプロイしたいときに .ebextensions フォルダに置いたコンフィギレーションファイルをスキップさせたいときがある。

awsebcli の eb deploy コマンドでは、Git リポジトリのカレントブランチの HEAD コミットをアーカイブしてアップロードする。Elastic Beanstalk (以下、EB) の仕様上、EB 環境毎に EC2 インスタンスの設定(.ebextensions)をデプロイ処理時に切り分けるには、container_commands において環境変数を参照して行うしかない。それ以前に走る files や Resources の設定は固定になってしまう。

eb deploy コマンドの --staged オプションを指定すると、Git リポジトリの staged 状態を反映してアーカイブを作成してくれる。 つまり特定の設定ファイルをスキップしたければ git rm .ebextensions/skip.config を行ってから、eb deploy --staged target-eb-env を叩くようにすれば良いのだ。

例えば CI サービスの設定 yaml などに次のように定義する。

- git rm .ebextensions/skip.config
- eb deploy --staged target-eb-env

特定の処理を追加したいときは別の場所に設定ファイルを用意しておいて、git mv で .ebextensions フォルダに移動すると良いだろう。

- git mv other-ebextensions/add.config .ebextensions
- eb deploy --staged target-eb-env

iOS 9 のコンテンツブロックで始まる HTTP 2.0 時代の Web 広告

iOS 9 から Safari の Extension 機能としてコンテントブロックの実装が可能になった。

developer.apple.com

Web ページ内の特定の HTML 要素を見えなくしたり、特定のURLのロードをブロックする仕組みを Apple は ブラウザ Safari に提供した、それだけである。しかしこれにより Web ページの広告やトラッカーといったJavaScriptの読み込みを遮断する Safari 拡張アプリの実装が可能になることから、実質 Apple が Web 広告を潰しにきたと iOS 9 発表当時から話題になった。

実際 iOS 9 リリースとともに広告・トラッカーをブロックするアプリがストアに並び、ランキングの上位に躍り出ることになった。以前から Apple が Web 広告を蔑ろにしていることは、iOS 5Safari に追加したリーダー(Web ページの記事の文章だけにフォーカスする)機能などから伺えた。ただこれは読み込んだページに対して機能することと、ブログなど Safari が記事ページと判断したときだけ有効になるので、広告潰しとして話題になることはなかった。

少し話が逸れるがニュースアプリの SmartNews がリリースされた当初、ネットワークがオフラインでも先読みしてニュース記事の文章だけを表示するという機能が物議を醸したことがあった。その後 SmartNews はメディアと協業していく形で成功している。

ブラウザに広告ブロック機能が追加できることは目新しいことではない。PC ブラウザではアドオン・拡張機能として古くからある。昔から言われているのはこういったアドオンをインストールする人は、そもそも広告を踏むことはないので収入には影響がないという話だ。概ね正しいと思うが、厳密には広告インプレッションは減ってしまう。

これまでと状況が異なるのはブラウザのアドオンマーケットと違い、普通に iPhone を使う人がその存在を認知できる形で提供されることだ。無料アプリであればソムリエな学生の口コミで瞬く間に普及するシナリオは大いにあり得る。ユーザーからすれば広告を消すことにデメリットはない。通信量も減りブラウジングが軽くなるのは事実で、特にバッテリーで動作するスマホには恩恵が大きい。過去に Android もブロックアプリがストアに並んでいたが、広告収入がメインの Google は一斉に排除している。

もし無視できないほど広告ブロックアプリが普及したらどうなるのか。iOSSafari は今のネットにおいてトップシェアを誇るため、確実に収益減少に繋がる。その一方で、ブロック機能をオフにしないとサイトの閲覧をさせないようにする方法はある。サイトに広告が表示できたかどうか検証するスクリプトを直接ページに埋め込み、ブロックされた場合にコンテンツを隠すようにできるからだ。コンテンツに自信のあるサイトは、ブロックアプリを排除するか会員制に移行するか選択するだろう。

ちなみにトラッカーまでブロックされるとステルスアクセスになり、Google Analytics でページビューを把握してると単に訪問者が減ったように見える。Web サーバの HTTP アクセスログと照らし合わせないとわからない。

ここで立ち戻って、なぜ広告をブロックしたい状況なのか考える。以下の2点が大きい。

  • 鬱陶しく広告の載せているサイトがある。スマホでは成人向けの広告が一般サイトで表示されることも多い。
  • 広告・トラッカーのスクリプトをクライアントサイドでロード・処理しているため、ユーザー負担が大きい。

一方でサイト運営者はなぜ広告を掲載するのか大きく分けると以下の2つだ。

  • 商用 Web サイトで収益を上げるための手段として広告掲載する。
  • フリー Web サイトを維持するインフラコストを賄うため広告を掲載する。

今後望ましい状況を考えてみる。

  • 健全に広告を表示していないサイトが淘汰されること。
  • ユーザーが負担する広告配信(スクリプト)の処理が無くなる(減る)こと。
  • 維持費を広告で賄うフリーで情報提供しているサイトが残ること。

1 番目の事項は、広告配信業界が自己解決すべき問題だと思うのでこれ以上扱わない。エンジニアとして 2,3 番目の事項の解決策を考えてみたい。

基本的に Web ページは読み込み毎にスクリプトのロードと実行が走る。スクリプトの特性上、scriptタグで指定したものをトリガーに五月雨式に続くスクリプトをロードできるので、複数の外部サーバに接続しロードする構成が広告・トラッカー系には多い。それでもブロック自体が簡単にできるのは、トリガーのスクリプトさえ抑えればいいからだ。逆にその URL が特定のドメインやファイルパスを含むルールにマッチできなければブロックすることはほぼ不可能になる。

つまり Web サイト自体のサーバから(経由して)広告を配信することが、前述の答えになる。

こうすれば実質 URL がランダムにできるためにブロックできない。サイト運営者が利用する広告・トラッカーサービスはまちまちだがサーバサイドで一括で処理すればクライアントの負担も減らせる。また HTTP 2.0 時代では、従来の複数の並列なサーバ接続は確立コストが大きいため、1つの接続で複数のストリームを構築するようになる。これもプラスに働く。バックヤードでは Web サイトのサーバと広告・トラッカーのサーバは常に接続を維持できるのも接続確立コストの削減になる。

昨今はほとんどのサイトが CMS で構築されているため、この実装ハードルは低い。デメリットは Web サイトのサーバ負担が増えることだが、これは本来広告掲載するために賄うべきだったはずだ。

広告自体無くなれば良いと思う人は、GoogleGmailGoogle ドライブをフリーで提供していることを思い出してほしい。 サービス、コンテンツの対価として広告を用いることが健全に Web に存在していくことを期待したい。

Ubuntu で GitLab を Omnibus に移行し GitLab CI も使い始める

インストールが大変という定評のあった GitLab ですが、マニュアルインストールとアップグレードで凌いでいました。

GitLab CI を導入しようと試す過程で、サブディレクトリ運用だと OAuth 連携が上手くいかないことがわかりました。

現在はオールインワンのパッケージが提供されているため、下記を使えば簡単に GitLab, GitLab CI を一気に入れることができます。Nginx、PostgreSQL、Redis なども全てまとめて /opt/gitlab に入ります。 GitLab | Download GitLab Community Edition (CE)

サーバスペックをアップグレードするついでに上げますが、基本的にバーチャルホストを使うため FQDN を GitLab, GitLab CI で同じにできません。SSL 証明書に関しては、ワイルドカードを持ってないと辛いところです。

GitLab を最新に更新

下記の手順どおりやれば簡単に今は上がります。

gitlabhq/upgrader.md at master · gitlabhq/gitlabhq · GitHub

バックアップ

$ cd /home/git/gitlab
$ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production

サービス停止

$ sudo service gitlab stop

アップグレード

$ sudo -u git -i
$ bundle exec rake gitlab:backup:create RAILS_ENV=production
$ cd gitlab
$ ruby bin/upgrade.rb 
GitLab 7 upgrade tool
Your version is 7.5.3
Latest available version for GitLab 7 is 7.7.2
Newer GitLab version is available
Do you want to upgrade (yes/no)? yes
Stash changed files
 -> git stash
Saved working directory and index state WIP on 7-5-stable: b656b85 Version 7.5.3
HEAD is now at b656b85 Version 7.5.3
 -> OK
Get latest code
 -> git fetch
remote: Counting objects: 7452, done.                                                  
remote: Compressing objects: 100% (3095/3095), done.                                   
remote: Total 7452 (delta 5170), reused 6001 (delta 4268)                              
Receiving objects: 100% (7452/7452), 2.18 MiB | 387.00 KiB/s, done.
Resolving deltas: 100% (5170/5170), done.
From https://github.com/gitlabhq/gitlabhq
(省略)
 * [new tag]         v7.7.2     -> v7.7.2
 -> OK
Switch to new version
 -> git checkout v7.7.2
Note: checking out 'v7.7.2'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b new_branch_name

HEAD is now at bd98290... Version 7.7.2
 -> OK
Install gems
 -> bundle
Fetching source index from https://rubygems.org/
(省略)
Your bundle is complete!
Gems in the groups development, test and postgres were not installed.
It was installed into ./vendor/bundle
 -> OK
Migrate DB
 -> bundle exec rake db:migrate
(省略)

 -> OK
Recompile assets
 -> bundle exec rake assets:clean assets:precompile
(省略)
 -> OK
Clear cache
 -> bundle exec rake cache:clear
 -> OK
Done

サービス起動

$ sudo service gitlab start

このタイミングで自分の環境では、rbenv で入っていたのが、2.1.3 で 2.1.5 じゃないとエラーが出ました。 /home/git/gitlab/.ruby-version を 2.1.5 に書き換えたら動きました。

$ sudo service nginx restart

更新後のチェック

$ sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production

GitLab Shell の更新

$ sudo -u git -i
$ cd gitlab-shell
$ git fetch
$ sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION`

データを MySQL から PostgreSQL に移行

現状のサーバでは MySQL をデータベースにしていましたが、Omnibus では PostgreSQL になってしまうのでコンバートする必要がありました。

GitLab.org / GitLab Community Edition | GitLab

バックアップを利用するため Converting a GitLab backup file from MySQL to Postgres を参照しました。

$ sudo -u git -i
$ cd gitlab
$ bundle exec rake gitlab:backup:create RAILS_ENV=production

最新のバックアップ tar ファイル名を確認します。

$ ls -ltr tmp/backups/

最新の tar を一時ディレクトリに移動

$ mkdir -p tmp/backups/postgresql
$ mv tmp/backups/<タイムスタンプ>_gitlab_backup.tar tmp/backups/postgresql/
$ cd tmp/backups/postgresql

DBスキーマPostgreSQL 対応でダンプします。

$ mysqldump --compatible=postgresql --default-character-set=utf8 -r gitlabhq_production.mysql -u root gitlabhq_production

コンバーターを落としてきます。

$ git clone https://github.com/gitlabhq/mysql-postgresql-converter.git

スキーマを変換して、バックアップ tar の中の のスキーマ SQL ファイルを置き換えます。

$ mkdir db
$ python mysql-postgresql-converter/db_converter.py gitlabhq_production.mysql db/database.sql
$ tar rf <タイムスタンプ>_gitlab_backup.tar db/database.sql

<タイムスタンプ>_gitlab_backup.tar を新規サーバに転送します。

GitLab CE Omnibus package のインストール

ここから先は新サーバでの作業になります。

パッケージをインストール

$ sudo apt-get install openssh-server
$ sudo apt-get install postfix
$ wget https://downloads-packages.s3.amazonaws.com/ubuntu-14.04/gitlab_7.7.2-omnibus.5.4.2.ci-1_amd64.deb
$ sudo dpkg -i gitlab_7.7.2-omnibus.5.4.2.ci-1_amd64.deb

設定

/etc/gitlab/gitlab.rb を編集します。

以下、GitLab の FQDNgitlab.example.com、GitLab CI の FQDNci.example.com とします。

external_url 'http://gitlab.example.com/'
ci_external_url 'http://ci.example.com/'

設定を反映させます。

$ sudo gitlab-ctl reconfigure

一連のサービスが起動します。Nginx が 80、Unicorn が 8080 ポートを使うので気を付けましょう。

$ sudo gitlab-ctl start

データベースバックアップをリストア

先ほどのバックアップアーカイブを /var/opt/gitlab/backups に移動して復元コマンドを実行します。

$ mv <タイムスタンプ>_gitlab_backup.tar /var/opt/gitlab/backups/
$ sudo gitlab-rake gitlab:backup:restore 

GitLab と GitLab CI を連携

GitLab (http://gitlab.example.com/) にブラウザからログインします。 「Admin area」→「Applications」を開き、[New Application] を押して、Name に「GitLab CI」, Redirect URL に「http://ci.example.com/user_sessions/callback」を登録します。

Application IdSecret が表示されるので、/etc/gitlab/gitlab.rb を編集して以下の行を追記します。

gitlab_ci['gitlab_server'] = { 'url' => 'http://gitlab.example.com', 'app_id' => "<Application Id>", 'app_secret' => '<Secret>'}

これを反映させます。

$ sudo gitlab-ctl reconfigure
$ sudo gitlab-ctl restart

以上で、GitLab と GitLab CI をシングルサインオンにできます。

GitLab CI Runner のインストール

最後に実際ビルドテストを行うランナーをインストールします。

gitlabhq/gitlab-ci-runner · GitHub を参考にしますが、ちょっとそのままだとできないところがありました。

依存パッケージをインストール

$ sudo apt-get update -y
$ sudo apt-get install -y wget curl gcc libxml2-dev libxslt-dev libcurl4-openssl-dev libreadline6-dev libc6-dev libssl-dev make build-essential zlib1g-dev openssh-server git-core libyaml-dev postfix libpq-dev libicu-dev

gitlab_ci_runner ユーザーを追加

ユーザーを追加します。

$ sudo adduser --disabled-login --gecos 'GitLab Runner' gitlab_ci_runner
$ sudo -u gitlab_ci_runner -i

rbenv と ruby-build で Ruby をインストール

$ git clone https://github.com/sstephenson/rbenv.git ~/.rbenv
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
$ type rbenv
$ git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
$ rbenv install 2.1.5
$ rbenv global 2.1.5

bundler をインストール

$ gem install bundler

gitlab-ci-runner をインストール

$ git clone https://gitlab.com/gitlab-org/gitlab-ci-runner.git
$ cd gitlab-ci-runner
$ bundle install --deployment
$ bundle exec ./bin/setup

最後のコマンドは対話式に GitLab CI の URL とトークンを入力します。トークンは GitLab CI の Runners ページに表示されます。

サービスに登録

$ exit
$ cd /home/gitlab_ci_runner/gitlab-ci-runner
$ sudo cp lib/support/upstart/gitlab-ci-runner.conf /etc/init/
$ cd /etc/init.d
$ sudo ln -s /lib/init/upstart-job gitlab-ci-runner

ランナー設定

rbenv の設定を /etc/init/gitlab-ci-runner.confScript に定義します。

script
  # optional: set PATH or other environment variables
  export HOME=/home/gitlab_ci_runner
  export PATH=$HOME/.rbenv/bin:$PATH
  eval "$(rbenv init -)"
  exec bundle exec bin/runner
end script

ランナー起動

$ sudo service gitlab-ci-runner start

後は、Web画面から対象リポジトリを追加して、Jobとしてテストコマンドを定義すると、CI が実行できるようになります。

Xcode 6.1 で自家製フレームワークを追加する方法

適当に Xyz.framework を追加するだけでは、コンパイルは通っても起動後に下記のエラーになっていた。

dyld: Library not loaded: @rpath/Xyz.framework/Xyz
  Referenced from: /Users/xxxx/Library/Developer/CoreSimulator/Devices/xxxx-xxxx-xxxxxx/data/Containers/Bundle/Application/xxxx-xxxx-xxxxxx/Xxxxx.app/Xxxxx
  Reason: image not found

プロジェクト設定の Build Phrases で以下を行う。

  1. Link Binary With Libraries → + → [Add Other...] で生成したフレームワークを選択
  2. Embed Frameworks →Destination を Frameworks に → + → ツリーからフレームワークを選択