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

この記事は ハンズラボ Advent Calendar 2017 1日目の記事です。

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

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

iOSアプリの証明書とプロビジョニングプロファイルの有効期限忘れがち問題

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

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

iOSアプリの証明書とプロビジョニングプロファイルの有効期限が後何日で切れるかを毎週自動でSlackに通知してみた

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

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

old.png

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

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

GitリポジトリにPushされていない証明書・プロビジョニングプロファイルの有効期限は通知されない

指定のGitリポジトリへのPushは手動での作業となっていたため、Apple Developer Portal上にはあるけどGitリポジトリ上にはないという状態が発生する可能性があります。もしGitリポジトリにPushされていないものがあった場合、このBotではそれを救う術はありません。

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

新アーキテクチャ

new.png

上のようなアーキテクチャに刷新しました。かの有名なfastlaneの一部であるspaceshipを使えば、Apple Developer Portalへのログイン、証明書・プロビジョニングプロファイルの情報取得が可能です。更にこのためだけにEC2を常時稼働させるよりも、CloudWatch EventでAWS Lambdaを定期実行させた方が運用面でもコスト面でも良いと考えたため、AWSのブログ記事を参考に、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の実行環境を入れます。

$ 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イメージを持ってきます。

$ docker pull amazonlinux:latest

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

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

Rubyのインストール

今回はAWSのブログ記事と同じくTraveling Rubyを使います。

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

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

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

$ 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/gemfastlaneのインストールを行うと、unf_ext-0.0.7.4.gemのインストールで失敗してしまいます。

/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)のインストール

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

ここで、AWS Lambdaのデプロイメントの制限について少し考えます。公式ドキュメントによると、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 RubygemとシステムのgemBundlerをインストールします。

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

Gemfileを作成しましょう。

$ bundle init

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

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を使ってfastlaneslack-incoming-webhooksをインストールしましょう。

$ 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のスクリプトです。肝となるのは以下の部分です。

require 'spaceship'

<中略>

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

fastlane/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を使って暗号化しましょう。

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

<中略>

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

暗号化されたキーは、KMSのdecryptを使って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に通知してくれます。

bot.png

自動構築の手順は以下です。

1. Rubyの実行環境をローカルに持って来る

前提条件:Dockerが起動していること

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

$ ./scripts/create-ruby-env.sh

このスクリプトでdocker builddocker runを行います。
自動的にコンテナに入るので、コンテナ内で以下のコマンドを実行します。

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: <スケジュール式>の形で指定してください。スケジュール式についてはこちら(公式ドキュメント)をご参照ください。

3. Lambdaの構築

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

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

$ ./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を実行すれば、全ての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 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/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