CI/CD โ GitHub Actions
GitHub Actions is a powerful CI/CD platform integrated directly into GitHub. This guide covers reusable workflows, matrix builds, secrets management, and best practices for production pipelines based on real-world experience migrating 50+ microservices from Jenkins and CircleCI.
Why GitHub Actions
After migrating Samsung's microservices fleet through Jenkins, CircleCI, and finally GitHub Actions, the advantages of GitHub Actions for GitHub-hosted repositories became clear. The platform eliminates the need for separate CI infrastructure while providing powerful features:
| Feature | GitHub Actions | Jenkins | CircleCI |
|---|---|---|---|
| GitHub integration | Native (same platform) | Webhook-based | OAuth app |
| Configuration | YAML in repo | Jenkinsfile / GUI | YAML in repo |
| Runner management | Hosted + self-hosted | Master + agents | Cloud + self-hosted |
| Marketplace | 20,000+ actions | Plugins (1,800+) | Orbs |
| Reusable components | Reusable workflows, composite actions | Shared libraries | Orbs |
| Pricing model | Minutes-based | Free (infrastructure cost) | Minutes-based |
| OIDC/AWS | Native | Plugin required | Context-based |
| Secrets management | Repository + org + environment level | Credential store | Contexts + projects |
The killer feature is workflow composition: reusable workflows allow a central platform team to define standard patterns (build, test, security scan, deploy) that 50+ service teams consume without copy-paste. When the platform team updates the reusable workflow, all consuming workflows get the improvement automatically.
Workflow Structure
A GitHub Actions workflow is a YAML file stored in .github/workflows/. The core components are:
| Component | Purpose | Required |
|---|---|---|
name | Workflow display name | No |
on | Trigger events (push, pull_request, schedule, workflow_dispatch) | Yes |
env | Environment variables available to all jobs | No |
permissions | GITHUB_TOKEN and OIDC permission scopes | No (defaults to write-all) |
jobs | Collection of jobs that run in the workflow | Yes |
<job_id> | Unique identifier for a job | Yes |
runs-on | Runner type (ubuntu-latest, self-hosted, etc.) | Yes |
steps | Sequential tasks within a job | Yes |
uses | References an action (from marketplace or repo) | No (alternative to run) |
run | Shell command to execute | No (alternative to uses) |
needs | Job dependencies; creates a DAG | No |
if | Conditional execution | No |
strategy.matrix | Parallel job execution across combinations | No |
secrets | Secrets passed to reusable workflows | Conditional |
Trigger Events Reference
on:
push:
branches: [main, develop]
tags: ['v*']
paths: ['src/**', 'Dockerfile']
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
workflow_dispatch:
inputs:
environment:
description: 'Target environment'
required: true
type: choice
options: [dev, staging, prod]
schedule:
- cron: '0 2 * * 1' # Weekly Monday 2 AM UTC
workflow_call:
inputs:
node_version:
required: true
type: string
secrets:
AWS_ROLE_ARN:
required: true
Complete Working Example: Build + Test + Deploy
The following is a production-grade workflow used for a containerized microservice. It includes build, test, security scan, artifact push, and deployment with OIDC authentication:
#.github/workflows/pipeline.yml
name: CI/CD Pipeline
on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]
env:
AWS_REGION: us-east-1
ECR_REGISTRY: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.us-east-1.amazonaws.com
IMAGE_NAME: my-service
permissions:
id-token: write # Required for OIDC
contents: read # Required for checkout
pull-requests: write # Required for PR comments
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Run unit tests
run: npm run test:unit -- --coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
- name: Build application
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7
security-scan:
runs-on: ubuntu-latest
needs: build-and-test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
severity: 'CRITICAL,HIGH'
exit-code: '1'
- name: Run npm audit
run: npm audit --audit-level=moderate
build-and-push-image:
runs-on: ubuntu-latest
needs: [build-and-test, security-scan]
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Set image tag
id: vars
run: |
if [[ $GITHUB_REF == refs/tags/* ]]; then
echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
else
echo "tag=sha-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
fi
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.tag }}
${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Output image digest
run: echo "Image pushed - ${{ steps.build-push.outputs.digest }}"
deploy-staging:
runs-on: ubuntu-latest
needs: build-and-push-image
if: github.ref == 'refs/heads/main'
environment:
name: staging
url: https://staging-api.example.com
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN_STAGING }}
aws-region: ${{ env.AWS_REGION }}
- name: Update kubeconfig
run: aws eks update-kubeconfig --name staging-cluster --region ${{ env.AWS_REGION }}
- name: Deploy to Staging
run: |
helm upgrade --install ${{ env.IMAGE_NAME }} ./helm-chart \
--namespace default \
--set image.tag=sha-$(git rev-parse --short HEAD) \
--set environment=staging \
--wait --timeout 300s
deploy-production:
runs-on: ubuntu-latest
needs: build-and-push-image
if: startsWith(github.ref, 'refs/tags/v')
environment:
name: production
url: https://api.example.com
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN_PRODUCTION }}
aws-region: ${{ env.AWS_REGION }}
- name: Update kubeconfig
run: aws eks update-kubeconfig --name production-cluster --region ${{ env.AWS_REGION }}
- name: Deploy to Production
run: |
TAG=${GITHUB_REF#refs/tags/}
helm upgrade --install ${{ env.IMAGE_NAME }} ./helm-chart \
--namespace default \
--set image.tag=$TAG \
--set environment=production \
--wait --timeout 300s
- name: Verify deployment
run: |
kubectl rollout status deployment/${{ env.IMAGE_NAME }} --timeout=120s
kubectl get pods -l app=${{ env.IMAGE_NAME }}
Reusable Workflows
Reusable workflows (triggered by workflow_call) are the most powerful feature for platform engineering teams. A central repository defines standard patterns; service repositories reference them.
Reusable Build Workflow (Platform Team Repository)
#.github/workflows/reusable-build.yml
name: Reusable Build and Push
on:
workflow_call:
inputs:
image_name:
required: true
type: string
dockerfile:
required: false
type: string
default: './Dockerfile'
aws_region:
required: false
type: string
default: 'us-east-1'
secrets:
AWS_ROLE_ARN:
required: true
outputs:
image_tag:
description: "The pushed image tag"
value: ${{ jobs.build.outputs.tag }}
image_digest:
description: "The image digest"
value: ${{ jobs.build.outputs.digest }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
outputs:
tag: ${{ steps.vars.outputs.tag }}
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ inputs.aws_region }}
- uses: aws-actions/amazon-ecr-login@v2
- name: Set image tag
id: vars
run: echo "tag=sha-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- uses: docker/setup-buildx-action@v3
- name: Build and push
id: build
uses: docker/build-push-action@v5
with:
context: .
file: ${{ inputs.dockerfile }}
push: true
tags: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ inputs.aws_region }}.amazonaws.com/${{ inputs.image_name }}:${{ steps.vars.outputs.tag }}
cache-from: type=gha
cache-to: type=gha,mode=max
Consuming the Reusable Workflow (Service Repository)
#.github/workflows/service-pipeline.yml
name: Service Pipeline
on:
push:
branches: [main]
jobs:
build:
uses: my-org/platform-workflows/.github/workflows/reusable-build.yml@main
with:
image_name: payment-service
dockerfile: './Dockerfile'
secrets:
AWS_ROLE_ARN: ${{ secrets.AWS_ROLE_ARN }}
deploy:
needs: build
uses: my-org/platform-workflows/.github/workflows/reusable-deploy.yml@main
with:
image_name: payment-service
image_tag: ${{ needs.build.outputs.image_tag }}
environment: staging
secrets:
AWS_ROLE_ARN: ${{ secrets.AWS_ROLE_ARN_STAGING }}
Matrix Strategy for Multi-Environment Builds
Matrix builds enable parallel execution across multiple combinations of configurations. This is essential for building across multiple environments, Node.js versions, or architectures:
jobs:
test-matrix:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: [18, 20, 21]
os: [ubuntu-latest, windows-latest]
include:
- node-version: 20
os: ubuntu-latest
coverage: true
exclude:
- node-version: 18
os: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
- name: Upload coverage
if: matrix.coverage == true
uses: codecov/codecov-action@v3
Secrets Management
GitHub Actions provides secrets at multiple scopes. Proper scoping follows the principle of least privilege:
| Scope | Visibility | Use Case | Management |
|---|---|---|---|
| Repository | Single repository | Service-specific credentials | Repo Settings > Secrets |
| Environment | Single environment in a repo | Production credentials requiring approval | Repo Settings > Environments |
| Organization | All repos in the org | Shared credentials (ECR registry, monitoring) | Org Settings > Secrets |
| Codespaces | Codespaces for the repo | Development environment secrets | Repo Settings > Codespaces |
| Dependabot | Dependabot runs only | Private registry access for Dependabot | Repo Settings > Dependabot |
OIDC Authentication with AWS (No Long-Term Credentials)
The most secure method for GitHub Actions to authenticate with AWS is OpenID Connect (OIDC). This eliminates the need to store long-lived AWS credentials as GitHub secrets. Instead, GitHub provides a short-lived JWT token that AWS IAM validates:
Step 1: Create the IAM OIDC Provider
# Terraform: GitHub OIDC Provider for AWS
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["6938fd4e98bab03faadb97b34396831e3780aea1"]
tags = {
Name = "github-actions-oidc"
}
}
Step 2: Create the IAM Role with Trust Policy
# Terraform: IAM Role for GitHub Actions
resource "aws_iam_role" "github_actions" {
name = "github-actions-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.github.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
StringLike = {
"token.actions.githubusercontent.com:sub" = [
"repo:my-org/*:*",
"repo:my-org/infrastructure:ref:refs/heads/main"
]
}
}
}
]
})
tags = {
Name = "github-actions-role"
}
}
resource "aws_iam_role_policy" "github_ecr_push" {
name = "github-ecr-push"
role = aws_iam_role.github_actions.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload",
"ecr:PutImage"
]
Resource = "*"
}
]
})
}
Step 3: Use in Workflow
permissions:
id-token: write # Required for OIDC
contents: read
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-role
aws-region: us-east-1
# AWS CLI commands now use temporary credentials from OIDC
Self-Hosted Runners
For workloads requiring private network access, larger compute, or specialized hardware:
# Install GitHub Actions runner (Ubuntu)
mkdir actions-runner && cd actions-runner
curl -o actions-runner-linux-x64-2.311.0.tar.gz -L \
https://github.com/actions/runner/releases/download/v2.311.0/actions-runner-linux-x64-2.311.0.tar.gz
tar xzf actions-runner-linux-x64-2.311.0.tar.gz
./config.sh --url https://github.com/my-org --token $REGISTRATION_TOKEN
sudo ./svc.sh install
sudo ./svc.sh start
# autoscaling with Kubernetes (Actions Runner Controller)
# Install ARC
helm repo add actions-runner-controller https://actions-runner-controller.github.io/actions-runner-controller
helm install arc actions-runner-controller/actions-runner-controller \
--namespace actions-runner-system \
--set syncPeriod=1m
# Define RunnerDeployment
kubectl apply -f - <
Composite Actions
For reusable steps within a repository or organization, composite actions bundle multiple steps into a single callable unit:
#.github/actions/setup-and-build/action.yml
name: 'Setup and Build'
description: 'Sets up Node.js, installs dependencies, and builds'
inputs:
node-version:
description: 'Node.js version'
required: true
default: '20'
build-command:
description: 'Build command to run'
required: true
default: 'npm run build'
outputs:
build-dir:
description: 'Build output directory'
value: ${{ steps.build.outputs.dir }}
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- run: npm ci
shell: bash
- id: build
run: |
${{ inputs.build-command }}
echo "dir=dist" >> $GITHUB_OUTPUT
shell: bash
# Usage in workflow:
# - uses: ./.github/actions/setup-and-build
# with:
# node-version: '20'
# build-command: 'npm run build:prod'
Caching Strategies
Effective caching dramatically reduces build times. GitHub Actions provides the actions/cache action and built-in caching for setup actions:
# Built-in caching (recommended for package managers)
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # Automatically caches ~/.npm
# Manual caching for Docker layers
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
with:
cache-from: type=gha # GitHub Actions cache backend
cache-to: type=gha,mode=max
# Manual caching for Gradle/Maven/others
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
Concurrency Groups
Prevent multiple deployments from colliding:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment || '' }}
cancel-in-progress: true
Best Practices Summary
| Practice | Implementation |
|---|---|
| Use OIDC for cloud auth | Never store long-lived cloud credentials in secrets |
| Pin action versions | Use @v4, not @master; use SHA for critical workflows |
| Least privilege permissions | Explicitly declare permissions block, avoid write-all |
| Reusable workflows | Centralize standard patterns to reduce drift |
| Environment protection | Use environments with required reviewers for production |
| Artifact management | Use upload-artifact/download-artifact for inter-job transfers |
| Fail fast | Use needs and if conditions to skip unnecessary jobs |
| Timeout all jobs | Set job-level and step-level timeouts to prevent runaway jobs |
| Matrix fail-fast false for critical tests | Ensure all matrix combinations run for complete test coverage |