Gopher

I wrote this post for sharing, how we can do continuous integration for Golang APIs with some tools. In this post I’m going to use terraform for making a codebuild resource, Code build to build the Golang API and generate a docker image and ECR for keeping the docker image.

The workflow:

The continuous integration workflow is very easy to understand, every time a new tag is created the webhook will start the job, Codebuild will compile the Golang API and will generate the docker image, then will run a docker push to AWS ECR.

For this exercise, I pushed the code at https://github.com/braybaut/Golang-API This repository has the Golang API, Terraform files to deploy the AWS resources and buildspec to generate the steps

This code can be consulted at Github: https://github.com/braybaut/Golang-API

├── books.go
├── config
│   ├── aws.tf
│   ├── buildspec.yml
│   ├── codebuild.tf
│   ├── terraform.tfvars
│   └── variables.tf
├── Dockerfile
├── handlers.go
├── main.go
├── models.go
├── README.md
├── router.go
└── routes.go

The Golang Application is a minimal API that just show a little information of some books information that I’ve read:

Routes:

func init() {
	routes = Routes{
		Route{
			"Index",
			"GET",
			"/",
			"Index",
			Index,
		},
		Route{
			"TodoShow",
			"GET",
			"/books/{bookId}",
			"Get specific book with ID",
			GetBook,
		},
		Route{
			"TodoIndex",
			"GET",
			"/books",
			"Get all Books",
			GetBooks,
		},

		Route{
			"info",
			"GET",
			"/info",
			"Get Paths info",
			GetPaths,
		},
	}
}

I won’t talk much about the Golang code if you want to know more about the code, you can see the code at GitHub (I can write a post about golang in the future).

In orchestration Environment where many microservices run (for example golang) we need to pack our microservices, yes… the Golang binary can be our artifact but in this scenario, the real artifact is the Docker images that keep the Golang binary. For this case codebuild must compile the source code and generate the docker image, I wrote this buildspec.yml that run a docker build and docker do magic!!

Dockerfile:

FROM golang:alpine AS  builder 

RUN apk update && apk add --no-cache git

RUN mkdir /app
ADD . /app
WORKDIR /app
RUN go get -d -v  
RUN go build -o library

FROM golang:alpine

COPY --from=builder /app/library /

EXPOSE 8080

ENTRYPOINT ["/library"]

The Dockerfile generates the Golang binary (library) y run this like entry point, also use the feature (multistage build) where we can make more optimized images (less size, fewer layers, etc)

Codebuild will handle the process of generating the docker image, for this, we can define the instruction (phases) with a yml file ( buildspec.yml)

The buildspec.yml must run these steps:

  • login to ECR
  • Docker build and tag the docker image
  • Docker push to ECR

buildspec.yml

version: 0.2

git-credential-helper: yes

phases:
    install:
        runtime-versions:
            docker: 18
    pre_build:
        commands:
            - echo "loggin to AWS ECR"
            - $(aws ecr get-login --no-include-email --region $AWS_DEFAULT_REGION)

    build:
        commands:
            - echo "Getting Tag.."
            - |
              if [ -z $CODEBUILD_WEBHOOK_TRIGGER ]; then  
                export IMAGE_TAG=$CODEBUILD_SOURCE_VERSION
              else 
                export IMAGE_TAG=$(echo $CODEBUILD_WEBHOOK_TRIGGER | cut -d "/" -f2)
                fi 
            - echo "verify tag.."
            - echo $IMAGE_TAG 
            - echo "Building the docker image.."
            - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG .
            - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG 
    post_build:
        commands:
            - echo "Pushing Docker image"
            - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG 

How determinate the tag?

Our pipeline can start manually or automatically, if start manually we can set the repository tag to build, this tag is keeping an environment variable CODEBUILD_SOURCE_VERSION, but if start automatically the value is keeping in CODEBUILD_WEBHOOK_TRIGGER to know this, the build stage has a conditional to get the correct tag

if [ -z $CODEBUILD_WEBHOOK_TRIGGER ]; then  
   export IMAGE_TAG=$CODEBUILD_SOURCE_VERSION
else 
   export IMAGE_TAG=$(echo $CODEBUILD_WEBHOOK_TRIGGER | cut -d "/" -f2)
fi 

We have the golang code and dockerfile to build the docker image and buildspec.yml, now we need the terraform file to deploy codebuild and ECR.

