1+ <!DOCTYPE html>
2+ < html lang ="en ">
3+
4+ < head >
5+ < meta charset ="UTF-8 ">
6+ < meta name ="viewport " content ="width=device-width, initial-scale=1.0 ">
7+
8+ < title > Building My First CI/CD Pipeline | Cliffable</ title >
9+ < meta name ="description "
10+ content ="Field note on building a GitHub Actions CI/CD pipeline to deploy a static AWS website to S3 and invalidate CloudFront. ">
11+ < meta name ="robots " content ="index, follow ">
12+ < link rel ="canonical " href ="https://cliffable.com/field-notes/building-my-first-cicd-pipeline.html ">
13+
14+ < link rel ="stylesheet " href ="../styles.css ">
15+
16+ < link rel ="icon " type ="image/x-icon " href ="../favicon/favicon.ico ">
17+ < link rel ="icon " type ="image/png " sizes ="32x32 " href ="../favicon/favicon-32x32.png ">
18+ < link rel ="icon " type ="image/png " sizes ="16x16 " href ="../favicon/favicon-16x16.png ">
19+ < link rel ="apple-touch-icon " sizes ="180x180 " href ="../favicon/apple-touch-icon.png ">
20+
21+ <!-- Google tag (gtag.js) -->
22+ < script async src ="https://www.googletagmanager.com/gtag/js?id=G-L41X6NGBMK "> </ script >
23+
24+ < script >
25+ window . dataLayer = window . dataLayer || [ ] ;
26+ function gtag ( ) {
27+ dataLayer . push ( arguments ) ;
28+ }
29+ gtag ( 'js' , new Date ( ) ) ;
30+
31+ gtag ( 'config' , 'G-L41X6NGBMK' ) ;
32+ </ script >
33+ </ head >
34+
35+ < body >
36+ < header >
37+ < div class ="container header-inner ">
38+
39+ < div class ="logo ">
40+ < a href ="/ ">
41+ < img src ="/assets/logos/cliffable-logo-white-v2.svg " alt ="Cliffable logo " height ="60 ">
42+ </ a >
43+ </ div >
44+
45+ < nav class ="main-nav ">
46+ < a href ="/projects/ "> Projects</ a >
47+ < a href ="/field-notes/ "> Field Notes</ a >
48+ < a href ="/about.html "> About</ a >
49+ < a href ="/contact.html "> Contact</ a >
50+ </ nav >
51+
52+ </ div >
53+ </ header >
54+
55+ < main >
56+
57+ < section class ="hero field-note-article-hero ">
58+ < div class ="container field-note-container ">
59+
60+ < p class ="field-note-article-meta ">
61+ AWS • CI/CD • GitHub Actions
62+ </ p >
63+
64+ < p class ="field-note-date ">
65+ Published 23 May 2026
66+ </ p >
67+
68+ < h1 > Building my first CI/CD pipeline</ h1 >
69+
70+ < p class ="field-note-intro ">
71+ Moving from manual S3 uploads to a GitHub Actions deployment pipeline using OIDC, IAM roles and
72+ CloudFront invalidation.
73+ </ p >
74+
75+ </ div >
76+ </ section >
77+
78+ < section class ="field-note-featured-image ">
79+ < div class ="container field-note-container ">
80+
81+ < a href ="../assets/images/field-notes/building-my-first-cicd-pipeline/automated-deployment-pipeline.png "
82+ class ="lightbox-trigger ">
83+
84+ < img src ="../assets/images/field-notes/building-my-first-cicd-pipeline/automated-deployment-pipeline.png "
85+ alt ="GitHub Actions automated deployment pipeline architecture diagram ">
86+
87+ </ a >
88+
89+ < p class ="field-note-image-caption ">
90+ From local code changes to automated live deployment.
91+ </ p >
92+
93+ </ div >
94+ </ section >
95+
96+ < section class ="field-note-content-section ">
97+ < div class ="container field-note-container ">
98+
99+ < h2 > The original workflow</ h2 >
100+ < p >
101+ This project started as a way to remove manual deployment steps from the Cliffable static website
102+ workflow.
103+ Previously, changes were uploaded directly to S3 by hand after local testing.
104+ </ p >
105+
106+ < p >
107+ While this worked for a small personal site, it quickly became obvious that the process was:
108+ </ p >
109+
110+ < ul >
111+ < li > slow</ li >
112+ < li > error-prone</ li >
113+ < li > difficult to scale</ li >
114+ < li > lacking rollback visibility</ li >
115+ </ ul >
116+
117+ < p >
118+ The goal was to build a lightweight CI/CD pipeline that could automatically deploy changes whenever
119+ code was pushed to the main branch.
120+ </ p >
121+
122+ < h2 > How the pipeline works</ h2 >
123+ < p >
124+ The automated deployment pipeline connects GitHub, GitHub Actions and AWS into a single workflow
125+ that deploys website updates automatically whenever new code is pushed to the repository.
126+ </ p >
127+ < p >
128+ When code is pushed to the main branch, GitHub Actions automatically triggers a workflow defined
129+ inside the repository. The workflow authenticates to AWS using OpenID Connect (OIDC), temporarily
130+ assumes a tightly scoped IAM role and synchronises the website files to the S3 bucket hosting the
131+ site.
132+ </ p >
133+
134+ < p >
135+ After deployment, the workflow creates a CloudFront invalidation so cached files are refreshed
136+ across the CDN. This ensures visitors immediately receive the latest version of the website.
137+ </ p >
138+
139+ < h2 > Security improvements</ h2 >
140+ < p >
141+ One of the most important improvements was removing long-lived AWS access keys from the deployment
142+ process. Instead of storing permanent credentials inside GitHub, the workflow uses OpenID Connect
143+ (OIDC) to request temporary credentials directly from AWS.
144+ </ p >
145+
146+ < div class ="field-note-inline-image ">
147+
148+ < a href ="../assets/images/field-notes/building-my-first-cicd-pipeline/github-actions-oidc-permissions.png "
149+ class ="lightbox-trigger ">
150+
151+ < img src ="../assets/images/field-notes/building-my-first-cicd-pipeline/github-actions-oidc-permissions.png "
152+ alt ="GitHub Actions deploy.yml permissions block using OIDC authentication ">
153+
154+ </ a >
155+
156+ < p class ="field-note-image-caption ">
157+ GitHub Actions workflow permissions for OIDC authentication.
158+ </ p >
159+
160+ </ div >
161+
162+ < p >
163+ GitHub Actions is allowed to assume a tightly scoped IAM role that only grants permission to deploy
164+ the website and create CloudFront invalidations. Access is further restricted to the Cliffable
165+ repository and the main branch.
166+ </ p >
167+
168+ < h2 > What I learned</ h2 >
169+ < p >
170+ This project was my first experience building a real CI/CD deployment workflow using GitHub Actions
171+ and AWS. It reinforced how much operational complexity can be removed with relatively simple
172+ automation.
173+ </ p >
174+
175+ < p >
176+ It also highlighted the importance of security boundaries in cloud systems. Learning how OIDC trust
177+ relationships, IAM policies and repository restrictions work together gave me a much better
178+ understanding of secure deployment design.
179+ </ p >
180+
181+ < p >
182+ Beyond the technical implementation itself, the project changed the way I think about infrastructure
183+ management. Deployments became faster, more repeatable and significantly less stressful than manual
184+ uploads.
185+ </ p >
186+
187+ < h2 > Conclusion</ h2 >
188+ < p >
189+ Automating deployments removed a large amount of manual operational overhead from the Cliffable
190+ website workflow while also improving security and consistency. It was a relatively small project
191+ technically, but a significant step forward in understanding how modern cloud deployment pipelines
192+ are designed and operated.
193+ </ p >
194+
195+ < h2 > Related project</ h2 >
196+
197+ < p class ="field-note-callout ">
198+ This deployment pipeline was implemented as part of the
199+ < a href ="../projects/csgl-lightsail-wordpress.html ">
200+ WordPress on AWS Lightsail
201+ </ a >
202+ project.
203+ </ p >
204+ </ div >
205+
206+ </ section >
207+
208+ </ main >
209+
210+ < footer >
211+ < div class ="container footer-content ">
212+
213+ < div class ="footer-contact ">
214+ < p class ="footer-heading "> Contact</ p >
215+
216+ < div class ="footer-links ">
217+ < a href ="mailto:mailcliffsmith@gmail.com "> Email</ a >
218+ < a href ="https://www.linkedin.com/in/cliff-smith-london/ " target ="_blank "
219+ rel ="noopener noreferrer "> LinkedIn</ a >
220+ < a href ="https://github.com/cliffable " target ="_blank " rel ="noopener noreferrer "> GitHub</ a >
221+ </ div >
222+ </ div >
223+
224+ < div class ="footer-meta ">
225+ < p > © 2026 Cliff Smith</ p >
226+ </ div >
227+
228+ </ div >
229+ </ footer >
230+
231+ < div id ="lightbox " class ="lightbox ">
232+ < img id ="lightbox-img " src ="" alt ="">
233+ </ div >
234+
235+ < script >
236+ const lightboxTriggers = document . querySelectorAll ( '.lightbox-trigger' ) ;
237+ const lightbox = document . getElementById ( 'lightbox' ) ;
238+ const lightboxImg = document . getElementById ( 'lightbox-img' ) ;
239+
240+ lightboxTriggers . forEach ( trigger => {
241+ trigger . addEventListener ( 'click' , event => {
242+ event . preventDefault ( ) ;
243+ lightbox . style . display = 'flex' ;
244+ lightboxImg . src = trigger . href ;
245+ } ) ;
246+ } ) ;
247+
248+ lightbox . addEventListener ( 'click' , ( ) => {
249+ lightbox . style . display = 'none' ;
250+ lightboxImg . src = '' ;
251+ } ) ;
252+ </ script >
253+
254+ </ body >
255+
256+ </ html >
0 commit comments