0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWS CodePipeline × 3rd Party リポジトリ:カスタムアクションで実現するシームレス連携

Last updated at Posted at 2026-01-22

はじめに

 AWSではGit互換のリポジトリとしてCodeCommitが提供されています。(CodeCommitは段階的な廃止のために新規受付を停止していましたが、2025/11/24 復活しました!)
 しかし、ワークロードはAWSでホストするけれどもソースコードに関しては別のベンダのGit リポジトリを利用するというユースケースは珍しくないかと思います。それらのAWS以外のソースリポジトリを利用するモチベーションは様々であり、今回の記事ではその部分については触れません。
 重要なのは、AWS以外のソースリポジトリでホストされているソースコードに対してCodePipelineでCI/CDを実施しようとした場合に、GitHub,GitHub Enterprise,BitBucket, GitLabに関しては連携の仕組みが用意されていますが、それ以外のリポジトリに関しては連携するためにユーザー側で仕組みを検討する必要があるという点です。
 この記事では、Azure DevOpsの機能の一つとして提供されるGit互換リポジトリと、CodePipelineを連携させるための仕組みを、CodePipelineのカスタムアクションを利用して作成したので、その仕組みについてご紹介します。
 なお、本記事の内容は、AWSブログの記事「サードパーティの Git リポジトリを AWS CodePipeline のソースとして使用するためのイベント駆動型アーキテクチャ」を参考に実装を行っています。

CodePipelineカスタムアクションとは

 CodePipelineにてデフォルトで用意されていない独自のアクションを追加することのできる機能です。詳細については、AWSから公開されているAWS公式ドキュメントや、BlackBeltの資料をご参照ください。
 カスタムアクションでは、自由度が高く様々な処理を定義できますが、ジョブの失敗時の処理などをユーザ側でハンドリングする必要があるところに注意が必要です。

アーキテクチャ

 今回作成した仕組みの概要は下の図の通りです。
architecture01.png

赤文字で番号を振ってある各リソースの役割について紹介します。

# リソース名 説明
CodePipeline Webhook Trigger CodePipelineを起動するためのWebhook URLを発行する機能を利用して、Webhookによる連携を行います。
Azure Reposでソースコードの変更があった場合、Azure Repos側の機能でその変更を検知し、CodePipelineにて発行されたURLを叩くことでCodePipelineをスタートさせます。
CodePipeline Source Stage CodePipelineのSourceステージとして、Custom Actionを実行します。
CodeBuild (Custom Action) Custom Actionが開始されると、そのイベントを検知しCodeBuildが実行されます。CodeBuildでは、SecretManagerに格納してあるAzureのSSH鍵を利用してソースコードをCloneし、ソースコード格納用のS3バケットに格納します。
Secret Manager Azure ReposにアクセスするためのSSH鍵が、Base64エンコードされた状態で格納されています。
S3 Bucket CodeBuildによってCloneされたソースコードが格納されるバケットです。
Lambda Function CodeBuildでなにかしらの失敗があった場合にのみ実行されるLambda関数です。
CodePipelineの状態をFailedに変更します。

 今回の仕組みでは、例としてAzure DevOps Repos上に格納されたCDKのソースコードをAWSのCodePipelineを利用してデプロイするサンプルとしています。
 CDKをデプロイする部分に関しては、アプリケーションソースコードのビルドなどご自身の利用したい用途に修正していただくのが良いかと思います。

CDK テンプレート

 今回は、この仕組みをCDKテンプレートとして実装しました。
 下記にカスタムのConstructとそれを呼ぶStack, Appの部分を記載します。
 カスタムアクションの部分は、使いまわしがしやすいようにカスタムのConstructとして実装しています。
 CDK ソースコードのディレクトリ構造は下記のようにしました。
※ なお、検証用のコードのためWebhookの認証を無しにしています。本番環境では利用しない(もしくは、コード修正した上で自身の責任の上で利用いただく)ようお願いします。(UNAUTHENTICATED は URL を知っていれば誰でも叩けてしまうため、実運用では HMAC 署名検証(API Gateway + Lambda 等)の導入を強く推奨します。)