Deploying Codebuild:

For this point, we have some terraform files, for example:

terraform.tfvars: Set the needed variables for example AWS_ACCOUNT_ID

AWS_ACCOUNT_ID=
AWS_REGION="us-east-1"
IMAGE_REPO_NAME="library-api"
URL_REPO="https://github.com/braybaut/Golang-API.git"

variables.tf: make the variables with terraform.tfvars values

variable "URL_REPO" {
  type = string

}

variable "AWS_REGION" {
  type = string
}

variable "IMAGE_REPO_NAME" {
  type = string
}

variable "AWS_ACCOUNT_ID" {
  type = string
}

aws.tf: Set the provider to use

provider "aws" {
  region = "${var.AWS_REGION}"
}

codebuild.tf: policies, roles, codebuild, ECR to create

This file has S3, ECR, policies, role, and other definitions that are needed to this workflow, but I just paste the codebuild resources, this definition only set some environment variables needed to Buildspec, set the log-stream configuration and define the source

resource "aws_codebuild_project" "library-build" {
  name = "library-build"
  description = "Easy build to generate docker image for Golang Applications"
  build_timeout = "5"
  service_role = "${aws_iam_role.code-build-library.arn}"

  artifacts {
    type = "NO_ARTIFACTS"
  }

  cache {
    type = "S3"
    location = "${aws_s3_bucket.library-build.bucket}"
  }

  environment {
    compute_type = "BUILD_GENERAL1_SMALL"
    image = "aws/codebuild/standard:2.0"
    type = "LINUX_CONTAINER"
    image_pull_credentials_type = "CODEBUILD"
privileged_mode = true

   environment_variable {
      name = "AWS_DEFAULT_REGION"
      value = "${var.AWS_REGION}"
    }
   environment_variable {
      name = "AWS_ACCOUNT_ID"
      value = "${var.AWS_ACCOUNT_ID}"
    }

    environment_variable {
      name = "IMAGE_REPO_NAME"
      value = "${var.IMAGE_REPO_NAME}"
    }

  }

  logs_config {
    cloudwatch_logs {
      group_name = "log-group"
      stream_name = "log-stream"
    }

    s3_logs {
      status = "ENABLED"
      location = "${aws_s3_bucket.library-build.id}/build-log"
    }
  }

  source {
    type = "GITHUB"
    location = "${var.URL_REPO}"
    git_clone_depth = 1
    buildspec = "${file("buildspec.yml")}"
  }
 
}

Enabling webhook:

This definition will create a webhook to start the job, this webhook has filters that exclude everything that meets the “^refs/heads/.” pattern ( new commits, new branches, etc ) and the hook will trigger when the pattern “^refs/tags/.” is met ( create new tags )

resource "aws_codebuild_webhook" "library-webhook" {
  project_name = "${aws_codebuild_project.library-build.name}"

  filter_group {
    filter {
      type = "EVENT"
      pattern = "PUSH"
    }

   filter {
      type = "HEAD_REF"
      pattern = "^refs/tags/.*"
    }
   filter {
      type = "HEAD_REF"
      pattern = "^refs/heads/.*"
      exclude_matched_pattern = "true"
    }


  }
}

Now that we have all files to make the pipeline, we need to deploy all resources, to Deploy terraform files is needed set the AWS keys as environment variables, then run terraform init and terraform apply commands:

Terraform will create 7 resources, if the creation process was good, we can see these resources created:

    aws_codebuild_project.library-build
    aws_codebuild_webhook.library-webhook
    aws_ecr_repository.library-ecr
    aws_iam_role.code-build-library
    aws_iam_role_policy.ecr-policy
    aws_iam_role_policy.s3-policy
    aws_s3_bucket.library-build

we can see the new Codebuild project and the ECR created

Codebuild project:

ECR

The resources is created, we can test the webhook create a new tag from github:

we create a new release:

Verify that the build is running:

The build was started, also we can see the logs

When the job finished, we can see the new docker image at ECR

Our Continuous integration workflow finish there, we can see that the docker image tag is the same to the tag set as release at github:

This was a little example that how we can create an easy continuous integration for Golang APIs with Codebuild, GitHub and Terraform, In the next post I will write how can create a continuous delivery workflow with CodeDeploy and Fargate and other post joining both workflows to make a pipeline with continuous integration and delivery workflow.