cdklabs / cdk-cicd-wrapper

This repository contains the infrastructure as code to wrap your AWS CDK project with CI/CD around it.
https://cdklabs.github.io/cdk-cicd-wrapper/
Apache License 2.0
26 stars 6 forks source link

[FEATURE] Add KMS arn to NPMRegistryConfig #59

Closed ArneOttenVW closed 4 months ago

ArneOttenVW commented 4 months ago

Describe the feature

I want to configure the codebuild job to use our private npm registry and for that, i want to use the .npmRegistry functionality on the PipelineBluprintBuilder.

Setting it like this:

.npmRegistry({
    basicAuthSecretArn: 'arn:aws:secretsmanager:eu-west-1:123456789123:secret:JFrogCredentialsTest-abcdef',   
    url: 'https://jfrog....com/artifactory/api/npm/npm/',
})

Results in the codebuild IAM policy to access the secret:

{
    "Action": "secretsmanager:GetSecretValue",
    "Resource": [
        "arn:aws:secretsmanager:eu-west-1:123456789123:secret:JFrogCredentialsTest-abcdef",
     ],
    "Effect": "Allow"
},

However, there is no way that I can see to also grant it permission to the KMS key it is encrypted with. If we are supposed to use the kms-key from the encryption stack, that's somewhat of a chicken-and-egg problem right? The encryption-stack key only exists after deployment but the npm secret needs to be encrypted with it before deployment.

Use Case

I would need to edit the codebuild policy manually at the time which would result in frequent overrides whenever there is an update.

Proposed Solution

Add another field to NPMRegistryConfig maybe. Or let us edit the codebuild policy with the PipelineBlueprintBuilder just like we can already add CodeBuild environment variables that way.

Other Information

No response

Acknowledgements

Environment details (OS name and version, etc.)

Ubuntu 22.04

gmuslia commented 4 months ago

This can be done in the following way by overriding the GlobalResources.CODEBUILD_FACTORY:

.resourceProvider(GlobalResources.CODEBUILD_FACTORY, {
      provide(context) {
        let proxyConfig;
        if (context.has(GlobalResources.PROXY)) {
          proxyConfig = context.get(GlobalResources.PROXY)!;
        }
        const vpc = context.get(GlobalResources.VPC).vpc;
        const parameterProvider = context.get(GlobalResources.PARAMETER_STORE);
        return new class extends DefaultCodeBuildFactory {
          protected generateCodeBuildSecretsManager(props: DefaultCodeBuildFactoryProps) {
            const secrets = super.generateCodeBuildSecretsManager(props);
            return {
              PROXY_USERNAME: secrets.PROXY_USERNAME, // keep only this from SecretsManager
              PROXY_PASSWORD: secrets.PROXY_PASSWORD, // keep only this from SecretsManager
            };
          }
          protected generateBuildEnvironmentVariables (props: DefaultCodeBuildFactoryProps) {
            let variables = super.generateBuildEnvironmentVariables(props);
            return {
              ...variables,
              HTTP_PROXY_PORT: '8080', // overwrite this var as is not available in SecretsManager
              HTTPS_PROXY_PORT: '8443', // overwrite this var as is not available in SecretsManager
              PROXY_DOMAIN: 'xxxxxxx, // overwrite this var as is not available in SecretsManager
            };
          }
          protected generateRolePolicies (props: DefaultCodeBuildFactoryProps) {
            const rolePolicies = super.generateRolePolicies(props);
            const newPolicyStatements = [
              new cdk.aws_iam.PolicyStatement({
                effect: cdk.aws_iam.Effect.ALLOW,
                actions: ['codeartifact:GetAuthorizationToken'],
                resources: [
                  'arn:aws:codeartifact:eu-west-1:0123456789012:domain/my-domain',
                ],
              }),
              new cdk.aws_iam.PolicyStatement({
                actions: [
                  'codeartifact:DescribePackageVersion',
                  'codeartifact:DescribeRepository',
                  'codeartifact:GetPackageVersionReadme',
                  'codeartifact:GetRepositoryEndpoint',
                  'codeartifact:ListPackages',
                  'codeartifact:ListPackageVersions',
                  'codeartifact:ListPackageVersionAssets',
                  'codeartifact:ListPackageVersionDependencies',
                  'codeartifact:ReadFromRepository',
                ],
                resources: [
                  'arn:aws:codeartifact:eu-west-1:0123456789012:repository/my-domain/my-domain',
                ],
              }),
              new cdk.aws_iam.PolicyStatement({
                effect: cdk.aws_iam.Effect.ALLOW,
                actions: ['sts:GetServiceBearerToken'],
                resources: ['*'],
              }),
            ];
            return [
              ...rolePolicies,
              ...newPolicyStatements,
            ];
          }
        }({
          resAccount: context.blueprintProps.deploymentDefinition.RES.env.account,
          vpc,
          proxyConfig,
          npmRegistry: context.blueprintProps.npmRegistry,
          codeBuildEnvSettings: context.blueprintProps.codeBuildEnvSettings,
          parameterProvider,
          applicationQualifier: context.blueprintProps.applicationQualifier,
          region: context.blueprintProps.deploymentDefinition.RES.env.region,
          installCommands: context.get(GlobalResources.PHASE).getCommands(PipelinePhases.INITIALIZE),
        });
      },
    })
    .definePhase(PipelinePhases.INITIALIZE, [
      PhaseCommands.CONFIGURE_HTTP_PROXY,
      PhaseCommands.ENVIRONMENT_PREPARATION,
      new ShellScriptPhaseCommand('scripts/codeartifact-login.sh'),
    ])
