AWS CDK: how to deploy resources to different accounts? - amazon-web-services

Is it possible to deploy resources to two different AWS accounts using CDK?
As a simple example, imagine 2 resources that are linked (2 different IAM roles, perhaps), that need to be deployed to accounts accountA and accountB. They are linked, so their lifecycles should be tied together (i.e. they are created and destroyed at the same time and I shouldn't have to run two deployment actions). I'd like to be able to specify at deployment time where the resources go, not at synthesis time.
This requirement is easily achieved using Terraform (by means of two different provisioner definitions). How do I achieve this using CDK? Can this be achieved within a single stack, or is the CDK model that of one stack per target account? What does a simple example look like?

Yes, this is possible. you need to pass environment config object to stack props.
From the docs:
Each Stack instance in your AWS CDK app is explicitly or implicitly
associated with an environment (env). An environment is the target AWS
account and AWS Region into which the stack is intended to be
deployed.
Usage:
const envEU = { account: '2383838383', region: 'eu-west-1' };
const envUSA = { account: '8373873873', region: 'us-west-2' };
new MyFirstStack(app, 'first-stack-us', { env: envUSA, encryption: false });
new MyFirstStack(app, 'first-stack-eu', { env: envEU, encryption: true });
More info here.
In case you want to deploy 2 different resources within the same stack to 2 different accounts, this is not supported yet.
You would need to create different stack for each resource and pass the environment object accordingly.

Related

aws-cdk kms multi-region key. What constructors use to setup regions?

Using AWS CDK we could create multi-region KMS keys by
Creating the principal key(pk) with the level 1 constructor CfnKey
Creating the replica of the principal key using the level 1 constructor CfnReplicaKey, which takes as one of its parameters the pk_arn
Those constructors however do not specify the regions, where I want to make those keys available.
My question is:
What aws-CDK constructor or pattern should I use to make the replicas available in certain regions, using aws-CDK?
Thanks in advance
CfnReplicaKey will be created in the parent stack's region (see a CloudFormation example in the docs).
For the CDK (and CloudFormation), the unit of deployment is [Edit:] the Stack, which is tied to one environment:
Each Stack instance in your AWS CDK app is explicitly or implicitly associated with an environment (env). An environment is the target AWS account and region into which the stack is intended to be deployed.
This logic applies generally to all CDK resources - the account/region is defined at the stack level, not the construct level. Stacks can be replicated across regions and accounts in several ways, including directly in a CDK app:
# replicate the stack in several regions using CDK
app = core.App()
for region in ["us-east-1". "us-west-1", "us-central-1", "eu-west-1"]:
MyStack(app, "MyStack_" + region, env=Environment(
region=region,
account="555599931100"
))

Terraform deployment of Docker Containers to aws ecr

I am having issues deploying my docker images to aws ecr as part of a terraform deployment and I am trying to think through the best long term strategy.
At the moment I have a terraform remote backend in s3 and dynamodb on let's call it my master account. I then have dev/test etc environments in separate accounts. The terraform deployment is currently run off my local machine (mac) and uses the aws 'master' account and its credentials which in turn assumes a role in the target deployment account to create the resources as per:
provider "aws" { // tell terraform which SDK it needs to load
alias = "target"
region = var.region
assume_role {
role_arn = "arn:aws:iam::${var.deployment_account}:role/${var.provider_env_deployment_role_name}"
}
}
I am creating a number of ecs services with Fargate deployments. The container images are built in separate repos by GitHub Actions and saved as GitHub packages. These package names and versions are being deployed after the creation of the ecr and service (maybe that's not ideal thinking about it) and this is where the problems arise.
The process is to pull the image from GitHub Packages, retag it and upload to the ecr using multiple executions of a null_resource local-exec. Works fine stand alone but has problems as part of the terraform process. I think the reason is that the other resources use the above provider to get permissions but as null_resource does not accept a provider it cannot get the permissions this way. So I have been passing the aws creds values into the shell. Not convinced this is really secure but that's currently moot as it ain't working either. I get this error:
Error saving credentials: error storing credentials - err: exit status 1, out: `error storing credentials - err: exit status 1, out: `The specified item already exists in the keychain.``
Part of me thinks this is the wrong approach and that as I migrate to deploying via a Github action I can separate the infrastructure deployment via terraform from what is really the application deployment and just use GitHub secrets to reset the credentials values then run the script.
Alternatively, maybe the keychain thing just goes away and my process will work fine? Secure ??
That's fine for this scenario but it isn't really a generic approach for all my use cases.
I am shortly going to start deploying multiple aws lambda functions with docker containers. Haven't done it before but it looks like the process is going to be: create ecr, deploy container, deploy lambda function. This really implies that the container deployment should integral to the terraform deployment which loops back to my issue with the local-exec??
I found Actions to deploy to ECR which would imply splitting the deployments into multiple files but that seems inelegant and potentially brittle.
Maybe there is a simple solution, but given where I am trying to go with this, what is my best approach?
I know this isn't a complete answer, but you should be pulling your AWS creds from environment variables. I don't really understand if you need credentials for different accounts, but if you do then swap them during the progress of your action. See https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html . Terraform should pick these up and automatically use them for AWS access.
Instead of those hard coded access key/secret access keys I'd suggest making use of Github/AWS's ability to assume role through temporary credentials with OIDC https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services
You'd likely only define one initial role that you'd authenticate into and from there assume into the other accounts you're deploying into.
These the assume role credentials are only good for an hour and do not have the operation overhead of having to rotate them.
As suggested by Kevin Buchs answer...
My primary issue was related to deploying from a mac and the use of the keychain. As this was not on the critical path I went round it and set up a GitHub Action.
The Action loaded environmental variables from GitHub secrets for my 'master' aws account credentials.
AWS_ACCESS_KEY_ID: ${{ secrets.NK_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NK_AWS_SECRET_ACCESS_KEY }}
I also loaded the target accounts credentials into environmental variables in the same way BUT with the prefix TF_VAR.
TF_VAR_DEVELOP_AWS_ACCESS_KEY_ID: ${{ secrets.DEVELOP_AWS_ACCESS_KEY_ID }}
TF_VAR_DEVELOP_AWS_SECRET_ACCESS_KEY: ${{ secrets.DEVELOP_AWS_SECRET_ACCESS_KEY }}
I then declare terraform variables which will be automatically populated from the environment variables.
variable "DEVELOP_AWS_ACCESS_KEY_ID" {
description = "access key for the dev account"
type = string
}
variable "DEVELOP_AWS_SECRET_ACCESS_KEY" {
description = "secret access key for the dev account"
type = string
}
And when I run a shell script with a local exec:
resource "null_resource" "image-upload-to-importcsv-ecr" {
provisioner "local-exec" {
command = "./ecr-push.sh ${var.DEVELOP_AWS_ACCESS_KEY_ID} ${var.DEVELOP_AWS_SECRET_ACCESS_KEY} "
}
}
Within the script I can then use these arguments to set the credentials eg
AWS_ACCESS=$1
AWS_SECRET=$1
.....
export AWS_ACCESS_KEY_ID=${AWS_ACCESS}
export AWS_SECRET_ACCESS_KEY=${AWS_SECRET}
and the script now has credentials to do whatever.

AWS CDK - How to pass Access Key Secret and Secret Key Id as Env Param to Container

I'm using CDK to build our infra on AWS. I create my IAM User for my microservices to talk to AWS Services under the defined policies. My issue is I cannot get aws secret key and id and then, pass as Env variable to my container. See below:
First, I create my IAM user which will be used by my microservices.
const user = new User(this, "user", {
userName: `${myAppEnv}-api-iam-user`,
});
Second, I'm trying to create Access Key.
const accessKey = new CfnAccessKey(this, "ApiUserAccessKey", {
userName: user.userName,
});
const accessKeyId = new CfnOutput(this, "AccessKeyId", {
value: accessKey.ref,
});
const accessKeySecret = new CfnOutput(this, "SecretAccessKeyId", {
value: accessKey.attrSecretAccessKey,
});
Next, I want to pass it as an env variable.
const apiContainer = apiTaskDefinition.addContainer(containerId, {
image: apiImage,
environment: {
APP_ENV: myAppEnv,
AWS_ACCESS_KEY_ID: awsAccessKeyId.value || ":(",
AWS_SECRET_ACCESS_KEY: awsSecretAccessKey.value || ":(",
NOTIFICATIONS_TABLE_ARN: notificationsTableDynamoDBArn,
NOTIFICATIONS_QUEUE_URL: notificationsQueueUrl,
},
cpu: 256,
memoryLimitMiB: 512,
logging: new AwsLogDriver({ streamPrefix: `${myAppEnv}-ec-api` }),
});
When my CDK deploy finishes successfully, I see the below printed out :/
Outputs:
staging-ecstack.AccessKeyId = SOMETHING
staging-ecstack.SecretAccessKeyId = SOMETHINGsy12X21xSSOMETHING2X2
Do you have any idea how I can achieve this?
Generally speaking, creating an IAM user isn't the way to go here - you're better off using an IAM role. With the CDK it will create a taskRole for you automatically when you instantiate the taskDefinition construct. You can assign permissions to other constructs in your stack using various grant* methods as described here:
const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef');
const container = taskDefinition.addContainer('web', {
image: ecs.ContainerImage.fromRegistry("apps/myapp"),
memoryLimitMiB: 256,
});
// Grant this task role access to use other resources
myDynamodbTable.grantReadWriteData(taskDefinition.taskRole);
mySnsTopic.grantPublish(taskDefinition.taskRole);
In short, find the answer in my blogpost here:
https://blog.michaelfecher.com/i-tell-you-a-secret-provide-database-credentials-to-an-ecs-fargate-task-in-aws-cdk
To explain a bit more detailled on your issue:
Avoid custom created IAM Roles and use the generated ones by CDK. They are aligned and using the least-privilege principle. In my blog post, the manual IAM role creation isn't necessary. Yes, I know... I need to update that. ;)
Use the grant* method of the corresponding resource, e.g. taskDefinition.
This will create a policy statement for the generated role of 1)
Don't use environment variables for secrets.
There are a lot of resources on the web, which tell you why.
One is this here: https://security.stackexchange.com/questions/197784/is-it-unsafe-to-use-environmental-variables-for-secret-data
Especially, when working with ECS, make use of the secrets argument in the task definition (see the blog post).
It seems, that you're passing the token instead of the actual secret value.
That doesn't work. The token is resolved during synth / Cloud Formation generation.
You won't need the CfnOutput. Use the direct Stack -> Stack passing of fields.
The CfnOutput is really sth., which you should avoid, when having all Stacks under control in one CDK application.
That only makes sense, if you want to share between CDK applications, which are separated deployments and repositories.
If sth. is unclear, don't hesitate asking questions.

Is there a way to specify cloudformation stack deletion policy using AWS CDK?

I have created a VPC using CDK, and now have a need to change the number of subnets and NAT gateways without destroying the (now in production) ec2 instances in it or the (now in production) EIPs that are whitelisted in other systems.
I tried this using CDK and it tried to destroy the VPC and re-provision everything, which is undesirable in production.
Since it is apparently not possible to do what I want by changing the stack definition, I need to get rid of the stack, without removing the VPC.
https://docs.aws.amazon.com/cdk/api/latest/docs/#aws-cdk_core.RemovalPolicy.html
I found this document here about a RemovalPoliy enum in CDK, but there is nothing saying where that can be applied. Google searches resulted in only references to S3 and RDS, even though this enum is in CDK core. I tried applying it to the following CDK resources: stack, vpc, security group, etc, but none of those constructs accept the removalPolicy parameter.
eg:
const vpc = new ec2.Vpc(this, 'PRODVPC', {
maxAzs: 2,
removalPolicy: cdk.RemovalPolicy.RETAIN
});
new VPCPRODStack(app, 'RDVPCPRODStack', {
env: {region: 'ca-central-1', account: '12345'},
removalPolicy: cdk.RemovalPolicy.RETAIN
});
What am I missing? Is it even possible to set a cloudformation removal policy using CDK at the stack level?
I think the one you're requesting for is https://github.com/aws/aws-cdk-rfcs/issues/72
For now
You can define a custom resource backed by lambda which calls cloudformation set-stack-policy api and updates policy.
You have https://docs.aws.amazon.com/cdk/api/latest/docs/#aws-cdk_core.CfnResource.html#apply-wbr-removal-wbr-policypolicy-options which you can use to applyRemovalPolicy i.e to either destroy or retain.
ex: If I want to apply destroy policy to all my cfnresources
import { CfnResource, IAspect, IConstruct, RemovalPolicy } from '#aws-cdk/core';
apply_destroy_removal_policy_aspect
export class ApplyDestroyRemovalPolicyAspect implements IAspect {
public visit(construct: IConstruct): void {
if (CfnResource.isCfnResource(construct)) {
construct.applyRemovalPolicy(RemovalPolicy.DESTROY);
}
}
}
and apply it on all the resources by a stack
const destroyResourcesAspect = new ApplyDestroyRemovalPolicyAspect();
core.Aspects.of(safetyStack).add(destroyResourcesAspect));
This I've seen lot of people using to ensure it destroys any resources created as part of Personal Stacks in CDK.

