Backlog と連携して Lambda で git clone してコードを S3 に配置する
Backlog の GIT が AWS の Codepipeline と直接連携出来ないので Webhook ApiGateway Lambda S3 を使用する事で連携出来るようにします。
ここでは下記 2 点の設定を記載します。
- Serverless Framework で ApiGateway Lambda のデプロイ
- Backlog の Webhook の設定
参考
- AWS CodePipeline が対応していない git リポからの CI/CD 構築
- serverless (公式の whitelist 形式)
- Webhook 送信サーバーの IP アドレスを教えてください
流れ
- Backlog の GIT にプッシュする。
- Backlog の Webhook が ApiGateway にリクエストを送る。
- ApiGateway が Lambda を実行する。
- Lambda が git clone を実行し S3 にコードを配置する。
前提
- awscli が設定済み。
- S3 バケットが作成済み。
- KMS でマスターキーを作成済み。
- serverless をインストール済み。
階層
serverless で作成したプロジェクト直下は下記のとおりです。
.
├── README.md
├── add_encrypt_password.py
├── handler.py
└── serverless.yml
設定手順
- シークレットファイルを作成する
- 暗号化したパスワードを追加する
- デプロイする
- レイヤーを追加する
- Backlog の Webhook を設定する
シークレットファイルを作成する
ファイル .secrets.yml をプロジェクト直下に作り下記の通り設定します。
このファイルの値を使用して暗号化と環境変数を設定します。
arn: arn:aws:kms:ap-northeast-1:xxxxxxxxxxxx:key/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
bucket_name: bucket_name
plain_password: password
repository_url: https://subdomain.backlog.com/git/PROJECT/repository-name.git
target_branches: '["master","develop"]'
user_name: firstname.lastname@example.com
- arn: KMS の ARN の値を設定する。
- bucket_name: コードを配置したい S3 のバケット名を設定する。
- plain_password: GIT のパスワードを設定する。
- repository_url: GIT のリポジトリの URL を設定する。
- target_branches: プッシュされた時に S3 に入れたいブランチ名を設定する。
- user_name: GIT のユーザー名を設定する。
暗号化したパスワードを追加する
ファイル add_encrypt_password.py を実行します。
ファイル .secrets.yml に password 属性で GIT のパスワードを暗号化した値を追加します。
python add_encrypt_password.py
ローカル環境で実行する
ローカル環境にて実行するのコマンドは下記の通りです。
sls invoke local --function main --data '{"body": {"content": {"ref": "refs/heads/master"}}}'
オプション「--data」の引数でリクエストの body を擬似的に取得しています。
テストなので最低限必要な JSON のみで master ブランチがプッシュされたとして記載しています。
デプロイする
Lambda にデプロイするコマンドは下記の通りです。
sls deploy
レイヤーを追加する
Lambda で git コマンドが使えるようにレイヤーを追加します。
- このページの「Version ARNs for Amazon Linux 2 runtimes」の「ARN」をコピーする。
- Lambda のページを開く。
- 対象の関数をクリックする。
- 「関数概要」の図の中の「Layers (0)」をクリックする。
- 項目「レイヤー」の「レイヤーの追加」をクリックする。
- 「RAN を指定」をクリックする。
- 手順 1 でコピーした値をテキストボックスにペーストする。
- ペーストした値のリージョンを該当するリージョンに書き換える。
- 「追加」をクリックする。
※ 手順 8 の値の例。
arn:aws:lambda:ap-northeast-1:553035198032:layer:git-lambda2:8
Backlog の Webhook を設定する
- 左サイドバーの「プロジェクトの設定」をクリックする。
- 「インテグレーション」をクリックする。
- 「Webhook」の「設定」をクリックする。
- 「設定」タブの「Webhook を追加する」をクリックする。
- 下記の設定をする。
- Webhook 名: put_source_into_s3
- 説明: ApiGateway -> lambda -> s3 と言う流れでソースコードを s3 に保存する。
- WebHook URL: [ApiGateway のエンドポイント]
- 「通知するイベント」の「Git プッシュ」にチェックを付ける。
- 「保存」をクリックする。
コード
add_encrypt_password.py
import base64
import boto3
import yaml
kms = boto3.client("kms")
class Encrypt:
"""
secrets.yml の plain_password を暗号化し、password 属性として追加する。
"""
secrets = {}
arn = None
plain_password = None
encrypted_password = None
decoded_password = None
def __init__(self):
"""
secrets.yml を読み込み arn と plain_password を設定する。
"""
with open("./.secrets.yml", "r") as yml:
self.secrets = yaml.safe_load(yml)
self.arn = self.secrets.get("arn", None)
self.plain_password = self.secrets.get("plain_password", None)
def create_encrypted_password(self):
"""
パスワードを暗号化する。
"""
response = kms.encrypt(KeyId=self.arn, Plaintext=self.plain_password)
self.encrypted_password = response["CiphertextBlob"]
def create_decoded_password(self):
"""
(暗号化された)パスワードをデコードする。
"""
self.decoded_password = base64.b64encode(self.encrypted_password).decode(
"utf-8"
)
def write_encrypted_password(self):
"""
(暗号化された)パスワードを password 属性として追加する。
"""
self.secrets["password"] = self.decoded_password
with open("./.secrets.yml", "w") as f:
yaml.dump(self.secrets, f)
encrypt = Encrypt()
encrypt.create_encrypted_password()
encrypt.create_decoded_password()
encrypt.write_encrypted_password()
serverless.yml
service: put-source-into-s3
frameworkVersion: "3"
custom:
kmsSecrets:
file: ${file(./.secrets.yml)}
provider:
name: aws
stage: any
runtime: python3.9
region: ap-northeast-1
apiGateway:
resourcePolicy:
- Effect: Allow
Principal: "*"
Action: execute-api:Invoke
Resource:
- execute-api:/*/*/*
Condition:
IpAddress:
aws:SourceIp:
- "54.64.128.240"
- "54.178.233.194"
- "13.112.1.142"
- "13.112.147.36"
- "54.238.175.47"
- "54.168.25.33"
- "52.192.156.153"
- "54.178.230.204"
- "52.197.88.78"
- "13.112.137.175"
- "34.211.15.3"
- "35.160.57.23"
- "54.68.48.106"
- "52.88.47.69"
- "52.68.247.253"
- "18.182.251.152"
- "54.248.107.22"
- "54.248.105.89"
- "54.238.168.195"
- "52.192.66.90"
- "54.65.251.183"
- "54.250.148.49"
- "35.166.55.243"
- "50.112.242.159"
- "52.199.112.83"
- "35.73.201.244"
- "35.72.166.154"
- "35.73.143.41"
- "35.74.201.20"
- "52.198.115.185"
- "35.165.230.177"
- "18.236.6.123"
iamRoleStatements:
- Effect: "Allow"
Action:
- "s3:PutObject"
Resource: "arn:aws:s3:::${self:custom.kmsSecrets.file.bucket_name}/*"
- Effect: "Allow"
Action:
- KMS:Decrypt
Resource: ${self:custom.kmsSecrets.file.arn}
functions:
main:
handler: handler.main
events:
- http:
path: git/push
method: post
async: true
integration: lambda
environment:
REPOSITORY_URL: ${self:custom.kmsSecrets.file.repository_url}
USER_NAME: ${self:custom.kmsSecrets.file.user_name}
PASSWORD: ${self:custom.kmsSecrets.file.password}
BUCKET_NAME: ${self:custom.kmsSecrets.file.bucket_name}
TARGET_BRANCHES: ${self:custom.kmsSecrets.file.target_branches}
handler.py
import base64
import datetime
import json
import os
import shutil
import subprocess
import traceback
import urllib
import urllib.request
import boto3
REPOSITORY_URL = os.environ.get("REPOSITORY_URL", None)
USER_NAME = os.environ.get("USER_NAME", None).replace("@", "%40")
PASSWORD = os.environ.get("PASSWORD", None)
BUCKET_NAME = os.environ.get("BUCKET_NAME", None)
TARGET_BRANCHES = os.environ.get("TARGET_BRANCHES", [])
class PutSourceIntoS3:
"""
ソースコードをZIPしてS3に配置する。
Backlog の Webhook によるトリガーを想定。
インスタンス変数は、レスポンスで見れるようにしているので、
複合したパスワードやGITへのリクエストのURLは、設定しないでおく。
"""
backlog_params = None # Backlog から受け取ったパラメータ
branch_name = None # ブランチ名
repository_name = None # リポジトリ名
request_url = None # クローン生成のリクエストURL
root = None # ルートのパス
source_path = None # クローンしたソースコードのパス
tmpdir = None # 作業ディレクトリ
def __init__(self, backlog_params):
"""
初期値として Backlog のパラメータと root のパスを設定する。
"""
self.backlog_params = backlog_params
self.root = os.path.abspath(os.path.join(__file__, ".."))
def get_decrypted_password(self):
"""
パスワードを複合する。
"""
kms = boto3.client('kms')
return kms.decrypt(
CiphertextBlob=base64.b64decode(PASSWORD)
)['Plaintext'].decode('utf-8')
def get_request_url(self):
"""
リポジトリのURLから clone 生成のためのユーザー名とパスワード名を追加したリポジトリのURLを生成し設定する。
"""
parsed_url = urllib.parse.urlparse(REPOSITORY_URL)
return (
parsed_url.scheme
+ "://"
+ USER_NAME
+ ":"
+ self.get_decrypted_password()
+ "@"
+ parsed_url.netloc
+ parsed_url.path
)
def set_working_dir(self):
"""
作業ディレクトリ ./tmp/yyyymmddssss/ を生成し設定する。
"""
now = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
self.tmpdir = "/tmp/" + now
os.makedirs(self.tmpdir)
def set_branch_name(self):
"""
Backlog のリクエストからブランチ名を取得し設定する。
"""
self.branch_name = self.backlog_params["body"]["content"]["ref"].split("/")[-1]
def set_repository_name(self):
"""
リポジトリのURLからリポジトリ名を取得し設定する。
"""
parsed_url = urllib.parse.urlparse(REPOSITORY_URL)
repository_name_with_extension = parsed_url.path.split("/")[-1]
self.repository_name = repository_name_with_extension.split(".")[0]
def set_source_path(self):
"""
zip するファイルのパスを取得する。
"""
self.source_path = self.tmpdir + "/" + self.repository_name
def git_clone(self):
"""
コマンド git clone を実行し tmpdir 配下にファイルを設置する。
"""
os.chdir(self.tmpdir)
subprocess.call(
[
"git",
"clone",
"--branch",
self.branch_name,
self.get_request_url(),
]
)
def zip_files(self):
"""
source_path に指定されたソースを zip にまとめる。
"""
shutil.make_archive(self.source_path, "zip", self.source_path)
def upload_to_s3(self):
"""
zip したファイルを s3 にアップロードする。
"""
s3 = boto3.resource("s3")
bucket = s3.Bucket(BUCKET_NAME)
try:
bucket.upload_file(
f"{self.source_path}.zip",
f"{self.repository_name}.zip"
)
except Exception:
raise Exception
def main(event, context):
"""
serverless.yml から呼び出されるメインメソッド。
"""
try:
psis = PutSourceIntoS3(event)
# Ready
psis.set_working_dir()
psis.set_branch_name()
psis.set_repository_name()
psis.set_source_path()
if psis.branch_name in json.loads(TARGET_BRANCHES):
message = f"Put '{psis.branch_name}' of source into S3."
# Execute
psis.git_clone()
psis.zip_files()
psis.upload_to_s3()
else:
message = f"'{psis.branch_name}' of branch is not target."
return {
"statusCode": 200,
"params": vars(psis),
"message": message,
}
except Exception:
traceback.print_exc()
return {
"statusCode": 400,
"params": vars(psis),
"error": traceback.format_exc(),
}
finally:
del psis
ディスカッション
コメント一覧
まだ、コメントがありません