JAWS-UG 浜松 AWS 勉強会 2022#2 2022/2/25
モノレポの GitHub (Enterprise) からCodePipeline を呼び出す小ネタJAWS-UG 浜松 AWS 勉強会 2022#2 2022/2/25まつひさ(hmatsu47)
View Slide
自己紹介松久裕保(@hmatsu47)● https://qiita.com/hmatsu47● 現在のステータス:○ 名古屋で Web インフラのお守り係をしています○ Aurora MySQL v1(5.6)の EoL が発表されたのでアップ開始■ https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/Aurora.MySQL56.EOL.html● v1 → v3 移行を画策中2
本日の小ネタ● こちら↓の Lambda 関数を正しく動くよう実装○ GitHub モノレポを AWS CodePipeline と統合して、プロジェクト固有の CI/CDパイプラインを実行する(Amazon Web Services ブログ)● Zenn で記事化済み○ https://zenn.dev/hmatsu47/articles/73c624fb5730dd● 参考にした記事○ Backlogの課題にGitHubのコミットを連携する方法(ponsuke_tarou’s blog)3
モノレポの課題● 開発プロジェクト(プロダクト・サービス)別にビルド→デプロイするのに手間が掛かる○ 必要がなくても全プロジェクトをビルドパイプラインが走る● そこで提示されたのが前掲の記事○ GitHub モノレポを AWS CodePipeline と統合して、プロジェクト固有の CI/CDパイプラインを実行する(Amazon Web Services ブログ)○ ただし動作に問題がある4
何が問題?● 対象ブランチの指定がない○ どこのブランチに push してもパイプラインが実行されてしまう● コードの変更以外の操作まで拾う○ 誤動作の可能性がある● 複数フォルダにまたがる push でも、1 本のパイプラインしか実行されない5
修正後は● 対象ブランチの指定が可能● パイプライン実行対象のフォルダを指定可能● 複数フォルダにまたがる push で複数のパイプラインを並列呼び出し可能● 例外的に全パイプラインを並列呼び出しするフォルダの指定が可能6
設定の流れ(詳細は前掲の Zenn 記事を参照)1. Secrets Manager にシークレットを保存2. Lambda 関数を作成3. API Gateway を作成し、Lambda 関数を統合4. IAM Role(Lambda 実行用)にポリシーを追加5. GitHub (Enterprise) で Webhook を設定6. CodePipeline を設定(変更)7
1. Secrets Manager にシークレットを保存● GitHub → (API Gateway →)Lambda 認証時に使用○ パスワードジェネレータなどで生成○ Secrets Manager で「その他のシークレットのタイプ」を選択■ キー : GHE_SECRETS■ 値 : 生成したシークレットの値■ 任意の名前を付けて保存8
2. Lambda 関数を作成● GitHub Webhook からのリクエストを受けて、対象のCodePipeline を呼び出す○ コードと↓の環境変数を登録■ 呼び出すパイプライン名のサフィックス : job_name_suffix● 「【フォルダ名】+【サフィックス】」の CodePipeline を呼び出します■ シークレット名(先ほど保存したもの) : secrets_name■ パイプライン実行対象のブランチ名 :trigger_branch● 「refs/heads/【ブランチ名】」の形で指定9
2. Lambda 関数を作成import jsonimport hmac, hashlibimport boto3import base64import ast, reimport osfrom botocore.exceptions import ClientErrordef lambda_handler(event, context):body = event['body']if is_correct_signature(event['headers']['x-hub-signature'], body):print('認証成功')project_names = []job_name_suffix = os.environ['job_name_suffix']body_json = json.loads(body)ref = body_json['ref']if ref == os.environ['trigger_branch'] and len(body_json['commits']) > 0:# 指定ブランチへのコミットの場合だけ処理added_files = body_json['commits'][0]['added']removed_files = body_json['commits'][0]['removed']modified_files = body_json['commits'][0]['modified'] + added_files + removed_filesprint('added / removed / modified : {}'.format(modified_files))# どのプロジェクトのビルドを行うかファイルパスから判断includes = ['project1', 'project2', 'project3']pipelines_count_max = len(includes)common = ['common']10
2. Lambda 関数を作成for file_path in modified_files:pos = file_path.find('/')if pos > 0:# パスにフォルダを含む→プロジェクト名を確認project_name = file_path[:pos]if common.count(project_name) > 0:# 共有プロジェクト名であれば全て呼び出しパイプラインに含めるproject_names = includesbreakif project_names.count(project_name) == 0:# 対象プロジェクト初検出→呼び出しパイプラインに含めるproject_names.append(project_name)if len(project_names) == pipelines_count_max:# すべてのプロジェクトを検出→ループを抜けるbreak# 対象プロジェクトをビルドするパイプラインを呼び出すprint('projects : {}'.format(project_names))if len(project_names) > 0:for project_name in project_names:return_code = start_code_pipeline('{}{}'.format(project_name, job_name_suffix))print(return_code)return {'statusCode': 200,'body': json.dumps('Modified project in repo: {}'.format(project_names))}11
2. Lambda 関数を作成def get_secrets_manager_dict(secret_name: str) -> dict:"""Secrets Managerからシークレットのセットを辞書型で取得する"""secrets_dict = {}if not secret_name:print('シークレットの名前未設定')else:session = boto3.session.Session()client = session.client(service_name='secretsmanager',region_name='ap-northeast-1')try:get_secret_value_response = client.get_secret_value(SecretId=secret_name)except ClientError as e:print('シークレット取得失敗:シークレットの名前={}'.format(secret_name))print(e.response['Error'])else:if 'SecretString' in get_secret_value_response:secret = get_secret_value_response['SecretString']else:secret = base64.b64decode(get_secret_value_response['SecretBinary'])secrets_dict = ast.literal_eval(secret)return secrets_dict12
2. Lambda 関数を作成def get_secrets_manager_key_value(secret_name: str, secret_key: str) -> str:"""AWS Secrets Managerからシークレットキーの値を取得する."""value = ''secrets_dict = get_secrets_manager_dict(secret_name)if secrets_dict:if secret_key in secrets_dict:# secrets_dictが設定されていてsecret_keyがキーとして存在する場合value = secrets_dict[secret_key]else:print('シークレットキーの値取得失敗:シークレットの名前={}、シークレットキー={}'.format(secret_name, secret_key))return valuedef is_correct_signature(signature: str, body: dict) -> bool:"""GitHubから送られてきた情報をHMAC認証する."""if signature and body:# GitHubのWebhookに設定したSecretをSecrets Managerから取得するsecret = get_secrets_manager_key_value(os.environ['secrets_name'], 'GHE_SECRETS')if secret:secret_bytes = bytes(secret, 'utf-8')body_bytes = bytes(body, 'utf-8')# Secretから16進数ダイジェストを作成するsignedBody = "sha1=" + hmac.new(secret_bytes, body_bytes, hashlib.sha1).hexdigest()return signature == signedBodyelse:return False13
2. Lambda 関数を作成def start_code_pipeline(pipelineName):client = codepipeline_client()response = client.start_pipeline_execution(name=pipelineName)return Truecpclient = Nonedef codepipeline_client():global cpclientif not cpclient:cpclient = boto3.client('codepipeline')return cpclient14● こちらで公開中○ https://github.com/hmatsu47/github-monorepo-codepipeline
3. API Gateway を作成し、Lambda 関数を統合● HTTP の API Gateway を作成○ 先ほど作成した Lambda 関数を統合○ 任意の API 名を指定○ ルートのメソッドは POST に限定○ ステージ名「$default」のまま自動デプロイ指定で作成15
{"Version": "2012-10-17","Statement": [{"Sid": "VisualEditor0","Effect": "Allow","Action": ["secretsmanager:GetResourcePolicy","secretsmanager:GetSecretValue","secretsmanager:DescribeSecret","secretsmanager:ListSecretVersionIds"],"Resource": "【シークレットのARN】"},{"Sid": "VisualEditor1","Effect": "Allow","Action": ["secretsmanager:GetRandomPassword","secretsmanager:ListSecrets"],"Resource": "*"}]}4. IAM Role(Lambda 実行用)にポリシーを追加16
5. GitHub (Enterprise) で Webhook を設定● ↓を指定して作成○ Payload URL :API Gateway の URL■ https://XXX.execute-api.ap-northeast-1.amazonaws.com/【リソースパス】○ Content type :application/json○ Secret :生成したシークレット17
6. CodePipeline を設定(変更)● Source ステージで↓のチェックを外す○ 検出オプションを変更する■ ソースコードの変更時にパイプラインを開始する18
やってみた感想など● 意外と面倒○ 情報があまり出回っていない○ 本当に大変なのは GitHub Webhooks 〜 Lambda よりもCodePipeline 側○ GitHub Actions でやったほうが…● 事情で GitHub Actions が使えない場合の代替策19