Inject Secure Parameters

When working with secrets in an AWS infrastructure workload, you have the option to use AWS System Manager Parameter Store.

AWS Systems Manager Parameter Store provides secure, hierarchical storage for configuration data management and secrets management. You can store data such as passwords, database strings, Amazon Machine Image (AMI) IDs, and license codes as parameter values.

You can store values as plain text or encrypted data. You can reference Systems Manager parameters in your scripts, commands, SSM documents, and configuration and automation workflows by using the unique name that you specified when you created the parameter.

As a best practice, you should generally opt to use Secrets Manager over Parameter store for secure credentials infrequently accessed.

Click here to expand feature comparison

Now we will add a secure parameter via the AWS SSM Parameter STore. This secure parameter is a path to an image that the application will display.

APP=$(copilot svc show --json | jq -r .application)
CENV=$(copilot svc show --json | jq -r .configurations[].environment)
aws ssm put-parameter --name DEMO_PARAMETER --value "static/parameter-diagram.png" --type SecureString --tags Key=copilot-environment,Value=$CENV Key=copilot-application,Value=$APP

The parameter is created with the correct copilot application name and environment value as tags so copilot knows how to retrieve it.

{
    "Tier": "Standard", 
    "Version": 1
}

Next, inside of our manifest.yml file we add a section called called secrets. Any secure parameter from SSM can be accessed via this area in the manifest.  The key is the name of the environment variable, the value is the name of the SSM parameter. Paste the code below to append to the manifest.yml file.

cat << EOF >> copilot/todo-app/manifest.yml
secrets:                      # Pass secrets from AWS Systems Manager (SSM) Parameter Store.
  DEMO_PARAMETER: DEMO_PARAMETER  # The key is the name of the environment variable, the value is the name of the SSM parameter.
EOF

The value of the secure string inside SSM Parameter store will be available to your app via the DEMO_PARAMETER environment variable.

Now that the secure parameter secret value has been added, the ECS service running task needs to pick up the newly defined parameter.

First we must commit the change to the local git repository - copilot only picks up committed changes in a git-enabled project.

git commit -am "Added Secrets to manifest"

Next, we trigger a copilot deployment for the ECS Task Definition to receive the new parameter. We pass the arbitrary tag of “update-credentials” to force the new deployment.

copilot svc deploy --tag update-credentials

Head back to the ECS Console to check on progress - this usually takes 1-2 minutes. Once the task is running, go back to the todo app and refresh, you should see a fully functional app once again with a diagram displayed.

working-app

Within a CDK application, you can pull both plaintext and secure secret parameters via the aws-cdk/aws-ssm library. This library is included inside the repository’s package.json file.

This part of the tutorial will demonstrate how to add a secure SSM parameter and use it in the application to display an image that is conditional on the value of the parameter passed via environment variables.

To create the secure SSM parameter for this tutorial (specifying the name of the image to display which is already present in the application):

aws ssm put-parameter --name DEMO_PARAMETER --value "static/parameter-diagram.png" --type SecureString

You should see the result:

{
    "Tier": "Standard", 
    "Version": 1
}

Next, replace the contents of the file lib/ecs-fargate-stack.ts with the below code. This code can also be found in lib/ecs-fargate-stack-ssm.ts for reference.

cd ~/environment/secret-ecs-cdk-example
cat << EOF > lib/ecs-fargate-stack.ts
import { App, Stack, StackProps, CfnOutput } from '@aws-cdk/core';
import { Vpc } from "@aws-cdk/aws-ec2";
import { Cluster, ContainerImage, Secret as ECSSecret } from "@aws-cdk/aws-ecs";
import { ApplicationLoadBalancedFargateService } from '@aws-cdk/aws-ecs-patterns';
import { Secret } from '@aws-cdk/aws-secretsmanager';

//SSM Parameter imports
import { SecretValue } from '@aws-cdk/core';
import { StringParameter, ParameterTier, ParameterType } from "@aws-cdk/aws-ssm";

export interface ECSStackProps extends StackProps {
  vpc: Vpc
  dbSecretArn: string
}

export class ECSStack extends Stack {

  constructor(scope: App, id: string, props: ECSStackProps) {
    super(scope, id, props);

    const containerPort = this.node.tryGetContext("containerPort");
    const containerImage = this.node.tryGetContext("containerImage");
    const creds = Secret.fromSecretCompleteArn(this, 'postgresCreds', props.dbSecretArn);

    //fetch existing parameter from parameter store securely
    const DEMOPARAM = StringParameter.fromSecureStringParameterAttributes(this, 'demo_param', {
      parameterName: 'DEMO_PARAMETER',
      version: 1
    });

    const cluster = new Cluster(this, 'Cluster', {
      vpc: props.vpc,
      clusterName: 'fargateClusterDemo'
    });

    const fargateService = new ApplicationLoadBalancedFargateService(this, "fargateService", {
      cluster,
      taskImageOptions: {
        image: ContainerImage.fromRegistry(containerImage),
        containerPort: containerPort,
        enableLogging: true,
        secrets: {
          POSTGRES_DATA: ECSSecret.fromSecretsManager(creds),
          //Inject parameter value securely
          DEMO_PARAMETER: ECSSecret.fromSsmParameter(DEMOPARAM),
        },
      },
      desiredCount: 1,
      publicLoadBalancer: true,
      serviceName: 'fargateServiceDemo'
    });

    new CfnOutput(this, 'LoadBalancerDNS', { value: fargateService.loadBalancer.loadBalancerDnsName });
  }
}
EOF

After you make the changes, save the file and redeploy the app:

cdk deploy --all --require-approval never

This revision to the stack injects the parameter DEMO_PARAMETER into the container via the secrets property.

Once the deployment is complete, go back to the browser and you should see the app again with the new image displayed.

working-app