├── .gitignore ├── Chapter01 ├── CreatingYourFirstStack │ ├── README.md │ ├── my_bucket.yaml │ └── my_bucket_with_public_read.yaml ├── DriftDetection │ ├── README.md │ └── iam_role.yaml └── UnderstandingCloudFormationIamPermissions │ ├── README.md │ ├── cfn_iam_role.yaml │ ├── iam_role.yaml │ └── my_bucket.yaml ├── Chapter02 ├── core.yaml ├── database.yaml ├── middleware.yaml ├── production.json ├── testing.json └── webtier.yaml ├── Chapter03 ├── README.md ├── cfn_guard_rules │ └── subnets_have_tags.guard ├── core_broken.yaml ├── core_drift.yaml ├── core_full.yaml ├── core_missing_tag.yaml ├── core_partial.yaml ├── custom_rules │ └── rdsdeletionpolicy.py ├── database_failing.yaml └── testing.json ├── Chapter04 ├── README.md ├── asg_test.py ├── broken_asg.yaml ├── cfn_source │ ├── buildspec.yml │ ├── core.yaml │ └── tests │ │ └── core_subnets.py ├── cicd.yaml ├── core_compliant.yaml ├── core_non_compliant.yaml ├── core_subnets.py └── working_asg.yaml ├── Chapter05 ├── README.md ├── multi-account │ ├── StackSetAdmin.yaml │ ├── StackSetExec.yaml │ └── core.yaml ├── multi-region │ ├── StackSetPermissions.yaml │ └── core.yaml └── target-account-gate │ ├── tag.py │ ├── tag.yaml │ └── webtier.yaml ├── Chapter06 ├── README.md ├── hello-world │ ├── hello-world-app.yaml │ ├── hello-world-flask.py │ └── hello-world-prep.yaml └── lnmp │ ├── lnmp-signal.yaml │ └── lnmp.yaml ├── Chapter07 ├── README.md ├── cr.yaml ├── custom-db │ ├── customdb.py │ └── requirements.txt ├── customdb.yaml ├── customdb_missing_property.yaml └── rds.yaml ├── Chapter08 ├── README.md ├── dynamodb-item │ ├── item.yaml │ └── prerequisite.yaml └── private-database-registry │ ├── database-resource-type │ ├── .gitignore │ ├── .rpdk-config │ ├── README.md │ ├── docs │ │ └── README.md │ ├── inputs │ │ ├── inputs_1_create.json │ │ ├── inputs_1_invalid.json │ │ └── inputs_1_update.json │ ├── org-storage-database.json │ ├── requirements.txt │ ├── resource-role.yaml │ ├── src │ │ └── org_storage_database │ │ │ ├── __init__.py │ │ │ ├── dbclient.py │ │ │ ├── handlers.py │ │ │ └── models.py │ └── template.yml │ ├── database.yaml │ └── rds.yaml ├── Chapter09 ├── README.md ├── macros │ ├── ami-filler │ │ ├── amifinder.py │ │ ├── lt.yaml │ │ └── macro.yaml │ └── standard-app │ │ ├── app.yaml │ │ ├── core.yaml │ │ ├── macro.yaml │ │ └── standard_app.py └── modules │ ├── core.yaml │ ├── module │ ├── .rpdk-config │ ├── fragments │ │ └── template.yaml │ └── schema.json │ └── standard_app.yaml ├── Chapter10 └── app │ ├── .gitignore │ ├── README.md │ ├── app.py │ ├── app │ ├── __init__.py │ ├── app_stack.py │ ├── core_stack.py │ ├── rds_stack.py │ └── web_stack.py │ ├── cdk.json │ ├── requirements-dev.txt │ ├── requirements.txt │ ├── source.bat │ └── tests │ ├── __init__.py │ └── unit │ ├── __init__.py │ └── test_app_stack.py ├── Chapter11 ├── README.md ├── hello-world │ ├── README.md │ ├── __init__.py │ ├── events │ │ └── event.json │ ├── hello_world │ │ ├── __init__.py │ │ ├── app.py │ │ └── requirements.txt │ ├── template.yaml │ └── tests │ │ ├── __init__.py │ │ └── unit │ │ ├── __init__.py │ │ └── test_handler.py └── party-planner │ ├── events │ └── event.json │ ├── registration │ ├── __init__.py │ ├── app.py │ └── requirements.txt │ ├── reporting │ ├── __init__.py │ ├── app.py │ └── requirements.txt │ └── template.yaml ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | # registry artifacts 163 | *.zip -------------------------------------------------------------------------------- /Chapter01/CreatingYourFirstStack/README.md: -------------------------------------------------------------------------------- 1 | # Creating the stack 2 | 3 | Make sure you are in this directory and run the following command. 4 | > Note that if you have multiple profiles like me, you want to add --profile $PROFILE_NAME at the end of your command 5 | ```bash 6 | aws cloudformation create-stack --stack-name mybucket --template-body file://my_bucket.yaml 7 | ``` 8 | You will see the `stackId` of your CloudFormation stack. 9 | After a while your bucket will be created and you can verify it by running the following command 10 | ```bash 11 | aws s3 ls 12 | ``` 13 | 14 | #Updating the stack 15 | 16 | Run the following command to update your stack and add the PublicAccess to the bucket. Note that name of the template is now different. 17 | ```bash 18 | aws cloudformation update-stack --stack-name mybucket --template-body file://my_bucket_with_public_read.yaml 19 | ``` 20 | You will receive the same `stackId` in the output. 21 | You can review the status of the update operation by running `describe-stacks` command 22 | ```bash 23 | aws cloudformation describe-stacks --stack-name mybucket 24 | ``` 25 | You should see the following line in your JSON response: 26 | ```json 27 | { 28 | "Stacks": [ 29 | { 30 | // ... 31 | "StackStatus": "UPDATE_COMPLETE" 32 | // ... 33 | } 34 | ] 35 | } 36 | ``` 37 | You can check if the changes were applied to your bucket by running the following: 38 | ```bash 39 | aws s3api get-bucket-acl --bucket $YOUR_BUCKET_NAME 40 | ``` 41 | You will see the following block in the output: 42 | ```json 43 | { 44 | "Grantee": { 45 | "Type": "Group", 46 | "URI": "http://acs.amazonaws.com/groups/global/AllUsers" 47 | }, 48 | "Permission": "READ" 49 | } 50 | ``` -------------------------------------------------------------------------------- /Chapter01/CreatingYourFirstStack/my_bucket.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: This is my first bucket 3 | Resources: 4 | MyBucket: 5 | Type: "AWS::S3::Bucket" 6 | -------------------------------------------------------------------------------- /Chapter01/CreatingYourFirstStack/my_bucket_with_public_read.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: This is my first bucket 3 | Resources: 4 | MyBucket: 5 | Type: "AWS::S3::Bucket" 6 | Properties: 7 | AccessControl: PublicRead -------------------------------------------------------------------------------- /Chapter01/DriftDetection/README.md: -------------------------------------------------------------------------------- 1 | # Drift detection 2 | 3 | Make sure you are in this directory and run the following command. 4 | > Note that if you have multiple profiles like me, you want to add --profile $PROFILE_NAME at the end of your command 5 | 6 | We start with the stack creation 7 | ```bash 8 | aws cloudformation create-stack --stack-name iamrole --template-body file://iam_role.yaml --capabilities CAPABILITY_IAM 9 | ``` 10 | Now when we've reviewed that our stack is in sync, we can manually edit our role 11 | ```bash 12 | ROLENAME=$(aws cloudformation describe-stack-resources --stack-name iamrole --query "StackResources[0].PhysicalResourceId" --output text) 13 | aws iam attach-role-policy --role-name $ROLENAME --policy-arn "arn:aws:iam::aws:policy/AdministratorAccess" 14 | ``` 15 | When we run drift detection again we will find modification. 16 | > You will not be able to delete that stack until you detach that policy from your new role 17 | ```bash 18 | aws iam detach-role-policy --role-name $ROLENAME --policy-arn "arn:aws:iam::aws:policy/AdministratorAccess" 19 | aws cloudformation delete-stack --stack-name iamrole 20 | ``` -------------------------------------------------------------------------------- /Chapter01/DriftDetection/iam_role.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: "This is a dummy role" 3 | Resources: 4 | IamRole: 5 | Type: AWS::IAM::Role 6 | Properties: 7 | AssumeRolePolicyDocument: 8 | Version: 2012-10-17 9 | Statement: 10 | - Sid: AllowAssumeRole 11 | Effect: Allow 12 | Principal: 13 | AWS: 14 | - !Join 15 | - "" 16 | - - "arn:aws:iam::" 17 | - !Ref "AWS::AccountId" 18 | - ":user/Admin" 19 | Action: "sts:AssumeRole" 20 | ManagedPolicyArns: 21 | - "arn:aws:iam::aws:policy/AmazonEC2FullAccess" 22 | - "arn:aws:iam::aws:policy/AWSCloudFormationFullAccess" 23 | Outputs: 24 | IamRole: 25 | Value: !GetAtt IamRole.Arn -------------------------------------------------------------------------------- /Chapter01/UnderstandingCloudFormationIamPermissions/README.md: -------------------------------------------------------------------------------- 1 | # Understanding CloudFormation IAM permissions 2 | 3 | Make sure you are in this directory and run the following command. 4 | > Note that if you have multiple profiles like me, you want to add --profile $PROFILE_NAME at the end of your command 5 | 6 | We begin with creating our dummy role without necessary permissions. 7 | ```bash 8 | aws cloudformation create-stack \ 9 | --stack-name iamrole \ 10 | --capabilities CAPABILITY_IAM \ 11 | --template-body file://iam_role.yaml 12 | ``` 13 | We obtain STS credentials for that role 14 | ```bash 15 | IAM_ROLE_ARN=$(aws cloudformation describe-stacks \ 16 | --stack-name iamrole \ 17 | --profile personal \ 18 | --query "Stacks[0].Outputs[?OutputKey=='IamRole'].OutputValue" \ 19 | --output text) 20 | aws sts assume-role --role-arn $IAM_ROLE_ARN \ 21 | --role-session-name tmp 22 | ``` 23 | We will use these short-term credentials to invoke creation of our Bucket stack 24 | ```bash 25 | export AWS_ACCESS_KEY_ID=… 26 | export AWS_SECRET_ACCESS_KEY=… 27 | export AWS_SESSION_TOKEN=… 28 | aws cloudformation create-stack \ 29 | --stack-name mybucket \ 30 | --template-body file://my_bucket.yaml 31 | ``` 32 | We will see that our stack creation failed. Let's create a new role for CloudFormation 33 | ```bash 34 | aws cloudformation create-stack \ 35 | --stack-name cfniamrole \ 36 | --capabilities CAPABILITY_IAM \ 37 | --template-body file://cfn_iam_role.yaml 38 | ``` 39 | And use this role to create our stack. 40 | ```bash 41 | IAM_ROLE_ARN=$(aws cloudformation describe-stacks \ 42 | --stack-name cfniamrole \ 43 | --query "Stacks[0].Outputs[?OutputKey=='IamRole'].OutputValue" \ 44 | --output text) 45 | aws cloudformation create-stack \ 46 | --stack-name mybucket \ 47 | --template-body file://my_bucket.yaml \ 48 | --role-arn $IAM_ROLE_ARN 49 | ``` 50 | And validate that our bucket is created. 51 | ```bash 52 | aws s3 ls 53 | ``` 54 | Don't forget to clean up your stacks: 55 | ```bash 56 | for i in mybucket iamrole cfniamrole; do aws cloudformation delete-stack --stack-name $i ; done 57 | ``` -------------------------------------------------------------------------------- /Chapter01/UnderstandingCloudFormationIamPermissions/cfn_iam_role.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: "This is a CFN role" 3 | Resources: 4 | IamRole: 5 | Type: AWS::IAM::Role 6 | Properties: 7 | AssumeRolePolicyDocument: 8 | Version: 2012-10-17 9 | Statement: 10 | - Sid: AllowAssumeRole 11 | Effect: Allow 12 | Principal: 13 | Service: "cloudformation.amazonaws.com" 14 | Action: "sts:AssumeRole" 15 | ManagedPolicyArns: 16 | - "arn:aws:iam::aws:policy/AdministratorAccess" 17 | Outputs: 18 | IamRole: 19 | Value: !GetAtt IamRole.Arn -------------------------------------------------------------------------------- /Chapter01/UnderstandingCloudFormationIamPermissions/iam_role.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: "This is a dummy role" 3 | Resources: 4 | IamRole: 5 | Type: AWS::IAM::Role 6 | Properties: 7 | AssumeRolePolicyDocument: 8 | Version: 2012-10-17 9 | Statement: 10 | - Sid: AllowAssumeRole 11 | Effect: Allow 12 | Principal: 13 | AWS: 14 | - !Ref "AWS::AccountId" 15 | Action: "sts:AssumeRole" 16 | ManagedPolicyArns: 17 | - "arn:aws:iam::aws:policy/AmazonEC2FullAccess" 18 | - "arn:aws:iam::aws:policy/AWSCloudFormationFullAccess" 19 | Outputs: 20 | IamRole: 21 | Value: !GetAtt IamRole.Arn -------------------------------------------------------------------------------- /Chapter01/UnderstandingCloudFormationIamPermissions/my_bucket.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: This is my first bucket 3 | Resources: 4 | MyBucket: 5 | Type: "AWS::S3::Bucket" 6 | -------------------------------------------------------------------------------- /Chapter02/database.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::LanguageExtensions 3 | Description: Database template 4 | 5 | Parameters: 6 | Environment: 7 | Type: String 8 | Default: test 9 | AllowedValues: [ "test", "prod" ] 10 | 11 | Conditions: 12 | ProdEnv: !Equals [!Ref Environment, "prod"] 13 | TestEnv: !Equals [!Ref Environment, "test"] 14 | 15 | Resources: 16 | 17 | HardcodedDatabaseSecret: 18 | Condition: TestEnv 19 | Type: "AWS::SecretsManager::Secret" 20 | Properties: 21 | Description: "Database credentials" 22 | SecretString: 23 | Fn::ToJsonString: 24 | username: "dbuser" 25 | password: "dbpassword" # Do not do this in production! 26 | Tags: 27 | - Key: "Env" 28 | Value: !Ref Environment 29 | - Key: "Name" 30 | Value: !Sub "${Environment}-${AWS::StackName}-secret" 31 | 32 | GeneratedDatabaseSecret: 33 | Condition: ProdEnv 34 | Type: "AWS::SecretsManager::Secret" 35 | Properties: 36 | Description: "Database credentials" 37 | GenerateSecretString: 38 | SecretStringTemplate: 39 | Fn::ToJsonString: 40 | username: "dbuser" 41 | GenerateStringKey: "password" 42 | PasswordLength: 16 43 | ExcludeCharacters: '"@/\' 44 | Tags: 45 | - Key: "Env" 46 | Value: !Ref Environment 47 | - Key: "Name" 48 | Value: !Sub "${Environment}-${AWS::StackName}-secret" 49 | 50 | DatabaseSg: 51 | Type: "AWS::EC2::SecurityGroup" 52 | Properties: 53 | GroupDescription: Database security group 54 | VpcId: !ImportValue VpcId 55 | Tags: 56 | - Key: "Env" 57 | Value: !Ref Environment 58 | - Key: "Name" 59 | Value: !Sub "${Environment}-${AWS::StackName}-sg" 60 | 61 | Database: 62 | DeletionPolicy: !If [ProdEnv, "Retain", "Delete"] 63 | Type: "AWS::RDS::DBCluster" 64 | Properties: 65 | Engine: aurora 66 | EngineMode: serverless 67 | DBSubnetGroupName: !ImportValue DbSubnetGroupId 68 | ScalingConfiguration: 69 | AutoPause: True 70 | MaxCapacity: 1 71 | MinCapacity: 1 72 | SecondsUntilAutoPause: 300 73 | MasterUsername: 74 | Fn::If: 75 | - ProdEnv 76 | - !Sub "{{resolve:secretsmanager:${GeneratedDatabaseSecret}:SecretString:username}}" 77 | - !Sub "{{resolve:secretsmanager:${HardcodedDatabaseSecret}:SecretString:username}}" 78 | MasterUserPassword: 79 | Fn::If: 80 | - ProdEnv 81 | - !Sub "{{resolve:secretsmanager:${GeneratedDatabaseSecret}:SecretString:password}}" 82 | - !Sub "{{resolve:secretsmanager:${HardcodedDatabaseSecret}:SecretString:password}}" 83 | VpcSecurityGroupIds: 84 | - !Ref DatabaseSg 85 | Tags: 86 | - Key: "Env" 87 | Value: !Ref Environment 88 | - Key: "Name" 89 | Value: !Sub "${Environment}-${AWS::StackName}-cluster" 90 | 91 | DatabaseSgIngressRule: 92 | Type: "AWS::EC2::SecurityGroupIngress" 93 | Properties: 94 | IpProtocol: tcp 95 | FromPort: !GetAtt Database.Endpoint.Port 96 | ToPort: !GetAtt Database.Endpoint.Port 97 | SourceSecurityGroupId: !ImportValue MiddlewareInstanceSg 98 | GroupId: !Ref DatabaseSg 99 | 100 | Outputs: 101 | DatabaseEndpointAddress: 102 | Value: Database.Endpoint.Address 103 | Export: 104 | Name: DatabaseEndpointAddress 105 | 106 | DatabaseEndpointPort: 107 | Value: Database.Endpoint.Port 108 | Export: 109 | Name: DatabseEndpointPort 110 | 111 | DbCredentials: 112 | Value: !If [ProdEnv, GeneratedDatabaseSecret, HardcodedDatabaseSecret] 113 | Export: 114 | Name: DbCredentials -------------------------------------------------------------------------------- /Chapter02/middleware.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: Middleware template 3 | 4 | Parameters: 5 | Environment: 6 | Type: String 7 | Default: test 8 | AllowedValues: [ "test", "prod" ] 9 | ImageId: 10 | Type: AWS::SSM::Parameter::Value 11 | Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2 12 | KeyName: 13 | Type: AWS::EC2::KeyPair::KeyName 14 | Default: mykey 15 | 16 | Resources: 17 | 18 | MiddlewareLoadBalancer: 19 | Type: "AWS::ElasticLoadBalancingV2::LoadBalancer" 20 | Properties: 21 | Type: "application" 22 | Subnets: 23 | - !ImportValue MiddlewareSubnet1Id 24 | - !ImportValue MiddlewareSubnet2Id 25 | - !ImportValue MiddlewareSubnet3Id 26 | Scheme: "internal" 27 | SecurityGroups: 28 | - !Ref MiddlewareLbSg 29 | Tags: 30 | - Key: "Env" 31 | Value: !Ref Environment 32 | - Key: "Name" 33 | Value: !Sub "${Environment}-${AWS::StackName}-lb" 34 | 35 | MiddlewareInstanceLaunchTemplate: 36 | Type: "AWS::EC2::LaunchTemplate" 37 | Properties: 38 | LaunchTemplateData: 39 | ImageId: !Ref ImageId 40 | InstanceType: "t3.micro" 41 | KeyName: !Ref KeyName 42 | SecurityGroupIds: 43 | - !Ref MiddlewareInstanceSg 44 | TagSpecifications: 45 | - ResourceType: "instance" 46 | Tags: 47 | - Key: "Env" 48 | Value: !Ref Environment 49 | - Key: "Name" 50 | Value: !Sub "${Environment}-${AWS::StackName}-lt" 51 | 52 | MiddlewareLbSg: 53 | Type: "AWS::EC2::SecurityGroup" 54 | Properties: 55 | GroupDescription: "LoadBalancer Security Group" 56 | SecurityGroupIngress: 57 | - IpProtocol: "tcp" 58 | SourceSecurityGroupId: !ImportValue WebInstanceSg 59 | FromPort: 80 60 | ToPort: 80 61 | VpcId: !ImportValue VpcId 62 | Tags: 63 | - Key: "Env" 64 | Value: !Ref Environment 65 | - Key: "Name" 66 | Value: !Sub "${Environment}-${AWS::StackName}-lt-sg" 67 | 68 | MiddlewareInstanceSg: 69 | Type: "AWS::EC2::SecurityGroup" 70 | Properties: 71 | GroupDescription: "Middleware Instance SG" 72 | SecurityGroupIngress: 73 | - IpProtocol: "tcp" 74 | SourceSecurityGroupId: !Ref MiddlewareLbSg 75 | FromPort: 80 76 | ToPort: 80 77 | VpcId: !ImportValue VpcId 78 | Tags: 79 | - Key: "Env" 80 | Value: !Ref Environment 81 | - Key: "Name" 82 | Value: !Sub "${Environment}-${AWS::StackName}-inst-sg" 83 | 84 | MiddlewareAsg: 85 | DependsOn: MiddlewareLbListener 86 | Type: "AWS::AutoScaling::AutoScalingGroup" 87 | Properties: 88 | MaxSize: !Sub ["{{resolve:ssm:${parameter}:1}}", parameter: !ImportValue MiddlewareMaxSizeParameter] 89 | MinSize: !Sub ["{{resolve:ssm:${parameter}:1}}", parameter: !ImportValue MiddlewareMinSizeParameter] 90 | DesiredCapacity: !Sub ["{{resolve:ssm:${parameter}:1}}", parameter: !ImportValue MiddlewareDesSizeParameter] 91 | VPCZoneIdentifier: 92 | - !ImportValue MiddlewareSubnet1Id 93 | - !ImportValue MiddlewareSubnet2Id 94 | - !ImportValue MiddlewareSubnet3Id 95 | LaunchTemplate: 96 | LaunchTemplateId: !Ref MiddlewareInstanceLaunchTemplate 97 | Version: "1" 98 | TargetGroupARNs: 99 | - !Ref MiddlewareTg 100 | Tags: 101 | - Key: "Env" 102 | Value: !Ref Environment 103 | PropagateAtLaunch: True 104 | - Key: "Name" 105 | Value: !Sub "${Environment}-${AWS::StackName}-asg" 106 | PropagateAtLaunch: True 107 | 108 | MiddlewareLbListener: 109 | Type: "AWS::ElasticLoadBalancingV2::Listener" 110 | Properties: 111 | LoadBalancerArn: !Ref MiddlewareLoadBalancer 112 | Port: 80 113 | Protocol: "HTTP" 114 | DefaultActions: 115 | - Type: "forward" 116 | TargetGroupArn: !Ref MiddlewareTg 117 | 118 | MiddlewareTg: 119 | Type: "AWS::ElasticLoadBalancingV2::TargetGroup" 120 | Properties: 121 | Port: 80 122 | Protocol: "HTTP" 123 | VpcId: !ImportValue VpcId 124 | Tags: 125 | - Key: "Env" 126 | Value: !Ref Environment 127 | - Key: "Name" 128 | Value: !Sub "${Environment}-${AWS::StackName}-lb-tg" 129 | 130 | Outputs: 131 | MiddlewareInstanceSg: 132 | Value: !Ref MiddlewareInstanceSg 133 | Export: 134 | Name: MiddlewareInstanceSg -------------------------------------------------------------------------------- /Chapter02/production.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ParameterKey": "Environment", 4 | "ParameterValue": "prod" 5 | }, 6 | { 7 | "ParameterKey": "VpcCidr", 8 | "ParameterValue": "10.0.0.0/16" 9 | } 10 | ] -------------------------------------------------------------------------------- /Chapter02/testing.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ParameterKey": "Environment", 4 | "ParameterValue": "test" 5 | }, 6 | { 7 | "ParameterKey": "VpcCidr", 8 | "ParameterValue": "10.1.0.0/16" 9 | } 10 | ] -------------------------------------------------------------------------------- /Chapter02/webtier.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: WebTier template 3 | 4 | Parameters: 5 | Environment: 6 | Type: String 7 | Default: test 8 | AllowedValues: [ "test", "prod" ] 9 | ImageId: 10 | Type: AWS::SSM::Parameter::Value 11 | Default: "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2" 12 | KeyName: 13 | Type: AWS::EC2::KeyPair::KeyName 14 | Default: mykey 15 | 16 | Resources: 17 | 18 | WebTierLoadBalancer: 19 | Type: "AWS::ElasticLoadBalancingV2::LoadBalancer" 20 | Properties: 21 | Type: "application" 22 | Subnets: 23 | - !ImportValue PublicSubnet1Id 24 | - !ImportValue PublicSubnet2Id 25 | - !ImportValue PublicSubnet3Id 26 | Scheme: "internet-facing" 27 | SecurityGroups: 28 | - !Ref WebTierLbSg 29 | Tags: 30 | - Key: "Env" 31 | Value: !Ref Environment 32 | - Key: "Name" 33 | Value: !Sub "${Environment}-${AWS::StackName}-lb" 34 | 35 | WebInstanceLaunchTemplate: 36 | Type: "AWS::EC2::LaunchTemplate" 37 | Properties: 38 | LaunchTemplateData: 39 | ImageId: !Ref ImageId 40 | InstanceType: "t3.micro" 41 | KeyName: !Ref KeyName 42 | SecurityGroupIds: 43 | - !Ref WebInstanceSg 44 | TagSpecifications: 45 | - ResourceType: "instance" 46 | Tags: 47 | - Key: "Env" 48 | Value: !Ref Environment 49 | - Key: "Name" 50 | Value: !Sub "${Environment}-${AWS::StackName}-lt" 51 | 52 | WebTierLbSg: 53 | Type: "AWS::EC2::SecurityGroup" 54 | Properties: 55 | GroupDescription: "LoadBalancer Security Group" 56 | SecurityGroupIngress: 57 | - IpProtocol: "tcp" 58 | CidrIp: "0.0.0.0/0" 59 | FromPort: 80 60 | ToPort: 80 61 | VpcId: !ImportValue VpcId 62 | Tags: 63 | - Key: "Env" 64 | Value: !Ref Environment 65 | - Key: "Name" 66 | Value: !Sub "${Environment}-${AWS::StackName}-lb-sg" 67 | 68 | WebInstanceSg: 69 | Type: "AWS::EC2::SecurityGroup" 70 | Properties: 71 | GroupDescription: "WebTier Instance SG" 72 | SecurityGroupIngress: 73 | - IpProtocol: "tcp" 74 | SourceSecurityGroupId: !Ref WebTierLbSg 75 | FromPort: 80 76 | ToPort: 80 77 | VpcId: !ImportValue VpcId 78 | Tags: 79 | - Key: "Env" 80 | Value: !Ref Environment 81 | - Key: "Name" 82 | Value: !Sub "${Environment}-${AWS::StackName}-inst-sg" 83 | 84 | WebTierAsg: 85 | DependsOn: WebTierLbListener 86 | Type: "AWS::AutoScaling::AutoScalingGroup" 87 | Properties: 88 | MaxSize: !Sub [ "{{resolve:ssm:${parameter}:1}}", parameter: !ImportValue WebTierMaxSizeParameter ] 89 | MinSize: !Sub [ "{{resolve:ssm:${parameter}:1}}", parameter: !ImportValue WebTierMinSizeParameter ] 90 | DesiredCapacity: !Sub [ "{{resolve:ssm:${parameter}:1}}", parameter: !ImportValue WebTierDesSizeParameter ] 91 | VPCZoneIdentifier: !Split [ ",", !ImportValue PublicSubnetIds ] 92 | LaunchTemplate: 93 | LaunchTemplateId: !Ref WebInstanceLaunchTemplate 94 | Version: "1" 95 | TargetGroupARNs: 96 | - !Ref WebTierTg 97 | Tags: 98 | - Key: "Env" 99 | Value: !Ref Environment 100 | PropagateAtLaunch: True 101 | - Key: "Name" 102 | Value: !Sub "${Environment}-${AWS::StackName}-asg" 103 | PropagateAtLaunch: True 104 | 105 | WebTierLbListener: 106 | Type: "AWS::ElasticLoadBalancingV2::Listener" 107 | Properties: 108 | LoadBalancerArn: !Ref WebTierLoadBalancer 109 | Port: 80 110 | Protocol: "HTTP" 111 | DefaultActions: 112 | - Type: "forward" 113 | TargetGroupArn: !Ref WebTierTg 114 | 115 | WebTierTg: 116 | Type: "AWS::ElasticLoadBalancingV2::TargetGroup" 117 | Properties: 118 | Port: 80 119 | Protocol: "HTTP" 120 | VpcId: !ImportValue VpcId 121 | Tags: 122 | - Key: "Env" 123 | Value: !Ref Environment 124 | - Key: "Name" 125 | Value: !Sub "${Environment}-${AWS::StackName}-lb-tg" 126 | 127 | Outputs: 128 | WebInstanceSg: 129 | Value: !Ref WebTierLbSg 130 | Export: 131 | Name: WebInstanceSg -------------------------------------------------------------------------------- /Chapter03/README.md: -------------------------------------------------------------------------------- 1 | # Validation, linting and deployment of the stack 2 | `Since I have multiple AWS profiles, I will append my commands with --profile argument` 3 | ## Validation 4 | Validating broken template 5 | ```bash 6 | aws cloudformation validate-template --template-body file://core_broken.yaml 7 | ``` 8 | 9 | Validating valid template 10 | ```bash 11 | aws cloudformation validate-template --template-body file://core_full.yaml 12 | ``` 13 | 14 | ## Linting 15 | Running linter against broken template 16 | ```bash 17 | cfn-lint core_broken.yaml 18 | ``` 19 | Running linter against all regions 20 | ```bash 21 | cfn-lint core_full.yaml --regions 'ALL_REGIONS' 22 | ``` 23 | Running linter with custom rules 24 | ```bash 25 | cfn-lint database_failing.yaml -a custom_rules 26 | ``` 27 | 28 | ## Provisioning 29 | Using `create-stack` and `update-stack` 30 | ```bash 31 | aws cloudformation create-stack --stack-name core --template-body file://core_partial.yaml --parameters file://testing.json 32 | aws cloudformation update-stack --stack-name core --template-body file://core_full.yaml --parameters file://testing.json --capabilities CAPABILITY_IAM 33 | ``` 34 | 35 | Using change sets 36 | ```bash 37 | aws cloudformation create-stack --stack-name core --template-body file://core_partial.yaml --parameters file://testing.json 38 | aws cloudformation create-change-set --stack-name core --change-set-name our-change-set --template-body file://core_full.yaml --parameters file://testing.json --capabilities CAPABILITY_IAM 39 | aws cloudformation execute-change-set --change-set-name our-change-set --stack-name core 40 | ``` 41 | Using `deploy` 42 | ```bash 43 | aws cloudformation deploy --stack-name core --template-file core_partial.yaml --capabilities CAPABILITY_IAM --parameter-overrides VpcCidr="10.1.0.0/16" Environment="test" 44 | aws cloudformation deploy --stack-name core --template-file core_full.yaml --capabilities CAPABILITY_IAM --parameter-overrides VpcCidr="10.1.0.0/16" Environment="test" 45 | ``` 46 | 47 | ## Drifts 48 | Deploy the stack (if haven't before) 49 | ```bash 50 | aws cloudformation deploy --template-file core_full.yaml --stack-name core --parameter-overrides VpcCidr=10.1.0.0/16 Environment=test --capabilities CAPABILITY_IAM 51 | ``` 52 | Obtain IAM Role name 53 | ```bash 54 | aws cloudformation describe-stack-resource --stack-name core --logical-resource-id DevRole 55 | ``` 56 | Attach extra policy 57 | ```bash 58 | aws iam attach-role-policy --role-name ROLE_NAME --policy-arn arn:aws:iam::aws:policy/AmazonEC2FullAccess 59 | ``` 60 | Check the drift 61 | ```bash 62 | aws cloudformation detect-stack-resource-drift --stack-name core --logical-resource-id DevRole 63 | ``` 64 | Apply the managed change 65 | ```bash 66 | aws cloudformation deploy --template-file core_drift.yaml --stack-name core --parameter-overrides VpcCidr=10.1.0.0/16 Environment=test --capabilities CAPABILITY_IAM 67 | ``` -------------------------------------------------------------------------------- /Chapter03/cfn_guard_rules/subnets_have_tags.guard: -------------------------------------------------------------------------------- 1 | let subnets = Resources.*[ Type == "AWS::EC2::Subnet" ] 2 | 3 | rule no_hardcoded_cidr when %subnets !empty { 4 | %subnets.Properties.Tags[*] { 5 | Key in ["Env", "Name"] 6 | << 7 | The subnet is missing Env or Name tag key 8 | >> 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Chapter03/custom_rules/rdsdeletionpolicy.py: -------------------------------------------------------------------------------- 1 | from cfnlint import CloudFormationLintRule # pip install cfnlint 2 | from cfnlint import RuleMatch # pip install cfnlint 3 | 4 | 5 | class RdsDeletionPolicy(CloudFormationLintRule): 6 | id = 'W9001' 7 | shortdesc = 'Check RDS deletion policy' 8 | description = 'This rule checks DeletionPolicy on RDS resources to be Snapshot or Retain' 9 | RDS_RESOURCES = [ 10 | 'AWS::RDS::DBInstance', 11 | 'AWS::RDS::DBCluster' 12 | ] 13 | 14 | def match(self, cfn): 15 | matches = [] 16 | resources = cfn.get_resources(self.RDS_RESOURCES) 17 | for resource_name, resource in resources.items(): 18 | deletion_policy = resource.get('DeletionPolicy') 19 | path = ['Resources', resource_name] 20 | if deletion_policy is None: 21 | message = f'Resource {resource_name} does not have Deletion Policy!' 22 | matches.append(RuleMatch(path, message)) 23 | elif deletion_policy not in ('Snapshot', 'Retain'): 24 | message = f'Resource {resource_name} does not have Deletion Policy set to Snapshot or Retain!' 25 | matches.append(RuleMatch(path, message)) 26 | return matches 27 | -------------------------------------------------------------------------------- /Chapter03/database_failing.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::LanguageExtensions 3 | Description: Database template 4 | 5 | Parameters: 6 | Environment: 7 | Type: String 8 | Default: test 9 | AllowedValues: [ "test", "prod" ] 10 | 11 | Conditions: 12 | ProdEnv: !Equals [!Ref Environment, "prod"] 13 | TestEnv: !Equals [!Ref Environment, "test"] 14 | 15 | Resources: 16 | 17 | HardcodedDatabaseSecret: 18 | Condition: TestEnv 19 | Type: "AWS::SecretsManager::Secret" 20 | Properties: 21 | Description: "Database credentials" 22 | SecretString: 23 | Fn::ToJsonString: 24 | username: "dbuser" 25 | password: "dbpassword" # Do not do this in production! 26 | Tags: 27 | - Key: "Env" 28 | Value: !Ref Environment 29 | - Key: "Name" 30 | Value: !Sub "${Environment}-${AWS::StackName}-secret" 31 | 32 | GeneratedDatabaseSecret: 33 | Condition: ProdEnv 34 | Type: "AWS::SecretsManager::Secret" 35 | Properties: 36 | Description: "Database credentials" 37 | GenerateSecretString: 38 | SecretStringTemplate: 39 | Fn::ToJsonString: 40 | username: "dbuser" 41 | GenerateStringKey: "password" 42 | PasswordLength: 16 43 | ExcludeCharacters: '"@/\' 44 | Tags: 45 | - Key: "Env" 46 | Value: !Ref Environment 47 | - Key: "Name" 48 | Value: !Sub "${Environment}-${AWS::StackName}-secret" 49 | 50 | DatabaseSg: 51 | Type: "AWS::EC2::SecurityGroup" 52 | Properties: 53 | GroupDescription: Database security group 54 | VpcId: !ImportValue VpcId 55 | Tags: 56 | - Key: "Env" 57 | Value: !Ref Environment 58 | - Key: "Name" 59 | Value: !Sub "${Environment}-${AWS::StackName}-sg" 60 | 61 | Database: 62 | # DeletionPolicy: !If [ProdEnv, "Retain", "Delete"] 63 | Type: "AWS::RDS::DBCluster" 64 | Properties: 65 | Engine: aurora 66 | EngineMode: serverless 67 | DBSubnetGroupName: !ImportValue DbSubnetGroupId 68 | ScalingConfiguration: 69 | AutoPause: True 70 | MaxCapacity: 1 71 | MinCapacity: 1 72 | SecondsUntilAutoPause: 300 73 | MasterUsername: 74 | Fn::If: 75 | - ProdEnv 76 | - !Sub "{{resolve:secretsmanager:${GeneratedDatabaseSecret}:SecretString:username}}" 77 | - !Sub "{{resolve:secretsmanager:${HardcodedDatabaseSecret}:SecretString:username}}" 78 | MasterUserPassword: 79 | Fn::If: 80 | - ProdEnv 81 | - !Sub "{{resolve:secretsmanager:${GeneratedDatabaseSecret}:SecretString:password}}" 82 | - !Sub "{{resolve:secretsmanager:${HardcodedDatabaseSecret}:SecretString:password}}" 83 | VpcSecurityGroupIds: 84 | - !Ref DatabaseSg 85 | Tags: 86 | - Key: "Env" 87 | Value: !Ref Environment 88 | - Key: "Name" 89 | Value: !Sub "${Environment}-${AWS::StackName}-cluster" 90 | 91 | DatabaseSgIngressRule: 92 | Type: "AWS::EC2::SecurityGroupIngress" 93 | Properties: 94 | IpProtocol: tcp 95 | FromPort: !GetAtt Database.Endpoint.Port 96 | ToPort: !GetAtt Database.Endpoint.Port 97 | SourceSecurityGroupId: !ImportValue MiddlewareInstanceSg 98 | GroupId: !Ref DatabaseSg 99 | 100 | Outputs: 101 | DatabaseEndpointAddress: 102 | Value: Database.Endpoint.Address 103 | Export: 104 | Name: DatabaseEndpointAddress 105 | 106 | DatabaseEndpointPort: 107 | Value: Database.Endpoint.Port 108 | Export: 109 | Name: DatabseEndpointPort 110 | 111 | DbCredentials: 112 | Value: !If [ProdEnv, GeneratedDatabaseSecret, HardcodedDatabaseSecret] 113 | Export: 114 | Name: DbCredentials -------------------------------------------------------------------------------- /Chapter03/testing.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ParameterKey": "Environment", 4 | "ParameterValue": "test" 5 | }, 6 | { 7 | "ParameterKey": "VpcCidr", 8 | "ParameterValue": "10.1.0.0/16" 9 | } 10 | ] -------------------------------------------------------------------------------- /Chapter04/README.md: -------------------------------------------------------------------------------- 1 | # Smoke tests 2 | 3 | Create a "non working" ASG stack (use your own default VPC ID and Subnet IDs) 4 | ```bash 5 | aws cloudformation deploy \ 6 | --template-file broken_asg.yaml \ 7 | --stack-name broken \ 8 | --parameter-overrides VpcId=vpc-12345678\ 9 | SubnetIds=subnet-123,subnet-456,subnet-789,subnet-012,subnet-345,subnet-678 10 | ``` 11 | Obtain ELB URL 12 | ```bash 13 | aws cloudformation describe-stacks --stack-name broken | jq .[][].Outputs[].OutputValue 14 | ``` 15 | Run the test 16 | ```bash 17 | python asg_test.py broken 18 | ``` 19 | Apply the "fix" on the stack 20 | ```bash 21 | aws cloudformation deploy \ 22 | --template-file working_asg.yaml \ 23 | --stack-name broken \ 24 | --parameter-overrides VpcId=vpc-12345678\ 25 | SubnetIds=subnet-123,subnet-456,subnet-789,subnet-012,subnet-345,subnet-678 26 | ``` 27 | Run the test again 28 | 29 | Create a "non compliant" Core stack 30 | ```bash 31 | aws cloudformation deploy --stack-name core --template-file core_non_compliant.yaml --capabilities CAPABILITY_IAM 32 | ``` 33 | Run testing: 34 | ```bash 35 | python core_subnets.py core 36 | ``` 37 | Deploy a fix: 38 | ```bash 39 | aws cloudformation deploy --stack-name core --template-file core_compliant.yaml --capabilities CAPABILITY_IAM 40 | ``` 41 | Run the test again. 42 | 43 | # Continuous Delivery 44 | 45 | Deploy the CI/CD stack 46 | ```bash 47 | aws cloudformation deploy \ 48 | --stack-name cicd \ 49 | --template-file cicd.yaml \ 50 | --capabilities CAPABILITY_IAM 51 | ``` 52 | From the AWS Console add all the necessary files to the repository. Add buildspec.yml last to avoid tmp stacks conflict. 53 | If you had several failures (you definitely had) rerun the pipeline or retry it from the last failed stage. 54 | 55 | -------------------------------------------------------------------------------- /Chapter04/asg_test.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | import boto3 3 | import botocore 4 | import requests 5 | import sys 6 | 7 | try: 8 | stack = sys.argv[1] 9 | except IndexError: 10 | print('Please provide a stack name') 11 | sys.exit(1) 12 | 13 | cfn_client = boto3.client('cloudformation') 14 | 15 | try: 16 | stack = cfn_client.describe_stacks(StackName=stack) 17 | except botocore.exceptions.ClientError: 18 | print('This stack does not exist or region is incorrect') 19 | sys.exit(1) 20 | 21 | elb_dns = stack['Stacks'][0]['Outputs'][0]['OutputValue'] 22 | for _ in range(0, 2): 23 | resp = requests.get(f"http://{elb_dns}") 24 | if resp.status_code == 200: 25 | print("Test succeeded") 26 | sys.exit(0) 27 | sleep(5) 28 | 29 | print(f"Result of test: {resp.content}") 30 | print(f"HTTP Response code: {resp.status_code}") 31 | print("Test did not succeed") 32 | sys.exit(1) 33 | -------------------------------------------------------------------------------- /Chapter04/broken_asg.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Parameters: 3 | AmiId: 4 | Type: 'AWS::SSM::Parameter::Value' 5 | Default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2' 6 | VpcId: 7 | Type: AWS::EC2::VPC::Id 8 | Description: Use it to provide default vpc 9 | SubnetIds: 10 | Type: List 11 | Description: Use it to provide a list of subnetids 12 | 13 | Resources: 14 | 15 | Lc: 16 | Type: "AWS::AutoScaling::LaunchConfiguration" 17 | Properties: 18 | ImageId: !Ref AmiId 19 | InstanceType: "t2.micro" 20 | UserData: # can you find a mistake in this UserData? 21 | Fn::Base64: | 22 | #!/bin/bash 23 | yum -y install yolo 24 | systemctl start yolo 25 | SecurityGroups: 26 | - !Ref Sg 27 | 28 | Sg: 29 | Type: "AWS::EC2::SecurityGroup" 30 | Properties: 31 | GroupDescription: "not secure sg" 32 | SecurityGroupIngress: 33 | - IpProtocol: "-1" 34 | CidrIp: "0.0.0.0/0" 35 | VpcId: !Ref VpcId 36 | 37 | Asg: 38 | Type: "AWS::AutoScaling::AutoScalingGroup" 39 | Properties: 40 | MaxSize: "1" 41 | MinSize: "1" 42 | LaunchConfigurationName: !Ref Lc 43 | AvailabilityZones: !GetAZs 44 | HealthCheckGracePeriod: 30 45 | HealthCheckType: "ELB" 46 | TargetGroupARNs: 47 | - !Ref Tg 48 | 49 | Tg: 50 | Type: "AWS::ElasticLoadBalancingV2::TargetGroup" 51 | Properties: 52 | Port: 80 53 | Protocol: "HTTP" 54 | VpcId: !Ref VpcId 55 | UnhealthyThresholdCount: 5 56 | 57 | Elb: 58 | Type: "AWS::ElasticLoadBalancingV2::LoadBalancer" 59 | Properties: 60 | Type: "application" 61 | Scheme: "internet-facing" 62 | SecurityGroups: 63 | - !Ref Sg 64 | Subnets: !Ref SubnetIds 65 | 66 | Listener: 67 | Type: "AWS::ElasticLoadBalancingV2::Listener" 68 | Properties: 69 | Port: 80 70 | Protocol: "HTTP" 71 | LoadBalancerArn: !Ref Elb 72 | DefaultActions: 73 | - Type: "forward" 74 | TargetGroupArn: !Ref Tg 75 | 76 | Outputs: 77 | Dns: 78 | Value: !GetAtt Elb.DNSName -------------------------------------------------------------------------------- /Chapter04/cfn_source/buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | phases: 4 | install: 5 | runtime-versions: 6 | python: 3.8 7 | commands: 8 | - pip install awscli cfn-lint 9 | build: 10 | commands: 11 | - aws cloudformation validate-template --template-body file://core.yaml 12 | - cfn-lint core.yaml 13 | post_build: 14 | commands: 15 | - aws cloudformation deploy --template-file core.yaml --stack-name core-tmp --capabilities CAPABILITY_NAMED_IAM --role-arn $CFN_ROLE 16 | - python tests/core_subnets.py core-tmp 17 | finally: 18 | - aws cloudformation delete-stack --stack-name core-tmp --role-arn $CFN_ROLE 19 | artifacts: 20 | files: 21 | - core.yaml -------------------------------------------------------------------------------- /Chapter04/cfn_source/tests/core_subnets.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import botocore 3 | import sys 4 | 5 | matches = [] 6 | 7 | try: 8 | stack = sys.argv[1] 9 | except IndexError: 10 | print('Please provide a stack name') 11 | sys.exit(1) 12 | 13 | cfn_client = boto3.client('cloudformation') 14 | 15 | try: 16 | resources = cfn_client.describe_stack_resources(StackName=stack) 17 | except botocore.exceptions.ClientError: 18 | print('This stack does not exist or region is incorrect') 19 | sys.exit(1) 20 | 21 | subnets_in_stack = [] 22 | for resource in resources['StackResources']: 23 | if resource['LogicalResourceId'] == 'PrivateRouteTable': 24 | private_route_table = resource['PhysicalResourceId'] 25 | if resource['ResourceType'] == 'AWS::EC2::Subnet': 26 | subnets_in_stack.append(resource['PhysicalResourceId']) 27 | 28 | ec2_client = boto3.client('ec2') 29 | subnets_to_check = [] 30 | for subnet in subnets_in_stack: 31 | resp = ec2_client.describe_subnets(SubnetIds=[subnet]) 32 | for tag in resp['Subnets'][0]['Tags']: 33 | if tag['Key'] == 'Private' and tag['Value'] == 'True': 34 | subnets_to_check.append(subnet) 35 | 36 | route_table = ec2_client.describe_route_tables(RouteTableIds=[private_route_table]) 37 | private_subnets = [] 38 | for assoc in route_table['RouteTables'][0]['Associations']: 39 | private_subnets.append(assoc['SubnetId']) 40 | 41 | for subnet in subnets_to_check: 42 | if subnet not in private_subnets: 43 | matches.append(subnet) 44 | 45 | if matches: 46 | print('One or more private subnets are not associated with proper route table!') 47 | print(f"Non-compliant subnets: {matches}") 48 | sys.exit(1) 49 | 50 | print('All subnets are compliant!') 51 | exit(0) 52 | -------------------------------------------------------------------------------- /Chapter04/cicd.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: CI/CD pipeline template 3 | Resources: 4 | 5 | CfnRole: 6 | Type: "AWS::IAM::Role" 7 | Properties: 8 | AssumeRolePolicyDocument: 9 | Version: "2012-10-17" 10 | Statement: 11 | - Effect: "Allow" 12 | Principal: 13 | Service: "cloudformation.amazonaws.com" 14 | Action: 15 | - "sts:AssumeRole" 16 | ManagedPolicyArns: 17 | - "arn:aws:iam::aws:policy/AdministratorAccess" 18 | 19 | Repository: 20 | Type: "AWS::CodeCommit::Repository" 21 | Properties: 22 | RepositoryName: core 23 | 24 | TemplateBucket: 25 | Type: "AWS::S3::Bucket" 26 | 27 | BuildRole: 28 | Type: "AWS::IAM::Role" 29 | Properties: 30 | AssumeRolePolicyDocument: 31 | Version: 2012-10-17 32 | Statement: 33 | - Sid: "AllowAssumeRole" 34 | Effect: "Allow" 35 | Principal: 36 | Service: "codebuild.amazonaws.com" 37 | Action: "sts:AssumeRole" 38 | Policies: 39 | - PolicyDocument: 40 | Version: "2012-10-17" 41 | Statement: 42 | - Sid: "CodeBuild" 43 | Effect: "Allow" 44 | Action: 45 | - "logs:CreateLogGroup" 46 | - "logs:CreateLogStream" 47 | - "logs:PutLogEvents" 48 | - "codecommit:GitPull" 49 | - "s3:GetObject" 50 | - "s3:GetObjectVersion" 51 | - "s3:PutObject" 52 | - "ecr:BatchCheckLayerAvailability" 53 | - "ecr:GetDownloadUrlForLayer" 54 | - "ecr:BatchGetImage" 55 | - "ecr:GetAuthorizationToken" 56 | - "s3:GetBucketAcl" 57 | - "s3:GetBucketLocation" 58 | - "cloudformation:*" 59 | - "iam:PassRole" 60 | - "ec2:Describe*" 61 | Resource: "*" 62 | PolicyName: "BuildPolicy" 63 | 64 | Build: 65 | Type: "AWS::CodeBuild::Project" 66 | Properties: 67 | Artifacts: 68 | Type: "CODEPIPELINE" 69 | ServiceRole: !GetAtt BuildRole.Arn 70 | Name: "Core" 71 | Source: 72 | Type: "CODEPIPELINE" 73 | Environment: 74 | Type: "LINUX_CONTAINER" 75 | ComputeType: "BUILD_GENERAL1_SMALL" 76 | Image: "aws/codebuild/amazonlinux2-x86_64-standard:2.0" 77 | EnvironmentVariables: 78 | - Name: "CFN_ROLE" 79 | Type: "PLAINTEXT" 80 | Value: !GetAtt CfnRole.Arn 81 | 82 | 83 | PipelineRole: 84 | Type: "AWS::IAM::Role" 85 | Properties: 86 | AssumeRolePolicyDocument: 87 | Version: "2012-10-17" 88 | Statement: 89 | - Effect: Allow 90 | Principal: 91 | Service: "codepipeline.amazonaws.com" 92 | Action: "sts:AssumeRole" 93 | Policies: 94 | - PolicyName: CodePipeline 95 | PolicyDocument: 96 | Version: "2012-10-17" 97 | Statement: 98 | - Effect: Allow 99 | Action: 100 | - "codecommit:CancelUploadArchive" 101 | - "codecommit:GetBranch" 102 | - "codecommit:GetCommit" 103 | - "codecommit:GetUploadArchiveStatus" 104 | - "codecommit:UploadArchive" 105 | - "codebuild:BatchGetBuilds" 106 | - "codebuild:StartBuild" 107 | - "cloudwatch:*" 108 | - "cloudformation:*" 109 | - "s3:*" 110 | - "iam:PassRole" 111 | Resource: "*" 112 | 113 | Pipeline: 114 | Type: "AWS::CodePipeline::Pipeline" 115 | Properties: 116 | RoleArn: !GetAtt PipelineRole.Arn 117 | ArtifactStore: 118 | Location: !Ref TemplateBucket 119 | Type: "S3" 120 | Name: "Core" 121 | Stages: 122 | - 123 | Name: "Clone" 124 | Actions: 125 | - ActionTypeId: 126 | Category: "Source" 127 | Owner: "AWS" 128 | Provider: "CodeCommit" 129 | Version: "1" 130 | Name: "Clone" 131 | OutputArtifacts: 132 | - Name: "CloneOutput" 133 | Configuration: 134 | BranchName: "main" 135 | RepositoryName: !GetAtt Repository.Name 136 | RunOrder: 1 137 | - 138 | Name: "Build" 139 | Actions: 140 | - Name: "Build" 141 | InputArtifacts: 142 | - Name: "CloneOutput" 143 | ActionTypeId: 144 | Category: "Build" 145 | Owner: "AWS" 146 | Version: "1" 147 | Provider: "CodeBuild" 148 | OutputArtifacts: 149 | - Name: "BuildOutput" 150 | Configuration: 151 | ProjectName: !Ref Build 152 | RunOrder: 1 153 | - 154 | Name: "Deploy" 155 | Actions: 156 | - Name: "Deploy" 157 | InputArtifacts: 158 | - Name: "BuildOutput" 159 | ActionTypeId: 160 | Category: "Deploy" 161 | Owner: "AWS" 162 | Version: "1" 163 | Provider: "CloudFormation" 164 | OutputArtifacts: 165 | - Name: DeployOutput 166 | Configuration: 167 | ActionMode: "CREATE_UPDATE" 168 | RoleArn: !GetAtt CfnRole.Arn 169 | Capabilities: "CAPABILITY_NAMED_IAM" 170 | StackName: "Core" 171 | TemplatePath: "BuildOutput::core.yaml" 172 | -------------------------------------------------------------------------------- /Chapter04/core_subnets.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import botocore 3 | import sys 4 | 5 | matches = [] 6 | 7 | try: 8 | stack = sys.argv[1] 9 | except IndexError: 10 | print('Please provide a stack name') 11 | sys.exit(1) 12 | 13 | cfn_client = boto3.client('cloudformation') 14 | 15 | try: 16 | resources = cfn_client.describe_stack_resources(StackName=stack) 17 | except botocore.exceptions.ClientError: 18 | print('This stack does not exist or region is incorrect') 19 | sys.exit(1) 20 | 21 | subnets_in_stack = [] 22 | for resource in resources['StackResources']: 23 | if resource['LogicalResourceId'] == 'PrivateRouteTable': 24 | private_route_table = resource['PhysicalResourceId'] 25 | if resource['ResourceType'] == 'AWS::EC2::Subnet': 26 | subnets_in_stack.append(resource['PhysicalResourceId']) 27 | 28 | ec2_client = boto3.client('ec2') 29 | subnets_to_check = [] 30 | for subnet in subnets_in_stack: 31 | resp = ec2_client.describe_subnets(SubnetIds=[subnet]) 32 | for tag in resp['Subnets'][0]['Tags']: 33 | if tag['Key'] == 'Private' and tag['Value'] == 'True': 34 | subnets_to_check.append(subnet) 35 | 36 | route_table = ec2_client.describe_route_tables(RouteTableIds=[private_route_table]) 37 | private_subnets = [] 38 | for assoc in route_table['RouteTables'][0]['Associations']: 39 | private_subnets.append(assoc['SubnetId']) 40 | 41 | for subnet in subnets_to_check: 42 | if subnet not in private_subnets: 43 | matches.append(subnet) 44 | 45 | if matches: 46 | print('One or more private subnets are not associated with proper route table!') 47 | print(f"Non-compliant subnets: {matches}") 48 | sys.exit(1) 49 | 50 | print('All subnets are compliant!') 51 | exit(0) 52 | -------------------------------------------------------------------------------- /Chapter04/working_asg.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Parameters: 3 | AmiId: 4 | Type: "AWS::SSM::Parameter::Value" 5 | Default: "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2" 6 | VpcId: 7 | Type: "AWS::EC2::VPC::Id" 8 | Description: "Use it to provide default vpc" 9 | SubnetIds: 10 | Type: List 11 | Description: "Use it to provide a list of subnetids" 12 | 13 | Resources: 14 | 15 | Lc: 16 | Type: "AWS::AutoScaling::LaunchConfiguration" 17 | Properties: 18 | ImageId: !Ref AmiId 19 | InstanceType: "t2.micro" 20 | UserData: 21 | Fn::Base64: | 22 | #!/bin/bash 23 | amazon-linux-extras install -y epel 24 | yum -y install nginx 25 | systemctl start nginx 26 | SecurityGroups: 27 | - !Ref Sg 28 | 29 | Sg: 30 | Type: "AWS::EC2::SecurityGroup" 31 | Properties: 32 | GroupDescription: "not secure sg" 33 | SecurityGroupIngress: 34 | - IpProtocol: "-1" 35 | CidrIp: "0.0.0.0/0" 36 | VpcId: !Ref VpcId 37 | 38 | Asg: 39 | Type: "AWS::AutoScaling::AutoScalingGroup" 40 | Properties: 41 | MaxSize: "1" 42 | MinSize: "1" 43 | LaunchConfigurationName: !Ref Lc 44 | AvailabilityZones: !GetAZs 45 | HealthCheckGracePeriod: 30 46 | HealthCheckType: "ELB" 47 | TargetGroupARNs: 48 | - !Ref Tg 49 | 50 | Tg: 51 | Type: "AWS::ElasticLoadBalancingV2::TargetGroup" 52 | Properties: 53 | Port: 80 54 | Protocol: "HTTP" 55 | VpcId: !Ref VpcId 56 | UnhealthyThresholdCount: 5 57 | 58 | Elb: 59 | Type: "AWS::ElasticLoadBalancingV2::LoadBalancer" 60 | Properties: 61 | Type: "application" 62 | Scheme: "internet-facing" 63 | SecurityGroups: 64 | - !Ref Sg 65 | Subnets: !Ref SubnetIds 66 | 67 | Listener: 68 | Type: "AWS::ElasticLoadBalancingV2::Listener" 69 | Properties: 70 | Port: 80 71 | Protocol: "HTTP" 72 | LoadBalancerArn: !Ref Elb 73 | DefaultActions: 74 | - Type: "forward" 75 | TargetGroupArn: !Ref Tg 76 | 77 | Outputs: 78 | Dns: 79 | Value: !GetAtt Elb.DNSName -------------------------------------------------------------------------------- /Chapter05/README.md: -------------------------------------------------------------------------------- 1 | # Multi-region 2 | 3 | Deploy permissions stack 4 | ```bash 5 | aws cloudformation deploy --stack-name ss-perm --template-file multi-region/StackSetPermissions.yaml --capabilities CAPABILITY_NAMED_IAM 6 | ``` 7 | Create StackSet 8 | ```bash 9 | aws cloudformation create-stack-set --stack-set-name core --template-body file://multi-region/core.yaml --capabilities CAPABILITY_NAMED_IAM 10 | ``` 11 | Create Stack instances 12 | ```bash 13 | aws cloudformation create-stack-instances --stack-set-name core --accounts ACCT_ID --regions eu-west-1 --parameter-overrides ParameterKey=VpcCidr,ParameterValue=10.1.0.0/16 ParameterKey=Environment,ParameterValue=test 14 | aws cloudformation create-stack-instances --stack-set-name core --accounts ACCT_ID --regions us-east-1 --parameter-overrides ParameterKey=VpcCidr,ParameterValue=10.0.0.0/16 ParameterKey=Environment,ParameterValue=prod 15 | ``` 16 | 17 | # Multi-account 18 | 19 | Deploy permission stacks to admin and target accounts 20 | ```bash 21 | aws cloudformation deploy --stack-name Admin-Role --template-file multi-account/StackSetAdmin.yaml --capabilities CAPABILITY_NAMED_IAM 22 | aws cloudformation deploy --stack-name Exec-Role --profile test --template-file multi-account/StackSetExec.yaml --parameter-overrides AdministratorAccountId=ACCT_ID --capabilities CAPABILITY_NAMED_IAM 23 | aws cloudformation deploy --stack-name Exec-Role --profile prod --template-file multi-account/StackSetExec.yaml --parameter-overrides AdministratorAccountId=ACCT_ID --capabilities CAPABILITY_NAMED_IAM 24 | ``` 25 | 26 | Create StackSet 27 | ```bash 28 | aws cloudformation create-stack-set --stack-set-name core --template-body file://multi-account/core.yaml --capabilities CAPABILITY_NAMED_IAM 29 | ``` 30 | Create Stack instances 31 | ```bash 32 | aws cloudformation create-stack-instances --stack-set-name core --accounts ACCT_ID_PROD ACCT_ID_TEST --regions REGION --operation-preferences MaxConcurrentPercentage=100 33 | ``` 34 | 35 | # TAG 36 | Deploy permissions stack 37 | ```bash 38 | aws cloudformation deploy --stack-name ss-perm --template-file multi-region/StackSetPermissions.yaml --capabilities CAPABILITY_NAMED_IAM 39 | ``` 40 | Deploy TAG 41 | ```bash 42 | aws cloudformation deploy --stack-name tag --template-file target-account-gate/tag.yaml --capabilities CAPABILITY_IAM 43 | ``` 44 | Create StackSet and instances 45 | ```bash 46 | aws cloudformation create-stack-set --stack-set-name webtier --template-body file://target-account-gate/webtier.yaml 47 | aws cloudformation create-stack-instances --stack-set-name webtier --accounts ACCT_ID --regions eu-central-1 48 | ``` -------------------------------------------------------------------------------- /Chapter05/multi-account/StackSetAdmin.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: StackSet IAM roles 3 | Resources: 4 | 5 | StackSetAdministratorRole: 6 | Type: AWS::IAM::Role 7 | Properties: 8 | RoleName: "AWSCloudFormationStackSetAdministrationRole" 9 | AssumeRolePolicyDocument: 10 | Version: 2012-10-17 11 | Statement: 12 | Effect: Allow 13 | Principal: 14 | Service: cloudformation.amazonaws.com 15 | Action: "sts:AssumeRole" 16 | Policies: 17 | - PolicyName: StackSetAdministratorPolicy 18 | PolicyDocument: 19 | Version: 2012-10-17 20 | Statement: 21 | Effect: Allow 22 | Action: "sts:AssumeRole" 23 | Resource: 24 | - "arn:aws:iam::*:role/AWSCloudFormationStackSetExecutionRole" 25 | 26 | -------------------------------------------------------------------------------- /Chapter05/multi-account/StackSetExec.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: StackSet IAM roles 3 | 4 | Parameters: 5 | AdminAcctId: 6 | Type: String 7 | AllowedPattern: '(\d+)$' 8 | 9 | Resources: 10 | 11 | StackSetExecutionRole: 12 | Type: AWS::IAM::Role 13 | Properties: 14 | RoleName: AWSCloudFormationStackSetExecutionRole 15 | AssumeRolePolicyDocument: 16 | Version: 2012-10-17 17 | Statement: 18 | Effect: Allow 19 | Action: "sts:AssumeRole" 20 | Principal: 21 | AWS: 22 | - !Sub "arn:aws:iam::${AdminAcctId}:root" 23 | ManagedPolicyArns: 24 | - "arn:aws:iam::aws:policy/AdministratorAccess" 25 | -------------------------------------------------------------------------------- /Chapter05/multi-region/StackSetPermissions.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: StackSet IAM roles 3 | Resources: 4 | 5 | StackSetAdministratorRole: 6 | Type: AWS::IAM::Role 7 | Properties: 8 | RoleName: "AWSCloudFormationStackSetAdministrationRole" 9 | AssumeRolePolicyDocument: 10 | Version: 2012-10-17 11 | Statement: 12 | Effect: Allow 13 | Principal: 14 | Service: cloudformation.amazonaws.com 15 | Action: "sts:AssumeRole" 16 | Policies: 17 | - PolicyName: StackSetAdministratorPolicy 18 | PolicyDocument: 19 | Version: 2012-10-17 20 | Statement: 21 | Effect: Allow 22 | Action: "sts:AssumeRole" 23 | Resource: 24 | - "arn:aws:iam::*:role/AWSCloudFormationStackSetExecutionRole" 25 | 26 | StackSetExecutionRole: 27 | Type: AWS::IAM::Role 28 | Properties: 29 | RoleName: AWSCloudFormationStackSetExecutionRole 30 | AssumeRolePolicyDocument: 31 | Version: 2012-10-17 32 | Statement: 33 | Effect: Allow 34 | Action: "sts:AssumeRole" 35 | Principal: 36 | AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root" 37 | ManagedPolicyArns: 38 | - "arn:aws:iam::aws:policy/AdministratorAccess" 39 | -------------------------------------------------------------------------------- /Chapter05/target-account-gate/tag.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | 3 | 4 | def check_if_key_exists(): 5 | client = boto3.client('ec2') 6 | try: 7 | resp = client.describe_key_pairs(KeyNames=["mykey"]) 8 | except Exception: 9 | return False 10 | if len(resp['KeyPairs']) == 0: 11 | return False 12 | return True 13 | 14 | 15 | def check_if_core_stack_exists(): 16 | client = boto3.client('cloudformation') 17 | try: 18 | resp = client.describe_stacks(StackName="core") 19 | except Exception: 20 | return False 21 | if len(resp['Stacks']) == 0: 22 | return False 23 | return True 24 | 25 | 26 | def check_if_exports_exist(): 27 | to_check = ["WebTierSubnet1Id", 28 | 'WebTierSubnet2Id', 29 | "WebTierSubnet3Id", 30 | "VpcId", 31 | "WebTierMaxSizeParameter", 32 | "WebTierMinSizeParameter", 33 | "WebTierDesSizeParameter"] 34 | exports = [] 35 | client = boto3.client('cloudformation') 36 | try: 37 | resp = client.list_exports() 38 | except Exception: 39 | return False 40 | for export in resp['Exports']: 41 | exports.append(export['Name']) 42 | if not all(exp in exports for exp in to_check): 43 | return False 44 | return True 45 | 46 | 47 | def lambda_handler(event, context): 48 | status = "SUCCEEDED" 49 | if not (check_if_key_exists() and check_if_core_stack_exists() and check_if_exports_exist()): 50 | status = "FAILED" 51 | return { 52 | 'Status': status 53 | } 54 | -------------------------------------------------------------------------------- /Chapter05/target-account-gate/tag.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Resources: 3 | 4 | AccountGateFunctionRole: 5 | Type: 'AWS::IAM::Role' 6 | Properties: 7 | AssumeRolePolicyDocument: 8 | Version: '2012-10-17' 9 | Statement: 10 | - 11 | Effect: Allow 12 | Principal: 13 | Service: 14 | - lambda.amazonaws.com 15 | Action: 16 | - 'sts:AssumeRole' 17 | ManagedPolicyArns: 18 | - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' 19 | Policies: 20 | - PolicyName: tag 21 | PolicyDocument: 22 | Version: 2012-10-17 23 | Statement: 24 | Effect: Allow 25 | Action: 26 | - "ec2:Get*" 27 | - "ec2:Describe*" 28 | - "cloudformation:Describe*" 29 | - "cloudformation:List*" 30 | Resource: "*" 31 | 32 | AccountGateFunction: 33 | Type: 'AWS::Lambda::Function' 34 | Properties: 35 | FunctionName: 'AWSCloudFormationStackSetAccountGate' 36 | Code: 37 | ZipFile: | 38 | import boto3 39 | def check_if_key_exists(): 40 | client = boto3.client('ec2') 41 | try: 42 | resp = client.describe_key_pairs(KeyNames=["mykey"]) 43 | except Exception: 44 | return False 45 | if len(resp['KeyPairs']) == 0: 46 | return False 47 | return True 48 | def check_if_core_stack_exists(): 49 | client = boto3.client('cloudformation') 50 | try: 51 | resp = client.describe_stacks(StackName="core") 52 | except Exception: 53 | return False 54 | if len(resp['Stacks']) == 0: 55 | return False 56 | return True 57 | def check_if_exports_exist(): 58 | to_check = ["WebTierSubnet1Id", 59 | 'WebTierSubnet2Id', 60 | "WebTierSubnet3Id", 61 | "VpcId", 62 | "WebTierMaxSizeParameter", 63 | "WebTierMinSizeParameter", 64 | "WebTierDesSizeParameter"] 65 | exports = [] 66 | client = boto3.client('cloudformation') 67 | try: 68 | resp = client.list_exports() 69 | except Exception: 70 | return False 71 | for export in resp['Exports']: 72 | exports.append(export['Name']) 73 | if not all(exp in exports for exp in to_check): 74 | return False 75 | return True 76 | def lambda_handler(event, context): 77 | status = "SUCCEEDED" 78 | if not (check_if_key_exists() and check_if_core_stack_exists() and check_if_exports_exist()): 79 | status = "FAILED" 80 | return { 81 | 'Status': status 82 | } 83 | 84 | Handler: index.lambda_handler 85 | MemorySize: 128 86 | Runtime: python3.7 87 | Timeout: 30 88 | Role: !GetAtt AccountGateFunctionRole.Arn 89 | -------------------------------------------------------------------------------- /Chapter05/target-account-gate/webtier.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: WebTier template 3 | 4 | Parameters: 5 | ImageId: 6 | Type: AWS::SSM::Parameter::Value 7 | Default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2' 8 | KeyName: 9 | Type: AWS::EC2::KeyPair::KeyName 10 | Default: mykey 11 | 12 | Resources: 13 | 14 | WebTierLoadBalancer: 15 | Type: "AWS::ElasticLoadBalancingV2::LoadBalancer" 16 | Properties: 17 | Type: application 18 | Subnets: 19 | - !ImportValue PublicSubnet1Id 20 | - !ImportValue PublicSubnet2Id 21 | - !ImportValue PublicSubnet3Id 22 | Scheme: internet-facing 23 | SecurityGroups: 24 | - !Ref WebTierLbSg 25 | 26 | WebInstanceLaunchTemplate: 27 | Type: "AWS::EC2::LaunchTemplate" 28 | Properties: 29 | LaunchTemplateData: 30 | ImageId: !Ref ImageId 31 | InstanceType: t2.micro 32 | KeyName: !Ref KeyName 33 | SecurityGroupIds: 34 | - !Ref WebInstanceSg 35 | 36 | WebTierLbSg: 37 | Type: "AWS::EC2::SecurityGroup" 38 | Properties: 39 | GroupDescription: LoadBalancer Security Group 40 | SecurityGroupIngress: 41 | - IpProtocol: tcp 42 | CidrIp: 0.0.0.0/0 43 | FromPort: 80 44 | ToPort: 80 45 | VpcId: !ImportValue VpcId 46 | 47 | WebInstanceSg: 48 | Type: "AWS::EC2::SecurityGroup" 49 | Properties: 50 | GroupDescription: WebTier Instance SG 51 | SecurityGroupIngress: 52 | - IpProtocol: tcp 53 | SourceSecurityGroupId: !Ref WebTierLbSg 54 | FromPort: 80 55 | ToPort: 80 56 | VpcId: !ImportValue VpcId 57 | 58 | WebTierAsg: 59 | DependsOn: WebTierLbListener 60 | Type: "AWS::AutoScaling::AutoScalingGroup" 61 | Properties: 62 | MaxSize: !Sub [ "{{resolve:ssm:${parameter}:1}}", parameter: !ImportValue WebTierMaxSizeParameter ] 63 | MinSize: !Sub [ "{{resolve:ssm:${parameter}:1}}", parameter: !ImportValue WebTierMinSizeParameter ] 64 | DesiredCapacity: !Sub [ "{{resolve:ssm:${parameter}:1}}", parameter: !ImportValue WebTierDesSizeParameter ] 65 | VPCZoneIdentifier: 66 | - !ImportValue WebTierSubnet1Id 67 | - !ImportValue WebTierSubnet2Id 68 | - !ImportValue WebTierSubnet3Id 69 | LaunchTemplate: 70 | LaunchTemplateId: !Ref WebInstanceLaunchTemplate 71 | Version: "1" 72 | TargetGroupARNs: 73 | - !Ref WebTierTg 74 | 75 | WebTierLbListener: 76 | Type: "AWS::ElasticLoadBalancingV2::Listener" 77 | Properties: 78 | LoadBalancerArn: !Ref WebTierLoadBalancer 79 | Port: 80 80 | Protocol: HTTP 81 | DefaultActions: 82 | - Type: "forward" 83 | TargetGroupArn: !Ref WebTierTg 84 | 85 | WebTierTg: 86 | Type: "AWS::ElasticLoadBalancingV2::TargetGroup" 87 | Properties: 88 | Port: 80 89 | Protocol: HTTP 90 | VpcId: !ImportValue VpcId 91 | 92 | Outputs: 93 | WebInstanceSg: 94 | Value: !Ref WebInstanceSg 95 | Export: 96 | Name: WebInstanceSg -------------------------------------------------------------------------------- /Chapter06/README.md: -------------------------------------------------------------------------------- 1 | # cfn-init 2 | 3 | ## Hello-world 4 | 5 | Create core stack 6 | ```bash 7 | aws cloudformation deploy --stack-name hello-world-prep --template-file hello-world-prep.yaml 8 | ``` 9 | Obtain your S3 bucket 10 | ```bash 11 | aws s3 ls 12 | ``` 13 | Send source code to S3 14 | ```bash 15 | aws s3 cp hello-world-flask.py s3://$BUCKET_NAME 16 | ``` 17 | Deploy app stack 18 | ```bash 19 | aws cloudformation deploy --stack-name hello-world-app --template-file hello-world-app.yaml --capabilities CAPABILITY_IAM 20 | ``` 21 | 22 | ## LMNP 23 | 24 | Deploy the stack 25 | ```bash 26 | aws cloudformation deploy --stack-name lnmp --template-file lnmp-signal.yaml --parameter-overrides DBName=foo DBPassword=foobar123 DBRootPassword=barfoo321 DBUsername=bar 27 | ``` -------------------------------------------------------------------------------- /Chapter06/hello-world/hello-world-app.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: Hello World application 3 | Parameters: 4 | 5 | ImageId: 6 | Type: AWS::SSM::Parameter::Value 7 | Default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2' 8 | 9 | KeyName: 10 | Type: AWS::EC2::KeyPair::KeyName 11 | Default: mykey 12 | 13 | Resources: 14 | Lc: 15 | Type: AWS::AutoScaling::LaunchConfiguration 16 | Properties: 17 | InstanceType: t2.micro 18 | ImageId: !Ref ImageId 19 | SecurityGroups: 20 | - !Ref AppSg 21 | KeyName: !Ref KeyName 22 | IamInstanceProfile: !Ref AppInstanceProfile 23 | UserData: 24 | Fn::Base64: !Sub | 25 | #!/bin/bash -xe 26 | /opt/aws/bin/cfn-init -v \ 27 | --stack ${AWS::StackName} \ 28 | --resource Lc --configsets InstallAndRun \ 29 | --region ${AWS::Region} 30 | Metadata: 31 | AWS::CloudFormation::Init: 32 | configSets: 33 | InstallAndRun: 34 | - "Configure" 35 | Configure: 36 | packages: 37 | yum: 38 | python3: [] 39 | python3-pip: [] 40 | files: 41 | /opt/helloworld.py: 42 | owner: root 43 | group: root 44 | mode: 755 45 | source: !Sub 46 | - "https://${bucket}.s3.${AWS::Region}.${AWS::URLSuffix}/hello-world-flask.py" 47 | - bucket: !ImportValue S3Bucket 48 | authentification: "role" 49 | /etc/systemd/system/helloworld.service: 50 | owner: root 51 | group: root 52 | mode: 755 53 | content: | 54 | [Unit] 55 | Description=HelloWorld service 56 | After=network.target 57 | [Service] 58 | Type=simple 59 | User=root 60 | ExecStart=/opt/helloworld.py 61 | Restart=on-abort 62 | [Install] 63 | WantedBy=multi-user.target 64 | commands: 65 | installflask: 66 | # This commands runs installation 67 | command: "pip3 install flask" 68 | # This commands runs BEFORE command above 69 | # and checks if pip3 is present on system 70 | # if return code is not 0 cfn-init stops 71 | test: "which pip3" 72 | reloadsystemd: 73 | command: "systemctl daemon-reload" 74 | services: 75 | sysvinit: 76 | helloworld: 77 | enabled: "true" 78 | ensureRunning: "true" 79 | AWS::CloudFormation::Authentication: 80 | role: 81 | type: "S3" 82 | buckets: 83 | - !ImportValue S3Bucket 84 | roleName: !Ref AppRole 85 | AppRole: 86 | Type: AWS::IAM::Role 87 | Properties: 88 | AssumeRolePolicyDocument: 89 | Version: "2012-10-17" 90 | Statement: 91 | - Effect: Allow 92 | Action: "sts:AssumeRole" 93 | Principal: 94 | Service: "ec2.amazonaws.com" 95 | Policies: 96 | - PolicyName: S3 97 | PolicyDocument: 98 | Version: "2012-10-17" 99 | Statement: 100 | - Effect: Allow 101 | Action: "s3:*" 102 | Resource: "*" 103 | 104 | AppInstanceProfile: 105 | Type: AWS::IAM::InstanceProfile 106 | Properties: 107 | Roles: 108 | - !Ref AppRole 109 | 110 | AppSg: 111 | Type: AWS::EC2::SecurityGroup 112 | Properties: 113 | GroupDescription: App Sg 114 | SecurityGroupIngress: 115 | - IpProtocol: tcp 116 | SourceSecurityGroupId: !Ref ElbSg 117 | FromPort: 5000 118 | ToPort: 5000 119 | - IpProtocol: tcp 120 | FromPort: 22 121 | ToPort: 22 122 | CidrIp: 0.0.0.0/0 123 | VpcId: !ImportValue VpcId 124 | 125 | Elb: 126 | Type: AWS::ElasticLoadBalancingV2::LoadBalancer 127 | Properties: 128 | Type: application 129 | Subnets: 130 | - !ImportValue Subnet1Id 131 | - !ImportValue Subnet2Id 132 | Scheme: internet-facing 133 | SecurityGroups: 134 | - !Ref ElbSg 135 | 136 | ElbSg: 137 | Type: AWS::EC2::SecurityGroup 138 | Properties: 139 | GroupDescription: Elb Sg 140 | SecurityGroupIngress: 141 | - IpProtocol: tcp 142 | FromPort: 80 143 | ToPort: 80 144 | CidrIp: 0.0.0.0/0 145 | VpcId: !ImportValue VpcId 146 | 147 | Listener: 148 | Type: AWS::ElasticLoadBalancingV2::Listener 149 | Properties: 150 | LoadBalancerArn: !Ref Elb 151 | Port: 80 152 | Protocol: HTTP 153 | DefaultActions: 154 | - Type: "forward" 155 | TargetGroupArn: !Ref Tg 156 | 157 | Tg: 158 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 159 | Properties: 160 | Port: 5000 161 | Protocol: HTTP 162 | VpcId: !ImportValue VpcId 163 | 164 | Asg: 165 | DependsOn: Listener 166 | Type: AWS::AutoScaling::AutoScalingGroup 167 | Properties: 168 | MaxSize: "1" 169 | MinSize: "1" 170 | DesiredCapacity: "1" 171 | VPCZoneIdentifier: 172 | - !ImportValue Subnet1Id 173 | - !ImportValue Subnet2Id 174 | LaunchConfigurationName: !Ref Lc 175 | TargetGroupARNs: 176 | - !Ref Tg 177 | -------------------------------------------------------------------------------- /Chapter06/hello-world/hello-world-flask.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from flask import Flask 3 | app = Flask(__name__) 4 | 5 | 6 | @app.route("/") 7 | def hello(): 8 | return "Hello, World, from AWS!" 9 | 10 | 11 | if __name__ == "__main__": 12 | app.run(host="0.0.0.0", port=5000) 13 | -------------------------------------------------------------------------------- /Chapter06/hello-world/hello-world-prep.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: HelloWorld prep template 3 | Parameters: 4 | VpcCidr: 5 | Type: String 6 | AllowedPattern: '(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})' 7 | Default: 10.0.0.0/16 8 | Resources: 9 | SourceBucket: 10 | Type: AWS::S3::Bucket 11 | Vpc: 12 | Type: AWS::EC2::VPC 13 | Properties: 14 | CidrBlock: !Ref VpcCidr 15 | Igw: 16 | Type: AWS::EC2::InternetGateway 17 | IgwAttachment: 18 | Type: AWS::EC2::VPCGatewayAttachment 19 | Properties: 20 | VpcId: !Ref Vpc 21 | InternetGatewayId: !Ref Igw 22 | Subnet1: 23 | Type: AWS::EC2::Subnet 24 | Properties: 25 | CidrBlock: !Select [ 0, !Cidr [ !Ref VpcCidr, 12, 8 ] ] 26 | VpcId: !Ref Vpc 27 | MapPublicIpOnLaunch: True 28 | AvailabilityZone: !Select 29 | - 0 30 | - Fn::GetAZs: "" 31 | Subnet2: 32 | Type: AWS::EC2::Subnet 33 | Properties: 34 | CidrBlock: !Select [ 1, !Cidr [ !Ref VpcCidr, 12, 8 ] ] 35 | VpcId: !Ref Vpc 36 | MapPublicIpOnLaunch: True 37 | AvailabilityZone: !Select 38 | - 1 39 | - Fn::GetAZs: "" 40 | PublicRouteTable: 41 | Type: AWS::EC2::RouteTable 42 | Properties: 43 | VpcId: !Ref Vpc 44 | PublicRoute: 45 | Type: AWS::EC2::Route 46 | Properties: 47 | RouteTableId: !Ref PublicRouteTable 48 | DestinationCidrBlock: "0.0.0.0/0" 49 | GatewayId: !Ref Igw 50 | PublicRtAssoc1: 51 | Type: AWS::EC2::SubnetRouteTableAssociation 52 | Properties: 53 | RouteTableId: !Ref PublicRouteTable 54 | SubnetId: !Ref Subnet1 55 | PublicRtAssoc2: 56 | Type: AWS::EC2::SubnetRouteTableAssociation 57 | Properties: 58 | RouteTableId: !Ref PublicRouteTable 59 | SubnetId: !Ref Subnet2 60 | 61 | Outputs: 62 | VpcId: 63 | Value: !Ref Vpc 64 | Export: 65 | Name: VpcId 66 | Subnet1Id: 67 | Value: !Ref Subnet1 68 | Export: 69 | Name: Subnet1Id 70 | Subnet2Id: 71 | Value: !Ref Subnet2 72 | Export: 73 | Name: Subnet2Id 74 | S3Bucket: 75 | Value: !Ref SourceBucket 76 | Export: 77 | Name: S3Bucket -------------------------------------------------------------------------------- /Chapter06/lnmp/lnmp-signal.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | 3 | Parameters: 4 | KeyName: 5 | Type: AWS::EC2::KeyPair::KeyName 6 | Default: mykey 7 | ImageId: 8 | Type: AWS::SSM::Parameter::Value 9 | Default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2' 10 | DBName: 11 | Type: String 12 | MinLength: 1 13 | AllowedPattern: '[a-zA-Z][a-zA-Z0-9]*' 14 | DBUsername: 15 | Type: String 16 | NoEcho: True 17 | MinLength: 1 18 | MaxLength: 16 19 | AllowedPattern: '[a-zA-Z][a-zA-Z0-9]*' 20 | DBPassword: 21 | Type: String 22 | NoEcho: True 23 | MinLength: 1 24 | MaxLength: 41 25 | AllowedPattern: '[a-zA-Z][a-zA-Z0-9]*' 26 | DBRootPassword: 27 | Type: String 28 | NoEcho: True 29 | MinLength: 1 30 | MaxLength: 41 31 | AllowedPattern: '[a-zA-Z][a-zA-Z0-9]*' 32 | 33 | Resources: 34 | Sg: 35 | Type: AWS::EC2::SecurityGroup 36 | Properties: 37 | GroupDescription: Lnmp 38 | SecurityGroupIngress: 39 | - IpProtocol: tcp 40 | FromPort: 80 41 | ToPort: 80 42 | CidrIp: 0.0.0.0/0 43 | Lnmp: 44 | Type: AWS::EC2::Instance 45 | CreationPolicy: 46 | ResourceSignal: 47 | Count: 1 48 | Timeout: PT5M 49 | Properties: 50 | ImageId: !Ref ImageId 51 | InstanceType: t2.micro 52 | KeyName: !Ref KeyName 53 | UserData: 54 | Fn::Base64: 55 | Fn::Sub: | 56 | #!/bin/bash -xe 57 | /opt/aws/bin/cfn-init -v \ 58 | --stack ${AWS::StackName} \ 59 | --resource Lnmp \ 60 | --configsets Configure \ 61 | --region ${AWS::Region} 62 | # signal creation 63 | /opt/aws/bin/cfn-signal -e $? \ 64 | --stack ${AWS::StackName} \ 65 | --resource Lnmp \ 66 | --region ${AWS::Region} 67 | SecurityGroupIds: 68 | - !Ref Sg 69 | Metadata: 70 | AWS::CloudFormation::Init: 71 | configSets: 72 | Configure: 73 | - "Mysql" 74 | - "DbSetup" 75 | - "Php" 76 | - "Nginx" 77 | Mysql: 78 | packages: 79 | yum: 80 | mariadb: [] 81 | mariadb-server: [] 82 | mariadb-libs: [] 83 | files: 84 | /tmp/setup.mysql: 85 | content: !Sub | 86 | CREATE DATABASE ${DBName}; 87 | GRANT ALL ON ${DBName}.* TO '${DBUsername}'@localhost IDENTIFIED BY '${DBPassword}'; 88 | mode: 400 89 | owner: root 90 | group: root 91 | /etc/cfn/cfn-hup.conf: 92 | content: !Sub | 93 | [main] 94 | stack=${AWS::StackId} 95 | region=${AWS::Region} 96 | mode: 400 97 | owner: root 98 | group: root 99 | /etc/cfn/hooks.d/cfn-auto-reloader.conf: 100 | content: !Sub | 101 | [cfn-auto-reloader-hook] 102 | triggers=post.update 103 | path=Resources.Lnmp.Metadata.AWS::CloudFormation::Init 104 | action=/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource Lnmp --configsets Configure --region ${AWS::Region} 105 | runas=root 106 | services: 107 | sysvinit: 108 | mariadb: 109 | enabled: True 110 | ensureRunning: True 111 | cfn-hup: 112 | enabled: True 113 | ensureRunning: True 114 | files: 115 | - "/etc/cfn/cfn-hup.conf" 116 | - "/etc/cfn/hooks.d/cfn-auto-reloader.conf" 117 | DbSetup: 118 | commands: 119 | 01_set_mysql_root_pw: 120 | command: !Sub "mysqladmin -u root password '${DBRootPassword}'" 121 | 02_create_database: 122 | command: !Sub "mysql -u root --password='${DBRootPassword}' < /tmp/setup.mysql" 123 | test: !Sub "$(mysql ${DBUsername} -u root --password='${DBRootPassword}' >/dev/null 2>&1 136 | 137 | AWS CloudFormation PHP Sample 138 | 139 | 140 | 141 |

