├── .github └── workflows │ └── publish.yml ├── bootstrap └── bootstrap ├── build.sh ├── cloudenv └── cloudenv.go ├── docs ├── README.md └── execution-result.png ├── example.yml ├── example ├── example.go ├── go.mod └── go.sum ├── go.mod ├── go.sum └── macro ├── macro.py └── macro.yml /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - uses: actions/setup-go@v4 15 | with: 16 | go-version: "1.20" 17 | 18 | - uses: ko-build/setup-ko@v0.6 19 | 20 | - name: build and push 21 | run: > 22 | ko build 23 | --bare 24 | --platform=linux/amd64,linux/arm64 25 | --sbom none 26 | --tags latest 27 | --tags ${GITHUB_RUN_NUMBER}.${GITHUB_RUN_ATTEMPT} 28 | ./cloudenv 29 | -------------------------------------------------------------------------------- /bootstrap/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec /opt/cloudenv "$_HANDLER" 3 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eux 3 | 4 | export GOOS=linux 5 | export CGO_ENABLED=0 6 | 7 | mkdir -p built/arm64 8 | GOARCH=arm64 go build -ldflags="-s -w -buildid=" -o built/arm64/cloudenv ./cloudenv 9 | 10 | mkdir -p built/x86_64 11 | GOARCH=amd64 go build -ldflags="-s -w -buildid=" -o built/x86_64/cloudenv ./cloudenv 12 | 13 | cd example 14 | GOARCH=arm64 go build -ldflags="-s -w -buildid=" 15 | -------------------------------------------------------------------------------- /cloudenv/cloudenv.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/aws/aws-sdk-go-v2/aws" 7 | "github.com/aws/aws-sdk-go-v2/config" 8 | "github.com/aws/aws-sdk-go-v2/service/secretsmanager" 9 | "github.com/aws/aws-sdk-go-v2/service/ssm" 10 | "golang.org/x/sync/errgroup" 11 | "os" 12 | "strings" 13 | "syscall" 14 | ) 15 | 16 | const prefixSsm = "{aws-ssm}" 17 | const prefixSm = "{aws-sm}" 18 | 19 | func main() { 20 | ctx := context.Background() 21 | 22 | cfg, err := config.LoadDefaultConfig(ctx) 23 | if err != nil { 24 | panic(fmt.Sprintf("%+v", err)) 25 | } 26 | 27 | // extract set of parameter names and secret names to fetch 28 | envmap := map[string]string{} 29 | params := map[string]string{} 30 | secrets := map[string]string{} 31 | 32 | for _, kv := range os.Environ() { 33 | name, value, _ := strings.Cut(kv, "=") 34 | envmap[name] = value 35 | 36 | if strings.HasPrefix(value, prefixSsm) { 37 | param := strings.TrimPrefix(value, prefixSsm) 38 | params[param] = "" 39 | } else if strings.HasPrefix(value, prefixSm) { 40 | secret := strings.TrimPrefix(value, prefixSm) 41 | secrets[secret] = "" 42 | } 43 | } 44 | 45 | // populate map with values from parameter store 46 | err = populateParameters(ctx, ssm.NewFromConfig(cfg), params) 47 | if err != nil { 48 | panic(fmt.Sprintf("%+v", err)) 49 | } 50 | 51 | // populate map with values from secrets manager 52 | err = populateSecrets(ctx, secretsmanager.NewFromConfig(cfg), secrets, 5) 53 | if err != nil { 54 | panic(fmt.Sprintf("%+v", err)) 55 | } 56 | 57 | // turn env map back into a slice for exec syscall 58 | envslice := make([]string, len(envmap)) 59 | for name, value := range envmap { 60 | if strings.HasPrefix(value, prefixSsm) { 61 | param := strings.TrimPrefix(value, prefixSsm) 62 | value = params[param] 63 | } else if strings.HasPrefix(value, prefixSm) { 64 | secret := strings.TrimPrefix(value, prefixSm) 65 | value = secrets[secret] 66 | } 67 | 68 | envslice = append(envslice, fmt.Sprintf("%s=%s", name, value)) 69 | } 70 | 71 | // now pass control to the program proper 72 | err = syscall.Exec(os.Args[1], os.Args[1:], envslice) 73 | if err != nil { 74 | panic(fmt.Sprintf("%+v", err)) 75 | } 76 | } 77 | 78 | func populateParameters(ctx context.Context, api *ssm.Client, params map[string]string) error { 79 | arnChunks := chunk[string](keys(params), 10) 80 | for _, arns := range arnChunks { 81 | names := make([]string, len(arns)) 82 | for i := range arns { 83 | if strings.HasPrefix(arns[i], "arn:") { 84 | // arn:aws:ssm:us-east-1:514202201242:parameter/arn 85 | split := strings.SplitN(arns[i], ":", 6) 86 | names[i] = strings.TrimPrefix(split[5], "parameter") 87 | } else { 88 | names[i] = arns[i] 89 | } 90 | } 91 | 92 | get, err := api.GetParameters(ctx, &ssm.GetParametersInput{ 93 | Names: names, 94 | WithDecryption: aws.Bool(true), 95 | }) 96 | if err != nil { 97 | return fmt.Errorf("getting parameters: %w", err) 98 | } 99 | 100 | if len(get.InvalidParameters) > 0 { 101 | return fmt.Errorf("invalid parameters: %s", strings.Join(get.InvalidParameters, ", ")) 102 | } 103 | 104 | for _, parameter := range get.Parameters { 105 | params[*parameter.ARN] = *parameter.Value 106 | } 107 | } 108 | 109 | return nil 110 | } 111 | 112 | func populateSecrets(ctx context.Context, api *secretsmanager.Client, secrets map[string]string, concurrency int) error { 113 | inch := make(chan string, len(secrets)) 114 | for secret := range secrets { 115 | inch <- secret 116 | } 117 | close(inch) 118 | 119 | outch := make(chan map[string]string, len(secrets)) 120 | 121 | g, ctx := errgroup.WithContext(ctx) 122 | for idx := 0; idx < concurrency; idx++ { 123 | g.Go(func() error { 124 | for secret := range inch { 125 | gsv, err := api.GetSecretValue(ctx, &secretsmanager.GetSecretValueInput{ 126 | SecretId: &secret, 127 | }) 128 | if err != nil { 129 | return fmt.Errorf(": %w", err) 130 | } 131 | 132 | if gsv.SecretString != nil { 133 | outch <- map[string]string{secret: *gsv.SecretString} 134 | } else if gsv.SecretBinary != nil { 135 | outch <- map[string]string{secret: string(gsv.SecretBinary)} 136 | } 137 | } 138 | 139 | return nil 140 | }) 141 | } 142 | 143 | err := g.Wait() 144 | if err != nil { 145 | return fmt.Errorf("getting secrets: %w", err) 146 | } 147 | 148 | for idx := 0; idx < len(secrets); idx++ { 149 | out := <-outch 150 | for k, v := range out { 151 | secrets[k] = v 152 | } 153 | } 154 | 155 | close(outch) 156 | return nil 157 | } 158 | 159 | func keys[K comparable, V any](m map[K]V) []K { 160 | slice := make([]K, len(m)) 161 | 162 | idx := 0 163 | for t := range m { 164 | slice[idx] = t 165 | idx++ 166 | } 167 | 168 | return slice 169 | } 170 | 171 | func chunk[T any](slice []T, chunkSize int) [][]T { 172 | var chunks [][]T 173 | 174 | for i := 0; i < len(slice); i += chunkSize { 175 | end := i + chunkSize 176 | 177 | // necessary check to avoid slicing beyond 178 | // slice capacity 179 | if end > len(slice) { 180 | end = len(slice) 181 | } 182 | 183 | chunks = append(chunks, slice[i:end]) 184 | } 185 | 186 | return chunks 187 | } 188 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # `cloudenv` 2 | 3 | See also: [my blog post][my-blog]. 4 | 5 | I wish that AWS Lambda functions could be configured to use secrets stored in 6 | AWS Parameter Store and AWS Secrets Manager in the same way that AWS ECS task 7 | definitions can be. Specifically, I wish I could do this: 8 | 9 | ```yaml 10 | Transform: 11 | - AWS::LanguageExtensions 12 | - cloudenv 13 | - AWS::Serverless-2016-10-31 14 | 15 | Resources: 16 | Example: 17 | Type: AWS::Serverless::Function 18 | Properties: 19 | Architectures: [arm64] 20 | Runtime: python3.9 21 | Handler: index.handler 22 | Environment: 23 | Variables: 24 | HELLO: WORLD 25 | Secrets: 26 | MY_SECRET: !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/my/parameter/name 27 | MY_SECOND_SECRET: !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/my/parameter/name 28 | THIRD_SECRET: !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/my/parameter/third 29 | REAL_SECRET: !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:mysecret-rlBksU 30 | InlineCode: | 31 | import os 32 | 33 | def handler(event, context): 34 | return { 35 | 'HELLO' : os.environ['HELLO'], 36 | 'MY_SECRET' : os.environ['MY_SECRET'], 37 | 'MY_SECOND_SECRET': os.environ['MY_SECOND_SECRET'], 38 | 'THIRD_SECRET' : os.environ['THIRD_SECRET'], 39 | 'REAL_SECRET' : os.environ['REAL_SECRET'], 40 | } 41 | ``` 42 | 43 | The code in this repo achieves the above. That's all you need to know if you want 44 | to _use_ it. 45 | 46 | ![example](/docs/execution-result.png) 47 | 48 | ## Supported runtimes 49 | 50 | * dotnet6 51 | * dotnetcore2.1 52 | * dotnetcore3.1 53 | * java11 54 | * java8.al2 55 | * nodejs10.x 56 | * nodejs12.x 57 | * nodejs14.x 58 | * nodejs16.x 59 | * python3.8 60 | * python3.9 61 | * ruby2.7 62 | * provided (see below) 63 | * provided.al2 (see below) 64 | 65 | ## How it's built 66 | 67 | If you want to know how it was _built_, read on. It is made up of three parts: 68 | 69 | An executable in [`cloudenv/cloudenv.go`](/cloudenv/cloudenv.go). This application 70 | is bundled into a Lambda layer that does the following: 71 | 72 | * Is invoked like so: `cloudenv /var/lang/bin/python3.9 /var/runtime/bootstrap.py` 73 | * Looks for environment variables that look like either `MY_PASSWORD={aws-ssm}arn:aws:ssm:...` 74 | or `OTHER_VAL={aws-sm}arn:aws:secretsmanager:...`. 75 | * Fetches the values for those ARNs and substitutes them into the current 76 | environment, using `ssm:GetParameters` and `secretsmanager:GetSecretValue`. 77 | * Calls `exec()` to pass control to `/var/lang/bin/python3.9 /var/runtime/bootstrap.py` 78 | * The Python code for the user's Lambda function can access the values at 79 | `os.environ.MY_PASSWORD` or `os.environ.OTHER_VAL`, with no AWS SDKs required. 80 | 81 | This means that secrets are fetched during Lambda _init_ time, which is free on 82 | most runtimes. It also runs as parallel as possible for best performance. 83 | 84 | The second part is a CloudFormation macro, seen on the third line of the example 85 | YAML above. When included in a CloudFormation template, the `cloudenv` macro 86 | looks for any `AWS::Serverless::Function` that has an `Environment.Secrets` 87 | property (like the example function does) and: 88 | 89 | * Moves these to the function's `Environment.Variables` section with the 90 | appropriate `{aws-ssm}` or `{aws-sm}` prefix expected by the Lambda layer. 91 | * Adds the (correct per CPU architecture) Lambda layer to the function's list 92 | of `Layers`. 93 | * Adds an `AWS_LAMBDA_EXEC_WRAPPER` environment variable to intercept function 94 | cold starts with the executable in the Lambda layer. 95 | * Adds IAM policies that grant access to the **specific** values in Parameter 96 | Store and Secrets Manager. 97 | 98 | The third part is a second two-line Lambda layer. It is an implementation detail 99 | required by the fact that `provided` and `provided.al2` Lambda runtimes don't 100 | support [wrapper scripts][wrapper-scripts]. So the function's `Handler` needs to 101 | be a command that executes the actual Lambda function handler in those cases. 102 | 103 | [my-blog]: https://awsteele.com/blog/2022/10/19/configuration-in-the-cloud.html 104 | [wrapper-scripts]: https://docs.aws.amazon.com/lambda/latest/dg/runtimes-modify.html#runtime-wrapper 105 | -------------------------------------------------------------------------------- /docs/execution-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aidansteele/cloudenv/b47007834e162107e3bae98f1748b3ec37f3eb77/docs/execution-result.png -------------------------------------------------------------------------------- /example.yml: -------------------------------------------------------------------------------- 1 | Transform: 2 | - AWS::LanguageExtensions 3 | - cloudenv 4 | - AWS::Serverless-2016-10-31 5 | 6 | Resources: 7 | NodeExample: 8 | Type: AWS::Serverless::Function 9 | Properties: 10 | Runtime: nodejs16.x 11 | Handler: index.handler 12 | Environment: 13 | Variables: 14 | HELLO: WORLD 15 | Secrets: 16 | MY_SECRET: !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/my/parameter/name 17 | MY_SECOND_SECRET: !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/my/parameter/name 18 | THIRD_SECRET: !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/my/parameter/third 19 | REAL_SECRET: !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:mysecret-rlBksU 20 | InlineCode: | 21 | exports.handler = async function(event) { 22 | return { 23 | 'HELLO' : process.env['HELLO'], 24 | 'MY_SECRET' : process.env['MY_SECRET'], 25 | 'MY_SECOND_SECRET': process.env['MY_SECOND_SECRET'], 26 | 'THIRD_SECRET' : process.env['THIRD_SECRET'], 27 | 'REAL_SECRET' : process.env['REAL_SECRET'], 28 | } 29 | } 30 | 31 | PyExample: 32 | Type: AWS::Serverless::Function 33 | Properties: 34 | Architectures: [arm64] 35 | Runtime: python3.9 36 | Handler: index.handler 37 | AutoPublishAlias: live 38 | Environment: 39 | Variables: 40 | HELLO: WORLD 41 | Secrets: 42 | MY_SECRET: !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/my/parameter/name 43 | MY_SECOND_SECRET: !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/my/parameter/name 44 | THIRD_SECRET: !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/my/parameter/third 45 | REAL_SECRET: !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:mysecret-rlBksU 46 | InlineCode: | 47 | import os 48 | 49 | def handler(event, context): 50 | return { 51 | 'HELLO' : os.environ['HELLO'], 52 | 'MY_SECRET' : os.environ['MY_SECRET'], 53 | 'MY_SECOND_SECRET': os.environ['MY_SECOND_SECRET'], 54 | 'THIRD_SECRET' : os.environ['THIRD_SECRET'], 55 | 'REAL_SECRET' : os.environ['REAL_SECRET'], 56 | } 57 | 58 | CustomRuntimeExample: 59 | Type: AWS::Serverless::Function 60 | Properties: 61 | Architectures: [arm64] 62 | Runtime: provided.al2 63 | Handler: ./example 64 | AutoPublishAlias: live 65 | CodeUri: ./example/example 66 | Environment: 67 | Variables: 68 | HELLO: WORLD. 69 | Secrets: 70 | MY_SECRET: !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/my/parameter/name 71 | MY_SECOND_SECRET: !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/my/parameter/name 72 | THIRD_SECRET: !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/my/parameter/third 73 | REAL_SECRET: !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:mysecret-rlBksU 74 | 75 | Outputs: 76 | NodeExample: 77 | Value: !Ref NodeExample 78 | PyExample: 79 | Value: !Ref PyExample.Version 80 | CustomRuntimeExample: 81 | Value: !Ref CustomRuntimeExample.Version 82 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "github.com/aws/aws-lambda-go/lambda" 7 | "os" 8 | ) 9 | 10 | func main() { 11 | lambda.Start(handle) 12 | } 13 | 14 | func handle(ctx context.Context, _ json.RawMessage) (any, error) { 15 | env := map[string]string{} 16 | keys := []string{"HELLO", "MY_SECRET", "MY_SECOND_SECRET", "THIRD_SECRET", "REAL_SECRET"} 17 | for _, key := range keys { 18 | env[key] = os.Getenv(key) 19 | } 20 | 21 | return env, nil 22 | } 23 | -------------------------------------------------------------------------------- /example/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aidansteele/cloudenv/example 2 | 3 | go 1.19 4 | 5 | require github.com/aws/aws-lambda-go v1.34.1 // indirect 6 | -------------------------------------------------------------------------------- /example/go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-lambda-go v1.34.1 h1:M3a/uFYBjii+tDcOJ0wL/WyFi2550FHoECdPf27zvOs= 2 | github.com/aws/aws-lambda-go v1.34.1/go.mod h1:jwFe2KmMsHmffA1X2R09hH6lFzJQxzI8qK17ewzbQMM= 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aidansteele/cloudenv 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go-v2 v1.16.16 // indirect 7 | github.com/aws/aws-sdk-go-v2/config v1.17.8 // indirect 8 | github.com/aws/aws-sdk-go-v2/credentials v1.12.21 // indirect 9 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.17 // indirect 10 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23 // indirect 11 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17 // indirect 12 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.24 // indirect 13 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.17 // indirect 14 | github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.2 // indirect 15 | github.com/aws/aws-sdk-go-v2/service/ssm v1.31.0 // indirect 16 | github.com/aws/aws-sdk-go-v2/service/sso v1.11.23 // indirect 17 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.6 // indirect 18 | github.com/aws/aws-sdk-go-v2/service/sts v1.16.19 // indirect 19 | github.com/aws/smithy-go v1.13.3 // indirect 20 | github.com/jmespath/go-jmespath v0.4.0 // indirect 21 | golang.org/x/sync v0.1.0 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go-v2 v1.16.16 h1:M1fj4FE2lB4NzRb9Y0xdWsn2P0+2UHVxwKyOa4YJNjk= 2 | github.com/aws/aws-sdk-go-v2 v1.16.16/go.mod h1:SwiyXi/1zTUZ6KIAmLK5V5ll8SiURNUYOqTerZPaF9k= 3 | github.com/aws/aws-sdk-go-v2/config v1.17.8 h1:b9LGqNnOdg9vR4Q43tBTVWk4J6F+W774MSchvKJsqnE= 4 | github.com/aws/aws-sdk-go-v2/config v1.17.8/go.mod h1:UkCI3kb0sCdvtjiXYiU4Zx5h07BOpgBTtkPu/49r+kA= 5 | github.com/aws/aws-sdk-go-v2/credentials v1.12.21 h1:4tjlyCD0hRGNQivh5dN8hbP30qQhMLBE/FgQR1vHHWM= 6 | github.com/aws/aws-sdk-go-v2/credentials v1.12.21/go.mod h1:O+4XyAt4e+oBAoIwNUYkRg3CVMscaIJdmZBOcPgJ8D8= 7 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.17 h1:r08j4sbZu/RVi+BNxkBJwPMUYY3P8mgSDuKkZ/ZN1lE= 8 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.17/go.mod h1:yIkQcCDYNsZfXpd5UX2Cy+sWA1jPgIhGTw9cOBzfVnQ= 9 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23 h1:s4g/wnzMf+qepSNgTvaQQHNxyMLKSawNhKCPNy++2xY= 10 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23/go.mod h1:2DFxAQ9pfIRy0imBCJv+vZ2X6RKxves6fbnEuSry6b4= 11 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17 h1:/K482T5A3623WJgWT8w1yRAFK4RzGzEl7y39yhtn9eA= 12 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17/go.mod h1:pRwaTYCJemADaqCbUAxltMoHKata7hmB5PjEXeu0kfg= 13 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.24 h1:wj5Rwc05hvUSvKuOF29IYb9QrCLjU+rHAy/x/o0DK2c= 14 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.24/go.mod h1:jULHjqqjDlbyTa7pfM7WICATnOv+iOhjletM3N0Xbu8= 15 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.17 h1:Jrd/oMh0PKQc6+BowB+pLEwLIgaQF29eYbe7E1Av9Ug= 16 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.17/go.mod h1:4nYOrY41Lrbk2170/BGkcJKBhws9Pfn8MG3aGqjjeFI= 17 | github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.2 h1:3x1Qilin49XQ1rK6pDNAfG+DmCFPfB7Rrpl+FUDAR/0= 18 | github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.2/go.mod h1:HEBBc70BYi5eUvxBqC3xXjU/04NO96X/XNUe5qhC7Bc= 19 | github.com/aws/aws-sdk-go-v2/service/ssm v1.31.0 h1:zBiXS2v+ycKZ61bTBR1jGqIJhEW7Qjcl8c/mrkUNeog= 20 | github.com/aws/aws-sdk-go-v2/service/ssm v1.31.0/go.mod h1:JtkQSJFGEovwP6s+guH5Ap7iUemh3nMqHtg5liCv9ok= 21 | github.com/aws/aws-sdk-go-v2/service/sso v1.11.23 h1:pwvCchFUEnlceKIgPUouBJwK81aCkQ8UDMORfeFtW10= 22 | github.com/aws/aws-sdk-go-v2/service/sso v1.11.23/go.mod h1:/w0eg9IhFGjGyyncHIQrXtU8wvNsTJOP0R6PPj0wf80= 23 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.6 h1:OwhhKc1P9ElfWbMKPIbMMZBV6hzJlL2JKD76wNNVzgQ= 24 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.6/go.mod h1:csZuQY65DAdFBt1oIjO5hhBR49kQqop4+lcuCjf2arA= 25 | github.com/aws/aws-sdk-go-v2/service/sts v1.16.19 h1:9pPi0PsFNAGILFfPCk8Y0iyEBGc6lu6OQ97U7hmdesg= 26 | github.com/aws/aws-sdk-go-v2/service/sts v1.16.19/go.mod h1:h4J3oPZQbxLhzGnk+j9dfYHi5qIOVJ5kczZd658/ydM= 27 | github.com/aws/smithy-go v1.13.3 h1:l7LYxGuzK6/K+NzJ2mC+VvLUbae0sL3bXU//04MkmnA= 28 | github.com/aws/smithy-go v1.13.3/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= 29 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 30 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 31 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 32 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 33 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 34 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 35 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 36 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 37 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 38 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 39 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 40 | -------------------------------------------------------------------------------- /macro/macro.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | 5 | def handler(event, context): 6 | print(json.dumps({'before': event})) 7 | 8 | resources = event['fragment']['Resources'] 9 | for name in resources: 10 | resource = resources[name] 11 | if resource['Type'] == 'AWS::Serverless::Function': 12 | handle(name, resource) 13 | 14 | print(json.dumps({'after': event})) 15 | 16 | return { 17 | 'status': 'success', 18 | 'requestId': event['requestId'], 19 | 'fragment': event['fragment'], 20 | } 21 | 22 | 23 | def handle(name, resource): 24 | props = resource['Properties'] 25 | secrets = props['Environment'].pop('Secrets') 26 | print(json.dumps(secrets)) 27 | 28 | env = props['Environment'].get('Variables', {}) 29 | props['Environment']['Variables'] = env 30 | 31 | package_type = props.get('PackageType', 'Zip') 32 | 33 | if len(secrets) > 0: 34 | if package_type == 'Zip': 35 | layers = props.get('Layers', []) 36 | props['Layers'] = layers 37 | 38 | if props.get('Architectures', ['x86_64'])[0] == 'arm64': 39 | layers.append(os.environ.get('LayerArm64')) 40 | else: 41 | layers.append(os.environ.get('LayerX8664')) 42 | 43 | env['AWS_LAMBDA_EXEC_WRAPPER'] = '/opt/cloudenv' 44 | 45 | runtime = props['Runtime'] 46 | if runtime == 'provided' or runtime == 'provided.al2': 47 | layers.append(os.environ.get('BootstrapLayer')) 48 | elif package_type == 'Image': 49 | image_config = props.get('ImageConfig', {}) 50 | entrypoint = image_config.get('EntryPoint', []) 51 | entrypoint = ['/opt/cloudenv'].extend(entrypoint) 52 | image_config['EntryPoint'] = entrypoint 53 | props['ImageConfig'] = image_config 54 | 55 | ssmParams = [] 56 | smSecrets = [] 57 | 58 | for key in secrets: 59 | arn = secrets[key] 60 | 61 | prefix = "" 62 | service = arn.split(":")[2] 63 | if service == "ssm": 64 | ssmParams.append(arn) 65 | prefix = "{aws-ssm}" 66 | elif service == "secretsmanager": 67 | smSecrets.append(arn) 68 | prefix = "{aws-sm}" 69 | else: 70 | raise "oh no!" 71 | 72 | env[key] = prefix + arn 73 | 74 | if len(ssmParams) > 0: 75 | policies = props.get('Policies', []) 76 | props['Policies'] = policies 77 | policies.append({ 78 | 'Statement': [ 79 | { 80 | 'Effect': 'Allow', 81 | 'Action': 'ssm:GetParameters', 82 | 'Resource': list(set(ssmParams)) 83 | } 84 | ] 85 | }) 86 | 87 | if len(smSecrets) > 0: 88 | policies = props.get('Policies', []) 89 | props['Policies'] = policies 90 | policies.append({ 91 | 'Statement': [ 92 | { 93 | 'Effect': 'Allow', 94 | 'Action': 'secretsmanager:GetSecretValue', 95 | 'Resource': list(set(smSecrets)) 96 | } 97 | ] 98 | }) 99 | -------------------------------------------------------------------------------- /macro/macro.yml: -------------------------------------------------------------------------------- 1 | Transform: AWS::Serverless-2016-10-31 2 | 3 | Resources: 4 | Function: 5 | Type: AWS::Serverless::Function 6 | Properties: 7 | Architectures: [arm64] 8 | Runtime: python3.9 9 | Handler: macro.handler 10 | CodeUri: ./macro.py 11 | AutoPublishAlias: live 12 | MemorySize: 512 13 | Environment: 14 | Variables: 15 | LayerX8664: !Ref LayerX8664 16 | LayerArm64: !Ref LayerArm64 17 | BootstrapLayer: !Ref BootstrapLayer 18 | 19 | BootstrapLayer: 20 | Type: AWS::Lambda::LayerVersion 21 | UpdateReplacePolicy: Retain 22 | Properties: 23 | CompatibleArchitectures: [x86_64, arm64] 24 | Content: ../bootstrap 25 | 26 | LayerX8664: 27 | Type: AWS::Lambda::LayerVersion 28 | UpdateReplacePolicy: Retain 29 | Properties: 30 | CompatibleArchitectures: [x86_64] 31 | Content: ../built/x86_64/cloudenv 32 | 33 | LayerArm64: 34 | Type: AWS::Lambda::LayerVersion 35 | UpdateReplacePolicy: Retain 36 | Properties: 37 | CompatibleArchitectures: [arm64] 38 | Content: ../built/arm64/cloudenv 39 | 40 | Macro: 41 | Type: AWS::CloudFormation::Macro 42 | Properties: 43 | Name: cloudenv 44 | FunctionName: !Ref Function.Alias 45 | 46 | Outputs: 47 | Function: 48 | Value: !Ref Function.Version 49 | LayerArm64: 50 | Value: !Ref LayerArm64 51 | LayerX8664: 52 | Value: !Ref LayerX8664 53 | --------------------------------------------------------------------------------