ArneOttenVW commented 4 months ago

Awesome, what a crazy response time. 😄

I think that's going to work. I was thinking about overriding the CodeBuildFactoryProvider but wanted to check before spending hours of deep diving into your source code. I will check tomorrow and close it if it works.

gmuslia commented 4 months ago

Lets have a quick call tomorrow and I can walk you through the possible options for it. I will send a meeting invitation so we can discuss more on this.

gmuslia commented 4 months ago

@ArneOttenVW I did a small update to the example above as we introduce 2 new params for the generateRolePolicies() (I have mentioned them in the updated release note too).

Please let me know if the example above works so I can close this issue. Thanks in advance 😊

ArneOttenVW commented 4 months ago

Thank you very much @gmuslia, that part works great! I will continue testing in the coming days.

gmuslia commented 4 months ago

@ArneOttenVW please check again, I added a missing param which was introduced in V0.1.5 (installCommands)

Without providing the installCommands as input in the GlobalResources.CODEBUILD_FACTORY overriding in your overriding of the scripts/codeartifact-login.sh wont work. Check it and let me know.

ArneOttenVW commented 4 months ago

I was just about to reopen this because I noticed that my defined phases were missing. Your fix sets the defined commands for the install phase correctly but when I define an additional phase, it is missing.

This sets the scripts in the install phase but the pre_build phase is missing.

[...]
        } ({
          resAccount: context.blueprintProps.deploymentDefinition.RES.env.account,
          vpc,
          proxyConfig,
          npmRegistry: context.blueprintProps.npmRegistry,
          codeBuildEnvSettings: context.blueprintProps.codeBuildEnvSettings,
          parameterProvider,
          applicationQualifier: context.blueprintProps.applicationQualifier,
          region: context.blueprintProps.deploymentDefinition.RES.env.region,
          installCommands: context.get(GlobalResources.PHASE).getCommands(PipelinePhases.INITIALIZE),
        });
      },
    })
    .definePhase(PipelinePhases.INITIALIZE, [
      PhaseCommands.CONFIGURE_HTTP_PROXY,
      PhaseCommands.ENVIRONMENT_PREPARATION,
      PhaseCommands.NPM_LOGIN,
    ])
    .definePhase(PipelinePhases.PRE_BUILD, [
      PhaseCommands.VALIDATE
    ])
    .synth(app);

And when I add the the PRE_BUILD phase to the installCommands array, it just adds the commands definded in the pre_build phase to the install phase:

[...]
        } ({
          resAccount: context.blueprintProps.deploymentDefinition.RES.env.account,
          vpc,
          proxyConfig,
          npmRegistry: context.blueprintProps.npmRegistry,
          codeBuildEnvSettings: context.blueprintProps.codeBuildEnvSettings,
          parameterProvider,
          applicationQualifier: context.blueprintProps.applicationQualifier,
          region: context.blueprintProps.deploymentDefinition.RES.env.region,
          installCommands:[
            context.get(GlobalResources.PHASE).getCommands(PipelinePhases.INITIALIZE),
            context.get(GlobalResources.PHASE).getCommands(PipelinePhases.PRE_BUILD),
          ] 
        });
      },
    })
    .definePhase(PipelinePhases.INITIALIZE, [
      PhaseCommands.CONFIGURE_HTTP_PROXY,
      PhaseCommands.ENVIRONMENT_PREPARATION,
      PhaseCommands.NPM_LOGIN,
    ])
    .definePhase(PipelinePhases.PRE_BUILD, [
      PhaseCommands.VALIDATE
    ])
    .synth(app);

buildspec result:

"phases": {
    "install": {
      "commands": [
        "export HTTP_PROXY=\"http://$PROXY_USERNAME:$PROXY_PASSWORD@$PROXY_DOMAIN:$HTTP_PROXY_PORT\"",
        "export HTTPS_PROXY=\"https://$PROXY_USERNAME:$PROXY_PASSWORD@$PROXY_DOMAIN:$HTTPS_PROXY_PORT\"",
        "echo \"--- Proxy Test ---\"",
        "curl -Is --connect-timeout 5 https://my-proxy-test.com | grep \"HTTP/\"",
        [
          "bash_command=$[...] // Docker proxy
          "bash_command=$[...] // Warming
          "bash_command=$[...] // npm login
        ],
        [
          "npm run validate"
        ]
      ]
    },
    "build": {
      "commands": [
        "npm run validate",
        "npm run build",
        "npm run test",
        "bash_command=$[...]" // cdk synth
      ]
    }
  },

Or is this intentional?

gmuslia commented 4 months ago

@ArneOttenVW please dont add it into the installCommands the PRE_BUILD and installCommands is just an array and as you see there you passed an array of arrays (we dont have any checks so far hence you have that behavior).

Check it out and let me know.

gmuslia commented 4 months ago

But yes you are right, the PRE_BUILD actually puts them in the BUILD and not in the PRE_BUILD of the Synth, see below:

Screenshot 2024-06-21 at 11 47 57 Screenshot 2024-06-21 at 11 48 15

So in other words we have another mapping we use and this is due to the jsii:

gmuslia commented 4 months ago

In case there are more concerns please feel free to open a new issue, will be closing this one. Thank you