ディレクトリ構造
bin
 ∟ cdk-azurerepos-pipeline.ts
lib
 ∟ constructor
   ∟ custom-pipeline-for-azuredevops.ts
 ∟ pipelines
   ∟ azuredevops-stack.ts
   ∟ pipeline-stack.ts
bin/cdk-azurerepos-pipeline.ts
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { CdkCicdPipelineStack } from '../lib/pipelines/pipeline-stack';
import { AzureDevOpsActionStack } from '../lib/pipelines/azuredevops-stack';

const app = new cdk.App({
  context: {
    pjName: 'sample',
    env: 'dev',
    version: '1'
  }
});

const azureDevOpsActionStack = new AzureDevOpsActionStack(app, app.node.tryGetContext('pjName') + app.node.tryGetContext('env') + 'AzureDevOpsActionStack', {
  stackName: 'CDKPIPELINE-' + app.node.tryGetContext('pjName') + '-' + app.node.tryGetContext('env') + '-AzureDevOpsActionStack',
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: process.env.CDK_DEFAULT_REGION
  },
  tags: {
    Env: app.node.tryGetContext('env'),
    PjName: app.node.tryGetContext('pjName')
  }
});

const cdkPipelineStack01 = new CdkCicdPipelineStack(app, app.node.tryGetContext('pjName') + app.node.tryGetContext('env') + 'cdkPipelineStack01', {
  stackName: 'CDKPIPELINE-' + app.node.tryGetContext('pjName') + '-' + app.node.tryGetContext('env') + '-cdkPipelineStack01',
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: process.env.CDK_DEFAULT_REGION
  },
  tags: {
    Env: app.node.tryGetContext('env'),
    PjName: app.node.tryGetContext('pjName')
  },
  // Azure Repo Git Repository Information
  organization: 'sample-org',
  project: 'sample',
  repo: 'sample_repo',
  branch: 'master',

  // Arn of Secretmanager that contains Azure Repo Git Credential
  sshKeySecretManagerArn: 'arn:aws:secretsmanager:<region>:<account-id>:secret:<SecretName>'
});
cdkPipelineStack01.addDependency(azureDevOpsActionStack);
/lib/pipelines/pipeline-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as codepipeline from 'aws-cdk-lib/aws-codepipeline';
import * as codebuild from 'aws-cdk-lib/aws-codebuild';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';

interface CdkCicdPipelineStackProps extends cdk.StackProps {
    // Azure Repo Git Repository Information
    organization: string,
    repo: string,
    branch: string,
    project: string,
    // Arn of Secretmanager that contains Azure Repo Git Credential
    sshKeySecretManagerArn: string
}

