buildlet: use cloud client with AWS buildlet

Use the cloud package for AWS functions. Remove the unused
destroyVM function.

Updates golang/go#36841

Change-Id: I00e1a20c904f7c4be6460ac302085b28f518d161
Reviewed-on: https://go-review.googlesource.com/c/build/+/236300
Run-TryBot: Carlos Amedee <carlos@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Alexander Rakoczy <alex@golang.org>
diff --git a/buildlet/aws.go b/buildlet/aws.go
deleted file mode 100644
index 1d96c66..0000000
--- a/buildlet/aws.go
+++ /dev/null
@@ -1,254 +0,0 @@
-// Copyright 2020 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package buildlet
-
-import (
-	"context"
-	"encoding/base64"
-	"encoding/json"
-	"errors"
-	"fmt"
-	"log"
-	"net"
-	"time"
-
-	"github.com/aws/aws-sdk-go/aws"
-	"github.com/aws/aws-sdk-go/aws/credentials"
-	"github.com/aws/aws-sdk-go/aws/request"
-	"github.com/aws/aws-sdk-go/aws/session"
-	"github.com/aws/aws-sdk-go/service/ec2"
-	"golang.org/x/build/buildenv"
-	"golang.org/x/build/dashboard"
-)
-
-// EC2UserData is stored in the user data for each EC2 instance. This is
-// used to store metadata about the running instance. The buildlet will retrieve
-// this on EC2 instances before allowing connections from the coordinator.
-type EC2UserData struct {
-	BuildletBinaryURL string            `json:"buildlet_binary_url,omitempty"`
-	BuildletHostType  string            `json:"buildlet_host_type,omitempty"`
-	BuildletImageURL  string            `json:"buildlet_image_url,omitempty"`
-	BuildletName      string            `json:"buildlet_name,omitempty"`
-	Metadata          map[string]string `json:"metadata,omitempty"`
-	TLSCert           string            `json:"tls_cert,omitempty"`
-	TLSKey            string            `json:"tls_key,omitempty"`
-	TLSPassword       string            `json:"tls_password,omitempty"`
-}
-
-// ec2Client represents the EC2 specific calls made durring the
-// lifecycle of a buildlet.
-type ec2Client interface {
-	DescribeInstancesWithContext(context.Context, *ec2.DescribeInstancesInput, ...request.Option) (*ec2.DescribeInstancesOutput, error)
-	RunInstancesWithContext(context.Context, *ec2.RunInstancesInput, ...request.Option) (*ec2.Reservation, error)
-	TerminateInstancesWithContext(context.Context, *ec2.TerminateInstancesInput, ...request.Option) (*ec2.TerminateInstancesOutput, error)
-	WaitUntilInstanceRunningWithContext(context.Context, *ec2.DescribeInstancesInput, ...request.WaiterOption) error
-}
-
-// AWSClient is the client used to create and destroy buildlets on AWS.
-type AWSClient struct {
-	client ec2Client
-}
-
-// NewAWSClient creates a new AWSClient.
-func NewAWSClient(region, keyID, accessKey string) (*AWSClient, error) {
-	s, err := session.NewSession(&aws.Config{
-		Region:      aws.String(region),
-		Credentials: credentials.NewStaticCredentials(keyID, accessKey, ""), // Token is only required for STS
-	})
-	if err != nil {
-		return nil, fmt.Errorf("failed to create AWS session: %v", err)
-	}
-	return &AWSClient{
-		client: ec2.New(s),
-	}, nil
-}
-
-// StartNewVM boots a new VM on EC2, waits until the client is accepting connections
-// on the configured port and returns a buildlet client configured communicate with it.
-func (c *AWSClient) StartNewVM(ctx context.Context, buildEnv *buildenv.Environment, hconf *dashboard.HostConfig, vmName, hostType string, opts *VMOpts) (*Client, error) {
-	// check required params
-	if opts == nil || opts.TLS.IsZero() {
-		return nil, errors.New("TLS keypair is not set")
-	}
-	if buildEnv == nil {
-		return nil, errors.New("invalid build enviornment")
-	}
-	if hconf == nil {
-		return nil, errors.New("invalid host configuration")
-	}
-	if vmName == "" || hostType == "" {
-		return nil, fmt.Errorf("invalid vmName: %q and hostType: %q", vmName, hostType)
-	}
-
-	// configure defaults
-	if opts.Description == "" {
-		opts.Description = fmt.Sprintf("Go Builder for %s", hostType)
-	}
-	if opts.Zone == "" {
-		opts.Zone = buildEnv.RandomEC2VMZone()
-	}
-	if opts.DeleteIn == 0 {
-		opts.DeleteIn = 30 * time.Minute
-	}
-
-	vmConfig := c.configureVM(buildEnv, hconf, vmName, hostType, opts)
-
-	vmID, err := c.createVM(ctx, vmConfig, opts)
-	if err != nil {
-		return nil, err
-	}
-	if err = c.WaitUntilVMExists(ctx, vmID, opts); err != nil {
-		return nil, err
-	}
-	vm, err := c.RetrieveVMInfo(ctx, vmID)
-	if err != nil {
-		return nil, err
-	}
-	buildletURL, ipPort, err := ec2BuildletParams(vm, opts)
-	if err != nil {
-		return nil, err
-	}
-	return buildletClient(ctx, buildletURL, ipPort, opts)
-}
-
-// createVM submits a request for the creation of a VM.
-func (c *AWSClient) createVM(ctx context.Context, vmConfig *ec2.RunInstancesInput, opts *VMOpts) (string, error) {
-	runResult, err := c.client.RunInstancesWithContext(ctx, vmConfig)
-	if err != nil {
-		return "", fmt.Errorf("unable to create instance: %w", err)
-	}
-	condRun(opts.OnInstanceRequested)
-	return *runResult.Instances[0].InstanceId, nil
-}
-
-// WaitUntilVMExists submits a request which waits until an instance exists before returning.
-func (c *AWSClient) WaitUntilVMExists(ctx context.Context, instID string, opts *VMOpts) error {
-	err := c.client.WaitUntilInstanceRunningWithContext(ctx, &ec2.DescribeInstancesInput{
-		InstanceIds: []*string{aws.String(instID)},
-	})
-	if err != nil {
-		return fmt.Errorf("failed waiting for vm instance: %w", err)
-	}
-	condRun(opts.OnInstanceCreated)
-	return err
-}
-
-// RetrieveVMInfo retrives the information about a VM.
-func (c *AWSClient) RetrieveVMInfo(ctx context.Context, instID string) (*ec2.Instance, error) {
-	instances, err := c.client.DescribeInstancesWithContext(ctx, &ec2.DescribeInstancesInput{
-		InstanceIds: []*string{aws.String(instID)},
-	})
-	if err != nil {
-		return nil, fmt.Errorf("unable to retrieve instance %q information: %w", instID, err)
-	}
-
-	instance, err := ec2Instance(instances)
-	if err != nil {
-		return nil, fmt.Errorf("failed to read instance description: %w", err)
-	}
-	return instance, err
-}
-
-// configureVM creates a configuration for an EC2 VM instance.
-func (c *AWSClient) configureVM(buildEnv *buildenv.Environment, hconf *dashboard.HostConfig, vmName, hostType string, opts *VMOpts) *ec2.RunInstancesInput {
-	vmConfig := &ec2.RunInstancesInput{
-		ImageId:      aws.String(hconf.VMImage),
-		InstanceType: aws.String(hconf.MachineType()),
-		MinCount:     aws.Int64(1),
-		MaxCount:     aws.Int64(1),
-		Placement: &ec2.Placement{
-			AvailabilityZone: aws.String(opts.Zone),
-		},
-		KeyName:                           aws.String("ec2-go-builders"),
-		InstanceInitiatedShutdownBehavior: aws.String("terminate"),
-		TagSpecifications: []*ec2.TagSpecification{
-			&ec2.TagSpecification{
-				ResourceType: aws.String("instance"),
-				Tags: []*ec2.Tag{
-					&ec2.Tag{
-						Key:   aws.String("Name"),
-						Value: aws.String(vmName),
-					},
-					&ec2.Tag{
-						Key:   aws.String("Description"),
-						Value: aws.String(opts.Description),
-					},
-				},
-			},
-		},
-	}
-	c.vmUserDataSpec(vmConfig, buildEnv, hconf, vmName, hostType, opts)
-	return vmConfig
-}
-
-func (c *AWSClient) vmUserDataSpec(vmConfig *ec2.RunInstancesInput, buildEnv *buildenv.Environment, hconf *dashboard.HostConfig, vmName, hostType string, opts *VMOpts) {
-	// add custom metadata to the user data.
-	ud := EC2UserData{
-		BuildletName:      vmName,
-		BuildletBinaryURL: hconf.BuildletBinaryURL(buildEnv),
-		BuildletHostType:  hostType,
-		BuildletImageURL:  hconf.ContainerVMImage(),
-		Metadata:          make(map[string]string),
-		TLSCert:           opts.TLS.CertPEM,
-		TLSKey:            opts.TLS.KeyPEM,
-		TLSPassword:       opts.TLS.Password(),
-	}
-	for k, v := range opts.Meta {
-		ud.Metadata[k] = v
-	}
-	jsonUserData, err := json.Marshal(ud)
-	if err != nil {
-		log.Printf("unable to marshal user data: %v", err)
-	}
-	// user data must be base64 encoded
-	jud := base64.StdEncoding.EncodeToString([]byte(jsonUserData))
-	vmConfig.SetUserData(jud)
-}
-
-// DestroyVM submits a request to destroy a VM.
-func (c *AWSClient) DestroyVM(ctx context.Context, vmID string) error {
-	_, err := c.client.TerminateInstancesWithContext(ctx, &ec2.TerminateInstancesInput{
-		InstanceIds: []*string{aws.String(vmID)},
-	})
-	if err != nil {
-		return fmt.Errorf("unable to destroy vm: %w", err)
-	}
-	return err
-}
-
-// ec2Instance extracts the first instance found in the the describe instances output.
-func ec2Instance(dio *ec2.DescribeInstancesOutput) (*ec2.Instance, error) {
-	if dio == nil || dio.Reservations == nil || dio.Reservations[0].Instances == nil {
-		return nil, errors.New("describe instances output does not contain a valid instance")
-	}
-	return dio.Reservations[0].Instances[0], nil
-}
-
-// ec2InstanceIPs returns the internal and external ip addresses for the VM.
-func ec2InstanceIPs(inst *ec2.Instance) (intIP, extIP string, err error) {
-	if inst.PrivateIpAddress == nil || *inst.PrivateIpAddress == "" {
-		return "", "", errors.New("internal IP address is not set")
-	}
-	if inst.PublicIpAddress == nil || *inst.PublicIpAddress == "" {
-		return "", "", errors.New("external IP address is not set")
-	}
-	return *inst.PrivateIpAddress, *inst.PublicIpAddress, nil
-}
-
-// ec2BuildletParams returns the necessary information to connect to an EC2 buildlet. A
-// buildlet URL and an IP address port are required to connect to a buildlet.
-func ec2BuildletParams(inst *ec2.Instance, opts *VMOpts) (string, string, error) {
-	_, extIP, err := ec2InstanceIPs(inst)
-	if err != nil {
-		return "", "", fmt.Errorf("failed to retrieve IP addresses: %w", err)
-	}
-	buildletURL := fmt.Sprintf("https://%s", extIP)
-	ipPort := net.JoinHostPort(extIP, "443")
-
-	if opts.OnGotEC2InstanceInfo != nil {
-		opts.OnGotEC2InstanceInfo(inst)
-	}
-	return buildletURL, ipPort, err
-}
diff --git a/buildlet/aws_test.go b/buildlet/aws_test.go
deleted file mode 100644
index d13871b..0000000
--- a/buildlet/aws_test.go
+++ /dev/null
@@ -1,741 +0,0 @@
-// Copyright 2020 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package buildlet
-
-import (
-	"context"
-	"encoding/base64"
-	"encoding/json"
-	"errors"
-	"testing"
-	"time"
-
-	"github.com/aws/aws-sdk-go/aws"
-	"github.com/aws/aws-sdk-go/aws/request"
-	"github.com/aws/aws-sdk-go/service/ec2"
-	"github.com/google/go-cmp/cmp"
-	"golang.org/x/build/buildenv"
-	"golang.org/x/build/dashboard"
-)
-
-type fakeEC2Client struct {
-	// returned in describe instances
-	PrivateIP *string
-	PublicIP  *string
-}
-
-func (f *fakeEC2Client) DescribeInstancesWithContext(ctx context.Context, input *ec2.DescribeInstancesInput, opt ...request.Option) (*ec2.DescribeInstancesOutput, error) {
-	if ctx == nil || input == nil || len(input.InstanceIds) == 0 {
-		return nil, request.ErrInvalidParams{}
-	}
-	return &ec2.DescribeInstancesOutput{
-		Reservations: []*ec2.Reservation{
-			&ec2.Reservation{
-				Instances: []*ec2.Instance{
-					&ec2.Instance{
-						InstanceId:       input.InstanceIds[0],
-						PrivateIpAddress: f.PrivateIP,
-						PublicIpAddress:  f.PublicIP,
-					},
-				},
-			},
-		},
-	}, nil
-}
-
-func (f *fakeEC2Client) RunInstancesWithContext(ctx context.Context, input *ec2.RunInstancesInput, opts ...request.Option) (*ec2.Reservation, error) {
-	if ctx == nil || input == nil {
-		return nil, request.ErrInvalidParams{}
-	}
-	if input.ImageId == nil || input.InstanceType == nil || input.MinCount == nil || input.Placement == nil {
-		return nil, errors.New("invalid instance configuration")
-	}
-	return &ec2.Reservation{
-		Instances: []*ec2.Instance{
-			&ec2.Instance{
-				ImageId:      input.ImageId,
-				InstanceType: input.InstanceType,
-				InstanceId:   aws.String("44"),
-				Placement:    input.Placement,
-			},
-		},
-		ReservationId: aws.String("res_id"),
-	}, nil
-}
-
-func (f *fakeEC2Client) TerminateInstancesWithContext(ctx context.Context, input *ec2.TerminateInstancesInput, opts ...request.Option) (*ec2.TerminateInstancesOutput, error) {
-	if ctx == nil || input == nil || len(input.InstanceIds) == 0 {
-		return nil, request.ErrInvalidParams{}
-	}
-	for _, id := range input.InstanceIds {
-		if *id == "" {
-			return nil, errors.New("invalid instance id")
-		}
-	}
-	return &ec2.TerminateInstancesOutput{
-		TerminatingInstances: nil,
-	}, nil
-}
-
-func (f *fakeEC2Client) WaitUntilInstanceRunningWithContext(ctx context.Context, input *ec2.DescribeInstancesInput, opt ...request.WaiterOption) error {
-	if ctx == nil || input == nil || len(input.InstanceIds) == 0 {
-		return request.ErrInvalidParams{}
-	}
-	return nil
-}
-
-func TestRetrieveVMInfo(t *testing.T) {
-	wantVMID := "22"
-	ctx := context.Background()
-	c := &AWSClient{
-		client: &fakeEC2Client{},
-	}
-	gotInst, gotErr := c.RetrieveVMInfo(ctx, wantVMID)
-	if gotErr != nil {
-		t.Fatalf("RetrieveVMInfo(%v, %q) failed with error %s", ctx, wantVMID, gotErr)
-	}
-	if gotInst == nil || *gotInst.InstanceId != wantVMID {
-		t.Errorf("RetrieveVMInfo(%v, %q) failed with error %s", ctx, wantVMID, gotErr)
-	}
-}
-
-func TestStartNewVM(t *testing.T) {
-	kp, err := NewKeyPair()
-	if err != nil {
-		t.Fatalf("unable to generate key pair: %s", err)
-	}
-	buildEnv := &buildenv.Environment{}
-	hconf := &dashboard.HostConfig{}
-	vmName := "sample-vm"
-	hostType := "host-sample-os"
-	opts := &VMOpts{
-		Zone:        "us-west",
-		ProjectID:   "project1",
-		TLS:         kp,
-		Description: "Golang builder for sample",
-		Meta: map[string]string{
-			"Owner": "george",
-		},
-		DeleteIn:                 45 * time.Second,
-		SkipEndpointVerification: true,
-	}
-	c := &AWSClient{
-		client: &fakeEC2Client{
-			PrivateIP: aws.String("8.8.8.8"),
-			PublicIP:  aws.String("9.9.9.9"),
-		},
-	}
-	gotClient, gotErr := c.StartNewVM(context.Background(), buildEnv, hconf, vmName, hostType, opts)
-	if gotErr != nil {
-		t.Fatalf("error is not nil: %v", gotErr)
-	}
-	if gotClient == nil {
-		t.Fatalf("response is nil")
-	}
-}
-
-func TestStartNewVMError(t *testing.T) {
-	kp, err := NewKeyPair()
-	if err != nil {
-		t.Fatalf("unable to generate key pair: %s", err)
-	}
-
-	testCases := []struct {
-		desc     string
-		buildEnv *buildenv.Environment
-		hconf    *dashboard.HostConfig
-		vmName   string
-		hostType string
-		opts     *VMOpts
-	}{
-		{
-			desc:     "nil-buildenv",
-			hconf:    &dashboard.HostConfig{},
-			vmName:   "sample-vm",
-			hostType: "host-sample-os",
-			opts: &VMOpts{
-				Zone:        "us-west",
-				ProjectID:   "project1",
-				TLS:         kp,
-				Description: "Golang builder for sample",
-				Meta: map[string]string{
-					"Owner": "george",
-				},
-				DeleteIn: 45 * time.Second,
-			},
-		},
-		{
-			desc:     "nil-hconf",
-			buildEnv: &buildenv.Environment{},
-			vmName:   "sample-vm",
-			hostType: "host-sample-os",
-			opts: &VMOpts{
-				Zone:        "us-west",
-				ProjectID:   "project1",
-				TLS:         kp,
-				Description: "Golang builder for sample",
-				Meta: map[string]string{
-					"Owner": "george",
-				},
-				DeleteIn: 45 * time.Second,
-			},
-		},
-		{
-			desc:     "empty-vnName",
-			buildEnv: &buildenv.Environment{},
-			hconf:    &dashboard.HostConfig{},
-			vmName:   "",
-			hostType: "host-sample-os",
-			opts: &VMOpts{
-				Zone:        "us-west",
-				ProjectID:   "project1",
-				TLS:         kp,
-				Description: "Golang builder for sample",
-				Meta: map[string]string{
-					"Owner": "george",
-				},
-				DeleteIn: 45 * time.Second,
-			},
-		},
-		{
-			desc:     "empty-hostType",
-			buildEnv: &buildenv.Environment{},
-			hconf:    &dashboard.HostConfig{},
-			vmName:   "sample-vm",
-			hostType: "",
-			opts: &VMOpts{
-				Zone:        "us-west",
-				ProjectID:   "project1",
-				TLS:         kp,
-				Description: "Golang builder for sample",
-				Meta: map[string]string{
-					"Owner": "george",
-				},
-				DeleteIn: 45 * time.Second,
-			},
-		},
-		{
-			desc:     "missing-certs",
-			buildEnv: &buildenv.Environment{},
-			hconf:    &dashboard.HostConfig{},
-			vmName:   "sample-vm",
-			hostType: "host-sample-os",
-			opts: &VMOpts{
-				Zone:        "us-west",
-				ProjectID:   "project1",
-				Description: "Golang builder for sample",
-				Meta: map[string]string{
-					"Owner": "george",
-				},
-				DeleteIn: 45 * time.Second,
-			},
-		},
-		{
-			desc:     "nil-opts",
-			buildEnv: &buildenv.Environment{},
-			hconf:    &dashboard.HostConfig{},
-			vmName:   "sample-vm",
-			hostType: "host-sample-os",
-		},
-	}
-	for _, tc := range testCases {
-		t.Run(tc.desc, func(t *testing.T) {
-			c := &AWSClient{
-				client: &fakeEC2Client{},
-			}
-			gotClient, gotErr := c.StartNewVM(context.Background(), tc.buildEnv, tc.hconf, tc.vmName, tc.hostType, tc.opts)
-			if gotErr == nil {
-				t.Errorf("expected error did not occur")
-			}
-			if gotClient != nil {
-				t.Errorf("got %+v; expected nil", gotClient)
-			}
-		})
-	}
-}
-
-func TestWaitUntilInstanceExists(t *testing.T) {
-	vmID := "22"
-	invoked := false
-	opts := &VMOpts{
-		OnInstanceCreated: func() {
-			invoked = true
-		},
-	}
-	ctx := context.Background()
-	c := &AWSClient{
-		client: &fakeEC2Client{},
-	}
-	gotErr := c.WaitUntilVMExists(ctx, vmID, opts)
-	if gotErr != nil {
-		t.Fatalf("WaitUntilVMExists(%v, %v, %v) failed with error %s", ctx, vmID, opts, gotErr)
-	}
-	if !invoked {
-		t.Errorf("OnInstanceCreated() was not invoked")
-	}
-}
-
-func TestCreateVM(t *testing.T) {
-	vmConfig := &ec2.RunInstancesInput{
-		ImageId:      aws.String("foo"),
-		InstanceType: aws.String("type-a"),
-		MinCount:     aws.Int64(15),
-		Placement: &ec2.Placement{
-			AvailabilityZone: aws.String("eu-15"),
-		},
-	}
-	invoked := false
-	opts := &VMOpts{
-		OnInstanceRequested: func() {
-			invoked = true
-		},
-	}
-	wantVMID := aws.String("44")
-
-	c := &AWSClient{
-		client: &fakeEC2Client{},
-	}
-	gotVMID, gotErr := c.createVM(context.Background(), vmConfig, opts)
-	if gotErr != nil {
-		t.Fatalf("createVM(ctx, %v, %v) failed with %s", vmConfig, opts, gotErr)
-	}
-	if gotVMID != *wantVMID {
-		t.Errorf("createVM(ctx, %v, %v) = %s, nil; want %s, nil", vmConfig, opts, gotVMID, *wantVMID)
-	}
-	if !invoked {
-		t.Errorf("OnInstanceRequested() was not invoked")
-	}
-}
-
-func TestCreateVMError(t *testing.T) {
-	testCases := []struct {
-		desc     string
-		vmConfig *ec2.RunInstancesInput
-		opts     *VMOpts
-	}{
-		{
-			desc: "missing-vmConfig",
-		},
-		{
-			desc: "missing-image-id",
-			vmConfig: &ec2.RunInstancesInput{
-				InstanceType: aws.String("type-a"),
-				MinCount:     aws.Int64(15),
-				Placement: &ec2.Placement{
-					AvailabilityZone: aws.String("eu-15"),
-				},
-			},
-			opts: &VMOpts{
-				OnInstanceRequested: func() {},
-			},
-		},
-		{
-			desc: "missing-instance-id",
-			vmConfig: &ec2.RunInstancesInput{
-				ImageId:  aws.String("foo"),
-				MinCount: aws.Int64(15),
-				Placement: &ec2.Placement{
-					AvailabilityZone: aws.String("eu-15"),
-				},
-			},
-			opts: &VMOpts{
-				OnInstanceRequested: func() {},
-			},
-		},
-		{
-			desc: "missing-placement",
-			vmConfig: &ec2.RunInstancesInput{
-				ImageId:      aws.String("foo"),
-				InstanceType: aws.String("type-a"),
-				MinCount:     aws.Int64(15),
-			},
-			opts: &VMOpts{
-				OnInstanceRequested: func() {},
-			},
-		},
-	}
-	for _, tc := range testCases {
-		t.Run(tc.desc, func(t *testing.T) {
-			c := &AWSClient{
-				client: &fakeEC2Client{},
-			}
-			gotVMID, gotErr := c.createVM(context.Background(), tc.vmConfig, tc.opts)
-			if gotErr == nil {
-				t.Errorf("createVM(ctx, %v, %v) = %s, %v; want error", tc.vmConfig, tc.opts, gotVMID, gotErr)
-			}
-			if gotVMID != "" {
-				t.Errorf("createVM(ctx, %v, %v) = %s, %v; %q, error", tc.vmConfig, tc.opts, gotVMID, gotErr, "")
-			}
-		})
-	}
-}
-
-func TestDestroyVM(t *testing.T) {
-	testCases := []struct {
-		desc    string
-		ctx     context.Context
-		vmID    string
-		wantErr bool
-	}{
-		{"baseline request", context.Background(), "vm-20", false},
-		{"nil context", nil, "vm-20", true},
-		{"nil context", context.Background(), "", true},
-	}
-	for _, tc := range testCases {
-		t.Run(tc.desc, func(t *testing.T) {
-			c := &AWSClient{
-				client: &fakeEC2Client{},
-			}
-			gotErr := c.DestroyVM(tc.ctx, tc.vmID)
-			if (gotErr != nil) != tc.wantErr {
-				t.Errorf("DestroyVM(%v, %q) = %v; want error %t", tc.ctx, tc.vmID, gotErr, tc.wantErr)
-			}
-		})
-	}
-}
-
-func TestEC2BuildletParams(t *testing.T) {
-	testCases := []struct {
-		desc       string
-		inst       *ec2.Instance
-		opts       *VMOpts
-		wantURL    string
-		wantPort   string
-		wantCalled bool
-	}{
-		{
-			desc: "base case",
-			inst: &ec2.Instance{
-				PrivateIpAddress: aws.String("9.9.9.9"),
-				PublicIpAddress:  aws.String("8.8.8.8"),
-			},
-			opts:       &VMOpts{},
-			wantCalled: true,
-			wantURL:    "https://8.8.8.8",
-			wantPort:   "8.8.8.8:443",
-		},
-	}
-	for _, tc := range testCases {
-		t.Run(tc.desc, func(t *testing.T) {
-			gotURL, gotPort, gotErr := ec2BuildletParams(tc.inst, tc.opts)
-			if gotErr != nil {
-				t.Fatalf("ec2BuildletParams(%v, %v) failed; %v", tc.inst, tc.opts, gotErr)
-			}
-			if gotURL != tc.wantURL || gotPort != tc.wantPort {
-				t.Errorf("ec2BuildletParams(%v, %v) = %q, %q, nil; want %q, %q, nil", tc.inst, tc.opts, gotURL, gotPort, tc.wantURL, tc.wantPort)
-			}
-		})
-	}
-}
-
-func TestConfigureVM(t *testing.T) {
-	testCases := []struct {
-		desc              string
-		buildEnv          *buildenv.Environment
-		hconf             *dashboard.HostConfig
-		hostType          string
-		opts              *VMOpts
-		vmName            string
-		wantDesc          string
-		wantImageID       string
-		wantInstanceType  string
-		wantName          string
-		wantZone          string
-		wantBuildletName  string
-		wantBuildletImage string
-	}{
-		{
-			desc:     "default-values",
-			buildEnv: &buildenv.Environment{},
-			hconf: &dashboard.HostConfig{
-				KonletVMImage: "gcr.io/symbolic-datum-552/gobuilder-arm64-aws",
-			},
-			vmName:            "base_vm",
-			hostType:          "host-foo-bar",
-			opts:              &VMOpts{},
-			wantInstanceType:  "n1-highcpu-2",
-			wantName:          "base_vm",
-			wantBuildletName:  "base_vm",
-			wantBuildletImage: "gcr.io/symbolic-datum-552/gobuilder-arm64-aws",
-		},
-		{
-			desc:     "full-configuration",
-			buildEnv: &buildenv.Environment{},
-			hconf: &dashboard.HostConfig{
-				VMImage:       "awesome_image",
-				KonletVMImage: "gcr.io/symbolic-datum-552/gobuilder-arm64-aws",
-			},
-			vmName:   "base-vm",
-			hostType: "host-foo-bar",
-			opts: &VMOpts{
-				Zone: "sa-west",
-				TLS: KeyPair{
-					CertPEM: "abc",
-					KeyPEM:  "xyz",
-				},
-				Description: "test description",
-				Meta: map[string]string{
-					"sample": "value",
-				},
-			},
-			wantDesc:          "test description",
-			wantImageID:       "awesome_image",
-			wantInstanceType:  "n1-highcpu-2",
-			wantName:          "base-vm",
-			wantZone:          "sa-west",
-			wantBuildletName:  "base-vm",
-			wantBuildletImage: "gcr.io/symbolic-datum-552/gobuilder-arm64-aws",
-		},
-	}
-	for _, tc := range testCases {
-		t.Run(tc.desc, func(t *testing.T) {
-			c := &AWSClient{}
-			got := c.configureVM(tc.buildEnv, tc.hconf, tc.vmName, tc.hostType, tc.opts)
-			if *got.ImageId != tc.wantImageID {
-				t.Errorf("ImageId got %s; want %s", *got.ImageId, tc.wantImageID)
-			}
-			if *got.InstanceType != tc.wantInstanceType {
-				t.Errorf("InstanceType got %s; want %s", *got.InstanceType, tc.wantInstanceType)
-			}
-
-			if *got.MinCount != 1 {
-				t.Errorf("MinCount got %d; want %d", *got.MinCount, 1)
-			}
-			if *got.MaxCount != 1 {
-				t.Errorf("MaxCount got %d; want %d", *got.MaxCount, 1)
-			}
-			if *got.Placement.AvailabilityZone != tc.wantZone {
-				t.Errorf("AvailabilityZone got %s; want %s", *got.Placement.AvailabilityZone, tc.wantZone)
-			}
-			if *got.InstanceInitiatedShutdownBehavior != "terminate" {
-				t.Errorf("InstanceType got %s; want %s", *got.InstanceInitiatedShutdownBehavior, "terminate")
-			}
-			if *got.TagSpecifications[0].ResourceType != "instance" {
-				t.Errorf("Tag Resource Type got %s; want %s", *got.TagSpecifications[0].ResourceType, "instance")
-			}
-			if *got.TagSpecifications[0].Tags[0].Key != "Name" {
-				t.Errorf("First Tag Key got %s; want %s", *got.TagSpecifications[0].Tags[0].Key, "Name")
-			}
-			if *got.TagSpecifications[0].Tags[0].Value != tc.wantName {
-				t.Errorf("First Tag Value got %s; want %s", *got.TagSpecifications[0].Tags[0].Value, tc.wantName)
-			}
-			if *got.TagSpecifications[0].Tags[1].Key != "Description" {
-				t.Errorf("Second Tag Key got %s; want %s", *got.TagSpecifications[0].Tags[1].Key, "Description")
-			}
-			if *got.TagSpecifications[0].Tags[1].Value != tc.wantDesc {
-				t.Errorf("Second Tag Value got %s; want %s", *got.TagSpecifications[0].Tags[1].Value, tc.wantDesc)
-			}
-			gotUD := &EC2UserData{}
-			gotUDJson, err := base64.StdEncoding.DecodeString(*got.UserData)
-			if err != nil {
-				t.Fatalf("unable to base64 decode string %q: %s", *got.UserData, err)
-			}
-			err = json.Unmarshal([]byte(gotUDJson), gotUD)
-			if err != nil {
-				t.Errorf("unable to unmarshal user data: %v", err)
-			}
-			if gotUD.BuildletBinaryURL != tc.hconf.BuildletBinaryURL(tc.buildEnv) {
-				t.Errorf("buildletBinaryURL got %s; want %s", gotUD.BuildletBinaryURL, tc.hconf.BuildletBinaryURL(tc.buildEnv))
-			}
-			if gotUD.BuildletHostType != tc.hostType {
-				t.Errorf("buildletHostType got %s; want %s", gotUD.BuildletHostType, tc.hostType)
-			}
-			if gotUD.BuildletName != tc.wantBuildletName {
-				t.Errorf("buildletName got %s; want %s", gotUD.BuildletName, tc.wantBuildletName)
-			}
-			if gotUD.BuildletImageURL != tc.wantBuildletImage {
-				t.Errorf("buildletImageURL got %s; want %s", gotUD.BuildletImageURL, tc.wantBuildletImage)
-			}
-
-			if gotUD.TLSCert != tc.opts.TLS.CertPEM {
-				t.Errorf("TLSCert got %s; want %s", gotUD.TLSCert, tc.opts.TLS.CertPEM)
-			}
-			if gotUD.TLSKey != tc.opts.TLS.KeyPEM {
-				t.Errorf("TLSKey got %s; want %s", gotUD.TLSKey, tc.opts.TLS.KeyPEM)
-			}
-			if gotUD.TLSPassword != tc.opts.TLS.Password() {
-				t.Errorf("TLSPassword got %s; want %s", gotUD.TLSPassword, tc.opts.TLS.Password())
-			}
-		})
-	}
-}
-
-func TestEC2Instance(t *testing.T) {
-	instSample1 := &ec2.Instance{
-		InstanceId: aws.String("id1"),
-	}
-	instSample2 := &ec2.Instance{
-		InstanceId: aws.String("id2"),
-	}
-	resSample1 := &ec2.Reservation{
-		Instances: []*ec2.Instance{
-			instSample1,
-		},
-		RequesterId:   aws.String("user1"),
-		ReservationId: aws.String("reservation12"),
-	}
-	resSample2 := &ec2.Reservation{
-		Instances: []*ec2.Instance{
-			instSample2,
-		},
-		RequesterId:   aws.String("user2"),
-		ReservationId: aws.String("reservation22"),
-	}
-
-	testCases := []struct {
-		desc     string
-		dio      *ec2.DescribeInstancesOutput
-		wantInst *ec2.Instance
-	}{
-		{
-			desc: "single reservation",
-			dio: &ec2.DescribeInstancesOutput{
-				Reservations: []*ec2.Reservation{
-					resSample1,
-				},
-			},
-			wantInst: instSample1,
-		},
-		{
-			desc: "multiple reservations",
-			dio: &ec2.DescribeInstancesOutput{
-				Reservations: []*ec2.Reservation{
-					resSample2,
-					resSample1,
-				},
-			},
-			wantInst: instSample2,
-		},
-	}
-	for _, tc := range testCases {
-		t.Run(tc.desc, func(t *testing.T) {
-			gotInst, gotErr := ec2Instance(tc.dio)
-			if gotErr != nil {
-				t.Errorf("ec2Instance(%v) failed: %v",
-					tc.dio, gotErr)
-			}
-			if !cmp.Equal(gotInst, tc.wantInst) {
-				t.Errorf("ec2Instance(%v) = %s; want %s",
-					tc.dio, gotInst, tc.wantInst)
-			}
-		})
-	}
-}
-
-func TestEC2InstanceError(t *testing.T) {
-	testCases := []struct {
-		desc string
-		dio  *ec2.DescribeInstancesOutput
-	}{
-		{
-			desc: "nil input",
-			dio:  nil,
-		},
-		{
-			desc: "nil reservation",
-			dio: &ec2.DescribeInstancesOutput{
-				Reservations: nil,
-			},
-		},
-		{
-			desc: "nil instances",
-			dio: &ec2.DescribeInstancesOutput{
-				Reservations: []*ec2.Reservation{
-					&ec2.Reservation{
-						Instances:     nil,
-						RequesterId:   aws.String("user1"),
-						ReservationId: aws.String("reservation12"),
-					},
-				},
-			},
-		},
-	}
-	for _, tc := range testCases {
-		t.Run(tc.desc, func(t *testing.T) {
-			_, gotErr := ec2Instance(tc.dio)
-			if gotErr == nil {
-				t.Errorf("ec2Instance(%v) did not fail", tc.dio)
-			}
-		})
-	}
-}
-
-func TestEC2InstanceIPs(t *testing.T) {
-	testCases := []struct {
-		desc      string
-		inst      *ec2.Instance
-		wantIntIP string
-		wantExtIP string
-	}{
-		{
-			desc: "base case",
-			inst: &ec2.Instance{
-				PrivateIpAddress: aws.String("1.1.1.1"),
-				PublicIpAddress:  aws.String("8.8.8.8"),
-			},
-			wantIntIP: "1.1.1.1",
-			wantExtIP: "8.8.8.8",
-		},
-	}
-	for _, tc := range testCases {
-		t.Run(tc.desc, func(t *testing.T) {
-			gotIntIP, gotExtIP, gotErr := ec2InstanceIPs(tc.inst)
-			if gotErr != nil {
-				t.Errorf("ec2InstanceIPs(%v) failed: %v",
-					tc.inst, gotErr)
-			}
-			if gotIntIP != tc.wantIntIP || gotExtIP != tc.wantExtIP {
-				t.Errorf("ec2InstanceIPs(%v) = %s, %s, %v; want %s, %s, nil",
-					tc.inst, gotIntIP, gotExtIP, gotErr, tc.wantIntIP, tc.wantExtIP)
-			}
-		})
-	}
-}
-
-func TestEC2InstanceIPsErrors(t *testing.T) {
-	testCases := []struct {
-		desc string
-		inst *ec2.Instance
-	}{
-		{
-			desc: "default vallues",
-			inst: &ec2.Instance{},
-		},
-		{
-			desc: "missing public ip",
-			inst: &ec2.Instance{
-				PrivateIpAddress: aws.String("1.1.1.1"),
-			},
-		},
-		{
-			desc: "missing private ip",
-			inst: &ec2.Instance{
-				PublicIpAddress: aws.String("8.8.8.8"),
-			},
-		},
-		{
-			desc: "empty public ip",
-			inst: &ec2.Instance{
-				PrivateIpAddress: aws.String("1.1.1.1"),
-				PublicIpAddress:  aws.String(""),
-			},
-		},
-		{
-			desc: "empty private ip",
-			inst: &ec2.Instance{
-				PrivateIpAddress: aws.String(""),
-				PublicIpAddress:  aws.String("8.8.8.8"),
-			},
-		},
-	}
-	for _, tc := range testCases {
-		t.Run(tc.desc, func(t *testing.T) {
-			_, _, gotErr := ec2InstanceIPs(tc.inst)
-			if gotErr == nil {
-				t.Errorf("ec2InstanceIPs(%v) = nil: want error", tc.inst)
-			}
-		})
-	}
-}
diff --git a/buildlet/buildlet.go b/buildlet/buildlet.go
index 2e6e911..9a58a90 100644
--- a/buildlet/buildlet.go
+++ b/buildlet/buildlet.go
@@ -13,7 +13,7 @@
 	"net/http"
 	"time"
 
