|
1 | | -# GitHub Actions EC2 Pipeline |
| 1 | +# github-actions-ec2-pipeline |
2 | 2 |
|
3 | | -A simple Node.js application with a CI/CD pipeline using GitHub Actions to deploy to an EC2 instance. |
| 3 | +> GitHub Actions pipeline that builds, tests, versions, and deploys a Node.js app |
| 4 | +> to AWS EC2 — zero manual steps after initial setup. |
4 | 5 |
|
5 | | -This project is a demonstration of how to set up a full CI/CD pipeline for a Node.js application. It includes a simple Express application, a set of tests, and a GitHub Actions workflow that automatically builds, tests, and deploys the application to an AWS EC2 instance. |
| 6 | +[](https://github.com/darestack/github-actions-ec2-pipeline/actions/workflows/ci.yml) |
6 | 7 |
|
7 | | -## Features |
| 8 | +--- |
8 | 9 |
|
9 | | -- **Node.js Express Application:** A simple REST API with a few endpoints. |
10 | | -- **CI/CD Pipeline:** A GitHub Actions workflow that automates the entire build, test, and deployment process. |
11 | | -- **Zero-Downtime Deployment:** The deployment script uses `pm2 reload` to ensure that the application is always available, even during deployments. |
12 | | -- **Rollback Mechanism:** The deployment script automatically creates a backup of the previous release. If a deployment fails, it automatically rolls back to the previous version. |
13 | | -- **Automated Versioning and Releases:** The pipeline automatically bumps the version number, creates a tag, and generates a GitHub release. |
14 | | -- **Health Checks:** The CI/CD pipeline includes a health check step to ensure that the application is running correctly after a deployment. |
15 | | -- **Monitoring:** A separate GitHub Actions workflow runs every 5 minutes to check the health of the application and sends a notification if it's down. |
| 10 | +## Pipeline Overview |
16 | 11 |
|
17 | | -## CI/CD Pipeline |
18 | | - |
19 | | -The CI/CD pipeline is split into two workflows: `ci.yml` for Continuous Integration and `release.yml` for Continuous Deployment. |
20 | | - |
21 | | -### `ci.yml` - Continuous Integration |
22 | | - |
23 | | -This workflow runs on every push to the `main`, `development`, and `feature/*` branches. It consists of two jobs: |
24 | | - |
25 | | -1. **`build-and-test`:** This job builds the application and runs the test suite. |
26 | | -2. **`bump-version`:** This job runs only on the `main` branch after the `build-and-test` job succeeds. It automatically bumps the patch version of the application and creates a new Git tag (e.g., `v1.0.1`). The tagging step uses a Personal Access Token stored as `REPO_ACCESS_TOKEN`. |
27 | | - |
28 | | -### `release.yml` - Continuous Deployment |
29 | | - |
30 | | -This workflow is triggered whenever a new tag is pushed to the repository (tags starting with 'v\*'). It consists of two jobs: |
31 | | - |
32 | | -1. **`deploy`:** |
33 | | - |
34 | | - - Checks out the code |
35 | | - - Sets up deployment environment variables from GitHub secrets |
36 | | - - Creates a deployment package (tar.gz) |
37 | | - - Deploys to EC2 using SSH: |
38 | | - - Copies the package to /tmp/ |
39 | | - - Executes the deployment script at /var/www/app/scripts/deploy.sh |
40 | | - |
41 | | -2. **`create-release`:** |
42 | | - - Runs after successful deployment |
43 | | - - Creates a GitHub Release with the tag name using `GITHUB_TOKEN` and `permissions: contents: write` |
44 | | - |
45 | | -To trigger this workflow: |
46 | | - |
47 | | -```bash |
48 | | -git tag v1.0.0 |
49 | | -git push origin v1.0.0 |
50 | 12 | ``` |
51 | | - |
52 | | -### Deployment Diagram |
53 | | - |
54 | | -```mermaid |
55 | | -graph TD |
56 | | - A[Push to main] --> B{CI Workflow}; |
57 | | - B --> C[Build and Test]; |
58 | | - C --> D[Bump Version & Create Tag]; |
59 | | - D --> E{Release Workflow}; |
60 | | - E --> F[Deploy to Production]; |
61 | | - F --> G[Create GitHub Release]; |
| 13 | +Push to main / feature branch |
| 14 | + │ |
| 15 | + └── ci.yml |
| 16 | + ├── build-and-test: npm ci → Jest tests → pass/fail gate |
| 17 | + └── bump-version (main only): patch version bump → git tag v1.x.x |
| 18 | + │ |
| 19 | + └── release.yml (triggered by tag v*) |
| 20 | + ├── deploy: tar.gz → SCP to EC2 → deploy.sh (PM2 reload, atomic symlink swap) |
| 21 | + └── create-release: GitHub Release with changelog |
62 | 22 | ``` |
63 | 23 |
|
64 | | -## Technologies Used |
| 24 | +### Key Design Decisions |
65 | 25 |
|
66 | | -- **Node.js 20.x (LTS)** |
67 | | -- **Express** |
68 | | -- **Jest** for testing |
69 | | -- **GitHub Actions** for CI/CD |
70 | | -- **AWS EC2** for hosting |
71 | | -- **PM2** for process management |
72 | | -- **Bootstrap** for the frontend |
| 26 | +| Decision | Implementation | Why | |
| 27 | +|---|---|---| |
| 28 | +| **Zero-downtime deploy** | `pm2 reload` + atomic symlink swap (`current → release-timestamp`) | App stays up during deployment; rollback is a symlink change | |
| 29 | +| **Auto-rollback** | `deploy.sh` keeps previous release; restores on failure | No manual intervention if deploy breaks the app | |
| 30 | +| **Automatic versioning** | `bump-version` job creates `v1.x.x` tags on every merge to main | Release history is automatic; no manual tagging | |
| 31 | +| **Health check monitoring** | Scheduled workflow every 5 min; creates a GitHub Issue on failure | On-call alert without a third-party service | |
| 32 | +| **Separate CI / CD workflows** | `ci.yml` + `release.yml` split by tag trigger | CD only runs on verified, tagged builds — not every push | |
73 | 33 |
|
74 | | -## Local Setup |
| 34 | +--- |
75 | 35 |
|
76 | | -1. Clone the repository: |
| 36 | +## Workflows |
77 | 37 |
|
78 | | - ```bash |
79 | | - git clone https://github.com/daretechie/github-actions-ec2-pipeline.git |
80 | | - ``` |
| 38 | +### `ci.yml` — Continuous Integration |
| 39 | +Triggers: push to `main`, `development`, `feature/*` branches + all PRs |
81 | 40 |
|
82 | | -2. Install the dependencies: |
| 41 | +1. **`build-and-test`**: `npm ci` → Jest test suite → pass required before merge |
| 42 | +2. **`bump-version`** (main only): increments patch version, pushes `v1.x.x` tag — triggers `release.yml` |
83 | 43 |
|
84 | | - ```bash |
85 | | - npm install |
86 | | - ``` |
| 44 | +### `release.yml` — Continuous Deployment |
| 45 | +Triggers: new tag matching `v*` |
87 | 46 |
|
88 | | -3. Start the application: |
| 47 | +1. **`deploy`**: packages build → SCP to EC2 → runs `/var/www/app/scripts/deploy.sh` |
| 48 | + - Installs dependencies in release dir → atomic symlink `current` → `pm2 reload` |
| 49 | + - On failure: restores previous symlink → `pm2 reload` (auto-rollback) |
| 50 | +2. **`create-release`**: publishes GitHub Release with tag name |
89 | 51 |
|
90 | | - ```bash |
91 | | - npm start |
92 | | - ``` |
| 52 | +### `health-check.yml` — Uptime Monitoring |
| 53 | +Runs every 5 minutes. Hits `/api/health`. Creates a GitHub Issue if the check fails. |
93 | 54 |
|
94 | | -4. The application will be available at `http://localhost:3000`. |
| 55 | +--- |
95 | 56 |
|
96 | | -## Configuration |
| 57 | +## Required GitHub Secrets |
97 | 58 |
|
98 | | -The CI/CD pipeline requires the following secrets to be set in the GitHub repository: |
| 59 | +| Secret | Purpose | |
| 60 | +|---|---| |
| 61 | +| `PROD_EC2_HOST` | Production EC2 hostname or IP | |
| 62 | +| `PROD_EC2_USER` | SSH username | |
| 63 | +| `PROD_EC2_KEY` | Private SSH key (PEM format) | |
| 64 | +| `REPO_ACCESS_TOKEN` | PAT with `repo` scope — needed for `bump-version` to push tags | |
99 | 65 |
|
100 | | -- `PROD_EC2_HOST`: The hostname or IP address of the production EC2 instance. |
101 | | -- `PROD_EC2_USER`: The username for the production EC2 instance. |
102 | | -- `PROD_EC2_KEY`: The private SSH key for the production EC2 instance. |
103 | | -- `DEV_EC2_HOST`: The hostname or IP address of the development EC2 instance. |
104 | | -- `DEV_EC2_USER`: The username for the development EC2 instance. |
105 | | -- `DEV_EC2_KEY`: The private SSH key for the development EC2 instance. |
106 | | -- `REPO_ACCESS_TOKEN`: A Personal Access Token (classic) with `repo` scope. This is required for the `bump-version` job to create tags that trigger `release.yml`. |
| 66 | +Also set: **Actions → General → Workflow permissions → Read and write** (allows built-in token to create releases and issues). |
107 | 67 |
|
108 | | -Optional: |
109 | | -- `HEALTHCHECK_ISSUE_TOKEN`: A Personal Access Token (classic) with `repo` scope if you prefer creating issues from the scheduled health-check with a PAT instead of the built-in token. |
| 68 | +--- |
110 | 69 |
|
111 | | -Repository Settings → Actions → General: |
112 | | -- Set Workflow permissions to “Read and write permissions” to allow the built-in token to create releases and issues. |
| 70 | +## Application Stack |
113 | 71 |
|
114 | | -## Health Check and Monitoring |
| 72 | +`Node.js 20 LTS` · `Express` · `Jest` · `PM2` · `GitHub Actions` · `AWS EC2` |
115 | 73 |
|
116 | | -- Health endpoint: `/api/health`. |
117 | | -- Workflow: `.github/workflows/health-check.yml` runs every 5 minutes and can be run manually via “Run workflow”. |
118 | | -- On failure, it creates an issue in the repository (requires write permissions as noted above). |
| 74 | +--- |
119 | 75 |
|
120 | | -## Deployment and Access |
| 76 | +## Local Setup |
121 | 77 |
|
122 | | -- Zero-downtime releases with PM2: the deploy script runs the app via the stable symlink `current/src/server.js` and swaps releases atomically. |
123 | | -- The app listens on `0.0.0.0:3000`. You can access it at: |
124 | | - - `http://YOUR_EC2_PUBLIC_IP:3000/` |
125 | | - - `http://YOUR_EC2_PUBLIC_IP:3000/api/health` |
126 | | -- If you prefer port 80/443, add an Nginx reverse proxy in front of the app. Example server block (HTTP only): |
| 78 | +```bash |
| 79 | +git clone https://github.com/darestack/github-actions-ec2-pipeline.git |
| 80 | +cd github-actions-ec2-pipeline |
| 81 | +npm install |
| 82 | +npm test |
| 83 | +npm start |
| 84 | +# → http://localhost:3000 |
| 85 | +# → http://localhost:3000/api/health |
| 86 | +``` |
127 | 87 |
|
| 88 | +To add Nginx as a reverse proxy on the EC2 instance (port 80 → 3000): |
128 | 89 | ```nginx |
129 | 90 | server { |
130 | 91 | listen 80; |
131 | 92 | server_name _; |
132 | | -
|
133 | 93 | location / { |
134 | 94 | proxy_pass http://127.0.0.1:3000; |
135 | 95 | proxy_set_header Host $host; |
136 | 96 | proxy_set_header X-Real-IP $remote_addr; |
137 | | - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
138 | | - proxy_set_header X-Forwarded-Proto $scheme; |
139 | 97 | } |
140 | 98 | } |
141 | 99 | ``` |
0 commit comments