export class CdkCicdPipelineStack extends cdk.Stack {
  constructor (scope: Construct, id: string, props: CdkCicdPipelineStackProps) {
    super(scope, id, props);

    const pjName = this.node.tryGetContext('pjName');
    const env = this.node.tryGetContext('env');

    const pipelineName = pjName.replace('-', '') + env + 'cdkpipeline'

    const bucket = new s3.Bucket(this, pipelineName + 'ArtifactBucket');

    const powerUserAccess = iam.ManagedPolicy.fromManagedPolicyArn(this, 'PowerUserAccess', 'arn:aws:iam::aws:policy/PowerUserAccess');
    const iamFullAccess = iam.ManagedPolicy.fromManagedPolicyArn(this, 'IAMFullAccess', 'arn:aws:iam::aws:policy/IAMFullAccess');

    // IAM Role for Pipeline
    const pipelineRole = new iam.Role(this, pipelineName + 'PipelineRole', {
      roleName: pipelineName + '-PipelineRole',
      assumedBy: new iam.ServicePrincipal('codepipeline.amazonaws.com'),
      description: 'PipelineRole',
      path: '/',
      managedPolicies: [
        powerUserAccess,
        iamFullAccess
      ]
    });

    // IAM Role for CodeBuild
    const buildRole = new iam.Role(this, pipelineName + 'BuildRole', {
      roleName: pipelineName + '-BuildRole',
      assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'),
      description: 'BuildRole',
      path: '/',
      managedPolicies: [
        powerUserAccess,
        iamFullAccess
      ]
    });

    // CodeBuild Project
    const codebuildProject = new codebuild.Project(this, pipelineName + 'CdkBuildProject', {
      projectName: pipelineName + '-CdkBuildProject',
      environment: {
        buildImage: codebuild.LinuxBuildImage.STANDARD_7_0,
        computeType: codebuild.ComputeType.SMALL
      },
      buildSpec: codebuild.BuildSpec.fromObject({
        version: '0.2',
        phases: {
          install: {
            commands: [
              'pwd',
              'ls',
              'npm install -g aws-cdk'
            ]
          },
          build: {
            commands: [
              'npm ci',
              'npx eslint . --ext ts',
              'npm run build',
              'npx cdk synth',
              'npm test',
              'cdk deploy --require-approval never --all'
            ]
          }
        },
        artifacts: {
          'base-directory': 'cdk.out',
          files: '**/*'
        }
      }),
      role: buildRole
    });

    // Pipeline Actions
    const pipeline = new codepipeline.CfnPipeline(this, pipelineName + 'CdkPipeline', {
      name: pipelineName,
      roleArn: pipelineRole.roleArn,
      stages: [
        {
          name: 'Source',
          actions: [
            {
              name: 'AzureDevOps',
              actionTypeId: {
                category: 'Source',
                owner: 'Custom',
                version: this.node.tryGetContext('version'),
                provider: 'AzureDevOpsRepo'
              },
              outputArtifacts: [
                {
                  name: 'AzureCode'
                }
              ],
              configuration: {
                Organization: props.organization,
                Repo: props.repo,
                Branch: props.branch,
                Project: props.project,
                PipelineName: pipelineName,
                CredentialSecretArn: props.sshKeySecretManagerArn
              },
              runOrder: 1
            }
          ]
        },
        {
          name: 'Deploy',
          actions: [
            {
              name: 'DeployStep',
              actionTypeId: {
                category: 'Build',
                owner: 'AWS',
                version: '1',
                provider: 'CodeBuild'
              },
              inputArtifacts: [
                {
                  name: 'AzureCode'
                }
              ],
              outputArtifacts: [
                {
                  name: 'SynthStep_Output'
                }
              ],
              configuration: {
                ProjectName: codebuildProject.projectName
              },
              runOrder: 1
            }
          ]
        }
      ],
      artifactStore: {
        location: bucket.bucketName,
        type: 'S3'
      }
    });

    // pipeline webhook
    const webhook = new codepipeline.CfnWebhook(this, pipelineName + 'PipelineWebhook', {
      authenticationConfiguration: {},
      filters: [
        {
          jsonPath: '$.resource.refUpdates..name',
          matchEquals: `refs/heads/${props.branch}`
        }
      ],
      authentication: 'UNAUTHENTICATED',
      targetPipeline: pipelineName,
      targetAction: 'Source',
      name: pipelineName,
      targetPipelineVersion: 1,
      registerWithThirdParty: false
    });
    webhook.addDependsOn(pipeline);

    new cdk.CfnOutput(this, pipelineName + 'PipelineWebhookUrl', {
      value: webhook.attrUrl
    });
  }
}

/lib/constructor/pipeline-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as codepipeline from 'aws-cdk-lib/aws-codepipeline';
import * as codebuild from 'aws-cdk-lib/aws-codebuild';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';

interface CdkCicdPipelineStackProps extends cdk.StackProps {
    // Azure Repo Git Repository Information
    organization: string,
    repo: string,
    branch: string,
    project: string,
    // Arn of Secretmanager that contains Azure Repo Git Credential
    sshKeySecretManagerArn: string
}

