├── .gitignore ├── .pre-commit-config.yaml ├── .rpdk-config ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Config ├── LICENSE ├── NOTICE ├── README.md ├── awsutility-cloudformation-commandrunner.json ├── banner.txt ├── buildspec.yml ├── docs └── README.md ├── examples ├── .DS_Store ├── awscli-ssm-get-parameter-template.yaml ├── commandrunner-example-iopscalc-template.yaml └── gatsby-static-website │ └── gatsby-deploy-template.yaml ├── lombok.config ├── overrides.json ├── pom.xml ├── resource-role.yaml ├── scripts ├── build.sh ├── cleanup.sh └── register.sh ├── src ├── main │ ├── java │ │ └── software │ │ │ └── awsutility │ │ │ └── cloudformation │ │ │ └── commandrunner │ │ │ ├── CallbackContext.java │ │ │ ├── Configuration.java │ │ │ ├── CreateHandler.java │ │ │ ├── DeleteHandler.java │ │ │ └── ReadHandler.java │ └── resources │ │ └── BaseTemplate.json └── test │ └── java │ └── software │ └── awsutility │ └── cloudformation │ └── commandrunner │ ├── CreateHandlerTest.java │ ├── DeleteHandlerTest.java │ └── ReadHandlerTest.java └── template.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | awsutility-cloudformation-commandrunner.zip 3 | *.log 4 | target/ 5 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.4.0 4 | hooks: 5 | - id: check-case-conflict 6 | - id: detect-private-key 7 | - id: end-of-file-fixer 8 | - id: mixed-line-ending 9 | args: 10 | - --fix=lf 11 | - id: trailing-whitespace 12 | - id: pretty-format-json 13 | args: 14 | - --autofix 15 | - --indent=2 16 | - --no-sort-keys 17 | - id: check-merge-conflict 18 | - id: check-yaml 19 | exclude: codebuild-ci.yaml 20 | -------------------------------------------------------------------------------- /.rpdk-config: -------------------------------------------------------------------------------- 1 | { 2 | "artifact_type": "RESOURCE", 3 | "typeName": "AWSUtility::CloudFormation::CommandRunner", 4 | "language": "java", 5 | "runtime": "java8.al2", 6 | "entrypoint": "software.awsutility.cloudformation.commandrunner.HandlerWrapper::handleRequest", 7 | "testEntrypoint": "software.awsutility.cloudformation.commandrunner.HandlerWrapper::testEntrypoint", 8 | "settings": { 9 | "namespace": [ 10 | "software", 11 | "awsutility", 12 | "cloudformation", 13 | "commandrunner" 14 | ], 15 | "protocolVersion": "2.0.0" 16 | }, 17 | "executableEntrypoint": "software.awsutility.cloudformation.commandrunner.HandlerWrapperExecutable" 18 | } 19 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /Config: -------------------------------------------------------------------------------- 1 | package.Aws-cloudformation-command = { 2 | interfaces = (1.0); 3 | 4 | # Use NoOpBuild. See https://w.amazon.com/index.php/BrazilBuildSystem/NoOpBuild 5 | build-system = no-op; 6 | build-tools = { 7 | 1.0 = { 8 | NoOpBuild = 1.0; 9 | }; 10 | }; 11 | 12 | # Use runtime-dependencies for when you want to bring in additional 13 | # packages when deploying. 14 | # Use dependencies instead if you intend for these dependencies to 15 | # be exported to other packages that build against you. 16 | dependencies = { 17 | 1.0 = { 18 | }; 19 | }; 20 | 21 | runtime-dependencies = { 22 | 1.0 = { 23 | }; 24 | }; 25 | 26 | }; 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWSUtility::CloudFormation::CommandRunner 2 | 3 | ### CommandRunner v2.0.1 is here! 🚀 🚀 🚀 4 | 5 | I took all the feedback, issues and feature requests from all our users to create this new major version. All known bugs for CommandRunner have now been fixed! 6 | 7 | This version comes with 3 new properties `InstanceType`, `Timeout` and `DisableTerminateInstancesCheck`, improved error handling, logging, reliability, documentation and functionality. 8 | 9 | To update to the new version or do a fresh install, simply run the following commands in a new directory. 10 | 11 | ``` 12 | git clone https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-awsutilities-commandrunner.git 13 | 14 | cd aws-cloudformation-resource-providers-awsutilities-commandrunner 15 | 16 | curl -LO https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-awsutilities-commandrunner/releases/latest/download/awsutility-cloudformation-commandrunner.zip 17 | 18 | ./scripts/register.sh --set-default 19 | ``` 20 | 21 | For more details, check the [Change Log](#change-log) section below. 22 | 23 | --- 24 | 25 | # **Table Of Contents** 26 | - [Introduction](#introduction) 27 | - [Prerequisites](#prerequisites) 28 | - [User Installation Steps](#user-installation-steps) 29 | - [Documentation](#documentation) 30 | - [Syntax](#syntax) 31 | - [Properties](#properties) 32 | - [Return Values](#return-values) 33 | - [User Guides](#user-guides) 34 | - [Run a Command before or after any Resource](#run-a-command-before-or-after-a-resource) 35 | - [Run a script in any programming language using any SDK](#run-a-script-in-any-programming-language-using-any-sdk) 36 | - [Install Packages before Running Command](#install-packages-before-running-command) 37 | - [Run a Multi-Line Script](#run-a-multi-line-script) 38 | - [Use Cases](#use-cases) 39 | - [FAQ](#faq) 40 | - [Developer Build Steps](#developer-build-steps) 41 | - [Change Log](#change-log) 42 | - [See Also](#see-also) 43 | 44 | --- 45 | 46 | # Introduction 47 | 48 | AWSUtility::CloudFormation::CommandRunner is a CloudFormation resource type created using the recently released CloudFormation Resource Providers framework. 49 | 50 | The AWSUtility::CloudFormation::CommandRunner resource allows users to run Bash commands in any CloudFormation stack. 51 | 52 | This allows for unlimited customization such as executing AWS CLI/API calls, running scripts in any language, querying databases, doing external REST API calls, cleanup routines, validations, dynamically referencing parameters and just about anything that can be done using the shell on an EC2 instance. 53 | 54 | The `AWSUtility::CloudFormation::CommandRunner` resource runs any command provided to it before or after any resource in the Stack. 55 | 56 | `AWSUtility::CloudFormation::CommandRunner` can be used to perform inside your CloudFormation stack, any API call, script, custom logic, external check, conditions, cleanup, dynamic parameter retrieval and just about anything that can be done using a command. 57 | 58 | Any output written using the command to the reserved file `/command-output.txt` can be referenced anywhere in your template by using `!Fn::GetAtt Command.Output` like below, where `Command` is the logical name of the `AWSUtility::CloudFormation::CommandRunner`resource. 59 | 60 | ```yaml 61 | Resources: 62 | MyCommand: 63 | Type: 'AWSUtility::CloudFormation::CommandRunner' 64 | Properties: 65 | Command: aws s3 ls | sed -n 1p | cut -d " " -f3 > /command-output.txt 66 | Role: String #Optional 67 | LogGroup: String #Optional 68 | SubnetId: String #Optional 69 | SecurityGroupId: String #Optional 70 | KeyId: String #Optional 71 | Timeout: String #Optional **NEW** 72 | DisableTerminateInstancesCheck: String #Optional **NEW** 73 | InstanceType: #Optional **NEW** 74 | 75 | Outputs: 76 | Output: 77 | Description: The output of the CommandRunner. 78 | Value: !GetAtt MyCommand.Output 79 | ``` 80 | *Note: In the above example, `sed -n 1p` prints only the first line from the response returned by `aws s3 ls`. To get the bucket name, `sed -n 1p` pipes the response to `cut -d " " -f3`, which chooses the third element in the array created after splitting the line delimited by a space.* 81 | 82 | Only the property `Command` is required, while `Role`, `LogGroup`, `SubnetId` and `SecurityGroupId` are not required and have defaults. 83 | 84 | `Command` is the Bash command. 85 | `Role` is the IAM Role to run the command. 86 | `LogGroup` is the CloudWatch Log Group to send logs from the command's execution. 87 | `SubnetId` is the ID of the Subnet that the command will be executed in. 88 | `SecurityGroupId` is the ID of the Security Group applied during the execution of the command. 89 | 90 | For more information about the above properties, navigate to [Properties](#properties) in the [Documentation](#documentation). 91 | 92 | _Note that the command once executed cannot be undone. It is highly recommended to test the AWSUtility::CloudFormation::CommandRunner resource out in a test stack before adding it to your production stack._ 93 | 94 | --- 95 | 96 | # Prerequisites 97 | 98 | - [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) 99 | - To register the `AWSUtility::CloudFormation::CommandRunner` resource to your AWS account/region, you need IAM permissions to perform the following actions. 100 | 101 | ``` 102 | s3:CreateBucket 103 | s3:DeleteBucket 104 | s3:PutBucketPolicy 105 | s3:PutObject 106 | cloudformation:RegisterType 107 | cloudformation:DescribeTypeRegistration 108 | iam:createRole 109 | logs:CreateLogGroup 110 | ``` 111 | 112 | --- 113 | 114 | # User Installation Steps 115 | 116 | *Note: To build the source yourself, see the `Developer Build Steps` section below.* 117 | 118 | **Step 0**: Clone this repository and download the latest release using the following commands. 119 | 120 | ```text 121 | git clone https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-awsutilities-commandrunner.git 122 | cd aws-cloudformation-resource-providers-awsutilities-commandrunner 123 | curl -LO https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-awsutilities-commandrunner/releases/latest/download/awsutility-cloudformation-commandrunner.zip 124 | ``` 125 | 126 | **Step 1**: Use the `register.sh` bash script to register resource from scratch and upload package to S3 bucket. Pass the optional `--set-default` option to set this version to be the default version for the `AWSUtility::CloudFormation::CommandRunner` resource. 127 | 128 | ```text 129 | $ ./scripts/register.sh --set-default 130 | ``` 131 | 132 | ...And that's it! 133 | 134 | Below is an example of a successful registration using the `register.sh` script. 135 | 136 | ```text 137 | $ ./scripts/register.sh 138 | Creating Execution Role... 139 | Waiting for execution role stack to complete... 140 | Waiting for execution role stack to complete... 141 | Waiting for execution role stack to complete... 142 | Waiting for execution role stack to complete... 143 | Creating/Updating Execution Role complete. 144 | Creating temporary S3 Bucket 7c96b969af1c41bfb2bd10f552255ca2... 145 | Creating temporary S3 Bucket 7c96b969af1c41bfb2bd10f552255ca2 complete. 146 | Configuring S3 Bucket Policy for temporary S3 Bucket 7c96b969af1c41bfb2bd10f552255ca2... 147 | Configuring S3 Bucket Policy for temporary S3 Bucket 7c96b969af1c41bfb2bd10f552255ca2 complete. 148 | Copying Schema Handler Package to temporary S3 Bucket 7c96b969af1c41bfb2bd10f552255ca2... 149 | Copying Schema Handler Package to temporary S3 Bucket 7c96b969af1c41bfb2bd10f552255ca2 complete. 150 | Creating CommandRunner Log Group called awsutility-cloudformation-commandrunner-logs2... 151 | Creating CommandRunner Log Group complete. 152 | Registering AWSUtility::CloudFormation::CommandRunner to AWS CloudFormation... 153 | RegistrationToken: 0ae0622e-af3d-463b-9b2d-1d1e5fa41d14 154 | Waiting for registration to complete... 155 | Waiting for registration to complete... 156 | Waiting for registration to complete... 157 | Waiting for registration to complete... 158 | Registering AWSUtility::CloudFormation::CommandRunner to AWS CloudFormation complete. 159 | Cleaning up temporary S3 Bucket... 160 | Deleting SchemaHandlerPackage from temporary S3 Bucket 7c96b969af1c41bfb2bd10f552255ca2... 161 | Deleting SchemaHandlerPackage from temporary S3 Bucket 7c96b969af1c41bfb2bd10f552255ca2 complete. 162 | Cleaning up temporary S3 Bucket complete. 163 | 164 | AWSUtility::CloudFormation::CommandRunner is ready to use. 165 | ``` 166 | 167 | The `register.sh` script performs the following operations to register the `AWSUtility::CloudFormation::CommandRunner` resource. 168 | 169 | - Creates an Execution Role for the `AWSUtility::CloudFormation::CommandRunner` resource to give it permissions to perform the following actions. 170 | 171 | ```yaml 172 | cloudformation:DeleteStack 173 | cloudformation:CreateStack 174 | cloudformation:DescribeStacks 175 | 176 | logs:CreateLogStream 177 | logs:DescribeLogGroups 178 | logs:PutLogEvents 179 | 180 | cloudwatch:PutMetricData 181 | 182 | ssm:GetParameter 183 | ssm:PutParameter 184 | ssm:DeleteParameter 185 | 186 | ec2:DescribeSubnets 187 | ec2:DescribeVpcs 188 | ec2:DescribeSecurityGroups 189 | ec2:CreateSecurityGroup 190 | ec2:RevokeSecurityGroupEgress 191 | ec2:RevokeSecurityGroupIngress 192 | ec2:CreateTags 193 | ec2:AuthorizeSecurityGroupIngress 194 | ec2:AuthorizeSecurityGroupEgress 195 | ec2:RunInstances 196 | ec2:DescribeInstances 197 | ec2:TerminateInstances 198 | ec2:DeleteSecurityGroup 199 | 200 | iam:PassRole 201 | iam:GetInstanceProfile 202 | iam:SimulatePrincipalPolicy 203 | 204 | #Only required if using the KeyId property, i.e custom KMS Key for the SSM SecureString 205 | kms:Encrypt 206 | kms:Decrypt 207 | 208 | sts:GetCallerIdentity 209 | ``` 210 | 211 | - Runs the `aws s3 mb` AWS CLI command to create an S3 bucket with the name specified. 212 | - Runs the `aws s3api put-bucket-policy` AWS CLI command to put the following bucket policy on the new bucket, where `` is the specified bucket name. 213 | 214 | ```json 215 | { 216 | "Version": "2012-10-17", 217 | "Statement": [ 218 | { 219 | "Action": [ 220 | "s3:GetObject", 221 | "s3:ListBucket" 222 | ], 223 | "Effect": "Allow", 224 | "Resource": [ 225 | "arn:aws:s3:::/*", 226 | "arn:aws:s3::: /command-output.txt` 347 | 348 | #### Note: 349 | The command is run on the latest Amazon Linux 2 AMI in your region. 350 | 351 | _Required_: Yes 352 | 353 | _Type_: String 354 | 355 | _Update requires_: Replacement 356 | 357 | ### Role 358 | 359 | The IAM Instance Profile to be used to run the Command. The Role in the Instance Profile will need all the permissions required to run the above `Command`. 360 | 361 | #### Note: 362 | The Role should have permissions to perform the actions below to write logs to CloudWatch from the command's execution. 363 | ``` 364 | "logs:CreateLogStream", 365 | "logs:CreateLogGroup", 366 | "logs:PutLogEvents" 367 | ``` 368 | 369 | If the Role does not have the above logging permissions, the command will still work but no logs will be written. 370 | 371 | #### Note: 372 | The Role in the Instance Profile should specify `ec2.amazonaws.com` as a Trusted Entity. 373 | An Instance Profile is created automatically when a Role is created using the Console for an EC2 instance. 374 | 375 | _Required_: No 376 | 377 | _Type_: String 378 | 379 | _Update requires_: Replacement 380 | 381 | ### LogGroup 382 | 383 | The CloudWatch Log Group to stream the logs from the specified command. 384 | 385 | If one is not provided the default `cloudformation-commandrunner-log-group` one will be used. 386 | 387 | If the specified log group does not exist, a new one will be created. 388 | 389 | #### Tip: 390 | To log a trace of your commands and their arguments after they are expanded and before they are executed, run `set -xe` in the `Command` property before your actual command. 391 | 392 | _Required_: No 393 | 394 | _Type_: String 395 | 396 | _Update requires_: Replacement 397 | 398 | ### SubnetId 399 | 400 | The Id of the Subnet to execute the command in. Note that the SubnetId specified should have access to the internet to be able to communicate back to CloudFormation. Ensure that the Route Table associated with the Subnet has a route to the internet via either an Internet Gateway (IGW) or a NAT Gateway (NGW). 401 | 402 | #### Note: 403 | If the `SubnetID` is not specified, it will create the resource in a subnet in the default VPC of the region. 404 | 405 | _Required_: No 406 | 407 | _Type_: String 408 | 409 | _Update requires_: Replacement 410 | 411 | ### SecurityGroupId 412 | 413 | The Id of the Security Group to attach to the instance the command is run in. If using SecurityGroup, the SubnetId property is required. 414 | 415 | #### Note: 416 | If the `SecurityGroupId` is not specified, the command will be run with a security group with open Egress rules and no Ingress rules. 417 | 418 | _Required_: No 419 | 420 | _Type_: String 421 | 422 | _Update requires_: Replacement 423 | 424 | ### KeyId 425 | 426 | Id of the KMS key to use when encrypting the output stored in SSM Parameter Store. If not specified, the account's default KMS key is used. 427 | 428 | _Required_: No 429 | 430 | _Type_: String 431 | 432 | _Update requires_: Replacement 433 | 434 | ### Timeout 435 | 436 | By default, the timeout is 600 seconds. To increase the timeout specify a higher Timeout value in seconds. The maximum timeout value is 43200 seconds i.e 12 hours. 437 | 438 | _Required_: No 439 | 440 | _Type_: String 441 | 442 | _Update requires_: Replacement 443 | 444 | ### DisableTerminateInstancesCheck 445 | 446 | By default, CommandRunner checks to see if the execution role can perform a TerminateInstances API call. Set this property to true if you want to skip the check. Note that this means that the CommandRunner instance may not be terminated and will have to be terminated manually. 447 | 448 | _Required_: No 449 | 450 | _Type_: String 451 | 452 | _Update requires_: Replacement 453 | 454 | ### InstanceType 455 | 456 | By default, the instance type used is t2.medium. However you can use this property to specify any supported instance type. 457 | 458 | _Required_: No 459 | 460 | _Type_: String 461 | 462 | _Update requires_: Replacement 463 | 464 | --- 465 | 466 | # Return Values 467 | 468 | ### Fn::GetAtt 469 | 470 | Users can reference the output of the command written to `/command-output.txt` using `Fn::GetAtt` like in the following syntax. 471 | 472 | ```yaml 473 | Outputs: 474 | Output: 475 | Description: The output of the command. 476 | Value: !GetAtt Command.Output 477 | ``` 478 | 479 | --- 480 | 481 | # User Guides 482 | 483 | ## Run A Command Before Or After A Resource 484 | 485 | To run the command after a resource with logical name `Resource`, specify `DependsOn: Resource` in the AWSUtility::CloudFormation::CommandRunner resource's definition. 486 | 487 | ```yaml 488 | Resources: 489 | Command: 490 | DependsOn: Resource 491 | Type: AWSUtility::CloudFormation::CommandRunner 492 | Properties: 493 | Command: echo success > /command-output.txt 494 | LogGroup: my-cloudwatch-log-group 495 | Role: MyEC2InstanceProfile 496 | Resource: 497 | Type: AWS::EC2::Instance 498 | Properties: 499 | Image: ami-abcd1234 500 | ``` 501 | 502 | To run the command before a resource, put a `DependsOn` with the logical name of the AWSUtility::CloudFormation::CommandRunner resource in that resource's definition. 503 | 504 | ```yaml 505 | Resources: 506 | Command: 507 | Type: AWSUtility::CloudFormation::CommandRunner 508 | Properties: 509 | Command: echo success > /command-output.txt 510 | LogGroup: my-cloudwatch-log-group 511 | Role: MyEC2InstanceProfile 512 | Resource: 513 | DependsOn: Command 514 | Type: AWS::EC2::Instance 515 | Properties: 516 | Image: ami-abcd1234 517 | ``` 518 | 519 | ## Run a script in any programming language using any SDK 520 | 521 | You can write a script in any programming language and upload it to S3. Use the `aws s3 cp` command to copy the script from S3 followed by `&&` and the command to run the script like the following example. 522 | 523 | ```yaml 524 | Resources: 525 | Command: 526 | Type: AWSUtility::CloudFormation::CommandRunner 527 | Properties: 528 | Command: 'aws s3 cp s3://cfn-cli-project/S3BucketCheck.py . && python S3BucketCheck.py my-bucket third-name-option-a' 529 | Role: MyEC2InstanceProfile 530 | LogGroup: my-cloudwatch-log-group 531 | Outputs: 532 | Output: 533 | Description: The output of the command. 534 | Value: !GetAtt Command.Output 535 | ``` 536 | 537 | ## Install Packages before Running Command 538 | 539 | ```yaml 540 | Resources: 541 | Command: 542 | Type: AWSUtility::CloudFormation::CommandRunner 543 | Properties: 544 | Command: 545 | Fn::Sub: | 546 | yum install jq -y 547 | aws ssm get-parameter --name RepositoryName --region us-east-1 > response.json 548 | jq -r .Parameter.Value response.json > /command-output.txt' 549 | Role: MyEC2InstanceProfile 550 | LogGroup: my-cloudwatch-log-group 551 | Outputs: 552 | Output: 553 | Description: The output of the command. 554 | Value: !GetAtt Command.Output 555 | ``` 556 | 557 | 558 | ## Using AWSCLI --query option 559 | 560 | ```yaml 561 | Resources: 562 | Command: 563 | Type: AWSUtility::CloudFormation::CommandRunner 564 | Properties: 565 | Command: 566 | Fn::Sub: | 567 | aws ssm get-parameter --name RepositoryName --region us-east-1 --query Parameter.Value --output text > /command-output.txt 568 | Role: MyEC2InstanceProfile 569 | LogGroup: my-cloudwatch-log-group 570 | Outputs: 571 | Output: 572 | Description: The output of the command. 573 | Value: !GetAtt Command.Output 574 | ``` 575 | 576 | ## Run A Multi-Line Script 577 | 578 | ```yaml 579 | Resources: 580 | CommandRunner: 581 | Type: AWSUtility::CloudFormation::CommandRunner 582 | Properties: 583 | Command: 584 | Fn::Sub: | 585 | echo "my log" 586 | echo '{"key":"value"}' > mydata.json 587 | ... 588 | 589 | ... 590 | echo success > /command-output.txt 591 | LogGroup: my-cloudwatch-log-group 592 | Role: MyEC2InstanceProfile 593 | ``` 594 | 595 | # Use Cases 596 | 597 | - The AWSUtility::CloudFormation::CommandRunner resource lets you perform any API call, script, custom logic, external check, conditions, cleanup, dynamic parameter retrieval and anything else that can be done using a command. 598 | 599 | - Get parameters dynamically during the Stack's execution instead of passing in Parameters during stack creation. 600 | - Currently, Dynamic Referencing i.e SSM {{resolve}} on CloudFormation cannot automatically get the latest version of the SSM Parameter. Due to this, users have to know the latest version number and manually put it in every time or the CFN stack will continue to resolve to the old version. This can be worked around using AWSUtility::CloudFormation::CommandRunner to always get the latest parameter value. 601 | 602 | - Currently, there is no AWS::ECS resource that allows you to configure the Account Settings. However, you can do this using the AWSUtility::CloudFormation::CommandRunner resource by running the `aws ecs put-account-setting` CLI commmand. 603 | 604 | - Currently, there is no way to create an image (AMI) using a running EC2 instance, but it can be done using the AWSUtility::CloudFormation::CommandRunner resource by using the `aws ec2 create-image` CLI command. 605 | 606 | - Add a lag between resources using CommandRunner. Specify a sleep command in the Command property. 607 | 608 | --- 609 | 610 | # FAQ 611 | 612 | #### Q. Why use EC2 instead of Lambda? 613 | 614 | - Lambda does not natively support using Bash. 615 | 616 | - Even if Bash is added using custom Lambda Layers it will still not allow installing new packages or running other programming / scripting languages. 617 | 618 | - Lambda is also more expensive than running a small EC2 instance for approximately 2 minutes. 619 | 620 | #### Q. Why make this when Custom Resources and Macros are available? 621 | 622 | - Developing a Custom Resource or Macro requires writing several lines of code and troubleshooting which is considered to be development effort. 623 | 624 | - The AWSUtility::CloudFormation::CommandRunner provides a quick fix to solve problems with a single line of code and does not require any development effort. 625 | 626 | - Using the AWSUtility::CloudFormation::CommandRunner, users can quickly unblock themselves without relying on CloudFormation to resolve the issue to unblock them. 627 | 628 | --- 629 | 630 | # Developer Build Steps 631 | 632 | Please execute the included build script by running `./scripts/build.sh` to build and register the resource to AWS CloudFormation for your account. The script waits while CloudFormation registers the resource so it typically takes about 5-10 minutes. 633 | 634 | Note that the script assumes that you have AWS CLI configured and the necessary permissions to register a resource provider to CloudFormation, jq, mvn and the prerequisites mentioned here. `https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/what-is-cloudformation-cli.html` 635 | 636 | The build script will do the following: 637 | 638 | 1. Runs `cfn generate` to generate the rpdk files for the Resource Provider. 639 | 640 | 2. `mvn package` packages the Java code up and `cfn submit` registers the built Resource to AWS CloudFormation in your AWS account. 641 | 642 | 3. From the output of the `cfn submit` command, it gets the version of the build and updates the default version to be used in CloudFormation. 643 | 644 | Once the script finishes, the AWSUtility::CloudFormation::CommandRunner resource will be ready to use. 645 | 646 | You can find an example of how to use the resource in the file `usage-template.yaml`. 647 | 648 | As of March 2024, the recommended versions and dependencies for the build are as follows. 649 | ``` 650 | $ cfn --version 651 | cfn 0.2.35 652 | 653 | $ mvn -version 654 | Apache Maven 3.9.6 (bc0240f3c744dd6b6ec2920b3cd08dcc295161ae) 655 | Maven home: /opt/homebrew/Cellar/maven/3.9.6/libexec 656 | Java version: 19.0.2, vendor: Oracle Corporation, runtime: /Library/Java/JavaVirtualMachines/jdk-19.jdk/Contents/Home 657 | Default locale: en_US, platform encoding: UTF-8 658 | OS name: "mac os x", version: "14.1.1", arch: "aarch64", family: "mac" 659 | 660 | $ java -version 661 | java 19.0.2 2023-01-17 662 | Java(TM) SE Runtime Environment (build 19.0.2+7-44) 663 | Java HotSpot(TM) 64-Bit Server VM (build 19.0.2+7-44, mixed mode, sharing) 664 | 665 | $ ./scripts/build.sh 666 | ``` 667 | 668 | --- 669 | 670 | # Change Log 671 | 672 | ### v2.0.1 673 | 674 | * Updated from `java8` to `java8.al2`. 675 | * Updated README and added more examples. 676 | * Fixed IAM policy. 677 | * Fixed `register.sh`. 678 | * Better region determination in `register.sh`. 679 | 680 | ### v2.0 681 | 682 | * Updated package versions in pom.xml to latest, fixing build issues related to outdated dependencies. 683 | * Improved Error Handling 684 | * For when command fails or when invalid value is written to `/command-output.txt` 685 | * Error message about checking cloud-init-output.log also includes network related issues. 686 | * When no default VPC exists. 687 | * Added try catch block for catching exception if no default VPC. 688 | * Error message - "No default VPC found in this region, please specify a subnet using the SubnetId property." 689 | * Improved logging 690 | * Added contents of `/command-output.txt` to CloudWatch logs under `cloud-init-output.log`. 691 | * Updated BaseTemplate to add contents of /command-output.txt to cloudwatch logs. 692 | * Added catch for failures on CommandRunner stack. 693 | * Failure on CommandRunner stack was not being caught when response sent to CommandRunner stack in WaitCondition is malformed. 694 | * Failures are now caught right away, if CommandRunner stack goes into ROLLBACK_COMPLETE, or ROLLBACK_FAILED, then it will now gracefully clean up the CommandRunner stack. 695 | * Updated user installation script `register.sh` 696 | * Added creation of log group in `register.sh`, along with handling the case where it already exists. 697 | * LogGroup not created for new region, line 103 of register.sh check if log-group exists if not, create one. 698 | * register.sh will try to create a fresh execution role stack, if it exists it will try to update it, if it is up to date it will skip it. 699 | * Fixed bugs with networking configuration properties i.e `SubnetId`, `SecurityGroupId` 700 | * Removed empty string checks, now all different scenarios with/without SubnetId, SecurityGroupId work. 701 | * Added new `Timeout` property. 702 | * Timeout property to change timeout in WaitCondition in BaseTemplate, this will give the option to easy fail, by default timeout is 600 right now, this will allow for a max timeout of 12 hours i.e 43200 703 | * Added new `DisableTerminateInstancesCheck` property. 704 | * Some users were running into issues where their SCP policies did not allow the `ec2:TerminateInstances` action, but they still want to create CommandRunner instances. Setting this property to true allows them to create CommandRunner instances even without the `ec2:TerminateInstances` action. 705 | * Added new `InstanceType` property. 706 | * Now works in Private Subnets. We had seen some issues where CommandRunner wouldn't work in private subnets, this issue is now resolved. 707 | * Added check for Instance Profile validity. An error is thrown within 5 seconds of resource creation if the Role property specified is not a valid Instance Profile. 708 | * Check for Instance Profile validity, performs DescribeInstanceProfile and catches the error if it doesn’t exist. 709 | * https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/identitymanagement/model/GetInstanceProfileResult.html 710 | * https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/identitymanagement/model/GetInstanceProfileRequest.html 711 | * Error message: *“*The Role property specified is not a valid Instance Profile.” 712 | * Added .gitignore to repository, removed unnecessary temporary files. 713 | * Added the CommandRunner banner to both installation and build scripts. 714 | * Documentation 715 | * Fixed typos and grammatical errors. 716 | * Added new properties to all examples and schemas. 717 | * Added new properties to documentation. 718 | * Added new permissions to documentation. 719 | * Fixed a bug where a fresh installation using register.sh wouldn’t work unless build.sh had been used before it. 720 | 721 | 722 | ### v1.21 723 | 724 | - Updated README to add more clarification into what values are accepted by `/command-output.txt`. 725 | - Changed `cloudwatch:` to `logs:`, fixing the permissions issue when writing logs. 726 | - Updated README to improve instructions for user installation steps. 727 | 728 | ### v1.2 729 | 730 | - Output stored in SSM Parameter Store is now `SecureString` by default i.e Encrypted at rest using the Default KMS key of the account. 731 | - Added new parameter `KMSKeyId` allowing users to specify their own customer-managed KMS Key to encrypt the SSM SecureString Parameter. 732 | - Updated README with log permissions and specified that no error is thrown when it can’t write to log group. It requires the following permissions to write logs. If not provided, it won’t do any logging. 733 | ``` 734 | "logs:CreateLogStream", 735 | "logs:CreateLogGroup", 736 | "logs:PutLogEvents" 737 | ``` 738 | - The idea is that the command should still run even if the logs can’t be written. Users should have the option to not log if not required. 739 | 740 | ### v1.1 741 | 742 | - Added `register.sh`and user build steps 743 | - Added notes to the Properties 744 | - Contract tests using `cfn test` all work with the following results. 745 | 746 | ```bash 747 | collected 12 items / 5 deselected / 7 selected 748 | 749 | handler_create.py::contract_create_delete PASSED [ 14%] 750 | handler_create.py::contract_create_duplicate PASSED [ 28%] 751 | handler_create.py::contract_create_read_success PASSED [ 42%] 752 | handler_delete.py::contract_delete_read PASSED [ 57%] 753 | handler_delete.py::contract_delete_delete PASSED [ 71%] 754 | handler_delete.py::contract_delete_create SKIPPED [ 85%] 755 | handler_misc.py::contract_check_asserts_work PASSED [100%] 756 | ``` 757 | 758 | ### v1.0 759 | 760 | - Improved build script `build.sh`, it now does not use S3. The `BaseTemplate.json` is stored as a resource in the `.jar` file. 761 | - Cleaned up code, removed comments, verbose code, etc. 762 | 763 | ### v0.9 764 | 765 | 766 | - Removed all the extra build steps, now it takes only running the `build.sh` script after you’ve cloned the repo. 767 | - Previously, to build the project, the static variables in `CreateHandler.java` needed to be replaced. They are now dynamically inferred and replaced in `CreateHandler.java` using the `build.sh` script. 768 | - Added 2 new parameters and the Java logic to support them. Users can now optionally specify the `SubnetId` and the `SecurityGroupId` or both. If neither is provided it’ll use a subnet in the default VPC and create a SecurityGroup automatically. 769 | 770 | - Instead of using `{{resolve}}`, `Fn::GetAtt` now works, you can do `!GetAtt Command.Output` 771 | 772 | - Updated the presentation to reflect the above changes. 773 | 774 | - Updated the documentation 775 | - New build steps and what `build.sh` does 776 | - Dependencies and versions used 777 | - Disclaimer about Command in docs 778 | - Properties SubnetId and SecurityGroupId 779 | - Referencing using Fn::GetAtt 780 | - Added Change History section 781 | 782 | - If a security group is not provided, the one that is automatically created has no inbound rules and only allows outbound communication. 783 | 784 | - Previously I had provided wildcard permissions to the resource. Now, only the below permissions are used and there are no wildcards. 785 | 786 | ``` 787 | cloudformation:DeleteStack 788 | cloudformation:CreateStack 789 | cloudformation:DescribeStacks 790 | 791 | logs:CreateLogStream 792 | logs:DescribeLogGroups 793 | 794 | ssm:GetParameter 795 | ssm:PutParameter 796 | 797 | ec2:DescribeSubnets 798 | ec2:DescribeVpcs 799 | ec2:DescribeSecurityGroups 800 | ec2:CreateSecurityGroup 801 | ec2:RevokeSecurityGroupEgress 802 | ec2:RevokeSecurityGroupIngress 803 | ec2:CreateTags 804 | ec2:AuthorizeSecurityGroupIngress 805 | ec2:AuthorizeSecurityGroupEgress 806 | ec2:RunInstances 807 | ec2:DescribeInstances 808 | ec2:TerminateInstances 809 | ec2:DeleteSecurityGroup 810 | iam:PassRole 811 | 812 | #Only required if using the KeyId property, i.e custom KMS Key for the SSM SecureString 813 | kms:Encrypt 814 | kms:Decrypt 815 | ``` 816 | 817 | --- 818 | 819 | # See Also 820 | 821 | [AWS Blogs - AWS Cloud Operations & Migrations Blog - Running bash commands in AWS CloudFormation templates](https://aws.amazon.com/blogs/mt/running-bash-commands-in-aws-cloudformation-templates/) 822 | 823 | [AWS Premium Support - Knowledge Center - CloudFormation - How do I use AWSUtility::CloudFormation::CommandRunner to run a command before or after a resource in my CloudFormation stack?](https://aws.amazon.com/blogs/mt/running-bash-commands-in-aws-cloudformation-templates/) 824 | -------------------------------------------------------------------------------- /awsutility-cloudformation-commandrunner.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeName": "AWSUtility::CloudFormation::CommandRunner", 3 | "description": "The AWSUtility::CloudFormation::CommandRunner resource runs any command provided to it before or after any Stack resource. The output of the command can be accessed by using Fn::GetAtt on the AWSUtility::CloudFormation::CommandRunner resource.", 4 | "properties": { 5 | "Command": { 6 | "description": "The command to be run using the provided IAM role. Use '>' to output to the file /command-output.txt and it will be available when referencing this resource using Fn::Ref.", 7 | "type": "string" 8 | }, 9 | "Role": { 10 | "description": "The IAM role used to run the specified command.", 11 | "type": "string" 12 | }, 13 | "LogGroup": { 14 | "description": "The CloudWatch Log Group to stream the logs from the specified command.", 15 | "type": "string" 16 | }, 17 | "Output": { 18 | "type": "string", 19 | "description": "Output of the command that was executed." 20 | }, 21 | "Id": { 22 | "type": "string", 23 | "description": "Id of the command executed [Read-Only]" 24 | }, 25 | "SubnetId": { 26 | "type": "string", 27 | "description": "Id of the Subnet to execute the command in." 28 | }, 29 | "KeyId": { 30 | "type": "string", 31 | "description": "Id of the KMS key to use when encrypting the output stored in SSM. If not specified, the account's default KMS key is used." 32 | }, 33 | "SecurityGroupId": { 34 | "type": "string", 35 | "description": "Id of the Security Group to attach to the instance the command is run in. If using SecurityGroup, SubnetId is required." 36 | }, 37 | "DisableTerminateInstancesCheck": { 38 | "type": "string", 39 | "description": "By default, CommandRunner checks to see if the execution role can perform a TerminateInstances API call. Set this property to true if you want to skip the check. Note that this means that the CommandRunner instance may not be terminated and will have to be terminated manually." 40 | }, 41 | "Timeout": { 42 | "type": "string", 43 | "description": "By default, the timeout is 600 seconds. To increase the timeout specify a higher Timeout value in seconds. Maximum timeout value is 43200 seconds i.e 12 hours." 44 | }, 45 | "InstanceType": { 46 | "type": "string", 47 | "description": "By default, the instance type used is t2.medium. However you can use this property to specify any supported instance type." 48 | } 49 | }, 50 | "additionalProperties": false, 51 | "required": [ 52 | "Command" 53 | ], 54 | "readOnlyProperties": [ 55 | "/properties/Output", 56 | "/properties/Id" 57 | ], 58 | "primaryIdentifier": [ 59 | "/properties/Id" 60 | ], 61 | "handlers": { 62 | "create": { 63 | "permissions": [ 64 | "ec2:DescribeSubnets", 65 | "ec2:DescribeVpcs", 66 | "ec2:DescribeSecurityGroups", 67 | "ec2:CreateSecurityGroup", 68 | "ec2:RevokeSecurityGroupEgress", 69 | "ec2:RevokeSecurityGroupIngress", 70 | "ec2:CreateTags", 71 | "ec2:AuthorizeSecurityGroupIngress", 72 | "ec2:AuthorizeSecurityGroupEgress", 73 | "ec2:RunInstances", 74 | "ec2:DescribeInstances", 75 | "ec2:TerminateInstances", 76 | "ec2:DeleteSecurityGroup", 77 | "cloudformation:DeleteStack", 78 | "cloudformation:CreateStack", 79 | "cloudformation:DescribeStacks", 80 | "logs:CreateLogStream", 81 | "logs:DescribeLogGroups", 82 | "cloudwatch:PutMetricData", 83 | "logs:PutLogEvents", 84 | "ssm:GetParameter", 85 | "ssm:PutParameter", 86 | "iam:PassRole", 87 | "kms:Encrypt", 88 | "kms:Decrypt", 89 | "sts:GetCallerIdentity", 90 | "iam:SimulatePrincipalPolicy", 91 | "iam:GetInstanceProfile" 92 | ] 93 | }, 94 | "read": { 95 | "permissions": [ 96 | "ssm:GetParameter", 97 | "cloudwatch:PutMetricData" 98 | ] 99 | }, 100 | "delete": { 101 | "permissions": [ 102 | "ssm:DeleteParameter" 103 | ] 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /banner.txt: -------------------------------------------------------------------------------- 1 | ____________________________________________________________________________________ 2 | \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 3 | \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 4 | / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / 5 | /_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/ 6 | 7 | __________ __ _____ ______ _ ______ ____ __ ___ ___ ____________ 8 | / ____/ __ \/ |/ / |/ / | / | / / __ \/ __ \/ / / / | / / | / / ____/ __ \ 9 | / / / / / / /|_/ / /|_/ / /| | / |/ / / / / /_/ / / / / |/ / |/ / __/ / /_/ / 10 | / /___/ /_/ / / / / / / / ___ |/ /| / /_/ / _, _/ /_/ / /| / /| / /___/ _, _/ 11 | \____/\____/_/ /_/_/ /_/_/ |_/_/ |_/_____/_/ |_|\____/_/ |_/_/ |_/_____/_/ |_| 12 | 13 | ____________________________________________________________________________________ 14 | \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 15 | \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 16 | / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / 17 | /_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/ 18 | -------------------------------------------------------------------------------- /buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | phases: 3 | install: 4 | runtime-versions: 5 | java: openjdk8 6 | python: 3.7 7 | commands: 8 | - pip install pre-commit cloudformation-cli-java-plugin 9 | build: 10 | commands: 11 | - pre-commit run --all-files 12 | # - cd "$CODEBUILD_SRC_DIR/my_resource" 13 | # - mvn clean verify --no-transfer-progress 14 | # finally: 15 | # - cat "$CODEBUILD_SRC_DIR/my_resource/rpdk.log" 16 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # AWSUtility::CloudFormation::CommandRunner 2 | 3 | The AWSUtility::CloudFormation::CommandRunner resource runs any command provided to it before or after any Stack resource. The output of the command can be accessed by using Fn::GetAtt on the AWSUtility::CloudFormation::CommandRunner resource. 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" : "AWSUtility::CloudFormation::CommandRunner",
 14 |     "Properties" : {
 15 |         "Command" : String,
 16 |         "Role" : String,
 17 |         "LogGroup" : String,
 18 |         "SubnetId" : String,
 19 |         "KeyId" : String,
 20 |         "SecurityGroupId" : String,
 21 |         "DisableTerminateInstancesCheck" : String,
 22 |         "Timeout" : String,
 23 |         "InstanceType" : String
 24 |     }
 25 | }
 26 | 
