Skip to content

Commit 1c27a96

Browse files
authored
Use CloudFormation role (#324)
1 parent 3278214 commit 1c27a96

33 files changed

+820
-234
lines changed

main.tf

+107
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,111 @@ resource "aws_dynamodb_table" "deployments" {
8484
projection_type = "INCLUDE"
8585
non_key_attributes = ["CreateDate", "DeploymentAlias", "DeploymentId", "Status"]
8686
}
87+
88+
tags = var.tags
89+
}
90+
91+
#####################
92+
# CloudFormation Role
93+
#####################
94+
95+
# Policy that controls which actions can be performed when CloudFormation
96+
# creates a substack (from CDK)
97+
data "aws_iam_policy_document" "cloudformation_permission" {
98+
# Allow CloudFormation to publish status changes to the SNS queue
99+
statement {
100+
effect = "Allow"
101+
actions = [
102+
"sns:Publish"
103+
]
104+
resources = [module.deploy_controller.sns_topic_arn]
105+
}
106+
107+
# Allow CloudFormation to access the lambda content
108+
statement {
109+
effect = "Allow"
110+
actions = [
111+
"s3:GetObject"
112+
]
113+
resources = [
114+
module.statics_deploy.static_bucket_arn,
115+
"${module.statics_deploy.static_bucket_arn}/*"
116+
]
117+
}
118+
119+
# Stack creation
120+
statement {
121+
effect = "Allow"
122+
actions = [
123+
# TODO: Restrict the API Gateway action more
124+
"apigateway:*",
125+
"iam:CreateRole",
126+
"iam:GetRole",
127+
"iam:GetRolePolicy",
128+
"iam:PassRole",
129+
"iam:PutRolePolicy",
130+
"iam:TagRole",
131+
"lambda:AddPermission",
132+
"lambda:CreateFunction",
133+
"lambda:CreateFunctionUrlConfig",
134+
"lambda:GetFunctionUrlConfig",
135+
"lambda:GetFunction",
136+
"lambda:TagResource",
137+
"logs:CreateLogGroup",
138+
"logs:PutRetentionPolicy",
139+
"logs:TagLogGroup"
140+
]
141+
resources = ["*"]
142+
}
143+
144+
# Stack deletion
145+
statement {
146+
effect = "Allow"
147+
actions = [
148+
"apigateway:*",
149+
"iam:DeleteRole",
150+
"iam:DeleteRolePolicy",
151+
"iam:UntagRole",
152+
"lambda:DeleteFunction",
153+
"lambda:DeleteFunctionUrlConfig",
154+
"lambda:RemovePermission",
155+
"lambda:UntagResource",
156+
"logs:DeleteLogGroup",
157+
"logs:DeleteRetentionPolicy",
158+
"logs:UntagLogGroup"
159+
]
160+
resources = ["*"]
161+
}
162+
}
163+
164+
data "aws_iam_policy_document" "cloudformation_permission_assume_role" {
165+
statement {
166+
effect = "Allow"
167+
actions = ["sts:AssumeRole"]
168+
169+
principals {
170+
type = "Service"
171+
identifiers = ["cloudformation.amazonaws.com"]
172+
}
173+
}
174+
}
175+
176+
resource "aws_iam_policy" "cloudformation_permission" {
177+
name = "cloudformation-control"
178+
path = "/${var.deployment_name}/"
179+
description = "Managed by Terraform Next.js"
180+
policy = data.aws_iam_policy_document.cloudformation_permission.json
181+
182+
tags = var.tags
183+
}
184+
185+
resource "aws_iam_role" "cloudformation_permission" {
186+
name = "cloudformation-control"
187+
path = "/${var.deployment_name}/"
188+
assume_role_policy = data.aws_iam_policy_document.cloudformation_permission_assume_role.json
189+
managed_policy_arns = [
190+
aws_iam_policy.cloudformation_permission.arn
191+
]
87192
}
88193

89194
###################
@@ -125,6 +230,8 @@ module "statics_deploy" {
125230
dynamodb_table_deployments_arn = aws_dynamodb_table.deployments.arn
126231
dynamodb_table_deployments_name = aws_dynamodb_table.deployments.id
127232

233+
cloudformation_role_arn = aws_iam_role.cloudformation_permission.arn
234+
128235
lambda_role_permissions_boundary = var.lambda_role_permissions_boundary
129236

130237
deployment_name = var.deployment_name

modules/api/main.tf

+15-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@ data "aws_iam_policy_document" "access_upload_bucket" {
3535
}
3636
}
3737