export class CdkCicdPipelineStack extends cdk.Stack {
  constructor (scope: Construct, id: string, props: CdkCicdPipelineStackProps) {
    super(scope, id, props);

    const pjName = this.node.tryGetContext('pjName');
    const env = this.node.tryGetContext('env');

    const pipelineName = pjName.replace('-', '') + env + 'cdkpipeline'

    const bucket = new s3.Bucket(this, pipelineName + 'ArtifactBucket');

    const powerUserAccess = iam.ManagedPolicy.fromManagedPolicyArn(this, 'PowerUserAccess', 'arn:aws:iam::aws:policy/PowerUserAccess');
    const iamFullAccess = iam.ManagedPolicy.fromManagedPolicyArn(this, 'IAMFullAccess', 'arn:aws:iam::aws:policy/IAMFullAccess');

    // IAM Role for Pipeline
    const pipelineRole = new iam.Role(this, pipelineName + 'PipelineRole', {
      roleName: pipelineName + '-PipelineRole',
      assumedBy: new iam.ServicePrincipal('codepipeline.amazonaws.com'),
      description: 'PipelineRole',
      path: '/',
      managedPolicies: [
        powerUserAccess,
        iamFullAccess
      ]
    });

    // IAM Role for CodeBuild
    const buildRole = new iam.Role(this, pipelineName + 'BuildRole', {
      roleName: pipelineName + '-BuildRole',
      assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'),
      description: 'BuildRole',
      path: '/',
      managedPolicies: [
        powerUserAccess,
        iamFullAccess
      ]
    });

    // CodeBuild Project
    const codebuildProject = new codebuild.Project(this, pipelineName + 'CdkBuildProject', {
      projectName: pipelineName + '-CdkBuildProject',
      environment: {
        buildImage: codebuild.LinuxBuildImage.STANDARD_7_0,
        computeType: codebuild.ComputeType.SMALL
      },
      buildSpec: codebuild.BuildSpec.fromObject({
        version: '0.2',
        phases: {
          install: {
            commands: [
              'pwd',
              'ls',
              'npm install -g aws-cdk'
            ]
          },
          build: {
            commands: [
              'npm ci',
              'npx eslint . --ext ts',
              'npm run build',
              'npx cdk synth',
              'npm test',
              'cdk deploy --require-approval never --all'
            ]
          }
        },
        artifacts: {
          'base-directory': 'cdk.out',
          files: '**/*'
        }
      }),
      role: buildRole
    });

    // Pipeline Actions
    const pipeline = new codepipeline.CfnPipeline(this, pipelineName + 'CdkPipeline', {
      name: pipelineName,
      roleArn: pipelineRole.roleArn,
      stages: [
        {
          name: 'Source',
          actions: [
            {
              name: 'AzureDevOps',
              actionTypeId: {
                category: 'Source',
                owner: 'Custom',
                version: this.node.tryGetContext('version'),
                provider: 'AzureDevOpsRepo'
              },
              outputArtifacts: [
                {
                  name: 'AzureCode'
                }
              ],
              configuration: {
                Organization: props.organization,
                Repo: props.repo,
                Branch: props.branch,
                Project: props.project,
                PipelineName: pipelineName,
                CredentialSecretArn: props.sshKeySecretManagerArn
              },
              runOrder: 1
            }
          ]
        },
        {
          name: 'Deploy',
          actions: [
            {
              name: 'DeployStep',
              actionTypeId: {
                category: 'Build',
                owner: 'AWS',
                version: '1',
                provider: 'CodeBuild'
              },
              inputArtifacts: [
                {
                  name: 'AzureCode'
                }
              ],
              outputArtifacts: [
                {
                  name: 'SynthStep_Output'
                }
              ],
              configuration: {
                ProjectName: codebuildProject.projectName
              },
              runOrder: 1
            }
          ]
        }
      ],
      artifactStore: {
        location: bucket.bucketName,
        type: 'S3'
      }
    });

    // pipeline webhook
    const webhook = new codepipeline.CfnWebhook(this, pipelineName + 'PipelineWebhook', {
      authenticationConfiguration: {},
      filters: [
        {
          jsonPath: '$.resource.refUpdates..name',
          matchEquals: `refs/heads/${props.branch}`
        }
      ],
      authentication: 'UNAUTHENTICATED',
      targetPipeline: pipelineName,
      targetAction: 'Source',
      name: pipelineName,
      targetPipelineVersion: 1,
      registerWithThirdParty: false
    });
    webhook.addDependsOn(pipeline);

    new cdk.CfnOutput(this, pipelineName + 'PipelineWebhookUrl', {
      value: webhook.attrUrl
    });
  }
}
/lib/constructor/custom-pipeline-for-azuredevops.ts
import * as cdk from 'aws-cdk-lib';
import * as codepipeline from 'aws-cdk-lib/aws-codepipeline';
import * as codebuild from 'aws-cdk-lib/aws-codebuild';
import { Construct } from 'constructs';
import * as yaml from 'yaml';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as event from 'aws-cdk-lib/aws-events';
import * as lambda from 'aws-cdk-lib/aws-lambda';