27 | 28 | ### YAML 29 | 30 |
 31 | Type: AWSUtility::CloudFormation::CommandRunner
 32 | Properties:
 33 |     Command: String
 34 |     Role: String
 35 |     LogGroup: String
 36 |     SubnetId: String
 37 |     KeyId: String
 38 |     SecurityGroupId: String
 39 |     DisableTerminateInstancesCheck: String
 40 |     Timeout: String
 41 |     InstanceType: String
 42 | 
43 | 44 | ## Properties 45 | 46 | #### Command 47 | 48 | The command to be run using the provided IAM role. Use '>' to output to the file /command-output.txt and it will be available when referencing this resource using Fn::Ref. 49 | 50 | _Required_: Yes 51 | 52 | _Type_: String 53 | 54 | _Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) 55 | 56 | #### Role 57 | 58 | The IAM role used to run the specified command. 59 | 60 | _Required_: No 61 | 62 | _Type_: String 63 | 64 | _Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) 65 | 66 | #### LogGroup 67 | 68 | The CloudWatch Log Group to stream the logs from the specified command. 69 | 70 | _Required_: No 71 | 72 | _Type_: String 73 | 74 | _Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) 75 | 76 | #### SubnetId 77 | 78 | Id of the Subnet to execute the command in. 79 | 80 | _Required_: No 81 | 82 | _Type_: String 83 | 84 | _Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) 85 | 86 | #### KeyId 87 | 88 | Id of the KMS key to use when encrypting the output stored in SSM. If not specified, the account's default KMS key is used. 89 | 90 | _Required_: No 91 | 92 | _Type_: String 93 | 94 | _Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) 95 | 96 | #### SecurityGroupId 97 | 98 | Id of the Security Group to attach to the instance the command is run in. If using SecurityGroup, SubnetId is required. 99 | 100 | _Required_: No 101 | 102 | _Type_: String 103 | 104 | _Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) 105 | 106 | #### DisableTerminateInstancesCheck 107 | 108 | By default, CommandRunner checks to see if the execution role can perform a TerminateInstances API call. Set this property to true if you want to skip the check. Note that this means that the CommandRunner instance may not be terminated and will have to be terminated manually. 109 | 110 | _Required_: No 111 | 112 | _Type_: String 113 | 114 | _Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) 115 | 116 | #### Timeout 117 | 118 | By default, the timeout is 600 seconds. To increase the timeout specify a higher Timeout value in seconds. Maximum timeout value is 43200 seconds i.e 12 hours. 119 | 120 | _Required_: No 121 | 122 | _Type_: String 123 | 124 | _Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) 125 | 126 | #### InstanceType 127 | 128 | By default, the instance type used is t2.medium. However you can use this property to specify any supported instance type. 129 | 130 | _Required_: No 131 | 132 | _Type_: String 133 | 134 | _Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) 135 | 136 | ## Return Values 137 | 138 | ### Ref 139 | 140 | When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the Id. 141 | 142 | ### Fn::GetAtt 143 | 144 | The `Fn::GetAtt` intrinsic function returns a value for a specified attribute of this type. The following are the available attributes and sample return values. 145 | 146 | For more information about using the `Fn::GetAtt` intrinsic function, see [Fn::GetAtt](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html). 147 | 148 | #### Output 149 | 150 | Output of the command that was executed. 151 | 152 | #### Id 153 | 154 | Id of the command executed [Read-Only] 155 | -------------------------------------------------------------------------------- /examples/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-cloudformation/aws-cloudformation-resource-providers-awsutilities-commandrunner/b59933e2f79a4e5991741cfa1e4f1a2be70fc57d/examples/.DS_Store -------------------------------------------------------------------------------- /examples/awscli-ssm-get-parameter-template.yaml: -------------------------------------------------------------------------------- 1 | Resources: 2 | Command: 3 | Type: AWSUtility::CloudFormation::CommandRunner 4 | Properties: 5 | Command: 'yum install jq -y && aws ssm get-parameter --name RepositoryName --region us-east-1 | jq -r .Parameter.Value > /command-output.txt' 6 | Role: EC2AdminRole 7 | LogGroup: my-cloudwatch-log-group 8 | Outputs: 9 | Output: 10 | Description: The output of the commandrunner. 11 | Value: 12 | Fn::GetAtt: Command.Output 13 | -------------------------------------------------------------------------------- /examples/commandrunner-example-iopscalc-template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Parameters: 3 | EBSVolumeSize: 4 | Type: Number 5 | Default: 10 6 | MinValue: 10 7 | MaxValue: 50 8 | Resources: 9 | IopsCalculator: 10 | Type: AWSUtility::CloudFormation::CommandRunner 11 | Properties: 12 | Command: 13 | Fn::Sub: 'expr ${EBSVolumeSize} \* 20 > /command-output.txt' 14 | Outputs: 15 | Iops: 16 | Description: EBS IOPS 17 | Value: 18 | Fn::GetAtt: IopsCalculator.Output 19 | -------------------------------------------------------------------------------- /examples/gatsby-static-website/gatsby-deploy-template.yaml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | BucketName: 3 | Type: String 4 | 5 | Resources: 6 | CommandRunner: 7 | Type: AWSUtility::CloudFormation::CommandRunner 8 | Properties: 9 | Command: 10 | Fn::Sub: | 11 | sudo su 12 | cd /home/ec2-user/ 13 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash 14 | export NVM_DIR="$HOME/.nvm" 15 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" 16 | [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" 17 | nvm --version 18 | nvm install node 19 | npm install -g gatsby-cli 20 | yum install git -y 21 | gatsby new gatsby-starter-portfolio-cara https://github.com/LekoArts/gatsby-starter-portfolio-cara 22 | cd /home/ec2-user/gatsby-starter-portfolio-cara/src/@lekoarts/gatsby-theme-cara/sections/ 23 | echo '# Congratulations!' > intro.mdx 24 | echo '' >> intro.mdx 25 | echo 'You can now run Bash commands in your CloudFormation Stacks.' >> intro.mdx 26 | cd /home/ec2-user/gatsby-starter-portfolio-cara/node_modules/@lekoarts/gatsby-theme-cara/src/templates/ 27 | sed -i 's/5/1/g' cara.tsx 28 | cd /home/ec2-user/gatsby-starter-portfolio-cara/ 29 | sed -i 's/Cara - Gatsby Starter Portfolio/CommandRunner/g' gatsby-config.js 30 | gatsby build 31 | cd public 32 | aws s3 sync . s3://${S3Bucket} 33 | #aws s3api put-bucket-website --bucket ${S3Bucket} --website-configuration '{"IndexDocument":{"Suffix":"index.html"}}' 34 | #aws s3api put-bucket-policy --policy '{"Version":"2012-10-17","Statement":[{"Sid":"PublicReadGetObject","Effect":"Allow","Principal":"*","Action":"s3:GetObject","Resource":"arn:aws:s3:::'"${S3Bucket}"'/*"}]}' --bucket ${S3Bucket} 35 | gatsby -v | grep CLI -A 0 -B 0 >> /command-output.txt 36 | Role: EC2AdminRole 37 | LogGroup: new-log-group-pls-delete 38 | 39 | S3Bucket: 40 | Type: AWS::S3::Bucket 41 | Properties: 42 | BucketName: 43 | Ref: BucketName 44 | PublicAccessBlockConfiguration: 45 | BlockPublicAcls: FALSE 46 | WebsiteConfiguration: 47 | IndexDocument: index.html 48 | AccessControl: PublicRead 49 | VersioningConfiguration: 50 | Status: Enabled 51 | 52 | S3BucketPolicy: 53 | Type: AWS::S3::BucketPolicy 54 | Properties: 55 | Bucket: 56 | Ref: S3Bucket 57 | PolicyDocument: 58 | Statement: 59 | - Sid: PublicReadGetObject 60 | Effect: Allow 61 | Principal: '*' 62 | Action: 's3:GetObject' 63 | Resource: 64 | Fn::Sub: 'arn:aws:s3:::${S3Bucket}/*' 65 | 66 | 67 | 68 | Outputs: 69 | WebsiteURL: 70 | Description: The output of the commandrunner. 71 | Value: 72 | Fn::Sub: 'http://${S3Bucket}.s3-website-${AWS::Region}.amazonaws.com' 73 | GatsbyVersion: 74 | Description: The version of Gatsby CLI used to create the static website. 75 | Value: 76 | Fn::GetAtt: CommandRunner.Output 77 | -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | lombok.addLombokGeneratedAnnotation = true 2 | -------------------------------------------------------------------------------- /overrides.json: -------------------------------------------------------------------------------- 1 | { 2 | "CREATE": { 3 | "/Command": "yum install jq -y && aws ssm get-parameter --name RepositoryName --region us-east-1 | jq -r .Parameter.Value > /command-output.txt", 4 | "/Role": "EC2AdminRole", 5 | "/LogGroup": "my-cloudwatch-log-group" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 4.0.0 7 | 8 | software.awsutility.cloudformation.commandrunner 9 | awsutility-cloudformation-commandrunner-handler 10 | awsutility-cloudformation-commandrunner-handler 11 | 1.0-SNAPSHOT 12 | jar 13 | 14 | 15 | 1.8 16 | 1.8 17 | UTF-8 18 | UTF-8 19 | 20 | 21 | 22 | 23 | central 24 | https://repo.maven.apache.org/maven2/ 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | com.amazonaws 33 | aws-java-sdk-ec2 34 | 1.12.131 35 | 36 | 37 | 38 | 39 | software.amazon.cloudformation 40 | aws-cloudformation-rpdk-java-plugin 41 | 2.0.10 42 | 43 | 44 | 45 | com.amazonaws 46 | aws-java-sdk-s3 47 | 1.12.131 48 | 49 | 50 | 51 | org.projectlombok 52 | lombok 53 | 1.18.22 54 | provided 55 | 56 | 57 | 58 | 59 | org.assertj 60 | assertj-core 61 | 3.22.0 62 | test 63 | 64 | 65 | 66 | org.junit.jupiter 67 | junit-jupiter 68 | 5.8.2 69 | test 70 | 71 | 72 | 73 | org.mockito 74 | mockito-core 75 | 4.2.0 76 | test 77 | 78 | 79 | 80 | org.mockito 81 | mockito-junit-jupiter 82 | 4.2.0 83 | test 84 | 85 | 86 | 91 | 92 | 93 | 94 | com.amazonaws 95 | aws-java-sdk-cloudformation 96 | 1.12.131 97 | 98 | 99 | 100 | 101 | com.amazonaws 102 | aws-java-sdk-ssm 103 | 1.12.131 104 | 105 | 106 | 107 | 108 | com.amazonaws 109 | aws-java-sdk-core 110 | 1.12.131 111 | 112 | 113 | 114 | 115 | com.amazonaws 116 | aws-java-sdk-sts 117 | 1.12.131 118 | 119 | 120 | 121 | 122 | com.amazonaws 123 | aws-java-sdk-iam 124 | 1.12.131 125 | 126 | 127 | 128 | 129 | 130 | com.amazonaws 131 | aws-lambda-java-core 132 | 1.2.1 133 | 134 | 135 | 136 | 137 | com.amazonaws 138 | jmespath-java 139 | 1.12.131 140 | 141 | 142 | 143 | 144 | com.fasterxml.jackson.core 145 | jackson-core 146 | 2.13.1 147 | 148 | 149 | org.slf4j 150 | slf4j-jdk14 151 | 1.7.25 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | org.apache.maven.plugins 160 | maven-compiler-plugin 161 | 3.8.1 162 | 163 | 164 | -Xlint:all,-options,-processing 165 | -Werror 166 | 167 | 168 | 169 | 170 | org.apache.maven.plugins 171 | maven-shade-plugin 172 | 3.2.4 173 | 174 | false 175 | 176 | 177 | 178 | package 179 | 180 | shade 181 | 182 | 183 | 184 | 185 | 186 | org.codehaus.mojo 187 | exec-maven-plugin 188 | 1.6.0 189 | 190 | 191 | generate 192 | generate-sources 193 | 194 | exec 195 | 196 | 197 | cfn 198 | generate 199 | ${project.basedir} 200 | 201 | 202 | 203 | 204 | 205 | org.codehaus.mojo 206 | build-helper-maven-plugin 207 | 3.2.0 208 | 209 | 210 | add-source 211 | generate-sources 212 | 213 | add-source 214 | 215 | 216 | 217 | ${project.basedir}/target/generated-sources/rpdk 218 | 219 | 220 | 221 | 222 | 223 | 224 | org.apache.maven.plugins 225 | maven-resources-plugin 226 | 3.2.0 227 | 228 | 229 | maven-surefire-plugin 230 | 3.0.0-M5 231 | 232 | 233 | org.jacoco 234 | jacoco-maven-plugin 235 | 0.8.8 236 | 237 | 238 | **/BaseConfiguration* 239 | **/BaseHandler* 240 | **/HandlerWrapper* 241 | **/ResourceModel* 242 | 243 | 244 | 245 | 246 | 247 | prepare-agent 248 | 249 | 250 | 251 | report 252 | test 253 | 254 | report 255 | 256 | 257 | 258 | jacoco-check 259 | 260 | check 261 | 262 | 263 | 264 | 265 | PACKAGE 266 | 267 | 268 | BRANCH 269 | COVEREDRATIO 270 | 0.8 271 | 272 | 273 | INSTRUCTION 274 | COVEREDRATIO 275 | 0.8 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | ${project.basedir} 288 | 289 | awsutility-cloudformation-commandrunner.json 290 | 291 | 292 | 293 | src/main/resources/ 294 | 295 | BaseTemplate.json 296 | 297 | 298 | 299 | 300 | 301 | -------------------------------------------------------------------------------- /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/AWSUtility-CloudFormation-CommandRunner/* 25 | Path: "/" 26 | Policies: 27 | - PolicyName: ResourceTypePolicy 28 | PolicyDocument: 29 | Version: '2012-10-17' 30 | Statement: 31 | - Effect: Allow 32 | Action: 33 | - "cloudformation:CreateStack" 34 | - "cloudformation:DeleteStack" 35 | - "cloudformation:DescribeStacks" 36 | - "cloudwatch:PutMetricData" 37 | - "ec2:AuthorizeSecurityGroupEgress" 38 | - "ec2:AuthorizeSecurityGroupIngress" 39 | - "ec2:CreateSecurityGroup" 40 | - "ec2:CreateTags" 41 | - "ec2:DeleteSecurityGroup" 42 | - "ec2:DescribeInstances" 43 | - "ec2:DescribeSecurityGroups" 44 | - "ec2:DescribeSubnets" 45 | - "ec2:DescribeVpcs" 46 | - "ec2:RevokeSecurityGroupEgress" 47 | - "ec2:RevokeSecurityGroupIngress" 48 | - "ec2:RunInstances" 49 | - "ec2:TerminateInstances" 50 | - "iam:GetInstanceProfile" 51 | - "iam:PassRole" 52 | - "iam:SimulatePrincipalPolicy" 53 | - "kms:Decrypt" 54 | - "kms:Encrypt" 55 | - "logs:CreateLogStream" 56 | - "logs:DescribeLogGroups" 57 | - "logs:PutLogEvents" 58 | - "ssm:DeleteParameter" 59 | - "ssm:GetParameter" 60 | - "ssm:PutParameter" 61 | - "sts:GetCallerIdentity" 62 | Resource: "*" 63 | Outputs: 64 | ExecutionRoleArn: 65 | Value: 66 | Fn::GetAtt: ExecutionRole.Arn 67 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | cat banner.txt 2 | 3 | set -e 4 | cfn generate 5 | mvn -Dmaven.test.skip=true package 6 | cfn submit --set-default 7 | echo 'AWSUtility::CloudFormation::CommandRunner is ready to use.' 8 | -------------------------------------------------------------------------------- /scripts/cleanup.sh: -------------------------------------------------------------------------------- 1 | region=`aws configure get region` 2 | if [ -z "$region" ]; then 3 | echo "No region configured, please configure a default region using aws configure." 4 | exit 5 | fi 6 | 7 | while test $# -gt 0 8 | do 9 | case "$1" in 10 | --region) 11 | region=$2 12 | ;; 13 | # --bucket-name) 14 | ## echo "Bucket name provided is" $2 15 | # bucket_name=$2 16 | # ;; 17 | --*) 18 | echo "Not an option $1" 19 | ;; 20 | # *) echo "argument $1" 21 | # ;; 22 | esac 23 | shift 24 | done 25 | 26 | 27 | # Delete Execution Role 28 | 29 | stack_name='awsutility-cloudformation-commandrunner-execution-role-stack' 30 | 31 | echo "Checking if Execution Role exists..." 32 | 33 | describe_result=`aws cloudformation describe-stacks --stack-name $stack_name 2>&1` 34 | 35 | if ! [ $? -eq 0 ]; then 36 | echo "Execution role does not exist." 37 | echo "Deleting Execution Role skipped." 38 | else 39 | echo "Execution role exists." 40 | echo "Deleting Execution Role..." 41 | result=`aws cloudformation delete-stack --stack-name $stack_name 2>&1` 42 | if [ $? -eq 0 ]; then 43 | echo "Deleting Execution Role complete." 44 | else 45 | echo $result 46 | fi 47 | fi 48 | 49 | # Deregister Type 50 | 51 | 52 | type_name='AWSUtility::CloudFormation::CommandRunner' 53 | 54 | version='00000001' 55 | count=1 56 | 57 | while [ $version != "00000099" ] 58 | do 59 | echo "Deregistering version "$version... 60 | command=`aws cloudformation deregister-type --version-id $version --type RESOURCE --type-name $type_name --region $region 2>&1` 61 | if [ $? -eq 0 ]; then 62 | echo "Deregistering version "$version complete. 63 | else 64 | if [[ $command == *"error"* ]]; then 65 | aws cloudformation deregister-type --type RESOURCE --type-name $type_name --region $region 66 | echo Successfully deregistered $type_name from region $region. 67 | exit 0 68 | else 69 | echo $command 70 | exit 1 71 | fi 72 | fi 73 | count=`expr $count + 1` 74 | if [ $count -lt 10 ]; then 75 | version='0000000'$count 76 | fi 77 | if [ $count -ge 10 ]; then 78 | version='000000'$count 79 | fi 80 | done 81 | -------------------------------------------------------------------------------- /scripts/register.sh: -------------------------------------------------------------------------------- 1 | #if [ $# == 0 ]; then 2 | # echo "Usage: $0 --bucket-name " 3 | # exit 4 | #fi 5 | 6 | #region=`aws configure get region` 7 | region=`aws ec2 describe-availability-zones --output text --query 'AvailabilityZones[0].[RegionName]'` 8 | if [ -z "$region" ]; then 9 | echo "No region configured, please configure a default region using aws configure." 10 | exit 11 | fi 12 | 13 | 14 | set_default=0 15 | 16 | 17 | while test $# -gt 0 18 | do 19 | case "$1" in 20 | --set-default) 21 | set_default=1 22 | ;; 23 | # --bucket-name) 24 | ## echo "Bucket name provided is" $2 25 | # bucket_name=$2 26 | # ;; 27 | --*) 28 | echo "Not an option $1" 29 | ;; 30 | # *) echo "argument $1" 31 | # ;; 32 | esac 33 | shift 34 | done 35 | 36 | cat banner.txt 37 | 38 | # Create Execution Role 39 | 40 | echo Creating Execution Role... 41 | role_stack_id=`aws cloudformation create-stack --stack-name awsutility-cloudformation-commandrunner-execution-role-stack --template-body file://resource-role.yaml --capabilities CAPABILITY_IAM --query StackId --output text 2>> registration_logs.log` 42 | 43 | if ! [ $? -eq 0 ]; then 44 | #Check if any updates can be made if it already exists 45 | role_stack_id=`aws cloudformation update-stack --stack-name awsutility-cloudformation-commandrunner-execution-role-stack --template-body file://resource-role.yaml --capabilities CAPABILITY_IAM --query StackId --output text 2>> registration_logs.log` 46 | if ! [ $? -eq 0 ]; then 47 | echo Execution role already exists, no changes to be made. 48 | echo Creating Execution Role skipped. 49 | else 50 | echo Execution role already exists 51 | echo Updating Execution Role... 52 | fi 53 | fi 54 | 55 | stack_progress=`aws cloudformation describe-stacks --stack-name awsutility-cloudformation-commandrunner-execution-role-stack --query Stacks[0].StackStatus --output text` 56 | #stack_progress="CREATE_IN_PROGRESS" 57 | while [[ $stack_progress == *"IN_PROGRESS" ]] 58 | do 59 | echo "Waiting for execution role stack to complete..." 60 | sleep 10 61 | stack_progress=`aws cloudformation describe-stacks --stack-name awsutility-cloudformation-commandrunner-execution-role-stack --query Stacks[0].StackStatus --output text` 62 | if [[ $stack_progress == "CREATE_COMPLETE" ]] || [[ $stack_progress == "UPDATE_COMPLETE" ]]; then 63 | echo "Creating/Updating Execution Role complete." 64 | fi 65 | if [[ $stack_progress == "CREATE_FAILED" ]] || [[ $stack_progress == "ROLLBACK_COMPLETE" ]]; then 66 | echo "Execution role failed to create, check CloudFormation Stack awsutility-cloudformation-commandrunner-execution-role-stack for errors." 67 | exit 1 68 | fi 69 | done 70 | 71 | execution_role_arn=`aws cloudformation describe-stacks --stack-name awsutility-cloudformation-commandrunner-execution-role-stack --query Stacks[0].Outputs[0].OutputValue --output text` 72 | 73 | 74 | # Create Temporary S3 Bucket 75 | 76 | bucket_name=`uuidgen | tr '[:upper:]' '[:lower:]' | tr -d '-'` 77 | 78 | # Create a bucket always as it will be cleaned up after installation. 79 | echo Creating temporary S3 Bucket $bucket_name... 80 | mb_result=`aws s3 mb s3://$bucket_name --region $region 2>&1` 81 | if [ $? -eq 0 ]; then 82 | echo Creating temporary S3 Bucket $bucket_name complete. 83 | else 84 | if [[ $mb_result == *"BucketAlreadyOwnedByYou"* ]]; then 85 | echo Error: Bucket already owned, this installation requires the creation of a new temporary S3 Bucket. 86 | exit 1 87 | else 88 | echo Creating temporary S3 Bucket $bucket_name failed, please try again. 89 | echo $mb_result >> registration_logs.log 90 | echo $mb_result 91 | exit 1 92 | fi 93 | fi 94 | 95 | 96 | # Configure S3 Bucket Policy 97 | 98 | #set -e 99 | echo "Configuring S3 Bucket Policy for temporary S3 Bucket" $bucket_name... 100 | aws s3api put-bucket-policy --bucket $bucket_name --policy '{"Version":"2012-10-17","Statement":[{"Action":["s3:GetObject","s3:ListBucket"],"Effect":"Allow","Resource":["arn:aws:s3:::'"$bucket_name"'/*","arn:aws:s3:::'"$bucket_name"'"],"Principal":{"Service":"cloudformation.amazonaws.com"}}]}' 101 | echo "Configuring S3 Bucket Policy for temporary S3 Bucket" $bucket_name complete. 102 | 103 | 104 | echo Copying Schema Handler Package to temporary S3 Bucket $bucket_name... 105 | aws s3 cp awsutility-cloudformation-commandrunner.zip s3://$bucket_name/ >> registration_logs.log 106 | echo Copying Schema Handler Package to temporary S3 Bucket $bucket_name complete. 107 | 108 | #CFN Registration 109 | 110 | echo Creating CommandRunner Log Group called awsutility-cloudformation-commandrunner-logs2... 111 | log_group=`aws logs create-log-group --log-group-name awsutility-cloudformation-commandrunner-logs2 2>> registration_logs.log` 112 | if ! [ $? -eq 0 ]; then 113 | echo "Command Runner Log Group already exists, no changes to be made." 114 | echo "Creating CommandRunner Log Group skipped." 115 | else 116 | echo Creating CommandRunner Log Group complete. 117 | fi 118 | 119 | registration_token=`aws cloudformation register-type --type RESOURCE --type-name AWSUtility::CloudFormation::CommandRunner --schema-handler-package s3://$bucket_name/awsutility-cloudformation-commandrunner.zip --query RegistrationToken --output text --execution-role-arn $execution_role_arn --logging-config LogRoleArn=$execution_role_arn,LogGroupName=awsutility-cloudformation-commandrunner-logs2` 120 | echo "Registering AWSUtility::CloudFormation::CommandRunner to AWS CloudFormation..." 121 | echo "RegistrationToken:" $registration_token 122 | 123 | progress_status="IN_PROGRESS" 124 | while [[ $progress_status == "IN_PROGRESS" ]] 125 | do 126 | echo "Waiting for registration to complete..." 127 | sleep 15 128 | progress_status=`aws cloudformation describe-type-registration --registration-token $registration_token --query ProgressStatus --output text` 129 | if [[ $progress_status == "COMPLETE" ]]; then 130 | echo "Registering AWSUtility::CloudFormation::CommandRunner to AWS CloudFormation complete." 131 | if [ $set_default -eq 1 ]; then 132 | echo "Setting current version as default..." 133 | build_version_number=`aws cloudformation describe-type-registration --registration-token $registration_token --query TypeVersionArn --output text | cut -d "/" -f4` 134 | aws cloudformation set-type-default-version --type RESOURCE --type-name AWSUtility::CloudFormation::CommandRunner --version-id $build_version_number 135 | echo "Setting current version as default complete." "(Current Version is" $build_version_number")" 136 | fi 137 | 138 | fi 139 | if [[ $progress_status == "FAILED" ]]; then 140 | echo "Type registration failed." 141 | aws cloudformation describe-type-registration --registration-token $registration_token 142 | fi 143 | done 144 | 145 | # Clean up temporary S3 Bucket 146 | echo "Cleaning up temporary S3 Bucket..." 147 | echo "Deleting SchemaHandlerPackage from temporary S3 Bucket $bucket_name..." 148 | rm_result=`aws s3 rm s3://$bucket_name/awsutility-cloudformation-commandrunner.zip 2>&1` 149 | if [ $? -eq 0 ]; then 150 | echo "Deleting SchemaHandlerPackage from temporary S3 Bucket $bucket_name complete." 151 | else 152 | echo Deleting SchemaHandlerPackage from temporary S3 Bucket $bucket_name failed, please delete it manually. 153 | echo $rm_result >> registration_logs.log 154 | echo $rm_result 155 | exit 1 156 | fi 157 | rb_result=`aws s3 rb s3://$bucket_name 2>&1` 158 | if [ $? -eq 0 ]; then 159 | echo Cleaning up temporary S3 Bucket complete. 160 | else 161 | echo Cleaning up temporary S3 Bucket $bucket_name failed, please delete it manually. 162 | echo $rb_result >> registration_logs.log 163 | echo $rb_result 164 | exit 1 165 | fi 166 | 167 | echo "" 168 | echo "AWSUtility::CloudFormation::CommandRunner is ready to use." 169 | echo "" 170 | 171 | exit 0 172 | -------------------------------------------------------------------------------- /src/main/java/software/awsutility/cloudformation/commandrunner/CallbackContext.java: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package software.awsutility.cloudformation.commandrunner; 4 | 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | @Data 11 | @NoArgsConstructor 12 | @Builder(toBuilder = true) 13 | @AllArgsConstructor 14 | public class CallbackContext { 15 | private String stackName; 16 | private Integer stabilizationRetriesRemaining; 17 | private String stackId; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/software/awsutility/cloudformation/commandrunner/Configuration.java: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package software.awsutility.cloudformation.commandrunner; 4 | 5 | import java.util.Map; 6 | import org.json.JSONObject; 7 | import org.json.JSONTokener; 8 | 9 | class Configuration extends BaseConfiguration { 10 | 11 | public Configuration() { 12 | super("awsutility-cloudformation-commandrunner.json"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/software/awsutility/cloudformation/commandrunner/CreateHandler.java: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package software.awsutility.cloudformation.commandrunner; 4 | 5 | import com.amazonaws.AmazonClientException; 6 | import com.amazonaws.AmazonServiceException; 7 | import com.amazonaws.services.cloudformation.AmazonCloudFormation; 8 | import com.amazonaws.services.cloudformation.AmazonCloudFormationClientBuilder; 9 | import com.amazonaws.services.cloudformation.model.*; 10 | import com.amazonaws.services.ec2.*; 11 | import com.amazonaws.services.ec2.model.*; 12 | import com.amazonaws.services.ec2.AmazonEC2; 13 | import com.amazonaws.services.ec2.AmazonEC2ClientBuilder; 14 | import com.amazonaws.services.identitymanagement.AmazonIdentityManagement; 15 | import com.amazonaws.services.identitymanagement.AmazonIdentityManagementClientBuilder; 16 | import com.amazonaws.services.identitymanagement.model.SimulatePrincipalPolicyRequest; 17 | import com.amazonaws.services.identitymanagement.model.SimulatePrincipalPolicyResult; 18 | import com.amazonaws.services.identitymanagement.model.GetInstanceProfileRequest; 19 | import com.amazonaws.services.identitymanagement.model.GetInstanceProfileResult; 20 | import com.amazonaws.services.s3.AmazonS3ClientBuilder; 21 | import com.amazonaws.services.securitytoken.AWSSecurityTokenService; 22 | import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder; 23 | import com.amazonaws.services.securitytoken.model.GetCallerIdentityRequest; 24 | import com.amazonaws.services.securitytoken.model.GetCallerIdentityResult; 25 | import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagement; 26 | import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagementClientBuilder; 27 | import com.amazonaws.services.simplesystemsmanagement.model.GetParameterRequest; 28 | import com.amazonaws.services.simplesystemsmanagement.model.GetParameterResult; 29 | import com.amazonaws.services.simplesystemsmanagement.model.PutParameterRequest; 30 | import com.amazonaws.services.simplesystemsmanagement.model.PutParameterResult; 31 | import org.apache.commons.lang3.StringUtils; 32 | import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; 33 | import software.amazon.cloudformation.proxy.HandlerErrorCode; 34 | import software.amazon.cloudformation.proxy.Logger; 35 | import software.amazon.cloudformation.proxy.ProgressEvent; 36 | import software.amazon.cloudformation.proxy.OperationStatus; 37 | import software.amazon.cloudformation.proxy.ResourceHandlerRequest; 38 | import com.amazonaws.services.s3.AmazonS3; 39 | import software.awsutility.cloudformation.commandrunner.CallbackContext; 40 | 41 | import java.io.IOException; 42 | import java.io.InputStream; 43 | import java.io.InputStreamReader; 44 | 45 | import java.util.Collection; 46 | import java.util.LinkedList; 47 | import java.util.List; 48 | import java.util.Random; 49 | import java.io.BufferedReader; 50 | 51 | public class CreateHandler extends BaseHandler { 52 | 53 | 54 | @Override 55 | public ProgressEvent handleRequest( 56 | final AmazonWebServicesClientProxy proxy, 57 | final ResourceHandlerRequest request, 58 | final CallbackContext callbackContext, 59 | final Logger logger) { 60 | 61 | final ResourceModel model = request.getDesiredResourceState(); 62 | 63 | /* 64 | INFO: 'model' has all the properties from the CFN resource i.e Triggers, Command, Role, and LogGroup 65 | INFO: 'request' has information like region and AWS account ID 66 | */ 67 | 68 | if (callbackContext == null) { 69 | 70 | AmazonCloudFormation stackbuilder = AmazonCloudFormationClientBuilder.standard() 71 | .build(); 72 | 73 | Random random = new Random(); 74 | String generatedString = random.ints(97, 122 + 1) 75 | .limit(10) 76 | .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) 77 | .toString(); 78 | String stackName = "AWSUtility-CloudFormation-CommandRunner-"+generatedString; 79 | 80 | try { 81 | 82 | //Check if TerminateInstances is allowed 83 | if (model.getDisableTerminateInstancesCheck() != null && model.getDisableTerminateInstancesCheck().toLowerCase() != "true") { 84 | System.out.println("Performing TerminateInstances Check"); 85 | AWSSecurityTokenService stsClient = AWSSecurityTokenServiceClientBuilder.standard().build(); 86 | GetCallerIdentityRequest getCallerIdentityRequest = new GetCallerIdentityRequest(); 87 | GetCallerIdentityResult getCallerIdentityResult = proxy.injectCredentialsAndInvoke(getCallerIdentityRequest, stsClient::getCallerIdentity); 88 | String roleArn = getCallerIdentityResult.getArn(); 89 | if (roleArn.contains("sts")) { 90 | roleArn = roleArn.replace("sts", "iam"); 91 | } 92 | if (roleArn.contains("assumed-role")) { 93 | roleArn = roleArn.replace("assumed-role", "role"); 94 | } 95 | if (StringUtils.countMatches(roleArn,"/") == 2) { 96 | for (int i = roleArn.lastIndexOf("/"); i < roleArn.length(); i++) 97 | roleArn = roleArn.replace(roleArn.substring(roleArn.lastIndexOf("/"), roleArn.length()), ""); 98 | } 99 | 100 | AmazonIdentityManagement iamClient = AmazonIdentityManagementClientBuilder.standard().build(); 101 | SimulatePrincipalPolicyRequest simulatePrincipalPolicyRequest = new SimulatePrincipalPolicyRequest(); 102 | Collection actions = new LinkedList<>(); 103 | actions.add("ec2:TerminateInstances"); 104 | simulatePrincipalPolicyRequest.setActionNames(actions); 105 | simulatePrincipalPolicyRequest.setPolicySourceArn(roleArn); 106 | 107 | SimulatePrincipalPolicyResult simulatePrincipalPolicyResult = proxy.injectCredentialsAndInvoke(simulatePrincipalPolicyRequest, iamClient::simulatePrincipalPolicy); 108 | if ( simulatePrincipalPolicyResult.getEvaluationResults().get(0).getEvalDecision().contains("Deny") || simulatePrincipalPolicyResult.getEvaluationResults().get(0).getEvalDecision().equals("ExplicitDeny") || simulatePrincipalPolicyResult.getEvaluationResults().get(0).getEvalDecision().equals("ImplicitDeny") ) { 109 | return ProgressEvent.builder() 110 | .status(OperationStatus.FAILED) 111 | .errorCode(HandlerErrorCode.InvalidRequest) 112 | .message("You do not have permissions to make the TerminateInstances API call. Please try again with the necessary permissions.") 113 | .build(); 114 | } 115 | } 116 | 117 | 118 | 119 | InputStream in = CreateHandler.class.getResourceAsStream("/BaseTemplate.json"); 120 | BufferedReader reader = new BufferedReader(new InputStreamReader(in)); 121 | StringBuilder out = new StringBuilder(); 122 | String line; 123 | while ((line = reader.readLine()) != null) { 124 | out.append(line); 125 | } 126 | 127 | CreateStackRequest createRequest = new CreateStackRequest(); 128 | createRequest.setStackName(stackName); 129 | createRequest.setTemplateBody(out.toString()); 130 | reader.close(); 131 | System.out.println("Creating a stack called " + createRequest.getStackName() + "."); 132 | Collection parameters = new LinkedList<>(); 133 | 134 | //Timeout Parameter 135 | Parameter Timeout = new Parameter(); 136 | Timeout.setParameterKey("Timeout"); 137 | if(model.getTimeout() != null) { 138 | Timeout.setParameterValue(model.getTimeout()); 139 | parameters.add(Timeout); 140 | } 141 | 142 | //InstanceType Parameter 143 | Parameter InstanceType = new Parameter(); 144 | InstanceType.setParameterKey("InstanceType"); 145 | if(model.getInstanceType() != null) { 146 | InstanceType.setParameterValue(model.getInstanceType()); 147 | parameters.add(InstanceType); 148 | } 149 | 150 | Parameter AMIId = new Parameter(); 151 | AMIId.setParameterKey("AMIId"); 152 | 153 | //Dynamically get latest Amazon Linux 2 AMI for the region 154 | AWSSimpleSystemsManagement simpleSystemsManagementClient = ((AWSSimpleSystemsManagementClientBuilder.standard())).build(); 155 | 156 | GetParameterRequest parameterRequest = new GetParameterRequest(); 157 | parameterRequest.withName("/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2").setWithDecryption(Boolean.valueOf(true)); 158 | GetParameterResult parameterResult = proxy.injectCredentialsAndInvoke(parameterRequest, simpleSystemsManagementClient::getParameter); 159 | String parameterValue = parameterResult.getParameter().getValue(); 160 | 161 | AMIId.setParameterValue(parameterValue); 162 | parameters.add(AMIId); 163 | 164 | Parameter Command = new Parameter(); 165 | Command.setParameterKey("Command"); 166 | Command.setParameterValue(model.getCommand()); 167 | parameters.add(Command); 168 | 169 | if (model.getRole() != null) { 170 | 171 | //Check if Instance Profile exists 172 | try { 173 | AmazonIdentityManagement iamClient2 = AmazonIdentityManagementClientBuilder.standard().build(); 174 | GetInstanceProfileRequest instanceProfileRequest = new GetInstanceProfileRequest(); 175 | instanceProfileRequest.withInstanceProfileName(model.getRole()); 176 | GetInstanceProfileResult instanceProfileResult = proxy.injectCredentialsAndInvoke(instanceProfileRequest, iamClient2::getInstanceProfile); 177 | } catch (Exception e) { 178 | System.out.println("The Role property specified is not a valid Instance Profile."); 179 | return ProgressEvent.builder() 180 | .status(OperationStatus.FAILED) 181 | .errorCode(HandlerErrorCode.InvalidRequest) 182 | .message("The Role property specified is not a valid Instance Profile.") 183 | .build(); 184 | } 185 | 186 | Parameter IamInstanceProfile = new Parameter(); 187 | IamInstanceProfile.setParameterKey("IamInstanceProfile"); 188 | IamInstanceProfile.setParameterValue(model.getRole()); 189 | parameters.add(IamInstanceProfile); 190 | } 191 | 192 | // Parameter InstanceType = new Parameter(); 193 | // InstanceType.setParameterKey("InstanceType"); 194 | // //Note: HardCoded for now, will change in the future if the resource allows the customer to specify instance type. 195 | // InstanceType.setParameterValue(INSTANCE_TYPE); 196 | // parameters.add(InstanceType); 197 | 198 | if (model.getLogGroup() != null) { 199 | Parameter LogGroup = new Parameter(); 200 | LogGroup.setParameterKey("LogGroup"); 201 | LogGroup.setParameterValue(model.getLogGroup()); 202 | parameters.add(LogGroup); 203 | } 204 | 205 | //Dynamically gets both vpcId and subnetId 206 | System.out.println(model.toString()); 207 | if ((model.getSubnetId() == null && model.getSecurityGroupId() == null)) { //Check if user provided the subnetId, if not get a default. 208 | System.out.println("Inside dynamic creation workflow!"); 209 | Parameter SubnetId = new Parameter(); 210 | SubnetId.setParameterKey("SubnetId"); 211 | 212 | Parameter VpcId = new Parameter(); 213 | VpcId.setParameterKey("VpcId"); 214 | 215 | AmazonEC2 ec2 = AmazonEC2ClientBuilder.standard().build(); 216 | DescribeVpcsRequest describeVpcsRequest = new DescribeVpcsRequest(); 217 | describeVpcsRequest.withFilters(new Filter("isDefault").withValues("true")); 218 | DescribeVpcsResult describeVpcsResult = proxy.injectCredentialsAndInvoke(describeVpcsRequest, ec2::describeVpcs); 219 | // Issue #14, fix for error Index: 0, Size: 0 when trying to .get(0) when it doesn't exist. 220 | String vpcId; 221 | if ( describeVpcsResult.getVpcs().isEmpty()) { 222 | System.out.println("No default VPC found in this region, please specify a subnet using the SubnetId property."); 223 | return ProgressEvent.builder() 224 | .status(OperationStatus.FAILED) 225 | .errorCode(HandlerErrorCode.InvalidRequest) 226 | .message("No default VPC found in this region, please specify a subnet using the SubnetId property.") 227 | .build(); 228 | } 229 | try { 230 | vpcId = describeVpcsResult.getVpcs().get(0).getVpcId(); 231 | } catch (Exception e) { 232 | System.out.println("No default VPC found in this region, please specify a subnet using the SubnetId property."); 233 | return ProgressEvent.builder() 234 | .status(OperationStatus.FAILED) 235 | .errorCode(HandlerErrorCode.InvalidRequest) 236 | .message("No default VPC found in this region, please specify a subnet using the SubnetId property.") 237 | .build(); 238 | } 239 | 240 | VpcId.setParameterValue(vpcId); 241 | DescribeSubnetsRequest describeSubnetsRequest = new DescribeSubnetsRequest(); 242 | describeSubnetsRequest.withFilters(new Filter("vpc-id").withValues(vpcId)); 243 | DescribeSubnetsResult describeSubnetsResult = proxy.injectCredentialsAndInvoke(describeSubnetsRequest, ec2::describeSubnets); 244 | String subnetId = describeSubnetsResult.getSubnets().get(describeSubnetsResult.getSubnets().size()-1).getSubnetId(); 245 | if (subnetId == null || subnetId.isEmpty()) { 246 | System.out.println("Default VPC has no subnets. Please specify a subnet using the NetworkConfiguration property"); 247 | return ProgressEvent.builder() 248 | .status(OperationStatus.FAILED) 249 | .errorCode(HandlerErrorCode.InvalidRequest) 250 | .message("Default VPC has no subnets. Please specify a subnet using the NetworkConfiguration property.") 251 | .build(); 252 | } 253 | SubnetId.setParameterValue(subnetId); 254 | System.out.println("SubnetId=" + SubnetId); 255 | parameters.add(SubnetId); 256 | parameters.add(VpcId); 257 | } 258 | //Both are provided 259 | else if ((model.getSubnetId() != null && model.getSecurityGroupId() != null)) { 260 | System.out.println("INSIDE BOTH ARE PROVIDED WORKFLOW."); 261 | Parameter SubnetId = new Parameter(); 262 | SubnetId.setParameterKey("SubnetId"); 263 | SubnetId.setParameterValue(model.getSubnetId()); 264 | parameters.add(SubnetId); 265 | 266 | Parameter SecurityGroupId = new Parameter(); 267 | SecurityGroupId.setParameterKey("SecurityGroupId"); 268 | //Note: HardCoded for now, will have to change in the future. 269 | SecurityGroupId.setParameterValue(model.getSecurityGroupId()); 270 | parameters.add(SecurityGroupId); 271 | } 272 | //Subnet is provided, but not SecurityGroup. Infer VPC from Subnet and provide VPCId to CFN Stack 273 | else if ((model.getSubnetId() != null && model.getSecurityGroupId() == null)) { 274 | System.out.println("INSIDE SUBNET PROVIDED NO SECURITY GROUP WORKFLOW."); 275 | Parameter SubnetId = new Parameter(); 276 | SubnetId.setParameterKey("SubnetId"); 277 | SubnetId.setParameterValue(model.getSubnetId()); 278 | parameters.add(SubnetId); 279 | 280 | Parameter VpcId = new Parameter(); 281 | VpcId.setParameterKey("VpcId"); 282 | 283 | AmazonEC2 ec2 = AmazonEC2ClientBuilder.standard().build(); 284 | DescribeSubnetsRequest describeSubnetsRequest = new DescribeSubnetsRequest(); 285 | describeSubnetsRequest.withFilters(new Filter("subnet-id").withValues(model.getSubnetId())); 286 | DescribeSubnetsResult describeSubnetsResult = proxy.injectCredentialsAndInvoke(describeSubnetsRequest, ec2::describeSubnets); 287 | String vpcId = describeSubnetsResult.getSubnets().get(0).getVpcId(); 288 | VpcId.setParameterValue(vpcId); 289 | parameters.add(VpcId); 290 | 291 | } 292 | 293 | else if ((model.getSubnetId() == null && model.getSecurityGroupId() != null)) { 294 | System.out.println("No SubnetId provided, when using SecurityGroupId, property SubnetId is required."); 295 | return ProgressEvent.builder() 296 | .status(OperationStatus.FAILED) 297 | .errorCode(HandlerErrorCode.InvalidRequest) 298 | .message("No SubnetId provided, when using SecurityGroupId, property SubnetId is required.") 299 | .build(); 300 | } 301 | 302 | createRequest.setParameters(parameters); 303 | System.out.println(createRequest.getParameters().toString()); 304 | //Inject creds and call instead 305 | proxy.injectCredentialsAndInvoke(createRequest, stackbuilder::createStack); 306 | 307 | //If CallbackContext coming in is null, always create stack and set OperationStatus.IN_PROGRESS 308 | model.setId(generatedString); 309 | return ProgressEvent.builder() 310 | .resourceModel(model) 311 | .callbackContext(CallbackContext.builder().stackName(stackName).stackId(generatedString).build()) 312 | .status(OperationStatus.IN_PROGRESS) 313 | .callbackDelaySeconds(90) 314 | .build(); 315 | 316 | } catch (AmazonServiceException ase) { 317 | System.out.println("Caught an AmazonServiceException, which means your request made it " 318 | + "to AWS CloudFormation, but was rejected with an error response for some reason."); 319 | System.out.println("Error Message: " + ase.getMessage()); 320 | System.out.println("HTTP Status Code: " + ase.getStatusCode()); 321 | System.out.println("AWS Error Code: " + ase.getErrorCode()); 322 | System.out.println("Error Type: " + ase.getErrorType()); 323 | System.out.println("Request ID: " + ase.getRequestId()); 324 | return ProgressEvent.builder() 325 | .status(OperationStatus.FAILED) 326 | .errorCode(HandlerErrorCode.InternalFailure) 327 | .message(ase.getMessage() + " " + ase.getStatusCode() + " " + ase.getErrorCode() + " " + ase.getErrorType() + " " + ase.getRequestId()) 328 | .build(); 329 | 330 | } catch (AmazonClientException ace) { 331 | System.out.println("Caught an AmazonClientException, which means the client encountered " 332 | + "a serious internal problem while trying to communicate with AWS CloudFormation, " 333 | + "such as not being able to access the network."); 334 | System.out.println("Error Message: " + ace.getMessage()); 335 | return ProgressEvent.builder() 336 | .status(OperationStatus.FAILED) 337 | .errorCode(HandlerErrorCode.InternalFailure) 338 | .message(ace.getMessage()) 339 | .build(); 340 | } catch (IOException e) { 341 | e.printStackTrace(); 342 | } 343 | 344 | } else { 345 | 346 | AmazonCloudFormation stackbuilder = AmazonCloudFormationClientBuilder.standard() 347 | .build(); 348 | 349 | //From context check the status of the stack by looking up the stackName property. 350 | String stackName = callbackContext.getStackName(); 351 | DescribeStacksRequest wait = new DescribeStacksRequest(); 352 | wait.setStackName(stackName); 353 | String stackStatus = "Unknown"; 354 | String stackReason = ""; 355 | List stacks = proxy.injectCredentialsAndInvoke(wait, stackbuilder::describeStacks).getStacks(); 356 | if ( 357 | stacks.get(0).getStackStatus().equals(StackStatus.CREATE_COMPLETE.toString()) || 358 | stacks.get(0).getStackStatus().equals(StackStatus.CREATE_FAILED.toString()) || 359 | stacks.get(0).getStackStatus().equals(StackStatus.ROLLBACK_FAILED.toString()) || 360 | stacks.get(0).getStackStatus().equals(StackStatus.ROLLBACK_COMPLETE.toString()) || 361 | stacks.get(0).getStackStatus().equals(StackStatus.DELETE_FAILED.toString()) 362 | ) { 363 | stackStatus = stacks.get(0).getStackStatus(); 364 | stackReason = stacks.get(0).getStackStatusReason(); 365 | String returnString = stackStatus + " (" + stackReason + ")"; 366 | System.out.println(returnString); 367 | 368 | if(stacks.get(0).getStackStatus().equals(StackStatus.CREATE_COMPLETE.toString())) { 369 | model.setId(callbackContext.getStackId()); 370 | model.setOutput(stacks.get(0).getOutputs().get(0).getOutputValue()); 371 | //DELETE Stack and terminate EC2 instance. 372 | AmazonCloudFormation stackBuilder = AmazonCloudFormationClientBuilder.standard() 373 | .build(); 374 | DeleteStackRequest deleteRequest = new DeleteStackRequest(); 375 | deleteRequest.setStackName(stackName); 376 | proxy.injectCredentialsAndInvoke(deleteRequest, stackBuilder::deleteStack); 377 | 378 | //Make new SSM Parameter with Key=Id and Value=Output 379 | AWSSimpleSystemsManagement simpleSystemsManagementClient = ((AWSSimpleSystemsManagementClientBuilder.standard())).build(); 380 | PutParameterRequest parameterRequest = new PutParameterRequest(); 381 | parameterRequest.setName(callbackContext.getStackId()); 382 | parameterRequest.setValue(stacks.get(0).getOutputs().get(0).getOutputValue()); 383 | parameterRequest.setType("SecureString"); 384 | if (model.getKeyId() != null || model.getKeyId() != "") { 385 | parameterRequest.setKeyId(model.getKeyId()); 386 | } 387 | //Catch for if SSM PutParameter failed due to invalid value in /command-output.txt 388 | try { 389 | PutParameterResult parameterResult = proxy.injectCredentialsAndInvoke(parameterRequest, simpleSystemsManagementClient::putParameter); 390 | } catch (Exception e) { 391 | return ProgressEvent.builder() 392 | .status(OperationStatus.FAILED) 393 | .errorCode(HandlerErrorCode.NotStabilized) 394 | .message("Either the command failed to execute, the value written to /command-output.txt was invalid or the Subnet specified did not have internet access. The value written to /command-output.txt must be a non-empty single word value without quotation marks. Check cloud-init.log in the LogGroup specified for more information.") 395 | .build(); 396 | } 397 | 398 | return ProgressEvent.builder() 399 | .resourceModel(model) 400 | .status(OperationStatus.SUCCESS) 401 | .build(); 402 | } 403 | if (stacks.get(0).getStackStatus().equals(StackStatus.CREATE_FAILED.toString()) || 404 | stacks.get(0).getStackStatus().equals(StackStatus.ROLLBACK_COMPLETE.toString())){ 405 | //DELETE Stack and terminate EC2 instance. 406 | AmazonCloudFormation stackBuilder = AmazonCloudFormationClientBuilder.standard() 407 | .build(); 408 | DeleteStackRequest deleteRequest = new DeleteStackRequest(); 409 | deleteRequest.setStackName(stackName); 410 | proxy.injectCredentialsAndInvoke(deleteRequest, stackBuilder::deleteStack); 411 | return ProgressEvent.builder() 412 | .status(OperationStatus.FAILED) 413 | .errorCode(HandlerErrorCode.NotStabilized) 414 | .message("Either the command failed to execute, the value written to /command-output.txt was invalid or the Subnet specified did not have internet access. The value written to /command-output.txt must be a non-empty single word value without quotation marks. Check cloud-init.log in the LogGroup specified for more information.") 415 | .build(); 416 | } 417 | 418 | } else { 419 | return ProgressEvent.builder() 420 | .resourceModel(model) 421 | .status(OperationStatus.IN_PROGRESS) 422 | .callbackContext(CallbackContext.builder().stackName(stackName).stackId(callbackContext.getStackId()).build()) 423 | .callbackDelaySeconds(30) 424 | .build(); 425 | } 426 | 427 | } 428 | 429 | //It should never reach this code, if it does something went wrong, so it returns internal failure. 430 | //Reaches here if callbackContext is not null and it's not of the stack statuses above. 431 | return ProgressEvent.builder() 432 | .status(OperationStatus.FAILED) 433 | .errorCode(HandlerErrorCode.InternalFailure) 434 | .message("Internal Failure") 435 | .build(); 436 | } 437 | } 438 | -------------------------------------------------------------------------------- /src/main/java/software/awsutility/cloudformation/commandrunner/DeleteHandler.java: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package software.awsutility.cloudformation.commandrunner; 4 | 5 | import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagement; 6 | import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagementClientBuilder; 7 | import com.amazonaws.services.simplesystemsmanagement.model.DeleteParameterRequest; 8 | import com.amazonaws.services.simplesystemsmanagement.model.DeleteParameterResult; 9 | import com.amazonaws.services.simplesystemsmanagement.model.GetParameterRequest; 10 | import com.amazonaws.services.simplesystemsmanagement.model.GetParameterResult; 11 | import software.amazon.cloudformation.proxy.*; 12 | import software.awsutility.cloudformation.commandrunner.CallbackContext; 13 | 14 | public class DeleteHandler extends BaseHandler { 15 | 16 | @Override 17 | public ProgressEvent handleRequest( 18 | final AmazonWebServicesClientProxy proxy, 19 | final ResourceHandlerRequest request, 20 | final CallbackContext callbackContext, 21 | final Logger logger) { 22 | 23 | final ResourceModel model = request.getDesiredResourceState(); 24 | AWSSimpleSystemsManagement simpleSystemsManagementClient = ((AWSSimpleSystemsManagementClientBuilder.standard())).build(); 25 | DeleteParameterRequest parameterRequest = new DeleteParameterRequest(); 26 | parameterRequest.setName(model.getId()); 27 | try { 28 | 29 | DeleteParameterResult parameterResult = proxy.injectCredentialsAndInvoke(parameterRequest, simpleSystemsManagementClient::deleteParameter); 30 | return ProgressEvent.builder() 31 | .status(OperationStatus.SUCCESS) 32 | .resourceModel(model) 33 | .build(); 34 | } catch (Exception e) { 35 | return ProgressEvent.builder() 36 | .errorCode(HandlerErrorCode.NotFound) 37 | .status(OperationStatus.FAILED) 38 | .build(); 39 | } 40 | 41 | 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/software/awsutility/cloudformation/commandrunner/ReadHandler.java: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package software.awsutility.cloudformation.commandrunner; 4 | 5 | import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagement; 6 | import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagementClientBuilder; 7 | import com.amazonaws.services.simplesystemsmanagement.model.GetParameterRequest; 8 | import com.amazonaws.services.simplesystemsmanagement.model.GetParameterResult; 9 | import com.amazonaws.services.simplesystemsmanagement.model.PutParameterRequest; 10 | import com.amazonaws.services.simplesystemsmanagement.model.PutParameterResult; 11 | import software.amazon.cloudformation.proxy.*; 12 | import software.awsutility.cloudformation.commandrunner.CallbackContext; 13 | 14 | public class ReadHandler extends BaseHandler { 15 | 16 | @Override 17 | public ProgressEvent handleRequest( 18 | final AmazonWebServicesClientProxy proxy, 19 | final ResourceHandlerRequest request, 20 | final CallbackContext callbackContext, 21 | final Logger logger) { 22 | 23 | final ResourceModel model = request.getDesiredResourceState(); 24 | AWSSimpleSystemsManagement simpleSystemsManagementClient = ((AWSSimpleSystemsManagementClientBuilder.standard())).build(); 25 | GetParameterRequest parameterRequest = new GetParameterRequest(); 26 | parameterRequest.setName(model.getId()); 27 | parameterRequest.setWithDecryption(true); 28 | try { 29 | 30 | GetParameterResult parameterResult = proxy.injectCredentialsAndInvoke(parameterRequest, simpleSystemsManagementClient::getParameter); 31 | model.setOutput(parameterResult.getParameter().getValue()); 32 | return ProgressEvent.builder() 33 | .resourceModel(model) 34 | .status(OperationStatus.SUCCESS) 35 | .message(model.getPrimaryIdentifier().toString()) 36 | .build(); 37 | } catch (Exception e) { 38 | return ProgressEvent.builder() 39 | .errorCode(HandlerErrorCode.NotFound) 40 | .status(OperationStatus.FAILED) 41 | .build(); 42 | } 43 | 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/resources/BaseTemplate.json: -------------------------------------------------------------------------------- 1 | { 2 | "Parameters": { 3 | "SubnetId": { 4 | "Type": "AWS::EC2::Subnet::Id" 5 | }, 6 | "Timeout": { 7 | "Type": "String", 8 | "Default": "600" 9 | }, 10 | "AMIId": { 11 | "Type": "String", 12 | "Default": "ami-062f7200baf2fa504" 13 | }, 14 | "InstanceType": { 15 | "Type": "String", 16 | "Default": "t2.medium" 17 | }, 18 | "IamInstanceProfile": { 19 | "Type": "String", 20 | "Default": "empty" 21 | }, 22 | "SecurityGroupId": { 23 | "Type": "String", 24 | "Default": "empty" 25 | }, 26 | "VpcId": { 27 | "Type": "String", 28 | "Default": "empty" 29 | }, 30 | "Command": { 31 | "Type": "String", 32 | "Default": "yum install jq -y && aws ssm get-parameter --name RepositoryName --region us-east-1 | jq -r .Parameter.Value > /commandrunner-output.txt" 33 | }, 34 | "LogGroup": { 35 | "Type": "String", 36 | "Default": "cloudformation-commandrunner-log-group" 37 | } 38 | }, 39 | "Conditions": { 40 | "CreateSecurityGroup": { 41 | "Fn::Equals": [ 42 | { 43 | "Ref": "SecurityGroupId" 44 | }, 45 | "empty" 46 | ] 47 | }, 48 | "UseInstanceProfile": { 49 | "Fn::Not": [ 50 | { 51 | "Fn::Equals": [ 52 | { 53 | "Ref": "IamInstanceProfile" 54 | }, 55 | "empty" 56 | ] 57 | } 58 | ] 59 | } 60 | }, 61 | "Resources": { 62 | "SecurityGroup": { 63 | "Condition": "CreateSecurityGroup", 64 | "Type": "AWS::EC2::SecurityGroup", 65 | "Properties": { 66 | "GroupName": { 67 | "Fn::Sub": "aws-cloudformation-commandrunner-temp-sg-${AWS::StackName}}" 68 | }, 69 | "GroupDescription": "A temporary security group for AWS::CloudFormation::Command", 70 | "SecurityGroupEgress": [ 71 | { 72 | "CidrIp": "0.0.0.0/0", 73 | "FromPort": -1, 74 | "IpProtocol": -1, 75 | "ToPort": -1 76 | } 77 | ], 78 | "VpcId": { 79 | "Ref": "VpcId" 80 | } 81 | } 82 | }, 83 | "EC2Instance": { 84 | "Type": "AWS::EC2::Instance", 85 | "Metadata": { 86 | "AWS::CloudFormation::Init": { 87 | "config": { 88 | "packages": { 89 | "yum": { 90 | "awslogs": [] 91 | } 92 | }, 93 | "files": { 94 | "/etc/awslogs/awslogs.conf": { 95 | "content": { 96 | "Fn::Sub": "[general]\nstate_file= /var/awslogs/state/agent-state\n[/var/log/cloud-init.log]\nfile = /var/log/cloud-init.log\\n\nlog_group_name = ${LogGroup}\nlog_stream_name = {instance_id}/cloud-init.log\n[/var/log/cloud-init-output.log]\nfile = /var/log/cloud-init-output.log\nlog_group_name = ${LogGroup}\nlog_stream_name = {instance_id}/cloud-init-output.log\n[/var/log/cfn-init.log]\nfile = /var/log/cfn-init.log\nlog_group_name = ${LogGroup}\nlog_stream_name = {instance_id}/cfn-init.log\n[/var/log/cfn-hup.log]\nfile = /var/log/cfn-hup.log\nlog_group_name = ${LogGroup}\nlog_stream_name = {instance_id}/cfn-hup.log\n[/var/log/cfn-wire.log]\nfile = /var/log/cfn-wire.log\nlog_group_name = ${LogGroup}\nlog_stream_name = {instance_id}/cfn-wire.log\n" 97 | }, 98 | "mode": "000444", 99 | "owner": "root", 100 | "group": "root" 101 | }, 102 | "/etc/awslogs/awscli.conf": { 103 | "content": { 104 | "Fn::Sub": "[plugins]\ncwlogs = cwlogs\n[default]\nregion = ${AWS::Region}\n" 105 | }, 106 | "mode": "000444", 107 | "owner": "root", 108 | "group": "root" 109 | } 110 | }, 111 | "commands": { 112 | "01_create_state_directory": { 113 | "command": "mkdir -p /var/awslogs/state" 114 | } 115 | }, 116 | "services": { 117 | "sysvinit": { 118 | "awslogsd": { 119 | "enabled": true, 120 | "ensureRunning": true, 121 | "files": [ 122 | "/etc/awslogs/awslogs.conf" 123 | ] 124 | } 125 | } 126 | } 127 | } 128 | } 129 | }, 130 | "Properties": { 131 | "UserData": { 132 | "Fn::Base64": { 133 | "Fn::Sub": "#!/bin/bash\nyum install -y aws-cfn-bootstrap\n/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource EC2Instance --region ${AWS::Region}\n${Command}\n/opt/aws/bin/cfn-signal -r 'Command ran successfully.' -e 0 --id 'Command Output' --data \"$(cat /command-output.txt)\" '${WaitConditionHandle}'\necho Contents of /command-output.txt = $(cat /command-output.txt)" 134 | } 135 | }, 136 | "InstanceType": { 137 | "Ref": "InstanceType" 138 | }, 139 | "SecurityGroupIds": [ 140 | { 141 | "Fn::If": [ 142 | "CreateSecurityGroup", 143 | { 144 | "Ref": "SecurityGroup" 145 | }, 146 | { 147 | "Ref": "SecurityGroupId" 148 | } 149 | ] 150 | } 151 | ], 152 | "ImageId": { 153 | "Ref": "AMIId" 154 | }, 155 | "SubnetId": { 156 | "Ref": "SubnetId" 157 | }, 158 | "IamInstanceProfile": { 159 | "Fn::If": [ 160 | "UseInstanceProfile", 161 | { 162 | "Ref": "IamInstanceProfile" 163 | }, 164 | { 165 | "Ref": "AWS::NoValue" 166 | } 167 | ] 168 | } 169 | } 170 | }, 171 | "WaitConditionHandle": { 172 | "Type": "AWS::CloudFormation::WaitConditionHandle" 173 | }, 174 | "WaitCondition": { 175 | "Type": "AWS::CloudFormation::WaitCondition", 176 | "Properties": { 177 | "Count": 1, 178 | "Handle": { 179 | "Ref": "WaitConditionHandle" 180 | }, 181 | "Timeout": { 182 | "Ref": "Timeout" 183 | } 184 | } 185 | } 186 | }, 187 | "Outputs": { 188 | "Result": { 189 | "Description": "The output of the commandrunner.", 190 | "Value": { 191 | "Fn::Select": [ 192 | 3, 193 | { 194 | "Fn::Split": [ 195 | "\"", 196 | { 197 | "Fn::GetAtt": [ 198 | "WaitCondition", 199 | "Data" 200 | ] 201 | } 202 | ] 203 | } 204 | ] 205 | } 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/test/java/software/awsutility/cloudformation/commandrunner/CreateHandlerTest.java: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package software.awsutility.cloudformation.commandrunner; 4 | 5 | import com.amazonaws.services.cloudformation.model.*; 6 | import com.amazonaws.services.ec2.model.*; 7 | import com.amazonaws.services.identitymanagement.model.EvaluationResult; 8 | import com.amazonaws.services.identitymanagement.model.SimulatePrincipalPolicyRequest; 9 | import com.amazonaws.services.identitymanagement.model.SimulatePrincipalPolicyResult; 10 | import com.amazonaws.services.securitytoken.model.GetCallerIdentityRequest; 11 | import com.amazonaws.services.securitytoken.model.GetCallerIdentityResult; 12 | import com.amazonaws.services.simplesystemsmanagement.model.*; 13 | import com.amazonaws.services.simplesystemsmanagement.model.Parameter; 14 | import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; 15 | import software.amazon.cloudformation.proxy.Logger; 16 | import software.amazon.cloudformation.proxy.ProgressEvent; 17 | import software.amazon.cloudformation.proxy.ResourceHandlerRequest; 18 | import org.junit.jupiter.api.BeforeEach; 19 | import org.junit.jupiter.api.Test; 20 | import org.junit.jupiter.api.extension.ExtendWith; 21 | import org.mockito.Mock; 22 | import org.mockito.junit.jupiter.MockitoExtension; 23 | import java.util.function.Function; 24 | 25 | 26 | import static org.mockito.ArgumentMatchers.any; 27 | import static org.mockito.Mockito.mock; 28 | import static org.mockito.Mockito.when; 29 | 30 | @ExtendWith(MockitoExtension.class) 31 | public class CreateHandlerTest { 32 | 33 | //Replace these with your own testing variables. 34 | private static final String COMMAND = "aws s3 cp s3://cfn-cli-project/S3BucketCheck.py . && python S3BucketCheck.py my-bucket cloudformation-bucket-fgbfgndddd"; 35 | private static final String LOG_GROUP = "my-cloudwatch-log-group"; 36 | private static final String ROLE = "my-example-role"; 37 | private static final String AWS_ACCOUNT_ID = "112233445566"; 38 | private static final String AWS_REGION = "us-east-1"; 39 | 40 | @Mock 41 | private AmazonWebServicesClientProxy proxy; 42 | 43 | @Mock 44 | private Logger logger; 45 | 46 | @BeforeEach 47 | @SuppressWarnings("unchecked") 48 | public void setup() { 49 | proxy = mock(AmazonWebServicesClientProxy.class); 50 | when(proxy.injectCredentialsAndInvoke(any(GetParameterRequest.class), any(Function.class))).thenReturn(new GetParameterResult().withParameter(new Parameter().withValue("ami-1234"))); 51 | 52 | when(proxy.injectCredentialsAndInvoke(any(DescribeVpcsRequest.class), any(Function.class))).thenReturn(new DescribeVpcsResult().withVpcs(new Vpc().withVpcId("vpc-1234").withIsDefault(Boolean.TRUE))); 53 | 54 | when(proxy.injectCredentialsAndInvoke(any(DescribeSubnetsRequest.class), any(Function.class))).thenReturn(new DescribeSubnetsResult().withSubnets(new Subnet().withSubnetId("subnet-1234"))); 55 | 56 | when(proxy.injectCredentialsAndInvoke(any(CreateStackRequest.class), any(Function.class))).thenReturn(new CreateStackResult()); 57 | 58 | when(proxy.injectCredentialsAndInvoke(any(DescribeStacksRequest.class), any(Function.class))).thenReturn(new DescribeStacksResult().withStacks(new Stack().withStackStatus(StackStatus.CREATE_COMPLETE).withStackStatusReason("Successful").withOutputs(new Output().withOutputValue("expected-value")))); 59 | 60 | when(proxy.injectCredentialsAndInvoke(any(DeleteStackRequest.class), any(Function.class))).thenReturn(new DeleteStackResult()); 61 | 62 | when(proxy.injectCredentialsAndInvoke(any(PutParameterRequest.class), any(Function.class))).thenReturn(new PutParameterResult()); 63 | 64 | when(proxy.injectCredentialsAndInvoke(any(GetCallerIdentityRequest.class), any(Function.class))).thenReturn(new GetCallerIdentityResult().withArn("arn:aws:sts::123456789012:assumed-role/my-role-name/my-role-session-name")); 65 | 66 | when(proxy.injectCredentialsAndInvoke(any(SimulatePrincipalPolicyRequest.class), any(Function.class))).thenReturn(new SimulatePrincipalPolicyResult().withEvaluationResults(new EvaluationResult().withEvalDecision("Allowed"))); 67 | 68 | logger = mock(Logger.class); 69 | } 70 | 71 | @Test 72 | public void handleRequest_SimpleSuccess() { 73 | final CreateHandler handler = new CreateHandler(); 74 | 75 | final ResourceModel model = ResourceModel.builder().command(COMMAND).logGroup(LOG_GROUP).role(ROLE).build(); 76 | 77 | final ResourceHandlerRequest request = ResourceHandlerRequest.builder() 78 | .desiredResourceState(model) 79 | .awsAccountId(AWS_ACCOUNT_ID) 80 | .region(AWS_REGION) 81 | .build(); 82 | try { 83 | final ProgressEvent response 84 | = handler.handleRequest(proxy, request, null, logger); 85 | //IN_PROGRESS ASSERTS 86 | assertThat(response).isNotNull(); 87 | assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); 88 | assertThat(response.getCallbackContext()).isNotNull(); 89 | assertThat(response.getCallbackDelaySeconds()).isEqualTo(90); 90 | assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); 91 | assertThat(response.getResourceModels()).isNull(); 92 | assertThat(response.getMessage()).isNull(); 93 | assertThat(response.getErrorCode()).isNull(); 94 | request.setDesiredResourceState(request.getDesiredResourceState()); 95 | System.out.println(response.getCallbackContext().getStackName()); 96 | final ProgressEvent nextResponse = handler.handleRequest(proxy, request, response.getCallbackContext(), logger); 97 | 98 | //CREATE_COMPLETE ASSERTS 99 | assertThat(nextResponse).isNotNull(); 100 | assertThat(nextResponse.getStatus()).isEqualTo(OperationStatus.SUCCESS); 101 | assertThat(nextResponse.getCallbackDelaySeconds()).isEqualTo(0); 102 | assertThat(nextResponse.getCallbackContext()).isNull(); 103 | assertThat(nextResponse.getResourceModel()).isEqualTo(request.getDesiredResourceState()); 104 | assertThat(nextResponse.getResourceModels()).isNull(); 105 | assertThat(nextResponse.getMessage()).isNull(); 106 | assertThat(nextResponse.getErrorCode()).isNull(); 107 | 108 | } catch (Exception e) { 109 | e.printStackTrace(); 110 | } 111 | 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/test/java/software/awsutility/cloudformation/commandrunner/DeleteHandlerTest.java: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package software.awsutility.cloudformation.commandrunner; 4 | 5 | import com.amazonaws.services.simplesystemsmanagement.model.*; 6 | import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; 7 | import software.amazon.cloudformation.proxy.Logger; 8 | import software.amazon.cloudformation.proxy.ProgressEvent; 9 | import software.amazon.cloudformation.proxy.ResourceHandlerRequest; 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Test; 12 | import org.junit.jupiter.api.extension.ExtendWith; 13 | import org.mockito.Mock; 14 | import org.mockito.junit.jupiter.MockitoExtension; 15 | import static org.mockito.ArgumentMatchers.any; 16 | import static org.mockito.Mockito.mock; 17 | import static org.mockito.Mockito.when; 18 | import java.util.function.Function; 19 | 20 | @ExtendWith(MockitoExtension.class) 21 | public class DeleteHandlerTest { 22 | 23 | @Mock 24 | private AmazonWebServicesClientProxy proxy; 25 | 26 | @Mock 27 | private Logger logger; 28 | 29 | @BeforeEach 30 | @SuppressWarnings("unchecked") 31 | public void setup() { 32 | proxy = mock(AmazonWebServicesClientProxy.class); 33 | when(proxy.injectCredentialsAndInvoke(any(DeleteParameterRequest.class), any(Function.class))).thenReturn(new DeleteParameterResult()); 34 | logger = mock(Logger.class); 35 | } 36 | 37 | @Test 38 | public void handleRequest_SimpleSuccess() { 39 | final DeleteHandler handler = new DeleteHandler(); 40 | 41 | final ResourceModel model = ResourceModel.builder().build(); 42 | 43 | final ResourceHandlerRequest request = ResourceHandlerRequest.builder() 44 | .desiredResourceState(model) 45 | .build(); 46 | 47 | final ProgressEvent response 48 | = handler.handleRequest(proxy, request, null, logger); 49 | 50 | assertThat(response).isNotNull(); 51 | assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); 52 | assertThat(response.getCallbackContext()).isNull(); 53 | assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); 54 | assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); 55 | assertThat(response.getResourceModels()).isNull(); 56 | assertThat(response.getMessage()).isNull(); 57 | assertThat(response.getErrorCode()).isNull(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/software/awsutility/cloudformation/commandrunner/ReadHandlerTest.java: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | package software.awsutility.cloudformation.commandrunner; 4 | 5 | import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagement; 6 | import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagementClientBuilder; 7 | import com.amazonaws.services.simplesystemsmanagement.model.GetParameterRequest; 8 | import com.amazonaws.services.simplesystemsmanagement.model.GetParameterResult; 9 | import com.amazonaws.services.simplesystemsmanagement.model.Parameter; 10 | import org.mockito.Mockito; 11 | import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; 12 | import software.amazon.cloudformation.proxy.Logger; 13 | import software.amazon.cloudformation.proxy.OperationStatus; 14 | import software.amazon.cloudformation.proxy.ProgressEvent; 15 | import software.amazon.cloudformation.proxy.ResourceHandlerRequest; 16 | import org.junit.jupiter.api.BeforeEach; 17 | import org.junit.jupiter.api.Test; 18 | import org.junit.jupiter.api.extension.ExtendWith; 19 | import org.mockito.Mock; 20 | import org.mockito.junit.jupiter.MockitoExtension; 21 | import software.awsutility.cloudformation.commandrunner.CallbackContext; 22 | import java.util.function.Function; 23 | 24 | import static org.assertj.core.api.Assertions.assertThat; 25 | import static org.mockito.ArgumentMatchers.any; 26 | import static org.mockito.ArgumentMatchers.*; 27 | import static org.mockito.Mockito.mock; 28 | import static org.mockito.Mockito.when; 29 | 30 | @ExtendWith(MockitoExtension.class) 31 | public class ReadHandlerTest { 32 | 33 | //Replace these with your own testing variables. 34 | private static final String AWS_ACCOUNT_ID = "112233445566"; 35 | private static final String AWS_REGION = "us-east-1"; 36 | private static final String PHYSICAL_ID = "rihgnyajuh"; 37 | private static final String EXPECTED_OUTPUT = "cloudformation-bucket-fgbfgndddd"; 38 | 39 | @Mock 40 | private AmazonWebServicesClientProxy proxy; 41 | 42 | @Mock 43 | private GetParameterResult parameterResult; 44 | 45 | @Mock 46 | private Logger logger; 47 | 48 | @BeforeEach 49 | @SuppressWarnings("unchecked") 50 | public void setup() { 51 | proxy = mock(AmazonWebServicesClientProxy.class, Mockito.RETURNS_DEEP_STUBS); 52 | when(proxy.injectCredentialsAndInvoke(any(GetParameterRequest.class), any(Function.class))).thenReturn(new GetParameterResult().withParameter(new Parameter().withValue(EXPECTED_OUTPUT))); 53 | logger = mock(Logger.class, Mockito.RETURNS_DEEP_STUBS); 54 | } 55 | 56 | @Test 57 | public void handleRequest_SimpleSuccess() { 58 | final ReadHandler handler = new ReadHandler(); 59 | 60 | final ResourceModel model = ResourceModel.builder().id(PHYSICAL_ID).build(); 61 | 62 | final ResourceHandlerRequest request = ResourceHandlerRequest.builder() 63 | .desiredResourceState(model) 64 | .region(AWS_REGION) 65 | .awsAccountId(AWS_ACCOUNT_ID) 66 | .build(); 67 | 68 | final CallbackContext context = CallbackContext.builder() 69 | .stabilizationRetriesRemaining(2) 70 | .build(); 71 | 72 | final ProgressEvent response 73 | = handler.handleRequest(proxy, request, context, logger); 74 | 75 | assertThat(response).isNotNull(); 76 | assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); 77 | assertThat(response.getCallbackContext()).isNull(); 78 | assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); 79 | assertThat(response.getResourceModel().getOutput()).isEqualTo(EXPECTED_OUTPUT); 80 | assertThat(response.getResourceModels()).isNull(); 81 | assertThat(response.getMessage()).isNotNull(); 82 | assertThat(response.getErrorCode()).isNull(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /template.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: AWS SAM template for the AWSUtility::CloudFormation::CommandRunner resource type 4 | 5 | Globals: 6 | Function: 7 | Timeout: 60 # docker start-up times can be long for SAM CLI 8 | 9 | Resources: 10 | TypeFunction: 11 | Type: AWS::Serverless::Function 12 | Properties: 13 | MemorySize: 8192 14 | Handler: software.awsutility.cloudformation.commandrunner.HandlerWrapper::handleRequest 15 | Runtime: java8.al2 16 | CodeUri: ./target/awsutility-cloudformation-commandrunner-handler-1.0-SNAPSHOT.jar 17 | 18 | TestEntrypoint: 19 | Type: AWS::Serverless::Function 20 | Properties: 21 | MemorySize: 8192 22 | Handler: software.awsutility.cloudformation.commandrunner.HandlerWrapper::testEntrypoint 23 | Runtime: java8.al2 24 | CodeUri: ./target/awsutility-cloudformation-commandrunner-handler-1.0-SNAPSHOT.jar 25 | --------------------------------------------------------------------------------