End to End Serverless deployment on IBM Cloud
Introduction
Last year I have worked on a project to improve the efficiency of a release to production process. This process took 7hrs to complete, for each production region. This was a major pain during our go live process. While investigating, I have realized that the process was running sequential operations and implementing concurrency would highly improve efficiency. In addition, there was an opportunity to greatly reduce costs with the use of a serverless approach.
Serverless allows provisioning compute resources required to execute a job on demand, and once the job is completed the computing resources are released, avoiding unnecessary costs. Serverless also allows deployment of applications, including the ability to scale as the traffic increases or diminishes.
In this tutorial I will build a simple foundational framework to deploy a web server in a container as a serverless app on IBM Cloud Code Engine
Flow
The high-level flow is similar to this:
flowchart TD
A(write application code) --> B(build & push image) --> C(Create Code Engine resources) --> D(Deploy the app) --> E(Access app on a web browser)
The code presented in this tutorial is found here
Problem
Creating a serverless app is a trivial task on most cloud service providers, it can be done via API, on the UI or CLI. However, there is a little more involved in creating a repeatable deployment for a serverless application, including the ability to make code changes, update container images and/or change the serverless service configuration, for instance to allocate more resources or auto-scale based on traffic load.
Plan
In this tutorial I would provide a simple framework to include all major steps required to make an application code available online as a serverless app.
Expected result
In short, the expected results is
1
2
❯ curl https://ce-serverless-app.1e89nllvdtjo.eu-gb.codeengine.appdomain.cloud
Here is a simple web-server!%
Tutorial
The steps involved are the following:
- Write a simple web app with go
- Create a Dockerfile to containerize the web app
- Create the Terraform resource definitions for Code Engine Serverless deployment
- Write the GitHub Actions CI pipeline to create Docker image and Push to registry
- Create script with Schematics helpers functions
- Write the GitHub Actions CI pipeline to create a Terraform plan job for Code Engine resources
- View GitHub Action jobs
- Deploy Serverless app via Schematics
Schematics workspaces deliver Terraform as a service to automate the provisioning and configuration management of your IBM Cloud resources, and rapidly build, duplicate, and scale complex, multi-tiered cloud environments see IBM Cloud docs.
Step 1: Write a simple go web server
This step involves setting up a new go project for the web server which will be published using a serverless approach.
1
go mod init https://github.com/SRodi/e2e-serverless-deployment
Create the web app by adding the following code to a new file named go/go_http.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
import (
"fmt"
"log"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Here is a simple web-server!")
}
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
Run locally as follows:
1
go run go_http.go
To test you can send a curl request from your terminal as follows:
1
2
❯ curl localhost:8080
Here is a simple web-server!%
Step 2: Create Dockerfile
This step goal is to create the Dockerfile so that our web app can be containerized and portable.
Write the following to a new file named docker/Dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Specifies a parent image
FROM golang:1.21.7-bullseye AS build-stage
WORKDIR /app
COPY ./go/go.mod ./
RUN go mod download
COPY ./go/*.go ./
RUN CGO_ENABLED=0 GOOS=linux go build -o /go_http
# Create a new release build stage
FROM scratch AS build-release-stage
# Set the working directory to the root directory path
WORKDIR /
# Copy over the binary built from the previous stage
COPY --from=build-stage /go_http /go_http
EXPOSE 8080
ENTRYPOINT ["/go_http"]
The docker image can be built locally as follows:
1
docker build . -f docker/Dockerfile -t my-local-test:0.0.1
To run your container locally:
1
docker run --rm -p 3000:8080 my-local-test:0.0.1
Send a request to your server running in the Docker container locally:
1
2
❯ curl localhost:3000
Here is a simple web-server!%
Step 3: Create Code Engine Terraform resources
In this step we create Code Engine Terraform resources to deploy our app in IBM Cloud using a serverless approach.
Write the following to a new file named terraform/main.tf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
locals {
prefix = "ce-serverless"
image = "srodi/go_http:latest"
}
resource "ibm_resource_group" "resource_group" {
name = "${local.prefix}-resource-group"
}
resource "ibm_code_engine_project" "code_engine_project_instance" {
name = "${local.prefix}-project"
resource_group_id = ibm_resource_group.resource_group.id
}
resource "ibm_code_engine_app" "code_engine_app_instance" {
project_id = ibm_code_engine_project.code_engine_project_instance.project_id
name = "${local.prefix}-app"
image_reference = local.image
scale_min_instances = 1
}
output "app_url" {
value = ibm_code_engine_app.code_engine_app_instance.endpoint
}
Create provider.tf
in the same directory
1
2
3
4
5
6
7
8
9
10
11
12
13
provider "ibm" {
region = var.ibmcloud_region
}
terraform {
required_version = ">=1.3.0, <2.0"
required_providers {
ibm = {
source = "IBM-Cloud/ibm"
version = "1.62.0"
}
}
}
Finally, create the variable declaration in a file named variables.tf
1
2
3
4
5
variable "ibmcloud_region" {
description = "IBM Cloud region where Code Engine resources will be deployed."
type = string
default = "eu-gb"
}
To run the code locally:
1
2
3
4
export IC_API_KEY=# your own IBM Cloud ApiKey
terraform init
terraform plan
terraform apply
To destroy all resources:
1
terraform destroy
Step 4: Write Docker image build pipeline
This step involves creating the GitHub Actions configuration to automatically build a new Docker image when web app code changes and a commit is pushed to the GitHub repo.
Create the following configuration in a new file .github/workflows/main.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
name: ci
on:
push:
branches:
- "main"
env:
IBM_CLOUD_API_KEY: $
IBM_CLOUD_REGION: eu-de
SCHEMATICS_WS_ID: eu-de.workspace.ce-serverless-workspace.62cf517a
jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: $
password: $
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Set sha_short
id: vars
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
-
name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ./docker/Dockerfile
push: true
tags: $/go_http:sha-$ , $/go_http:latest
Step 5: Create Schematics helpers functions
This step involves the creation of a script to provide helpers functions to communicate with IBM Cloud Schematics in the automated GitHub Actions pipeline that we will write in Step 6.
Create a new file schematics/ops.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/bin/bash
WORKSPACE_ID="$SCHEMATICS_WS_ID"
function ws_status() {
RCVAR=255
TRYCOUNT=0
while [[ $RCVAR != 0 ]] && [[ $TRYCOUNT -le 10 ]]; do
printf $(ibmcloud schematics ws action --id "$WORKSPACE_ID" --output json | jq -r '.jobs[0].status.workspace_job_status.commands[].outcome') && RCVAR=$? || RCVAR=$?
if [ $RCVAR != 0 ]; then sleep 10; fi
TRYCOUNT=$((TRYCOUNT+1))
done
}
function job_status(){
action_id="$1"
RCVAR=255
TRYCOUNT=0
while [[ $RCVAR != 0 ]] && [[ $TRYCOUNT -le 10 ]]; do
printf $(ibmcloud schematics job get --id "$action_id" --output json | jq -r '.status.workspace_job_status.status_code') && RCVAR=$? || RCVAR=$?
if [ $RCVAR != 0 ]; then sleep 10; fi
TRYCOUNT=$((TRYCOUNT+1))
done
}
function ws_update() {
ibmcloud schematics ws update --id "$SCHEMATICS_WS_ID" --pull-latest
action_status=$(ws_status)
while [[ "$action_status" != "COMPLETED" ]]; do echo "The Schematics WS Status is $action_status"; action_status=$(ws_status); sleep 5; done
echo "Schematics WS update done"
}
function plan(){
action_id=$(ibmcloud schematics plan --id "$WORKSPACE_ID" --output json | jq -r .activityid)
while [[ ! $(ibmcloud schematics job get --id "$action_id") ]]; do job_status=$(job_status "$action_id"); done
while [[ "$job_status" == "job_pending" && "$job_status" != "job_finished" ]]; do
job_status=$(job_status "$action_id")
echo "Schematics plan status for WS $WORKSPACE_ID: $job_status"
if [[ "$job_status" == "job_failed" ]]; then exit 2; fi
done
}
Make this file executable
1
chmod +x schematics/ops.sh
Note: you will need to create a Schematics workspace in order to have the automation working for the next step. I have shared the instructions, including a basic config and script, on this tutorial repo. Link here
Step 6: Write Terraform plan pipeline
This step is adding to the GitHub action configuration an additional job
to perform a Terraform plan using IBM Cloud Schematics.
Add the following code to .github/workflows/main.yml
under jobs
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
schematics:
needs: docker
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Install IBM Cloud CLI
run: |
curl -fsSL https://clis.cloud.ibm.com/install/linux | sh
ibmcloud --version
ibmcloud config --check-version=false
ibmcloud plugin install -f schematics
-
name: Authenticate with IBM Cloud CLI
run: |
ibmcloud login --apikey "${IBM_CLOUD_API_KEY}" -r "${IBM_CLOUD_REGION}" -g Default
-
name: Schematics update
run: |
source ./schematics/ops.sh
ws_update
shell: bash
-
name: Schematics plan
run: |
source ./schematics/ops.sh
plan
shell: bash
Step 7: GitHub Actions
The following prerequisites must be satisfied prior enabling automation with GitHub Actions:
Create a DockerHub access token see Docker docs: Create an access token
Create
DOCKERHUB_USERNAME
andDOCKERHUB_TOKEN
secrets in your GitHub repository see Docker docs: Get started with GitHub ActionsCreate
IBM_CLOUD_API_KEY
secret in your GitHub repository
With all the above correctly configured, you can now push your code to your remote GitHub repository on the main
branch and GitHub Actions jobs will be automatically executed.
When code is pushed to main
, access the GitHub action view to validate all jobs are completed.
Step 8: Deploy the app via Schematics
In this final step we access IBM Cloud Schematics to apply the Terraform plan job that has been triggered by GitHub Actions. The Terraform plan job was saved on Schematics workspace and the details can be seen under the jobs
tab in the main workspace menu.
After analyzing the diff by expanding the plan job window, the job can be applied directly on the Schematics UI so that Code Engine resources defined with Terraform in step 3 are deployed on IBM Cloud.
Note the output of our Terraform apply is the app_url
. You can test the Serverless deployment by accessing the URL on the browser or sending a curl request via the terminal:
1
2
❯ curl https://api.eu-gb.codeengine.cloud.ibm.com/v2/projects/4131b8cb-2054-400e-a327-8f1dd41c14e6/apps/ce-serverless-app
Here is a simple web-server!%
As you can see, the response is the same as the one received in step 1 and step 2 when we tested the app locally by running the go application and Docker container locally. However, this time the app is running in IBM Cloud as a Serverless application on Code Engine! 🚀
Note: The above URL is no longer working, as I have destroyed all resources after testing.
Conclusions
In this quick tutorial we created a very simple framework to support the full lifecycle of a serverless application on IBM Cloud Schematics using Terraform, Go, GitHub Actions, Schematics and some supporting Bash scripts.
Of course this is far from a production-grade solution, but it can provide a starting point on how to integrate production-grade IBM Cloud technologies and mainstream solutions like GitHub and Docker to efficiently deploy a serverless application.