38+
# Initiate deletion of CloudFormation stacks
39+
data "aws_iam_policy_document" "delete_cloudformation_stack" {
40+
statement {
41+
effect = "Allow"
42+
actions = [
43+
"cloudformation:DeleteStack"
44+
]
45+
resources = [
46+
"arn:aws:cloudformation:*:*:stack/*/*"
47+
]
48+
}
49+
}
50+
3851
module "lambda" {
3952
source = "../lambda-worker"
4053

@@ -49,10 +62,11 @@ module "lambda" {
4962
memory_size = 128
5063

5164
attach_policy_jsons = true
52-
number_of_policy_jsons = 2
65+
number_of_policy_jsons = 3
5366
policy_jsons = [
5467
data.aws_iam_policy_document.access_dynamodb_tables.json,
5568
data.aws_iam_policy_document.access_upload_bucket.json,
69+
data.aws_iam_policy_document.delete_cloudformation_stack.json,
5670
]
5771

5872
environment_variables = {

modules/deploy-controller/main.tf

+2-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ data "aws_iam_policy_document" "access_dynamodb_tables" {
3636
"dynamodb:GetItem",
3737
"dynamodb:PutItem",
3838
"dynamodb:Query",
39-
"dynamodb:UpdateItem"
39+
"dynamodb:UpdateItem",
40+
"dynamodb:DeleteItem"
4041
]
4142
resources = [
4243
var.dynamodb_table_deployments_arn,

modules/statics-deploy/main.tf

+13-38
Original file line numberDiff line numberDiff line change
@@ -112,46 +112,20 @@ data "aws_iam_policy_document" "access_static_deploy" {
112112
resources = [var.cloudfront_arn]
113113
}
114114

115-
# Permissions for CloudFormation to create resources
115+
# Create new substacks from CDK templates
116116
statement {
117117
actions = [
118-
"apigateway:*",
119-
"cloudformation:CreateStack",
120-
"iam:CreateRole",
121-
"iam:DeleteRole",
122-
"iam:DeleteRolePolicy",
123-
"iam:GetRole",
124-
"iam:GetRolePolicy",
125-
"iam:PassRole",
126-
"iam:PutRolePolicy",
127-
"iam:TagRole",
128-
"iam:UntagRole",
129-
"lambda:AddPermission",
130-
"lambda:CreateFunction",
131-
"lambda:DeleteFunction",
132-
"lambda:CreateFunctionUrlConfig",
133-
"lambda:GetFunctionUrlConfig",
134-
"lambda:GetFunction",
135-
"lambda:RemovePermission",
136-
"lambda:TagResource",
137-
"lambda:UntagResource",
138-
"logs:CreateLogGroup",
139-
"logs:DeleteLogGroup",
140-
"logs:DeleteRetentionPolicy",
141-
"logs:PutRetentionPolicy",
142-
"logs:TagLogGroup",
143-
"logs:UntagLogGroup",
118+
"cloudformation:CreateStack"
144119
]
145120
resources = ["*"]
146121
}
147122

148-
# Allow CloudFormation to publish status changes to the SNS queue
123+
# Allow to pass the cloudfront role to the cloudformation stack
149124
statement {
150-
effect = "Allow"
151125
actions = [
152-
"sns:Publish"
126+
"iam:PassRole"
153127
]
154-
resources = [var.deploy_status_sns_topic_arn]
128+
resources = [var.cloudformation_role_arn]
155129
}
156130
}
157131

@@ -254,13 +228,14 @@ module "deploy_trigger" {
254228
]
255229

256230
environment_variables = {
257-
NODE_ENV = "production"
258-
TARGET_BUCKET = aws_s3_bucket.static_deploy.id
259-
DISTRIBUTION_ID = var.cloudfront_id
260-
SQS_QUEUE_URL = aws_sqs_queue.this.id
261-
DEPLOY_STATUS_SNS_ARN = var.deploy_status_sns_topic_arn
262-
TABLE_REGION = var.dynamodb_region
263-
TABLE_NAME_DEPLOYMENTS = var.dynamodb_table_deployments_name
231+
NODE_ENV = "production"
232+
TARGET_BUCKET = aws_s3_bucket.static_deploy.id
233+
DISTRIBUTION_ID = var.cloudfront_id
234+
SQS_QUEUE_URL = aws_sqs_queue.this.id
235+
DEPLOY_STATUS_SNS_ARN = var.deploy_status_sns_topic_arn
236+
TABLE_REGION = var.dynamodb_region
237+
TABLE_NAME_DEPLOYMENTS = var.dynamodb_table_deployments_name
238+
CLOUDFORMATION_ROLE_ARN = var.cloudformation_role_arn
264239
}
265240

266241
event_source_mapping = {

modules/statics-deploy/variables.tf

+9
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ variable "lambda_role_permissions_boundary" {
1818
default = null
1919
}
2020

21+
################
22+
# CloudFormation
23+
################
24+
25+
variable "cloudformation_role_arn" {
26+
description = "Role ARN that should be assigned to the CloudFormation substacks created by CDK."
27+
type = string
28+
}
29+
2130
#####################
2231
# Deployment database
2332
#####################

packages/api/schema.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,13 @@ export interface paths {
125125
};
126126
};
127127
responses: {
128-
/** Successful response. */
128+
/** Deletion successfully requested. */
129+
200: {
130+
content: {
131+
'application/json': components['schemas']['Deployment'];
132+
};
133+
};
134+
/** Successful deletion. */
129135
204: never;
130136
400: components['responses']['InvalidParamsError'];
131137
};
@@ -156,7 +162,8 @@ export interface components {
156162
| 'CREATE_FAILED'
157163
| 'FINISHED'
158164
| 'DESTROY_IN_PROGRESS'
159-
| 'DESTROY_FAILED';
165+
| 'DESTROY_FAILED'
166+
| 'DESTROY_REQUESTED';
160167
DeploymentInitialized: {
161168
id: string;
162169
status: components['schemas']['DeploymentStatus'];

packages/api/schema.yaml

+8-1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ components:
6363
- FINISHED
6464
- DESTROY_IN_PROGRESS
6565
- DESTROY_FAILED
66+
- DESTROY_REQUESTED
6667

6768
DeploymentInitialized:
6869
type: object
@@ -271,7 +272,13 @@ paths:
271272
required: true
272273
description: The id of the deployment to delete.
273274
responses:
275+
'200':
276+
description: Deletion successfully requested.
277+
content:
278+
application/json:
279+
schema:
280+
$ref: '#/components/schemas/Deployment'
274281
'204':
275-
description: Successful response.
282+
description: Successful deletion.
276283
'400':
277284
$ref: '#/components/responses/InvalidParamsError'

packages/api/src/actions/deployment/delete-deployment-by-id.ts

+12-6
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ import {
55
getDeploymentById,
66
deleteDeploymentById as dynamoDBdeleteDeploymentById,
77
deleteAliasById,
8-
updateDeploymentStatusDestroyInProgress,
8+
updateDeploymentStatusDestroyRequested,
99
} from '@millihq/tfn-dynamodb-actions';
1010

1111
import { paths } from '../../../schema';
1212
import { DynamoDBServiceType } from '../../services/dynamodb';
1313
import { CloudFormationServiceType } from '../../services/cloudformation';
14+
import { deploymentDefaultSerializer } from '../../serializers/deployment';
1415

16+
type SuccessResponse =
17+
paths['/deployments/{deploymentId}']['delete']['responses']['200']['content']['application/json'];
1518
type ErrorResponse =
1619
paths['/deployments/{deploymentId}']['delete']['responses']['400']['content']['application/json'];
1720

@@ -24,7 +27,7 @@ const RESPONSE_DEPLOYMENT_DELETION_FAILED: ErrorResponse = {
2427
async function deleteDeploymentById(
2528
req: Request,
2629
res: Response
27-
): Promise<void> {
30+
): Promise<SuccessResponse | void> {
2831
const cloudFormationService = req.namespace
2932
.cloudFormation as CloudFormationServiceType;
3033
const dynamoDB = req.namespace.dynamoDB as DynamoDBServiceType;
@@ -122,18 +125,20 @@ async function deleteDeploymentById(
122125

123126
// Trigger stack deletion
124127
if (deployment.CFStack) {
125-
await cloudFormationService.deleteStack(deployment.CFStack);
126-
127-
await updateDeploymentStatusDestroyInProgress({
128+
const updatedDeployment = await updateDeploymentStatusDestroyRequested({
128129
dynamoDBClient: dynamoDB.getDynamoDBClient(),
129130
deploymentTableName: dynamoDB.getDeploymentTableName(),
130131
deploymentId: {
131132
PK: deployment.PK,
132133
SK: deployment.SK,
133134
},
134135
});
136+
await cloudFormationService.deleteStack(deployment.CFStack);
135137

136-
return res.sendStatus(204);
138+
// Deployment status is updated by deployment-controller when
139+
// CloudFormation triggers a `DELETE_IN_PROGRESS` event on the stack
140+
res.status(200);
141+
return deploymentDefaultSerializer(updatedDeployment);
137142
}
138143

139144
// No CloudFormation Stack present, so we can delete it from the database
@@ -152,6 +157,7 @@ async function deleteDeploymentById(
152157

153158
return res.sendStatus(204);
154159

160+
case 'DESTROY_REQUESTED':
155161
case 'DESTROY_IN_PROGRESS': {
156162
const errorResponse: ErrorResponse = {
157163
code: 'DEPLOYMENT_DESTROY_IN_PROGRESS',

0 commit comments

Comments
 (0)