From your laptop to
an EC2 box, seven
steps, no SSH.
A hands-on course in continuous delivery on AWS. Build the same pipeline twice — once with GitHub Actions, once with Jenkins — and see exactly where the two differ. This page runs both in front of you. Break the tests and watch the deploy never reach AWS.
Six modules · 5 hours · you finish with a workflow file, a Jenkinsfile, and a running deploy.
The same commit, two very different maps
The steps look identical on the rail above. What actually changes is where the build happens and how it proves its identity to AWS.
Picking one, honestly
Neither is better. One removes a server from your life, the other gives you a server you fully control. Read down the column that matches your constraints.
| GitHub Actions | Jenkins | |
|---|---|---|
| Where the build runs | GitHub's cloud, outside your VPC | An EC2 instance you own, inside the VPC |
| You maintain | A YAML file | OS patches, Java, plugins, backups, agents |
| AWS authentication | OIDC role assumption — no stored keys | Instance role plus credentials in Jenkins |
| Reaching a private server | Via SSM, or a self-hosted runner in the VPC | Direct — it is already on the private network |
| Cost shape | Free minutes, then per build minute | EC2 hours, whether you build or not |
| Config drift risk | Low — everything is in the repo | Real — UI settings live outside the Jenkinsfile |
| Approval gates | Environment protection rules | An input step, which pauses the job |
| Choose it when | Your code is on GitHub and the stack is ordinary | Private networks, odd toolchains, existing plugins |
Both pipelines, written down
This is the run you watched, in the file it actually lives in. Same steps, same AWS account.
name: deploy-to-aws
on:
push: { branches: [main] }
pull_request:
permissions:
id-token: write
contents: read
env:
AWS_REGION: ap-south-1
ECR: 4155.dkr.ecr.ap-south-1.amazonaws.com/app
jobs:
build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with: { php-version: 8.3 }
- run: composer install --no-dev -o
- run: vendor/bin/phpunit --testdox
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::4155:role/gha-deploy
aws-region: ap-south-1
- uses: aws-actions/amazon-ecr-login@v2
- run: docker build -t app .
- run: docker push app
deploy:
needs: build
environment: production
runs-on: ubuntu-24.04
steps:
- run: aws ssm send-command --targets Key=tag:App,Values=web
- run: curl -fsS https://app.example.com/health
pipeline {
agent { label 'linux-1' }
environment {
AWS_REGION = 'ap-south-1'
ECR = '4155.dkr.ecr.ap-south-1.amazonaws.com/app'
EC2_HOST = '10.0.2.14'
}
triggers { githubPush() }
stages {
stage('Build') {
steps {
sh 'composer install --no-dev -o'
sh 'docker build -t app .'
}
}
stage('Test') {
steps { sh 'vendor/bin/phpunit --log-junit reports/junit.xml' }
post { always { junit 'reports/junit.xml' } }
}
stage('Push to ECR') {
steps {
withAWS(role: 'jenkins-deploy', region: 'ap-south-1') {
sh 'docker push app'
}
}
}
stage('Approve') {
when { branch 'main' }
steps { input message: 'Deploy to production?', ok: 'Ship it' }
}
stage('Deploy') {
steps {
sshagent(['ec2-deploy-key']) {
sh 'ssh ubuntu@10.0.2.14 docker compose up -d'
}
}
}
}
post {
failure { sh 'ssh ubuntu@10.0.2.14 ./rollback.sh' }
always { cleanWs() }
}
}
What you wire up once
Everything below happens before your first green pipeline. Do it in this order — each step needs the one above it.
GitHub Actions route
- Prepare AWS Create a registry repository, and launch the instances with the SSM agent running and a tag such as App=web.
- Add GitHub as an identity provider In IAM, register the GitHub OIDC provider. This is what removes access keys from your repo.
- Create the deploy role A role trusted by that provider, scoped to your repo and branch, allowed to push images and run SSM commands. Nothing else.
- Store the role ARN Repository variable, not a secret — it is not sensitive. Secrets are for things the role cannot replace.
- Commit the workflow Drop deploy.yml into .github/workflows/ and push. The first run is your test.
- Close the door Protect main, require the build check, add a production environment with a reviewer.
Jenkins route
- Launch the controller A small instance is enough to start. Install Jenkins, Docker and the AWS CLI on it.
- Put it behind HTTPS A reverse proxy and a certificate, security group locked to your IPs plus the webhook ranges. An open Jenkins is an open shell.
- Attach an instance role Give the controller registry push rights, so no AWS keys are ever typed into Jenkins.
- Add credentials The deploy SSH key goes in the Jenkins credentials store, referenced by ID only — never inline in the Jenkinsfile.
- Point the webhook at it Add the Jenkins webhook URL in your repository settings. Polling works, but it wastes cycles and lags.
- Create a multibranch pipeline Point it at the repo. Jenkins finds the Jenkinsfile per branch and builds pull requests automatically.
Six modules, in the order the pipeline runs
Every module is built twice — once per tool — against the same AWS account. Open one to see what is inside.
Where the definition lives, what a runner or agent actually is, and how a push becomes a job.
- Workflow YAML next to
Jenkinsfile— same ideas, different vocabulary - Webhooks versus polling, and why polling costs you
- Jobs, stages and the dependency graph
- Matrix builds across two PHP versions
One immutable image, built on your CI, tagged with the commit, pushed to a registry.
- Multi-stage Dockerfiles and layer ordering
- Caching dependencies between runs
- Registry lifecycle policies so storage does not creep
- Tagging with the SHA instead of
latest
The gate only works if red means something. Most broken pipelines break here.
- Unit, integration and a thin end-to-end layer
- Publishing JUnit reports in both tools
- Quarantining flaky tests instead of retrying them
- Coverage as a floor you raise, not a number you chase
How your pipeline proves who it is without leaving keys lying around.
- OIDC federation and a trust policy scoped to one branch
- Instance roles for a self-hosted controller
- Least privilege in practice
- SSH keys, Session Manager, and why SSM usually wins
Getting the new image onto running instances while requests keep being served.
- Load balancer connection draining and health check tuning
- Rolling updates across a scaling group
- Migrations that survive a rollback
- A rollback you can run in one command
A pipeline that ends at deploy is half a pipeline.
- Alarms that trigger the rollback automatically
- The four DORA metrics, measured honestly
- Alerts that page a human versus alerts that annoy one
- Postmortems that produce pipeline changes
Push a commit. Let the pipeline reach your server for you.
Free, self-paced, built around a repo and an AWS account you keep. Start with module one, or scroll back up, switch tools, and break the tests again.