前回好評頂いた記事、静的HTML公開フローをサーバレスでDevOps!(Github,CircleCI,AWS S3)を、AWSクラウドネイティブへ再実装。
そのメリッット・デメリットを考察してみる。
「そもそも、可能なの?」
当時(2015/12)にはなかった、CodeCommit+Triggerが搭載された!
ということは、
CodeCommit にpush -> trigger -> Lambda発火が可能。
trigger&Lambda実装次第で出来そう。
#1 元構成のおさらい
- GithubでHTMLコードを管理。
- CircleCIがブランチへのpushを検知。
- AWS S3の指定Bucketへ指定ブランチをデプロイ
#2 フルAWS構成版
- AWS S3 -> AWS S3 (そのまま)
- Github -> AWS CodeCommit
- CircleCI -> AWS Lambda
#3 Github -> CodeCommit
AWSコンソールにて
- Git管理用IAMユーザ追加
- CodeCommit用SSHKeyの登録
- CodeCommitにて新規リポジトリ作成
- git remote addでCodeCommit側のリポジトリを追加。
- IAMユーザ+SSH通信にて作成したリポジトリのclone/pushを確認。
次に肝となるトリガー追加
- CodeCommitの対象リポジトリ「triggers」->「create trigger」
- トリガー条件を[push]時のみにする。
- 対象ブランチは[All branches]
- Lambda関数を指定 (事前に空関数を作成しておく)
- trigger作成。
- テスト
Lambdaにてeventソースだけconsole.log出す簡単な関数を用意した上で、
「test trigger」をポチるも、、エラー。
Access権がないよ、と。
CodeCommitの公式ドキュメント(英語)より、
Lambda関数側にCLIから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
#4 CircleCI -> Lambda
元々、CircleCiでやってたこと
- 指定ブランチへのpushを検知(webhook)
- 指定ブランチをclone
- html/以下を指定のS3 Bucketへsync
ほぼ数行の指示ファイルで可能。
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により暗号化
- 実行時間はデフォルトの3sだとほぼ間違いなく時間切れとなるので、長めに確保しておく。(60s以上)
実装例
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データログが流れる。
ちなみに、それぞれの実行時間の比較。
(約4MBのHTMLファイル群アップロード時間)
CircleCI(1コンテナ=無料) = 60s〜80s
Lambda = 13〜14s
#5 まとめ
フルAWS構成化は実装できた。
ただ、万事OKかと言われると、一長一短があることも分かった。
##Github->CodeCommit
メリット
- 無料枠内なら無制限にプライベートリポジトリが作成可能
- ユーザをIAMにて統合管理可能
デメリット
- Githubの優良機能が使えない(pull req/レビュー/margeの連携など)
##CircleCi->Lambda
メリット
- ほぼ無料枠内で利用可能(100万リクエストまで)。
- 実行完了が短い。(ファイル数、サイズによる)
- 並列処理が可能。
- 同じ要領で、ほぼすべてのAWSリソースと連携可能。
デメリット
- 権限、コード実装が大変。複雑。
- Slack通知、hubot連携など必要なら自前て実装する必要あり。
- 上限時間以上の処理ができない。
##総評
単に、AWSクラウドネイティブにこだわるなら、この実装はありですが、世界的にデファクトスタンダードなGithubを利用する意義、CIに特化したCircleCIを利用する意義、
は必ずあります。
ここの挙げた以上のメリット・デメリットを把握し、状況に応じて選択出来ればと思います。
##追記。
CI(test/deploy)にLambda利用する事に特化したLambCIというOSSが最近発表されたらしい。
LmabdaでCi、これからのスタンダードになるのかも?
LambCI
https://github.com/lambci/lambci
https://medium.com/@hichaelmart/lambci-4c3e29d6599b#.ys4gcjmug