export class CustomPipelineForAzureDevOps extends Construct {
  constructor (scope: Construct, id: string) {
    super(scope, id);

    const region = process.env.CDK_DEFAULT_REGION;
    const account = process.env.CDK_DEFAULT_ACCOUNT;

    // CodePipeline Custom Action
    new codepipeline.CustomActionRegistration(this, 'AzureDevopsActionType', {
      category: codepipeline.ActionCategory.SOURCE,
      provider: 'AzureDevOpsRepo',
      artifactBounds: {
        minInputs: 0,
        maxInputs: 0,
        minOutputs: 1,
        maxOutputs: 1
      },
      version: this.node.tryGetContext('version'),
      entityUrl: 'https://dev.azure.com/{Config:Organization}/{Config:Project}/_git/{Config:Repo}?version=GB{Config:Branch}',
      executionUrl: 'https://dev.azure.com/{Config:Organization}/{Config:Project}/_git/{Config:Repo}?version=GB{Config:Branch}',
      actionProperties: [
        {
          description: 'The name of the MS Azure DevOps Organization',
          key: false,
          name: 'Organization',
          queryable: false,
          required: true,
          secret: false,
          type: 'String'
        },
        {
          description: 'The name of the repository',
          key: true,
          name: 'Repo',
          queryable: false,
          required: true,
          secret: false,
          type: 'String'
        },
        {
          description: 'The name of the project',
          key: false,
          name: 'Project',
          queryable: false,
          required: true,
          secret: false,
          type: 'String'
        },
        {
          description: 'The tracked branch',
          key: false,
          name: 'Branch',
          queryable: false,
          required: true,
          secret: false,
          type: 'String'
        },
        {
          description: 'The name of the CodePipeline',
          key: false,
          name: 'PipelineName',
          queryable: true,
          required: true,
          secret: false,
          type: 'String'
        },
        {
          description: 'The Arn of Secretmanager that contains AzureRepo Credential',
          key: false,
          name: 'CredentialSecretArn',
          queryable: false,
          required: true,
          secret: false,
          type: 'String'
        }
      ]
    });

    // IAM Role for Invoked CodeBuild Project
    const codeBuildRole = new iam.Role(this, 'GetAzureGitCodeBuildRole', {
      roleName: 'GetAzureGitCodeBuildRole',
      assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'),
      description: 'CodePipelineCustomActionInvokeRole',
      path: '/'
    });

    // Invoked CodeBuild Project from Codepipeline Custom Action
    const fromYaml = yaml.parse(`
      version: 0.2
      env:
        exported-variables:
          - jobid
      phases:
        pre_build:
          commands:
            - echo $pipelinename
            - echo $executionid
            - wait_period=0
            - |
              while true
              do
                  jobdetail=$(aws codepipeline poll-for-jobs --action-type-id category="Source",owner="Custom",provider="AzureDevOpsRepo",version="${this.node.tryGetContext('version')}" --query-param PipelineName=$pipelinename --max-batch-size 1)
                  provider=$(echo $jobdetail | jq '.jobs[0].data.actionTypeId.provider' -r)
                  wait_period=$(($wait_period+10))
                  if [ $provider = "AzureDevOpsRepo" ];then
                    echo $jobdetail
                    break
                  fi
                  if [ $wait_period -gt 300 ];then
                    echo "Haven't found a pipeline job for 5 minutes, will stop pipeline."
                    exit 1
                  else
                    echo "No pipeline job found, will try again in 10 seconds"
                    sleep 10
                  fi
              done
            - jobid=$(echo $jobdetail | jq '.jobs[0].id' -r)
            - echo $jobid
            - ack=$(aws codepipeline acknowledge-job --job-id $(echo $jobdetail | jq '.jobs[0].id' -r) --nonce $(echo $jobdetail | jq '.jobs[0].nonce' -r))
            - Branch=$(echo $jobdetail | jq '.jobs[0].data.actionConfiguration.configuration.Branch' -r)
            - Organization=$(echo $jobdetail | jq '.jobs[0].data.actionConfiguration.configuration.Organization' -r)
            - Repo=$(echo $jobdetail | jq '.jobs[0].data.actionConfiguration.configuration.Repo' -r)
            - Project=$(echo $jobdetail | jq '.jobs[0].data.actionConfiguration.configuration.Project' -r)
            - SshKeyId=$(echo $jobdetail | jq '.jobs[0].data.actionConfiguration.configuration.CredentialSecretArn' -r)
            - ObjectKey=$(echo $jobdetail | jq '.jobs[0].data.outputArtifacts[0].location.s3Location.objectKey' -r)
            - BucketName=$(echo $jobdetail | jq '.jobs[0].data.outputArtifacts[0].location.s3Location.bucketName' -r)
            - aws secretsmanager get-secret-value --secret-id $SshKeyId --query 'SecretString' --output text | base64 --decode > ~/.ssh/id_rsa
            - chmod 600 ~/.ssh/id_rsa
            - ssh-keygen -F ssh.dev.azure.com || ssh-keyscan ssh.dev.azure.com >>~/.ssh/known_hosts
        build:
          commands:
            - git clone "git@ssh.dev.azure.com:v3/$Organization/$Project/$Repo"
            - cd $Repo
            - git checkout $Branch
            - zip -r output_file.zip * .[^.]*
            - aws s3 cp output_file.zip s3://$BucketName/$ObjectKey
            - aws codepipeline put-job-success-result --job-id $(echo $jobdetail | jq '.jobs[0].id' -r)
      artifacts:
        files:
          - '**/*'
        base-directory: '$Repo'
    `);

    const project = new codebuild.Project(this, 'GetAzureDevOpsRepoProject', {
      projectName: 'GetAzureDevOpsRepoProject',
      environment: {
        buildImage: codebuild.LinuxBuildImage.STANDARD_7_0,
        computeType: codebuild.ComputeType.SMALL,
        environmentVariables: {
          pipelinename: {
            value: 'wbf'
          },
          executionid: {
            value: 'wbf'
          }
        }
      },
      buildSpec: codebuild.BuildSpec.fromObjectToYaml(fromYaml),
      role: codeBuildRole
    });

    const codeBuildPolicy = new iam.ManagedPolicy(this, 'GetAzureGitCodeBuildPolicy', {
      managedPolicyName: 'GetAzureGitCodeBuildPolicy',
      document: new iam.PolicyDocument({
        statements: [
          new iam.PolicyStatement({
            sid: 'logs',
            actions: [
              'logs:CreateLogGroup',
              'logs:CreateLogStream',
              'logs:PutLogEvents'
            ],
            effect: iam.Effect.ALLOW,
            resources: [
              'arn:aws:logs:' + region + ':' + account + ':log-group:/aws/codebuild/' + project.projectName,
              'arn:aws:logs:' + region + ':' + account + ':log-group:/aws/codebuild/' + project.projectName + ':*'
            ]
          }),
          new iam.PolicyStatement({
            sid: 'codebuild',
            actions: [
              'codebuild:BatchPutCodeCoverages',
              'codebuild:BatchPutTestCases',
              'codebuild:CreateReport',
              'codebuild:CreateReportGroup',
              'codebuild:UpdateReport'
            ],
            effect: iam.Effect.ALLOW,
            resources: [
              'arn:aws:codebuild:' + region + ':' + account + ':report-group/' + project.projectName + '-*'
            ]
          }),
          new iam.PolicyStatement({
            sid: 'codepipeline',
            actions: [
              'codepipeline:AcknowledgeJob',
              'codepipeline:PollForJobs',
              'codepipeline:PutJobSuccessResult',
              'codepipeline:StopPipelineExecution'
            ],
            effect: iam.Effect.ALLOW,
            resources: ['*']
          }),
          new iam.PolicyStatement({
            sid: 'secretsmanager',
            actions: [
              'secretsmanager:GetSecretValue'
            ],
            effect: iam.Effect.ALLOW,
            resources: ['*']
          }),
          new iam.PolicyStatement({
            sid: 's3',
            actions: [
              's3:GetBucketLocation',
              's3:ListBucket',
              's3:PutObject'
            ],
            effect: iam.Effect.ALLOW,
            resources: [
              'arn:aws:s3:::*'
            ]
          })
        ]
      })
    });
    const cfnCodeBuildPolicy = codeBuildPolicy.node.defaultChild as iam.CfnManagedPolicy;
    cfnCodeBuildPolicy.addPropertyOverride('Roles', [codeBuildRole.roleName]);

    // Invoke Rule for CodeBuild Project
    const cloudWatchEventPolicy = new iam.ManagedPolicy(this, 'CodePipelineActionPolicy', {
      managedPolicyName: 'CodePipelineCustomActionInvokePolicy',
      document: new iam.PolicyDocument({
        statements: [
          new iam.PolicyStatement({
            sid: 'codebuild',
            actions: ['codebuild:StartBuild'],
            effect: iam.Effect.ALLOW,
            resources: [project.projectArn]
          })
        ]
      })
    });

    const cloudWatchEventRole = new iam.Role(this, 'CodePipelineActionRole', {
      roleName: 'CodePipelineCustomActionInvokeRole',
      assumedBy: new iam.ServicePrincipal('events.amazonaws.com'),
      description: 'CodePipelineCustomActionInvokeRole',
      path: '/',
      managedPolicies: [cloudWatchEventPolicy]
    });

    new event.CfnRule(this, 'CodePipelineActionExecutionStateChange', {
      name: 'CodePipelineActionExecutionStateChange',
      eventPattern: {
        source: ['aws.codepipeline'],
        'detail-type': ['CodePipeline Action Execution State Change'],
        detail: {
          state: ['STARTED'],
          type: {
            provider: ['AzureDevOpsRepo']
          }
        }
      },
      targets: [
        {
          arn: project.projectArn,
          id: 'triggerjobworker',
          roleArn: cloudWatchEventRole.roleArn,
          inputTransformer: {
            inputPathsMap: {
              executionid: '$.detail.execution-id',
              pipelinename: '$.detail.pipeline'
            },
            inputTemplate: '{"environmentVariablesOverride": [{"name": "executionid", "type": "PLAINTEXT", "value": <executionid>},{"name": "pipelinename", "type": "PLAINTEXT", "value": <pipelinename>}]}'
          }
        }
      ]
    });

    // Lambda Project used when Build Fails
    const buildFailedLambdaExecutionPolicy = new iam.ManagedPolicy(this, 'BuildFailedLambdaExecutionPolicy', {
      managedPolicyName: 'BuildFailedLambdaExecutionPolicy',
      document: new iam.PolicyDocument({
        statements: [
          new iam.PolicyStatement({
            sid: 'codepipeline',
            actions: [
              'codepipeline:PutJobSuccessResult',
              'codepipeline:PutJobFailureResult',
              'codepipeline:StopPipelineExecution'
            ],
            effect: iam.Effect.ALLOW,
            resources: ['*']
          }),
          new iam.PolicyStatement({
            sid: 'logs',
            actions: [
              'logs:CreateLogGroup',
              'logs:CreateLogStream',
              'logs:PutLogEvents'
            ],
            effect: iam.Effect.ALLOW,
            resources: ['arn:aws:logs:*:*:*']
          })
        ]
      })
    });

    const buildFailedLambdaExecutionRole = new iam.Role(this, 'BuildFailedLambdaExecutionRole', {
      roleName: 'BuildFailedLambdaExecutionRole',
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
      description: 'BuildFailedLambdaExecutionRole',
      path: '/',
      managedPolicies: [buildFailedLambdaExecutionPolicy]
    });

    const buildFailedLambda = new lambda.Function(this, 'BuildFailedLambda', {
      handler: 'index.lambda_handler',
      runtime: lambda.Runtime.PYTHON_3_12,
      timeout: cdk.Duration.seconds(25),
      code: lambda.Code.fromAsset('lambda'),
      role: buildFailedLambdaExecutionRole
    });

    // Invoke Rule for Lambda Project (Failed case)
    const buildFailedRule = new event.CfnRule(this, 'cloudWatchEventBuildFailedRule', {
      name: 'cloudWatchEventBuildFailedRule',
      eventPattern: {
        source: ['aws.codebuild'],
        'detail-type': ['CodeBuild Build State Change'],
        detail: {
          'build-status': ['FAILED'],
          'project-name': [project.projectName]
        }
      },
      targets: [
        {
          arn: buildFailedLambda.functionArn,
          id: 'failtrigger',
          inputTransformer: {
            inputPathsMap: {
              loglink: '$.detail.additional-information.logs.deep-link',
              'environment-variables': '$.detail.additional-information.environment.environment-variables',
              'exported-environment-variables': '$.detail.additional-information.exported-environment-variables'
            },
            inputTemplate: '{"loglink": <loglink>, "environment-variables": <environment-variables>, "exported-environment-variables": <exported-environment-variables>}'
          }
        }
      ]
    });

    new lambda.CfnPermission(this, 'lambdaPermission', {
      action: 'lambda:InvokeFunction',
      principal: 'events.amazonaws.com',
      functionName: buildFailedLambda.functionName,
      sourceArn: buildFailedRule.attrArn
    });
  }
}

