Open schlarpc opened 3 years ago
Stacksets could also be used to capture Lambda@Edge log groups in all regions. Not sure yet how to get a list of regions to feed into StackInstances.Regions
, or how we'd deal with opt-in regions or regions launched between deployments of the stack.
I've added a stackset to capture Lambda@Edge logs in all regions.
I'm not sure that adding cross-region capabilities for the rest of the template is worth the effort though. Most of the services in use - CloudFront (+ Lambda@Edge), Route 53, and ACM - have to be configured in the partition's primary region. If deployed in another region, only S3 (content storage + log destination), CloudWatch metrics, CloudWatch Logs, and Lambda (for log ingestion from S3) would be homed in that region. This could be significant for someone who has a crappy connection to us-east-1 and needs to upload a lot of content, or has unique data sovereignty needs, but that isn't me.
If this ever gets done, the CloudFront log bucket can't live in an opt-in region, so we'll need logic to handle that:
Don’t choose an Amazon S3 bucket in any of the following Regions, because CloudFront doesn’t deliver access logs to buckets in these Regions:
Both challenges are solvable by using SSM documents to define the CFN template. This allows persisting the certificate ID into an SSM document in us-east-1, then instantiating that document as a nested stack in the origin region. No additional deployment artifacts are needed and the template is fairly clean, all considered.
Proof of concept:
from troposphere import (
Template,
Join,
Region,
Partition,
AccountId,
Sub,
Output,
Parameter,
Select,
Split,
StackId,
StackName,
)
from troposphere.ssm import Document
from troposphere.cloudformation import (
Stack,
StackSet,
DeploymentTargets,
StackInstances,
Parameter as StackSetParameter,
OperationPreferences,
WaitConditionHandle,
)
from troposphere.iam import Role, PolicyType, Policy
from awacs.aws import Statement, Principal, PolicyDocument, Allow, Action
from awacs import sts
import json
def create_passback_stack():
"""
The intrinsic functions in this template get evaluated in the remote region, at the deploy time
of create_remote_stack. By the time this is instantiated in the origin region, the values have
been resolved to primitives.
"""
template = Template()
template.add_resource(WaitConditionHandle("X"))
template.add_output(Output("Value", Value=Region))
return template
def create_remote_stack():
"""
In a real scenario, this stack would do something useful, like create an ACM certificate in
the remote region, then use the SSM document to pass back values to the origin region.
"""
template = Template()
document_name = template.add_parameter(Parameter("DocumentName", Type="String"))
document = template.add_resource(
Document(
"Document",
Name=document_name.ref(),
DocumentType="CloudFormation",
UpdateMethod="NewVersion",
Content={
"schemaVersion": "1.0",
"templateBody": json.loads(create_passback_stack().to_json()),
},
)
)
return template
def create_template():
"""
Most of this is stacksets boilerplate. At the bottom is a nested stack using an SSM
document stored in the remote region, which allows us to pull the resolved values from
the remote stack back into the origin region.
"""
template = Template()
stack_set_administration_role = template.add_resource(
Role(
"StackSetAdministrationRole",
AssumeRolePolicyDocument=PolicyDocument(
Version="2012-10-17",
Statement=[
Statement(
Effect=Allow,
Principal=Principal("Service", "cloudformation.amazonaws.com"),
Action=[sts.AssumeRole],
),
],
),
)
)
stack_set_execution_role = template.add_resource(
Role(
"StackSetExecutionRole",
AssumeRolePolicyDocument=PolicyDocument(
Version="2012-10-17",
Statement=[
Statement(
Effect=Allow,
Principal=Principal(
"AWS", stack_set_administration_role.get_att("Arn")
),
Action=[sts.AssumeRole],
),
],
),
Policies=[
Policy(
PolicyName="create-stackset-instances",
PolicyDocument=PolicyDocument(
Version="2012-10-17",
Statement=[
Statement(
Effect=Allow,
Action=[Action("*")],
Resource=["*"],
),
],
),
),
],
)
)
stack_set_administration_role_policy = template.add_resource(
PolicyType(
"StackSetAdministrationRolePolicy",
PolicyName="assume-execution-role",
PolicyDocument=PolicyDocument(
Version="2012-10-17",
Statement=[
Statement(
Effect=Allow,
Action=[sts.AssumeRole],
Resource=[stack_set_execution_role.get_att("Arn")],
),
],
),
Roles=[stack_set_administration_role.ref()],
)
)
stack_uuid = Select(2, Split("/", StackId))
stack_uuid_partial = Select(4, Split("-", stack_uuid))
remote_document_name = Join("-", [StackName, stack_uuid_partial, "Template"])
remote_region = "us-east-2"
stack_set = template.add_resource(
StackSet(
"StackSet",
Capabilities=["CAPABILITY_IAM"],
ManagedExecution={"Active": True},
Parameters=[
StackSetParameter(
ParameterKey="DocumentName",
ParameterValue=remote_document_name,
),
],
OperationPreferences=OperationPreferences(
FailureToleranceCount=0,
MaxConcurrentPercentage=100,
RegionConcurrencyType="PARALLEL",
),
AdministrationRoleARN=stack_set_administration_role.get_att("Arn"),
ExecutionRoleName=stack_set_execution_role.ref(),
StackInstancesGroup=[
StackInstances(
DeploymentTargets=DeploymentTargets(
Accounts=[AccountId],
),
Regions=[remote_region],
),
],
PermissionModel="SELF_MANAGED",
StackSetName=Join("-", [StackName, stack_uuid_partial, "StackSet"]),
TemplateBody=create_remote_stack().to_json(
sort_keys=True, indent=None, separators=(",", ":")
),
DependsOn=[stack_set_administration_role_policy],
)
)
stack = template.add_resource(
Stack(
"Stack",
TemplateURL=Join(
"",
[
"ssm-doc://",
Join(
":",
[
"arn",
Partition,
"ssm",
remote_region,
AccountId,
Join("/", ["document", remote_document_name]),
],
),
],
),
DependsOn=[stack_set],
)
)
template.add_output(Output("Value", Value=stack.get_att("Outputs.Value")))
return template
if __name__ == "__main__":
print(create_template().to_json())
With the introduction of the
AWS::CloudFormation::StackSet
resource, it should now be possible to delegate only the ACM certificate creation tous-east-1
as a separate stack: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-stackset.htmlThis will allow the S3 bucket (and logs, etc) to live in a region other than us-east-1.
There are two primary challenges:
TemplateBody
from the currently executing stack usingAWS::StackId
in a custom resource. This is disgusting and I love it.StackSet
resource doesn't give you access to theOutput
s of the child stacks. Since the certificate ID contains a UUID, we need to hoist it back up into the parent stack somehow.String
parameter to the child stackset.AWS::SSM::Parameter::Value<String>
passed immediately back into anOutput
, allowing it to be retrieved in the parent stack withGetAtt
.