Package: pulumi-command
Pulumi Command Provider (preview)The Pulumi Command Provider enables you to execute commands and scripts either locally or remotely as part of the Pulumi resource model.  Resources in the command package support running scripts oncreateanddestroyoperations, supporting stateful local and remote command execution.There are many scenarios where the Command package can be useful:Running a command locally after creating a resource, to register it with an external serviceRunning a command locally before deleting a resource, to deregister it with an external serviceRunning a command remotely on a remote host immediately after creating itCopying a file to a remote host after creating it (potentially as a script to be executed afterwards)As a simple alternative to some use cases for Dynamic Providers (especially in languages which do not yet support Dynamic Providers).Some users may have experience with Terraform "provisioners", and the Command package offers support for similar scenarios.  However, the Command package is provided as independent resources which can be combined with other resources in many interesting ways. This has many strengths, but also some differences, such as the fact that a Command resource failing does not cause a resource it is operating on to fail.You can use the Command package from a Pulumi program written in any Pulumi language: C#, Go, JavaScript/TypeScript, and Python.
You'll need toinstall and configure the Pulumi CLIif you haven't already.NOTE: The Command package is in preview.  The API design may change ahead of general availability based onuser feedback.ExamplesA simple local resource (random)The simplest use case forlocal.Commandis to just run a command oncreate, which can return some value which will be stored in the state file, and will be persistent for the life of the stack (or until the resource is destroyed or replaced).  The example below uses this as an alternative to therandompackage to create some randomness which is stored in Pulumi state.import{local}from"@pulumi/command";constrandom=newlocal.Command("random",{create:"openssl rand -hex 16",});exportconstoutput=random.stdout;packagemainimport("github.com/pulumi/pulumi-command/sdk/go/command/local""github.com/pulumi/pulumi/sdk/v3/go/pulumi")funcmain(){pulumi.Run(func(ctx*pulumi.Context)error{random,err:=local.NewCommand(ctx,"my-bucket",&local.CommandInput{Create:pulumi.String("openssl rand -hex 16"),})iferr!=nil{returnerr}ctx.Export("output",random.Stdout)returnnil})}Remote provisioning of an EC2 instanceThis example creates and EC2 instance, and then usesremote.Commandandremote.CopyFileto run commands and copy files to the remote instance (via SSH). Similar things are possible with Azure, Google Cloud and other cloud provider virtual machines.  Support for Windows-based VMs is being trackedhere.Note that implicit and explicit (dependsOn) dependencies can be used to control the order that theseCommandandCopyFileresources are constructed relative to each other and to the cloud resources they depend on.  This ensures that thecreateoperations run after all dependencies are created, and thedeleteoperations run before all dependencies are deleted.Because theCommandandCopyFileresources replace on changes to their connection, if the EC2 instance is replaced, the commands will all re-run on the new instance (and thedeleteoperations will run on the old instance).Note also thatdeleteBeforeReplacecan be composed withCommandresources to ensure that thedeleteoperation on an "old" instance is run before thecreateoperation of the new instance, in case a scarce resource is managed by the command.  Similarly, other resource options can naturally be applied toCommandresources, likeignoreChanges.import{interpolate,Config}from"@pulumi/pulumi";import{local,remote,types}from"@pulumi/command";import*asawsfrom"@pulumi/aws";import*asfsfrom"fs";import*asosfrom"os";import*aspathfrom"path";import{size}from"./size";constconfig=newConfig();constkeyName=config.get("keyName")??newaws.ec2.KeyPair("key",{publicKey:config.require("publicKey")}).keyName;constprivateKeyBase64=config.get("privateKeyBase64");constprivateKey=privateKeyBase64?Buffer.from(privateKeyBase64,'base64').toString('ascii'):fs.readFileSync(path.join(os.homedir(),".ssh","id_rsa")).toString("utf8");constsecgrp=newaws.ec2.SecurityGroup("secgrp",{description:"Foo",ingress:[{protocol:"tcp",fromPort:22,toPort:22,cidrBlocks:["0.0.0.0/0"]},{protocol:"tcp",fromPort:80,toPort:80,cidrBlocks:["0.0.0.0/0"]},],});constami=aws.ec2.getAmiOutput({owners:["amazon"],mostRecent:true,filters:[{name:"name",values:["amzn2-ami-hvm-2.0.????????-x86_64-gp2"],}],});constserver=newaws.ec2.Instance("server",{instanceType:size,ami:ami.id,keyName:keyName,vpcSecurityGroupIds:[secgrp.id],},{replaceOnChanges:["instanceType"]});// Now set up a connection to the instance and run some provisioning operations on the instance.constconnection:types.input.remote.ConnectionInput={host:server.publicIp,user:"ec2-user",privateKey:privateKey,};consthostname=newremote.Command("hostname",{connection,create:"hostname",});newremote.Command("remotePrivateIP",{connection,create:interpolate`echo${server.privateIp}> private_ip.txt`,delete:`rm private_ip.txt`,},{deleteBeforeReplace:true});newlocal.Command("localPrivateIP",{create:interpolate`echo${server.privateIp}> private_ip.txt`,delete:`rm private_ip.txt`,},{deleteBeforeReplace:true});constsizeFile=newremote.CopyFile("size",{connection,localPath:"./size.ts",remotePath:"size.ts",})constcatSize=newremote.Command("checkSize",{connection,create:"cat size.ts",},{dependsOn:sizeFile})exportconstconfirmSize=catSize.stdout;exportconstpublicIp=server.publicIp;exportconstpublicHostName=server.publicDns;exportconsthostnameStdout=hostname.stdout;Invoking a Lambda during Pulumi deploymentThere may be cases where it is useful to run some code within an AWS Lambda or other serverless function during the deployment.  For example, this may allow running some code from within a VPC, or with a specific role, without needing to have persistent compute available (such as the EC2 example above).Note that the Lambda function itself can be created within the same Pulumi program, and then invoked after creation.The example below simply creates some random value within the Lambda, which is a very roundabout way of doing the same thing as the first "random" example above, but this pattern can be used for more complex scenarios where the Lambda does things a local script could not.import{local}from"@pulumi/command";import*asawsfrom"@pulumi/aws";import*ascryptofrom"crypto";constf=newaws.lambda.CallbackFunction("f",{publish:true,callback:async(ev:any)=>{returncrypto.randomBytes(ev.len/2).toString('hex');}});constrand=newlocal.Command("execf",{create:`aws lambda invoke --function-name "$FN" --payload '{"len": 10}' --cli-binary-format raw-in-base64-out out.txt >/dev/null && cat out.txt | tr -d '"'  && rm out.txt`,environment:{FN:f.qualifiedArn,AWS_REGION:aws.config.region!,AWS_PAGER:"",},})exportconstoutput=rand.stdout;Usinglocal.Commandwith CURL to manage external REST APIThis example useslocal.Commandto create a simple resource provider for managing GitHub labels, by invokingcurlcommands oncreateanddeletecommands against the GitHub REST API.  A similar approach could be applied to build other simple providers against any REST API directly from within Pulumi programs in any language.  This approach is somewhat limited by the fact thatlocal.Commanddoes not yet supportdiff/update/read.  Support for those may beadded in the future.This example also shows howlocal.Commandcan be used as an implementation detail inside a nicer abstraction, like theGitHubLabelcomponent defined below.import*aspulumifrom"@pulumi/pulumi";import*asrandomfrom"@pulumi/random";import{local}from"@pulumi/command";interfaceLabelArgs{owner:pulumi.Input<string>;repo:pulumi.Input<string>;name:pulumi.Input<string>;githubToken:pulumi.Input<string>;}classGitHubLabelextendspulumi.ComponentResource{publicurl:pulumi.Output<string>;constructor(name:string,args:LabelArgs,opts?:pulumi.ComponentResourceOptions){super("example:github:Label",name,args,opts);constlabel=newlocal.Command("label",{create:"./create_label.sh",delete:"./delete_label.sh",environment:{OWNER:args.owner,REPO:args.repo,NAME:args.name,GITHUB_TOKEN:args.githubToken,}},{parent:this});constresponse=label.stdout.apply(JSON.parse);this.url=response.apply((x:any)=>x.urlasstring);}}constconfig=newpulumi.Config();constrand=newrandom.RandomString("s",{length:10,special:false});constlabel=newGitHubLabel("l",{owner:"pulumi",repo:"pulumi-command",name:rand.result,githubToken:config.requireSecret("githubToken"),});exportconstlabelUrl=label.url;# create_label.shcurl\-s\-XPOST\-H"authorization: Bearer$GITHUB_TOKEN"\-H"Accept: application/vnd.github.v3+json"\https://api.github.com/repos/$OWNER/$REPO/labels\-d"{\"name\":\"$NAME\"}"# delete_label.shcurl\-s\-XDELETE\-H"authorization: Bearer$GITHUB_TOKEN"\-H"Accept: application/vnd.github.v3+json"\https://api.github.com/repos/$OWNER/$REPO/labels/$NAMEGraceful cleanup of workloads in a Kubernetes clusterThere are cases where it's important to run some cleanup operation before destroying a resource such as when destroying the resource does not properly handle orderly cleanup.  For example, destroying an EKS Cluster will not ensure that all Kubernetes object finalizers are run, which may lead to leaking external resources managed by those Kubernetes resources.  This example shows how we can use adelete-onlyCommandto ensure some cleanup is run within a cluster before destroying it.import{local}from"@pulumi/command";import*aseksfrom"@pulumi/eks";import*asrandomfrom"@pulumi/random";import{interpolate}from"@pulumi/pulumi";constcluster=neweks.Cluster("cluster",{});// We could also use `RemoteCommand` to run this from within a node in the clusterconstcleanupKubernetesNamespaces=newlocal.Command("cleanupKubernetesNamespaces",{// This will run before the cluster is destroyed.  Everything else will need to// depend on this resource to ensure this cleanup doesn't happen too early.delete:"kubectl delete --all namespaces",environment:{KUBECONFIG:cluster.kubeconfig,},});Working with Assets and PathsWhen a local command creates assets as part of its execution, these can be captured by specifyingassetPathsorarchivePaths.constlambdaBuild=local.runOutput({dir:"../my-function",command:`yarn && yarn build`,archivePaths:["dist/**"],});newaws.lambda.Function("my-function",{code:lambdaBuild.archive,// ...});When using theassetPathsandarchivePaths, they take a list of 'globs'.We only include files not directories for assets and archives.Path separators are/on all platforms - including Windows.Patterns starting with!are 'exclude' rules.Rules are evaluated in order, so exclude rules should be after inclusion rules.*matches anything except/**matches anything,including/All returned paths are relative to the working directory (without leading./) e.g.file.textorsubfolder/file.txt.For full details of the globbing syntax, seegithub.com/gobwas/globAsset Paths ExampleGiven the rules:-"assets/**"-"src/**.js"-"!**secret.*"When evaluating against this folder:-assets/- logos/- logo.svg-src/- index.js- secret.jsThe following paths will be returned:-assets/logos/logo.svg-src/index.jsBuildingDependenciesGo 1.17NodeJS 10.X.X or laterPython 3.6 or later.NET Core 3.1Please refer toContributing to Pulumifor installation
guidance.Building locallyRun the following commands to install Go modules, generate all SDKs, and build the provider:$ make ensure
$ make build
$ make installAdd thebinfolder to your$PATHor copy thebin/pulumi-resource-aws-nativefile to another location in your$PATH.Running an exampleNavigate to the simple example and run Pulumi:$ cd examples/simple
$ yarn link @pulumi/command
$ yarn install
$ pulumi up