静的HTML公開フローをサーバレスでDevOps！(CodeCommit,Lambda,AWS S3)

前回好評頂いた記事、[静的HTML公開フローをサーバレスでDevOps！(Github,CircleCI,AWS S3)](http://qiita.com/toguma/items/a3c833e42c2469142ca4)を、AWSクラウドネイティブへ再実装。<br>
そのメリッット・デメリットを考察してみる。

---
「そもそも、可能なの？」

当時(2015/12)にはなかった、CodeCommit＋Triggerが搭載された！


<img width="687" alt="スクリーンショット 2016-07-12 17.42.52.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/e46b2f74-917a-4e83-b67e-b0b32a702ad4.png">

http://aws.typepad.com/aws_japan/2016/03/codecommit-repository-trigger.html

---
ということは、
CodeCommit にpush -> trigger -> Lambda発火が可能。

trigger&Lambda実装次第で出来そう。

---

#1 元構成のおさらい

<img width="945" alt="スクリーンショット 2016-07-12 00.22.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/4910ff57-4d34-6baa-68bd-17c96c2ae47a.png">

1. GithubでHTMLコードを管理。
2. CircleCIがブランチへのpushを検知。
3. AWS S3の指定Bucketへ指定ブランチをデプロイ

---
#2 フルAWS構成版

- AWS S3 -> AWS S3 (そのまま）
- Github -> AWS CodeCommit
- CircleCI -> AWS Lambda

<img width="619" alt="スクリーンショット 2016-07-13 15.12.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/5855d54a-96b6-a843-5969-6709a8d4e847.png">

---

#3 Github -> CodeCommit

---
AWSコンソールにて

 - Git管理用IAMユーザ追加
 - CodeCommit用SSHKeyの登録
 - CodeCommitにて新規リポジトリ作成
 - git remote addでCodeCommit側のリポジトリを追加。
 - IAMユーザ+SSH通信にて作成したリポジトリのclone/pushを確認。

---
<img width="765" alt="スクリーンショット 2016-07-13 14.40.13.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/a43647cb-3f58-12c2-6c40-91ef99726415.png">

---
次に肝となるトリガー追加

 - CodeCommitの対象リポジトリ「triggers」->「create trigger」
 - トリガー条件を[push]時のみにする。
 - 対象ブランチは[All branches]
 - Lambda関数を指定 (事前に空関数を作成しておく）

---
<img width="735" alt="スクリーンショット 2016-07-12 18.03.54.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/e08a8e58-6349-692a-bde2-dae488a97dbb.png">

---
- trigger作成。

<img width="983" alt="スクリーンショット 2016-07-13 12.33.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/5a3dd0b4-7025-1b95-e66c-6bed3a86523c.png">

---
- テスト　<br>
Lambdaにてeventソースだけconsole.log出す簡単な関数を用意した上で、<br>
「test trigger」をポチるも、、エラー。<br>
Access権がないよ、と。

<img width="802" alt="スクリーンショット 2016-07-12 18.25.59.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/c0a57bea-8eaf-f4e6-f453-58cd39cc989f.png">

---
CodeCommitの公式ドキュメント(英語）より、
Lambda関数側にCLIからJSON形式の許可設定を追加する必要あるとのこと。

---

```AllowAccessfromMyDemoRepo.json 
{
    "FunctionName": "push_s3_from_codecommit", 
    "StatementId": "1", 
    "Action": "lambda:InvokeFunction", 
    "Principal": "codecommit.amazonaws.com", 
    "SourceArn": "arn:aws:codecommit:us-east-1:[AWS ID]:devops-test-repo", 
    "SourceAccount": "[AWS ID]"
}
```

```
$ aws lambda add-permission --cli-input-json file://AllowAccessfromMyDemoRepo.json
```

---
今度はOK
<img width="813" alt="スクリーンショット 2016-07-13 12.59.00.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/64417127-5097-45dd-ac52-98ec0f31c686.png">

---

#4 CircleCI -> Lambda

---

元々、CircleCiでやってたこと
- 指定ブランチへのpushを検知(webhook)
- 指定ブランチをclone
- html/以下を指定のS3 Bucketへsync

ほぼ数行の指示ファイルで可能。

```circleci.yml

dependencies:
    override:
        - sudo pip install awscli

deployment:
  production:
    branch: master
    commands:
      - aws s3 sync html/ s3://[your s3 bucket]/ --delete
  staging:
    branch: staging
    commands:
      - aws s3 sync html/ s3://[your s3 bucket]/ --delete
```

---

Lambda for Pythonで実装

必要モジュール

SSH処理
- paramiko

git処理
- dulwich

aws cli処理
- awscli

他
- 上記関連モジュール


トータルでは諸々のモジュールが必要。
関連モジュールを含めるとzipして50MB近くなった。。（ただ、未精査なので、必要ないモジュールもあるかも。。）

---

Lambdaポイント

 - Gitログイン時のSSHキーやGit Cloneデータは/tmp/ 以下にファイル設置(500MB以内)
 - SSHキーは都度S3からDownloadさせる都合上、[SSE-KMS](http://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/UsingKMSEncryption.html?tag=viglink27239-20)により暗号化
 - 実行時間はデフォルトの3sだとほぼ間違いなく時間切れとなるので、長めに確保しておく。(60s以上)

---

実装例

```lambda_function.py
from __future__ import print_function  
import os  
import sys  
import subprocess  
import boto3  
  
from botocore.client import Config  
  
from dulwich.errors import (  
    SendPackError,  
    UpdateRefsError,  
    )  
from dulwich.objectspec import (  
    parse_object,  
    parse_reftuples,  
    )  
from contextlib import closing  
from dulwich import porcelain  
from dulwich.contrib.paramiko_vendor import ParamikoSSHVendor  
  
import dulwich  
import dulwich.repo  
import paramiko  
import paramiko.client  

import commands
import json
import os
from cStringIO import StringIO
import re
 
print('Loading function')
 
def _(cmd):
    return commands.getoutput(cmd) 
  
s3 =boto3.client('s3', config=Config(signature_version='s3v4'))  
  
class KeyParamikoSSHVendor(object):  
  
    def __init__(self):  
        self.ssh_kwargs = {'key_filename': '/tmp/id_rsa'}  
  
    def run_command(self, host, command, username=None, port=None,  
                    progress_stderr=None):  
        if not isinstance(command, bytes):  
            raise TypeError(command)  
        if port is None:  
            port = 22  
  
        client = paramiko.SSHClient()  
  
        policy = paramiko.client.MissingHostKeyPolicy()  
        client.set_missing_host_key_policy(policy)  
        client.set_missing_host_key_policy(paramiko.AutoAddPolicy())  
        client.connect(host, username=username, port=port,  
                       **self.ssh_kwargs)  
  
        # Open SSH session  
        channel = client.get_transport().open_session()  
  
        # Run commands  
        channel.exec_command(command)  
  
        from dulwich.contrib.paramiko_vendor import (  
                    _ParamikoWrapper as ParamikoWrapper)  
        return ParamikoWrapper(  
            client, channel, progress_stderr=progress_stderr)  
  
  
def lambda_handler(event, context):
    print(sys.stderr)
    print(getattr(sys.stderr, 'encoding', 'utf-8'))
    s3.download_file('pd-test-data', 'test_id_rsa', '/tmp/id_rsa')
    cmd = "chmod 600 /tmp/id_rsa"
    output = subprocess.check_output(cmd.split(" "))
    print(output)
  
    dulwich.client.get_ssh_vendor = KeyParamikoSSHVendor
    github_repo = '[Your CodeCommit Account]@git-codecommit.us-east-1.amazonaws.com:/v1/repos/[Your Repository]'
  
    repo = dulwich.porcelain.clone(github_repo, '/tmp/temp')

    print (_("./aws s3 sync /tmp/temp/html s3://[Your S3 Bucket] --delete"))
  
    cmd = "rm -rf /tmp/temp"
    output = subprocess.check_output(cmd.split(" "))
    print(output)
```

コード参考サイト
@mag4j さん
http://www.magtranetwork.com/aws/aws_lambda_python_aws_cli.html

@ijin さん
http://ijin.github.io/blog/2016/02/18/ssh-and-git-on-aws-lambda/

---

あと、必要に応じて、

- triggerの対象ブランチを特定したり、
- ブランチ名をevent情報から取得し、ブランチ毎の処理を実装。

作成した関数群をZIPで固め、LambdaにUP。
これで準備完了。


実際に対象ブランチにpushし、S3にファイルアップロードされることを確認。

---
実行ログ
S3へのputやGetObjectなどの各権限などが問題なければ、CloudWatchのLogに以下のようにSyncデータログが流れる。

<img width="827" alt="スクリーンショット 2016-07-13 13.45.13.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/b18d93f6-bd42-7634-99f5-d93c2898d865.png">

---
ちなみに、それぞれの実行時間の比較。
(約4MBのHTMLファイル群アップロード時間)

CircleCI(1コンテナ=無料) = 60s〜80s
Lambda = 13〜14s

---
#5 まとめ

フルAWS構成化は実装できた。
ただ、万事OKかと言われると、一長一短があることも分かった。

---

##Github->CodeCommit
メリット

 - 無料枠内なら無制限にプライベートリポジトリが作成可能
 - ユーザをIAMにて統合管理可能

デメリット

 - <Font color="red">Githubの優良機能が使えない(pull req/レビュー/margeの連携など)</Font>

---
##CircleCi->Lambda
メリット

- ほぼ無料枠内で利用可能(100万リクエストまで）。
- 実行完了が短い。（ファイル数、サイズによる）
- 並列処理が可能。
- 同じ要領で、ほぼすべてのAWSリソースと連携可能。

デメリット

- <Font color="red">権限、コード実装が大変。複雑。</Font>
- Slack通知、hubot連携など必要なら自前て実装する必要あり。
- 上限時間以上の処理ができない。

---

##総評
単に、AWSクラウドネイティブにこだわるなら、この実装はありですが、世界的にデファクトスタンダードなGithubを利用する意義、CIに特化したCircleCIを利用する意義、
は必ずあります。
ここの挙げた以上のメリット・デメリットを把握し、状況に応じて選択出来ればと思います。


---

##追記。
CI(test/deploy）にLambda利用する事に特化した[LambCI](https://github.com/lambci/lambci)というOSSが最近発表されたらしい。
LmabdaでCi、これからのスタンダードになるのかも？

<img width="746" alt="スクリーンショット 2016-07-13 14.36.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/89940/ca22f8f7-9d33-824d-91ab-bd0dbf689008.png">


LambCI
https://github.com/lambci/lambci
https://medium.com/@hichaelmart/lambci-4c3e29d6599b#.ys4gcjmug