モノレポのGitHub(Enterprise)からCodePipelineを呼び出す小ネタ

7.3K Views

April 16, 22

スライド概要

JAWS-UG 浜松 AWS 勉強会 2022#2 2022/2/25

profile-image

Qiita や Zenn でいろいろ書いてます。 https://qiita.com/hmatsu47 https://zenn.dev/hmatsu47 MySQL 8.0 の薄い本 : https://github.com/hmatsu47/mysql80_no_usui_hon Aurora MySQL v1 → v3 移行計画 : https://zenn.dev/hmatsu47/books/aurora-mysql3-plan-book

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

モノレポの GitHub (Enterprise) から CodePipeline を呼び出す小ネタ JAWS-UG 浜松 AWS 勉強会 2022#2 2022/2/25 まつひさ(hmatsu47)

2.

自己紹介 松久裕保(@hmatsu47) ● https://qiita.com/hmatsu47 ● 現在のステータス: ○ 名古屋で Web インフラのお守り係をしています ○ Aurora MySQL v1(5.6)の EoL が発表されたのでアップ開始 ■ https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/Aur ora.MySQL56.EOL.html ● v1 → v3 移行を画策中 2

3.

本日の小ネタ ● こちら↓の Lambda 関数を正しく動くよう実装 ○ GitHub モノレポを AWS CodePipeline と統合して、プロジェクト固有の CI/CD パイプラインを実行する(Amazon Web Services ブログ) ● Zenn で記事化済み ○ https://zenn.dev/hmatsu47/articles/73c624fb5730dd ● 参考にした記事 ○ Backlogの課題にGitHubのコミットを連携する方法(ponsuke_tarou’s blog) 3

4.

モノレポの課題 ● 開発プロジェクト(プロダクト・サービス)別にビルド →デプロイするのに手間が掛かる ○ 必要がなくても全プロジェクトをビルドパイプラインが走る ● そこで提示されたのが前掲の記事 ○ GitHub モノレポを AWS CodePipeline と統合して、プロジェクト固有の CI/CD パイプラインを実行する(Amazon Web Services ブログ) ○ ただし動作に問題がある 4

5.

何が問題? ● 対象ブランチの指定がない ○ どこのブランチに push してもパイプラインが実行されてしまう ● コードの変更以外の操作まで拾う ○ 誤動作の可能性がある ● 複数フォルダにまたがる push でも、1 本のパイプライン しか実行されない 5

6.

修正後は ● 対象ブランチの指定が可能 ● パイプライン実行対象のフォルダを指定可能 ● 複数フォルダにまたがる push で複数のパイプラインを並 列呼び出し可能 ● 例外的に全パイプラインを並列呼び出しするフォルダの 指定が可能 6

7.

設定の流れ(詳細は前掲の Zenn 記事を参照) 1. Secrets Manager にシークレットを保存 2. Lambda 関数を作成 3. API Gateway を作成し、Lambda 関数を統合 4. IAM Role(Lambda 実行用)にポリシーを追加 5. GitHub (Enterprise) で Webhook を設定 6. CodePipeline を設定(変更) 7

8.

1. Secrets Manager にシークレットを保存 ● GitHub → (API Gateway →)Lambda 認証時に使用 ○ パスワードジェネレータなどで生成 ○ Secrets Manager で「その他のシークレットのタイプ」を選択 ■ キー : GHE_SECRETS ■ 値 ■ 任意の名前を付けて保存 : 生成したシークレットの値 8

9.

2. Lambda 関数を作成 ● GitHub Webhook からのリクエストを受けて、対象の CodePipeline を呼び出す ○ コードと↓の環境変数を登録 ■ 呼び出すパイプライン名のサフィックス : job_name_suffix ● 「【フォルダ名】+【サフィックス】」の CodePipeline を呼び出します ■ シークレット名(先ほど保存したもの) : secrets_name ■ パイプライン実行対象のブランチ名 ● :trigger_branch 「refs/heads/【ブランチ名】」の形で指定 9

10.
[beta]
2. Lambda 関数を作成
import json
import hmac, hashlib
import boto3
import base64
import ast, re
import os
from botocore.exceptions import ClientError
def 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_files
print('added / removed / modified : {}'.format(modified_files))
# どのプロジェクトのビルドを行うかファイルパスから判断
includes = ['project1', 'project2', 'project3']
pipelines_count_max = len(includes)
common = ['common']

10

11.
[beta]
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 = includes
break
if 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

12.
[beta]
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_dict

12

13.
[beta]
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 value
def 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 == signedBody
else:
return False

13

14.

2. Lambda 関数を作成 def start_code_pipeline(pipelineName): client = codepipeline_client() response = client.start_pipeline_execution(name=pipelineName) return True cpclient = None def codepipeline_client(): global cpclient if not cpclient: cpclient = boto3.client('codepipeline') return cpclient ● こちらで公開中 ○ https://github.com/hmatsu47/github-monorepo-codepipeline 14

15.

3. API Gateway を作成し、Lambda 関数を統合 ● HTTP の API Gateway を作成 ○ 先ほど作成した Lambda 関数を統合 ○ 任意の API 名を指定 ○ ルートのメソッドは POST に限定 ○ ステージ名「$default」のまま自動デプロイ指定で作成 15

16.
[beta]
4. IAM Role(Lambda 実行用)にポリシーを追加
{

}

"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": "*"
}
]

16

17.

5. GitHub (Enterprise) で Webhook を設定 ● ↓を指定して作成 ○ Payload URL ■ :API Gateway の URL https://XXX.execute-api.ap-northeast-1.amazonaws.com/【リソースパス】 ○ Content type :application/json ○ Secret :生成したシークレット 17

18.

6. CodePipeline を設定(変更) ● Source ステージで↓のチェックを外す ○ 検出オプションを変更する ■ ソースコードの変更時にパイプラインを開始する 18

19.

やってみた感想など ● 意外と面倒 ○ 情報があまり出回っていない ○ 本当に大変なのは GitHub Webhooks 〜 Lambda よりも CodePipeline 側 ○ GitHub Actions でやったほうが… ● 事情で GitHub Actions が使えない場合の代替策 19