DEV Community

Cover image for Golang Private Module with CDK CodeBuild
Benjamen Pyle for AWS Community Builders

Posted on • Originally published at binaryheap.com

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`, {}));
    }
}
Enter fullscreen mode Exit fullscreen mode

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",
            }
        ),
    }
),
Enter fullscreen mode Exit fullscreen mode

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,
        },
    },
},
Enter fullscreen mode Exit fullscreen mode

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",
],
Enter fullscreen mode Exit fullscreen mode

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"'],
            },
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

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`, {}));
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)