-	"github.com/aws/aws-sdk-go/service/ec2"
+	"golang.org/x/build/internal/cloud"
 	"google.golang.org/api/compute/v1"
 )
 
@@ -61,7 +61,7 @@
 	// OnInstanceCreated optionally specifies a hook to run synchronously
 	// after the EC2 instance information is retrieved.
 	// Only valid for EC2 resources.
-	OnGotEC2InstanceInfo func(*ec2.Instance)
+	OnGotEC2InstanceInfo func(*cloud.Instance)
 
 	// OnBeginBuildletProbe optionally specifies a hook to run synchronously
 	// before StartNewVM tries to hit buildletURL to see if it's up yet.
diff --git a/buildlet/ec2.go b/buildlet/ec2.go
new file mode 100644
index 0000000..4a52b20
--- /dev/null
+++ b/buildlet/ec2.go
@@ -0,0 +1,159 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package buildlet
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net"
+	"time"
+
+	"golang.org/x/build/buildenv"
+	"golang.org/x/build/dashboard"
+	"golang.org/x/build/internal/cloud"
+)
+
+// awsClient represents the AWS specific calls made durring the
+// lifecycle of a buildlet. This is a partial implementation of the AWSClient found at
+// `golang.org/x/internal/cloud`.
+type awsClient interface {
+	Instance(ctx context.Context, instID string) (*cloud.Instance, error)
+	CreateInstance(ctx context.Context, config *cloud.EC2VMConfiguration) (*cloud.Instance, error)
+	WaitUntilInstanceRunning(ctx context.Context, instID string) error
+}
+
+// EC2Client is the client used to create buildlets on EC2.
+type EC2Client struct {
+	client awsClient
+}
+
+// NewEC2Client creates a new EC2Client.
+func NewEC2Client(client *cloud.AWSClient) *EC2Client {
+	return &EC2Client{
+		client: client,
+	}
+}
+
+// StartNewVM boots a new VM on EC2, waits until the client is accepting connections
+// on the configured port and returns a buildlet client configured communicate with it.
+func (c *EC2Client) StartNewVM(ctx context.Context, buildEnv *buildenv.Environment, hconf *dashboard.HostConfig, vmName, hostType string, opts *VMOpts) (*Client, error) {
+	// check required params
+	if opts == nil || opts.TLS.IsZero() {
+		return nil, errors.New("TLS keypair is not set")
+	}
+	if buildEnv == nil {
+		return nil, errors.New("invalid build enviornment")
+	}
+	if hconf == nil {
+		return nil, errors.New("invalid host configuration")
+	}
+	if vmName == "" || hostType == "" {
+		return nil, fmt.Errorf("invalid vmName: %q and hostType: %q", vmName, hostType)
+	}
+
+	// configure defaults
+	if opts.Description == "" {
+		opts.Description = fmt.Sprintf("Go Builder for %s", hostType)
+	}
+	if opts.Zone == "" {
+		opts.Zone = buildEnv.RandomEC2VMZone()
+	}
+	if opts.DeleteIn == 0 {
+		opts.DeleteIn = 30 * time.Minute
+	}
+
+	vmConfig := configureVM(buildEnv, hconf, vmName, hostType, opts)
+
+	vm, err := c.createVM(ctx, vmConfig, opts)
+	if err != nil {
+		return nil, err
+	}
+	if err = c.WaitUntilVMExists(ctx, vm.ID, opts); err != nil {
+		return nil, err
+	}
+	// once the VM is up and running then all of the configuration data is available
+	// when the API is querried for the VM.
+	vm, err = c.client.Instance(ctx, vm.ID)
+	if err != nil {
+		return nil, fmt.Errorf("unable to retrieve instance %q information: %w", vm.ID, err)
+	}
+	buildletURL, ipPort, err := ec2BuildletParams(vm, opts)
+	if err != nil {
+		return nil, err
+	}
+	return buildletClient(ctx, buildletURL, ipPort, opts)
+}
+
+// createVM submits a request for the creation of a VM.
+func (c *EC2Client) createVM(ctx context.Context, config *cloud.EC2VMConfiguration, opts *VMOpts) (*cloud.Instance, error) {
+	if config == nil || opts == nil {
+		return nil, errors.New("invalid parameter")
+	}
+	inst, err := c.client.CreateInstance(ctx, config)
+	if err != nil {
+		return nil, fmt.Errorf("unable to create instance: %w", err)
+	}
+	condRun(opts.OnInstanceRequested)
+	return inst, nil
+}
+
+// WaitUntilVMExists submits a request which waits until an instance exists before returning.
+func (c *EC2Client) WaitUntilVMExists(ctx context.Context, instID string, opts *VMOpts) error {
+	if err := c.client.WaitUntilInstanceRunning(ctx, instID); err != nil {
+		return fmt.Errorf("failed waiting for vm instance: %w", err)
+	}
+	condRun(opts.OnInstanceCreated)
+	return nil
+}
+
+// configureVM creates a configuration for an EC2 VM instance.
+func configureVM(buildEnv *buildenv.Environment, hconf *dashboard.HostConfig, vmName, hostType string, opts *VMOpts) *cloud.EC2VMConfiguration {
+	return &cloud.EC2VMConfiguration{
+		Description:    opts.Description,
+		ImageID:        hconf.VMImage,
+		Name:           vmName,
+		SSHKeyID:       "ec2-go-builders",
+		SecurityGroups: []string{buildEnv.AWSSecurityGroup},
+		Tags:           make(map[string]string),
+		Type:           hconf.MachineType(),
+		UserData:       vmUserDataSpec(buildEnv, hconf, vmName, hostType, opts),
+		Zone:           opts.Zone,
+	}
+}
+
+func vmUserDataSpec(buildEnv *buildenv.Environment, hconf *dashboard.HostConfig, vmName, hostType string, opts *VMOpts) string {
+	// add custom metadata to the user data.
+	ud := cloud.EC2UserData{
+		BuildletName:      vmName,
+		BuildletBinaryURL: hconf.BuildletBinaryURL(buildEnv),
+		BuildletHostType:  hostType,
+		BuildletImageURL:  hconf.ContainerVMImage(),
+		Metadata:          make(map[string]string),
+		TLSCert:           opts.TLS.CertPEM,
+		TLSKey:            opts.TLS.KeyPEM,
+		TLSPassword:       opts.TLS.Password(),
+	}
+	for k, v := range opts.Meta {
+		ud.Metadata[k] = v
+	}
+	return ud.EncodedString()
+}
+
+// ec2BuildletParams returns the necessary information to connect to an EC2 buildlet. A
+// buildlet URL and an IP address port are required to connect to a buildlet.
+func ec2BuildletParams(inst *cloud.Instance, opts *VMOpts) (string, string, error) {
+	if inst.IPAddressExternal == "" {
+		return "", "", errors.New("external IP address is not set")
+	}
+	extIP := inst.IPAddressExternal
+	buildletURL := fmt.Sprintf("https://%s", extIP)
+	ipPort := net.JoinHostPort(extIP, "443")
+
+	if opts.OnGotEC2InstanceInfo != nil {
+		opts.OnGotEC2InstanceInfo(inst)
+	}
+	return buildletURL, ipPort, nil
+}
diff --git a/buildlet/ec2_test.go b/buildlet/ec2_test.go
new file mode 100644
index 0000000..8fe1ae6
--- /dev/null
+++ b/buildlet/ec2_test.go
@@ -0,0 +1,451 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package buildlet
+
+import (
+	"context"
+	"encoding/base64"
+	"encoding/json"
+	"testing"
+	"time"
+
+	"golang.org/x/build/buildenv"
+	"golang.org/x/build/dashboard"
+	"golang.org/x/build/internal/cloud"
+)
+
+func TestStartNewVM(t *testing.T) {
+	kp, err := NewKeyPair()
+	if err != nil {
+		t.Fatalf("unable to generate key pair: %s", err)
+	}
+	buildEnv := &buildenv.Environment{}
+	hconf := &dashboard.HostConfig{
+		VMImage: "image-x",
+	}
+	vmName := "sample-vm"
+	hostType := "host-sample-os"
+	opts := &VMOpts{
+		Zone:        "us-west",
+		ProjectID:   "project1",
+		TLS:         kp,
+		Description: "Golang builder for sample",
+		Meta: map[string]string{
+			"Owner": "george",
+		},
+		DeleteIn:                 45 * time.Second,
+		SkipEndpointVerification: true,
+	}
+	c := &EC2Client{
+		client: cloud.NewFakeAWSClient(),
+	}
+	gotClient, gotErr := c.StartNewVM(context.Background(), buildEnv, hconf, vmName, hostType, opts)
+	if gotErr != nil {
+		t.Fatalf("error is not nil: %v", gotErr)
+	}
+	if gotClient == nil {
+		t.Fatalf("response is nil")
+	}
+}
+
+func TestStartNewVMError(t *testing.T) {
+	kp, err := NewKeyPair()
+	if err != nil {
+		t.Fatalf("unable to generate key pair: %s", err)
+	}
+
+	testCases := []struct {
+		desc     string
+		buildEnv *buildenv.Environment
+		hconf    *dashboard.HostConfig
+		vmName   string
+		hostType string
+		opts     *VMOpts
+	}{
+		{
+			desc:     "nil-buildenv",
+			hconf:    &dashboard.HostConfig{},
+			vmName:   "sample-vm",
+			hostType: "host-sample-os",
+			opts: &VMOpts{
+				Zone:        "us-west",
+				ProjectID:   "project1",
+				TLS:         kp,
+				Description: "Golang builder for sample",
+				Meta: map[string]string{
+					"Owner": "george",
+				},
+				DeleteIn: 45 * time.Second,
+			},
+		},
+		{
+			desc:     "nil-hconf",
+			buildEnv: &buildenv.Environment{},
+			vmName:   "sample-vm",
+			hostType: "host-sample-os",
+			opts: &VMOpts{
+				Zone:        "us-west",
+				ProjectID:   "project1",
+				TLS:         kp,
+				Description: "Golang builder for sample",
+				Meta: map[string]string{
+					"Owner": "george",
+				},
+				DeleteIn: 45 * time.Second,
+			},
+		},
+		{
+			desc:     "empty-vnName",
+			buildEnv: &buildenv.Environment{},
+			hconf:    &dashboard.HostConfig{},
+			vmName:   "",
+			hostType: "host-sample-os",
+			opts: &VMOpts{
+				Zone:        "us-west",
+				ProjectID:   "project1",
+				TLS:         kp,
+				Description: "Golang builder for sample",
+				Meta: map[string]string{
+					"Owner": "george",
+				},
+				DeleteIn: 45 * time.Second,
+			},
+		},
+		{
+			desc:     "empty-hostType",
+			buildEnv: &buildenv.Environment{},
+			hconf:    &dashboard.HostConfig{},
+			vmName:   "sample-vm",
+			hostType: "",
+			opts: &VMOpts{
+				Zone:        "us-west",
+				ProjectID:   "project1",
+				TLS:         kp,
+				Description: "Golang builder for sample",
+				Meta: map[string]string{
+					"Owner": "george",
+				},
+				DeleteIn: 45 * time.Second,
+			},
+		},
+		{
+			desc:     "missing-certs",
+			buildEnv: &buildenv.Environment{},
+			hconf:    &dashboard.HostConfig{},
+			vmName:   "sample-vm",
+			hostType: "host-sample-os",
+			opts: &VMOpts{
+				Zone:        "us-west",
+				ProjectID:   "project1",
+				Description: "Golang builder for sample",
+				Meta: map[string]string{
+					"Owner": "george",
+				},
+				DeleteIn: 45 * time.Second,
+			},
+		},
+		{
+			desc:     "nil-opts",
+			buildEnv: &buildenv.Environment{},
+			hconf:    &dashboard.HostConfig{},
+			vmName:   "sample-vm",
+			hostType: "host-sample-os",
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) {
+			c := &EC2Client{
+				client: cloud.NewFakeAWSClient(),
+			}
+			gotClient, gotErr := c.StartNewVM(context.Background(), tc.buildEnv, tc.hconf, tc.vmName, tc.hostType, tc.opts)
+			if gotErr == nil {
+				t.Errorf("StartNewVM(ctx, %+v, %+v, %s, %s, %+v) = %+v, nil; want error", tc.buildEnv, tc.hconf, tc.vmName, tc.hostType, tc.opts, gotClient)
+			}
+			if gotClient != nil {
+				t.Errorf("got %+v; expected nil", gotClient)
+			}
+		})
+	}
+}
+
+func TestWaitUntilInstanceExists(t *testing.T) {
+	vmConfig := &cloud.EC2VMConfiguration{
+		ImageID: "foo",
+		Type:    "type-a",
+		Zone:    "eu-15",
+	}
+	invoked := false
+	opts := &VMOpts{
+		OnInstanceCreated: func() {
+			invoked = true
+		},
+	}
+	ctx := context.Background()
+	c := &EC2Client{
+		client: cloud.NewFakeAWSClient(),
+	}
+	gotVM, gotErr := c.createVM(ctx, vmConfig, opts)
+	if gotErr != nil {
+		t.Fatalf("createVM(ctx, %v, %v) failed with %s", vmConfig, opts, gotErr)
+	}
+	gotErr = c.WaitUntilVMExists(ctx, gotVM.ID, opts)
+	if gotErr != nil {
+		t.Fatalf("WaitUntilVMExists(%v, %v, %v) failed with error %s", ctx, gotVM.ID, opts, gotErr)
+	}
+	if !invoked {
+		t.Errorf("OnInstanceCreated() was not invoked")
+	}
+}
+
+func TestCreateVM(t *testing.T) {
+	vmConfig := &cloud.EC2VMConfiguration{
+		ImageID: "foo",
+		Type:    "type-a",
+		Zone:    "eu-15",
+	}
+	invoked := false
+	opts := &VMOpts{
+		OnInstanceRequested: func() {
+			invoked = true
+		},
+	}
+	c := &EC2Client{
+		client: cloud.NewFakeAWSClient(),
+	}
+	gotVM, gotErr := c.createVM(context.Background(), vmConfig, opts)
+	if gotErr != nil {
+		t.Fatalf("createVM(ctx, %v, %v) failed with %s", vmConfig, opts, gotErr)
+	}
+	if gotVM.ImageID != vmConfig.ImageID || gotVM.Type != vmConfig.Type || gotVM.Zone != vmConfig.Zone {
+		t.Errorf("createVM(ctx, %+v, %+v) = %+v, nil; want vm to match config", vmConfig, opts, gotVM)
+	}
+	if !invoked {
+		t.Errorf("OnInstanceRequested() was not invoked")
+	}
+}
+
+func TestCreateVMError(t *testing.T) {
+	testCases := []struct {
+		desc     string
+		vmConfig *cloud.EC2VMConfiguration
+		opts     *VMOpts
+	}{
+		{
+			desc: "missing-vmConfig",
+		},
+		{
+			desc: "missing-image-id",
+			vmConfig: &cloud.EC2VMConfiguration{
+				Type: "type-a",
+				Zone: "eu-15",
+			},
+			opts: &VMOpts{
+				OnInstanceRequested: func() {},
+			},
+		},
+		{
+			desc: "missing-instance-id",
+			vmConfig: &cloud.EC2VMConfiguration{
+				ImageID: "foo",
+				Zone:    "eu-15",
+			},
+			opts: &VMOpts{
+				OnInstanceRequested: func() {},
+			},
+		},
+		{
+			desc: "missing-placement",
+			vmConfig: &cloud.EC2VMConfiguration{
+				Name: "foo",
+				Type: "type-a",
+			},
+			opts: &VMOpts{
+				OnInstanceRequested: func() {},
+			},
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) {
+			c := &EC2Client{
+				client: cloud.NewFakeAWSClient(),
+				//client: &fakeAWSClient{},
+			}
+			gotVM, gotErr := c.createVM(context.Background(), tc.vmConfig, tc.opts)
+			if gotErr == nil {
+				t.Errorf("createVM(ctx, %v, %v) = %s, %v; want error", tc.vmConfig, tc.opts, gotVM.ID, gotErr)
+			}
+			if gotVM != nil {
+				t.Errorf("createVM(ctx, %v, %v) = %s, %v; %q, error", tc.vmConfig, tc.opts, gotVM.ID, gotErr, "")
+			}
+		})
+	}
+}
+
+func TestEC2BuildletParams(t *testing.T) {
+	testCases := []struct {
+		desc       string
+		inst       *cloud.Instance
+		opts       *VMOpts
+		wantURL    string
+		wantPort   string
+		wantCalled bool
+		wantErr    bool
+	}{
+		{
+			desc: "base-case",
+			inst: &cloud.Instance{
+				IPAddressExternal: "8.8.8.8",
+				IPAddressInternal: "3.3.3.3",
+			},
+			opts:       &VMOpts{},
+			wantCalled: true,
+			wantURL:    "https://8.8.8.8",
+			wantPort:   "8.8.8.8:443",
+			wantErr:    false,
+		},
+		{
+			desc: "missing-int-ip",
+			inst: &cloud.Instance{
+				IPAddressExternal: "8.8.8.8",
+			},
+			opts:       &VMOpts{},
+			wantCalled: true,
+			wantURL:    "https://8.8.8.8",
+			wantPort:   "8.8.8.8:443",
+			wantErr:    false,
+		},
+		{
+			desc: "missing-ext-ip",
+			inst: &cloud.Instance{
+				IPAddressInternal: "3.3.3.3",
+			},
+			opts:       &VMOpts{},
+			wantCalled: true,
+			wantURL:    "",
+			wantPort:   "",
+			wantErr:    true,
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) {
+			gotURL, gotPort, gotErr := ec2BuildletParams(tc.inst, tc.opts)
+			if gotURL != tc.wantURL || gotPort != tc.wantPort || tc.wantErr != (gotErr != nil) {
+				t.Errorf("ec2BuildletParams(%v, %v) = %q, %q, nil; want %q, %q, nil", tc.inst, tc.opts, gotURL, gotPort, tc.wantURL, tc.wantPort)
+			}
+		})
+	}
+}
+
+func TestConfigureVM(t *testing.T) {
+	testCases := []struct {
+		desc              string
+		buildEnv          *buildenv.Environment
+		hconf             *dashboard.HostConfig
+		hostType          string
+		opts              *VMOpts
+		vmName            string
+		wantDesc          string
+		wantImageID       string
+		wantInstanceType  string
+		wantName          string
+		wantZone          string
+		wantBuildletName  string
+		wantBuildletImage string
+	}{
+		{
+			desc:     "default-values",
+			buildEnv: &buildenv.Environment{},
+			hconf: &dashboard.HostConfig{
+				KonletVMImage: "gcr.io/symbolic-datum-552/gobuilder-arm64-aws",
+			},
+			vmName:            "base_vm",
+			hostType:          "host-foo-bar",
+			opts:              &VMOpts{},
+			wantInstanceType:  "n1-highcpu-2",
+			wantName:          "base_vm",
+			wantBuildletName:  "base_vm",
+			wantBuildletImage: "gcr.io/symbolic-datum-552/gobuilder-arm64-aws",
+		},
+		{
+			desc:     "full-configuration",
+			buildEnv: &buildenv.Environment{},
+			hconf: &dashboard.HostConfig{
+				VMImage:       "awesome_image",
+				KonletVMImage: "gcr.io/symbolic-datum-552/gobuilder-arm64-aws",
+			},
+			vmName:   "base-vm",
+			hostType: "host-foo-bar",
+			opts: &VMOpts{
+				Zone: "sa-west",
+				TLS: KeyPair{
+					CertPEM: "abc",
+					KeyPEM:  "xyz",
+				},
+				Description: "test description",
+				Meta: map[string]string{
+					"sample": "value",
+				},
+			},
+			wantDesc:          "test description",
+			wantImageID:       "awesome_image",
+			wantInstanceType:  "n1-highcpu-2",
+			wantName:          "base-vm",
+			wantZone:          "sa-west",
+			wantBuildletName:  "base-vm",
+			wantBuildletImage: "gcr.io/symbolic-datum-552/gobuilder-arm64-aws",
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) {
+			got := configureVM(tc.buildEnv, tc.hconf, tc.vmName, tc.hostType, tc.opts)
+			if got.ImageID != tc.wantImageID {
+				t.Errorf("ImageId got %s; want %s", got.ImageID, tc.wantImageID)
+			}
+			if got.Type != tc.wantInstanceType {
+				t.Errorf("Type got %s; want %s", got.Type, tc.wantInstanceType)
+			}
+			if got.Zone != tc.wantZone {
+				t.Errorf("Zone got %s; want %s", got.Zone, tc.wantZone)
+			}
+			if got.Name != tc.wantName {
+				t.Errorf("Name got %s; want %s", got.Name, tc.wantName)
+			}
+			if got.Description != tc.wantDesc {
+				t.Errorf("Description got %s; want %s", got.Description, tc.wantDesc)
+			}
+			gotUDJson, err := base64.StdEncoding.DecodeString(got.UserData)
+			if err != nil {
+				t.Fatalf("unable to base64 decode string %q: %s", got.UserData, err)
+			}
+			gotUD := &cloud.EC2UserData{}
+			err = json.Unmarshal([]byte(gotUDJson), gotUD)
+			if err != nil {
+				t.Errorf("unable to unmarshal user data: %v", err)
+			}
+			if gotUD.BuildletBinaryURL != tc.hconf.BuildletBinaryURL(tc.buildEnv) {
+				t.Errorf("buildletBinaryURL got %s; want %s", gotUD.BuildletBinaryURL, tc.hconf.BuildletBinaryURL(tc.buildEnv))
+			}
+			if gotUD.BuildletHostType != tc.hostType {
+				t.Errorf("buildletHostType got %s; want %s", gotUD.BuildletHostType, tc.hostType)
+			}
+			if gotUD.BuildletName != tc.wantBuildletName {
+				t.Errorf("buildletName got %s; want %s", gotUD.BuildletName, tc.wantBuildletName)
+			}
+			if gotUD.BuildletImageURL != tc.wantBuildletImage {
+				t.Errorf("buildletImageURL got %s; want %s", gotUD.BuildletImageURL, tc.wantBuildletImage)
+			}
+
+			if gotUD.TLSCert != tc.opts.TLS.CertPEM {
+				t.Errorf("TLSCert got %s; want %s", gotUD.TLSCert, tc.opts.TLS.CertPEM)
+			}
+			if gotUD.TLSKey != tc.opts.TLS.KeyPEM {
+				t.Errorf("TLSKey got %s; want %s", gotUD.TLSKey, tc.opts.TLS.KeyPEM)
+			}
+			if gotUD.TLSPassword != tc.opts.TLS.Password() {
+				t.Errorf("TLSPassword got %s; want %s", gotUD.TLSPassword, tc.opts.TLS.Password())
+			}
+		})
+	}
+}
diff --git a/cmd/buildlet/buildlet.go b/cmd/buildlet/buildlet.go
index a48f884..2aa8c79 100644
--- a/cmd/buildlet/buildlet.go
+++ b/cmd/buildlet/buildlet.go
@@ -44,6 +44,7 @@
 	"github.com/aws/aws-sdk-go/aws/ec2metadata"
 	"github.com/aws/aws-sdk-go/aws/session"
 	"golang.org/x/build/buildlet"
