Tools: CI/CD with AWS CodePipeline and CodeBuild (2026)

Tools: CI/CD with AWS CodePipeline and CodeBuild (2026)

Overview

Pipeline Flow

CloudFormation Stack

CodeStar Connection (GitHub App)

Artifact Bucket

CodeBuild Project

CodePipeline

IAM Roles

CodePipeline Role

CodeBuild Role

buildspec.yml

Key points

Troubleshooting the GitHub Connection

Deployment Timeline Every push to the main branch on GitHub automatically builds the React app and deploys it to S3 + CloudFront — zero manual steps. This article covers how that pipeline is wired together using AWS CodePipeline, CodeBuild, and GitHub via CodeStar Connections. The pipeline infrastructure is defined in infra/stacks/pipeline.yml. Manual step required: After deploying the stack, you must go to AWS Console → CodePipeline → Settings → Connections and click "Update pending connection" to authorize the GitHub App. This cannot be automated — AWS requires explicit human approval to grant access to your GitHub account. When authorizing, install the AWS Connector for GitHub app on your GitHub account and grant it access to the specific repository. "Connect as a GitHub user" only works for CodeBuild — CodePipeline requires the GitHub App. Intermediate pipeline artifacts (source zip, build output) are stored in a private S3 bucket: DetectChanges: true means CodePipeline automatically triggers on every push to the configured branch — no webhooks to configure manually. Two IAM roles are needed: one for CodePipeline, one for CodeBuild. Principle of least privilege — CodeBuild can only write to the specific S3 bucket and invalidate the specific CloudFront distribution. The build specification lives in the repo root and tells CodeBuild exactly what to do: npm ci not npm install

npm ci installs exactly what's in package-lock.json and fails if there are any discrepancies. This ensures deterministic builds — the same packages every time, in every environment. --delete flag on s3 syncRemoves files from S3 that no longer exist in the build output. Without this, deleted pages or renamed assets would stay in S3 forever and get served to users. CloudFront invalidationVite includes content hashes in asset filenames (index-BX7FeaXh.js), so JS/CSS files are automatically cache-busted. However, index.html itself doesn't have a hash — it must be explicitly invalidated so CloudFront fetches the new version immediately. VITE_CONTACT_API_URL env varVite's import.meta.env.VITE_* variables are replaced at build time (not runtime). The API Gateway URL is injected by CodeBuild as an environment variable and baked into the built JS bundle. This means the frontend always has the correct endpoint URL without any runtime configuration. The CodeStar Connection requires careful setup. Common issues encountered: Status: PENDING after stack deployExpected. You must visit the AWS Console to authorize it. Cannot be done via CLI. "No Branch found" errorThe GitHub App was authorized but the private repository wasn't explicitly granted access. Fix: GitHub → Settings → Applications → AWS Connector for GitHub → Configure → add the repo. "Role does not have sufficient permissions" error

After replacing a broken connection with a new one, the CodePipeline IAM role policy still referenced the old connection ARN. Fix: update the codestar-connections:UseConnection resource ARN in the IAM policy to match the new connection ARN. From git push to live site: Full deployment in under two minutes on every push to main. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

