Building an End-to-End CI/CD Deployment Workflow on Oracle Cloud Infrastructure Using OCI DevOps and GitHub


Continuous Integration and Continuous Deployment, commonly known as CI/CD, has become an important part of modern application delivery. Instead of manually packaging application files, copying them to servers, restarting services and validating the deployment each time, a CI/CD workflow allows these steps to be automated and repeated with less manual effort.

In this article, I will provide an overview of how a CI/CD workflow can be designed using OCI DevOps and other Oracle Cloud Infrastructure services by referencing a CI/CD deployment project I created. The goal of the project was to build a practical deployment workflow for a multi component Laravel based application consisting of a frontend, backend and middleware layer.

The project uses OCI DevOps build pipelines, deployment pipelines, OCI Vault, OCI Artifact Registry, OCI Compute, OCI Networking, OCI Load Balancer, OCI Logging, OCI Notifications and GitHub integration.

Project Objective

The objective of this project was to create a working CI/CD workflow where changes pushed to GitHub can be built, packaged, stored as artifacts and deployed to OCI Compute instances.

The application architecture was separated into three components:

  • Frontend
  • Backend
  • Middleware

Each component had its own GitHub repository, build pipeline, deployment pipeline and target Ubuntu server.

The high-level goal was to achieve the following flow:

                                          GitHub commit
                              
                       OCI DevOps trigger
                               
                   OCI DevOps build pipeline
                               
                      OCI Artifact Registry
                               
                  OCI DevOps deployment pipeline
                               
                      OCI DevOps Shell stage
                               
                    Ubuntu application server
                               
                             Nginx
                               
                       OCI Load Balancer
                               
                            End user

High-Level Architecture

The architecture used in this project is shown below:


This design allowed each application component to be managed independently while still following the same deployment pattern.

OCI Services Used

The project used the following Oracle Cloud Infrastructure services:

  • OCI DevOps
  • OCI Vault
  • OCI Artifact Registry
  • OCI Compute
  • OCI Networking
  • OCI Load Balancer
  • OCI Logging
  • OCI Notifications
  • OCI Events

GitHub Integration with OCI DevOps

The first part of the setup is to connect GitHub with OCI DevOps.

I created a GitHub Personal Access Token and stored it securely in OCI Vault. Then I created an OCI DevOps external connection using the GitHub connection type and selected the Vault secret containing the token.

This connection allowed OCI DevOps to access the required GitHub repositories during build pipeline execution.

For this project, the repositories were separated as follows:

  • frontend repository
  • backend repository
  • middleware repository

This separation helped keep each application component independently buildable and deployable.

Build Pipeline Design

Each component had its own OCI DevOps build pipeline.

The build pipelines followed this pattern:

                         Managed Build
                               
                        Deliver Artifacts
                               
                       Trigger Deployment

Having separate build pipelines meant that the components could be built independently. This ensures that the pipelines did not interfere with one another. This helps in keeping the CI/CD workflow modular and easy to troubleshoot.

The Managed Build stage was responsible for running the build steps defined in the build_spec.yaml file. The build_spec.yaml file is the “build recipe” for the application and it includes the exact steps to follow when building the package. The build_spec.yaml file should be created and stored in the root directory of the GitHub repository.

The Deliver Artifacts stage stores the generated artifact in OCI Artifact Registry. The Trigger Deployment stage then started the corresponding deployment pipeline. Trigger deployment acts as the messenger between the build pipeline and the deployment pipeline. Once the artifacts have been built successfully and stored in the registry, this stage informs the deployment pipeline that it can proceed with deploying the artifact.

Artifact Management with OCI Artifact Registry

Let’s look a bit more at artifact management in this CI/CD workflow. Once the build pipeline completes building the package, it then stores the package in the OCI Artifact Registry. Essentially the build pipeline delivers the package to the registry.

Separate artifact paths were used for the three build pipelines to deliver the package. This, as mentioned before, helps in troubleshooting if anything goes wrong.

The delivered artifact is then consumed by the deployment pipeline in order to proceed with the deployment. This is useful because the artifact becomes the deployable unit. Instead of deploying directly from the developer’s machine or manually copying files, the deployment pipeline automates this.

Deployment Pipeline Design

The deployment pipeline was responsible for taking the artifact produced by the build pipeline and deploying it to the target Ubuntu server. In this project, each component had its own deployment pipeline and each deployment pipeline used a Shell stage to run the required deployment commands.

Before configuring the deployment pipelines, each Ubuntu server was prepared with a dedicated deployment user named deploy. Using a separate deployment user is a better practice than deploying directly as root, because it allows deployment access to be controlled.

A separate release directory was created on each server to store the deployed application versions along with shared and logs directories to store files that remain consistent across deployments and to store log files.

