← ResourcesDEVOPS Β· TOOLS

CI/CD tools and stack for AWS teams

Native CodePipeline, GitHub Actions, or Jenkins: choosing the ecosystem that minimizes friction for your team.

STRALYA16 min readJuly 2026

Why native AWS and third-party CI/CD tools are not equivalent in the same context When a team already operates on AWS, the choice of CI/CD tools is never neutral. Native services such as AWS CodePipeline offer direct integration with IAM roles, VPCs, S3 artifacts, and CloudFormation, with no network overhead or third-party authentication to maintain. On the other hand, platforms such as GitLab CI or GitHub Actions embody the "everything in Git" convergence, reducing the number of authentication points and simplifying the experience for developers who already live in these forges. Jenkins, older and agnostic, allows maximum freedom but requires internal expertise to leverage AWS without leaving traces of manual configuration behind. The concrete question is not "which tool is best" but "which ecosystem minimizes friction and technical debt for your specific team." A startup that invested in GitHub from day one will find no interest in switching to CodePipeline just because it runs on AWS. Conversely, an AWS-first team with complex pipelines (multi-account, cross-team approvals, automated blue-green deployment) will find in native CodePipeline a single source of truth without externalizing the orchestration logic. This first section sets the key lens: the ideal CI/CD tool exists only relative to your team's context (size, expertise, existing forges, AWS topology) and your pipeline ambition (light validations or complex orchestration). The rest of the article structures how to evaluate this relevance and configure the chosen option so that it becomes a productivity multiplier and not a maintenance sink. ## CodePipeline, CodeBuild, and CodeDeploy: the native AWS ecosystem and its strengths The trio of CodePipeline, CodeBuild, and CodeDeploy forms the backbone of native AWS CI/CD. CodePipeline orchestrates the workflow, CodeBuild compiles and tests your code, and CodeDeploy pushes the artifacts to the targets (EC2, on-premises, Lambda). The real strength here is the deep integration with IAM. You create only a single IAM role for the pipeline; it gets transparent access to S3 buckets (artifact storage), ECR registries, CloudFormation, and KMS for encryption. No AWS access keys to externalize, no secrets to inject via plugins. It is credential-less, secure by default. Second asset: the flexibility of the stages. Each stage can call a native action (deploy a Lambda, invoke a Step Functions function, execute a CloudFormation changeset) or a custom action (a shell script in CodeBuild, a custom Lambda call). You thus compose complex orchestrations without leaving the AWS ecosystem. A multi-account pipeline to deploy from a CI "hub" account to isolated application accounts becomes trivial: cross-account IAM + role assumption in CodePipeline, done. Third point: the native handling of GitHub/GitLab/Bitbucket webhooks. A commit on main automatically triggers the pipeline without having to poll a URL or configure an external runner. For teams that already operate on these forges, it is a practical seam that avoids an additional layer. The limitation? CodePipeline is not an intuitive interface for beginners. The visual editor is functional but not very flexible; most of the time, you will write CloudFormation or Terraform to version your pipeline-as-code. CodeBuild itself also requires a certain familiarity with buildspecs (the YML file that describes the build phases) and IAM roles. No magic, a lot of explicit configuration. ## GitHub Actions and GitLab CI: the "everything in Git" convergence and its operational implications GitHub Actions and GitLab CI flip the equation: instead of a separate CI/CD tool that queries your forge, you describe the workflow directly in a .github/workflows folder or a .gitlab-ci.yml versioned with your code. The pipeline and the code live in the same place. For a distributed team or a startup that moves fast, this is a blessing. Changing the test strategy? Modify the .yml, open a PR, review it with the code concerned, merge. No need to go configure a separate Jenkins or CodePipeline interface. The friction is minimal. GitHub Actions, in particular, has the advantage of maturity: the marketplace actions (AWS CLI, Terraform, Docker, Snyk, etc.) are maintained by their authors themselves, tested at scale, and often very well documented. A simple example: using the 'aws-actions/configure-aws-credentials' action to automatically authenticate your runner to AWS via OpenID Connect, without creating long-lived AWS access keys. It is a security best practice made trivial. GitLab CI pushes the model even further with self-hosted runners, native artifacts, and a particularly readable pipeline syntax. For teams that use self-hosted GitLab (on AWS or elsewhere), GitLab CI becomes a strategic tool, not just a convenience. The limitation of this approach: the workflow file grows. Teams often end up extracting logic into shell scripts or reusable actions, creating secondary maintenance. Also, if your team is not already on GitHub or GitLab (for example, you use Bitbucket), this convergence loses its appeal. And for very complex multi-account orchestrations or cross-team approvals, you will have to piece things together with webhooks and API calls via custom actions, which loses elegance compared to native CodePipeline. Last point: the runners. GitHub Actions offers runners hosted by GitHub, free for public repos, paid beyond a threshold for private ones. GitLab CI offers the same but with the flexibility of self-hosted runners. If your code must run in a VPC (access to private databases, for example), a hosted runner is not enough; you have to self-host or use a VPN tunnel, an additional layer of complexity. ## Jenkins and other third-party solutions: when agility is worth the maintenance Jenkins remains the historical tool preferred by teams that have operated Jenkins for 5, 10, or 15 years. It is on-premise, self-hosted, "you control everything." That also means you maintain everything: the Jenkins updates, the plugins, the server security, the encryption of credentials, the backup of the configuration. The real strength of Jenkins is flexibility. Thousands of plugins make it possible to integrate any service: deploy to AWS, Kubernetes, Azure, on-prem, send a Slack message, validate a PR, invoke a custom webhook. If your team needs to orchestrate heterogeneous systems (partial on-prem deployment, partial AWS, with complex validations between the two), Jenkins can do it without major limitation. In an AWS context specifically, Jenkins generally runs on a self-hosted EC2 instance. This creates several points of friction. First, you host a single point of failure: if the Jenkins server crashes, all your pipelines stop. You must therefore either maintain a very robust Jenkins cluster (master + agents, encryption, regular backup), or accept the downtime. Second, you create maintenance debt: every Jenkins update, every slave agent creation, every performance adjustment becomes internal work. Around 2015-2018, the industry began to use CloudBees (Jenkins hosted in the cloud) or GitLab Runner/GitHub Actions to eliminate this maintenance. But large organizations that invested in on-prem Jenkins often continue for reasons of inertia, and that is not illegitimate if the DevOps team has the expertise to maintain it. In practice, if you start from scratch and operate on AWS, choosing Jenkins means: you accept an expense in infrastructure and maintenance to benefit from a flexibility that CodePipeline or GitHub Actions do not give you. It is a conscious trade-off, rarely the "best option" for a startup or an SMB. ## Practical configuration and integration with AWS, version by version Once the tool is chosen, its configuration must follow the principles of good pipeline hygiene: no hardcoded credentials, infrastructure-as-code, centralized logs, versioned artifacts. With CodePipeline, you define everything in CloudFormation or Terraform. A typical example: a Pipeline CloudFormation resource that specifies the Source stage (with a GitHub webhook), the Build stage (CodeBuild with a buildspec.yml file), and the Deploy stage (CloudFormation changeset). You version this template in Git, review the changes like code, and trigger the pipeline creation via another bootstrap pipeline. This avoids manual clicks and creates a real single source of truth. With GitHub Actions, you directly version the .github/workflows/deploy.yml. A simple pattern: check out the code, install the tools (AWS CLI, Terraform, etc.), authenticate to AWS via OIDC (OpenID Connect), and run the build and deployment steps. OIDC is key: it eliminates the need to create an AWS access key that lives in GitHub Secrets, a source of risk if leaked. With GitLab CI, the .gitlab-ci.yml follows a similar structure but with explicit stages. Self-hosted runners can pull Docker images (e.g. aws-cli, terraform) directly from private ECR, which completes the "everything on AWS" experience. In all cases, three critical elements to lock down: (1) Authentication: use cross-account IAM roles when possible, OIDC for GitHub/GitLab, never long-lived AWS keys in secrets. (2) Artifacts: store in a dedicated S3 bucket, KMS-encrypted, with a retention and automatic cleanup policy. (3) Logs: redirect all build logs (CodeBuild, actions, steps) to CloudWatch or a centralized ELK, for audit and post-mortem debugging. One final point: the approaches also vary on deployment. CodePipeline integrates natively with CloudFormation (you can approve a changeset before applying it), or with CodeDeploy if you prefer a script-based approach. GitHub Actions and GitLab CI often deploy via calls to APIs (Terraform Cloud, ArgoCD, custom AWS Lambda) that you maintain. This gives more control but requires more piecing together. The common pitfall: choosing the tool without thinking about the infrastructure that supports it. A GitHub Actions pipeline that must access an RDS database in a VPC will require either self-hosting the runners (overhead), or using a VPN (friction), or exposing the database publicly (no, thanks). With CodePipeline + CodeBuild, you launch the build directly in VPC subnets, no problem. These architectural details are not cosmetic; they become permanent points of friction. ## Selection criteria: how to prioritize according to your specific AWS context You now have a view of three CI/CD worlds. How do you rank them for your precise situation? The first criterion is often the most obvious: where does your source code live today? If you have 20 repos on GitHub with teams that live in the GitHub interface, forcing CodePipeline means creating friction between "where I merge my code" and "where I see my pipeline," two different places. That is permanent cognitive round-trips. Conversely, GitHub Actions closes the loop. A single argument that often justifies a decision on its own. Second criterion: the complexity of your deployments. If you have a simple workflow (build Docker, push ECR, redeploy a Lambda or ECS task), any tool suffices. GitHub Actions is 30 lines of readable YAML. If you must orchestrate cross-team approvals, multi-account deployments (with role assumption), a validation by infrastructure-as-code and CloudFormation changeset, and automatic rollbacks, that is where native CodePipeline begins to show its value. It is not that GitHub Actions cannot do it, it is that you will write a lot of custom logic that is better expressed in CodePipeline with its ready-made Actions. Third criterion: internal expertise. If you have a complex VPC, strict compliance requirements, deep native AWS experience, CodePipeline aligns with your mental model. If your team is versatile, lives more in Docker and Kubernetes, less sharp on AWS IAM/CloudFormation, GitHub Actions is closer to the "infrastructure as code" that these teams know (Dockerfiles, Helm, YAML in Git). Fourth criterion: compliance and audit. In highly regulated industries (finance, healthcare), CodePipeline can be more comfortable because it lives entirely in AWS, so a single AWS CloudTrail audit trail is enough. With GitHub Actions, you will have GitHub logs, AWS logs, and potentially self-hosted runner logs to cross-reference during an inspection. Not impossible, but more surfaces. Fifth criterion: future flexibility. If your roadmap eventually includes multi-cloud (AWS + Azure, for example), GitHub Actions and GitLab CI absorb this evolution better because they are not AWS-native. With CodePipeline, adding an Azure deployment step will become more laborious. In practice, here is a simple recommendation matrix: a GitHub-first team with simple deployments? GitHub Actions. An AWS-first team, orchestrated deployments, multi-account? CodePipeline. A distributed team, complex legacy, need for maximum flexibility? Self-hosted Jenkins or CloudBees. A small team, just started on AWS? GitHub Actions for speed, migrate to CodePipeline later if orchestration becomes critical. ## Common pitfalls and antipatterns in adopting CI/CD tools on AWS Teams that migrate to AWS or set up their first serious pipeline often run into the same mistakes. First pitfall: forgetting that the tool only does orchestration. If your buildspec.yml or your GitHub Actions workflow contains manual 'aws s3 cp' commands, hardcoded AWS secret keys, or complex bash to do a "real" deployment, you do not have a robust pipeline, you have a fragile script hidden in a CI/CD tool. Best practices (infrastructure-as-code via Terraform or CloudFormation, versioned repositories, explicit approvals) must carry the weight of the deployment, not the CI/CD script. The tool must be thin, orchestrating higher-order abstractions. Second pitfall: logs that get lost. A CodeBuild build writes logs to CloudWatch, but when something breaks at 3 a.m., you have to dig through CloudWatch, then look at separate CodeDeploy logs, then read the ECS/Lambda application logs. Almost no one emits the logs of a pipeline in a single place. The solution: define a convention from the start. All pipeline and build logs go to a centralizing ElasticSearch or CloudWatch Logs index (not just the CloudWatch default), queryable and with alerts. You save hours of debugging later. Third pitfall: leaking credentials. An AWS key hardcoded in a buildspec.yml committed by accident, a GitHub secret exposed publicly, a Jenkins token in a visible build log. With OIDC (GitHub Actions) or cross-account IAM roles (CodePipeline), these leaks become pointless. But many teams do not know that this option exists and continue to generate long-lived keys. Clear education: never long-lived AWS secrets in a pipeline. The only permitted secrets are temporary patterns (STS role assumption) or credentials for third-party tools that do not sell AWS credentials (Slack webhook, private registry token, etc.). Fourth pitfall: ignoring the scalability of the runner or build executor. If you have a single Jenkins server and 50 teams merging code, or a single self-hosted GitHub Actions runner, you create a bottleneck. CodeBuild scales natively (AWS handles hundreds of concurrent builds effortlessly), hosted GitHub Actions likewise. But if you self-host (Jenkins, GitLab Runner), you must plan capacity. Many teams only ask this question when the pipeline has become too slow. Fifth pitfall: the "magic pipeline." At the start, it is simple: a commit triggers a test, the test passes, automatic deployment to staging. But over time, you add orchestration: "fetch a license from a central server," "request a Slack approval," "wait for an external event," "run a DB migration script," "notify 5 services." Little by little, the pipeline becomes a complex monster that costs more maintenance than all the application code. The solution: draw a key line. The pipeline describes the "what" and the "when" (build, test, deploy to staging/prod). The "how" (the scripts, the migrations, the integrations) must be moved out to infrastructure-as-code or versioned scripts that the pipeline calls, never embedded in the pipeline YAML. Sixth pitfall: forgetting rollbacks. You deploy to prod, something breaks. Your pipeline has no automatic rollback mechanism because "we thought it could not break." With CodePipeline, you can configure automatic rollbacks based on CloudWatch alarms (if the Lambda error rate goes above X percent, roll back the last version). With GitHub Actions, it is more manual (you call another workflow that reverts the deployment). But as a matter of fact, every serious pipeline must have a "revert" path as fast as "forward." ### Centralized, auditable logs, the forgotten foundation The majority of teams realize too late that their pipeline logs are a real pain to debug. CodeBuild sends logs to CloudWatch Logs by default, but the CodeDeploy logs go elsewhere, the application Lambda logs to a different CloudWatch group, and the GitHub Actions runner logs to GitHub itself. When you look for "why did the deployment of this commit fail," you have to jump between several interfaces. A real solution: decide on a single source. For example, all pipeline events go to a centralized CloudWatch Logs stream via a Lambda that listens to CodePipeline events, or an EventBridge rule that aggregates them. The build logs, instead of going to the CloudWatch default, go via a CloudWatch Logs forwarder to an ElasticSearch (or OpenSearch) cluster where they are queryable by commit SHA, pipeline ID, and team. This logging infrastructure must be in place on day 1, not added in a hurry in production. With GitHub Actions, you can add a final action that pushes all the logs to CloudWatch or S3, which completes the centralized scheme. The cost of this reflex: one or two days of engineering. The benefit: hours saved every month in debugging and compliance. ### Authentication setup: OIDC rather than long-lived secrets One question comes up constantly: how does my pipeline on GitHub/GitLab obtain the AWS credentials to deploy? The old answer (still too common): create an AWS access key and store it in GitHub Secrets or GitLab CI variables. Functional, but the risk is real. If the repo becomes public by accident, if a developer reads the logs, if the secret is exposed by mistake in a commit, the key lingers on the web. The right answer: OpenID Connect (OIDC). GitHub and GitLab can issue OIDC tokens identifying each run. Instead of using an AWS key, your pipeline uses this OIDC token to request a temporary AWS credential via STS AssumeRoleWithWebIdentity. The credential is valid for 15 minutes by default, then expires. No long-lived secret to protect. The setup: you create an IAM Identity Provider for GitHub (or GitLab) pointing to 'token.actions.githubusercontent.com' (or the GitLab equivalent). Then an IAM role with a trust policy that accepts the OIDC tokens issued by GitHub for your repo. In your workflow, instead of looking for AWS credentials, you call the 'aws-actions/configure-aws-credentials@v2' action with the role-to-assume and web-identity-token-file parameters. The action does the work, you get a temporary credential, and your pipeline can run 'aws s3 cp' without ever touching a key. It is safer, more limited in scope (the role can be very restrictive, "deploy only to this account/this region"), and easier to audit (CloudTrail shows you a role assumption with a specific SourceArn per GitHub run, not a vague generic key). Almost no team uses OIDC by default. Many do not know it exists. It is an easy security win that takes a day to set up and potentially saves incidents later.

AWS TEARDOWN Β· FREE

Get the AWS Teardown: where your bill really goes.

The guide listing the 12 cost areas that leak the most at scale-ups, and how to plug them. Free, by email, no strings attached.