+	"golang.org/x/build/internal/cloud"
 	"golang.org/x/build/pargzip"
 )
 
@@ -321,7 +322,7 @@
 
 var (
 	// ec2UD contains a copy of the EC2 vm user data retrieved from the metadata.
-	ec2UD *buildlet.EC2UserData
+	ec2UD *cloud.EC2UserData
 	// ec2MdC is an EC2 metadata client.
 	ec2MdC *ec2metadata.EC2Metadata
 )
@@ -342,7 +343,7 @@
 
 // mdValueFromUserData maps a metadata key value into the corresponding
 // EC2UserData value. If a mapping is not found, an empty string is returned.
-func mdValueFromUserData(ud *buildlet.EC2UserData, key string) string {
+func mdValueFromUserData(ud *cloud.EC2UserData, key string) string {
 	switch key {
 	case metaKeyTLSCert:
 		return ud.TLSCert
@@ -385,7 +386,7 @@
 		if err != nil {
 			log.Fatalf("unable to retrieve EC2 user data: %v", err)
 		}
-		ec2UD = &buildlet.EC2UserData{}
+		ec2UD = &cloud.EC2UserData{}
 		err = json.Unmarshal([]byte(ec2MetaJson), ec2UD)
 		if err != nil {
 			log.Fatalf("unable to unmarshal user data json: %v", err)
diff --git a/cmd/rundockerbuildlet/rundockerbuildlet.go b/cmd/rundockerbuildlet/rundockerbuildlet.go
index 59805e3..1ad6c00 100644
--- a/cmd/rundockerbuildlet/rundockerbuildlet.go
+++ b/cmd/rundockerbuildlet/rundockerbuildlet.go
@@ -27,7 +27,7 @@
 	"github.com/aws/aws-sdk-go/aws/ec2metadata"
 	"github.com/aws/aws-sdk-go/aws/session"
 	"golang.org/x/build/buildenv"
-	"golang.org/x/build/buildlet"
+	"golang.org/x/build/internal/cloud"
 )
 
 var (
@@ -47,7 +47,7 @@
 	isReverse    = true
 	isSingleRun  = false
 	// ec2UD contains a copy of the EC2 vm user data retrieved from the metadata.
-	ec2UD *buildlet.EC2UserData
+	ec2UD *cloud.EC2UserData
 	// ec2MetaClient is an EC2 metadata client.
 	ec2MetaClient *ec2metadata.EC2Metadata
 )
@@ -301,7 +301,7 @@
 	if err != nil {
 		log.Fatalf("unable to retrieve EC2 user data: %v", err)
 	}
-	ec2UD = &buildlet.EC2UserData{}
+	ec2UD = &cloud.EC2UserData{}
 	err = json.Unmarshal([]byte(ec2MetaJson), ec2UD)
 	if err != nil {
 		log.Fatalf("unable to unmarshal user data json: %v", err)