The .env files were intentionally kept under the shared directory on each server and were not included in the build artifacts. This was an important design decision because environment specific configuration and secrets should not be bundled into deployable packages.

By keeping the .env files on the servers, the same artifact can be deployed repeatedly without exposing sensitive configuration values or rebuilding the package for each environment.

Network and Security Design

For the deployment layer, I used an OCI DevOps Shell stage instead of an instance group deployment stage. The main reason was operating system compatibility. OCI DevOps instance group deployments are supported for Oracle Linux and CentOS, while my application servers were Ubuntu based OCI Compute instances. The Shell stage provided a more flexible option because it allowed custom deployment commands to run and connect to the Ubuntu servers over SSH.

The Shell stage was configured to run inside a selected OCI subnet. Because the target Ubuntu servers were private, the Shell stage needed network access to those servers over SSH. The server side NSG or security list allowed SSH only from the DevOps Shell stage subnet or NSG:

Source: DevOps Shell stage subnet or NSG
Protocol: TCP
Destination port: 22

For application access, only the frontend was exposed through an OCI Load Balancer. The Load Balancer accepted public HTTP traffic and forwarded it to the frontend server, while the backend and middleware servers remained private.

The frontend server allowed HTTP traffic only from the Load Balancer subnet or NSG:

Source: Load Balancer subnet or NSG
Protocol: TCP
Destination port: 80

This design helped separate deployment access from user access. The DevOps Shell stage was used only for deployments, while public traffic entered through the Load Balancer. As a result, the application servers did not need to be directly exposed to the internet, but users could still access the frontend application through a managed OCI entry point.

Triggering Builds from GitHub

At this point, you may be asking yourself; how does OCI DevOps know when a developer has pushed a commit to a GitHub repository? Although the OCI DevOps project has an external connection to GitHub, that connection mainly allows OCI DevOps to access the repository during the build. By itself, it does not automatically notify OCI every time a new commit is pushed. This is where GitHub webhooks come into play.

The trigger flow works as follows:

                    GitHub push to main branch
                                
                          GitHub webhook
                                
                        OCI DevOps trigger
                                
                    OCI DevOps build pipeline

When a developer pushes code to the main branch, GitHub sends a webhook event to the OCI DevOps trigger URL. OCI DevOps then receives that event and starts the respective build pipeline.

Separate triggers have been created for each pipeline in order to isolate the pipeline execution per component. A frontend code change triggers only the frontend build pipeline, a backend code change triggers only the backend build pipeline, and a middleware code change triggers only the middleware build pipeline.

This separation makes the CI/CD process more efficient and easier to troubleshoot because each repository is connected only to the pipeline responsible for building and deploying that specific component.

Logging, Notifications and Events

After the pipelines were created, I enabled OCI DevOps service logging.

Logging helped with troubleshooting and validation because pipeline execution details could be reviewed after each build or deployment run.

I also configured OCI Notifications and Events rules for build and deployment updates. This allowed email notifications to be sent when pipeline status updates occur.

Best Practices Applied

Several practical best practices were applied during this implementation:

  1. Separate frontend, backend, and middleware repositories
  2. Use separate build and deployment pipelines per component
  3. Store GitHub PAT and SSH private key references in OCI Vault
  4. Keep .env files outside the deployment artifacts
  5. Use OCI Artifact Registry for generated packages
  6. Use DevOps Shell stages for repeatable deployments
  7. Allow controlled service reloads through sudoers
  8. Retain only the latest five releases on the server
  9. Use OCI Logging for pipeline troubleshooting
  10. Use OCI Notifications and Events for visibility
  11. Place the frontend behind OCI Load Balancer

I recommend following these practices when designing a similar CI/CD workflow.

Conclusion

This blog provided a high level overview of how a CI/CD workflow can be designed using OCI DevOps and other Oracle Cloud Infrastructure services. The main objective was to explain how GitHub, OCI DevOps build pipelines, deployment pipelines, OCI Vault, OCI Artifact Registry, OCI Compute, Nginx, OCI Load Balancer, Logging, Notifications and Events can work together to support an end-to-end deployment process.

Rather than focusing only on the step-by-step configuration, this article explained the purpose of each major component in the architecture and how they contribute to a practical CI/CD workflow for a multi component application.

For readers who want hands-on experience with the actual configuration, I have created a separate technical guide with setup steps, build specifications, deployment command specs, Nginx configuration examples, architecture details, and validation screenshots. The guide is available here:

https://github.com/K-Jayakody/oci-laravel-cicd-deployment-demo


Written by Kulith Jayakody

Comments

Popular posts from this blog

Installing Oracle Database 19c on Linux 8 (OL8) Server

Configuring an Oracle Standby Database

Oracle GoldenGate Real-Time Replication (Unidirectional)