Welcome to the AWS CloudFormation PHP Sample

142 |

143 | "; 146 | print date("g:i A l, F j Y."); 147 | ?> 148 |

149 | "; 160 | } 161 | else 162 | { 163 | print "Server = " . $hostname . "
"; 164 | } 165 | // Get the instance-id of the instance from the instance metadata 166 | curl_setopt($curl_handle,CURLOPT_URL,'http://169.254.169.254/latest/meta-data/instance-id'); 167 | $instanceid = curl_exec($curl_handle); 168 | if (empty($instanceid)) 169 | { 170 | print "Sorry, for some reason, we got no instance id back
"; 171 | } 172 | else 173 | { 174 | print "EC2 instance-id = " . $instanceid . "
"; 175 | } 176 | $Database = "${DBName}"; 177 | $DBUser = "${DBUsername}"; 178 | $DBPassword = "${DBPassword}"; 179 | print "Database = " . $Database . "
"; 180 | $dbconnection = mysql_connect("localhost", $DBUser, $DBPassword) 181 | or die("Could not connect: " . mysql_error()); 182 | print ("Connected to $Database successfully"); 183 | mysql_close($dbconnection); 184 | ?> 185 |

PHP Information

186 |

187 | 190 | 191 | 192 | mode: 644 193 | owner: root 194 | group: root 195 | services: 196 | sysvinit: 197 | php-fpm: 198 | enabled: True 199 | ensureRunning: True 200 | Nginx: 201 | packages: 202 | yum: 203 | nginx: [] 204 | files: 205 | /etc/nginx/nginx.conf: 206 | content: | 207 | user nginx; 208 | worker_processes auto; 209 | error_log /var/log/nginx/error.log; 210 | pid /run/nginx.pid; 211 | include /usr/share/nginx/modules/*.conf; 212 | events { 213 | worker_connections 1024; 214 | } 215 | http { 216 | log_format main '\$remote_addr - \$remote_user [\$time_local] "\$request" ' 217 | '\$status \$body_bytes_sent "\$http_referer" ' 218 | '"\$http_user_agent" "\$http_x_forwarded_for"'; 219 | access_log /var/log/nginx/access.log main; 220 | sendfile on; 221 | tcp_nopush on; 222 | tcp_nodelay on; 223 | keepalive_timeout 65; 224 | types_hash_max_size 2048; 225 | include /etc/nginx/mime.types; 226 | default_type application/octet-stream; 227 | include /etc/nginx/conf.d/*.conf; 228 | } 229 | /etc/nginx/conf.d/default.conf: 230 | content: | 231 | server { 232 | listen 80; 233 | root /var/www/html; 234 | index index.php index.html index.htm; 235 | 236 | location / { 237 | try_files $uri $uri/ =404; 238 | } 239 | location ~ \.php$ { 240 | include fastcgi_params; 241 | fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name; 242 | fastcgi_pass 127.0.0.1:9000; 243 | } 244 | location ~ /\.ht { 245 | deny all; 246 | } 247 | } 248 | services: 249 | sysvinit: 250 | nginx: 251 | enabled: True 252 | ensureRunning: True 253 | Outputs: 254 | WebsiteURL: 255 | Value: !Sub "http://${Lnmp.PublicDnsName}" -------------------------------------------------------------------------------- /Chapter06/lnmp/lnmp.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | 3 | Parameters: 4 | KeyName: 5 | Type: AWS::EC2::KeyPair::KeyName 6 | Default: mykey 7 | ImageId: 8 | Type: AWS::SSM::Parameter::Value 9 | Default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2' 10 | DBName: 11 | Type: String 12 | Default: "MyDatabase" 13 | MinLength: 1 14 | AllowedPattern: '[a-zA-Z][a-zA-Z0-9]*' 15 | DBUsername: 16 | Type: String 17 | NoEcho: True 18 | MinLength: 1 19 | MaxLength: 16 20 | AllowedPattern: '[a-zA-Z][a-zA-Z0-9]*' 21 | DBPassword: 22 | Type: String 23 | NoEcho: True 24 | MinLength: 1 25 | MaxLength: 41 26 | AllowedPattern: '[a-zA-Z][a-zA-Z0-9]*' 27 | DBRootPassword: 28 | Type: String 29 | NoEcho: True 30 | MinLength: 1 31 | MaxLength: 41 32 | AllowedPattern: '[a-zA-Z][a-zA-Z0-9]*' 33 | 34 | Resources: 35 | Sg: 36 | Type: AWS::EC2::SecurityGroup 37 | Properties: 38 | GroupDescription: Lnmp 39 | SecurityGroupIngress: 40 | - IpProtocol: tcp 41 | FromPort: 80 42 | ToPort: 80 43 | CidrIp: 0.0.0.0/0 44 | Lnmp: 45 | Type: AWS::EC2::Instance 46 | Properties: 47 | ImageId: !Ref ImageId 48 | InstanceType: t2.micro 49 | KeyName: !Ref KeyName 50 | UserData: 51 | Fn::Base64: 52 | Fn::Sub: | 53 | #!/bin/bash -xe 54 | /opt/aws/bin/cfn-init -v \ 55 | --stack ${AWS::StackName} \ 56 | --resource Lnmp \ 57 | --configsets Configure \ 58 | --region ${AWS::Region} 59 | SecurityGroupIds: 60 | - !Ref Sg 61 | Metadata: 62 | AWS::CloudFormation::Init: 63 | configSets: 64 | Configure: 65 | - "Mysql" 66 | - "DbSetup" 67 | - "Php" 68 | - "Nginx" 69 | Mysql: 70 | packages: 71 | yum: 72 | mariadb: [] 73 | mariadb-server: [] 74 | mariadb-libs: [] 75 | files: 76 | /tmp/setup.mysql: 77 | content: !Sub | 78 | CREATE DATABASE ${DBName}; 79 | GRANT ALL ON ${DBName}.* TO '${DBUsername}'@localhost IDENTIFIED BY '${DBPassword}'; 80 | mode: 400 81 | owner: root 82 | group: root 83 | /etc/cfn/cfn-hup.conf: 84 | content: !Sub | 85 | [main] 86 | stack=${AWS::StackId} 87 | region=${AWS::Region} 88 | mode: 400 89 | owner: root 90 | group: root 91 | /etc/cfn/hooks.d/cfn-auto-reloader.conf: 92 | content: !Sub | 93 | [cfn-auto-reloader-hook] 94 | triggers=post.update 95 | path=Resources.Lnmp.Metadata.AWS::CloudFormation::Init 96 | action=/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource Lnmp --configsets Configure --region ${AWS::Region} 97 | runas=root 98 | services: 99 | sysvinit: 100 | mariadb: 101 | enabled: True 102 | ensureRunning: True 103 | cfn-hup: 104 | enabled: True 105 | ensureRunning: True 106 | files: 107 | - "/etc/cfn/cfn-hup.conf" 108 | - "/etc/cfn/hooks.d/cfn-auto-reloader.conf" 109 | DbSetup: 110 | commands: 111 | 01_set_mysql_root_pw: 112 | command: !Sub "mysqladmin -u root password '${DBRootPassword}'" 113 | 02_create_database: 114 | command: !Sub "mysql -u root --password='${DBRootPassword}' < /tmp/setup.mysql" 115 | test: !Sub "$(mysql ${DBUsername} -u root --password='${DBRootPassword}' >/dev/null 2>&1 128 | 129 | AWS CloudFormation PHP Sample 130 | 131 | 132 | 133 |

Welcome to the AWS CloudFormation PHP Sample

134 |

135 | "; 138 | print date("g:i A l, F j Y."); 139 | ?> 140 |

141 | "; 152 | } 153 | else 154 | { 155 | print "Server = " . $hostname . "
"; 156 | } 157 | // Get the instance-id of the instance from the instance metadata 158 | curl_setopt($curl_handle,CURLOPT_URL,'http://169.254.169.254/latest/meta-data/instance-id'); 159 | $instanceid = curl_exec($curl_handle); 160 | if (empty($instanceid)) 161 | { 162 | print "Sorry, for some reason, we got no instance id back
"; 163 | } 164 | else 165 | { 166 | print "EC2 instance-id = " . $instanceid . "
"; 167 | } 168 | $Database = "${DBName}"; 169 | $DBUser = "${DBUsername}"; 170 | $DBPassword = "${DBPassword}"; 171 | print "Database = " . $Database . "
"; 172 | $dbconnection = mysql_connect("localhost", $DBUser, $DBPassword) 173 | or die("Could not connect: " . mysql_error()); 174 | print ("Connected to $Database successfully"); 175 | mysql_close($dbconnection); 176 | ?> 177 |

PHP Information

178 |

179 | 182 | 183 | 184 | mode: 644 185 | owner: root 186 | group: root 187 | services: 188 | sysvinit: 189 | php-fpm: 190 | enabled: True 191 | ensureRunning: True 192 | Nginx: 193 | packages: 194 | yum: 195 | nginx: [] 196 | files: 197 | /etc/nginx/nginx.conf: 198 | content: | 199 | user nginx; 200 | worker_processes auto; 201 | error_log /var/log/nginx/error.log; 202 | pid /run/nginx.pid; 203 | include /usr/share/nginx/modules/*.conf; 204 | events { 205 | worker_connections 1024; 206 | } 207 | http { 208 | log_format main '\$remote_addr - \$remote_user [\$time_local] "\$request" ' 209 | '\$status \$body_bytes_sent "\$http_referer" ' 210 | '"\$http_user_agent" "\$http_x_forwarded_for"'; 211 | access_log /var/log/nginx/access.log main; 212 | sendfile on; 213 | tcp_nopush on; 214 | tcp_nodelay on; 215 | keepalive_timeout 65; 216 | types_hash_max_size 2048; 217 | include /etc/nginx/mime.types; 218 | default_type application/octet-stream; 219 | include /etc/nginx/conf.d/*.conf; 220 | } 221 | /etc/nginx/conf.d/default.conf: 222 | content: | 223 | server { 224 | listen 80; 225 | root /var/www/html; 226 | index index.php index.html index.htm; 227 | 228 | location / { 229 | try_files $uri $uri/ =404; 230 | } 231 | location ~ \.php$ { 232 | include fastcgi_params; 233 | fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name; 234 | fastcgi_pass 127.0.0.1:9000; 235 | } 236 | location ~ /\.ht { 237 | deny all; 238 | } 239 | } 240 | services: 241 | sysvinit: 242 | nginx: 243 | enabled: True 244 | ensureRunning: True 245 | Outputs: 246 | WebsiteURL: 247 | Value: !Sub "http://${Lnmp.PublicDnsName}" -------------------------------------------------------------------------------- /Chapter07/README.md: -------------------------------------------------------------------------------- 1 | # Custom resources 2 | 3 | Package lambda and store it on S3 4 | ```bash 5 | cd custom-db 6 | pip install -t . -r requirements.txt --upgrade 7 | aws s3 mb s3://masteringcloudformation 8 | zip -r lambda-cr.zip * 9 | aws s3 cp lambda-cr.zip s3://masteringcloudformation 10 | ``` 11 | 12 | Create CR and RDS stack 13 | ```bash 14 | cd .. 15 | aws cloudformation deploy --stack-name cr --template-file cr.yaml --capabilities CAPABILITY_IAM 16 | aws cloudformation deploy --template-file rds.yaml --stack-name rds --parameter-overrides VpcId=$VPC 17 | ``` 18 | 19 | Create your custom resource 20 | ```bash 21 | aws cloudformation deploy --stack-name customdb --template-file customdb.yaml 22 | ``` 23 | 24 | Test "broken" custom resource 25 | ```bash 26 | aws cloudformation deploy --stack-name customdb-broken --template-file customdb_missing_property.yaml 27 | ``` -------------------------------------------------------------------------------- /Chapter07/cr.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: CustomResource function template 3 | Parameters: 4 | S3Bucket: 5 | Type: String 6 | Default: "masteringcloudformation" 7 | S3Key: 8 | Type: String 9 | Default: "lambda-cr.zip" 10 | Resources: 11 | 12 | CrFunction: 13 | Type: "AWS::Lambda::Function" 14 | Properties: 15 | Code: 16 | S3Bucket: !Ref S3Bucket 17 | S3Key: !Ref S3Key 18 | Handler: "customdb.handler" 19 | Runtime: "python3.7" 20 | Timeout: 30 21 | Role: !GetAtt CrRole.Arn 22 | 23 | CrRole: 24 | Type: "AWS::IAM::Role" 25 | Properties: 26 | AssumeRolePolicyDocument: 27 | Version: "2012-10-17" 28 | Statement: 29 | - Effect: Allow 30 | Principal: 31 | Service: 32 | - lambda.amazonaws.com 33 | Action: 34 | - sts:AssumeRole 35 | Policies: 36 | - PolicyName: root 37 | PolicyDocument: 38 | Version: "2012-10-17" 39 | Statement: 40 | - Effect: Allow 41 | Action: 42 | - logs:CreateLogGroup 43 | - logs:CreateLogStream 44 | - logs:PutLogEvents 45 | Resource: arn:aws:logs:*:*:* 46 | 47 | Outputs: 48 | CrArn: 49 | Value: !GetAtt CrFunction.Arn 50 | Export: 51 | Name: CrArn -------------------------------------------------------------------------------- /Chapter07/custom-db/customdb.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import pymysql 4 | import sys 5 | 6 | SUCCESS = "SUCCESS" 7 | FAILED = "FAILED" 8 | 9 | class CustomResourceException(Exception): 10 | pass 11 | 12 | def create_or_update_db(dbname, dbuser, dbpassword, rdsendpoint, rdsuser, rdspassword): 13 | create_db_query = f"CREATE DATABASE {dbname};" 14 | create_user_query = f"CREATE USER '{dbuser}'@'%' IDENTIFIED BY '{dbpassword}';" 15 | grant_query = f"GRANT ALL PRIVILEGES ON {dbname}.* TO '{dbuser}'@'%';" 16 | flush_query = "FLUSH PRIVILEGES;" 17 | try: 18 | conn = pymysql.connect(host=rdsendpoint, 19 | user=rdsuser, 20 | password=rdspassword) 21 | cursor = conn.cursor() 22 | cursor.execute(create_db_query) 23 | cursor.execute(create_user_query) 24 | cursor.execute(grant_query) 25 | cursor.execute(flush_query) 26 | cursor.close() 27 | conn.commit() 28 | conn.close() 29 | except Exception as err: 30 | raise CustomResourceException(err) 31 | 32 | 33 | def delete_db(dbname, dbuser, rdsendpoint, rdsuser, rdspassword): 34 | delete_db_query = f"DROP DATABASE {dbname}" 35 | delete_user_query = f"DROP USER '{dbuser}'" 36 | db_exists_query = f"SHOW DATABASES LIKE '{dbname}'" 37 | user_exists_query = f"SELECT user FROM mysql.user where user='{dbuser}'" 38 | try: 39 | conn = pymysql.connect(host=rdsendpoint, 40 | user=rdsuser, 41 | password=rdspassword) 42 | cursor = conn.cursor() 43 | if cursor.execute(db_exists_query): 44 | cursor.execute(delete_db_query) 45 | if cursor.execute(user_exists_query): 46 | cursor.execute(delete_user_query) 47 | cursor.close() 48 | conn.commit() 49 | conn.close() 50 | except Exception as err: 51 | raise CustomResourceException(err) 52 | 53 | 54 | def handler(event, context): 55 | input_props = event["ResourceProperties"] 56 | required_props = ["DBName", "RDSEndpoint", "RDSUser", "RDSPassword"] 57 | missing_props = [prop for prop in required_props if prop not in input_props] 58 | if missing_props: 59 | if event['RequestType'] == "Delete": 60 | send(event, context, SUCCESS, response_data={}) 61 | sys.exit(0) 62 | reason = f"Required properties are missing: {missing_props}" 63 | send(event, context, FAILED, response_reason=reason, response_data={}) 64 | raise CustomResourceException(reason) 65 | db_name = input_props["DBName"] 66 | rds_endpoint = input_props["RDSEndpoint"] 67 | rds_user = input_props["RDSUser"] 68 | rds_password = input_props["RDSPassword"] 69 | 70 | if "DBUser" not in input_props or len(input_props["DBUser"]) == 0: 71 | db_user = db_name 72 | else: 73 | db_user = input_props["DBUser"] 74 | 75 | if "DBPassword" not in input_props or len(input_props["DBPassword"]) == 0: 76 | db_password = db_name 77 | else: 78 | db_password = input_props["DBPassword"] 79 | 80 | try: 81 | if event["RequestType"] == "Delete": 82 | delete_db(db_name, db_user, rds_endpoint, rds_user, rds_password) 83 | elif event["RequestType"] in ("Create", "Update"): 84 | create_or_update_db(db_name, db_user, db_password, rds_endpoint, rds_user, rds_password) 85 | except CustomResourceException as err: 86 | send(event, context, FAILED, responseReason=err, physicalResourceId="", responseData={}) 87 | sys.exit(1) 88 | 89 | send(event, context, SUCCESS, physicalResourceId=db_name, responseData={}) 90 | 91 | 92 | def send(event, context, response_status, response_data, response_reason="", physical_resource_id=None, no_echo=False): 93 | response_url = event["ResponseURL"] 94 | 95 | print(response_url) 96 | 97 | response_body = {} 98 | response_body["Status"] = response_status 99 | response_body["Reason"] = response_reason 100 | response_body["PhysicalResourceId"] = physical_resource_id or context.log_stream_name 101 | response_body["StackId"] = event["StackId"] 102 | response_body["RequestId"] = event["RequestId"] 103 | response_body["LogicalResourceId"] = event["LogicalResourceId"] 104 | response_body["NoEcho"] = no_echo 105 | response_body["Data"] = response_data 106 | 107 | response_body = json.dumps(response_body) 108 | 109 | print(f"Response body:\n{response_body}") 110 | 111 | headers = { 112 | "content-type": "", 113 | "content-length": str(len(response_body)) 114 | } 115 | 116 | try: 117 | response = requests.put(response_url, 118 | data=response_body, 119 | headers=headers) 120 | print(f"Status code: {response.reason}") 121 | except Exception as err: 122 | print(f"send(..) failed executing requests.put(..): {err}") 123 | -------------------------------------------------------------------------------- /Chapter07/custom-db/requirements.txt: -------------------------------------------------------------------------------- 1 | PyMySQL==0.9.3 2 | botocore==1.14.6 3 | requests==2.22.0 -------------------------------------------------------------------------------- /Chapter07/customdb.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: Custom DB 3 | Parameters: 4 | DBName: 5 | Type: String 6 | Default: mydb 7 | 8 | DBUser: 9 | Type: String 10 | Default: mydbuser 11 | 12 | DBPassword: 13 | Type: String 14 | Default: foobar1234 15 | 16 | RdsUser: 17 | Type: String 18 | Default: rdsuser 19 | 20 | RdsPassword: 21 | Type: String 22 | Default: barfoo12344321 23 | 24 | Resources: 25 | CustomDb: 26 | Type: "Custom::DB" 27 | Properties: 28 | ServiceToken: !ImportValue CrArn 29 | DBName: !Ref DBName 30 | DBUser: !Ref DBUser 31 | DBPassword: !Ref DBPassword 32 | RDSEndpoint: !ImportValue RdsEndpoint 33 | RDSUser: !Ref RdsUser 34 | RDSPassword: !Ref RdsPassword 35 | -------------------------------------------------------------------------------- /Chapter07/customdb_missing_property.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: Custom DB 3 | Parameters: 4 | DBName: 5 | Type: String 6 | Default: mydb 7 | 8 | DBUser: 9 | Type: String 10 | Default: mydbuser 11 | 12 | DBPassword: 13 | Type: String 14 | Default: foobar1234 15 | 16 | RdsUser: 17 | Type: String 18 | Default: rdsuser 19 | 20 | RdsPassword: 21 | Type: String 22 | Default: barfoo12344321 23 | 24 | Resources: 25 | CustomDb: 26 | Type: "Custom::DB" 27 | Properties: 28 | ServiceToken: !ImportValue CrArn 29 | DBUser: !Ref DBUser 30 | DBPassword: !Ref DBPassword 31 | RDSEndpoint: !ImportValue RdsEndpoint 32 | RDSUser: !Ref RdsUser 33 | RDSPassword: !Ref RdsPassword 34 | -------------------------------------------------------------------------------- /Chapter07/rds.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: DB instance 3 | 4 | Parameters: 5 | 6 | VpcId: 7 | Type: AWS::EC2::VPC::Id 8 | 9 | RdsUser: 10 | Type: String 11 | Default: rdsuser 12 | 13 | RdsPassword: 14 | Type: String 15 | Default: barfoo12344321 16 | NoEcho: true 17 | 18 | Resources: 19 | 20 | RdsSg: 21 | Type: "AWS::EC2::SecurityGroup" 22 | Properties: 23 | GroupDescription: "allow incoming traffic" 24 | VpcId: !Ref VpcId 25 | SecurityGroupIngress: 26 | - IpProtocol: "tcp" 27 | FromPort: 3306 28 | ToPort: 3306 29 | CidrIp: 0.0.0.0/0 30 | 31 | RdsDatabase: 32 | Type: "AWS::RDS::DBInstance" 33 | Properties: 34 | Engine: "MySQL" 35 | EngineVersion: "5.7.37" 36 | DBInstanceClass: "db.t2.micro" 37 | MasterUsername: !Ref RdsUser 38 | MasterUserPassword: !Ref RdsPassword 39 | PubliclyAccessible: True 40 | AllocatedStorage: "8" 41 | VPCSecurityGroups: 42 | - !GetAtt RdsSg.GroupId 43 | 44 | Outputs: 45 | RdsEndpoint: 46 | Value: !GetAtt RdsDatabase.Endpoint.Address 47 | Export: 48 | Name: RdsEndpoint -------------------------------------------------------------------------------- /Chapter08/README.md: -------------------------------------------------------------------------------- 1 | # CloudFormation Registry 2 | 3 | ## DynamoDB Item Extension 4 | 5 | 1. Create prerequisite stack 6 | ```bash 7 | cd dynamodb-item 8 | aws cloudformation deploy --stack-name extension-dynamo-item --template-file prerequisite.yaml --capabilities CAPABILITY_IAM 9 | ``` 10 | 11 | 2. Get IAM role ARN, LogGroup and DynamodB table names 12 | ```bash 13 | LOGGROUP=$(aws cloudformation describe-stacks --stack-name extension-dynamo-item --query 'Stacks[0].Outputs[0].OutputValue' --output text) 14 | TABLE=$(aws cloudformation describe-stacks --stack-name extension-dynamo-item --query 'Stacks[0].Outputs[1].OutputValue' --output text) 15 | ROLE=$(aws cloudformation describe-stacks --stack-name extension-dynamo-item --query 'Stacks[0].Outputs[2].OutputValue' --output text) 16 | ``` 17 | 18 | 3. Activate DynamoDB Item extension 19 | ```bash 20 | aws cloudformation activate-type --type RESOURCE --type-name AwsCommunity::DynamoDB::Item --execution-role-arn $ROLE --logging-config LogRoleArn=$ROLE,LogGroupName=$LOGGROUP --publisher-id c830e97710da0c9954d80ba8df021e5439e7134b 21 | ``` 22 | 23 | 4. Deploy a template with DynamoDB item 24 | ```bash 25 | aws cloudformation deploy --stack-name configuration-items --template-file item.yaml --parameter-overrides TableName=$TABLE 26 | ``` 27 | 28 | ## Private Registry 29 | 30 | 1. Deploy RDS stack if not already 31 | ```bash 32 | cd private-database-registry 33 | aws cloudformation deploy --stack-name rds --template-file rds.yaml --parameter-overrides VpcId=$VPCID # Locate your default VPC 34 | ``` 35 | 36 | 2. Install dependencies 37 | ```bash 38 | pip install cloudformation-cli aws-sam-cli # PIP installation 39 | brew update && brew install cloudformation-cli aws-sam-cli # Brew installation 40 | pip install -r database-resource-type/requirements.txt 41 | ``` 42 | 43 | 3. Update test input adding RDS endpoint. 44 | 45 | 4. Prepare and test the resource type 46 | ```bash 47 | cd database-resource-type 48 | cfn submit --dry-run 49 | sam local start-lambda # run in a separate terminal window 50 | cfn test 51 | ``` 52 | 53 | 5. Upload the resource type extension to registry 54 | ```bash 55 | cfn submit -v --region $REGION # pick any of your choice, recommend using the same region as RDS stack 56 | ``` 57 | 58 | 6. Deploy custom database using private registry 59 | ```bash 60 | cd .. 61 | aws cloudformation deploy --stack-name mydatabase --template-file database.yaml 62 | ``` 63 | 64 | 7. Clean up 65 | ```bash 66 | aws cloudformation delete-stack --stack-name mydatabase 67 | aws cloudformation delete-stack --stack-name rds 68 | aws cloudformation deregister-type --type RESOURCE --type-name 'Org::Storage::Database' 69 | ``` -------------------------------------------------------------------------------- /Chapter08/dynamodb-item/item.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: Maintain items in DynamoDB table 3 | Parameters: 4 | TableName: 5 | Type: String 6 | 7 | Resources: 8 | LogLevel: 9 | Type: "AwsCommunity::DynamoDB::Item" 10 | Properties: 11 | TableName: !Ref TableName 12 | Item: 13 | LogLevel: 14 | S: "DEBUG" 15 | Keys: 16 | - AttributeName: "configItem" 17 | AttributeType: "S" 18 | AttributeValue: "ApplicationLogLevel" 19 | ZookeeperNodes: 20 | Type: "AwsCommunity::DynamoDB::Item" 21 | Properties: 22 | TableName: !Ref TableName 23 | Item: 24 | Nodes: 25 | L: 26 | - S: "10.0.0.10" 27 | - S: "10.0.0.20" 28 | - S: "10.0.0.30" 29 | Keys: 30 | - AttributeName: "configItem" 31 | AttributeType: "S" 32 | AttributeValue: "ZookeeperNodes" 33 | -------------------------------------------------------------------------------- /Chapter08/dynamodb-item/prerequisite.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: > 3 | Resources required to prepare for 4 | using the DynamoDB Item resource type 5 | 6 | Resources: 7 | ExtensionIamRole: 8 | Type: "AWS::IAM::Role" 9 | Properties: 10 | AssumeRolePolicyDocument: 11 | Version: "2012-10-17" 12 | Statement: 13 | - Sid: "AllowAssumeRole" 14 | Effect: "Allow" 15 | Principal: 16 | Service: 17 | - "resources.cloudformation.amazonaws.com" 18 | Action: "sts:AssumeRole" 19 | 20 | ExtensionLogGroup: 21 | Type: "AWS::Logs::LogGroup" 22 | Properties: 23 | RetentionInDays: 7 24 | 25 | LogPolicy: 26 | Type: "AWS::IAM::Policy" 27 | Properties: 28 | PolicyName: "ExtensionLogPolicy" 29 | PolicyDocument: 30 | Version: "2012-10-17" 31 | Statement: 32 | - Sid: "AllowExtensionLogging" 33 | Effect: "Allow" 34 | Action: 35 | - "logs:CreateLogStream" 36 | - "logs:PutLogEvents" 37 | Resource: 38 | - !GetAtt ExtensionLogGroup.Arn 39 | - !Sub "${ExtensionLogGroup.Arn}:log-stream:*" 40 | Roles: 41 | - !Ref ExtensionIamRole 42 | 43 | DynamoPolicy: 44 | Type: "AWS::IAM::Policy" 45 | Properties: 46 | PolicyName: "ExtensionDynamoDBPolicy" 47 | PolicyDocument: 48 | Version: "2012-10-17" 49 | Statement: 50 | - Sid: "AllowExtensionAccessToDynamoDBTable" 51 | Effect: "Allow" 52 | Action: 53 | - "dynamodb:*Item" 54 | Resource: 55 | - !GetAtt Table.Arn 56 | Roles: 57 | - !Ref ExtensionIamRole 58 | 59 | Table: 60 | Type: "AWS::DynamoDB::Table" 61 | Properties: 62 | BillingMode: "PAY_PER_REQUEST" 63 | AttributeDefinitions: 64 | - AttributeName: "configItem" 65 | AttributeType: "S" 66 | KeySchema: 67 | - 68 | AttributeName: "configItem" 69 | KeyType: "HASH" 70 | 71 | Outputs: 72 | RoleArn: 73 | Value: !GetAtt ExtensionIamRole.Arn 74 | LogGroup: 75 | Value: !Ref ExtensionLogGroup 76 | DynamoDbTable: 77 | Value: !Ref Table 78 | -------------------------------------------------------------------------------- /Chapter08/private-database-registry/database-resource-type/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | db.sqlite3-journal 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # IPython 80 | profile_default/ 81 | ipython_config.py 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # pipenv 87 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 88 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 89 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 90 | # install all needed dependencies. 91 | #Pipfile.lock 92 | 93 | # celery beat schedule file 94 | celerybeat-schedule 95 | 96 | # SageMath parsed files 97 | *.sage.py 98 | 99 | # Environments 100 | .env 101 | .venv 102 | env/ 103 | venv/ 104 | ENV/ 105 | env.bak/ 106 | venv.bak/ 107 | 108 | # Spyder project settings 109 | .spyderproject 110 | .spyproject 111 | 112 | # Rope project settings 113 | .ropeproject 114 | 115 | # mkdocs documentation 116 | /site 117 | 118 | # mypy 119 | .mypy_cache/ 120 | .dmypy.json 121 | dmypy.json 122 | 123 | # Pyre type checker 124 | .pyre/ 125 | 126 | # contains credentials 127 | sam-tests/ 128 | 129 | rpdk.log* 130 | 131 | # build artifacts 132 | org-storage-database.zip -------------------------------------------------------------------------------- /Chapter08/private-database-registry/database-resource-type/.rpdk-config: -------------------------------------------------------------------------------- 1 | { 2 | "artifact_type": "RESOURCE", 3 | "typeName": "Org::Storage::Database", 4 | "language": "python39", 5 | "runtime": "python3.9", 6 | "entrypoint": "org_storage_database.handlers.resource", 7 | "testEntrypoint": "org_storage_database.handlers.test_entrypoint", 8 | "settings": { 9 | "version": false, 10 | "subparser_name": null, 11 | "verbose": 0, 12 | "force": false, 13 | "type_name": null, 14 | "artifact_type": null, 15 | "endpoint_url": null, 16 | "region": null, 17 | "target_schemas": [], 18 | "profile": null, 19 | "use_docker": true, 20 | "no_docker": false, 21 | "protocolVersion": "2.0.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Chapter08/private-database-registry/database-resource-type/README.md: -------------------------------------------------------------------------------- 1 | # Org::Storage::Database 2 | 3 | Congratulations on starting development! Next steps: 4 | 5 | 1. Write the JSON schema describing your resource, `org-storage-database.json` 6 | 2. Implement your resource handlers in `org_storage_database/handlers.py` 7 | 8 | > Don't modify `models.py` by hand, any modifications will be overwritten when the `generate` or `package` commands are run. 9 | 10 | Implement CloudFormation resource here. Each function must always return a ProgressEvent. 11 | 12 | ```python 13 | ProgressEvent( 14 | # Required 15 | # Must be one of OperationStatus.IN_PROGRESS, OperationStatus.FAILED, OperationStatus.SUCCESS 16 | status=OperationStatus.IN_PROGRESS, 17 | # Required on SUCCESS (except for LIST where resourceModels is required) 18 | # The current resource model after the operation; instance of ResourceModel class 19 | resourceModel=model, 20 | resourceModels=None, 21 | # Required on FAILED 22 | # Customer-facing message, displayed in e.g. CloudFormation stack events 23 | message="", 24 | # Required on FAILED: a HandlerErrorCode 25 | errorCode=HandlerErrorCode.InternalFailure, 26 | # Optional 27 | # Use to store any state between re-invocation via IN_PROGRESS 28 | callbackContext={}, 29 | # Required on IN_PROGRESS 30 | # The number of seconds to delay before re-invocation 31 | callbackDelaySeconds=0, 32 | ) 33 | ``` 34 | 35 | Failures can be passed back to CloudFormation by either raising an exception from `cloudformation_cli_python_lib.exceptions`, or setting the ProgressEvent's `status` to `OperationStatus.FAILED` and `errorCode` to one of `cloudformation_cli_python_lib.HandlerErrorCode`. There is a static helper function, `ProgressEvent.failed`, for this common case. 36 | 37 | ## What's with the type hints? 38 | 39 | We hope they'll be useful for getting started quicker with an IDE that support type hints. Type hints are optional - if your code doesn't use them, it will still work. 40 | -------------------------------------------------------------------------------- /Chapter08/private-database-registry/database-resource-type/docs/README.md: -------------------------------------------------------------------------------- 1 | # Org::Storage::Database 2 | 3 | Manage a MySQL database on a shared RDS instance 4 | 5 | ## Syntax 6 | 7 | To declare this entity in your AWS CloudFormation template, use the following syntax: 8 | 9 | ### JSON 10 | 11 |

 12 | {
 13 |     "Type" : "Org::Storage::Database",
 14 |     "Properties" : {
 15 |         "DatabaseName" : String,
 16 |         "DatabaseUser" : String,
 17 |         "DatabasePassword" : String,
 18 |         "RdsHost" : String,
 19 |         "RdsUser" : String,
 20 |         "RdsPassword" : String
 21 |     }
 22 | }
 23 | 
24 | 25 | ### YAML 26 | 27 |
 28 | Type: Org::Storage::Database
 29 | Properties:
 30 |     DatabaseName: String
 31 |     DatabaseUser: String
 32 |     DatabasePassword: String
 33 |     RdsHost: String
 34 |     RdsUser: String
 35 |     RdsPassword: String
 36 | 
37 | 38 | ## Properties 39 | 40 | #### DatabaseName 41 | 42 | Database name, alphanumeric 43 | 44 | _Required_: Yes 45 | 46 | _Type_: String 47 | 48 | _Pattern_: ^[A-Za-z0-9]+$ 49 | 50 | _Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) 51 | 52 | #### DatabaseUser 53 | 54 | Database user, alphanumeric 55 | 56 | _Required_: Yes 57 | 58 | _Type_: String 59 | 60 | _Pattern_: ^[A-Za-z0-9]+$ 61 | 62 | _Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) 63 | 64 | #### DatabasePassword 65 | 66 | Database password, minimum 8 symbols 67 | 68 | _Required_: Yes 69 | 70 | _Type_: String 71 | 72 | _Minimum Length_: 8 73 | 74 | _Pattern_: ^[A-Za-z0-9]+$ 75 | 76 | _Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) 77 | 78 | #### RdsHost 79 | 80 | Database endpoint 81 | 82 | _Required_: Yes 83 | 84 | _Type_: String 85 | 86 | _Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) 87 | 88 | #### RdsUser 89 | 90 | RDS admin user 91 | 92 | _Required_: Yes 93 | 94 | _Type_: String 95 | 96 | _Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) 97 | 98 | #### RdsPassword 99 | 100 | RDS admin password 101 | 102 | _Required_: Yes 103 | 104 | _Type_: String 105 | 106 | _Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) 107 | 108 | ## Return Values 109 | 110 | ### Ref 111 | 112 | When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the DatabaseName. 113 | -------------------------------------------------------------------------------- /Chapter08/private-database-registry/database-resource-type/inputs/inputs_1_create.json: -------------------------------------------------------------------------------- 1 | { 2 | "DatabaseName": "myDatabase", 3 | "DatabaseUser": "MyDatabaseUser", 4 | "DatabasePassword": "SuperUser!1234", 5 | "RdsHost": "RDSHOST", 6 | "RdsUser": "rdsuser", 7 | "RdsPassword": "barfoo12344321" 8 | } 9 | -------------------------------------------------------------------------------- /Chapter08/private-database-registry/database-resource-type/inputs/inputs_1_invalid.json: -------------------------------------------------------------------------------- 1 | { 2 | "DatabaseName": "myDatabase", 3 | "DatabaseUser": "MyDatabaseUser", 4 | "DatabasePassword": "", 5 | "RdsHost": "RDSHOST", 6 | "RdsUser": "rdsuser", 7 | "RdsPassword": "barfoo12344321" 8 | } 9 | -------------------------------------------------------------------------------- /Chapter08/private-database-registry/database-resource-type/inputs/inputs_1_update.json: -------------------------------------------------------------------------------- 1 | { 2 | "DatabaseName": "myDatabase", 3 | "DatabaseUser": "MyDatabaseUser", 4 | "DatabasePassword": "SuperUser12", 5 | "RdsHost": "RDSHOST", 6 | "RdsUser": "rdsuser", 7 | "RdsPassword": "barfoo12344321" 8 | } 9 | -------------------------------------------------------------------------------- /Chapter08/private-database-registry/database-resource-type/org-storage-database.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "Org::Storage::Database", 3 | "description": "Manage a MySQL database on a shared RDS instance", 4 | "sourceUrl": "https://github.com/aws-cloudformation/aws-cloudformation-rpdk.git", 5 | "replacementStrategy": "delete_then_create", 6 | "tagging": { 7 | "taggable": false 8 | }, 9 | "properties": { 10 | "DatabaseName": { 11 | "description": "Database name, alphanumeric", 12 | "type": "string", 13 | "pattern": "^[A-Za-z0-9]+$" 14 | }, 15 | "DatabaseUser": { 16 | "description": "Database user, alphanumeric", 17 | "type": "string", 18 | "pattern": "^[A-Za-z0-9]+$" 19 | }, 20 | "DatabasePassword": { 21 | "description": "Database password, minimum 8 symbols", 22 | "type": "string", 23 | "pattern": "^[A-Za-z0-9]+$", 24 | "minLength": 8 25 | }, 26 | "RdsHost": { 27 | "description": "Database endpoint", 28 | "type": "string" 29 | }, 30 | "RdsUser": { 31 | "description": "RDS admin user", 32 | "type": "string" 33 | }, 34 | "RdsPassword": { 35 | "description": "RDS admin password", 36 | "type": "string" 37 | } 38 | }, 39 | "additionalProperties": false, 40 | "required": [ 41 | "DatabaseName", 42 | "DatabaseUser", 43 | "DatabasePassword", 44 | "RdsHost", 45 | "RdsUser", 46 | "RdsPassword" 47 | ], 48 | "createOnlyProperties": [ 49 | "/properties/DatabaseName" 50 | ], 51 | "primaryIdentifier": [ 52 | "/properties/DatabaseName" 53 | ], 54 | "handlers": { 55 | "create": { 56 | "permissions": [] 57 | }, 58 | "read": { 59 | "permissions": [] 60 | }, 61 | "update": { 62 | "permissions": [] 63 | }, 64 | "delete": { 65 | "permissions": [] 66 | }, 67 | "list": { 68 | "permissions": [] 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Chapter08/private-database-registry/database-resource-type/requirements.txt: -------------------------------------------------------------------------------- 1 | cloudformation-cli-python-lib>=2.1.9 2 | PyMySQL==1.1.0 -------------------------------------------------------------------------------- /Chapter08/private-database-registry/database-resource-type/resource-role.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: > 3 | This CloudFormation template creates a role assumed by CloudFormation 4 | during CRUDL operations to mutate resources on behalf of the customer. 5 | 6 | Resources: 7 | ExecutionRole: 8 | Type: AWS::IAM::Role 9 | Properties: 10 | MaxSessionDuration: 8400 11 | AssumeRolePolicyDocument: 12 | Version: '2012-10-17' 13 | Statement: 14 | - Effect: Allow 15 | Principal: 16 | Service: resources.cloudformation.amazonaws.com 17 | Action: sts:AssumeRole 18 | Condition: 19 | StringEquals: 20 | aws:SourceAccount: 21 | Ref: AWS::AccountId 22 | StringLike: 23 | aws:SourceArn: 24 | Fn::Sub: arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:type/resource/Org-Storage-Database/* 25 | Path: "/" 26 | Policies: 27 | - PolicyName: ResourceTypePolicy 28 | PolicyDocument: 29 | Version: '2012-10-17' 30 | Statement: 31 | - Effect: Deny 32 | Action: 33 | - "*" 34 | Resource: "*" 35 | Outputs: 36 | ExecutionRoleArn: 37 | Value: 38 | Fn::GetAtt: ExecutionRole.Arn 39 | -------------------------------------------------------------------------------- /Chapter08/private-database-registry/database-resource-type/src/org_storage_database/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-AWS-CloudFormation-Second-Edition/7bffc66ec68256d25d79d0b9b106eed1566216ee/Chapter08/private-database-registry/database-resource-type/src/org_storage_database/__init__.py -------------------------------------------------------------------------------- /Chapter08/private-database-registry/database-resource-type/src/org_storage_database/dbclient.py: -------------------------------------------------------------------------------- 1 | import pymysql 2 | 3 | class DbClient(): 4 | CREATE_DB = "CREATE DATABASE {};" 5 | CREATE_USER = "CREATE USER '{}'@'%' IDENTIFIED BY '{}';" 6 | GRANT = "GRANT ALL PRIVILEGES ON {}.* TO '{}'@'%';" 7 | FLUSH = "FLUSH PRIVILEGES;" 8 | DELETE_DB = "DROP DATABASE {}" 9 | DELETE_USER = "DROP USER '{}'" 10 | DB_EXISTS = "SHOW DATABASES LIKE '{}'" 11 | USER_EXISTS = "SELECT user FROM mysql.user where user='{}'" 12 | CHANGE_USER_PASSWORD = "ALTER USER '{}'@'%' IDENTIFIED BY '{}'" 13 | 14 | def __init__(self, endpoint, user, password): 15 | self.connection = pymysql.connect(host=endpoint, 16 | user=user, 17 | password=password 18 | ) 19 | self.cursor = self.connection.cursor() 20 | 21 | def db_exists(self, db): 22 | if self.cursor.execute(self.DB_EXISTS.format(db)): 23 | return True 24 | 25 | def user_exists(self, user): 26 | if self.cursor.execute(self.USER_EXISTS.format(user)): 27 | return True 28 | 29 | def delete_db(self, db): 30 | self.cursor.execute(self.DELETE_DB.format(db)) 31 | 32 | def delete_user(self, user): 33 | self.cursor.execute(self.DELETE_USER.format(user)) 34 | 35 | def delete(self, db, user): 36 | if self.db_exists(db): 37 | self.delete_db(db) 38 | 39 | if self.user_exists(user): 40 | self.delete_user(user) 41 | 42 | def create_or_update(self, db, user, pw): 43 | if self.db_exists(db): 44 | self.delete_db(db) 45 | 46 | if self.user_exists(user): 47 | self.delete_user(user) 48 | 49 | self.cursor.execute(self.CREATE_USER.format(user,pw)) 50 | self.cursor.execute(self.CREATE_DB.format(db)) 51 | self.cursor.execute(self.GRANT.format(db, user)) 52 | self.cursor.execute(self.FLUSH) 53 | 54 | def change_user_password(self, user, pw): 55 | if not self.user_exists(user): 56 | raise ValueError(f'User {user} does not exist!') 57 | 58 | self.cursor.execute(self.CHANGE_USER_PASSWORD.format(user, pw)) 59 | -------------------------------------------------------------------------------- /Chapter08/private-database-registry/database-resource-type/src/org_storage_database/handlers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any, MutableMapping, Optional 3 | from cloudformation_cli_python_lib import ( 4 | Action, 5 | HandlerErrorCode, 6 | OperationStatus, 7 | ProgressEvent, 8 | Resource, 9 | SessionProxy, 10 | exceptions, 11 | identifier_utils, 12 | ) 13 | 14 | from .dbclient import DbClient 15 | from .models import ResourceHandlerRequest, ResourceModel 16 | 17 | 18 | # Use this logger to forward log messages to CloudWatch Logs. 19 | LOG = logging.getLogger(__name__) 20 | TYPE_NAME = "Org::Storage::Database" 21 | 22 | resource = Resource(TYPE_NAME, ResourceModel) 23 | test_entrypoint = resource.test_entrypoint 24 | 25 | def get_db_connection_parameters(request: ResourceHandlerRequest) -> (str, str, str): 26 | return ( 27 | request.desiredResourceState.RdsHost, 28 | request.desiredResourceState.RdsUser, 29 | request.desiredResourceState.RdsPassword 30 | ) 31 | 32 | def get_db_properties(request: ResourceHandlerRequest) -> (str, str, str): 33 | return ( 34 | request.desiredResourceState.DatabaseName, 35 | request.desiredResourceState.DatabaseUser, 36 | request.desiredResourceState.DatabasePassword, 37 | ) 38 | 39 | def only_password_change(request: ResourceHandlerRequest) -> bool: 40 | if ( 41 | request.desiredResourceState.DatabaseName == request.previousResourceState.DatabaseName and 42 | request.desiredResourceState.DatabaseUser == request.previousResourceState.DatabaseUser 43 | ): 44 | return True 45 | 46 | 47 | 48 | @resource.handler(Action.CREATE) 49 | def create_handler( 50 | session: Optional[SessionProxy], 51 | request: ResourceHandlerRequest, 52 | callback_context: MutableMapping[str, Any], 53 | ) -> ProgressEvent: 54 | model = request.desiredResourceState 55 | progress: ProgressEvent = ProgressEvent( 56 | status=OperationStatus.IN_PROGRESS, 57 | resourceModel=model, 58 | ) 59 | 60 | host, rds_user, rds_pw = get_db_connection_parameters(request) 61 | db, user, pw = get_db_properties(request) 62 | 63 | client = DbClient(host, rds_user, rds_pw) 64 | if client.user_exists(user): 65 | LOG.error(f'User {user} already exists! Consider importing it to the stack') 66 | return ProgressEvent.failed(HandlerErrorCode.AlreadyExists, 67 | f'User {user} already exists! Consider importing it to the stack') 68 | 69 | if client.db_exists(db): 70 | LOG.error(f'Database {db} already exists! Consider importing it to the stack') 71 | return ProgressEvent.failed(HandlerErrorCode.AlreadyExists, 72 | f'Database {db} already exists! Consider importing it to the stack') 73 | 74 | try: 75 | client.create_or_update(db, user, pw) 76 | LOG.info('Database created') 77 | progress.status = OperationStatus.SUCCESS 78 | except Exception as e: 79 | LOG.error(f'Failed to create resource. Reason: {e}') 80 | return ProgressEvent.failed(HandlerErrorCode.HandlerInternalFailure, 81 | f'Failed to create resource. Reason: {e}') 82 | 83 | return read_handler(session, request, callback_context) 84 | 85 | 86 | @resource.handler(Action.UPDATE) 87 | def update_handler( 88 | session: Optional[SessionProxy], 89 | request: ResourceHandlerRequest, 90 | callback_context: MutableMapping[str, Any], 91 | ) -> ProgressEvent: 92 | model = request.desiredResourceState 93 | progress: ProgressEvent = ProgressEvent( 94 | status=OperationStatus.IN_PROGRESS, 95 | resourceModel=model, 96 | ) 97 | 98 | host, rds_user, rds_pw = get_db_connection_parameters(request) 99 | db, user, pw = get_db_properties(request) 100 | client = DbClient(host, rds_user, rds_pw) 101 | 102 | if not client.db_exists(db): 103 | return ProgressEvent.failed(HandlerErrorCode.NotFound, 104 | f'Database does not exist') 105 | 106 | if not client.user_exists(user): 107 | return ProgressEvent.failed(HandlerErrorCode.NotFound, 108 | f'User does not exist') 109 | 110 | try: 111 | if only_password_change(request): 112 | client.change_user_password(user, pw) 113 | else: 114 | client.create_or_update(db, user, pw) 115 | progress.status = OperationStatus.SUCCESS 116 | except Exception as e: 117 | return ProgressEvent.failed(HandlerErrorCode.HandlerInternalFailure, 118 | f'Failed to create resource. Reason: {e}') 119 | 120 | return read_handler(session, request, callback_context) 121 | 122 | 123 | @resource.handler(Action.DELETE) 124 | def delete_handler( 125 | session: Optional[SessionProxy], 126 | request: ResourceHandlerRequest, 127 | callback_context: MutableMapping[str, Any], 128 | ) -> ProgressEvent: 129 | model = request.desiredResourceState 130 | progress: ProgressEvent = ProgressEvent( 131 | status=OperationStatus.IN_PROGRESS, 132 | resourceModel=None, 133 | ) 134 | 135 | host, rds_user, rds_pw = get_db_connection_parameters(request) 136 | db, user, _ = get_db_properties(request) 137 | client = DbClient(host, rds_user, rds_pw) 138 | 139 | if not client.db_exists(db): 140 | return ProgressEvent.failed(HandlerErrorCode.NotFound, 141 | f'Database {db} does not exist') 142 | 143 | if not client.user_exists(user): 144 | return ProgressEvent.failed(HandlerErrorCode.NotFound, 145 | f'User {user} does not exist') 146 | 147 | try: 148 | client.delete(db, user) 149 | progress.status = OperationStatus.SUCCESS 150 | except Exception as e: 151 | return ProgressEvent.failed(HandlerErrorCode.HandlerInternalFailure, 152 | f'Failed to delete resource. Reason: {e}') 153 | 154 | return progress 155 | 156 | 157 | @resource.handler(Action.READ) 158 | def read_handler( 159 | session: Optional[SessionProxy], 160 | request: ResourceHandlerRequest, 161 | callback_context: MutableMapping[str, Any], 162 | ) -> ProgressEvent: 163 | model = request.desiredResourceState 164 | 165 | host, rds_user, rds_pw = get_db_connection_parameters(request) 166 | db, user, pw = get_db_properties(request) 167 | client = DbClient(host, rds_user, rds_pw) 168 | 169 | if not client.db_exists(db): 170 | return ProgressEvent.failed(HandlerErrorCode.NotFound, 171 | f'Database {db} does not exist') 172 | 173 | if not client.user_exists(user): 174 | return ProgressEvent.failed(HandlerErrorCode.NotFound, 175 | f'User {user} does not exist') 176 | 177 | return ProgressEvent( 178 | status=OperationStatus.SUCCESS, 179 | resourceModel=model, 180 | ) 181 | 182 | 183 | @resource.handler(Action.LIST) 184 | def list_handler( 185 | session: Optional[SessionProxy], 186 | request: ResourceHandlerRequest, 187 | callback_context: MutableMapping[str, Any], 188 | ) -> ProgressEvent: 189 | model = request.desiredResourceState 190 | host, rds_user, rds_pw = get_db_connection_parameters(request) 191 | db, user, _ = get_db_properties(request) 192 | client = DbClient(host, rds_user, rds_pw) 193 | 194 | models = [] 195 | 196 | if client.db_exists(db) and client.user_exists(user): 197 | models.append(model) 198 | 199 | return ProgressEvent( 200 | status=OperationStatus.SUCCESS, 201 | resourceModels=models, 202 | ) 203 | -------------------------------------------------------------------------------- /Chapter08/private-database-registry/database-resource-type/src/org_storage_database/models.py: -------------------------------------------------------------------------------- 1 | # DO NOT modify this file by hand, changes will be overwritten 2 | from dataclasses import dataclass 3 | 4 | from cloudformation_cli_python_lib.interface import ( 5 | BaseModel, 6 | BaseResourceHandlerRequest, 7 | ) 8 | from cloudformation_cli_python_lib.recast import recast_object 9 | from cloudformation_cli_python_lib.utils import deserialize_list 10 | 11 | import sys 12 | from inspect import getmembers, isclass 13 | from typing import ( 14 | AbstractSet, 15 | Any, 16 | Generic, 17 | Mapping, 18 | MutableMapping, 19 | Optional, 20 | Sequence, 21 | Type, 22 | TypeVar, 23 | ) 24 | 25 | T = TypeVar("T") 26 | 27 | 28 | def set_or_none(value: Optional[Sequence[T]]) -> Optional[AbstractSet[T]]: 29 | if value: 30 | return set(value) 31 | return None 32 | 33 | 34 | @dataclass 35 | class ResourceHandlerRequest(BaseResourceHandlerRequest): 36 | # pylint: disable=invalid-name 37 | desiredResourceState: Optional["ResourceModel"] 38 | previousResourceState: Optional["ResourceModel"] 39 | typeConfiguration: Optional["TypeConfigurationModel"] 40 | 41 | 42 | @dataclass 43 | class ResourceModel(BaseModel): 44 | DatabaseName: Optional[str] 45 | DatabaseUser: Optional[str] 46 | DatabasePassword: Optional[str] 47 | RdsHost: Optional[str] 48 | RdsUser: Optional[str] 49 | RdsPassword: Optional[str] 50 | 51 | @classmethod 52 | def _deserialize( 53 | cls: Type["_ResourceModel"], 54 | json_data: Optional[Mapping[str, Any]], 55 | ) -> Optional["_ResourceModel"]: 56 | if not json_data: 57 | return None 58 | dataclasses = {n: o for n, o in getmembers(sys.modules[__name__]) if isclass(o)} 59 | recast_object(cls, json_data, dataclasses) 60 | return cls( 61 | DatabaseName=json_data.get("DatabaseName"), 62 | DatabaseUser=json_data.get("DatabaseUser"), 63 | DatabasePassword=json_data.get("DatabasePassword"), 64 | RdsHost=json_data.get("RdsHost"), 65 | RdsUser=json_data.get("RdsUser"), 66 | RdsPassword=json_data.get("RdsPassword"), 67 | ) 68 | 69 | 70 | # work around possible type aliasing issues when variable has same name as a model 71 | _ResourceModel = ResourceModel 72 | 73 | 74 | @dataclass 75 | class TypeConfigurationModel(BaseModel): 76 | 77 | @classmethod 78 | def _deserialize( 79 | cls: Type["_TypeConfigurationModel"], 80 | json_data: Optional[Mapping[str, Any]], 81 | ) -> Optional["_TypeConfigurationModel"]: 82 | if not json_data: 83 | return None 84 | return cls( 85 | ) 86 | 87 | 88 | # work around possible type aliasing issues when variable has same name as a model 89 | _TypeConfigurationModel = TypeConfigurationModel 90 | 91 | 92 | -------------------------------------------------------------------------------- /Chapter08/private-database-registry/database-resource-type/template.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: AWS SAM template for the Org::Storage::Database resource type 4 | 5 | Globals: 6 | Function: 7 | Timeout: 180 # docker start-up times can be long for SAM CLI 8 | MemorySize: 256 9 | 10 | Resources: 11 | TypeFunction: 12 | Type: AWS::Serverless::Function 13 | Properties: 14 | Handler: org_storage_database.handlers.resource 15 | Runtime: python3.9 16 | CodeUri: build/ 17 | 18 | TestEntrypoint: 19 | Type: AWS::Serverless::Function 20 | Properties: 21 | Handler: org_storage_database.handlers.test_entrypoint 22 | Runtime: python3.9 23 | CodeUri: build/ 24 | -------------------------------------------------------------------------------- /Chapter08/private-database-registry/database.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: Custom DB 3 | Parameters: 4 | DBName: 5 | Type: String 6 | Default: mydb 7 | 8 | DBUser: 9 | Type: String 10 | Default: mydbuser 11 | 12 | DBPassword: 13 | Type: String 14 | Default: foobar1234 15 | 16 | RdsUser: 17 | Type: String 18 | Default: rdsuser 19 | 20 | RdsPassword: 21 | Type: String 22 | Default: barfoo12344321 23 | 24 | Resources: 25 | Database: 26 | Type: Org::Storage::Database 27 | Properties: 28 | DatabaseName: !Ref DBName 29 | DatabaseUser: !Ref DBUser 30 | DatabasePassword: !Ref DBPassword 31 | RdsHost: !ImportValue RdsEndpoint 32 | RdsUser: !Ref RdsUser 33 | RdsPassword: !Ref RdsPassword 34 | -------------------------------------------------------------------------------- /Chapter08/private-database-registry/rds.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: DB instance 3 | 4 | Parameters: 5 | 6 | VpcId: 7 | Type: AWS::EC2::VPC::Id 8 | 9 | RdsUser: 10 | Type: String 11 | Default: rdsuser 12 | 13 | RdsPassword: 14 | Type: String 15 | Default: barfoo12344321 16 | NoEcho: true 17 | 18 | Resources: 19 | 20 | RdsSg: 21 | Type: "AWS::EC2::SecurityGroup" 22 | Properties: 23 | GroupDescription: "allow incoming traffic" 24 | VpcId: !Ref VpcId 25 | SecurityGroupIngress: 26 | - IpProtocol: "tcp" 27 | FromPort: 3306 28 | ToPort: 3306 29 | CidrIp: 0.0.0.0/0 30 | 31 | RdsDatabase: 32 | Type: "AWS::RDS::DBInstance" 33 | Properties: 34 | Engine: "MySQL" 35 | EngineVersion: "5.7.37" 36 | DBInstanceClass: "db.t2.micro" 37 | MasterUsername: !Ref RdsUser 38 | MasterUserPassword: !Ref RdsPassword 39 | PubliclyAccessible: True 40 | AllocatedStorage: "8" 41 | VPCSecurityGroups: 42 | - !GetAtt RdsSg.GroupId 43 | 44 | Outputs: 45 | RdsEndpoint: 46 | Value: !GetAtt RdsDatabase.Endpoint.Address 47 | Export: 48 | Name: RdsEndpoint -------------------------------------------------------------------------------- /Chapter09/README.md: -------------------------------------------------------------------------------- 1 | # Template Macros 2 | 3 | ## AMI filler 4 | 5 | 1. Deploy Macro stack 6 | ```bash 7 | cd macros/ami-filter 8 | aws cloudformation deploy --stack-name ami-filler-macro --template-file macro.yaml --capabilities CAPABILITY_IAM 9 | ``` 10 | 2. Deploy Launch Template stack 11 | ```bash 12 | aws cloudformation deploy --stack-name lt --template-file lt.yaml 13 | ``` 14 | 15 | ## Standard App 16 | 1. Deploy Core stack 17 | ```bash 18 | cd ../standard-app 19 | aws cloudformation deploy --stack-name core --template-file core.yaml --capabilities CAPABILITY_IAM 20 | ``` 21 | 2. Create bucket and upload the code 22 | ```bash 23 | aws s3 mb s3://masteringcfn 24 | zip lambda-macro.zip standard_app.py 25 | aws s3 cp lambda-macro.zip s3://masteringcfn 26 | ``` 27 | 3. Deploy Macro stack 28 | ```bash 29 | aws cloudformation deploy --stack-name standard-app-macro --template-file macro.yaml --capabilities CAPABILITY_IAM 30 | ``` 31 | 4. Deploy Standard app stack 32 | ```bash 33 | aws cloudformation deploy --stack-name app --template-file app.yaml 34 | ``` 35 | 36 | ## Modules 37 | 38 | 1. Publish the module 39 | ```bash 40 | cd ../../modules/module 41 | cfn submit --region REGION # pick yours 42 | ``` 43 | 2. Deploy core stack 44 | ```bash 45 | cd .. 46 | aws cloudformation deploy --stack-name modulecore --template-file core.yaml --capabilities CAPABILITY_IAM 47 | ``` 48 | 3. Deploy modular stack 49 | ```bash 50 | aws cloudformation deploy --stack-name app --template-file standard_app.yaml 51 | ``` 52 | 4. Change the template, set `NeedsBalancer` to true 53 | 5. Deploy update 54 | ```bash 55 | aws cloudformation deploy --stack-name app --template-file standard_app.yaml 56 | ``` 57 | -------------------------------------------------------------------------------- /Chapter09/macros/ami-filler/amifinder.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | 3 | image_names = { 4 | 'amazonlinux': 'al2023-ami-2023.1.20230809.0-kernel-6.1-x86_64', 5 | 'ubuntu': 'ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-20230516', 6 | 'rhel': 'RHEL-9.2.0_HVM-20230503-x86_64-41-Hourly2-GP2', 7 | 'sles': 'suse-sles-15-sp5-v20230620-hvm-ssd-x86_64' 8 | } 9 | 10 | 11 | def get_image(img_name): 12 | client = boto3.client('ec2') 13 | resp = client.describe_images( 14 | Filters=[{'Name': 'name', 15 | 'Values': [img_name]}]) 16 | return resp['Images'][0]['ImageId'] 17 | 18 | 19 | def lambda_handler(event, context): 20 | response = {} 21 | response['requestId'] = event['requestId'] 22 | response['fragment'] = {'ImageId': ''} 23 | response['status'] = 'SUCCESS' 24 | osfamily = event['params']['OSFamily'] 25 | 26 | if osfamily not in image_names.keys(): 27 | response['status'] = 'FAILURE' 28 | return response 29 | 30 | image_id = get_image(image_names[osfamily]) 31 | response['fragment']['ImageId'] = image_id 32 | return response -------------------------------------------------------------------------------- /Chapter09/macros/ami-filler/lt.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Resources: 3 | Lt: 4 | Type: "AWS::EC2::LaunchTemplate" 5 | Properties: 6 | LaunchTemplateData: 7 | Fn::Transform: 8 | Name: AMIFiller 9 | Parameters: 10 | OSFamily: "ubuntu" 11 | -------------------------------------------------------------------------------- /Chapter09/macros/ami-filler/macro.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Resources: 3 | MacroFunctionRole: 4 | Type: "AWS::IAM::Role" 5 | Properties: 6 | AssumeRolePolicyDocument: 7 | Version: "2012-10-17" 8 | Statement: 9 | - 10 | Action: 11 | - "sts:AssumeRole" 12 | Effect: "Allow" 13 | Principal: 14 | Service: 15 | - "lambda.amazonaws.com" 16 | ManagedPolicyArns: 17 | - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 18 | Policies: 19 | - PolicyName: "GetImages" 20 | PolicyDocument: 21 | Version: "2012-10-17" 22 | Statement: 23 | - 24 | Action: 25 | - "ec2:DescribeImages" 26 | Effect: Allow 27 | Resource: "*" 28 | 29 | MacroFunction: 30 | Type: "AWS::Lambda::Function" 31 | Properties: 32 | Handler: "index.lambda_handler" 33 | MemorySize: 128 34 | Runtime: "python3.7" 35 | Timeout: 30 36 | Role: !GetAtt MacroFunctionRole.Arn 37 | Code: 38 | ZipFile: | 39 | import boto3 40 | image_names = { 41 | 'amazonlinux': 'al2023-ami-2023.1.20230809.0-kernel-6.1-x86_64', 42 | 'ubuntu': 'ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-20230516', 43 | 'rhel': 'RHEL-9.2.0_HVM-20230503-x86_64-41-Hourly2-GP2', 44 | 'sles': 'suse-sles-15-sp5-v20230620-hvm-ssd-x86_64' 45 | } 46 | def get_image(img_name): 47 | client = boto3.client('ec2') 48 | resp = client.describe_images( 49 | Filters=[{'Name': 'name', 'Values': [img_name]}]) 50 | return resp['Images'][0]['ImageId'] 51 | def lambda_handler(event, context): 52 | print(event) 53 | response = {} 54 | response['requestId'] = event['requestId'] 55 | response['fragment'] = {'ImageId': ''} 56 | response['status'] = 'SUCCESS' 57 | osfamily = event['params']['OSFamily'] 58 | if osfamily not in image_names.keys(): 59 | response['status'] = 'FAILURE' 60 | return response 61 | image_id = get_image(image_names[osfamily]) 62 | response['fragment']['ImageId'] = image_id 63 | return response 64 | Macro: 65 | Type: "AWS::CloudFormation::Macro" 66 | Properties: 67 | Name: "AMIFiller" 68 | FunctionName: !GetAtt MacroFunction.Arn 69 | -------------------------------------------------------------------------------- /Chapter09/macros/standard-app/app.yaml: -------------------------------------------------------------------------------- 1 | Transform: StandardApplication 2 | Resources: 3 | Application: 4 | Properties: 5 | ApplicationImage: "nginx:latest" 6 | TaskCount: 1 7 | Memory: 512 8 | CPU: 256 9 | ApplicationPort: 80 10 | # RDSEngine: "" 11 | # RDSSize: "" 12 | # RDSMultiAz: False 13 | # NeedsBalancer: False 14 | # PubliclyAvailable: False 15 | -------------------------------------------------------------------------------- /Chapter09/macros/standard-app/core.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: This is the Core Infrastructure template 3 | Parameters: 4 | VpcCidr: 5 | Type: String 6 | AllowedPattern: '(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})' 7 | Default: 10.0.0.0/16 8 | 9 | 10 | Resources: 11 | 12 | EcsCluster: 13 | Type: "AWS::ECS::Cluster" 14 | 15 | ExecRole: 16 | Type: "AWS::IAM::Role" 17 | Properties: 18 | AssumeRolePolicyDocument: 19 | Statement: 20 | - Action: 21 | - sts:AssumeRole 22 | Effect: Allow 23 | Principal: 24 | Service: ecs-tasks.amazonaws.com 25 | Sid: AllowECSTaskExecution 26 | Policies: 27 | - PolicyDocument: 28 | Statement: 29 | - Action: 30 | - ecr:GetAuthorizationToken 31 | - ecr:BatchCheckLayerAvailability 32 | - ecr:GetDownloadUrlForLayer 33 | - ecr:BatchGetImage 34 | - logs:CreateLogStream 35 | - logs:PutLogEvents 36 | Effect: Allow 37 | Resource: ["*"] 38 | Sid: AllowECSTaskExecution 39 | PolicyName: ecs 40 | 41 | Igw: 42 | Type: "AWS::EC2::InternetGateway" 43 | 44 | IgwAttach: 45 | Type: "AWS::EC2::VPCGatewayAttachment" 46 | Properties: 47 | InternetGatewayId: !Ref Igw 48 | VpcId: !Ref Vpc 49 | 50 | AppSubnet01: 51 | Type: "AWS::EC2::Subnet" 52 | Properties: 53 | AvailabilityZone: !Select [0, !GetAZs ""] 54 | CidrBlock: !Select [ 0, !Cidr [ !Ref VpcCidr, 12, 8 ] ] 55 | MapPublicIpOnLaunch: True 56 | VpcId: !Ref Vpc 57 | 58 | AppSubnet02: 59 | Type: "AWS::EC2::Subnet" 60 | Properties: 61 | AvailabilityZone: !Select [1, !GetAZs ''] 62 | CidrBlock: !Select [ 1, !Cidr [ !Ref VpcCidr, 12, 8 ] ] 63 | MapPublicIpOnLaunch: True 64 | VpcId: !Ref Vpc 65 | 66 | AppSubnetAssoc01: 67 | Type: "AWS::EC2::SubnetRouteTableAssociation" 68 | Properties: 69 | RouteTableId: !Ref PublicRouteTable 70 | SubnetId: !Ref AppSubnet01 71 | 72 | AppSubnetAssoc02: 73 | Type: "AWS::EC2::SubnetRouteTableAssociation" 74 | Properties: 75 | RouteTableId: !Ref PublicRouteTable 76 | SubnetId: !Ref AppSubnet02 77 | 78 | PublicRoute: 79 | Type: "AWS::EC2::Route" 80 | DependsOn: IgwAttach 81 | Properties: 82 | DestinationCidrBlock: "0.0.0.0/0" 83 | GatewayId: !Ref Igw 84 | RouteTableId: !Ref PublicRouteTable 85 | 86 | PublicRouteTable: 87 | Type: "AWS::EC2::RouteTable" 88 | Properties: 89 | VpcId: !Ref Vpc 90 | 91 | PublicSubnet01: 92 | Type: "AWS::EC2::Subnet" 93 | Properties: 94 | AvailabilityZone: !Select [0, !GetAZs ""] 95 | CidrBlock: !Select [ 2, !Cidr [ !Ref VpcCidr, 12, 8 ] ] 96 | MapPublicIpOnLaunch: True 97 | VpcId: !Ref Vpc 98 | 99 | PublicSubnet02: 100 | Type: "AWS::EC2::Subnet" 101 | Properties: 102 | AvailabilityZone: !Select [1, !GetAZs ""] 103 | CidrBlock: !Select [ 3, !Cidr [ !Ref VpcCidr, 12, 8 ] ] 104 | MapPublicIpOnLaunch: True 105 | VpcId: !Ref Vpc 106 | 107 | PublicSubnetAssoc01: 108 | Type: "AWS::EC2::SubnetRouteTableAssociation" 109 | Properties: 110 | RouteTableId: !Ref PublicRouteTable 111 | SubnetId: !Ref PublicSubnet01 112 | 113 | PublicSubnetAssoc02: 114 | Type: "AWS::EC2::SubnetRouteTableAssociation" 115 | Properties: 116 | RouteTableId: !Ref PublicRouteTable 117 | SubnetId: !Ref PublicSubnet02 118 | 119 | DbSubnet01: 120 | Type: "AWS::EC2::Subnet" 121 | Properties: 122 | AvailabilityZone: !Select [0, !GetAZs ""] 123 | CidrBlock: !Select [ 4, !Cidr [ !Ref VpcCidr, 12, 8 ] ] 124 | MapPublicIpOnLaunch: True 125 | VpcId: !Ref Vpc 126 | 127 | DbSubnet02: 128 | Type: "AWS::EC2::Subnet" 129 | Properties: 130 | AvailabilityZone: !Select [1, !GetAZs ''] 131 | CidrBlock: !Select [ 5, !Cidr [ !Ref VpcCidr, 12, 8 ] ] 132 | MapPublicIpOnLaunch: True 133 | VpcId: !Ref Vpc 134 | 135 | DbSubnetAssoc01: 136 | Type: "AWS::EC2::SubnetRouteTableAssociation" 137 | Properties: 138 | RouteTableId: !Ref PublicRouteTable 139 | SubnetId: !Ref DbSubnet01 140 | 141 | DbSubnetAssoc02: 142 | Type: "AWS::EC2::SubnetRouteTableAssociation" 143 | Properties: 144 | RouteTableId: !Ref PublicRouteTable 145 | SubnetId: !Ref DbSubnet02 146 | 147 | DbSubnetGroup: 148 | Type: "AWS::RDS::DBSubnetGroup" 149 | Properties: 150 | DBSubnetGroupDescription: "DB Subnet Group" 151 | SubnetIds: 152 | - !Ref DbSubnet01 153 | - !Ref DbSubnet02 154 | 155 | Vpc: 156 | Type: "AWS::EC2::VPC" 157 | Properties: 158 | CidrBlock: !Ref VpcCidr 159 | EnableDnsHostnames: True 160 | EnableDnsSupport: True 161 | 162 | Outputs: 163 | 164 | FargateCluster: 165 | Description: "Fargate Cluster name" 166 | Value: !Ref EcsCluster 167 | Export: 168 | Name: EcsCluster 169 | 170 | ExecRole: 171 | Description: "Fargate Task Exec Role" 172 | Value: !GetAtt ExecRole.Arn 173 | Export: 174 | Name: ExecRole 175 | 176 | AppSubnet1: 177 | Description: "App Subnet 1 ID" 178 | Value: !Ref AppSubnet01 179 | Export: 180 | Name: AppSubnet01 181 | 182 | AppSubnet2: 183 | Description: "App Subnet 2 ID" 184 | Value: !Ref AppSubnet02 185 | Export: 186 | Name: AppSubnet02 187 | 188 | PublicSubnet1: 189 | Description: "Public Subnet 1 ID" 190 | Value: !Ref PublicSubnet01 191 | Export: 192 | Name: PublicSubnet01 193 | 194 | PublicSubnet2: 195 | Description: "Public Subnet 2 ID" 196 | Value: !Ref PublicSubnet02 197 | Export: 198 | Name: PublicSubnet02 199 | 200 | DbSubnetGroup: 201 | Description: "Db Subnet Group" 202 | Value: !Ref DbSubnetGroup 203 | Export: 204 | Name: DbSubnetGroup 205 | 206 | VpcId: 207 | Description: VpcID 208 | Value: !Ref Vpc 209 | Export: 210 | Name: VpcId 211 | -------------------------------------------------------------------------------- /Chapter09/macros/standard-app/macro.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Parameters: 3 | S3Bucket: 4 | Type: String 5 | Default: "masteringcfn" 6 | S3Key: 7 | Type: String 8 | Default: "lambda-macro.zip" 9 | Resources: 10 | MacroFunctionRole: 11 | Type: "AWS::IAM::Role" 12 | Properties: 13 | AssumeRolePolicyDocument: 14 | Version: "2012-10-17" 15 | Statement: 16 | - 17 | Action: 18 | - "sts:AssumeRole" 19 | Effect: Allow 20 | Principal: 21 | Service: 22 | - "lambda.amazonaws.com" 23 | ManagedPolicyArns: 24 | - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 25 | 26 | MacroFunction: 27 | Type: "AWS::Lambda::Function" 28 | Properties: 29 | Handler: "standard_app.lambda_handler" 30 | MemorySize: 128 31 | Runtime: python3.7 32 | Timeout: 30 33 | Role: !GetAtt MacroFunctionRole.Arn 34 | Code: 35 | S3Bucket: !Ref S3Bucket 36 | S3Key: !Ref S3Key 37 | 38 | Macro: 39 | Type: "AWS::CloudFormation::Macro" 40 | Properties: 41 | Name: "StandardApplication" 42 | FunctionName: !GetAtt MacroFunction.Arn 43 | -------------------------------------------------------------------------------- /Chapter09/macros/standard-app/standard_app.py: -------------------------------------------------------------------------------- 1 | def render_ecs(input): 2 | ecs_definition = {} 3 | ecs_task_definition = { 4 | 'TaskDefinition': { 5 | 'Type': 'AWS::ECS::TaskDefinition', 6 | 'Properties': { 7 | 'Family': '', # placeholder 8 | 'RequiresCompatibilities': ['FARGATE'], 9 | 'ContainerDefinitions': [ 10 | { 11 | 'Name': '', # placeholder 12 | 'Image': '', # placeholder 13 | 'PortMappings': [ 14 | { 15 | 'ContainerPort': '', # placeholder 16 | 'Protocol': 'tcp' 17 | } 18 | ], 19 | 'LogConfiguration': { 20 | 'LogDriver': 'awslogs', 21 | 'Options': { 22 | 'awslogs-group': {'Ref': 'Logs'}, 23 | 'awslogs-region': {'Ref': 'AWS::Region'}, 24 | 'awslogs-stream-prefix': '' # placeholder 25 | } 26 | } 27 | } 28 | ], 29 | 'Cpu': '', # placeholder 30 | 'ExecutionRoleArn': {'Fn::ImportValue': 'ExecRole'}, 31 | 'Memory': '', # placeholder 32 | 'NetworkMode': 'awsvpc' 33 | } 34 | } 35 | } 36 | application_image = input['ApplicationImage'] 37 | application_name = application_image.split(':')[0] 38 | ecs_task_definition_properties = ecs_task_definition['TaskDefinition']['Properties'] 39 | ecs_task_definition_properties['Family'] = application_name 40 | ecs_task_definition_properties['ContainerDefinitions'][0]['Name'] = application_name 41 | ecs_task_definition_properties['ContainerDefinitions'][0]['Image'] = application_image 42 | ecs_task_definition_properties['ContainerDefinitions'][0]['PortMappings'][0]['ContainerPort'] = input['ApplicationPort'] 43 | ecs_task_definition_properties['ContainerDefinitions'][0]['LogConfiguration']['Options']['awslogs-stream-prefix'] = f'{application_name}-' 44 | ecs_task_definition_properties['Cpu'] = str(input['CPU']) 45 | ecs_task_definition_properties['Memory'] = str(input['Memory']) 46 | ecs_task_definition['TaskDefinition']['Properties'] = ecs_task_definition_properties 47 | 48 | ecs_service_definition = { 49 | 'Service': { 50 | 'Type': 'AWS::ECS::Service', 51 | 'Properties': { 52 | 'Cluster': {'Fn::ImportValue': 'EcsCluster'}, 53 | 'DesiredCount': 0, 54 | 'TaskDefinition': {'Ref': 'TaskDefinition'}, 55 | 'LaunchType': 'FARGATE', 56 | 'NetworkConfiguration': { 57 | 'AwsvpcConfiguration': { 58 | 'SecurityGroups': [ 59 | {'Ref': 'Sg'} 60 | ], 61 | 'Subnets': [ 62 | {'Fn::ImportValue': 'AppSubnet01'}, 63 | {'Fn::ImportValue': 'AppSubnet02'} 64 | ], 65 | 'AssignPublicIp': 'ENABLED' 66 | } 67 | } 68 | } 69 | } 70 | } 71 | ecs_service_definition_properties = ecs_service_definition['Service']['Properties'] 72 | ecs_service_definition_properties['DesiredCount'] = input['TaskCount'] 73 | if 'NeedsBalancer' in input and input['NeedsBalancer']: 74 | ecs_service_definition_properties['LoadBalancers'] = [{ 75 | 'TargetGroupArn': {'Ref': 'Tg'}, 76 | 'ContainerPort': str(input['ApplicationPort']), 77 | 'ContainerName': application_name 78 | }] 79 | ecs_service_definition_properties['HealthCheckGracePeriodSeconds'] = 30 80 | 81 | ecs_definition['Logs'] = { 82 | 'Type': 'AWS::Logs::LogGroup', 83 | 'Properties': { 84 | 'LogGroupName': application_name, 85 | 'RetentionInDays': 1 86 | } 87 | } 88 | 89 | ecs_definition['Sg'] = { 90 | 'Type': 'AWS::EC2::SecurityGroup', 91 | 'Properties': { 92 | 'GroupDescription': 'Security Group', 93 | 'VpcId': {'Fn::ImportValue': 'VpcId'} 94 | } 95 | } 96 | if 'NeedsBalancer' in input and input['NeedsBalancer']: 97 | ecs_definition['Sg']['Properties']['SecurityGroupIngress'] = [ 98 | { 99 | 'IpProtocol': 'tcp', 100 | 'FromPort': str(input['ApplicationPort']), 101 | 'ToPort': str(input['ApplicationPort']), 102 | 'SourceSecurityGroupId': {'Ref': 'LbSg'} 103 | } 104 | ] 105 | 106 | ecs_definition.update(ecs_task_definition) 107 | ecs_definition.update(ecs_service_definition) 108 | 109 | return ecs_definition 110 | 111 | 112 | def render_rds(input): 113 | rds_definition = {} 114 | return rds_definition 115 | 116 | 117 | def render_elb(input): 118 | elb_definition = {} 119 | return elb_definition 120 | 121 | 122 | def lambda_handler(event, context): 123 | response = {} 124 | response['requestId'] = event['requestId'] 125 | response['status'] = 'SUCCESS' 126 | response['fragment'] = {} 127 | 128 | required_properties = ['ApplicationImage', 'ApplicationPort', 'TaskCount', 'Memory', 'CPU'] 129 | properties = event['fragment']['Resources']['Application']['Properties'] 130 | for req in required_properties: 131 | if req not in properties.keys() or not properties[req]: 132 | response['status'] = 'FAILED' 133 | return response 134 | 135 | rds_props = {} 136 | rds_props['RDSEngine'] = '' 137 | rds_props['RDSSize'] = 'db.t2.micro' 138 | rds_props['RDSMultiAz'] = 'false' 139 | 140 | for key in rds_props.keys(): 141 | if key in properties.keys() and properties[key]: 142 | rds_props[key] = properties[key] 143 | 144 | elb_props = {} 145 | elb_props['NeedBalancer'] = False 146 | elb_props['PubliclyAvailable'] = False 147 | 148 | for key in elb_props.keys(): 149 | if key in properties.keys() and properties[key]: 150 | elb_props[key] = properties[key] 151 | 152 | ecs_props = {} 153 | ecs_props['ApplicationImage'] = properties['ApplicationImage'] 154 | ecs_props['ApplicationPort'] = properties['ApplicationPort'] 155 | ecs_props['TaskCount'] = properties['TaskCount'] 156 | ecs_props['Memory'] = properties['Memory'] 157 | ecs_props['CPU'] = properties['CPU'] 158 | if 'NeedsBalancer' in properties: 159 | ecs_props['NeedsBalancer'] = properties['NeedsBalancer'] 160 | ecs_definition = render_ecs(ecs_props) 161 | resources = {'Resources': {}} 162 | resources['Resources'].update(ecs_definition) 163 | response['fragment'].update(resources) 164 | return response 165 | -------------------------------------------------------------------------------- /Chapter09/modules/core.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: This is the Core Infrastructure template 3 | Parameters: 4 | VpcCidr: 5 | Type: String 6 | AllowedPattern: '(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})' 7 | Default: 10.0.0.0/16 8 | 9 | 10 | Resources: 11 | 12 | EcsCluster: 13 | Type: AWS::ECS::Cluster 14 | 15 | ExecRole: 16 | Type: AWS::IAM::Role 17 | Properties: 18 | AssumeRolePolicyDocument: 19 | Statement: 20 | - Action: 21 | - sts:AssumeRole 22 | Effect: Allow 23 | Principal: 24 | Service: ecs-tasks.amazonaws.com 25 | Sid: AllowECSTaskExecution 26 | Policies: 27 | - PolicyDocument: 28 | Statement: 29 | - Action: 30 | - ecr:GetAuthorizationToken 31 | - ecr:BatchCheckLayerAvailability 32 | - ecr:GetDownloadUrlForLayer 33 | - ecr:BatchGetImage 34 | - logs:CreateLogStream 35 | - logs:PutLogEvents 36 | Effect: Allow 37 | Resource: 38 | - '*' 39 | Sid: AllowECSTaskExecution 40 | PolicyName: ecs 41 | 42 | Igw: 43 | Type: AWS::EC2::InternetGateway 44 | 45 | IgwAttach: 46 | Type: AWS::EC2::VPCGatewayAttachment 47 | Properties: 48 | InternetGatewayId: !Ref Igw 49 | VpcId: !Ref Vpc 50 | 51 | AppSubnet01: 52 | Type: AWS::EC2::Subnet 53 | Properties: 54 | AvailabilityZone: !Select [0, !GetAZs ''] 55 | CidrBlock: !Select [ 0, !Cidr [ !Ref VpcCidr, 12, 8 ] ] 56 | MapPublicIpOnLaunch: True 57 | VpcId: !Ref Vpc 58 | 59 | AppSubnet02: 60 | Type: AWS::EC2::Subnet 61 | Properties: 62 | AvailabilityZone: !Select [1, !GetAZs ''] 63 | CidrBlock: !Select [ 1, !Cidr [ !Ref VpcCidr, 12, 8 ] ] 64 | MapPublicIpOnLaunch: True 65 | VpcId: !Ref Vpc 66 | 67 | AppSubnetAssoc01: 68 | Type: AWS::EC2::SubnetRouteTableAssociation 69 | Properties: 70 | RouteTableId: !Ref PublicRouteTable 71 | SubnetId: !Ref AppSubnet01 72 | 73 | AppSubnetAssoc02: 74 | Type: AWS::EC2::SubnetRouteTableAssociation 75 | Properties: 76 | RouteTableId: !Ref PublicRouteTable 77 | SubnetId: !Ref AppSubnet02 78 | 79 | PublicRoute: 80 | Type: AWS::EC2::Route 81 | DependsOn: IgwAttach 82 | Properties: 83 | DestinationCidrBlock: "0.0.0.0/0" 84 | GatewayId: !Ref Igw 85 | RouteTableId: !Ref PublicRouteTable 86 | 87 | PublicRouteTable: 88 | Type: AWS::EC2::RouteTable 89 | Properties: 90 | VpcId: !Ref Vpc 91 | 92 | PublicSubnet01: 93 | Type: AWS::EC2::Subnet 94 | Properties: 95 | AvailabilityZone: !Select [0, !GetAZs ''] 96 | CidrBlock: !Select [ 2, !Cidr [ !Ref VpcCidr, 12, 8 ] ] 97 | MapPublicIpOnLaunch: True 98 | VpcId: !Ref Vpc 99 | 100 | PublicSubnet02: 101 | Type: AWS::EC2::Subnet 102 | Properties: 103 | AvailabilityZone: !Select [1, !GetAZs ''] 104 | CidrBlock: !Select [ 3, !Cidr [ !Ref VpcCidr, 12, 8 ] ] 105 | MapPublicIpOnLaunch: True 106 | VpcId: !Ref Vpc 107 | 108 | PublicSubnetAssoc01: 109 | Type: AWS::EC2::SubnetRouteTableAssociation 110 | Properties: 111 | RouteTableId: !Ref PublicRouteTable 112 | SubnetId: !Ref PublicSubnet01 113 | 114 | PublicSubnetAssoc02: 115 | Type: AWS::EC2::SubnetRouteTableAssociation 116 | Properties: 117 | RouteTableId: !Ref PublicRouteTable 118 | SubnetId: !Ref PublicSubnet02 119 | 120 | DbSubnet01: 121 | Type: AWS::EC2::Subnet 122 | Properties: 123 | AvailabilityZone: !Select [0, !GetAZs ''] 124 | CidrBlock: !Select [ 4, !Cidr [ !Ref VpcCidr, 12, 8 ] ] 125 | MapPublicIpOnLaunch: True 126 | VpcId: !Ref Vpc 127 | 128 | DbSubnet02: 129 | Type: AWS::EC2::Subnet 130 | Properties: 131 | AvailabilityZone: !Select [1, !GetAZs ''] 132 | CidrBlock: !Select [ 5, !Cidr [ !Ref VpcCidr, 12, 8 ] ] 133 | MapPublicIpOnLaunch: True 134 | VpcId: !Ref Vpc 135 | 136 | DbSubnetAssoc01: 137 | Type: AWS::EC2::SubnetRouteTableAssociation 138 | Properties: 139 | RouteTableId: !Ref PublicRouteTable 140 | SubnetId: !Ref DbSubnet01 141 | 142 | DbSubnetAssoc02: 143 | Type: AWS::EC2::SubnetRouteTableAssociation 144 | Properties: 145 | RouteTableId: !Ref PublicRouteTable 146 | SubnetId: !Ref DbSubnet02 147 | 148 | DbSubnetGroup: 149 | Type: AWS::RDS::DBSubnetGroup 150 | Properties: 151 | DBSubnetGroupDescription: "DB Subnet Group" 152 | SubnetIds: 153 | - !Ref DbSubnet01 154 | - !Ref DbSubnet02 155 | 156 | Vpc: 157 | Type: AWS::EC2::VPC 158 | Properties: 159 | CidrBlock: !Ref VpcCidr 160 | EnableDnsHostnames: True 161 | EnableDnsSupport: True 162 | 163 | Outputs: 164 | 165 | FargateCluster: 166 | Description: Fargate Cluster name 167 | Value: !Ref EcsCluster 168 | Export: 169 | Name: EcsCluster 170 | 171 | ExecRole: 172 | Description: Fargate Task Exec Role 173 | Value: !GetAtt ExecRole.Arn 174 | Export: 175 | Name: ExecRole 176 | 177 | AppSubnets: 178 | Description: App subnets 179 | Value: !Join [ ",", [ !Ref AppSubnet01, !Ref AppSubnet02 ] ] 180 | Export: 181 | Name: AppSubnets 182 | 183 | PublicSubnets: 184 | Description: Public subnets 185 | Value: !Join [ ",", [ !Ref PublicSubnet01, !Ref PublicSubnet02 ] ] 186 | Export: 187 | Name: PublicSubnets 188 | 189 | DbSubnetGroup: 190 | Description: DbSubnetGroup 191 | Value: !Ref DbSubnetGroup 192 | Export: 193 | Name: DbSubnetGroup 194 | 195 | VpcId: 196 | Description: VpcID 197 | Value: !Ref Vpc 198 | Export: 199 | Name: VpcId 200 | -------------------------------------------------------------------------------- /Chapter09/modules/module/.rpdk-config: -------------------------------------------------------------------------------- 1 | { 2 | "artifact_type": "MODULE", 3 | "typeName": "Org::Apps::StandardApp::MODULE", 4 | "settings": {} 5 | } 6 | -------------------------------------------------------------------------------- /Chapter09/modules/module/fragments/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Parameters: 3 | # You might notices that some parameters have default values and some are not 4 | # If the resource has a creation condition, but refers to a parameter 5 | # The parameter must be set despite if the resource will or will not be create 6 | # Default values here are protecting the module user from such errors when they 7 | # Don't need a database but must define its type and storage size anyway. 8 | ApplicationName: 9 | Type: String 10 | ApplicationImage: 11 | Type: String 12 | TaskCount: 13 | Type: Number 14 | Memory: 15 | Type: Number 16 | CPU: 17 | Type: Number 18 | ApplicationPort: 19 | Type: Number 20 | NeedsDatabase: 21 | Type: String 22 | AllowedValues: [True, False] 23 | RDSEngine: 24 | Type: String 25 | Default: MySQL 26 | RDSSize: 27 | Type: String 28 | Default: db.t3.small 29 | RDSMultiAz: 30 | Type: String 31 | AllowedValues: [True, False] 32 | Default: False 33 | RDSStorage: 34 | Type: Number 35 | Default: 10 36 | NeedsBalancer: 37 | Type: String 38 | AllowedValues: [True, False] 39 | PubliclyAvailable: 40 | Type: String 41 | AllowedValues: [True, False] 42 | VpcId: 43 | Type: AWS::EC2::VPC::Id 44 | PublicSubnets: 45 | Type: List 46 | AppSubnets: 47 | Type: List 48 | ExecRoleArn: 49 | Type: String 50 | EcsCluster: 51 | Type: String 52 | DbSubnetGroup: 53 | Type: String 54 | 55 | 56 | Conditions: 57 | NeedsBalancer: !Equals [!Ref NeedsBalancer, True] 58 | NeedsDatabase: !Equals [!Ref NeedsDatabase, True] 59 | PubliclyAvailable: !Equals [!Ref PubliclyAvailable, True] 60 | 61 | Resources: 62 | Logs: 63 | Type: "AWS::Logs::LogGroup" 64 | Properties: 65 | LogGroupName: !Ref ApplicationName 66 | RetentionInDays: 1 67 | 68 | LbSg: 69 | Condition: NeedsBalancer 70 | Type: "AWS::EC2::SecurityGroup" 71 | Properties: 72 | GroupDescription: "LB Security Group" 73 | VpcId: !Ref VpcId 74 | 75 | Lb: 76 | Condition: NeedsBalancer 77 | Type: "AWS::ElasticLoadBalancingV2::LoadBalancer" 78 | Properties: 79 | Scheme: !If [PubliclyAvailable, "internet-facing", "internal"] 80 | SecurityGroups: 81 | - !Ref LbSg 82 | Subnets: !Ref PublicSubnets 83 | 84 | Listener: 85 | Condition: NeedsBalancer 86 | Type: "AWS::ElasticLoadBalancingV2::Listener" 87 | Properties: 88 | DefaultActions: 89 | - Type: forward 90 | TargetGroupArn: !Ref AppTg 91 | LoadBalancerArn: !Ref Lb 92 | Port: !Ref ApplicationPort 93 | Protocol: HTTP 94 | 95 | # This wait condition handle is needed to provide a conditional DependsOn for ECS Service 96 | # Otherwise the ECS service will error out because the TG and not yet associated with ELB 97 | LbReady: 98 | Type: "AWS::CloudFormation::WaitConditionHandle" 99 | Metadata: 100 | LbPresent: !If [NeedsBalancer, !Ref Lb, !Ref AWS::NoValue] 101 | 102 | 103 | AppSg: 104 | Type: "AWS::EC2::SecurityGroup" 105 | Properties: 106 | GroupDescription: !Sub "${ApplicationName} Security Group" 107 | VpcId: !Ref VpcId 108 | 109 | AppSgRule: 110 | Condition: NeedsBalancer 111 | Type: "AWS::EC2::SecurityGroupIngress" 112 | Properties: 113 | IpProtocol: "tcp" 114 | FromPort: !Ref ApplicationPort 115 | ToPort: !Ref ApplicationPort 116 | SourceSecurityGroupId: !Ref LbSg 117 | GroupId: !Ref AppSg 118 | 119 | AppTg: 120 | Condition: NeedsBalancer 121 | Type: "AWS::ElasticLoadBalancingV2::TargetGroup" 122 | Properties: 123 | TargetType: "ip" 124 | HealthCheckIntervalSeconds: 6 125 | HealthCheckPath: "/" 126 | HealthCheckTimeoutSeconds: 5 127 | HealthyThresholdCount: 2 128 | Name: !Ref ApplicationName 129 | Port: !Ref ApplicationPort 130 | Protocol: "HTTP" 131 | UnhealthyThresholdCount: 2 132 | VpcId: !Ref VpcId 133 | EcsTask: 134 | Type: "AWS::ECS::TaskDefinition" 135 | Properties: 136 | Family: !Ref ApplicationName 137 | RequiresCompatibilities: ["FARGATE"] 138 | ContainerDefinitions: 139 | - Image: !Ref ApplicationImage 140 | Name: !Ref ApplicationName 141 | PortMappings: 142 | - ContainerPort: !Ref ApplicationPort 143 | Protocol: "tcp" 144 | LogConfiguration: 145 | LogDriver: "awslogs" 146 | Options: 147 | awslogs-group: !Ref Logs 148 | awslogs-region: !Ref AWS::Region 149 | awslogs-stream-prefix: !Sub "${ApplicationName}-" 150 | Cpu: !Ref CPU 151 | ExecutionRoleArn: !Ref ExecRoleArn 152 | Memory: !Ref Memory 153 | NetworkMode: "awsvpc" 154 | 155 | EcsService: 156 | Type: "AWS::ECS::Service" 157 | DependsOn: LbReady 158 | Properties: 159 | Cluster: !Ref EcsCluster 160 | DesiredCount: !Ref TaskCount 161 | TaskDefinition: !Ref EcsTask 162 | LaunchType: "FARGATE" 163 | NetworkConfiguration: 164 | AwsvpcConfiguration: 165 | AssignPublicIp: "ENABLED" 166 | SecurityGroups: 167 | - !Ref AppSg 168 | Subnets: !Ref AppSubnets 169 | LoadBalancers: 170 | Fn::If: 171 | - NeedsBalancer 172 | - - TargetGroupArn: !Ref AppTg 173 | ContainerPort: !Ref ApplicationPort 174 | ContainerName: !Ref ApplicationName 175 | - !Ref AWS::NoValue 176 | 177 | DatabaseSecret: 178 | Condition: NeedsDatabase 179 | Type: "AWS::SecretsManager::Secret" 180 | Properties: 181 | Description: !Sub "${ApplicationName} Database credentials" 182 | GenerateSecretString: 183 | SecretStringTemplate: !Sub '{"username": "${ApplicationName}"}' 184 | GenerateStringKey: "password" 185 | PasswordLength: 16 186 | ExcludeCharacters: '"@/\' 187 | 188 | DatabaseSg: 189 | Condition: NeedsDatabase 190 | Type: "AWS::EC2::SecurityGroup" 191 | Properties: 192 | GroupDescription: "Database security group" 193 | VpcId: !Ref VpcId 194 | 195 | Database: 196 | Condition: NeedsDatabase 197 | Type: "AWS::RDS::DBInstance" 198 | Properties: 199 | Engine: !Ref RDSEngine 200 | DBSubnetGroupName: !Ref DbSubnetGroup 201 | MasterUsername: !Sub "{{resolve:secretsmanager:${DatabaseSecret}:SecretString:username}}" 202 | MasterUserPassword: !Sub "{{resolve:secretsmanager:${DatabaseSecret}:SecretString:password}}" 203 | VPCSecurityGroups: 204 | - !Ref DatabaseSg 205 | MultiAZ: !Ref RDSMultiAz 206 | AllocatedStorage: !Ref RDSStorage 207 | DBInstanceClass: !Ref RDSSize 208 | 209 | DatabaseSgIngressRule: 210 | Condition: NeedsDatabase 211 | Type: "AWS::EC2::SecurityGroupIngress" 212 | Properties: 213 | IpProtocol: tcp 214 | FromPort: !GetAtt Database.Endpoint.Port 215 | ToPort: !GetAtt Database.Endpoint.Port 216 | SourceSecurityGroupId: !Ref AppSg 217 | GroupId: !Ref DatabaseSg 218 | -------------------------------------------------------------------------------- /Chapter09/modules/standard_app.yaml: -------------------------------------------------------------------------------- 1 | Resources: 2 | StandardApp: 3 | Type: "Org::Apps::StandardApp::MODULE" 4 | Properties: 5 | ApplicationName: nginx 6 | ApplicationImage: nginx:latest 7 | TaskCount: 1 8 | Memory: 512 9 | CPU: 256 10 | ApplicationPort: 80 11 | NeedsDatabase: False 12 | NeedsBalancer: False 13 | PubliclyAvailable: False 14 | VpcId: !ImportValue VpcId 15 | DbSubnetGroup: !ImportValue DbSubnetGroup 16 | ExecRoleArn: !ImportValue ExecRole 17 | EcsCluster: !ImportValue EcsCluster 18 | PublicSubnets: !Split [",", !ImportValue PublicSubnets] 19 | AppSubnets: !Split [",", !ImportValue AppSubnets] 20 | -------------------------------------------------------------------------------- /Chapter10/app/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | package-lock.json 3 | __pycache__ 4 | .pytest_cache 5 | .venv 6 | *.egg-info 7 | 8 | # CDK asset staging directory 9 | .cdk.staging 10 | cdk.out 11 | -------------------------------------------------------------------------------- /Chapter10/app/README.md: -------------------------------------------------------------------------------- 1 | # CDK 2 | 3 | ## Preparation 4 | Create virtualenv and install dependencies 5 | ```bash 6 | python3 -m venv venv 7 | source venv/bin/activate 8 | pip install -r requirements.txt 9 | ``` 10 | 11 | ## Run tests 12 | ```bash 13 | pip install -r requirements-dev.txt 14 | python -m pytest 15 | ``` 16 | 17 | ## Deploy all stacks 18 | ```bash 19 | cdk deploy --all 20 | ``` -------------------------------------------------------------------------------- /Chapter10/app/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import aws_cdk as cdk 3 | from app.core_stack import CoreStack 4 | from app.web_stack import WebStack 5 | from app.rds_stack import RdsStack 6 | 7 | 8 | 9 | app = cdk.App() 10 | core = CoreStack(app, 'CoreStack') 11 | web = WebStack(app, 'WebStack', vpc=core.vpc) 12 | RdsStack(app, 'RdsStack', vpc=core.vpc, webserver_sg=web.webserver_sg) 13 | 14 | app.synth() 15 | -------------------------------------------------------------------------------- /Chapter10/app/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-AWS-CloudFormation-Second-Edition/7bffc66ec68256d25d79d0b9b106eed1566216ee/Chapter10/app/app/__init__.py -------------------------------------------------------------------------------- /Chapter10/app/app/app_stack.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | # Duration, 3 | Stack, 4 | # aws_sqs as sqs, 5 | ) 6 | from constructs import Construct 7 | 8 | class AppStack(Stack): 9 | 10 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 11 | super().__init__(scope, construct_id, **kwargs) 12 | 13 | # The code that defines your stack goes here 14 | 15 | # example resource 16 | # queue = sqs.Queue( 17 | # self, "App2Queue", 18 | # visibility_timeout=Duration.seconds(300), 19 | # ) 20 | -------------------------------------------------------------------------------- /Chapter10/app/app/core_stack.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import Stack, Fn 2 | from constructs import Construct 3 | import aws_cdk.aws_ec2 as ec2 4 | import aws_cdk.aws_iam as iam 5 | 6 | 7 | class CoreStack(Stack): 8 | 9 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 10 | super().__init__(scope, construct_id, **kwargs) 11 | # IAM section 12 | admin_role = iam.Role(self, 'admin', assumed_by=iam.AccountPrincipal(Fn.ref('AWS::AccountId'))) 13 | self.dev_role = iam.Role(self, 'developer', assumed_by=iam.AccountPrincipal(Fn.ref('AWS::AccountId'))) 14 | admin_role.add_managed_policy(iam.ManagedPolicy.from_aws_managed_policy_name('AdministratorAccess')) 15 | self.dev_role.add_managed_policy(iam.ManagedPolicy.from_aws_managed_policy_name('ReadOnlyAccess')) 16 | # VPC section 17 | self.vpc = ec2.Vpc(self, 'vpc', ip_addresses=ec2.IpAddresses.cidr('10.0.0.0/16'), enable_dns_hostnames=True, enable_dns_support=True, 18 | max_azs=3, nat_gateways=1, 19 | subnet_configuration=[ 20 | ec2.SubnetConfiguration(name='Public', subnet_type=ec2.SubnetType.PUBLIC, cidr_mask=24), 21 | ec2.SubnetConfiguration(name='Private', subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS, cidr_mask=24) 22 | ]) 23 | -------------------------------------------------------------------------------- /Chapter10/app/app/rds_stack.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import Stack, RemovalPolicy 2 | from constructs import Construct 3 | import aws_cdk.aws_ec2 as ec2 4 | import aws_cdk.aws_rds as rds 5 | 6 | 7 | class RdsStack(Stack): 8 | 9 | def __init__(self, scope: Construct, construct_id: str, vpc: ec2.Vpc, webserver_sg: ec2.SecurityGroup, **kwargs) -> None: 10 | super().__init__(scope, construct_id, **kwargs) 11 | self.vpc = vpc 12 | self.webserver_sg = webserver_sg 13 | rds_instance = rds.DatabaseInstance(self, 'Rds', credentials=rds.Credentials.from_generated_secret('admin'), 14 | database_name="db", engine=rds.DatabaseInstanceEngine.MYSQL, 15 | vpc=vpc, instance_type=ec2.InstanceType.of( 16 | ec2.InstanceClass.BURSTABLE3, 17 | ec2.InstanceSize.MICRO), 18 | removal_policy=RemovalPolicy.DESTROY, deletion_protection=False) 19 | rds_instance.connections.allow_from(webserver_sg, ec2.Port.tcp(3306)) 20 | -------------------------------------------------------------------------------- /Chapter10/app/app/web_stack.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import Stack 2 | from constructs import Construct 3 | import aws_cdk.aws_autoscaling as autoscaling 4 | import aws_cdk.aws_ec2 as ec2 5 | import aws_cdk.aws_elasticloadbalancingv2 as elbv2 6 | 7 | 8 | class WebStack(Stack): 9 | 10 | def __init__(self, scope: Construct, construct_id: str, vpc: ec2.Vpc, **kwargs) -> None: 11 | super().__init__(scope, construct_id, **kwargs) 12 | self.vpc = vpc 13 | userdata = '''#!/bin/sh 14 | yum install httpd -y 15 | systemctl enable httpd 16 | systemctl start httpd 17 | echo " Example Web Server" > /var/www/html/index.html 18 | echo "" >> /var/www/html/index.html 19 | echo "

Welcome AWS $(hostname -f)

" >> /var/www/html/index.html 20 | echo "
" >> /var/www/html/index.html 21 | curl http://169.254.169.254/latest/meta-data/instance-id >> /var/www/html/index.html 22 | echo "
" >> /var/www/html/index.html''' 23 | websrv = ec2.UserData.for_linux() 24 | websrv.add_commands(userdata) 25 | asg = autoscaling.AutoScalingGroup(self, 'WebAsg', 26 | instance_type=ec2.InstanceType.of( 27 | ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.MICRO), 28 | machine_image=ec2.AmazonLinuxImage(), vpc=vpc, 29 | vpc_subnets=ec2.SubnetSelection(subnet_group_name='Private'), 30 | min_capacity=1, max_capacity=1, user_data=websrv) 31 | alb = elbv2.ApplicationLoadBalancer(self, 'WebLb', vpc=vpc, internet_facing=True, 32 | vpc_subnets=ec2.SubnetSelection(subnet_group_name='Public')) 33 | listener = alb.add_listener('WebListener', port=80) 34 | listener.add_targets('Target', port=80, targets=[asg]) 35 | self.webserver_sg = ec2.SecurityGroup(self, 'WebServerSg', vpc=vpc) 36 | asg.add_security_group(self.webserver_sg) 37 | -------------------------------------------------------------------------------- /Chapter10/app/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3 app.py", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "requirements*.txt", 11 | "source.bat", 12 | "**/__init__.py", 13 | "python/__pycache__", 14 | "tests" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 19 | "@aws-cdk/core:checkSecretUsage": true, 20 | "@aws-cdk/core:target-partitions": [ 21 | "aws", 22 | "aws-cn" 23 | ], 24 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 25 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 26 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 27 | "@aws-cdk/aws-iam:minimizePolicies": true, 28 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 29 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 30 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 31 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 32 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 33 | "@aws-cdk/core:enablePartitionLiterals": true, 34 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 35 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 36 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 37 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 38 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 39 | "@aws-cdk/aws-route53-patters:useCertificate": true, 40 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 41 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 42 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 43 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 44 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 45 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 46 | "@aws-cdk/aws-redshift:columnId": true, 47 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 48 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 49 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 50 | "@aws-cdk/aws-kms:aliasNameRef": true, 51 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 52 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 53 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Chapter10/app/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest==6.2.5 2 | -------------------------------------------------------------------------------- /Chapter10/app/requirements.txt: -------------------------------------------------------------------------------- 1 | aws-cdk-lib==2.92.0 2 | constructs>=10.0.0,<11.0.0 3 | -------------------------------------------------------------------------------- /Chapter10/app/source.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem The sole purpose of this script is to make the command 4 | rem 5 | rem source .venv/bin/activate 6 | rem 7 | rem (which activates a Python virtualenv on Linux or Mac OS X) work on Windows. 8 | rem On Windows, this command just runs this batch file (the argument is ignored). 9 | rem 10 | rem Now we don't need to document a Windows command for activating a virtualenv. 11 | 12 | echo Executing .venv\Scripts\activate.bat for you 13 | .venv\Scripts\activate.bat 14 | -------------------------------------------------------------------------------- /Chapter10/app/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-AWS-CloudFormation-Second-Edition/7bffc66ec68256d25d79d0b9b106eed1566216ee/Chapter10/app/tests/__init__.py -------------------------------------------------------------------------------- /Chapter10/app/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-AWS-CloudFormation-Second-Edition/7bffc66ec68256d25d79d0b9b106eed1566216ee/Chapter10/app/tests/unit/__init__.py -------------------------------------------------------------------------------- /Chapter10/app/tests/unit/test_app_stack.py: -------------------------------------------------------------------------------- 1 | import aws_cdk as core 2 | from aws_cdk.assertions import Template, Match 3 | 4 | from app.core_stack import CoreStack 5 | 6 | def test_vpc_has_public_subnets(): 7 | app = core.App() 8 | stack = CoreStack(app, 'core') 9 | assert len(stack.vpc.public_subnets) > 0 10 | 11 | def test_vpc_has_private_subnets(): 12 | app = core.App() 13 | stack = CoreStack(app, 'core') 14 | assert len(stack.vpc.private_subnets) > 0 15 | 16 | def test_dev_role_is_readonly(): 17 | app = core.App() 18 | stack = CoreStack(app, 'core') 19 | template = Template.from_stack(stack) 20 | template.has_resource_properties('AWS::IAM::Role', { 21 | 'ManagedPolicyArns': [Match.object_like({ 22 | "Fn::Join": ["",["arn:",{"Ref": "AWS::Partition"},":iam::aws:policy/ReadOnlyAccess"]] 23 | })] 24 | }) 25 | -------------------------------------------------------------------------------- /Chapter11/README.md: -------------------------------------------------------------------------------- 1 | # AWS SAM 2 | 3 | _Make sure you have followed prerequisites steps and installed Docker._ 4 | 5 | ## Hello World 6 | 1. Run `sam init` and initialize the project as it is written in the book 7 | 2. Run build by issuing `sam build --use-container` 8 | 3. Run function locally `sam local invoke` 9 | 4. Start API `sam local start-api` 10 | 5. In a separate terminal window make a call to local API `curl localhost:3000/hello` 11 | 6. Deploy the stack `sam deploy --stack-name hello-world --capabilities CAPABILITY_IAM --resolve-s3` 12 | 7. Invoke remote API `curl API_URL/Prod/hello` 13 | 14 | ## Party planner 15 | Steps are similar to Hello, World 16 | ```bash 17 | sam build --use-container 18 | sam deploy --stack-name party --capabilities CAPABILITY_IAM --resolve-s3 19 | ``` 20 | 1. Invoke remote API `curl -X POST -d @events/event.json -H "Content-Type: application/json" https://API_URL/Prod/register` 21 | 2. Examine the DynamoDB table 22 | 3. Invoke Lambda function for reporting `aws lambda invoke --function-name REPORTING_FUNCTION --payload '{}' out.txt` 23 | 4. Check S3 24 | ```bash 25 | aws s3 ls s3://REPORTING_BUCKET 26 | aws s3 cp s3://REPORTING_BUCKET/Birthday.txt . 27 | cat Birthday.txt 28 | ``` 29 | -------------------------------------------------------------------------------- /Chapter11/hello-world/README.md: -------------------------------------------------------------------------------- 1 | # hello-world 2 | 3 | This project contains source code and supporting files for a serverless application that you can deploy with the SAM CLI. It includes the following files and folders. 4 | 5 | - hello_world - Code for the application's Lambda function. 6 | - events - Invocation events that you can use to invoke the function. 7 | - tests - Unit tests for the application code. 8 | - template.yaml - A template that defines the application's AWS resources. 9 | 10 | The application uses several AWS resources, including Lambda functions and an API Gateway API. These resources are defined in the `template.yaml` file in this project. You can update the template to add AWS resources through the same deployment process that updates your application code. 11 | 12 | If you prefer to use an integrated development environment (IDE) to build and test your application, you can use the AWS Toolkit. 13 | The AWS Toolkit is an open source plug-in for popular IDEs that uses the SAM CLI to build and deploy serverless applications on AWS. The AWS Toolkit also adds a simplified step-through debugging experience for Lambda function code. See the following links to get started. 14 | 15 | * [PyCharm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) 16 | * [IntelliJ](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) 17 | * [VS Code](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/welcome.html) 18 | * [Visual Studio](https://docs.aws.amazon.com/toolkit-for-visual-studio/latest/user-guide/welcome.html) 19 | 20 | ## Deploy the sample application 21 | 22 | The Serverless Application Model Command Line Interface (SAM CLI) is an extension of the AWS CLI that adds functionality for building and testing Lambda applications. It uses Docker to run your functions in an Amazon Linux environment that matches Lambda. It can also emulate your application's build environment and API. 23 | 24 | To use the SAM CLI, you need the following tools. 25 | 26 | * SAM CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) 27 | * [Python 3 installed](https://www.python.org/downloads/) 28 | * Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community) 29 | 30 | To build and deploy your application for the first time, run the following in your shell: 31 | 32 | ```bash 33 | sam build --use-container 34 | sam deploy --guided 35 | ``` 36 | 37 | The first command will build the source of your application. The second command will package and deploy your application to AWS, with a series of prompts: 38 | 39 | * **Stack Name**: The name of the stack to deploy to CloudFormation. This should be unique to your account and region, and a good starting point would be something matching your project name. 40 | * **AWS Region**: The AWS region you want to deploy your app to. 41 | * **Confirm changes before deploy**: If set to yes, any change sets will be shown to you before execution for manual review. If set to no, the AWS SAM CLI will automatically deploy application changes. 42 | * **Allow SAM CLI IAM role creation**: Many AWS SAM templates, including this example, create AWS IAM roles required for the AWS Lambda function(s) included to access AWS services. By default, these are scoped down to minimum required permissions. To deploy an AWS CloudFormation stack which creates or modified IAM roles, the `CAPABILITY_IAM` value for `capabilities` must be provided. If permission isn't provided through this prompt, to deploy this example you must explicitly pass `--capabilities CAPABILITY_IAM` to the `sam deploy` command. 43 | * **Save arguments to samconfig.toml**: If set to yes, your choices will be saved to a configuration file inside the project, so that in the future you can just re-run `sam deploy` without parameters to deploy changes to your application. 44 | 45 | You can find your API Gateway Endpoint URL in the output values displayed after deployment. 46 | 47 | ## Use the SAM CLI to build and test locally 48 | 49 | Build your application with the `sam build --use-container` command. 50 | 51 | ```bash 52 | hello-world$ sam build --use-container 53 | ``` 54 | 55 | The SAM CLI installs dependencies defined in `hello_world/requirements.txt`, creates a deployment package, and saves it in the `.aws-sam/build` folder. 56 | 57 | Test a single function by invoking it directly with a test event. An event is a JSON document that represents the input that the function receives from the event source. Test events are included in the `events` folder in this project. 58 | 59 | Run functions locally and invoke them with the `sam local invoke` command. 60 | 61 | ```bash 62 | hello-world$ sam local invoke HelloWorldFunction --event events/event.json 63 | ``` 64 | 65 | The SAM CLI can also emulate your application's API. Use the `sam local start-api` to run the API locally on port 3000. 66 | 67 | ```bash 68 | hello-world$ sam local start-api 69 | hello-world$ curl http://localhost:3000/ 70 | ``` 71 | 72 | The SAM CLI reads the application template to determine the API's routes and the functions that they invoke. The `Events` property on each function's definition includes the route and method for each path. 73 | 74 | ```yaml 75 | Events: 76 | HelloWorld: 77 | Type: Api 78 | Properties: 79 | Path: /hello 80 | Method: get 81 | ``` 82 | 83 | ## Add a resource to your application 84 | The application template uses AWS Serverless Application Model (AWS SAM) to define application resources. AWS SAM is an extension of AWS CloudFormation with a simpler syntax for configuring common serverless application resources such as functions, triggers, and APIs. For resources not included in [the SAM specification](https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md), you can use standard [AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) resource types. 85 | 86 | ## Fetch, tail, and filter Lambda function logs 87 | 88 | To simplify troubleshooting, SAM CLI has a command called `sam logs`. `sam logs` lets you fetch logs generated by your deployed Lambda function from the command line. In addition to printing the logs on the terminal, this command has several nifty features to help you quickly find the bug. 89 | 90 | `NOTE`: This command works for all AWS Lambda functions; not just the ones you deploy using SAM. 91 | 92 | ```bash 93 | hello-world$ sam logs -n HelloWorldFunction --stack-name hello-world --tail 94 | ``` 95 | 96 | You can find more information and examples about filtering Lambda function logs in the [SAM CLI Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-logging.html). 97 | 98 | ## Unit tests 99 | 100 | Tests are defined in the `tests` folder in this project. Use PIP to install the [pytest](https://docs.pytest.org/en/latest/) and run unit tests. 101 | 102 | ```bash 103 | hello-world$ pip install pytest pytest-mock --user 104 | hello-world$ python -m pytest tests/ -v 105 | ``` 106 | 107 | ## Cleanup 108 | 109 | To delete the sample application that you created, use the AWS CLI. Assuming you used your project name for the stack name, you can run the following: 110 | 111 | ```bash 112 | aws cloudformation delete-stack --stack-name hello-world 113 | ``` 114 | 115 | ## Resources 116 | 117 | See the [AWS SAM developer guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) for an introduction to SAM specification, the SAM CLI, and serverless application concepts. 118 | 119 | Next, you can use AWS Serverless Application Repository to deploy ready to use Apps that go beyond hello world samples and learn how authors developed their applications: [AWS Serverless Application Repository main page](https://aws.amazon.com/serverless/serverlessrepo/) 120 | -------------------------------------------------------------------------------- /Chapter11/hello-world/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Chapter11/hello-world/events/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "{\"message\": \"hello world\"}", 3 | "resource": "/{proxy+}", 4 | "path": "/path/to/resource", 5 | "httpMethod": "POST", 6 | "isBase64Encoded": false, 7 | "queryStringParameters": { 8 | "foo": "bar" 9 | }, 10 | "pathParameters": { 11 | "proxy": "/path/to/resource" 12 | }, 13 | "stageVariables": { 14 | "baz": "qux" 15 | }, 16 | "headers": { 17 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 18 | "Accept-Encoding": "gzip, deflate, sdch", 19 | "Accept-Language": "en-US,en;q=0.8", 20 | "Cache-Control": "max-age=0", 21 | "CloudFront-Forwarded-Proto": "https", 22 | "CloudFront-Is-Desktop-Viewer": "true", 23 | "CloudFront-Is-Mobile-Viewer": "false", 24 | "CloudFront-Is-SmartTV-Viewer": "false", 25 | "CloudFront-Is-Tablet-Viewer": "false", 26 | "CloudFront-Viewer-Country": "US", 27 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com", 28 | "Upgrade-Insecure-Requests": "1", 29 | "User-Agent": "Custom User Agent String", 30 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 31 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", 32 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 33 | "X-Forwarded-Port": "443", 34 | "X-Forwarded-Proto": "https" 35 | }, 36 | "requestContext": { 37 | "accountId": "123456789012", 38 | "resourceId": "123456", 39 | "stage": "prod", 40 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 41 | "requestTime": "09/Apr/2015:12:34:56 +0000", 42 | "requestTimeEpoch": 1428582896000, 43 | "identity": { 44 | "cognitoIdentityPoolId": null, 45 | "accountId": null, 46 | "cognitoIdentityId": null, 47 | "caller": null, 48 | "accessKey": null, 49 | "sourceIp": "127.0.0.1", 50 | "cognitoAuthenticationType": null, 51 | "cognitoAuthenticationProvider": null, 52 | "userArn": null, 53 | "userAgent": "Custom User Agent String", 54 | "user": null 55 | }, 56 | "path": "/prod/path/to/resource", 57 | "resourcePath": "/{proxy+}", 58 | "httpMethod": "POST", 59 | "apiId": "1234567890", 60 | "protocol": "HTTP/1.1" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Chapter11/hello-world/hello_world/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Chapter11/hello-world/hello_world/app.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | # import requests 4 | 5 | 6 | def lambda_handler(event, context): 7 | """Sample pure Lambda function 8 | 9 | Parameters 10 | ---------- 11 | event: dict, required 12 | API Gateway Lambda Proxy Input Format 13 | 14 | Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format 15 | 16 | context: object, required 17 | Lambda Context runtime methods and attributes 18 | 19 | Context doc: https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html 20 | 21 | Returns 22 | ------ 23 | API Gateway Lambda Proxy Output Format: dict 24 | 25 | Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html 26 | """ 27 | 28 | # try: 29 | # ip = requests.get("http://checkip.amazonaws.com/") 30 | # except requests.RequestException as e: 31 | # # Send some context about this error to Lambda Logs 32 | # print(e) 33 | 34 | # raise e 35 | 36 | return { 37 | "statusCode": 200, 38 | "body": json.dumps({ 39 | "message": "hello world", 40 | # "location": ip.text.replace("\n", "") 41 | }), 42 | } 43 | -------------------------------------------------------------------------------- /Chapter11/hello-world/hello_world/requirements.txt: -------------------------------------------------------------------------------- 1 | requests -------------------------------------------------------------------------------- /Chapter11/hello-world/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | hello-world 5 | 6 | Sample SAM Template for hello-world 7 | 8 | # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst 9 | Globals: 10 | Function: 11 | Timeout: 3 12 | 13 | Resources: 14 | HelloWorldFunction: 15 | Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction 16 | Properties: 17 | CodeUri: hello_world/ 18 | Handler: app.lambda_handler 19 | Runtime: python3.8 20 | Events: 21 | HelloWorld: 22 | Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api 23 | Properties: 24 | Path: /hello 25 | Method: get 26 | 27 | Outputs: 28 | # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function 29 | # Find out more about other implicit resources you can reference within SAM 30 | # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api 31 | HelloWorldApi: 32 | Description: "API Gateway endpoint URL for Prod stage for Hello World function" 33 | Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" 34 | HelloWorldFunction: 35 | Description: "Hello World Lambda Function ARN" 36 | Value: !GetAtt HelloWorldFunction.Arn 37 | HelloWorldFunctionIamRole: 38 | Description: "Implicit IAM Role created for Hello World function" 39 | Value: !GetAtt HelloWorldFunctionRole.Arn 40 | -------------------------------------------------------------------------------- /Chapter11/hello-world/tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Chapter11/hello-world/tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Chapter11/hello-world/tests/unit/test_handler.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | 5 | from hello_world import app 6 | 7 | 8 | @pytest.fixture() 9 | def apigw_event(): 10 | """ Generates API GW Event""" 11 | 12 | return { 13 | "body": '{ "test": "body"}', 14 | "resource": "/{proxy+}", 15 | "requestContext": { 16 | "resourceId": "123456", 17 | "apiId": "1234567890", 18 | "resourcePath": "/{proxy+}", 19 | "httpMethod": "POST", 20 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 21 | "accountId": "123456789012", 22 | "identity": { 23 | "apiKey": "", 24 | "userArn": "", 25 | "cognitoAuthenticationType": "", 26 | "caller": "", 27 | "userAgent": "Custom User Agent String", 28 | "user": "", 29 | "cognitoIdentityPoolId": "", 30 | "cognitoIdentityId": "", 31 | "cognitoAuthenticationProvider": "", 32 | "sourceIp": "127.0.0.1", 33 | "accountId": "", 34 | }, 35 | "stage": "prod", 36 | }, 37 | "queryStringParameters": {"foo": "bar"}, 38 | "headers": { 39 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 40 | "Accept-Language": "en-US,en;q=0.8", 41 | "CloudFront-Is-Desktop-Viewer": "true", 42 | "CloudFront-Is-SmartTV-Viewer": "false", 43 | "CloudFront-Is-Mobile-Viewer": "false", 44 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 45 | "CloudFront-Viewer-Country": "US", 46 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 47 | "Upgrade-Insecure-Requests": "1", 48 | "X-Forwarded-Port": "443", 49 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com", 50 | "X-Forwarded-Proto": "https", 51 | "X-Amz-Cf-Id": "aaaaaaaaaae3VYQb9jd-nvCd-de396Uhbp027Y2JvkCPNLmGJHqlaA==", 52 | "CloudFront-Is-Tablet-Viewer": "false", 53 | "Cache-Control": "max-age=0", 54 | "User-Agent": "Custom User Agent String", 55 | "CloudFront-Forwarded-Proto": "https", 56 | "Accept-Encoding": "gzip, deflate, sdch", 57 | }, 58 | "pathParameters": {"proxy": "/examplepath"}, 59 | "httpMethod": "POST", 60 | "stageVariables": {"baz": "qux"}, 61 | "path": "/examplepath", 62 | } 63 | 64 | 65 | def test_lambda_handler(apigw_event, mocker): 66 | 67 | ret = app.lambda_handler(apigw_event, "") 68 | data = json.loads(ret["body"]) 69 | 70 | assert ret["statusCode"] == 200 71 | assert "message" in ret["body"] 72 | assert data["message"] == "hello world" 73 | # assert "location" in data.dict_keys() 74 | -------------------------------------------------------------------------------- /Chapter11/party-planner/events/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "PartyName": "Birthday", 3 | "PartyDate": "2021-01-01", 4 | "GuestName": "John Doe", 5 | "GuestDiet": "Vegan" 6 | } 7 | -------------------------------------------------------------------------------- /Chapter11/party-planner/registration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Mastering-AWS-CloudFormation-Second-Edition/7bffc66ec68256d25d79d0b9b106eed1566216ee/Chapter11/party-planner/registration/__init__.py -------------------------------------------------------------------------------- /Chapter11/party-planner/registration/app.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import os 3 | import json 4 | 5 | 6 | def lambda_handler(event, context): 7 | parties_table = os.getenv('PARTIES_TABLE') 8 | guests_table = os.getenv('GUESTS_TABLE') 9 | dynamo_client = boto3.client('dynamodb') 10 | print(event) 11 | body = json.loads(event['body']) 12 | resp = dynamo_client.get_item( 13 | TableName=parties_table, 14 | Key={'PartyName': {'S': body['PartyName']}} 15 | ) 16 | if 'Item' not in resp: 17 | dynamo_client.put_item( 18 | TableName=parties_table, 19 | Item={ 20 | 'PartyName': {'S': body['PartyName']}, 21 | 'Date': {'S': body['PartyDate']} 22 | } 23 | ) 24 | dynamo_client.put_item( 25 | TableName=guests_table, 26 | Item={ 27 | 'GuestName':{'S': body['GuestName']}, 28 | 'Diet': {'S': body['GuestDiet']}, 29 | 'PartyName':{'S': body['PartyName']} 30 | } 31 | ) 32 | return { 33 | 'statusCode': 200 34 | } 35 | -------------------------------------------------------------------------------- /Chapter11/party-planner/registration/requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Chapter11/party-planner/reporting/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Chapter11/party-planner/reporting/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import boto3 3 | import datetime as dt 4 | 5 | 6 | def lambda_handler(event, context): 7 | 8 | parties_table = boto3.resource('dynamodb').Table(os.getenv('PARTIES_TABLE')) 9 | guests_table = boto3.resource('dynamodb').Table(os.getenv('GUESTS_TABLE')) 10 | s3 = boto3.client('s3') 11 | 12 | parties = parties_table.scan(AttributesToGet=['PartyName', 'Date'])['Items'] 13 | 14 | for party in parties: 15 | party_date = dt.datetime.strptime(party['Date'], '%Y-%m-%d').date() 16 | if party_date > dt.date.today(): 17 | guests = guests_table.scan( 18 | AttributesToGet=['GuestName', 'Diet'], 19 | ScanFilter={'PartyName': {'AttributeValueList': [party['PartyName']], 'ComparisonOperator': 'EQ'}} 20 | )['Items'] 21 | party_doc = '---\n' 22 | party_doc += 'Party Planning Report!!!\n' 23 | party_doc += f'Prepare for {party["PartyName"]} on {party["Date"]}!\n' 24 | party_doc += 'Guests list:\n' 25 | for guest in guests: 26 | party_doc += f'- {guest["GuestName"]} who is restricted to eat {guest["Diet"]}\n' 27 | party_doc += '---\n' 28 | s3.put_object( 29 | Bucket=os.getenv('REPORTS_BUCKET'), 30 | Body=party_doc, 31 | Key=f'{party["PartyName"]}.txt', 32 | ) 33 | 34 | return { 35 | 'statusCode': 200 36 | } 37 | -------------------------------------------------------------------------------- /Chapter11/party-planner/reporting/requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Chapter11/party-planner/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Globals: 4 | Function: 5 | Timeout: 300 6 | Runtime: python3.8 7 | Handler: app.lambda_handler 8 | Environment: 9 | Variables: 10 | PARTIES_TABLE: !Ref PartiesTable 11 | GUESTS_TABLE: !Ref GuestsTable 12 | REPORTS_BUCKET: !Ref ReportsBucket 13 | 14 | Resources: 15 | RegistrationFunction: 16 | Type: AWS::Serverless::Function 17 | Properties: 18 | CodeUri: registration/ 19 | Events: 20 | register: 21 | Type: Api 22 | Properties: 23 | Path: /register 24 | Method: post 25 | Policies: 26 | Statement: 27 | - Effect: Allow 28 | Action: 29 | - dynamodb:PutItem 30 | - dynamodb:GetItem 31 | Resource: 32 | - !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${PartiesTable}" 33 | - !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${GuestsTable}" 34 | 35 | PartiesTable: 36 | Type: AWS::Serverless::SimpleTable 37 | Properties: 38 | PrimaryKey: 39 | Name: PartyName 40 | Type: String 41 | 42 | GuestsTable: 43 | Type: AWS::Serverless::SimpleTable 44 | Properties: 45 | PrimaryKey: 46 | Name: GuestName 47 | Type: String 48 | 49 | ReportsBucket: 50 | Type: AWS::S3::Bucket 51 | 52 | ReportingFunction: 53 | Type: AWS::Serverless::Function 54 | Properties: 55 | CodeUri: reporting/ 56 | Runtime: python3.8 57 | Policies: 58 | Statement: 59 | - Effect: Allow 60 | Action: 61 | - dynamodb:GetItem 62 | - dynamodb:Scan 63 | Resource: 64 | - !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${PartiesTable}" 65 | - !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${GuestsTable}" 66 | - Effect: Allow 67 | Action: 68 | - s3:ListBucket 69 | - s3:GetBucketAcl 70 | - s3:PutObject 71 | Resource: 72 | - !GetAtt ReportsBucket.Arn 73 | - !Sub "${ReportsBucket.Arn}/*" 74 | Events: 75 | scheduled: 76 | Type: Schedule 77 | Properties: 78 | Schedule: "rate(1 day)" 79 | 80 | Outputs: 81 | RegisterApi: 82 | Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/register/" 83 | RegisterFunction: 84 | Value: !GetAtt RegistrationFunction.Arn 85 | ReportingFunction: 86 | Value: !GetAtt ReportingFunction.Arn 87 | PartiesTable: 88 | Value: !Ref PartiesTable 89 | GuestsTable: 90 | Value: !Ref GuestsTable 91 | ReportsBucket: 92 | Value: !Ref ReportsBucket -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Packt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mastering AWS CloudFormation Second Edition 2 | 3 | Mastering AWS CloudFormation Second Edition 4 | 5 | This is the code repository for [Mastering AWS CloudFormation Second Edition](https://www.packtpub.com/product/mastering-aws-cloudformation-second-edition/9781805123903?utm_source=github&utm_medium=repository&utm_campaign=), published by Packt. 6 | 7 | **Build resilient and production-ready infrastructure in Amazon Web Services with CloudFormation** 8 | 9 | ## What is this book about? 10 | Mastering CloudFormation covers all the features that an engineer needs to use to effectively build, maintain, and operate large-scale infrastructures within AWS. It covers all the core features as well as various methods to successfully extend its capabilities beyond AWS. 11 | 12 | This book covers the following exciting features: 13 | * Understand modern approaches to IaC 14 | * Develop universal, modular, and reusable CloudFormation templates 15 | * Discover ways of applying continuous delivery with CloudFormation 16 | * Implement IaC best practices in the AWS cloud 17 | * Provision massive applications across multiple regions and accounts 18 | * Automate template generation and software provisioning for AWS 19 | * Extend CloudFormation features with custom resources and the registry 20 | * Modularize and unify templates using modules and macros 21 | 22 | If you feel this book is for you, get your [copy](https://www.amazon.com/dp/1805123904) today! 23 | 24 | https://www.packtpub.com/ 26 | 27 | ## Instructions and Navigations 28 | All of the code is organized into folders. For example, Chapter05. 29 | 30 | The code will look like the following: 31 | ``` 32 | import boto3 33 | def check_if_key_exists(): 34 | client = boto3.client() 35 | try: 36 | resp = client.describe_key_pairs(KeyNames=[«mykey»]) 37 | except Exception: 38 | ``` 39 | 40 | **Following is what you need for this book:** 41 | If you are a developer who wants to learn how to write templates, a DevOps engineer or SRE interested in deployment and orchestration, or a solutions architect looking to understand the benefits of streamlined and scalable infrastructure management, this book is for you. Prior understanding of the AWS Cloud is necessary. 42 | 43 | With the following software and hardware list you can run all code files present in the book (Chapter 1-12). 44 | ### Software and Hardware List 45 | | Chapter | Software required | OS required | 46 | | -------- | ------------------------------------ | ----------------------------------- | 47 | | 1-12 | AWS CLI 1.18 or later | Mac OS X, and Linux (Any) | 48 | | 1-12 | Python 3.6 or later | Windows, Mac OS X, and Linux (Any) | 49 | | 1-12 | Homebrew 2.2 or later | Windows, Mac OS X, and Linux (Any) | 50 | | 1-12 | Docker 19.03.5 or later | Windows, Mac OS X, and Linux (Any) | 51 | 52 | 53 | ### Related products 54 | * AWS for Solutions Architects - Second Edition [[Packt]](https://www.packtpub.com/product/aws-for-solutions-architects-second-edition/9781803238951?utm_source=github&utm_medium=repository&utm_campaign=9781803238951) [[Amazon]](https://www.amazon.com/dp/180323895X) 55 | 56 | * Building and Delivering Microservices on AWS [[Packt]](https://www.packtpub.com/product/building-and-delivering-microservices-on-aws/9781803238203?utm_source=github&utm_medium=repository&utm_campaign=9781803238203) [[Amazon]](https://www.amazon.com/dp/1803238208) 57 | 58 | 59 | ## Get to Know the Author 60 | **Karen Tovmasyan** 61 | is a Senior Software Engineer at Uber, working in the payments team and helping Uber successfully maintain its infrastructure across cloud providers. Previously he worked at various startups and consulting companies getting the most from the AWS Cloud. 62 | 63 | 64 | ## Other books by the authors 65 | [Mastering AWS CloudFormation](https://www.packtpub.com/product/mastering-aws-cloudformation/9781789130935?utm_source=github&utm_medium=repository&utm_campaign=9781789130935) 66 | 67 | ### Download a free PDF 68 | 69 | If you have already purchased a print or Kindle version of this book, you can get a DRM-free PDF version at no cost.
Simply click on the link to claim your free PDF.
70 |

https://packt.link/free-ebook/9781805123903

71 | 72 | --------------------------------------------------------------------------------