MEMO: GitHubのPrivateリポジトリをGitHubAppsで生成したTokenでLambdaでcloneする。

メモです。

AWS LambdaでとあるPrivate Repositoryをcloneして利用したかっただけなのですが、 とても苦労した&確実に後日忘れるので自分用にメモ。

やりたかったこと

  1. AWS Lambdaで特定のPrivate Repositoryをcloneして利用したい。
  2. Pythonで組みたい
  3. cloneするためだけにGitHubのアカウントを新規に作りたくない
    • GitHub Appsで生成するTokenでいい感じにやりたい

最初にやったこと

最初は、GitHubのアカウントを作成して、

参考にさせていただいたURL

ijin.github.io

qiita.com

上記を参考に下記のようにしてみた。 ただ、これだとGitHub側にユーザーを作らなければいけない(or自分のアカウントを利用しないといけない) ので後々困りそうだなと思い、GitHub Appsを利用した方法を模索してみることに。

import paramiko
import dulwich
import dulwich.client
from dulwich import porcelain
from dulwich.contrib.paramiko_vendor import _ParamikoWrapper

def def_github_repository_clone(ssh_secret_key_path, repository_url, save_dir):
    #ssh_secret_key_path: SSH秘密鍵のローカルパス
    #repository_url: GitHubリポジトリのClone用URL(SSHパスで指定)
    #save_dir: Cloneしたリポジトリを保管するローカルパス
    
    class KeyParamikoSSHVendor(object):
        #参考URL
        #https://ijin.github.io/blog/2016/02/18/ssh-and-git-on-aws-lambda/
        #https://qiita.com/yuu-eguci/items/c854856662336770decc
        def __init__(self):
            # 秘密鍵のパスを書きます。
            self.ssh_kwargs = {'key_filename': ssh_secret_key_path}
        
        def run_command(self, host, command, username=None, port=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)
            return _ParamikoWrapper(client, channel)
    
    #SSH接続
    dulwich.client.get_ssh_vendor = KeyParamikoSSHVendor
    
    #Clone
    result = dulwich.porcelain.clone(repository_url, save_dir)
    
    return result

試行錯誤

GitHub Appsなら個人ユーザーが必要無いので、「良さそう!」と思い、試行錯誤を開始。

まず、GitHub Appsを作成する。 Primissionは「Contents」を「Read-only」で設定して、clone対象のリポジトリを指定してInstallする。 (SSH方式と違って、対象リポジトリも柔軟に制限できるのでいい感じ)

f:id:undersooon:20211018115833p:plain

そして、local環境でテストしてみるために、pipで"GitPython","pyjwt","cryptography"をインストール後に下記のようなコードでテストしてみる。 そしてlocalではうまくいった。 (GitHub AppsのAppIDや、InstallationsID、秘密鍵の生成方法はググれば他にいくらでも記載があるので説明割愛)

import time
import jwt
import requests
import json
import git

from cryptography.hazmat.backends import default_backend
from functions.def_get_ssm_parameter import def_get_ssm_parameter

def def_clone_github_repository(github_repository_name, github_apps_app_id, github_apps_installations_id, github_apps_private_pem):
    #github_repository_name -> リポジトリ名
    #github_apps_app_id -> GitHubAppsのAppID
    #github_apps_installations_id -> GitHubAppsのInstallationsID
    #github_apps_private_pem -> GitHubAppsで生成した秘密鍵の値
    
    #jwt値を生成
    #参考:https://gist.github.com/pelson/47c0c89a3522ed8da5cc305afc2562b0#file-example-ipynb
    github_apps_private_pem_bytes = github_apps_private_pem.encode()
    github_apps_private_key = default_backend().load_pem_private_key(github_apps_private_pem_bytes, None)
    time_since_epoch_in_seconds = int(time.time())
    payload = {
      'iat': time_since_epoch_in_seconds, # issued at time
      'exp': time_since_epoch_in_seconds + (10 * 60), # JWT expiration time (10 minute maximum)
      'iss': github_apps_app_id # GitHub App's identifier
    }
    jwt_value = jwt.encode(payload, github_apps_private_pem, algorithm='RS256')
    
    #jwtからheaderを生成
    headers = {
        'Authorization': 'Bearer {}'.format(jwt_value),
        'Accept': 'application/vnd.github.v3+json',
    }
    
    #GitHub Tokenを生成
    response = requests.post(
        'https://api.github.com/app/installations/{}/access_tokens'.format(github_apps_installations_id),
        headers=headers
    )
    if 'token' in response.json():
        github_token = response.json()['token']
    
    #ad_deployリポジトリをclone
    github_repository_remote_url = 'https://x-access-token:{}@github.com/{}/{}.git'.format(str(github_token), str(github_org_name), str(github_repository_name))
    github_clone_local_dir = '/tmp/{}'.format(str(github_repository_name))
    git.Git().clone(github_repository_remote_url, github_clone_local_dir)
    

Lambdaにのせてみる

上記がlocalでうまくいったのでLambdaで実装してみる。環境はPython3.8で。

標準のLambda環境ではいくつかモジュールが足りないので、

qiita.com

を参考にして"GitPython","pyjwt","cryptography"と"requests"をrequirements.txtに書いて、Lambda Layer用のZipを作成して、実装しようとしているLambda環境のLayerに追加。(もしかしたらGitPythonは要らなかったかも)

上記コードを実行してみる。

...が、うまくいかない

発生したエラー

git commandが無い!的なエラー(たぶん、「GitCommandNotFound」とかいうエラー)を吐いて、異常終了する。 後々、わかったがGitPythonはローカルのgitコマンドを利用しているだけのモジュールだったぽい。 ググると、下記のようなLayerを追加すればいいとのこと。自分の環境はap-northeast-1だったので、リージョンだけ それに変えて、arn:aws:lambda:ap-northeast-1:553035198032:layer:git:6 というLayerを追加したらGit commandが無い!的なエラーは出なくなった。それでもうまくいかない

参考にしたURL stackoverflow.com

memo:もしかしたら上記Layerに頼らずとも、下記のように自前でgit関連コマンドを抽出すれば自己解決できるかもだが未検証。

qiita.com

発生したエラー2

今度は error while loading shared libraries: libcurl.so.4: cannot open shared object file: No such file or directory というエラーが発生。 どうやらcURL関係のライブラリが無いらしい。

というわけで今度はcURL関係のライブラリを公開してくれている人がいるのでありがたく拝借して、Layerに追加してみる。 下記URLにある通り、GitHubからZipダウンロードしたままではパス(階層)の関係でうまく利用できないので、ダウンロード後にunzipして、解凍ディレクトリに移動してzip化してAWSにアップロードするとちゃんと読み込んでくれる。

参考にしたURL

zenn.dev

再実行

git command関連のLayerとcURL関連のライブラリのLayerを追加したら無事、AWS Lambda上でもPrivate Repositoryのcloneに成功!