Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

hmatsu47
PRO
February 23, 2022

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

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

hmatsu47
PRO

February 23, 2022
Tweet

More Decks by hmatsu47

Other Decks in Technology

Transcript

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  10. 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

    View Slide

  11. 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

    View Slide

  12. 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

    View Slide

  13. 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

    View Slide

  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
    14
    ● こちらで公開中
    ○ https://github.com/hmatsu47/github-monorepo-codepipeline

    View Slide

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

    View Slide

  16. {
    "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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide