CategoriesInfrastructureProgramming

Golang Private Module with CDK CodeBuild

Even experienced builders run into things from time to time that they haven’t seen before and this causes them some trouble. I’ve been working with CDK, CodePipeline, CodeBuild and Golang for several years now and haven’t needed to construct a private Golang module. That changed a few weeks ago and it threw me, as I needed to also include it in a CodePipeline with a CodeBuild step. This article is more documentation and reference for the future, as I want to share the pattern learned for building Golang private modules with CodeBuild.

Solution Diagram

For reference, here is the solution diagram that I’ll be referencing throughout the article. For the infrastructure, I’ll be using CDK with TypeScript.

Building Golang private modules CodeBuild

The Pipeline

Let’s walk through the CodePipeline that’ll be responsible for receiving changes from GitHub and then running the build and deployment.

export class PipelineStack extends cdk.Stack {
    constructor(scope: Construct, id: string) {
        super(scope, id);

        const pipeline = new CodePipeline(this, "Pipeline", {
            pipelineName: "SamplePipeline",
            dockerEnabledForSynth: true,
            synth: new CodeBuildStep("Synth", {
                input: CodePipelineSource.gitHub(
                    "benbpyle/cdk-step-functions-local-testing",
                    "main",
                    {
                        authentication: SecretValue.secretsManager(
                            "sf-sample",
                            {
                                jsonField: "github",
                            }
                        ),
                    }
                ),

                buildEnvironment: {
                    buildImage: LinuxBuildImage.STANDARD_6_0,
                    environmentVariables: {
                        GITHUB_USERNAME: {
                            value: "benbpyle",
                            type: BuildEnvironmentVariableType.PLAINTEXT,
                        },
                        GITHUB_TOKEN: {
                            value: "sf-sample:github",
                            type: BuildEnvironmentVariableType.SECRETS_MANAGER,
                        },
                    },
                },
                partialBuildSpec: BuildSpec.fromObject({
                    phases: {
                        install: {
                            "runtime-versions": {
                                golang: "1.18",
                            },
                        },
                    },
                }),

                commands: [
                    'echo "machine github.com login $GITHUB_USERNAME password $GITHUB_TOKEN" >> ~/.netrc',
                    "npm i",
                    "export GOPRIVATE=github.com/benbpyle",
                    "npx cdk synth",
                ],
            }),
        });

        pipeline.addStage(new PipelineAppStage(this, `Deploy`, {}));
    }
}

I want to break down a few of the components of this.

The Source Action

I’m using a GitHub source and SecretsManager for storing a Personal Access Token that will handle changes and pulling the source into the CodeBuild step

input: CodePipelineSource.gitHub(
    "benbpyle/cdk-step-functions-local-testing",
    "main",
    {
        authentication: SecretValue.secretsManager(
            "sf-sample",
            {
                jsonField: "github",
            }
        ),
    }
),

Build Step

The build step also needs to have access to the SecretsManager. I’ll explain that in the commands block below. The BuildEnvironment allows me to set the build image and then environment variables. By using SecretsManager I can keep that access token hidden from view yet have access to it in the build. CodeBuild also does a nice job of masking ***** the value if you try and echo it out.

buildEnvironment: {
    buildImage: LinuxBuildImage.STANDARD_6_0,
    environmentVariables: {
        GITHUB_USERNAME: {
            value: "benbpyle",
            type: BuildEnvironmentVariableType.PLAINTEXT,
        },
        GITHUB_TOKEN: {
            value: "sf-sample:github",
            type: BuildEnvironmentVariableType.SECRETS_MANAGER,
        },
    },
},

Build Commands

The crux of this pattern is that I’m using the ~/.netrc file to store my GitHub PAT for logging in when Golang issues the command to pull from GitHub. For more on ~/.netrc, here’s a link to GNU. And for reading how Golang Modules work

commands: [
    'echo "machine github.com login $GITHUB_USERNAME password $GITHUB_TOKEN" >> ~/.netrc',
    "npm i",
    "export GOPRIVATE=github.com/benbpyle",
    "npx cdk synth",
],

