AWS LambdaでRubyを実行してiOSアプリの証明書とかの有効期限をSlackに通知する [自動構築手順あり]

[2018/10/9追記] **現在この記事で紹介されている方法は、Ruby、fastlaneともにかなり古いバージョンを使用しているため、実際の環境では使わないでください** :bow:

この記事は [ハンズラボ Advent Calendar 2017](https://qiita.com/advent-calendar/2017/handslab) 1日目の記事です。

こんにちは。ハンズラボでアプリケーションエンジニアをしている @mii-chan こと三井田です。今年は新卒研修や東急ハンズのAlexaスキルの開発 (途中まで) などを担当し、現在はSwift 4で業務用iOSアプリを新規開発しています。

今回はiOSアプリを開発する上でかかせない、iOSアプリの証明書とプロビジョニングプロファイルについての話をします。

# iOSアプリの証明書とプロビジョニングプロファイルの有効期限忘れがち問題
忘れずに更新が必要なiOSアプリの証明書とプロビジョニングプロファイルですが、証明書やプロビジョニングプロファイルの数が多くなってくると、全てを定期的にチェックするのは中々の労力を要します。

そこで当時新卒1年目だった私は「有効期限が後何日で切れるかを自動でSlackに通知すればいいんだ！」と考え、毎週自動でSlackに証明書・プロビジョニングプロファイルの有効期限を通知するBotを開発しました。

[iOSアプリの証明書とプロビジョニングプロファイルの有効期限が後何日で切れるかを毎週自動でSlackに通知してみた](https://www.hands-lab.com/tech/entry/2065.html)

しかし、このBotを運用していくうちに、いくつかの課題が出てきました。

## 旧アーキテクチャの問題点

![old.png](https://qiita-image-store.s3.amazonaws.com/0/172234/5a6913e7-7170-329a-3515-3acbcb1fa40d.png)

開発したBotは、cronでシェルスクリプトを定期実行し、指定のGitリポジトリにPushされた証明書・プロビジョニングプロファイルを取得後、その有効期限をSlackに通知するというものでした。

このアーキテクチャの問題点は以下です。

### GitリポジトリにPushされていない証明書・プロビジョニングプロファイルの有効期限は通知されない
指定のGitリポジトリへのPushは手動での作業となっていたため、Apple Developer Portal上にはあるけどGitリポジトリ上にはないという状態が発生する可能性があります。もしGitリポジトリにPushされていないものがあった場合、このBotではそれを救う術はありません。

「Apple Developer Portalから全ての証明書とプロビジョニングプロファイルの情報を取得できれば最高なのにな〜」と思っていたら、弊社iOSエンジニア駒場から「それ[`spaceship`](https://github.com/fastlane/fastlane/tree/master/spaceship)でできるよ」みたいな助言をもらったので、今回はこれを使って新しいBotを開発してみました。

## 新アーキテクチャ
![new.png](https://qiita-image-store.s3.amazonaws.com/0/172234/9c59f932-6fee-f78b-23c9-4a8fa5891361.png)

上のようなアーキテクチャに刷新しました。かの有名な[`fastlane`](https://github.com/fastlane/fastlane)の一部である`spaceship`を使えば、Apple Developer Portalへのログイン、証明書・プロビジョニングプロファイルの情報取得が可能です。更にこのためだけにEC2を常時稼働させるよりも、CloudWatch EventでAWS Lambdaを定期実行させた方が運用面でもコスト面でも良いと考えたため、[AWSのブログ記事](https://aws.amazon.com/jp/blogs/compute/scripting-languages-for-aws-lambda-running-php-ruby-and-go/)を参考に、Node.jsの`exec`コマンドを使ってAWS Lambda内でRubyスクリプトを実行させるようにしてみました。

ここからは、このBotをどのように開発したかを順に話していきます。
(今すぐ最終成果物が見たい、という方は[こちら](#最終成果物)まで飛んでください)

# 1. Dockerを使ってRubyの実行環境の構築を行う
まずは、AWS LambdaでRubyを実行するための環境を構築します。Linux上でコンパイルを行う必要があるため、EC2上で環境構築を行います。前述のAWSのブログ記事ではEC2(t2.large)インスタンスを立ち上げて作業を行っていますが、今回はAmazon LinuxのDockerイメージを使って環境構築を行います。

前提 ： ローカル環境にDockerがインストールされており、起動していること

最初にプロジェクトフォルダを適当に作ります。
今回は、プロジェクトフォルダ内の`ruby-env`ディレクトリにRubyの実行環境を入れます。

```bash
$ mkdir ios-cer-profile-expiration-date-checker
$ cd ios-cer-profile-expiration-date-checker
$ mkdir ruby-env
$ cd ruby-env
```

## Amazon LinuxのDockerイメージを使う
下記のコマンドでAmazon LinuxのDockerイメージを持ってきます。

```bash
$ docker pull amazonlinux:latest
```

持ってこれたら`docker run`しましょう。
ホスト側のカレントディレクトリと、コンテナ内の`/var/task`を共有しておきます。

```bash
docker run --rm -v "$(pwd)":/var/task -it amazonlinux:latest
```

## Rubyのインストール
今回はAWSのブログ記事と同じく[`Traveling Ruby`](https://github.com/phusion/traveling-ruby)を使います。

最初に、`yum`のアップデートとRubyのインストールに必要なパッケージのインストールを行います。

```bash
$ yum update -y
$ yum -y groupinstall "Development tools"
$ yum -y install openssl-devel zlib-devel
$ yum -y install wget
```

インストールが終了したら、`Traveling Ruby`を`wget`で取ってきて`/var/task`下に解凍します。

```bash
$ wget http://d6r77u77i8pq3.cloudfront.net/releases/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz

$ tar -xvf traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz -C /var/task
```

`$ cd /var/task`でディレクトリに移動後、以下のコマンドでRubyが正しくインストールされたかを確認します。

```
$ ./bin/ruby -v
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-linux]
```

正しくインストールされています。

このまま`Gem`なり`Bundler`なりでインストールすれば実行環境が構築できそうなのですが、`Traveling Ruby`にはヘッダーファイルが同梱されていないため、そのまま`./bin/gem`で`fastlane`のインストールを行うと、`unf_ext-0.0.7.4.gem`のインストールで失敗してしまいます。

```bash
/var/task/bin/ruby -r ./siteconf20171127-14946-1kqallk.rb extconf.rb
mkmf.rb can't find header files for ruby at /var/task/lib/ruby/include/ruby.h
```

そこで、Systemに同じバージョン(2.2.2)のRubyを入れ、それを使って`fastlane`をインストールすることにしました。

```
$ wget http://cache.ruby-lang.org/pub/ruby/2.2/ruby-2.2.2.tar.gz

$ tar -xvf ruby-2.2.2.tar.gz
$ cd ruby-2.2.2
$ ./configure && make && make install
```

## fastlane(とslack-incoming-webhooks)のインストール
準備が整ったところで、`fastlane`と`slack-incoming-webhooks`(Slack通知用)を`Bundler`を使ってインストールしましょう。

ここで、AWS Lambdaのデプロイメントの制限について少し考えます。[公式ドキュメント](http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/limits.html)によると、`Lambda 関数デプロイパッケージのサイズ (圧縮 .zip/.jar ファイル)`のデフォルトの制限は`50 MB`、`デプロイパッケージ (非圧縮 .zip/.jar サイズ) に圧縮できるコード/依存関係のサイズ`のデフォルトの制限は`250 MB`との記述があります。つまり、圧縮前のパッケージサイズが`250MB`、圧縮後のサイズが`50MB`を超えてはいけないという制限です。

通常は大丈夫だと思いますが、今回のようにRubyの実行環境ごとLambdaにアップロードする場合だとパッケージのサイズが大きくなってしまうため、この制限を考慮に入れながら環境構築を進める必要があります。

今回インストールする`fastlane`ですが、`fastlane-2.60.1`(46.6MB)から`fastlane-2.61.0`(60.3MB)で **13.7MB** くらいサイズが大きくなったため、`2.61.0`以降をインストールした場合、デプロイパッケージを圧縮した後のZipファイルのサイズが`50MB`を超えてしまいました。

そこで、今回はバージョン`2.60.1`をバージョン指定でインストールします。

まずは、`Traveling Ruby`の`gem`とシステムの`gem`に`Bundler`をインストールします。

```bash
$ cd /var/task
$ ./bin/gem install bundler --no-document
$ gem install bundler --no-document
```

Gemfileを作成しましょう。

```bash
$ bundle init
```

ホスト側でGemfileを以下のように編集します。

```text:Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "fastlane", "2.60.1"
gem "slack-incoming-webhooks"                        
```

編集後、コンテナ上で**Systemにインストールした**`Bundler`を使って`fastlane`と`slack-incoming-webhooks`をインストールしましょう。

```bash
$ bundle install --path vendor/bundle
```

以上でRubyの実行環境の構築は終わりです。ホスト側のカレントディレクトリにRubyの実行環境ができているはずです。

＊ここまでの手順をまとめた`Dockerfile`あります。
https://github.com/mii-chan/ios-cer-profile-expiration-date-checker/blob/master/templates/Dockerfile

# 2. Lambda関数の作成
次にLambda関数を作成します。

まずは、Rubyのスクリプトです。肝となるのは以下の部分です。

```rb
require 'spaceship'

<中略>

Spaceship::Portal.login(id, pass)
cer_profile_list = { CERTIFICATE => Spaceship::Portal.certificate.all, MOBILEPROVISION => Spaceship::Portal.provisioning_profile.all }
```

[fastlane/spaceship](https://github.com/fastlane/fastlane/tree/master/spaceship)をインポートし、Apple IDとパスワードでApple Developer Portalにログインします。ログイン後は、`Spaceship::Portal.certificate.all`で全ての証明書、`Spaceship::Portal.provisioning_profile.all`で全てのプロビジョニングプロファイルの情報を取得することができます。

その後、後何日で有効期限が切れるかを計算し、`slack-incoming-webhooks`を使ってSlackへの通知を行なっています。

次に、このスクリプトを実行するNode.jsスクリプトを書きます。

SlackのWebhook URL、Apple ID、PasswordはLambdaの環境変数に設定していますが、これらはKMSを使って暗号化しましょう。

```js:index.js
const KMS = new AWS.KMS();

<中略>

const encryptedParams = [
  process.env['WEBHOOKURL'],
  process.env['ID'],
  process.env['PASS']
];
const decryptedParams = {};
```

暗号化されたキーは、KMSの[`decrypt`](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/KMS.html#decrypt-property)を使ってLambda関数実行時に複合化します。

その後、`exec`コマンドを使ってNode.jsからRubyスクリプトを実行するという流れです。

これでLambda関数の作成が完了しました。

後はLambdaアップロード用のZipファイルを作成して、Lambda関数の新規作成としかるべき設定をすれば完成です。。

が、コンソールでポチポチLambdaを新規作成するのは手間なので、今回はLambda関数作成その他諸々の環境をAWS CloudFormation、AWS SAMを使って自動で構築できるようにしました。

# 3. 自動化しちゃうぞ
Lambda関数構築自動化のため、以下のテンプレートを用意しました。

デプロイパッケージをアップロードするS3バケット、環境変数を暗号化するためのKMS、Lambdaの実行ロールを作成するCloudFormationテンプレート
https://github.com/mii-chan/ios-cer-profile-expiration-date-checker/blob/master/templates/cf-template.yaml

Lambda関数をデプロイするSAMテンプレート
https://github.com/mii-chan/ios-cer-profile-expiration-date-checker/blob/master/templates/sam-template.yaml

# 最終成果物
最終的にできたものは以下に置いてあります。
https://github.com/mii-chan/ios-cer-profile-expiration-date-checker

下の画像のように、証明書・プロビジョニングプロファイルの有効期限が残り何日で切れるのかをSlackに通知してくれます。

<img width="664" alt="bot.png" src="https://qiita-image-store.s3.amazonaws.com/0/172234/d1b0a68c-675f-0907-85e8-8f57ad93251c.png">



自動構築の手順は以下です。
## 1. Rubyの実行環境をローカルに持って来る
前提条件：Dockerが起動していること

プロジェクトルートで以下のコマンドを実行します。

```bash
$ ./scripts/create-ruby-env.sh
```
このスクリプトで`docker build`、`docker run`を行います。
自動的にコンテナに入るので、コンテナ内で以下のコマンドを実行します。

```bash
bash-4.2# cp -rp * .[^\.]* /app
bash-4.2# exit
```
Rubyの実行環境が`ruby-env`ディレクトリ配下にあることを確認してください。

## 2. 各種パラメータの設定
`parameters`ディレクトリにある2つのファイルに書かれているパラメータを、お好きな値に変更します。

### parameters.json

Parameter Key | Parameter Value Example | Description
---|:---:|---
StackName| `"iOS-cer-profile-expiration-date-check"` | Lambdaをデプロイする事前準備で作成されるスタックの名前(**アルファベットとハイフンしか使えないので注意！**)。S3バケット、KMS、Lambdaの実行ロールの作成を行う。詳しくは`template`ディレクトリ内の[cf-template.yaml]()参照
StackNameLambda| `"iOS-cer-profile-expiration-date-check-lambda"` | Lambdaのデプロイで作成されるスタックの名前(**アルファベットとハイフンしか使えないので注意！**)。詳しくは`template`ディレクトリ内の[sam-template.yaml]()参照
CHANNEL|`"#general"`|通知先のSlackのChannel
USERNAME|`"iOS Monthly Bot"`|Slack通知を行うBotの名前
ICON|`":iphone:"`|Slack通知を行うBotのアイコン
WARNINGDAY|`"60"`|残り日数がこの日より少なくなると、Slack通知時のAttachmentの色がオレンジになります
DANGERDAY|`"30"`|残り日数がこの日より少なくなると、Slack通知時のAttachmentの色が赤になります

### schedule-expression
Lambdaを起動させるScheduled Eventのスケジュール式をここで指定します。ファイルの5行目に書かれているような`ScheduleExpression: <スケジュール式>`の形で指定してください。スケジュール式については[こちら(公式ドキュメント)](http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/tutorial-scheduled-events-schedule-expressions.html)をご参照ください。

## 3. Lambdaの構築
前提条件: 
1. AWS CLIがインストールされていること
2. 割と強めのIAMユーザーのクレデンシャルがセットされていること(実行に必要なポリシーは後日まとめます...)
3. Apple Developer PortalにログインするためのAppleID、Password、SlackのWebhook URLを知っていること

プロジェクトルートで以下のコマンドを実行します。

```bash
$ ./scripts/setup.sh
```
実行時にプロンプトで以下の入力を求められるので、然るべき値を入力してください(KMSで暗号化された後、Lambdaの環境変数にセットされます)。

1. `AppleID` : Apple Developer PortalにログインするためのApple ID
2. `Password` : Apple Developer PortalにログインするためのPassword
3. `WebhookURL` : SlackのWebhook URL


スクリプトが正常終了すると、Lambdaがアカウントにデプロイされているはずです。

### 注意事項
* デプロイ後、コンソールでSlackのWebhook URLを設定し直す際は、`https://`を除いた状態(`hooks.slack.com/xxx`)で環境変数に設定してください(暗号化もお忘れなく)
* スクリプト実行途中でエラーが起こってしまった場合は、エラーメッセージが表示されると思うので、修正して再度スクリプトを実行してください。
 - Stackが中途半端に作成された場合は、作られたStackを削除してから再実行した方がよいかもしれません
 - [`scripts/delete-all-stacks.sh`](https://github.com/mii-chan/ios-cer-profile-expiration-date-checker/blob/master/scripts/delete-all-stacks.sh)を実行すれば、全てのStackを削除できます。


# 実際にやってみて
* `Traveling Ruby`を使ったので、Rubyのバージョンが2.2.2
   - 新しいバージョンを使いたい...
   - 2.4系のソースコードをコンテナに落としてきて`/var/task`以下にインストールしてみたけどLambda関数の実行が上手くいかず...

* Lambdaデプロイパッケージ(圧縮)`50M`問題
   - 最新の`fastlane`がアップロードできない...

* それならいっそ、最新のRubyとfastlaneを入れたDockerイメージを作って、Lambdaの定期実行でECSを起動・停止させればよかったのでは？
   - やった後に思いました。未検証


今回は気の赴くままにやってみましたが、スクリプトの実行をどのようなアーキテクチャで行うかについては、引き続き検討を続けていく必要がありそうです。

しかしながら、`fastlane/spaceship`を使えば本当に簡単にApple Developer Portalから証明書・プロビジョニングプロファイル情報を取得できたので、今後はこのRubyスクリプトを何らかの形で実行して、全てのiOSアプリの証明書・プロビジョニングプロファイルの更新漏れを引き続き防いでいきたいと思います。

# まとめ
**AWS Lambdaで** (Goだけじゃなくて) **Rubyもサポートしてほしい！**

[ハンズラボ Advent Calendar 2017](https://qiita.com/advent-calendar/2017/handslab) 2日目の明日は、この前まで一緒にAngularで開発をしていた @daikiojm くんです！
お楽しみに〜

# 参考
Scripting Languages for AWS Lambda: Running PHP, Ruby, and Go
https://aws.amazon.com/jp/blogs/compute/scripting-languages-for-aws-lambda-running-php-ruby-and-go/

MacでDockerとAmazon Linuxコンテナを使ってAWS Lambda (Node.js-v4.3.2)にデプロイしてみた
https://qiita.com/tomiyan/items/be1ccb20b154cabb8c7c

【AWS Lambda】Amazon Linux の Docker イメージを使ってデプロイパッケージを作成する
https://qiita.com/morishin/items/cfba9ed41a73158b38f6

CentOS6に新しいバージョンのrubyをインストールする手順
http://zacodesign.net/blog/?p=1952

[AWS CloudFormation] AWS リソースプロパティタイプのリファレンス
http://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html

awslabs/serverless-application-model / versions / 2016-10-31.md
https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md