GitHubConnection: Type: AWS::CodeStarConnections::Connection Properties: ConnectionName: bonefish-github ProviderType: GitHub GitHubConnection: Type: AWS::CodeStarConnections::Connection Properties: ConnectionName: bonefish-github ProviderType: GitHub GitHubConnection: Type: AWS::CodeStarConnections::Connection Properties: ConnectionName: bonefish-github ProviderType: GitHub ArtifactBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub 'bonefish-pipeline-artifacts-${AWS::AccountId}' VersioningConfiguration: Status: Enabled PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true ArtifactBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub 'bonefish-pipeline-artifacts-${AWS::AccountId}' VersioningConfiguration: Status: Enabled PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true ArtifactBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub 'bonefish-pipeline-artifacts-${AWS::AccountId}' VersioningConfiguration: Status: Enabled PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true BuildProject: Type: AWS::CodeBuild::Project Properties: Name: bonefish-build ServiceRole: !GetAtt CodeBuildRole.Arn Artifacts: Type: CODEPIPELINE Environment: Type: LINUX_CONTAINER ComputeType: BUILD_GENERAL1_SMALL Image: aws/codebuild/standard:7.0 EnvironmentVariables: - Name: S3_BUCKET Value: !Ref S3BucketName - Name: DISTRIBUTION_ID Value: !Ref DistributionId Source: Type: CODEPIPELINE BuildSpec: buildspec.yml BuildProject: Type: AWS::CodeBuild::Project Properties: Name: bonefish-build ServiceRole: !GetAtt CodeBuildRole.Arn Artifacts: Type: CODEPIPELINE Environment: Type: LINUX_CONTAINER ComputeType: BUILD_GENERAL1_SMALL Image: aws/codebuild/standard:7.0 EnvironmentVariables: - Name: S3_BUCKET Value: !Ref S3BucketName - Name: DISTRIBUTION_ID Value: !Ref DistributionId Source: Type: CODEPIPELINE BuildSpec: buildspec.yml BuildProject: Type: AWS::CodeBuild::Project Properties: Name: bonefish-build ServiceRole: !GetAtt CodeBuildRole.Arn Artifacts: Type: CODEPIPELINE Environment: Type: LINUX_CONTAINER ComputeType: BUILD_GENERAL1_SMALL Image: aws/codebuild/standard:7.0 EnvironmentVariables: - Name: S3_BUCKET Value: !Ref S3BucketName - Name: DISTRIBUTION_ID Value: !Ref DistributionId Source: Type: CODEPIPELINE BuildSpec: buildspec.yml Pipeline: Type: AWS::CodePipeline::Pipeline Properties: Name: bonefish-website-pipeline RoleArn: !GetAtt PipelineRole.Arn PipelineType: V2 Stages: - Name: Source Actions: - Name: GitHub ActionTypeId: Category: Source Owner: AWS Provider: CodeStarSourceConnection Version: '1' Configuration: ConnectionArn: !Ref GitHubConnection FullRepositoryId: !Sub '${GitHubOwner}/${GitHubRepo}' BranchName: !Ref GitHubBranch DetectChanges: true OutputArtifacts: - Name: SourceArtifact - Name: Build Actions: - Name: BuildAndDeploy ActionTypeId: Category: Build Owner: AWS Provider: CodeBuild Version: '1' Configuration: ProjectName: !Ref BuildProject InputArtifacts: - Name: SourceArtifact Pipeline: Type: AWS::CodePipeline::Pipeline Properties: Name: bonefish-website-pipeline RoleArn: !GetAtt PipelineRole.Arn PipelineType: V2 Stages: - Name: Source Actions: - Name: GitHub ActionTypeId: Category: Source Owner: AWS Provider: CodeStarSourceConnection Version: '1' Configuration: ConnectionArn: !Ref GitHubConnection FullRepositoryId: !Sub '${GitHubOwner}/${GitHubRepo}' BranchName: !Ref GitHubBranch DetectChanges: true OutputArtifacts: - Name: SourceArtifact - Name: Build Actions: - Name: BuildAndDeploy ActionTypeId: Category: Build Owner: AWS Provider: CodeBuild Version: '1' Configuration: ProjectName: !Ref BuildProject InputArtifacts: - Name: SourceArtifact Pipeline: Type: AWS::CodePipeline::Pipeline Properties: Name: bonefish-website-pipeline RoleArn: !GetAtt PipelineRole.Arn PipelineType: V2 Stages: - Name: Source Actions: - Name: GitHub ActionTypeId: Category: Source Owner: AWS Provider: CodeStarSourceConnection Version: '1' Configuration: ConnectionArn: !Ref GitHubConnection FullRepositoryId: !Sub '${GitHubOwner}/${GitHubRepo}' BranchName: !Ref GitHubBranch DetectChanges: true OutputArtifacts: - Name: SourceArtifact - Name: Build Actions: - Name: BuildAndDeploy ActionTypeId: Category: Build Owner: AWS Provider: CodeBuild Version: '1' Configuration: ProjectName: !Ref BuildProject InputArtifacts: - Name: SourceArtifact PipelineRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: codepipeline.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: PipelinePolicy PolicyDocument: Statement: - Sid: ArtifactBucket Effect: Allow Action: [s3:GetObject, s3:PutObject, s3:GetObjectVersion, s3:GetBucketVersioning] Resource: [!Sub '${ArtifactBucket.Arn}', !Sub '${ArtifactBucket.Arn}/*'] - Sid: CodeBuild Effect: Allow Action: [codebuild:BatchGetBuilds, codebuild:StartBuild] Resource: !GetAtt BuildProject.Arn - Sid: CodeStarConnection Effect: Allow Action: [codestar-connections:UseConnection] Resource: !Ref GitHubConnection PipelineRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: codepipeline.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: PipelinePolicy PolicyDocument: Statement: - Sid: ArtifactBucket Effect: Allow Action: [s3:GetObject, s3:PutObject, s3:GetObjectVersion, s3:GetBucketVersioning] Resource: [!Sub '${ArtifactBucket.Arn}', !Sub '${ArtifactBucket.Arn}/*'] - Sid: CodeBuild Effect: Allow Action: [codebuild:BatchGetBuilds, codebuild:StartBuild] Resource: !GetAtt BuildProject.Arn - Sid: CodeStarConnection Effect: Allow Action: [codestar-connections:UseConnection] Resource: !Ref GitHubConnection PipelineRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: codepipeline.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: PipelinePolicy PolicyDocument: Statement: - Sid: ArtifactBucket Effect: Allow Action: [s3:GetObject, s3:PutObject, s3:GetObjectVersion, s3:GetBucketVersioning] Resource: [!Sub '${ArtifactBucket.Arn}', !Sub '${ArtifactBucket.Arn}/*'] - Sid: CodeBuild Effect: Allow Action: [codebuild:BatchGetBuilds, codebuild:StartBuild] Resource: !GetAtt BuildProject.Arn - Sid: CodeStarConnection Effect: Allow Action: [codestar-connections:UseConnection] Resource: !Ref GitHubConnection CodeBuildRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: codebuild.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: CodeBuildPolicy PolicyDocument: Statement: - Sid: Logs Effect: Allow Action: [logs:CreateLogGroup, logs:CreateLogStream, logs:PutLogEvents] Resource: '*' - Sid: ArtifactBucket Effect: Allow Action: [s3:GetObject, s3:PutObject, s3:GetObjectVersion] Resource: !Sub '${ArtifactBucket.Arn}/*' - Sid: WebsiteSync Effect: Allow Action: [s3:PutObject, s3:DeleteObject, s3:GetObject, s3:ListBucket] Resource: - !Sub 'arn:aws:s3:::${S3BucketName}' - !Sub 'arn:aws:s3:::${S3BucketName}/*' - Sid: CloudFrontInvalidation Effect: Allow Action: [cloudfront:CreateInvalidation] Resource: !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/${DistributionId}' CodeBuildRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: codebuild.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: CodeBuildPolicy PolicyDocument: Statement: - Sid: Logs Effect: Allow Action: [logs:CreateLogGroup, logs:CreateLogStream, logs:PutLogEvents] Resource: '*' - Sid: ArtifactBucket Effect: Allow Action: [s3:GetObject, s3:PutObject, s3:GetObjectVersion] Resource: !Sub '${ArtifactBucket.Arn}/*' - Sid: WebsiteSync Effect: Allow Action: [s3:PutObject, s3:DeleteObject, s3:GetObject, s3:ListBucket] Resource: - !Sub 'arn:aws:s3:::${S3BucketName}' - !Sub 'arn:aws:s3:::${S3BucketName}/*' - Sid: CloudFrontInvalidation Effect: Allow Action: [cloudfront:CreateInvalidation] Resource: !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/${DistributionId}' CodeBuildRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: codebuild.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: CodeBuildPolicy PolicyDocument: Statement: - Sid: Logs Effect: Allow Action: [logs:CreateLogGroup, logs:CreateLogStream, logs:PutLogEvents] Resource: '*' - Sid: ArtifactBucket Effect: Allow Action: [s3:GetObject, s3:PutObject, s3:GetObjectVersion] Resource: !Sub '${ArtifactBucket.Arn}/*' - Sid: WebsiteSync Effect: Allow Action: [s3:PutObject, s3:DeleteObject, s3:GetObject, s3:ListBucket] Resource: - !Sub 'arn:aws:s3:::${S3BucketName}' - !Sub 'arn:aws:s3:::${S3BucketName}/*' - Sid: CloudFrontInvalidation Effect: Allow Action: [cloudfront:CreateInvalidation] Resource: !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/${DistributionId}' version: 0.2 env: variables: VITE_CONTACT_API_URL: "" # overridden by CodeBuild project env var phases: install: runtime-versions: nodejs: 20 commands: - npm ci build: commands: - npm run build post_build: commands: - aws s3 sync dist/ s3://$S3_BUCKET --delete - aws cloudfront create-invalidation --distribution-id $DISTRIBUTION_ID --paths "/*" version: 0.2 env: variables: VITE_CONTACT_API_URL: "" # overridden by CodeBuild project env var phases: install: runtime-versions: nodejs: 20 commands: - npm ci build: commands: - npm run build post_build: commands: - aws s3 sync dist/ s3://$S3_BUCKET --delete - aws cloudfront create-invalidation --distribution-id $DISTRIBUTION_ID --paths "/*" version: 0.2 env: variables: VITE_CONTACT_API_URL: "" # overridden by CodeBuild project env var phases: install: runtime-versions: nodejs: 20 commands: - npm ci build: commands: - npm run build post_build: commands: - aws s3 sync dist/ s3://$S3_BUCKET --delete - aws cloudfront create-invalidation --distribution-id $DISTRIBUTION_ID --paths "/*"