From 7ba159ad01a59aaf138dbf98954ece418600ebef Mon Sep 17 00:00:00 2001 From: Scott Lowe Date: Sat, 20 Jan 2024 02:14:15 -0700 Subject: [PATCH] Add SSH-based Docker provider example Add Go program illustrating use of an SSH-based Docker provider Signed-off-by: Scott Lowe --- docker/docker-ssh-pulumi/Pulumi.yaml | 3 + docker/docker-ssh-pulumi/README.md | 49 +++++++ docker/docker-ssh-pulumi/go.mod | 95 ++++++++++++++ docker/docker-ssh-pulumi/main.go | 188 +++++++++++++++++++++++++++ 4 files changed, 335 insertions(+) create mode 100644 docker/docker-ssh-pulumi/Pulumi.yaml create mode 100644 docker/docker-ssh-pulumi/README.md create mode 100644 docker/docker-ssh-pulumi/go.mod create mode 100644 docker/docker-ssh-pulumi/main.go diff --git a/docker/docker-ssh-pulumi/Pulumi.yaml b/docker/docker-ssh-pulumi/Pulumi.yaml new file mode 100644 index 0000000..9e8ceeb --- /dev/null +++ b/docker/docker-ssh-pulumi/Pulumi.yaml @@ -0,0 +1,3 @@ +name: docker-ssh-pulumi +runtime: go +description: A Go Pulumi program to use SSH with a remote Docker host on AWS diff --git a/docker/docker-ssh-pulumi/README.md b/docker/docker-ssh-pulumi/README.md new file mode 100644 index 0000000..889eb74 --- /dev/null +++ b/docker/docker-ssh-pulumi/README.md @@ -0,0 +1,49 @@ +# Remote Docker Host via SSH on AWS Using Pulumi + +These files were created to demonstrate how to create a [Pulumi](https://www.pulumi.com) Docker provider that connects via SSH to a remote [Flatcar Linux](https://www.flatcar.org/) EC2 instance on AWS. This Pulumi program was written in [Go](https://go.dev). + +Based on the "Flatcar Linux on AWS Using Pulumi" code, the Pulumi program here illustrates a couple useful patterns: + +* Use of the `slices.Contains()` method for checking configuration values passed in from the user +* Using [the Pulumiverse Time provider](https://www.pulumi.com/registry/packages/time/) to introduce a delay in resource creation (allowing the EC2 instance to become ready) +* Configuring an explicit Docker provider to use SSH + +## Contents + +* `go.mod`: This file contains dependencies used by this Go program. + +* `go.sum`: This file contains checksums for each of the direct and indirect dependencies. The checksum is used to validate that none of them has been modified. + +* `main.go`: This Go file is the Pulumi program executed by the `pulumi` CLI, and contains the resource definitions to create a VPC with only public subnets, a security group to allow SSH access, and a Flatcar Linux-based EC2 instance in one of the public subnets. + +* `Pulumi.yaml`: This is the Pulumi project file. + +* `README.md`: This file you're currently reading. + +## Instructions + +These instructions assume you've already installed and configured Pulumi and all necessary dependencies (Go, for this example). Please refer to the Pulumi documentation for more details on installation or configuration. + +1. Copy the contents of this directory down to a directory on your system, or clone the entire repository and then change into the directory where this section of the cloned repository resides. + +1. Run `pulumi stack init` to create a new stack. + +1. Run `pulumi config set aws:region ` to set the AWS region where the Pulumi program should create resources. _This is a required configuration value; CLI operations will fail if you don't set this value._ + +1. Run `pulumi config set keypair ` to set the name of the AWS keypair that should be used. _This is a required configuration value._ + +1. Run `pulumi config set privatekeyfile ` to set the name of the private key file for the AWS keypair specified in the previous step. _This is a required configuration value._ + +1. (Optional) Run `pulumi config set` to set configuration values that affect the behavior of the Pulumi program. The optional configuration values are: + + * `architecture`: Set this to "amd64" or "arm64". The values "x86_64" and "x64" are also supported and will have the same effect as "amd64". The default value is "arm64". + * `networkcidr`: Set this to control the CIDR that will be used when the VPC is created. The default value is "10.0.0.0/16". + * `channel`: Set this to "stable", "alpha", "beta", or "lts" to control the release channel for the Flatcar Linux instance. The default value is "stable". + +1. Run `pulumi up` to instantiate the resources. The Pulumi program will create the Flatcar Linux EC2 instance, then use the Docker provider to pull down an image and launch a container---all remotely via SSH. + +Enjoy! When you're finished, run `pulumi destroy` to tear down all the provisioned resources. + +## License + +This content is licensed under the MIT License. diff --git a/docker/docker-ssh-pulumi/go.mod b/docker/docker-ssh-pulumi/go.mod new file mode 100644 index 0000000..2ce8ab5 --- /dev/null +++ b/docker/docker-ssh-pulumi/go.mod @@ -0,0 +1,95 @@ +module docker-ssh-pulumi + +go 1.21 + +require ( + github.com/pulumi/pulumi-aws/sdk/v6 v6.18.1 + github.com/pulumi/pulumi-awsx/sdk/v2 v2.1.1 + github.com/pulumi/pulumi-docker/sdk/v4 v4.5.1 + github.com/pulumi/pulumi/sdk/v3 v3.101.1 + github.com/pulumiverse/pulumi-time/sdk v0.0.0-20231010123146-089d7304da13 +) + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect + github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect + github.com/agext/levenshtein v1.2.3 // indirect + github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/blang/semver v3.5.1+incompatible // indirect + github.com/charmbracelet/bubbles v0.16.1 // indirect + github.com/charmbracelet/bubbletea v0.24.2 // indirect + github.com/charmbracelet/lipgloss v0.7.1 // indirect + github.com/cheggaaa/pb v1.0.29 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect + github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/djherbis/times v1.5.0 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.5.0 // indirect + github.com/go-git/go-git/v5 v5.11.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/glog v1.1.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/hcl/v2 v2.17.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mitchellh/go-ps v1.0.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/opentracing/basictracer-go v1.1.0 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/pgavlin/fx v0.1.6 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/term v1.1.0 // indirect + github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 // indirect + github.com/pulumi/esc v0.6.2 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect + github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect + github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 // indirect + github.com/sergi/go-diff v1.3.1 // indirect + github.com/skeema/knownhosts v1.2.1 // indirect + github.com/spf13/cast v1.4.1 // indirect + github.com/spf13/cobra v1.7.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/texttheater/golang-levenshtein v1.0.1 // indirect + github.com/tweekmonster/luser v0.0.0-20161003172636-3fa38070dbd7 // indirect + github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect + github.com/uber/jaeger-lib v2.4.1+incompatible // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/zclconf/go-cty v1.13.2 // indirect + go.uber.org/atomic v1.9.0 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sync v0.5.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/term v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.15.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230731190214-cbb8c96f2d6d // indirect + google.golang.org/grpc v1.57.1 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + lukechampine.com/frand v1.4.2 // indirect +) diff --git a/docker/docker-ssh-pulumi/main.go b/docker/docker-ssh-pulumi/main.go new file mode 100644 index 0000000..91b755b --- /dev/null +++ b/docker/docker-ssh-pulumi/main.go @@ -0,0 +1,188 @@ +package main + +import ( + "fmt" + "log" + "slices" + + "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/ec2" + awsx "github.com/pulumi/pulumi-awsx/sdk/v2/go/awsx/ec2" + "github.com/pulumi/pulumi-docker/sdk/v4/go/docker" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi/config" + "github.com/pulumiverse/pulumi-time/sdk/go/time" +) + +func main() { + pulumi.Run(func(ctx *pulumi.Context) error { + // Set up maps that are used later + channelNames := []string{"stable", "beta", "alpha", "lts"} + typeMap := map[string]string{"amd64": "t3a.small", "arm64": "t4g.small", "x86_64": "t3a.small", "x64": "t3a.small"} + + // Retrieve configuration values + instanceCpuArch, err := config.Try(ctx, "architecture") + if err != nil { + instanceCpuArch = "arm64" + } + instanceType, ok := typeMap[instanceCpuArch] + if !ok { + instanceCpuArch = "arm64" + instanceType = "t4g.small" + } + if instanceCpuArch == "x86_64" || instanceCpuArch == "x64" { + instanceCpuArch = "amd64" + } + vpcNetworkCidr, err := config.Try(ctx, "networkcidr") + if err != nil { + vpcNetworkCidr = "10.0.0.0/16" + } + channel, err := config.Try(ctx, "channel") + if err != nil { + channel = "stable" + } + if !slices.Contains(channelNames, channel) { + channel = "stable" + } + userSuppliedKeyPair := config.Require(ctx, "keypair") + userSuppliedPrivateKeyFile := config.Require(ctx, "privatekeyfile") + + // Create a new VPC, subnets, and associated infrastructure + dockerVpc, err := awsx.NewVpc(ctx, "docker-vpc", &awsx.VpcArgs{ + CidrBlock: &vpcNetworkCidr, + EnableDnsHostnames: pulumi.Bool(true), + EnableDnsSupport: pulumi.Bool(true), + NatGateways: &awsx.NatGatewayConfigurationArgs{ + Strategy: awsx.NatGatewayStrategyNone, + }, + SubnetSpecs: []awsx.SubnetSpecArgs{ + { + Type: awsx.SubnetTypePublic, + }, + }, + }) + if err != nil { + log.Printf("error creating VPC: %s", err.Error()) + } + + // Create a Security Group that we can use to connect to our instance + dockerSg, err := ec2.NewSecurityGroup(ctx, "docker-sg", &ec2.SecurityGroupArgs{ + VpcId: dockerVpc.VpcId, + Egress: ec2.SecurityGroupEgressArray{ + ec2.SecurityGroupEgressArgs{ + Protocol: pulumi.String("-1"), + FromPort: pulumi.Int(0), + ToPort: pulumi.Int(0), + CidrBlocks: pulumi.StringArray{pulumi.String("0.0.0.0/0")}, + }, + }, + Ingress: ec2.SecurityGroupIngressArray{ + ec2.SecurityGroupIngressArgs{ + Protocol: pulumi.String("tcp"), + FromPort: pulumi.Int(22), + ToPort: pulumi.Int(22), + CidrBlocks: pulumi.StringArray{pulumi.String("0.0.0.0/0")}, + }, + ec2.SecurityGroupIngressArgs{ + Protocol: pulumi.String("tcp"), + FromPort: pulumi.Int(2379), + ToPort: pulumi.Int(2380), + Description: pulumi.String("Allow etcd traffic from this security group"), + Self: pulumi.Bool(true), + }, + ec2.SecurityGroupIngressArgs{ + Protocol: pulumi.String("tcp"), + FromPort: pulumi.Int(4001), + ToPort: pulumi.Int(4001), + Description: pulumi.String("Allow etcd traffic from this security group"), + Self: pulumi.Bool(true), + }, + ec2.SecurityGroupIngressArgs{ + Protocol: pulumi.String("tcp"), + FromPort: pulumi.Int(7001), + ToPort: pulumi.Int(7001), + Description: pulumi.String("Allow etcd traffic from this security group"), + Self: pulumi.Bool(true), + }, + }, + }) + if err != nil { + log.Printf("error creating security group: %s", err.Error()) + } + + // Get AMI ID for Flatcar Linux instance + amiName := fmt.Sprintf("Flatcar-%s-*", channel) + flatcarAmi, err := ec2.LookupAmi(ctx, &ec2.LookupAmiArgs{ + Owners: []string{"075585003325"}, + MostRecent: pulumi.BoolRef(true), + Filters: []ec2.GetAmiFilter{ + {Name: "name", Values: []string{amiName}}, + {Name: "root-device-type", Values: []string{"ebs"}}, + {Name: "virtualization-type", Values: []string{"hvm"}}, + {Name: "architecture", Values: []string{instanceCpuArch}}, + }, + }) + if err != nil { + log.Printf("error looking up AMI: %s", err.Error()) + } + + // Launch an instance using Flatcar Linux AMI + flatcarInstance, err := ec2.NewInstance(ctx, "flatcar-instance", &ec2.InstanceArgs{ + Ami: pulumi.String(flatcarAmi.Id), + InstanceType: pulumi.String(instanceType), + AssociatePublicIpAddress: pulumi.Bool(true), + KeyName: pulumi.StringPtr(userSuppliedKeyPair), + SubnetId: dockerVpc.PublicSubnetIds.Index(pulumi.Int(0)), + VpcSecurityGroupIds: pulumi.StringArray{dockerSg.ID()}, + Tags: pulumi.StringMap{ + "Name": pulumi.String("flatcar-instance"), + }, + }) + if err != nil { + log.Printf("error launching instance: %s", err.Error()) + } + + // Sleep for 15 seconds to allow instance to boot + instanceBootDelay, err := time.NewSleep(ctx, "instance-boot-delay", &time.SleepArgs{ + CreateDuration: pulumi.String("20s"), + }, pulumi.DependsOn([]pulumi.Resource{flatcarInstance})) + if err != nil { + log.Printf("error generating delay: %s", err.Error()) + } + + // Create a new Docker provider + remoteDocker, err := docker.NewProvider(ctx, "remote-docker", &docker.ProviderArgs{ + Host: pulumi.Sprintf("ssh://core@%s", flatcarInstance.PublicIp), + SshOpts: pulumi.StringArray{ + pulumi.String("-i"), pulumi.String(userSuppliedPrivateKeyFile), + pulumi.String("-o"), pulumi.String("StrictHostKeyChecking=no"), + pulumi.String("-o"), pulumi.String("UserKnownHostsFile=/dev/null"), + }, + }, pulumi.DependsOn([]pulumi.Resource{instanceBootDelay})) + if err != nil { + log.Printf("error creating provider: %s", err.Error()) + } + + // Pull down a container image on the remote host + nginxImage, err := docker.NewRemoteImage(ctx, "nginx-image", &docker.RemoteImageArgs{ + Name: pulumi.String("nginx:1.17.4-alpine"), + }, pulumi.Provider(remoteDocker)) + if err != nil { + log.Printf("error pulling remote image: %s", err.Error()) + } + + // Launch a container on the remote host + _, err = docker.NewContainer(ctx, "nginx-container", &docker.ContainerArgs{ + Image: nginxImage.ImageId, + }, pulumi.Provider(remoteDocker)) + if err != nil { + log.Printf("error creating remote container: %s", err.Error()) + } + + // Export some values as stack outputs + ctx.Export("instanceId", flatcarInstance.ID()) + ctx.Export("instancePublicIpAddress", flatcarInstance.PublicIp) + ctx.Export("instancePrivateIpAddress", flatcarInstance.PrivateIp) + + return nil + }) +}