When the npx cdk synth command gets run, it’ll find that there is a Golang function in the Stack and go mod tidy will be executed which initiates the pull from the dependencies. The other key piece is that I’m setting the $GOPRIVATE environment variable which tells go to not use the public package registry and pull packages from these specific locations. This variable can be a top-level path or it can a comma-separated list. An article that describes its usage when it was released several Golang versions ago.

Deployment

For this example, I’ve got a single Stage that I’m deploying out to but in a production use case, you’d have your Dev, Test, Pre-Prod, Prod etc environments.

// The stage
export class PipelineAppStage extends cdk.Stage {
    constructor(scope: Construct, id: string, props: cdk.StageProps) {
        super(scope, id, props);

        new MainStack(this, `App`, {});
    }
}

// MainStack
export class MainStack extends cdk.Stack {
    constructor(scope: Construct, id: string, props: cdk.StackProps) {
        super(scope, id, props);

        new ExampleFunc(this, "ExampleFunc");
    }
}

// Function Definition
export class ExampleFunc extends Construct {
    constructor(scope: Construct, id: string) {
        super(scope, id);

        new GoFunction(scope, `ExampleFuncHandler`, {
            entry: path.join(__dirname, `../../../src/example-func`),
            functionName: `example-func`,
            timeout: Duration.seconds(30),
            bundling: {
                goBuildFlags: ['-ldflags "-s -w"'],
            },
        });
    }
}

Not much to discuss here, but you can see the definitions of the:

  • AppStage
  • MainStack
  • ExampleFunc

Bringing it all together, adding the stage to the Pipeline

pipeline.addStage(new PipelineAppStage(this, `Deploy`, {}));

Building Golang private modules with CodeBuild

Golang leverages a go.mod file and a go.sum file that stores the dependencies, the versions and the checksum of those dependencies. You also have direct and indirect dependencies listed if your code imports something directly or something your code imports has that dependency.

The go.mod file for this example looks like this.

I’ve got dependencies on

  • AWS
  • Sirupsen (logrus)
  • My personal private library
module example

go 1.18

require (
    github.com/aws/aws-lambda-go v1.40.0
    github.com/sirupsen/logrus v1.9.0
)

require (
    github.com/benbpyle/golang-private-sample v0.0.0-20230506132255-dc7062e24dff
    github.com/stretchr/testify v1.8.2
    golang.org/x/sys v0.7.0
)

And the handler code references those things in the go.mod file and just prints out the message

package main

import (
    "context"

    "github.com/aws/aws-lambda-go/lambda"
    s "github.com/benbpyle/golang-private-sample"
    "github.com/sirupsen/logrus"
)

func main() {
    lambda.Start(handler)
}

func handler(ctx context.Context, event interface{}) error {
    logrus.Info("Logging out the handler")

    s.TestMe("the handler")

    return nil
}

Wrap Up

Putting this all together will give you the ability to have some level of privacy in your Golang modules if you need to. And when building Golang private modules with CodeBuild, you can include this easily into your pipelines with CDK, Terraform or native CloudFormation. This approach will work too if you are using another CI/CD execution framework than CodePipeline.

As always, the source code for this article is available on GitHub. Feel free to clone it and try it out. But note that you won’t have access to the following.

  • Replace my private repos with yours
  • The sf-sample Secret is one I created, you’ll need to create your own

Enjoy and happy building!

Published by Benjamen Pyle

Benjamen is a genuine and resourceful technology creator with over 20 years of hands-on software development, team building and leadership experience. His passion is enabling technology teams to be their best by bridging modern technical design with outstanding business problem-solving. Recognized as an AWS Community leader in the areas of Event-Driven and Serverless Architecture, he brings multiple years of pragmatic experience designing and operating modern cloud-native and containerized solutions. When Benjamen doesn't have his head in the clouds, he's either playing golf with his wife and 2 boys or they are outside with their 12 paws.