├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── examples └── bootstrap-new-aws-account │ ├── LICENSE │ ├── README.md │ └── images │ ├── github-pat-permissions.png │ ├── github-pr-message-codebuild.png │ ├── github-repo-creation.png │ └── identity-center-access-portal-url.png └── modules ├── aws-billing-budget-notification ├── LICENSE ├── README.md ├── main.tf ├── outputs.tf ├── variables.tf └── versions.tf ├── bootstrap-aws-account ├── LICENSE ├── README.md ├── main.tf ├── outputs.tf ├── templates │ ├── terraform.tf.tmpl │ └── versions.tf.tmpl ├── variables.tf └── versions.tf ├── bootstrap-cicd-aws-codebuild ├── LICENSE ├── README.md ├── main.tf ├── outputs.tf ├── templates │ └── buildspec_terraform.yml.tmpl ├── variables.tf └── versions.tf ├── bootstrap-cicd-github-actions ├── LICENSE ├── README.md ├── main.tf ├── outputs.tf ├── providers.tf ├── templates │ ├── github_actions_workflow.yml.tmpl │ └── provider-github.tf.tmpl └── variables.tf └── bootstrap-cloudtrail ├── LICENSE ├── README.md ├── main.tf ├── outputs.tf ├── providers.tf └── variables.tf /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | # Local .terraform directories 3 | **/.terraform/* 4 | 5 | # .tfstate files 6 | *.tfstate 7 | *.tfstate.* 8 | 9 | # Crash log files 10 | crash.log 11 | crash.*.log 12 | 13 | # Exclude all .tfvars files, which are likely to contain sensitive data, such as 14 | # password, private keys, and other secrets. These should not be part of version 15 | # control as they are data points which are potentially sensitive and subject 16 | # to change depending on the environment. 17 | *.tfvars 18 | *.tfvars.json 19 | 20 | # Ignore override files as they are usually used to override resources locally and so 21 | # are not checked in 22 | override.tf 23 | override.tf.json 24 | *_override.tf 25 | *_override.tf.json 26 | 27 | # Ignore transient lock info files created by terraform apply 28 | .terraform.tfstate.lock.info 29 | 30 | # Include override files you do wish to add to version control using negated pattern 31 | # !example_override.tf 32 | 33 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 34 | # example: *tfplan* 35 | 36 | # Ignore CLI configuration files 37 | .terraformrc 38 | terraform.rc 39 | 40 | # WIP Code 41 | modules/temp-ec2-instance/* -------------------------------------------------------------------------------- /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 *main* 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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Terraform samples 2 | 3 | Collection of examples of how to use Terraform with AWS. 4 | 5 | List of modules: 6 | 7 | | Module | Description| 8 | |-|-| 9 | |[bootstrap-aws-account](./modules/bootstrap-aws-account/)|Helps bootstrap a new AWS account to use Terraform by creating the resources needed to store the state file.| 10 | |[bootstrap-cicd-aws-codebuild](./modules/bootstrap-cicd-aws-codebuild/)|Sets up a CI/CD pipeline for Terraform in a GitHub repo, with workflows to add the plan as a comment on pull requests, and apply changes when a PR is merged using AWS CodeBuild.| 11 | |[bootstrap-cicd-github-actions](./modules/bootstrap-cicd-github-actions/)|Sets up a CI/CD pipeline for Terraform in a GitHub repo, with workflows to add the plan as a comment on pull requests, and apply changes when a PR is merged using GitHub Actions.| 12 | |[bootstrap-cloudtrail](./modules/bootstrap-cloudtrail/)|Sets up a basic AWS CloudTrail configuration for the account.| 13 | |[aws-billing-budget-notification](./modules/aws-billing-budget-notification/)|Sets up an AWS Budget with an email alert.| 14 | 15 | ## Security 16 | 17 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 18 | 19 | ## License 20 | 21 | This library is licensed under the MIT-0 License. See the LICENSE file. 22 | -------------------------------------------------------------------------------- /examples/bootstrap-new-aws-account/LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /examples/bootstrap-new-aws-account/README.md: -------------------------------------------------------------------------------- 1 | ## Bootstrapping a new AWS account 2 | 3 | Steps to follow along the [YouTube video](https://www.youtube.com/watch?v=3XfgGFdoybE) on setting up a new AWS account with - do not action any of the steps below, the order is important covered further down under [steps to follow](#steps-to-follow): 4 | 5 | 1. [Setting the MFA on your root account](https://us-east-1.console.aws.amazon.com/iam/home#/security_credentials) 6 | 2. [Enabling AWS Cost Explorer](https://us-east-1.console.aws.amazon.com/costmanagement/home#/cost-explorer) 7 | 3. [Enabling IAM Identity Center](https://us-east-1.console.aws.amazon.com/singlesignon/home?region=us-east-1#!/instances/dashboard), which will enable [AWS Organizations](https://us-east-1.console.aws.amazon.com/organizations/v2/home?region=us-west-2) as well. Set the AWS access portal URL to a friendly name. 8 | 9 | 4. [Enabling Cost Optimization Hub](https://us-east-1.console.aws.amazon.com/costmanagement/home#/cost-optimization-hub/) 10 | 5. Setting up the AWS infrastructure to store the Terraform state file in an S3 bucket (with versioning and encryption), and a DynamoDB table to use for state file locking 11 | 6. A CI/CD pipeline with CodeBuild to run `terraform plan` on any pull request in one build project, and `terraform apply` in another build project. Each one will have its own IAM Role, with the `plan` one limited to Read-Only access, and the `apply` one with full Admin-Access. 12 | 7. Set up AWS CloudTrail across all regions and accounts in the Organization. 13 | 8. A Budget with a billing alert that will trigger when 75% of USD 10 / month is reached to send an email to the email specified. 14 | 9. Setting up the `Administrators` and `Developers` groups in Identity Center, with the `policy/AdministratorAccess` and `policy/job-function/ViewOnlyAccess` IAM Policies with access to the current AWS account, and users for Mary Major (administrator) and John Doe (developer). 15 | 10. Configure your local computer's [AWS CLI](https://aws.amazon.com/cli/) to use the single sign-on (SSO) from the user in Identity Center. 16 | 17 | ## Steps to follow 18 | 19 | 1. [Sign up for a new AWS account](https://signin.aws.amazon.com/signup?request_type=register) 20 | 2. Set up your [MFA for the root user](https://us-east-1.console.aws.amazon.com/iam/home#/security_credentials) 21 | 3. Enable Cost Explorer by opening [this link](https://us-east-1.console.aws.amazon.com/costmanagement/home#/cost-explorer) 22 | 4. Enable [IAM Identity Center](https://us-east-1.console.aws.amazon.com/singlesignon/home?region=us-east-1#!/instances/dashboard) - **Important:** Make sure to select the AWS region you will be using for your infrastructure as you can only have 1 enabled per account. Choose the default option to create it for your AWS Organization - this will future-proof you if you want to use more than 1 AWS account (future content coming soon). Once enabled, set a custom start URL on the right-hand side under "AWS access portal URL" - see the image below. 23 | ![Image of the AWS Identity Center Console screen, with the AWS access portal URL highlighted](./images/identity-center-access-portal-url.png) 24 | 5. Create a new private GitHub repository - suggested to make it private, select `Terraform` for the `.gitignore` file, and set an appropriate license. Adding a `README.md` is optional, but recommended. 25 | ![GitHub repo creation page, with NotCobusNotDarko as the organization, aws-infra as the repo name, Terraform for the .gitignore selected, and license MIT.](./images/github-repo-creation.png) 26 | 6. Generate a [fine-grained personal access token (PAT) on GitHub](https://github.com/settings/tokens?type=beta) with the following permissions on the Organization (Resource owner) that the repo is in (this will be your GitHub username if you don't have a GitHub organization, or select one of the organizations from the dropdown): 27 | 28 | 1. **Repository access:** *All repositories* - you can lock it down to just the single repo you are using to bootstrap your AWS account, but useful to allow for future pipelines for other repositories. 29 | 2. **Repository permissions:** 30 | 1. *Contents:* Read-only - used to read the source code for running in CodeBuild 31 | 2. *Metadata:* Read-only - automatically set when selecting read-only for Contents 32 | 3. *Pull requests:* Read and write - used to write a comment with the result of the build, and the `terraform plan` output for any pull request 33 | 4. *Webhooks:* Read and write - CodeBuild will create a webhook to trigger the relevant build when code is committed / merged to the `main` branch, or a new pull request is opened 34 | 3. **Account permissions:** None 35 | ![GitHub PAT with permissions set according to the guide](./images/github-pat-permissions.png) 36 | 7. Open up [AWS CloudShell](https://us-west-2.console.aws.amazon.com/cloudshell/home?region=us-west-2#) - make sure to select the Region at the top-right of the screen for where you want to provision your resources / where you created your Identity Center. The next set of commands will be run in CloudShell: 37 | 1. Store your GitHub PAT in [AWS SSM Parameter Store](https://us-west-2.console.aws.amazon.com/systems-manager/parameters/?region=us-west-2) with the name `/cicd/github_tokan` as a `SecureString` using this command: 38 | 39 | ```bash 40 | aws ssm put-parameter \ 41 | --name "/cicd/github_token" \ 42 | --value "your GitHub PAT" \ 43 | --type "SecureString" \ 44 | --overwrite 45 | ``` 46 | 47 | 2. Install v1.9.7 of Terraform: 48 | 49 | ```bash 50 | cd /tmp/ && \ 51 | wget https://releases.hashicorp.com/terraform/1.9.7/terraform_1.9.7_linux_amd64.zip && \ 52 | unzip terraform_1.9.7_linux_amd64.zip && \ 53 | sudo mv terraform /usr/bin && \ 54 | rm LICENSE.txt terraform_1.9.7_linux_amd64.zip && \ 55 | cd - 56 | ``` 57 | 58 | 3. Generate an ssh key to pull the private repo from GitHub with `ssh-keygen -t ed25519`, and then add the `.pub` part of the key to your [SSH keys](https://github.com/settings/keys) on GitHub. If you used the default key name, you can use `cat ~/.ssh/id_ed25519.pub` to get the string - make sure to copy the whole line. 59 | 4. Set git's name and email with your details (we'll be committing some code from CloudShell later) using: 60 | 61 | ```bash 62 | git config --global user.name "Your Name" 63 | git config --global user.email "Your email" 64 | ``` 65 | 66 | 5. Clone your infrastructure repo with `git clone git@github.com//`. 67 | 6. Change into the code folder with `cd `, and create the `terraform` directory with `mkdir terraform`, then change into it with `cd terraform`. 68 | 69 | 8. We're now ready to start using Terraform to build our infrastructure, but first, we need to create the resources to store our state file. 70 | 1. Create a file with a `.tf` extension, we'll be using `bootstrap.tf`, and add the following to it: 71 | 1. **`state_file_aws_region`** - Change to your region, this is where the state file bucket will be 72 | 2. **`state_file_bucket_name`** - Change to the name you want to use for the state file bucket - S3 bucket names are globally unique. If you want to make sure the name is available, you can open up the S3 section in the AWS console, and create the bucket, then delete it again - just make sure it is in the region you are using in this file, otherwise you will need to wait 45-60 minutes for the name to become available again. 73 | 3. **`aws_region`** - Region to use for creating the rest of our infrastructure. (**Update 2024-10-28:** The `aws_region` input variable is not needed, if you are following along with the video, please remove it. It won't break anything as it isn't being used at all.) 74 | 75 | ```terraform 76 | module "bootstrap" { 77 | source = "github.com/build-on-aws/terraform-samples//modules/bootstrap-aws-account" 78 | 79 | state_file_aws_region = "us-west-2" 80 | state_file_bucket_name = "name-for-the-state-file-bucket" 81 | } 82 | ``` 83 | 84 | 4. Run `terraform init` to install the providers needed, and download the module. 85 | 5. Run `terraform apply` - this will create the state file resources, and generate `terraform.tf` and `providers.tf`: 86 | 1. **`terraform.tf`** - contains the backend configuration to use S3 / DynamoDB to store the state file 87 | 2. **`providers.tf`** - specifies the version of Terraform to use, along with the version so the `aws` and `local` provider. 88 | 6. Run `terraform apply` again - it should now ask if you want to migrate the existing state file, type in `yes` and press enter. 89 | 7. Congrats! The first step to set up the CI/CD pipeline for Terraform, one more step to go! But first, let's ensure we don't lose our setup by committing it: 90 | 91 | ```bash 92 | git add . 93 | git commit -m "Set up Terraform state file infrastructure" 94 | git push 95 | ``` 96 | 97 | 9. Next, we will create the CodeBuild jobs for `terraform plan` and `terraform apply`. 98 | 1. Add to the `bootstrap.tf` file, or create `bootstrap-cicd.tf`, and add the following - **important:** if you changed the name of the module in the previous step from `bootstrap` to something else, please update the `aws_region` and `state_file_iam_policy_arn` accordingly: 99 | 1. **`github_organization`** - either your GitHub username, or the GitHub organization name 100 | 2. **`github_repository`** - name of the repo 101 | 3. **`aws_region`** - region for the AWS resources 102 | 4. **`state_file_iam_policy_arn`** - generated policy to allow access to state file resources, used for the IAM Roles for the CodeBuild projects 103 | 104 | ```terraform 105 | module "bootstrap_cicd_aws_codebuild" { 106 | source = "github.com/build-on-aws/terraform-samples//modules/bootstrap-cicd-aws-codebuild" 107 | 108 | github_organization = "your org | github username" 109 | github_repository = "your repo name" 110 | aws_region = "us-east-2" 111 | state_file_iam_policy_arn = module.bootstrap.state_file_iam_policy_arn 112 | 113 | codebuild_terraform_version = "1.9.7" 114 | } 115 | ``` 116 | 117 | 2. Run `terraform init` to download the new module. 118 | 3. Run `terraform apply` to create the CodeBuild projects. 119 | 4. Wohoo! Congrats! Your CI/CD pipeline is now ready, let's commit the changes to avoid losing them: 120 | 121 | ```bash 122 | git add . 123 | git commit -m "Adding the CI/CD pipeline" 124 | git push 125 | ``` 126 | 127 | 10. Now we can use our new pipeline to set up our Identity Center users, groups, permissions, and account access. But first, let's use our new CI/CD pipeline: 128 | 1. Create a new branch by running `git checkout -b add-identity-center` in the root of the rpo. 129 | 2. Create a new file called `identity-center.tf` with the following, and update the `sso_users`, `sso_groups`, `permission_sets`, and `account_assignments` to your needs: 130 | 131 | ```terraform 132 | #--------------------# 133 | # Retrieve account id 134 | #--------------------# 135 | data "aws_caller_identity" "current" {} 136 | 137 | locals { 138 | # Group definitions 139 | sso_groups = { 140 | Admin = { 141 | group_name = "Admin" 142 | group_description = "Admin IAM Identity Center Group" 143 | }, 144 | Developers = { 145 | group_name = "Developers" 146 | group_description = "Dev IAM Identity Center Group" 147 | } 148 | } 149 | 150 | # User definitions 151 | sso_users = { 152 | marymajor = { 153 | group_membership = [local.sso_groups.Admin.group_name, local.sso_groups.Developers.group_name] 154 | user_name = "marymajor" 155 | given_name = "Mary" 156 | family_name = "Major" 157 | email = "marymajor@example.com" 158 | }, 159 | johndoe = { 160 | group_membership = [local.sso_groups.Developers.group_name] 161 | user_name = "johndoe" 162 | given_name = "John" 163 | family_name = "Doe" 164 | email = "johndoe@example.com" 165 | } 166 | } 167 | 168 | # Permission sets definitions 169 | permission_sets = { 170 | AdministratorAccess = { 171 | description = "Provides AWS full access permissions." 172 | session_duration = "PT4H" 173 | aws_managed_policies = ["arn:aws:iam::aws:policy/AdministratorAccess"] 174 | tags = { ManagedBy = "Terraform" } 175 | }, 176 | ViewOnlyAccess = { 177 | description = "Provides AWS view only permissions." 178 | session_duration = "PT3H" 179 | aws_managed_policies = ["arn:aws:iam::aws:policy/job-function/ViewOnlyAccess"] 180 | tags = { ManagedBy = "Terraform" } 181 | } 182 | } 183 | 184 | # Account assignments 185 | account_assignments = { 186 | Admin = { 187 | principal_name = local.sso_groups.Admin.group_name 188 | principal_type = "GROUP" 189 | principal_idp = "INTERNAL" 190 | permission_sets = ["AdministratorAccess", "ViewOnlyAccess"] 191 | account_ids = [data.aws_caller_identity.current.account_id] 192 | }, 193 | Developers = { 194 | principal_name = local.sso_groups.Developers.group_name 195 | principal_type = "GROUP" 196 | principal_idp = "INTERNAL" 197 | permission_sets = ["ViewOnlyAccess"] 198 | account_ids = [data.aws_caller_identity.current.account_id] 199 | } 200 | } 201 | } 202 | 203 | module "aws-iam-identity-center" { 204 | source = "aws-ia/iam-identity-center/aws" 205 | 206 | # Use local variables for groups, users, permission sets, and account assignments 207 | sso_groups = local.sso_groups 208 | sso_users = local.sso_users 209 | permission_sets = local.permission_sets 210 | account_assignments = local.account_assignments 211 | } 212 | ``` 213 | 214 | 3. Run `terraform init` and then `terraform plan` to confirm there aren't any syntax issues. 215 | 4. Now commit and push this change with: 216 | 217 | ```bash 218 | git add . 219 | git commit -m "Adding Identity Center" 220 | git push --set-upstream origin add-identity-center 221 | ``` 222 | 223 | 5. Go to repository on [GitHub](https://github.com), it should have a message about the new branch, and ask if you want to create a pull request - do that. If you don't see the message, you can go to "branches", select the `add-identity-center` one, and from there create a PR. 224 | 6. Once the PR has been created, you should see the CodeBuild job start with a yellow status indicator. Once it has completed without errors and printed out output of the build as a comment on the PR, you can merge the PR. Here's what the message should look like: 225 | ![Comment on a GitHub PR with a markdown table with the status for init, fmt, validate, plan all showing successful, and expandable section to show the Terraform plan, and the username of the PR author / Action that triggered CodeBuild](./images/github-pr-message-codebuild.png) 226 | 7. Nice! You now have users that can log into the AWS account using SSO and the AWS access portal URL you set earlier - please note that each user would need to follow the Forgot Password flow to set it for the first time. 227 | 228 | 11. Next, we will set up an AWS CloudTrail to allow tracking all user and API activity in our account. 229 | 1. In CloudShell, switch to the `main` branch, `git pull` the latest changes (we just merged a PR, so our local `main` won't be up to date with that merge), then create a new branch for CloudTrail. You can use the following commands to do this: 230 | 231 | ```bash 232 | git checkout main 233 | git pull 234 | git checkout -b add-cloudtrail 235 | ``` 236 | 237 | 2. Create a new file in the `terraform/` folder called `cloudtrail.tf` with the following contents: 238 | 239 | ```terraform 240 | module "bootstrap_cloudtrail" { 241 | source = "github.com/build-on-aws/terraform-samples//modules/bootstrap-cloudtrail" 242 | 243 | aws_region = "us-west-2" 244 | cloudtrail_bucket_name = "name for your S3 bucket to store the trails" 245 | cloudtrail_name = "friendly name for the trail" 246 | } 247 | ``` 248 | 249 | 3. Run `terraform init` to download the new module used. 250 | 4. Confirm there aren't any syntax / other errors by running `terraform plan`. 251 | 5. If there are no errors, commit the change, and push the branch with: 252 | 253 | ```bash 254 | git add . 255 | git commit -m "Adding CloudTrail" 256 | git push --set-upstream origin add-cloudtrail 257 | ``` 258 | 259 | 6. Go to your repo on [GitHub](https://github.com), and create a new PR from this branch. 260 | 7. Wait for the CodeBuild job to finish, then confirm there weren't any errors by looking a the message that is posted to the PR. 261 | 8. If there aren't any errors, merge the branch. 262 | 12. Almost done, in the home stretch now! We want to add one more resource: a Budget with a Billing Alert to notify us in case we approach a monthly spend we want to monitor. 263 | 1. In CloudShell, switch to the `main` branch, `git pull` the latest changes (we just merged a PR, so our local `main` won't be up to date with that merge), then create a new branch for the Budget. You can use the following commands to do this: 264 | 265 | ```bash 266 | git checkout main 267 | git pull 268 | git checkout -b add-budget-billing-alert 269 | ``` 270 | 271 | 2. Create a new file in the `terraform/` folder called `budget.tf` with the following contents - you can set the commented out values if you want different ones from the defaults (the currently set values): 272 | 273 | ```terraform 274 | module "budget_billing_alert" { 275 | source = "github.com/build-on-aws/terraform-samples//modules/aws-billing-budget-notification" 276 | 277 | budget_email_address = "your-email-address@example.com" 278 | 279 | # budget_alert_amount = 10 280 | # budget_alert_currency = "USD" 281 | # budget_alert_threshold_percentage = 75 282 | } 283 | ``` 284 | 285 | 3. Run `terraform init` to download the new module used. 286 | 4. Confirm there aren't any syntax / other errors by running `terraform plan`. 287 | 5. If there are no errors, commit the change, and push the branch with: 288 | 289 | ```bash 290 | git add . 291 | git commit -m "Adding a Budget and Billing Alert" 292 | git push --set-upstream origin add-budget-billing-alert 293 | ``` 294 | 295 | 6. Go to your repo on [GitHub](https://github.com), and create a new PR from this branch. 296 | 7. Wait for the CodeBuild job to finish, then confirm there weren't any errors by looking a the message that is posted to the PR. 297 | 8. If there aren't any errors, merge the branch. 298 | 13. Congratulations! You've just set up a Terraform CI/CD pipeline to review PRs for infrastructure changes that will apply them when the PR is merged, Identity Center groups and users, CloudTrail to monitor activity on your account, and a Budget and Billing Alert to notify you when you reach 75% of the USD 10 budget. 299 | 14. The last step is to configure the users' AWS CLI using the AWS access portal URL. 300 | 1. On their computer, they can run the following after installing the AWS CLI: 301 | 302 | ```bash 303 | aws configure sso 304 | ``` 305 | 306 | 2. Enter an appropriate name for the session - this can be used to identify the user's activities from this specific computer, useful if they use multiple ones to differentiate. 307 | 3. Use the AWS access portal URL from Identity Center. 308 | 4. Select the default region, and output format on your preferences. 309 | 5. After completing the configuration, they can now log in with `aws sso login` - this will open up the browser to first log into the AWS account using the user we set up for them. Remember to use "Forgot password" if they have not yet logged in after we created the user. 310 | 15. And finally, in the AWS Console, you can click on the "Actions" dropdown in CloudShell, and select `Delete` to remove all data that was used for the setup. Remember to also remove the SSH key you added to your GitHub account as it was only used in CloudShell for the bootstrapping process. 311 | 312 | ## License 313 | 314 | This library is licensed under the MIT-0 License. See the LICENSE file. 315 | -------------------------------------------------------------------------------- /examples/bootstrap-new-aws-account/images/github-pat-permissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/terraform-samples/a12a8a947352533a58e7e50f56933674b151573b/examples/bootstrap-new-aws-account/images/github-pat-permissions.png -------------------------------------------------------------------------------- /examples/bootstrap-new-aws-account/images/github-pr-message-codebuild.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/terraform-samples/a12a8a947352533a58e7e50f56933674b151573b/examples/bootstrap-new-aws-account/images/github-pr-message-codebuild.png -------------------------------------------------------------------------------- /examples/bootstrap-new-aws-account/images/github-repo-creation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/terraform-samples/a12a8a947352533a58e7e50f56933674b151573b/examples/bootstrap-new-aws-account/images/github-repo-creation.png -------------------------------------------------------------------------------- /examples/bootstrap-new-aws-account/images/identity-center-access-portal-url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/terraform-samples/a12a8a947352533a58e7e50f56933674b151573b/examples/bootstrap-new-aws-account/images/identity-center-access-portal-url.png -------------------------------------------------------------------------------- /modules/aws-billing-budget-notification/LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /modules/aws-billing-budget-notification/README.md: -------------------------------------------------------------------------------- 1 | ## aws-billing-budget-notifications 2 | 3 | Utility module to set up a basic AWS Budget and send an email when the threshold is reached. Defaults to 75% of USD 20 for when to trigger. 4 | 5 | ## License 6 | 7 | This library is licensed under the MIT-0 License. See the LICENSE file. 8 | -------------------------------------------------------------------------------- /modules/aws-billing-budget-notification/main.tf: -------------------------------------------------------------------------------- 1 | #--------------------------------------------# 2 | # Using locals instead of hard-coding strings 3 | #--------------------------------------------# 4 | locals { 5 | aws_tags = coalesce(var.override_aws_tags, { 6 | Name = "tf-bootstrap", 7 | Module = "build-on-aws/terraform-samples/modules/bootstrap-aws-account", 8 | }) 9 | } 10 | 11 | #-----------------------------------# 12 | # Set up a Budget and Billing aleart 13 | #-----------------------------------# 14 | resource "aws_budgets_budget" "total_spend" { 15 | name = "budget-monthly" 16 | budget_type = "COST" 17 | limit_amount = var.budget_alert_amount 18 | limit_unit = var.budget_alert_currency 19 | time_unit = "MONTHLY" 20 | 21 | notification { 22 | comparison_operator = "GREATER_THAN" 23 | threshold = var.budget_alert_threshold_percentage 24 | threshold_type = "PERCENTAGE" 25 | notification_type = "FORECASTED" 26 | subscriber_email_addresses = [var.budget_email_address] 27 | } 28 | 29 | tags = local.aws_tags 30 | } -------------------------------------------------------------------------------- /modules/aws-billing-budget-notification/outputs.tf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/terraform-samples/a12a8a947352533a58e7e50f56933674b151573b/modules/aws-billing-budget-notification/outputs.tf -------------------------------------------------------------------------------- /modules/aws-billing-budget-notification/variables.tf: -------------------------------------------------------------------------------- 1 | variable "budget_email_address" { 2 | description = "(Required) Email address to send budget notifications to" 3 | type = string 4 | } 5 | 6 | variable "budget_alert_currency" { 7 | description = "(Required) Currency to use for budget alert mails, defaults to USD" 8 | type = string 9 | default = "USD" 10 | } 11 | 12 | variable "budget_alert_amount" { 13 | description = "(Required) Budget amount for the alert, defaults to USD 10" 14 | type = number 15 | default = 10 16 | } 17 | 18 | variable "budget_alert_threshold_percentage" { 19 | description = "(Required) Budget alert threshold percentage to trigger the alert, defaults to 75" 20 | type = number 21 | default = 75 22 | } 23 | 24 | variable "override_aws_tags" { 25 | description = "Override tags to apply to AWS resources" 26 | type = map(string) 27 | default = null 28 | } -------------------------------------------------------------------------------- /modules/aws-billing-budget-notification/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0" # Or use e.g. ">= 1.9.7" to be more specific 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = ">= 5.70.0" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /modules/bootstrap-aws-account/LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /modules/bootstrap-aws-account/README.md: -------------------------------------------------------------------------------- 1 | ## bootstrap-aws-account 2 | 3 | Sets up the required resources for Terraform to use Amazon S3 as a backend. Will create the following resources: 4 | 5 | 1. **S3 bucket:** stores the state file, encrypted with KMS, and with versioning enabled 6 | 2. **DynamoDB table:** state file locking table 7 | 3. **`terraform.tf`:** local file with the S3 backend configuration values specified 8 | 4. **`providers.tf`:** defines the versions for Terraform, AWS, and Local provider to use 9 | 10 | ## Minimum configuration 11 | 12 | To use the module, the following needs to be specified: 13 | 14 | ```terraform 15 | module "bootstrap" { 16 | source = "github.com/build-on-aws/terraform-samples//modules/bootstrap-aws-account" 17 | 18 | state_file_aws_region = "region-for-state-file-bucket" 19 | state_file_bucket_name = "name-for-the-state-file-bucket" 20 | } 21 | ``` 22 | 23 | ## Full configuration 24 | 25 | |Variable|Type|Details| 26 | |-|-|-| 27 | |state_file_profile_name|`string`|AWS Profile to use for credentials instead of the default one| 28 | |override_state_lock_table_name|`string`|Default value is `terraform-state-lock`, allows setting a different value.| 29 | |override_aws_tags|`string`|Default value is `{Name = "tf-bootstrap", Module = "build-on-aws/terraform-samples/modules/bootstrap-aws-account",}`, allows setting a different tags.| 30 | |override_kms_key_alias|`string`|Default value is `alias/aws/s3`, allows setting a different key alias to use.| 31 | |override_tf_version|`string`|Default value is `1.9.7`, allows setting a different value.| 32 | |override_local_provider_version|`string`|Default value is `null`, allows setting a different value.| 33 | |tf_additional_providers|`list(object({name = string provider_source = string provider_version = string}))`|Default value is `[]`, allows adding additional providers to add to the generated `providers.tf` file.| 34 | 35 | ## License 36 | 37 | This library is licensed under the MIT-0 License. See the LICENSE file. 38 | -------------------------------------------------------------------------------- /modules/bootstrap-aws-account/main.tf: -------------------------------------------------------------------------------- 1 | #--------------------------------------------# 2 | # Using locals instead of hard-coding strings 3 | #--------------------------------------------# 4 | locals { 5 | tf_version = coalesce(var.override_tf_version, "1.9.7") 6 | state_lock_table_name = coalesce(var.override_state_lock_table_name, "terraform-state-lock") 7 | kms_key_alias = coalesce(var.override_kms_key_alias, "alias/aws/s3") 8 | 9 | aws_tags = coalesce(var.override_aws_tags, { 10 | Name = "tf-bootstrap", 11 | Module = "build-on-aws/terraform-samples/modules/bootstrap-aws-account", 12 | }) 13 | 14 | provider_config = concat(var.tf_additional_providers, [ 15 | { 16 | name = "aws" 17 | provider_source = "hashicorp/aws" 18 | provider_version = coalesce(var.override_aws_provider_version, "5.70.0") 19 | }, 20 | { 21 | name = "local" 22 | provider_source = "hashicorp/local" 23 | provider_version = coalesce(var.override_local_provider_version, "2.5.2") 24 | } 25 | ]) 26 | } 27 | 28 | #----------------------------------------------# 29 | # AWS resources to store the state file 30 | #----------------------------------------------# 31 | # 1. S3 bucket, with versioning, KMS encryption, 32 | # no public access, and locked down ACLs 33 | # 2. DynamoDB table for state locking, encrypted 34 | 35 | # S3 Bucket to store state file 36 | resource "aws_s3_bucket" "state_file_bucket" { 37 | bucket = var.state_file_bucket_name 38 | tags = local.aws_tags 39 | } 40 | 41 | # Ignore other ACLs to ensure bucket stays private 42 | resource "aws_s3_bucket_public_access_block" "state_file_bucket" { 43 | bucket = aws_s3_bucket.state_file_bucket.id 44 | block_public_acls = false 45 | block_public_policy = false 46 | ignore_public_acls = false 47 | restrict_public_buckets = false 48 | } 49 | 50 | # Set ownership controls to bucket to prevent access from other AWS accounts 51 | resource "aws_s3_bucket_ownership_controls" "state_file_bucket" { 52 | bucket = aws_s3_bucket.state_file_bucket.id 53 | 54 | rule { 55 | object_ownership = "BucketOwnerPreferred" 56 | } 57 | } 58 | 59 | # Set bucket ACL to private 60 | resource "aws_s3_bucket_acl" "state_file_bucket" { 61 | depends_on = [ 62 | aws_s3_bucket_ownership_controls.state_file_bucket, 63 | aws_s3_bucket_public_access_block.state_file_bucket, 64 | ] 65 | 66 | bucket = aws_s3_bucket.state_file_bucket.id 67 | acl = "private" 68 | } 69 | 70 | # Enable bucket versioning 71 | resource "aws_s3_bucket_versioning" "state_file_bucket" { 72 | bucket = aws_s3_bucket.state_file_bucket.id 73 | 74 | versioning_configuration { 75 | status = "Enabled" 76 | } 77 | } 78 | 79 | data "aws_kms_alias" "s3" { 80 | name = local.kms_key_alias 81 | } 82 | 83 | # Encrypt bucket 84 | resource "aws_s3_bucket_server_side_encryption_configuration" "state_file_bucket" { 85 | bucket = aws_s3_bucket.state_file_bucket.id 86 | 87 | rule { 88 | apply_server_side_encryption_by_default { 89 | kms_master_key_id = data.aws_kms_alias.s3.target_key_arn 90 | sse_algorithm = "aws:kms" 91 | } 92 | } 93 | } 94 | 95 | # DynamoDB table for locking the state file while updating 96 | resource "aws_dynamodb_table" "state_file_lock_table" { 97 | name = local.state_lock_table_name 98 | billing_mode = "PAY_PER_REQUEST" 99 | hash_key = "LockID" 100 | 101 | attribute { 102 | name = "LockID" 103 | type = "S" 104 | } 105 | 106 | tags = local.aws_tags 107 | } 108 | 109 | # IAM Policy document to access the S3 bucket and DynamoDB table used by 110 | # the state file. 111 | data "aws_iam_policy_document" "state_file_access_permissions" { 112 | statement { 113 | effect = "Allow" 114 | actions = [ 115 | "dynamodb:DescribeTable", 116 | "dynamodb:GetItem", 117 | "dynamodb:PutItem", 118 | "dynamodb:DeleteItem", 119 | ] 120 | resources = [ 121 | "${aws_dynamodb_table.state_file_lock_table.arn}", 122 | ] 123 | } 124 | 125 | statement { 126 | effect = "Allow" 127 | actions = [ 128 | "s3:ListBucket" 129 | ] 130 | resources = [ 131 | "${aws_s3_bucket.state_file_bucket.arn}/${var.state_file_bucket_key}", 132 | ] 133 | } 134 | 135 | statement { 136 | effect = "Allow" 137 | actions = [ 138 | "s3:GetObject", 139 | "s3:PutObject" 140 | ] 141 | resources = [ 142 | "${aws_s3_bucket.state_file_bucket.arn}/", 143 | ] 144 | } 145 | 146 | statement { 147 | effect = "Allow" 148 | actions = [ 149 | "kms:Encrypt", 150 | "kms:Decrypt", 151 | "kms:GenerateDataKey" 152 | ] 153 | resources = [ 154 | "${data.aws_kms_alias.s3.target_key_arn}/", 155 | ] 156 | } 157 | 158 | } 159 | 160 | # State file access IAM policy - this will be used by other modules / resources 161 | # that will be defined when this module is used. 162 | resource "aws_iam_policy" "state_file_access_iam_policy" { 163 | name = "tf-state-file-access" 164 | policy = data.aws_iam_policy_document.state_file_access_permissions.json 165 | tags = local.aws_tags 166 | } 167 | 168 | # Create the terraform backend configuration - the catch 22 is that you need infrastructure to 169 | # store the state file before you can automate your infrastructure. The approach needs 2 steps: 170 | # 1. Create the S3 bucket and DynamoDB table to store the state, and generate the backend 171 | # config for terraform to use in the terraform.tf file. 172 | # 2. For the 2nd run, it will now use this config and migrate the local state file to S3. 173 | resource "local_file" "terraform_tf" { 174 | filename = "${path.root}/terraform.tf" 175 | content = templatefile("${path.module}/templates/terraform.tf.tmpl", { 176 | state_file_bucket_name = var.state_file_bucket_name 177 | state_file_bucket_key = var.state_file_bucket_key 178 | state_file_aws_region = var.state_file_aws_region 179 | kms_key_id = local.kms_key_alias 180 | dynamodb_table = aws_dynamodb_table.state_file_lock_table.name 181 | profile_name = var.state_file_profile_name 182 | }) 183 | directory_permission = "0666" 184 | file_permission = "0666" 185 | } 186 | 187 | # Generate the versions.tf to specify the providers to use with minimum versions 188 | resource "local_file" "versions_tf" { 189 | filename = "${path.root}/versions.tf" 190 | content = templatefile("${path.module}/templates/versions.tf.tmpl", { 191 | tf_version = local.tf_version 192 | providers = local.provider_config 193 | }) 194 | directory_permission = "0666" 195 | file_permission = "0666" 196 | } 197 | -------------------------------------------------------------------------------- /modules/bootstrap-aws-account/outputs.tf: -------------------------------------------------------------------------------- 1 | output "state_file_iam_policy_arn" { 2 | value = aws_iam_policy.state_file_access_iam_policy.arn 3 | } 4 | 5 | output "aws_region_for_resources" { 6 | value = var.aws_region 7 | } -------------------------------------------------------------------------------- /modules/bootstrap-aws-account/templates/terraform.tf.tmpl: -------------------------------------------------------------------------------- 1 | # Generated by the build-on-aws/terraform-samples/modules/bootstrap-aws-account module. 2 | # While changes won't be overwritten unless run locally and committed, highly recommended 3 | # to not change this outside of the module. 4 | terraform { 5 | backend "s3" { 6 | bucket = "${state_file_bucket_name}" 7 | key = "${state_file_bucket_key}" 8 | region = "${state_file_aws_region}" 9 | encrypt = true 10 | kms_key_id = "${kms_key_id}" 11 | dynamodb_table = "${dynamodb_table}"%{ if profile_name != null } 12 | profile = "${profile_name}"%{ endif } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /modules/bootstrap-aws-account/templates/versions.tf.tmpl: -------------------------------------------------------------------------------- 1 | # Generated by the build-on-aws/terraform-samples/modules/bootstrap-aws-account module. 2 | # While changes won't be overwritten unless run locally and committed, highly recommended 3 | # to not change this outside of the module. 4 | terraform { 5 | required_version = ">= ${tf_version}" 6 | 7 | required_providers { 8 | %{ for provider in providers ~} 9 | ${lower(provider.name)} = { 10 | source = "${provider.provider_source}" 11 | version = ">= ${provider.provider_version}" 12 | } 13 | %{ endfor ~} 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /modules/bootstrap-aws-account/variables.tf: -------------------------------------------------------------------------------- 1 | 2 | variable "state_file_bucket_name" { 3 | description = "(Required) Name of the S3 bucket to store the state file" 4 | type = string 5 | } 6 | 7 | variable "state_file_bucket_key" { 8 | description = "(Required) Key of the S3 bucket to store the state file" 9 | type = string 10 | default = "terraform-state" 11 | } 12 | 13 | variable "state_file_aws_region" { 14 | description = "(Required) AWS region of the S3 bucket to store the state file" 15 | type = string 16 | } 17 | 18 | variable "aws_region" { 19 | description = "(Deprecated) Not used, leaving for backward compatibility" 20 | type = string 21 | default = null 22 | } 23 | 24 | variable "state_file_profile_name" { 25 | description = "(Optional) Name of the AWS profile to use for the state file S3 bucket" 26 | type = string 27 | default = null 28 | } 29 | 30 | variable "tf_additional_providers" { 31 | description = "(Optional) List of additional Terraform providers" 32 | type = list(object({ 33 | name = string 34 | provider_source = string 35 | provider_version = string 36 | })) 37 | default = [] 38 | } 39 | 40 | variable "override_state_lock_table_name" { 41 | description = "(Optional) Override name of the DynamoDB table to use for locking while updating, defaults to terraform-state-lock" 42 | type = string 43 | default = null 44 | } 45 | 46 | variable "override_aws_tags" { 47 | description = "(Optional) Override tags to apply to AWS resources" 48 | type = map(string) 49 | default = null 50 | } 51 | 52 | variable "override_kms_key_alias" { 53 | description = "(Optional) Override KMS key alias to use for state file encryption, defaults to alias/kms/s3" 54 | type = string 55 | default = null 56 | } 57 | 58 | variable "override_tf_version" { 59 | description = "(Optional) Override version of Terraform to use, defaults to 1.9.7 if not set" 60 | type = string 61 | default = null 62 | } 63 | 64 | variable "override_aws_provider_version" { 65 | description = "(Optional) Override version of AWS provider to use, defaults to 5.70.0 if not set" 66 | type = string 67 | default = null 68 | } 69 | 70 | variable "override_local_provider_version" { 71 | description = "(Optional) Override version of local provider to use, defaults to 2.5.2 if not set" 72 | type = string 73 | default = null 74 | } 75 | -------------------------------------------------------------------------------- /modules/bootstrap-aws-account/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0" # Or use e.g. ">= 1.9.7" to be more specific 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = ">= 5.70.0" 8 | } 9 | local = { 10 | source = "hashicorp/local" 11 | version = ">= 2.5.2" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /modules/bootstrap-cicd-aws-codebuild/LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /modules/bootstrap-cicd-aws-codebuild/README.md: -------------------------------------------------------------------------------- 1 | ## bootstrap-cicd-aws-codebuild 2 | 3 | Creates the basic infrastructure for a CI/CD pipeline using a GitHub repository. Creates: 4 | 5 | 1. **IAM Roles:** One role with read-only IAM policy allowing `terraform plan` to be run, and a 2nd role with an admin policy allowing `terraform apply` to be run. 6 | 2. **CodeBuild:** Creates 2 CodeBuild projects, one for `terraform plan` using a read-only IAM role, and a 2nd for `terraform apply` using an IAM role with admin permissions. 7 | 3. **Webhooks**: Webhooks to start the respective CodeBuild projects on pull requests, or PR merges / commits to the `main` branch. 8 | 9 | ### Usage 10 | 11 | Requires a [GitHub PAT](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) with the following permissions on the repositories to add the build projects for: 12 | 13 | 1. **Code**: Read-only - read the code to run terraform 14 | 2. **Pull Requests**: Read and Write - allows posting a summary of changes / errors on PRs 15 | 3. **Webhooks**: Read and Write - create the webhooks to trigger the CodeBuild jobs 16 | 17 | The module uses the location `/cicd/github_token` in SSM Parameter Store as the default location, to store the PAT for this module, you can use the following command: 18 | 19 | ```bash 20 | aws ssm put-parameter \ 21 | --name "/cicd/github_token" \ 22 | --value "your PAT value" \ 23 | --type "SecureString" \ 24 | --overwrite 25 | ``` 26 | 27 | ## License 28 | 29 | This library is licensed under the MIT-0 License. See the LICENSE file. 30 | -------------------------------------------------------------------------------- /modules/bootstrap-cicd-aws-codebuild/main.tf: -------------------------------------------------------------------------------- 1 | #--------------------------------------------# 2 | # Using locals instead of hard-coding strings 3 | #--------------------------------------------# 4 | locals { 5 | codebuild_image_uri = coalesce(var.override_terraform_build_image_uri, "public.ecr.aws/hashicorp/terraform:${var.codebuild_terraform_version}") 6 | kms_key_alias = coalesce(var.override_kms_key_alias, "alias/aws/s3") 7 | codebuild_compute_type = "BUILD_GENERAL1_SMALL" 8 | repo_source_files_bucket_name = coalesce(var.override_repo_source_files_bucket_name, "gh-codebuild-${lower(var.github_organization)}-${lower(var.github_repository)}") 9 | terraform_source_dir = coalesce(var.override_terraform_source_dir, "terraform/") 10 | repository_default_branch_name = coalesce(var.override_repository_default_branch_name, "main") 11 | iam_role_name_codebuild_plan = coalesce(var.override_iam_role_name_codebuild_plan, "gh-${substr(var.github_repository, 0, 64 - length("gh--tf-apply"))}-tf-plan") 12 | iam_role_name_codebuild_apply = coalesce(var.override_iam_role_name_codebuild_apply, "gh-${substr(var.github_repository, 0, 64 - length("gh--tf-apply"))}-tf-apply") 13 | codebuild_project_name_plan = "terraform-plan" 14 | codebuild_project_name_apply = "terraform-apply" 15 | iam_policy_apply_arn = coalesce(var.override_iam_policy_apply_arn, "arn:aws:iam::aws:policy/AdministratorAccess") 16 | iam_policy_plan_arn = coalesce(var.override_iam_policy_plan_arn, "arn:aws:iam::aws:policy/ReadOnlyAccess") 17 | aws_ssm_name_github_token = coalesce(var.override_aws_ssm_name_github_token, "/cicd/github_token") 18 | codebuild_spec_template_filename = "buildspec_terraform.yml.tmpl" 19 | github_token_env_var_name = "GITHUB_TOKEN" 20 | 21 | aws_tags = { 22 | GitHubRepo = "${var.github_organization}/${var.github_repository}" 23 | Module = "build-on-aws/terraform-samples/modules/bootstrap-cicd-aws-codebuild-codepipeline" 24 | } 25 | } 26 | 27 | data "aws_caller_identity" "current" {} 28 | 29 | # Retrieve the GitHub token from AWS Systems Manager Parameter Store 30 | data "aws_ssm_parameter" "github_token" { 31 | name = local.aws_ssm_name_github_token 32 | } 33 | 34 | # Set up access to GitHub using the token 35 | resource "aws_codebuild_source_credential" "github" { 36 | auth_type = "PERSONAL_ACCESS_TOKEN" 37 | server_type = "GITHUB" 38 | token = data.aws_ssm_parameter.github_token.value 39 | } 40 | 41 | # S3 Bucket to store repo source code for builds 42 | resource "aws_s3_bucket" "repo_source_files" { 43 | bucket = local.repo_source_files_bucket_name 44 | 45 | tags = local.aws_tags 46 | } 47 | 48 | # Ignore other ACLs to ensure bucket stays private 49 | resource "aws_s3_bucket_public_access_block" "repo_source_files" { 50 | bucket = aws_s3_bucket.repo_source_files.id 51 | 52 | block_public_acls = false 53 | block_public_policy = false 54 | ignore_public_acls = false 55 | restrict_public_buckets = false 56 | } 57 | 58 | # Set ownership controls to bucket to prevent access from other AWS accounts 59 | resource "aws_s3_bucket_ownership_controls" "repo_source_files" { 60 | bucket = aws_s3_bucket.repo_source_files.id 61 | 62 | rule { 63 | object_ownership = "BucketOwnerPreferred" 64 | } 65 | } 66 | 67 | # Set bucket ACL to private 68 | resource "aws_s3_bucket_acl" "repo_source_files" { 69 | depends_on = [ 70 | aws_s3_bucket_ownership_controls.repo_source_files, 71 | aws_s3_bucket_public_access_block.repo_source_files, 72 | ] 73 | 74 | bucket = aws_s3_bucket.repo_source_files.id 75 | acl = "private" 76 | } 77 | 78 | # Enable bucket versioning 79 | resource "aws_s3_bucket_versioning" "repo_source_files" { 80 | bucket = aws_s3_bucket.repo_source_files.id 81 | 82 | versioning_configuration { 83 | status = "Enabled" 84 | } 85 | } 86 | 87 | data "aws_kms_alias" "s3" { 88 | name = local.kms_key_alias 89 | } 90 | 91 | # Encrypt bucket 92 | resource "aws_s3_bucket_server_side_encryption_configuration" "repo_source_files" { 93 | bucket = aws_s3_bucket.repo_source_files.id 94 | 95 | rule { 96 | apply_server_side_encryption_by_default { 97 | kms_master_key_id = data.aws_kms_alias.s3.target_key_arn 98 | sse_algorithm = "aws:kms" 99 | } 100 | } 101 | } 102 | 103 | #-------------------------------------# 104 | # IAM Roles and Policies for CodeBuild 105 | #-------------------------------------# 106 | data "aws_iam_policy_document" "codebuild_assume_role_policy" { 107 | statement { 108 | actions = ["sts:AssumeRole"] 109 | principals { 110 | type = "Service" 111 | identifiers = ["codebuild.amazonaws.com"] 112 | } 113 | } 114 | } 115 | 116 | data "aws_iam_policy_document" "codebuild_cloudwatch_plan" { 117 | statement { 118 | effect = "Allow" 119 | actions = [ 120 | "logs:CreateLogGroup", 121 | "logs:CreateLogStream", 122 | "logs:PutLogEvents" 123 | ] 124 | resources = [ 125 | "arn:aws:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:log-group:/aws/codebuild/${local.codebuild_project_name_plan}:*", 126 | ] 127 | } 128 | } 129 | 130 | resource "aws_iam_role" "codebuild_plan_role" { 131 | name = local.iam_role_name_codebuild_plan 132 | assume_role_policy = data.aws_iam_policy_document.codebuild_assume_role_policy.json 133 | } 134 | 135 | resource "aws_iam_role_policy_attachment" "plan_policy" { 136 | role = aws_iam_role.codebuild_plan_role.name 137 | policy_arn = local.iam_policy_plan_arn 138 | } 139 | 140 | resource "aws_iam_policy" "plan_cloudwatch_policy" { 141 | name = local.iam_role_name_codebuild_plan 142 | policy = data.aws_iam_policy_document.codebuild_cloudwatch_plan.json 143 | } 144 | 145 | resource "aws_iam_role_policy_attachment" "plan_cloudwatch_policy" { 146 | role = aws_iam_role.codebuild_plan_role.name 147 | policy_arn = aws_iam_policy.plan_cloudwatch_policy.arn 148 | } 149 | 150 | # Attach the state lock table access policy 151 | resource "aws_iam_role_policy_attachment" "plan_state_lock_policy" { 152 | role = aws_iam_role.codebuild_plan_role.name 153 | policy_arn = var.state_file_iam_policy_arn 154 | } 155 | 156 | data "aws_iam_policy_document" "codebuild_cloudwatch_apply" { 157 | statement { 158 | effect = "Allow" 159 | actions = [ 160 | "logs:CreateLogGroup", 161 | "logs:CreateLogStream", 162 | "logs:PutLogEvents" 163 | ] 164 | resources = [ 165 | "arn:aws:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:log-group:/aws/codebuild/${local.codebuild_project_name_apply}:*", 166 | ] 167 | } 168 | } 169 | 170 | resource "aws_iam_role" "codebuild_apply_role" { 171 | name = local.iam_role_name_codebuild_apply 172 | assume_role_policy = data.aws_iam_policy_document.codebuild_assume_role_policy.json 173 | } 174 | 175 | resource "aws_iam_role_policy_attachment" "apply_policy" { 176 | role = aws_iam_role.codebuild_apply_role.name 177 | policy_arn = local.iam_policy_apply_arn 178 | } 179 | 180 | resource "aws_iam_policy" "apply_cloudwatch_policy" { 181 | name = local.iam_role_name_codebuild_apply 182 | policy = data.aws_iam_policy_document.codebuild_cloudwatch_apply.json 183 | } 184 | 185 | resource "aws_iam_role_policy_attachment" "apply_cloudwatch_policy" { 186 | role = aws_iam_role.codebuild_apply_role.name 187 | policy_arn = aws_iam_policy.apply_cloudwatch_policy.arn 188 | } 189 | 190 | # Attach the state lock table access policy 191 | resource "aws_iam_role_policy_attachment" "apply_state_lock_policy" { 192 | role = aws_iam_role.codebuild_apply_role.name 193 | policy_arn = var.state_file_iam_policy_arn 194 | } 195 | 196 | #----------------------------------------# 197 | # CodeBuild projects and buildspec files 198 | #----------------------------------------# 199 | resource "aws_codebuild_project" "terraform_plan" { 200 | name = local.codebuild_project_name_plan 201 | service_role = aws_iam_role.codebuild_plan_role.arn 202 | 203 | artifacts { 204 | type = "NO_ARTIFACTS" 205 | } 206 | 207 | environment { 208 | compute_type = local.codebuild_compute_type 209 | image = local.codebuild_image_uri 210 | type = "LINUX_CONTAINER" 211 | privileged_mode = false 212 | 213 | environment_variable { 214 | name = local.github_token_env_var_name 215 | value = local.aws_ssm_name_github_token 216 | type = "PARAMETER_STORE" 217 | } 218 | 219 | environment_variable { 220 | name = "TF_WORKING_DIR" 221 | value = local.terraform_source_dir 222 | } 223 | 224 | environment_variable { 225 | name = "GH_ORG_REPO" 226 | value = "${var.github_organization}/${var.github_repository}" 227 | } 228 | } 229 | 230 | source { 231 | type = "GITHUB" 232 | location = "https://github.com/${var.github_organization}/${var.github_repository}" 233 | report_build_status = true 234 | 235 | buildspec = templatefile("${path.module}/templates/${local.codebuild_spec_template_filename}", 236 | { 237 | is_pr_buildspec = true 238 | terraform_source_dir = local.terraform_source_dir 239 | repository_default_branch_name = local.repository_default_branch_name 240 | } 241 | ) 242 | } 243 | 244 | cache { 245 | type = "S3" 246 | location = aws_s3_bucket.repo_source_files.bucket 247 | } 248 | 249 | encryption_key = data.aws_kms_alias.s3.target_key_arn 250 | } 251 | 252 | resource "aws_codebuild_project" "terraform_apply" { 253 | name = local.codebuild_project_name_apply 254 | service_role = aws_iam_role.codebuild_apply_role.arn 255 | 256 | artifacts { 257 | type = "NO_ARTIFACTS" 258 | } 259 | 260 | environment { 261 | compute_type = local.codebuild_compute_type 262 | image = local.codebuild_image_uri 263 | type = "LINUX_CONTAINER" 264 | privileged_mode = false 265 | 266 | environment_variable { 267 | name = local.github_token_env_var_name 268 | value = local.aws_ssm_name_github_token 269 | type = "PARAMETER_STORE" 270 | } 271 | 272 | environment_variable { 273 | name = "TF_WORKING_DIR" 274 | value = local.terraform_source_dir 275 | } 276 | 277 | environment_variable { 278 | name = "GH_ORG_REPO" 279 | value = "${var.github_organization}/${var.github_repository}" 280 | } 281 | } 282 | 283 | source { 284 | type = "GITHUB" 285 | location = "https://github.com/${var.github_organization}/${var.github_repository}" 286 | report_build_status = true 287 | 288 | buildspec = templatefile("${path.module}/templates/${local.codebuild_spec_template_filename}", 289 | { 290 | is_pr_buildspec = false 291 | terraform_source_dir = local.terraform_source_dir 292 | repository_default_branch_name = local.repository_default_branch_name 293 | } 294 | ) 295 | } 296 | 297 | cache { 298 | type = "S3" 299 | location = aws_s3_bucket.repo_source_files.bucket 300 | } 301 | 302 | encryption_key = data.aws_kms_alias.s3.target_key_arn 303 | } 304 | 305 | #--------------------------------------# 306 | # Webhooks between CodeBuild and GitHub 307 | #--------------------------------------# 308 | 309 | resource "aws_codebuild_webhook" "pr_webhook" { 310 | project_name = aws_codebuild_project.terraform_plan.name 311 | 312 | filter_group { 313 | filter { 314 | type = "EVENT" 315 | pattern = "PULL_REQUEST_CREATED,PULL_REQUEST_UPDATED,PULL_REQUEST_REOPENED" 316 | } 317 | 318 | filter { 319 | type = "BASE_REF" 320 | pattern = local.repository_default_branch_name 321 | } 322 | } 323 | } 324 | 325 | resource "aws_codebuild_webhook" "main_branch_webhook" { 326 | project_name = aws_codebuild_project.terraform_apply.name 327 | 328 | filter_group { 329 | filter { 330 | type = "EVENT" 331 | pattern = "PUSH" 332 | } 333 | 334 | filter { 335 | type = "HEAD_REF" 336 | pattern = "refs/heads/${local.repository_default_branch_name}" 337 | } 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /modules/bootstrap-cicd-aws-codebuild/outputs.tf: -------------------------------------------------------------------------------- 1 | output "codebuild_project_tf_plan" { 2 | value = resource.aws_codebuild_project.terraform_plan 3 | } 4 | 5 | output "codebuild_project_tf_apply" { 6 | value = resource.aws_codebuild_project.terraform_apply 7 | } -------------------------------------------------------------------------------- /modules/bootstrap-cicd-aws-codebuild/templates/buildspec_terraform.yml.tmpl: -------------------------------------------------------------------------------- 1 | # Managed by the build-on-aws/terraform-samples/modules/bootstrap-cicd-aws-codebuild 2 | # module. While changes won't be overwritten unless you run it locally and commit, 3 | # it is recommended not to make changes to this file directly. 4 | 5 | version: 0.2 6 | 7 | phases: 8 | install: 9 | commands: 10 | - apk update && apk add unzip curl jq 11 | %{ if is_pr_buildspec } 12 | - apk add github-cli 13 | %{ endif } 14 | pre_build: 15 | commands: 16 | # Check if the commit valid for this build 17 | - | 18 | echo "CODEBUILD_SOURCE_VERSION: $CODEBUILD_SOURCE_VERSION" 19 | echo "CODEBUILD_INITIATOR: $CODEBUILD_INITIATOR" 20 | export PR_NUMBER=$(echo "$CODEBUILD_SOURCE_VERSION" | cut -d'/' -f2) 21 | echo "PR_NUMBER: $PR_NUMBER" 22 | echo "GH_ORG_REPO: $GH_ORG_REPO" 23 | 24 | # Get the commit hash that triggered the build 25 | - | 26 | GIT_COMMIT=$(git rev-parse HEAD) 27 | echo "Commit hash: $GIT_COMMIT" 28 | 29 | # Fetch the GitHub username of the person who made the commit using GitHub API 30 | - | 31 | echo "Fetching GitHub username for commit $GIT_COMMIT in $GH_ORG_REPO" 32 | GITHUB_USER=$(curl -H "Authorization: token $GITHUB_TOKEN" \ 33 | -H "Accept: application/vnd.github.v3+json" \ 34 | https://api.github.com/repos/$${GH_ORG_REPO}/commits/$${GIT_COMMIT} \ 35 | | jq -r '.author.login') 36 | echo "Commit made by GitHub user: $GITHUB_USER" 37 | 38 | %{ if is_pr_buildspec } 39 | if [[ "$${CODEBUILD_SOURCE_VERSION}" == pr/* ]]; then 40 | echo "Detected a PR, only doing a plan" 41 | else 42 | echo "Detected a push to main branch, this should not happen, failing the build." 43 | exit 1 44 | fi 45 | %{ else} 46 | if [[ "$${CODEBUILD_SOURCE_VERSION}" != pr/* ]]; then 47 | echo "Detected a commit to the main branch, applying changes" 48 | else 49 | echo "Detected a PR, this should not happen, failing the build." 50 | exit 1 51 | fi 52 | %{ endif} 53 | 54 | build: 55 | commands: 56 | - cd $${TF_WORKING_DIR} 57 | - | 58 | terraform init -no-color 2> init_error.log 59 | export INIT_EXIT_CODE=$? 60 | 61 | terraform fmt -check -no-color 2> fmt_error.log 62 | export FMT_EXIT_CODE=$? 63 | 64 | terraform validate -no-color 2> validate_error.log 65 | export VALIDATE_EXIT_CODE=$? 66 | 67 | # Store the plan in tfplan, and any errors in plan_errors.log 68 | terraform plan -no-color -out=tfplan 2> plan_error.log 69 | export PLAN_EXIT_CODE=$? 70 | 71 | # Print any errors 72 | - | 73 | if [ "$INIT_EXIT_CODE" -ne 0 ] || [ "$FMT_EXIT_CODE" -ne 0 ] || [ "$VALIDATE_EXIT_CODE" -ne 0 ] || [ "$PLAN_EXIT_CODE" -ne 0 ]; then 74 | # Print the error to CodeBuild logs if init failed 75 | if [ "$INIT_EXIT_CODE" -ne 0 ]; then 76 | echo "💥💥💥 TERRAFORM INIT FAILED!!! 💥💥💥\n" 77 | echo $(cat init_error.log) 78 | echo "#--------------------------------#\n" 79 | fi 80 | 81 | # Print the error to CodeBuild logs if fmt failed 82 | if [ "$FMT_EXIT_CODE" -ne 0 ]; then 83 | echo "💥💥💥 TERRAFORM FMT FAILED!!! 💥💥💥\n" 84 | echo $(cat fmt_error.log) 85 | echo "#--------------------------------#\n" 86 | fi 87 | 88 | # Print the error to CodeBuild logs if validate failed 89 | if [ "$VALIDATE_EXIT_CODE" -ne 0 ]; then 90 | echo "💥💥💥 TERRAFORM VALIDATE FAILED!!! 💥💥💥\n" 91 | echo $(cat validate_error.log) 92 | echo "#--------------------------------#\n" 93 | fi 94 | 95 | # Print the error to CodeBuild logs if the plan failed 96 | if [ "$PLAN_EXIT_CODE" -ne 0 ]; then 97 | echo "💥💥💥 TERRAFORM PLAN FAILED!!! 💥💥💥\n" 98 | echo $(cat plan_error.log) 99 | echo "#--------------------------------#\n" 100 | fi 101 | fi 102 | %{ if is_pr_buildspec } 103 | # Create the build summary and comment on the PR 104 | - | 105 | echo "|Step|Status|" > build_job_pr_comment.md 106 | echo "|:---|:---|" >> build_job_pr_comment.md 107 | echo "|🖌 - Format and Style|$([ "$${FMT_EXIT_CODE:-0}" -eq 0 ] && echo "Success" || echo "Failed")|" >> build_job_pr_comment.md 108 | echo "|⚙️ - Initialization|$([ "$${INIT_EXIT_CODE:-0}" -eq 0 ] && echo "Success" || echo "Failed")|" >> build_job_pr_comment.md 109 | echo "|🤖 - Validation|$([ "$${VALIDATION_EXIT_CODE:-0}" -eq 0 ] && echo "Success" || echo "Failed")|" >> build_job_pr_comment.md 110 | echo "|📖 - Plan|$([ "$${PLAN_EXIT_CODE:-0}" -eq 0 ] && echo "Success" || echo "Failed")|" >> build_job_pr_comment.md 111 | echo "" >> build_job_pr_comment.md 112 | 113 | if [ "$PLAN_EXIT_CODE" -ne 0 ]; then 114 | TERRAFORM_PLAN_HEADER="Show Errors" 115 | TERRAFORM_PLAN_OUTPUT=$(cat plan_error.log) 116 | else 117 | TERRAFORM_PLAN_HEADER="Show Plan" 118 | TERRAFORM_PLAN_OUTPUT=$(terraform show -no-color tfplan) 119 | fi 120 | 121 | echo "## Terraform Plan" >> build_job_pr_comment.md 122 | echo "
$${TERRAFORM_PLAN_HEADER}" >> build_job_pr_comment.md 123 | echo "" >> build_job_pr_comment.md 124 | echo '```' >> build_job_pr_comment.md 125 | echo "$TERRAFORM_PLAN_OUTPUT" >> build_job_pr_comment.md 126 | echo '```' >> build_job_pr_comment.md 127 | echo "
" >> build_job_pr_comment.md 128 | 129 | echo "" >> build_job_pr_comment.md 130 | 131 | echo "*Author: @$${GITHUB_USER}, Action: $${CODEBUILD_SOURCE_VERSION}*" >> build_job_pr_comment.md 132 | 133 | BUILD_SUMMARY=$(cat build_job_pr_comment.md) 134 | 135 | # Post the comment to the PR using gh CLI 136 | gh pr comment --repo $${GH_ORG_REPO} $${PR_NUMBER} --body "$BUILD_SUMMARY" 137 | %{ endif } 138 | # Ensure job fails if there was an error 139 | - | 140 | if [ "$INIT_EXIT_CODE" -ne 0 ] || [ "$FMT_EXIT_CODE" -ne 0 ] || [ "$VALIDATE_EXIT_CODE" -ne 0 ] || [ "$PLAN_EXIT_CODE" -ne 0 ]; then 141 | exit 1 142 | fi 143 | %{ if !is_pr_buildspec } 144 | # Now apply the changes 145 | - | 146 | terraform apply -auto-approve 2> apply_error.log 147 | export APPLY_EXIT_CODE=$? 148 | 149 | # Fail the job if apply failed and print the error 150 | - | 151 | if [ "$PLAN_EXIT_CODE" -ne 0 ]; then 152 | echo "💥💥💥 TERRAFORM APPLY FAILED!!! 💥💥💥" 153 | echo "" 154 | echo $(cat apply_error.log) 155 | echo "" 156 | exit 1 157 | fi 158 | %{ endif } -------------------------------------------------------------------------------- /modules/bootstrap-cicd-aws-codebuild/variables.tf: -------------------------------------------------------------------------------- 1 | variable "github_organization" { 2 | description = "(Required) Name of the GitHub organization" 3 | type = string 4 | } 5 | 6 | variable "github_repository" { 7 | description = "(Required) The name of the GitHub repository to use" 8 | type = string 9 | } 10 | 11 | variable "aws_region" { 12 | description = "(Required) AWS region to use" 13 | type = string 14 | } 15 | 16 | variable "codebuild_terraform_version" { 17 | description = "(Required) Version of terraform to use in CodeBuild, only supply the version number, e.g. \"1.9.7\"" 18 | type = string 19 | } 20 | 21 | variable "state_file_iam_policy_arn" { 22 | description = "(Required) ARN of the IAM policy that allows reading & writing to the state file S3 bucket" 23 | type = string 24 | } 25 | 26 | variable "override_repo_source_files_bucket_name" { 27 | description = "Override the S3 bucket name for the repo source file, defaults to src__" 28 | type = string 29 | default = null 30 | } 31 | 32 | variable "override_kms_key_alias" { 33 | description = "Override KMS key alias to use for state file encryption, defaults to alias/kms/s3" 34 | type = string 35 | default = null 36 | } 37 | 38 | variable "override_terraform_build_image_uri" { 39 | description = "Override the Docker image URI for the CodeBuild project for terraform build, defaults to public.ecr.aws/hashicorp/terraform:" 40 | type = string 41 | default = null 42 | } 43 | 44 | variable "override_repository_default_branch_name" { 45 | description = "Override the default branch name, defaults to main" 46 | type = string 47 | default = null 48 | } 49 | 50 | variable "override_terraform_source_dir" { 51 | description = "Override the directory in the repo where the terraform code is, defaults to terraform/ - please include trailing slash in override" 52 | type = string 53 | default = null 54 | } 55 | 56 | variable "override_iam_role_name_codebuild_apply" { 57 | description = "Override the IAM role name used by CodeBuild for terraform apply, defaults to gh--tf-apply" 58 | type = string 59 | default = null 60 | } 61 | 62 | variable "override_iam_policy_apply_arn" { 63 | description = "Override the IAM policy ARN used by the CodeBuild for terraform apply, defaults to built-in policy/AdministratorAccess" 64 | type = string 65 | default = null 66 | } 67 | 68 | variable "override_iam_role_name_codebuild_plan" { 69 | description = "Override the IAM role name used by the CodeBuild for terraform plan, defaults to gh--tf-plan" 70 | type = string 71 | default = null 72 | } 73 | 74 | variable "override_iam_policy_plan_arn" { 75 | description = "Override the IAM policy ARN used by the CodeBuild for terraform plan, defaults to built-in policy/ReadOnlyAccess" 76 | type = string 77 | default = null 78 | } 79 | 80 | variable "override_aws_ssm_name_github_token" { 81 | description = "Name of the SSM parameter to store the GitHub token, defaults to /cicd/github_token" 82 | type = string 83 | default = null 84 | } 85 | 86 | variable "override_aws_tags" { 87 | description = "Override tags to apply to AWS resources" 88 | type = map(string) 89 | default = null 90 | } -------------------------------------------------------------------------------- /modules/bootstrap-cicd-aws-codebuild/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0" # Or use e.g. ">= 1.9.7" to be more specific 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = ">= 5.70.0" 8 | } 9 | local = { 10 | source = "hashicorp/local" 11 | version = ">= 2.5.2" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /modules/bootstrap-cicd-github-actions/LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /modules/bootstrap-cicd-github-actions/README.md: -------------------------------------------------------------------------------- 1 | ## bootstrap-cicd-github-actions 2 | 3 | Utility module to bootstrap creating a CI/CD pipeline for Terraform using GitHub Actions to provision AWS resources. Generates the Workflow file, and sets the variables / secrets on the workflow to allow running `terraform plan` on pull requests, and `terraform apply` on merges / commits to the `main` branch. IAM Role are limited to the type of trigger, PRs can only assume the IAM role with read-only access, and the `main` branch workflow can only assume the admin-level IAM role to create the infrastructure. 4 | 5 | ## License 6 | 7 | This library is licensed under the MIT-0 License. See the LICENSE file. 8 | -------------------------------------------------------------------------------- /modules/bootstrap-cicd-github-actions/main.tf: -------------------------------------------------------------------------------- 1 | #--------------------------------------------# 2 | # Using locals instead of hard-coding strings 3 | #--------------------------------------------# 4 | locals { 5 | terraform_source_dir = coalesce(var.override_terraform_source_dir, "terraform/") 6 | repository_default_branch_name = coalesce(var.override_repository_default_branch_name, "main") 7 | github_provider_version = coalesce(var.override_github_provider_version, "6.0") 8 | iam_role_name_apply = coalesce(var.override_iam_role_name_apply, "gh-tf-apply-${substr(var.github_repository, 0, 64 - length("gh-tf-apply-"))}") 9 | iam_role_name_plan = coalesce(var.override_iam_role_name_plan, "gh-tf-plan-${substr(var.github_repository, 0, 64 - length("gh-tf-apply-"))}") 10 | iam_policy_apply = coalesce(var.override_iam_policy_apply_arn, "arn:aws:iam::aws:policy/AdministratorAccess") 11 | iam_policy_plan = coalesce(var.override_iam_policy_plan_arn, "arn:aws:iam::aws:policy/ReadOnlyAccess") 12 | aws_ssm_name_github_token = coalesce(var.override_aws_ssm_name_github_token, "/cicd/github_token") 13 | github_terraform_workflow_file = coalesce(var.override_github_terraform_workflow_filename, "terraform.yml") 14 | github_env_var_name_iam_role_plan_arn = "AWS_IAM_ROLE_PLAN" 15 | github_env_var_name_iam_role_apply_arn = "AWS_IAM_ROLE_APPLY" 16 | github_env_var_name_aws_region = "AWS_REGION" 17 | github_env_var_name_terraform_version = "TF_VERSION" 18 | github_env_var_name_github_token = "GH_TOKEN" 19 | 20 | aws_tags = { 21 | GitHubRepo = "${var.github_organization}/${var.github_repository}" 22 | Module = "build-on-aws/terraform-samples/modules/bootstrap-cicd-github-actions" 23 | } 24 | 25 | # https://github.blog/changelog/2023-06-27-github-actions-update-on-oidc-integration-with-aws/ 26 | github_cert_thumbprint = [ 27 | "6938fd4d98bab03faadb97b34396831e3780aea1", 28 | "1c58a3a8518e8759bf075b76b750d4f2df264fcd" 29 | ] 30 | 31 | github_provider = [ 32 | { 33 | name = "github" 34 | provider_source = "integrations/github" 35 | provider_version = local.github_provider_version 36 | } 37 | ] 38 | } 39 | 40 | # Create the GitHub provider to use the GitHub token retrieved from SSM 41 | resource "local_file" "tf_github_provider" { 42 | filename = "${path.root}/provider-github.tf" 43 | directory_permission = "0666" 44 | file_permission = "0666" 45 | content = templatefile("${path.module}/templates/provider-github.tf.tmpl", { 46 | github_organization = var.github_organization 47 | }) 48 | } 49 | 50 | # # Set up access from GitHub into the account. The thumbprint for GitHub 51 | # # certificate can be used from the post 52 | # # https://github.blog/changelog/2022-01-13-github-actions-update-on-oidc-based-deployments-to-aws/ 53 | # # or generated. 54 | resource "aws_iam_openid_connect_provider" "github" { 55 | url = "https://token.actions.githubusercontent.com" 56 | 57 | thumbprint_list = local.github_cert_thumbprint 58 | tags = local.aws_tags 59 | client_id_list = ["sts.amazonaws.com"] 60 | } 61 | 62 | #------------------------------------------------------------# 63 | # IAM Role used to apply changes. 64 | # Defaults to policy/AdministratorAccess, 65 | # but can be overridden to a custom policy 66 | # by setting var.override_iam_policy_administrator_access_arn 67 | #------------------------------------------------------------# 68 | 69 | data "aws_iam_policy_document" "github_actions_write_assume_role_policy" { 70 | statement { 71 | actions = ["sts:AssumeRoleWithWebIdentity"] 72 | principals { 73 | type = "Federated" 74 | identifiers = [aws_iam_openid_connect_provider.github.arn] 75 | } 76 | 77 | # Condition to limit to default AWS OIDC audience 78 | # see: https://github.com/aws-actions/configure-aws-credentials?tab=readme-ov-file#oidc-audience 79 | condition { 80 | test = "StringEquals" 81 | variable = "token.actions.githubusercontent.com:aud" 82 | values = ["sts.amazonaws.com"] 83 | } 84 | 85 | # Condition to limit to commits to the main branch 86 | condition { 87 | test = "StringEquals" 88 | variable = "token.actions.githubusercontent.com:sub" 89 | values = [ 90 | "repo:${var.github_organization}/${var.github_repository}:ref:refs/heads/${local.repository_default_branch_name}" 91 | ] 92 | } 93 | } 94 | } 95 | 96 | # Role to allow GitHub actions to use this AWS account 97 | resource "aws_iam_role" "github_actions_apply" { 98 | name = local.iam_role_name_apply 99 | assume_role_policy = data.aws_iam_policy_document.github_actions_write_assume_role_policy.json 100 | tags = local.aws_tags 101 | } 102 | 103 | # Allow GitHub actions to create infrastructure 104 | resource "aws_iam_role_policy_attachment" "github_actions_apply_policy" { 105 | role = aws_iam_role.github_actions_apply.name 106 | policy_arn = local.iam_policy_apply 107 | } 108 | 109 | # Attach the state lock table access policy 110 | resource "aws_iam_role_policy_attachment" "github_actions_apply_state_lock_policy" { 111 | role = aws_iam_role.github_actions_apply.name 112 | policy_arn = var.state_file_iam_policy_arn 113 | } 114 | 115 | #------------------------------------------------------------# 116 | # IAM Role used to plan changes. 117 | # Defaults to policy/ReadOnly, 118 | # but can be overridden to a custom policy 119 | # by setting var.override_iam_policy_read_only_arn 120 | #------------------------------------------------------------# 121 | 122 | data "aws_iam_policy_document" "github_actions_read_assume_role_policy" { 123 | statement { 124 | actions = ["sts:AssumeRoleWithWebIdentity"] 125 | principals { 126 | type = "Federated" 127 | identifiers = [aws_iam_openid_connect_provider.github.arn] 128 | } 129 | 130 | # Condition to limit to default AWS OIDC audience 131 | # see: https://github.com/aws-actions/configure-aws-credentials?tab=readme-ov-file#oidc-audience 132 | condition { 133 | test = "StringEquals" 134 | variable = "token.actions.githubusercontent.com:aud" 135 | values = ["sts.amazonaws.com"] 136 | } 137 | 138 | # Condition to limit to pull requests 139 | condition { 140 | test = "StringEquals" 141 | variable = "token.actions.githubusercontent.com:sub" 142 | values = [ 143 | "repo:${var.github_organization}/${var.github_repository}:pull_request", 144 | "repo:${var.github_organization}/${var.github_repository}:ref/pull/*", 145 | "repo:${var.github_organization}/${var.github_repository}:ref:refs/heads/${local.repository_default_branch_name}" 146 | ] 147 | } 148 | # # Condition to limit to pull requests targeting 'main' branch 149 | # condition { 150 | # test = "StringEquals" 151 | # variable = "token.actions.githubusercontent.com:ref" 152 | # values = [ 153 | # "refs/heads/${var.repository_default_branch_name}" # Only allow for PRs targeting the 'main' branch 154 | # ] 155 | # } 156 | } 157 | } 158 | 159 | # Role to allow GitHub actions to use this AWS account to run terraform plan 160 | resource "aws_iam_role" "github_actions_plan" { 161 | name = local.iam_role_name_plan 162 | assume_role_policy = data.aws_iam_policy_document.github_actions_read_assume_role_policy.json 163 | tags = local.aws_tags 164 | } 165 | 166 | # Allow GitHub actions to create infrastructure 167 | resource "aws_iam_role_policy_attachment" "github_actions_plan_policy" { 168 | role = aws_iam_role.github_actions_plan.name 169 | policy_arn = local.iam_policy_plan 170 | } 171 | 172 | # Attach the state lock table access policy 173 | resource "aws_iam_role_policy_attachment" "github_actions_plan_state_lock_policy" { 174 | role = aws_iam_role.github_actions_plan.name 175 | policy_arn = var.state_file_iam_policy_arn 176 | } 177 | 178 | #---------------------------------------------------------# 179 | # Create the GitHub Actions workflow file in the code repo 180 | #---------------------------------------------------------# 181 | 182 | resource "local_file" "github_actions_cicd_workflow" { 183 | filename = "${path.root}/../.github/workflows/${local.github_terraform_workflow_file}" 184 | content = templatefile("${path.module}/templates/github_actions_workflow.yml.tmpl", { 185 | terraform_source_dir = local.terraform_source_dir 186 | aws_region = var.aws_region 187 | github_organization = var.github_organization 188 | github_repository = var.github_repository 189 | }) 190 | } 191 | 192 | data "aws_ssm_parameter" "github_token" { 193 | name = local.aws_ssm_name_github_token 194 | } 195 | 196 | resource "github_actions_secret" "github_cicd_token" { 197 | repository = var.github_repository 198 | secret_name = local.github_env_var_name_github_token 199 | 200 | # You can replace this with encrypted_value - this requires 201 | # encrypting the value and storing the encrypted string in SSM, 202 | # see https://docs.github.com/en/rest/guides/encrypting-secrets-for-the-rest-api 203 | plaintext_value = data.aws_ssm_parameter.github_token.value 204 | } 205 | 206 | resource "github_actions_variable" "tf_version" { 207 | repository = var.github_repository 208 | variable_name = local.github_env_var_name_terraform_version 209 | value = var.github_actions_terraform_version 210 | } 211 | 212 | resource "github_actions_secret" "iam_policy_apply_changes_name" { 213 | repository = var.github_repository 214 | secret_name = local.github_env_var_name_iam_role_apply_arn 215 | plaintext_value = aws_iam_role.github_actions_apply.arn 216 | } 217 | 218 | resource "github_actions_secret" "iam_role_plan_changes_name" { 219 | repository = var.github_repository 220 | secret_name = local.github_env_var_name_iam_role_plan_arn 221 | plaintext_value = aws_iam_role.github_actions_plan.arn 222 | } 223 | 224 | resource "github_actions_variable" "aws_region" { 225 | repository = var.github_repository 226 | variable_name = local.github_env_var_name_aws_region 227 | value = var.aws_region 228 | } 229 | 230 | -------------------------------------------------------------------------------- /modules/bootstrap-cicd-github-actions/outputs.tf: -------------------------------------------------------------------------------- 1 | output "aws_github_oidc_arn" { 2 | value = aws_iam_openid_connect_provider.github.arn 3 | } 4 | 5 | output "github_organization" { 6 | value = var.github_organization 7 | } 8 | 9 | output "aws_ssm_name_github_token" { 10 | value = local.aws_ssm_name_github_token 11 | } 12 | 13 | output "github_provider" { 14 | value = local.github_provider 15 | } -------------------------------------------------------------------------------- /modules/bootstrap-cicd-github-actions/providers.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = ">= 5.70.0" 8 | } 9 | github = { 10 | source = "integrations/github" 11 | version = "~> 6.0" 12 | } 13 | local = { 14 | source = "hashicorp/local" 15 | version = ">= 2.5.2" 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /modules/bootstrap-cicd-github-actions/templates/github_actions_workflow.yml.tmpl: -------------------------------------------------------------------------------- 1 | # Managed by the build-on-aws/terraform-samples/modules/bootstrap-cicd-github-actions 2 | # module. While changes won't be overwritten unless you run it locally and commit, 3 | # it is recommended not to make changes to this file directly. 4 | name: "Terraform Workflows" 5 | run-name: $${{ github.actor }} running on a PR or merge 🚀 6 | 7 | on: 8 | push: 9 | branches: 10 | - main # When PRs are merged, run terraform apply 11 | paths: 12 | - '${terraform_source_dir}**' # Only trigger on changes within the 'terraform/' folder, remove to trigger on every build 13 | 14 | pull_request: 15 | branches: 16 | - main # For new / updated PRs, only run terraform plan 17 | paths: 18 | - '${terraform_source_dir}**' # Only trigger on changes within the 'terraform/' folder, remove to trigger on every build 19 | 20 | permissions: 21 | id-token: write # This is required for requesting the JWT to interact with AWS 22 | contents: read # This is required for actions/checkout 23 | 24 | jobs: 25 | terraform: 26 | runs-on: ubuntu-latest 27 | 28 | steps: 29 | # Check out the code 30 | - name: Checkout code 31 | uses: actions/checkout@v4 32 | 33 | # Set up Terraform for the pipeline 34 | - name: Set up Terraform 35 | uses: hashicorp/setup-terraform@v3 36 | with: 37 | terraform_version: $${{ env.TF_VERSION }} 38 | 39 | # Set up the AWS credentials for this repo 40 | - name: Configure AWS credentials PR (read-only) 41 | uses: aws-actions/configure-aws-credentials@v4 42 | with: 43 | role-to-assume: $${{ secrets.AWS_IAM_ROLE_PLAN }} 44 | aws-region: ${aws_region} 45 | 46 | # Initialize Terraform 47 | - name: Terraform init 48 | id: init 49 | env: 50 | GITHUB_TOKEN: $${{ secrets.GH_TOKEN }} 51 | run: terraform init -no-color 52 | working-directory: ${terraform_source_dir} 53 | continue-on-error: true 54 | 55 | # Ensure code is formatted correctly 56 | - name: Terraform fmt 57 | id: fmt 58 | run: terraform fmt -check -no-color 59 | working-directory: ${terraform_source_dir} 60 | continue-on-error: true 61 | 62 | # Validate the terraform code is valid 63 | - name: Terraform validate 64 | id: validate 65 | run: terraform validate -no-color 66 | working-directory: ${terraform_source_dir} 67 | continue-on-error: true 68 | 69 | # Generate the plan 70 | - name: Terraform plan 71 | id: plan 72 | env: 73 | GITHUB_TOKEN: $${{ secrets.GH_TOKEN }} 74 | run: | 75 | terraform plan -no-color -out=tfplan 2> error.log || export PLAN_EXIT_CODE=$? 76 | 77 | if [ $PLAN_EXIT_CODE -eq 1 ]; then 78 | TERRAFORM_ERRORS=$(cat error.log) 79 | 80 | # Ensure to use a string that won't occur in the output 81 | echo "plan_output<> $GITHUB_OUTPUT 82 | echo "## Terraform Plan" >> $GITHUB_OUTPUT 83 | echo "
Show Errors" >> $GITHUB_OUTPUT 84 | echo "" >> $GITHUB_OUTPUT 85 | echo '\`\`\`' >> $GITHUB_OUTPUT 86 | echo "$TERRAFORM_ERRORS" >> $GITHUB_OUTPUT 87 | echo '\`\`\`' >> $GITHUB_OUTPUT 88 | echo "
" >> $GITHUB_OUTPUT 89 | echo "ABCDEFGH" >> $GITHUB_OUTPUT 90 | exit 1 91 | else 92 | TERRAFORM_PLAN=$(terraform show -no-color tfplan) 93 | 94 | # Ensure to use a string that won't occur in the output 95 | echo "plan_output<> $GITHUB_OUTPUT 96 | echo "## Terraform Plan" >> $GITHUB_OUTPUT 97 | echo "
Show Plan" >> $GITHUB_OUTPUT 98 | echo "" >> $GITHUB_OUTPUT 99 | echo '\`\`\`' >> $GITHUB_OUTPUT 100 | echo "$TERRAFORM_PLAN" >> $GITHUB_OUTPUT 101 | echo '\`\`\`' >> $GITHUB_OUTPUT 102 | echo "
" >> $GITHUB_OUTPUT 103 | echo "ABCDEFGH" >> $GITHUB_OUTPUT 104 | 105 | exit 0 106 | fi 107 | working-directory: ${terraform_source_dir} 108 | continue-on-error: true 109 | 110 | # Write the status of prior steps as a comment on the PR 111 | - name: Update PR with plan output and build status 112 | uses: actions/github-script@v7 113 | if: github.event_name == 'pull_request' 114 | with: 115 | github-token: $${{ secrets.GH_TOKEN }} 116 | script: | 117 | const build_summary = `|Step|Status| 118 | |:---|:---| 119 | |🖌 - Format and Style|\`$${{ steps.fmt.outcome }}\`|" 120 | |⚙️ - Initialization|\`$${{ steps.init.outcome }}\`|" 121 | |🤖 - Validation|\`$${{ steps.validate.outcome }}\`|" 122 | |📖 - Plan|\`$${{ steps.plan.outcome }}\`|`; 123 | 124 | const plan_output = `$${{ steps.plan.outputs.plan_output }}`; 125 | 126 | const commit_details = `*Pusher: @$${{ github.actor }}, Action: \`$${{ github.event_name }}\`, Workflow: \`$${{ github.workflow }}\`*`; 127 | 128 | // Build the output message 129 | const output = `$${build_summary}\n\n$${plan_output}\n\n$${commit_details}`; 130 | 131 | github.rest.issues.createComment({ 132 | issue_number: context.issue.number, 133 | owner: context.repo.owner, 134 | repo: context.repo.repo, 135 | body: output 136 | }); 137 | 138 | # After posting the PR output / status as a comment, exit with failure if any step failed 139 | - name: Exit based on status of fmt, init, and validate 140 | if: steps.init.outcome == 'failure' || steps.fmt.outcome == 'failure' || steps.validate.outcome == 'failure' 141 | run: | 142 | echo Init: $${{ steps.init.outcome }} 143 | echo Format: $${{ steps.fmt.outcome }} 144 | echo Validate: $${{ steps.validate.outcome }} 145 | exit 1 146 | 147 | # Set up the AWS credentials to allow changes if this is on the main branch 148 | - name: Configure AWS credentials PR (read-only) 149 | if: github.ref == 'refs/heads/main' && github.event_name == 'push' 150 | uses: aws-actions/configure-aws-credentials@v4 151 | with: 152 | role-to-assume: $${{ secrets.AWS_IAM_ROLE_APPLY }} 153 | aws-region: ${aws_region} 154 | 155 | # Only apply if this is on the main branch (after merging) 156 | - name: Terraform Apply 157 | id: apply 158 | if: github.ref == 'refs/heads/main' && github.event_name == 'push' 159 | env: 160 | GITHUB_TOKEN: $${{ secrets.GH_TOKEN }} 161 | run: terraform apply -auto-approve -input=false 162 | working-directory: ${terraform_source_dir} -------------------------------------------------------------------------------- /modules/bootstrap-cicd-github-actions/templates/provider-github.tf.tmpl: -------------------------------------------------------------------------------- 1 | # Managed by the build-on-aws/terraform-samples/modules/bootstrap-cicd-github-actions 2 | # module. While changes won't be overwritten unless you run it locally and commit, 3 | # it is recommended not to make changes to this file directly. 4 | 5 | provider "github" { 6 | owner = "${github_organization}" 7 | } -------------------------------------------------------------------------------- /modules/bootstrap-cicd-github-actions/variables.tf: -------------------------------------------------------------------------------- 1 | variable "github_organization" { 2 | description = "(Required) Name of the GitHub organization" 3 | type = string 4 | } 5 | 6 | variable "github_repository" { 7 | description = "(Required) The name of the GitHub repository to use" 8 | type = string 9 | } 10 | 11 | variable "state_file_iam_policy_arn" { 12 | description = "(Required) ARN of IAM policy allowing access to the state file" 13 | type = string 14 | } 15 | 16 | variable "aws_region" { 17 | description = "(Required) AWS region to use" 18 | type = string 19 | } 20 | 21 | variable "github_actions_terraform_version" { 22 | description = "(Required) Version of terraform to use in GitHub Actions" 23 | type = string 24 | } 25 | 26 | variable "override_repository_default_branch_name" { 27 | description = "Override the default branch name, defaults to main" 28 | type = string 29 | default = null 30 | } 31 | 32 | variable "override_terraform_source_dir" { 33 | description = "Override the directory in the repo where the terraform code is, defaults to terraform/ - please include trailing slash in override" 34 | type = string 35 | default = null 36 | } 37 | 38 | variable "override_aws_github_token_ssm_name" { 39 | description = "Override name of the AWS SSM location for the GitHub Token, defaults to /cicd/github_token" 40 | type = string 41 | default = null 42 | } 43 | 44 | variable "override_iam_role_name_apply" { 45 | description = "Override the IAM role name used by the GitHub Actions workflows for terraform apply, defaults to gh-tf-apply-" 46 | type = string 47 | default = null 48 | } 49 | 50 | variable "override_iam_policy_apply_arn" { 51 | description = "Override the IAM policy ARN used by the GitHub Actions workflows for terraform apply, defaults to built-in policy/AdministratorAccess" 52 | type = string 53 | default = null 54 | } 55 | 56 | variable "override_iam_role_name_plan" { 57 | description = "Override the IAM role name used by the GitHub Actions workflows for terraform plan, defaults to gh-tf-plan-" 58 | type = string 59 | default = null 60 | } 61 | 62 | variable "override_iam_policy_plan_arn" { 63 | description = "Override the IAM policy ARN used by the GitHub Actions workflows for terraform plan, defaults to built-in policy/ReadOnlyAccess" 64 | type = string 65 | default = null 66 | } 67 | 68 | variable "override_github_terraform_workflow_filename" { 69 | description = "Override the GitHub Actions terraform workflow filename, defaults to terraform.yml" 70 | type = string 71 | default = null 72 | } 73 | 74 | variable "override_aws_ssm_name_github_token" { 75 | description = "Name of the SSM parameter to store the GitHub token, defaults to /cicd/github_token" 76 | type = string 77 | default = null 78 | } 79 | 80 | variable "override_github_provider_version" { 81 | description = "Version of the GitHub provider to use, defaults to 6.0" 82 | type = string 83 | default = null 84 | } 85 | 86 | variable "override_aws_tags" { 87 | description = "Override tags to apply to AWS resources" 88 | type = map(string) 89 | default = null 90 | } -------------------------------------------------------------------------------- /modules/bootstrap-cloudtrail/LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /modules/bootstrap-cloudtrail/README.md: -------------------------------------------------------------------------------- 1 | ## bootstrap-cloudtrail 2 | 3 | Sets up CloudTrail with the required S3 bucket to store the logs, and appropriate bucket policy to allow writing to that bucket. Creates it for all regions and accounts in the AWS Organization. 4 | 5 | ## License 6 | 7 | This library is licensed under the MIT-0 License. See the LICENSE file. 8 | -------------------------------------------------------------------------------- /modules/bootstrap-cloudtrail/main.tf: -------------------------------------------------------------------------------- 1 | #--------------------------------------------# 2 | # Using locals instead of hard-coding strings 3 | #--------------------------------------------# 4 | locals { 5 | tf_version = coalesce(var.override_tf_version, "1.9.7") 6 | 7 | aws_tags = coalesce(var.override_aws_tags, { 8 | Name = "tf-bootstrap", 9 | Module = "build-on-aws/terraform-samples/modules/bootstrap-cloudtrail", 10 | }) 11 | } 12 | 13 | #----------------------------------# 14 | # Retrieve account id and partition 15 | #----------------------------------# 16 | data "aws_caller_identity" "current" {} 17 | 18 | data "aws_partition" "current" {} 19 | 20 | data "aws_organizations_organization" "current" {} 21 | 22 | #------------------# 23 | # Set up CloudTrail 24 | #------------------# 25 | resource "aws_cloudtrail" "all_accounts" { 26 | depends_on = [aws_s3_bucket_policy.cloudtrail] 27 | 28 | name = var.cloudtrail_name 29 | s3_bucket_name = aws_s3_bucket.cloudtrail.id 30 | include_global_service_events = true 31 | is_multi_region_trail = true 32 | is_organization_trail = true 33 | tags = local.aws_tags 34 | } 35 | 36 | resource "aws_s3_bucket" "cloudtrail" { 37 | bucket = var.cloudtrail_bucket_name 38 | tags = local.aws_tags 39 | } 40 | 41 | data "aws_iam_policy_document" "cloudtrail_bucket" { 42 | statement { 43 | sid = "AWSCloudTrailAclCheck" 44 | effect = "Allow" 45 | 46 | principals { 47 | type = "Service" 48 | identifiers = ["cloudtrail.amazonaws.com"] 49 | } 50 | 51 | actions = ["s3:GetBucketAcl"] 52 | resources = [aws_s3_bucket.cloudtrail.arn] 53 | condition { 54 | test = "StringEquals" 55 | variable = "aws:SourceArn" 56 | values = ["arn:${data.aws_partition.current.partition}:cloudtrail:${var.aws_region}:${data.aws_caller_identity.current.account_id}:trail/${var.cloudtrail_name}"] 57 | } 58 | } 59 | 60 | statement { 61 | sid = "AWSCloudTrailWrite" 62 | effect = "Allow" 63 | 64 | principals { 65 | type = "Service" 66 | identifiers = ["cloudtrail.amazonaws.com"] 67 | } 68 | 69 | actions = ["s3:PutObject"] 70 | resources = ["${aws_s3_bucket.cloudtrail.arn}/AWSLogs/${data.aws_caller_identity.current.account_id}/*"] 71 | 72 | condition { 73 | test = "StringEquals" 74 | variable = "s3:x-amz-acl" 75 | values = ["bucket-owner-full-control"] 76 | } 77 | 78 | condition { 79 | test = "StringEquals" 80 | variable = "aws:SourceArn" 81 | values = ["arn:${data.aws_partition.current.partition}:cloudtrail:${var.aws_region}:${data.aws_caller_identity.current.account_id}:trail/${var.cloudtrail_name}"] 82 | } 83 | } 84 | 85 | statement { 86 | sid = "AWSCloudTrailOrgWrite" 87 | effect = "Allow" 88 | 89 | principals { 90 | type = "Service" 91 | identifiers = ["cloudtrail.amazonaws.com"] 92 | } 93 | 94 | actions = ["s3:PutObject"] 95 | resources = ["${aws_s3_bucket.cloudtrail.arn}/AWSLogs/${data.aws_organizations_organization.current.id}/*"] 96 | 97 | condition { 98 | test = "StringEquals" 99 | variable = "s3:x-amz-acl" 100 | values = ["bucket-owner-full-control"] 101 | } 102 | 103 | condition { 104 | test = "StringEquals" 105 | variable = "aws:SourceArn" 106 | values = [ 107 | "arn:${data.aws_partition.current.partition}:cloudtrail:${var.aws_region}:${data.aws_caller_identity.current.account_id}:trail/${var.cloudtrail_name}" 108 | ] 109 | } 110 | } 111 | } 112 | 113 | resource "aws_s3_bucket_policy" "cloudtrail" { 114 | bucket = aws_s3_bucket.cloudtrail.id 115 | policy = data.aws_iam_policy_document.cloudtrail_bucket.json 116 | } -------------------------------------------------------------------------------- /modules/bootstrap-cloudtrail/outputs.tf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/terraform-samples/a12a8a947352533a58e7e50f56933674b151573b/modules/bootstrap-cloudtrail/outputs.tf -------------------------------------------------------------------------------- /modules/bootstrap-cloudtrail/providers.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0" # Or use e.g. ">= 1.9.7" to be more specific 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = ">= 5.70.0" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /modules/bootstrap-cloudtrail/variables.tf: -------------------------------------------------------------------------------- 1 | variable "aws_region" { 2 | description = "(Required) Primary AWS region to create resources in" 3 | type = string 4 | } 5 | 6 | variable "cloudtrail_bucket_name" { 7 | description = "(Required) Name of the S3 bucket to store the CloudTrail logs in" 8 | type = string 9 | } 10 | 11 | variable "cloudtrail_name" { 12 | description = "(Required) Name of the CloudTrail trail" 13 | type = string 14 | } 15 | 16 | variable "override_tf_version" { 17 | description = "Override version of Terraform to use, defaults to 1.9.7 if not set" 18 | type = string 19 | default = null 20 | } 21 | 22 | variable "override_aws_tags" { 23 | description = "Override tags to apply to AWS resources" 24 | type = map(string) 25 | default = null 26 | } --------------------------------------------------------------------------------