How to deploy AWS CDK stacks to multiple accounts?

AWS CDK stacks target an account or region based on an evironment, details here. Here is an example of an app that deploys one stack into multiple target accounts:
const envEU = { account: '2383838383', region: 'eu-west-1' };
const envUSA = { account: '8373873873', region: 'us-west-2' };
new MyFirstStack(app, 'first-stack-eu', { env: envEU });
new MyFirstStack(app, 'first-stack-us', { env: envUSA });
My question is how to deploy these 2 stacks - is it possible to deploy them as a single operation? If so, what credentials are used and what roles are required on the 2 accounts?
Ideally, I'd like to be able to do a single command to deploy all stacks across all accounts:
cdk deploy ...
Or is the deployment only possible via 2 steps?
cdk deploy first-stack-eu --profile=profile_for_account_2383838383
cdk deploy first-stack-us --profile=profile_for_account_8373873873
I ended up using the cdk-assume-role-credential-plugin to perform the task. The description of that plugin states:
This plugin allows the CDK CLI to automatically obtain AWS credentials
from a stack's target AWS account. This means that you can run a
single command (i.e. cdk synth) with a set of AWS credentials, and the
CLI will determine the target AWS account for each stack and
automatically obtain temporary credentials for the target AWS account
by assuming a role in the account.
I wrote up a detailed tutorial on how to use this plugin to perform AWS cross-account deployments using CDK here: https://johntipper.org/aws-cdk-cross-account-deployments-with-cdk-pipelines-and-cdk-assume-role-credential-plugin/
In cloudformation you can use Stack Sets for multi-account and multi-region deployments.
However, this is not yet supported in CDK according to the GitHub issue:
Support for CloudFormation StackSets #66
As of v2 of CDK this is available by default:
Now by default when you bootstrap an AWS account it will create a set of IAM roles for you, which the CDK will assume when performing actions in that account.
If you have multiple stacks in your app you have to pass every stack into the cdk deploy command e.g. cdk deploy WmStackRouteCertStack004BE231 WmStackUploadStackF8C20A98
I don't know of a way to deploy all stacks in an app, I don't like this behavior and it's the reason I try to avoid creating multiple stacks