事前準備その他

CDKテンプレートをデプロイする前にいくつか事前準備と、デプロイ後にAzure Repos側で設定が必要です。

Azure DevOps Reposの情報を取得

Azure DevOps Reposの対象のリポジトリに関する次の情報を、bin/cdk-azurerepos-pipeline.tsのcdkPipelineStack01の引数に指定する必要があります。(// Azure Repo Git Repository Informationと書いてあるところ)

organiztion: <Azure DevOpsのOrganization名>
project: <Azure DevOpsのProject名>
repo: <Azure DevOps Reposの対象のリポジトリ名>
branch: <Azure DevOps Reposの対象のブランチ名>

Azure DevOpsのSSH Keyの情報をAWS SecretManagerに登録

手元でSSH Keyを作成し、公開鍵の内容をAzure DevOpsに登録します。
秘密鍵の内容をAWS SecretManagerに登録し、bin/cdk-azurerepos-pipeline.tsのsshKeySecretManagerArnという引数にARNを指定してください。

Azure DevOps側でのWebhookの設定

CDKのデプロイが完了するとWebhookのURLがコンソールに表示されます。Azure DevOps側では、Project SettingsからServiceHooksを新規作成し、Webhook形式として表示されたURLを対象に設定し、CodePushを契機とすることでPush時にCodePipelineが動作するようになります。

おわりに

いかがでしたでしょうか?
ここまで AWS 側の準備を中心に仕組みをご紹介してきましたが、全体としては「外部リポジトリの更新を AWS で受け取り、自動でデプロイにつなげる」ための土台づくりと言えます。カスタムアクションや各種リソースを CDK でまとめて構築することで、環境の再現性と運用のしやすさが向上します。AWS 側が整ったら、最後に外部リポジトリ側で Webhook を設定し、Push を契機に CI/CD が流れるようにすれば完成です。
これでAWS CodePipelineと3rdPartyのGitリポジトリとの連携が可能となりました。
様々な環境を組み合わせて開発環境を構築されている皆様のCICD導入のご参考になれば幸いです。

記載されている会社名、製品名、サービス名、ロゴ等は各社の商標または登録商標です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?