├── .github ├── CODEOWNERS └── workflows │ ├── ghaw-tf-build-dev.yml │ ├── ghaw-tf-build-prod.yml │ ├── ghaw-tf-build-test.yml │ ├── ghaw-tf-destroy-dev.yml │ ├── ghaw-tf-destroy-prod.yml │ └── ghaw-tf-destroy-test.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bootstrap ├── accountsetup │ ├── awsiamsetup.tf │ ├── modules │ │ └── assumerole │ │ │ ├── main.tf │ │ │ ├── variables.tf │ │ │ └── versions.tf │ ├── provider.tf │ ├── terraform.tfvars │ └── variables.tf └── pipelinebuild │ ├── awsresources.tf │ ├── githubresources.tf │ ├── modules │ └── tfbootstrap │ │ ├── main.tf │ │ ├── sampleappIAM.tf │ │ ├── variables.tf │ │ └── versions.tf │ ├── provider.tf │ ├── terraform.tfvars │ └── variables.tf ├── build ├── appsrc │ ├── Dockerfile │ ├── Program.cs │ ├── appsettings.json │ ├── bin │ │ └── Release │ │ │ └── net8.0 │ │ │ └── appsettings.json │ ├── delete.me │ ├── mswebapp.csproj │ ├── mswebapp.sln │ └── obj │ │ ├── mswebapp.csproj.nuget.dgspec.json │ │ ├── mswebapp.csproj.nuget.g.props │ │ ├── mswebapp.csproj.nuget.g.targets │ │ └── project.assets.json ├── ecscluster.tf ├── ecsservice.tf ├── lzbase.tf ├── main.tf ├── network.tf ├── terraform.tfvars ├── variables.tf └── versions.tf ├── img ├── pipeline.png ├── sampleapp.png └── workflowlogic.png └── stage └── README.txt /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Codeowners will review pull requests. 2 | # Refer to: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners 3 | * @ @ 4 | -------------------------------------------------------------------------------- /.github/workflows/ghaw-tf-build-dev.yml: -------------------------------------------------------------------------------- 1 | name: "Review and deploy Development environment" 2 | run-name: ${{ github.actor}} is running a Terraform review and deploy to Dev 🚀 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | permissions: 8 | # id-token write required for OIDC, contents read required for actions/checkout 9 | id-token: write 10 | contents: read 11 | env: 12 | TF_LOG: INFO 13 | AWS_REGION: ${{ vars.TF_VAR_REGION }} 14 | AWS_BUCKET_NAME: ${{ secrets.TF_STATE_BUCKET_NAME }} 15 | AWS_BUCKET_KEY_NAME: ${{ secrets.TF_STATE_BUCKET_KEY }} 16 | 17 | jobs: 18 | release_to_dev: 19 | runs-on: ubuntu-latest 20 | defaults: 21 | run: 22 | working-directory: ./build 23 | environment: 24 | name: dev 25 | env: 26 | TF_VAR_Region: ${{ vars.TF_VAR_REGION }} 27 | TF_VAR_AZ01: ${{ vars.TF_VAR_AZ01 }} 28 | TF_VAR_AZ02: ${{ vars.TF_VAR_AZ02 }} 29 | TF_VAR_PublicIP: ${{ vars.TF_VAR_PUBLICIP }} 30 | TF_VAR_Prefix: ${{ vars.TF_VAR_PREFIX }} 31 | TF_VAR_SolTag: ${{ vars.TF_VAR_SOLTAG }} 32 | TF_VAR_GitHubRepo: ${{ vars.TF_VAR_GITHUBREPO }} 33 | TF_VAR_EnvCode: ${{ vars.TF_VAR_ENVCODE }} 34 | TF_VAR_EnvTag: ${{ vars.TF_VAR_ENVTAG }} 35 | TF_VAR_VPCCIDR: ${{ vars.TF_VAR_VPCCIDR }} 36 | TF_VAR_FQDN: ${{ vars.TF_VAR_FQDN }} 37 | TF_VAR_ECRRepo: ${{ vars.TF_VAR_ECRREPO }} 38 | TF_VAR_ImageTag: ${{ vars.TF_VAR_IMAGETAG }} 39 | steps: 40 | - name: Git checkout 41 | uses: actions/checkout@v4 42 | 43 | - name: Configure AWS credentials 44 | uses: aws-actions/configure-aws-credentials@v4 45 | with: 46 | aws-region: ${{ vars.TF_VAR_REGION }} 47 | role-to-assume: ${{ secrets.AWS_ROLE }} 48 | role-session-name: GitHub-Actions-OIDC-TERRAFORM 49 | 50 | - name: Terraform Setup 51 | uses: hashicorp/setup-terraform@v3 52 | with: 53 | terraform_version: 1.7.1 54 | 55 | - name: Terraform Format 56 | id: fmt 57 | run: terraform fmt -check -no-color 58 | continue-on-error: true 59 | 60 | - name: Terraform Init 61 | id: init 62 | run: terraform init -backend-config="bucket=${{ secrets.TF_STATE_BUCKET_NAME }}" -backend-config="key=${{ secrets.TF_STATE_BUCKET_KEY }}" -backend-config="region=${{ vars.TF_VAR_REGION }}" -backend-config="encrypt=true" -backend-config="dynamodb_table=${{ secrets.TF_STATE_DYNAMODB_TABLE }}" -input=false 63 | 64 | - name: Terraform Validate 65 | id: validate 66 | run: terraform validate -no-color 67 | 68 | - name: Terraform Plan 69 | id: plan 70 | run: terraform plan -no-color -input=false 71 | continue-on-error: true 72 | 73 | - name: Terraform Plan Status 74 | if: steps.plan.outcome == 'failure' 75 | run: exit 1 76 | 77 | - name: Terraform Apply 78 | id: base 79 | run: terraform apply -auto-approve -input=false 80 | 81 | - name: Check ECS image exists 82 | id: ecsimage 83 | run: | 84 | if aws ecr describe-images --repository-name ${{ vars.TF_VAR_ECRREPO }} --image-ids imageTag=${{ vars.TF_VAR_IMAGETAG }} >/dev/null 2>&1 85 | then 86 | echo "image_exists=true" >> "$GITHUB_ENV" 87 | else 88 | echo "image_exists=false" >> "$GITHUB_ENV" 89 | fi 90 | 91 | - name: Check for changes in ./appsrc 92 | uses: dorny/paths-filter@v3 93 | id: filter 94 | with: 95 | base: ${{ github.ref }} 96 | filters: | 97 | appsrc: 98 | - 'build/appsrc/**' 99 | 100 | - name: Replace placeholder in Program.cs 101 | if: steps.filter.outputs.appsrc == 'true' || env.image_exists == 'false' 102 | run: sed -i 's/\$Environment/${{ vars.TF_VAR_ENVTAG }}/g' ./appsrc/Program.cs 103 | 104 | # https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 105 | - name: Setup .NET 106 | if: steps.filter.outputs.appsrc == 'true' || env.image_exists == 'false' 107 | uses: actions/setup-dotnet@v4 108 | with: 109 | dotnet-version: 8.0.x 110 | 111 | - name: Build .NET 112 | if: steps.filter.outputs.appsrc == 'true' || env.image_exists == 'false' 113 | run: dotnet publish ./appsrc -c Release --no-restore --property:PublishDir=${{ github.workspace }}/build/appbuild 114 | 115 | - name: Login to Amazon ECR 116 | if: steps.filter.outputs.appsrc == 'true' || env.image_exists == 'false' 117 | id: login-ecr 118 | uses: aws-actions/amazon-ecr-login@v2 119 | 120 | - name: Build, tag, and push docker image to Amazon ECR 121 | if: steps.filter.outputs.appsrc == 'true' || env.image_exists == 'false' 122 | env: 123 | REGISTRY: ${{ steps.login-ecr.outputs.registry }} 124 | REPOSITORY: ${{ vars.TF_VAR_ECRREPO }} 125 | IMAGE_TAG: ${{ vars.TF_VAR_IMAGETAG }} 126 | run: | 127 | docker build -t $REGISTRY/$REPOSITORY:$IMAGE_TAG -f ./appsrc/Dockerfile ./appbuild 128 | docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG 129 | 130 | - name: Print Web App URL in Job Summary 131 | run: echo "${{ vars.TF_VAR_ENVTAG }} environment URL http://$(terraform output -raw mswebapp_alb_dns_name) :rocket:" >> $GITHUB_STEP_SUMMARY -------------------------------------------------------------------------------- /.github/workflows/ghaw-tf-build-prod.yml: -------------------------------------------------------------------------------- 1 | name: "Review and deploy Production environment" 2 | run-name: ${{ github.actor}} is running a Terraform review and deploy to Prod 🚀 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | permissions: 11 | # id-token write required for OIDC, contents read required for actions/checkout, pull-requests write required to comment on PR 12 | id-token: write 13 | contents: read 14 | pull-requests: write 15 | env: 16 | TF_LOG: INFO 17 | AWS_REGION: ${{ vars.TF_VAR_REGION }} 18 | AWS_BUCKET_NAME: ${{ secrets.TF_STATE_BUCKET_NAME }} 19 | AWS_BUCKET_KEY_NAME: ${{ secrets.TF_STATE_BUCKET_KEY }} 20 | 21 | jobs: 22 | release_to_prod: 23 | runs-on: ubuntu-latest 24 | defaults: 25 | run: 26 | working-directory: ./build 27 | environment: 28 | name: prod 29 | env: 30 | TF_VAR_Region: ${{ vars.TF_VAR_REGION }} 31 | TF_VAR_AZ01: ${{ vars.TF_VAR_AZ01 }} 32 | TF_VAR_AZ02: ${{ vars.TF_VAR_AZ02 }} 33 | TF_VAR_PublicIP: ${{ vars.TF_VAR_PUBLICIP }} 34 | TF_VAR_Prefix: ${{ vars.TF_VAR_PREFIX }} 35 | TF_VAR_SolTag: ${{ vars.TF_VAR_SOLTAG }} 36 | TF_VAR_GitHubRepo: ${{ vars.TF_VAR_GITHUBREPO }} 37 | TF_VAR_EnvCode: ${{ vars.TF_VAR_ENVCODE }} 38 | TF_VAR_EnvTag: ${{ vars.TF_VAR_ENVTAG }} 39 | TF_VAR_VPCCIDR: ${{ vars.TF_VAR_VPCCIDR }} 40 | TF_VAR_FQDN: ${{ vars.TF_VAR_FQDN }} 41 | TF_VAR_ECRRepo: ${{ vars.TF_VAR_ECRREPO }} 42 | TF_VAR_ImageTag: ${{ vars.TF_VAR_IMAGETAG }} 43 | steps: 44 | - name: Git checkout 45 | uses: actions/checkout@v4 46 | 47 | - name: Configure AWS credentials 48 | uses: aws-actions/configure-aws-credentials@v4 49 | with: 50 | aws-region: ${{ vars.TF_VAR_REGION }} 51 | role-to-assume: ${{ secrets.AWS_ROLE }} 52 | role-session-name: GitHub-Actions-OIDC-TERRAFORM 53 | 54 | - name: Terraform Setup 55 | uses: hashicorp/setup-terraform@v3 56 | with: 57 | terraform_version: 1.7.1 58 | 59 | - name: Terraform Format 60 | id: fmt 61 | run: terraform fmt -check -no-color 62 | continue-on-error: true 63 | 64 | - name: Terraform Init 65 | id: init 66 | run: terraform init -backend-config="bucket=${{ secrets.TF_STATE_BUCKET_NAME }}" -backend-config="key=${{ secrets.TF_STATE_BUCKET_KEY }}" -backend-config="region=${{ vars.TF_VAR_REGION }}" -backend-config="encrypt=true" -backend-config="dynamodb_table=${{ secrets.TF_STATE_DYNAMODB_TABLE }}" -input=false 67 | 68 | - name: Terraform Validate 69 | id: validate 70 | run: terraform validate -no-color 71 | 72 | - name: Terraform Plan 73 | id: plan 74 | run: terraform plan -no-color -input=false 75 | continue-on-error: true 76 | 77 | - name: Terraform plan PR comment 78 | uses: actions/github-script@v6 79 | if: github.event_name == 'pull_request' 80 | env: 81 | PLAN: "terraform\n${{ steps.plan.outputs.stdout }}" 82 | with: 83 | github-token: ${{ secrets.GITHUB_TOKEN }} 84 | script: | 85 | const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\` 86 | #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\` 87 | #### Terraform Validation 🤖\`${{ steps.validate.outcome }}\` 88 |
Validation Output 89 | 90 | \`\`\`\n 91 | ${{ steps.validate.outputs.stdout }} 92 | \`\`\` 93 | 94 |
95 | 96 | #### Terraform Plan 📖\`${{ steps.plan.outcome }}\` 97 | 98 |
Show Plan 99 | 100 | \`\`\`\n 101 | ${process.env.PLAN} 102 | \`\`\` 103 | 104 |
105 | 106 | *Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`, Working Directory: \`${{ env.tf_actions_working_dir }}\`, Workflow: \`${{ github.workflow }}\`*`; 107 | 108 | github.rest.issues.createComment({ 109 | issue_number: context.issue.number, 110 | owner: context.repo.owner, 111 | repo: context.repo.repo, 112 | body: output 113 | }) 114 | 115 | - name: Terraform Plan Status 116 | if: steps.plan.outcome == 'failure' 117 | run: exit 1 118 | 119 | - name: Terraform Apply 120 | if: github.ref == 'refs/heads/main' && github.event_name == 'push' 121 | id: base 122 | run: terraform apply -auto-approve -input=false 123 | 124 | - name: Check ECS image exists 125 | if: github.ref == 'refs/heads/main' && github.event_name == 'push' 126 | id: ecsimage 127 | run: | 128 | if aws ecr describe-images --repository-name ${{ vars.TF_VAR_ECRREPO }} --image-ids imageTag=${{ vars.TF_VAR_IMAGETAG }} >/dev/null 2>&1 129 | then 130 | echo "image_exists=true" >> "$GITHUB_ENV" 131 | else 132 | echo "image_exists=false" >> "$GITHUB_ENV" 133 | fi 134 | 135 | - name: Check for changes in ./appsrc 136 | if: github.ref == 'refs/heads/main' && github.event_name == 'push' 137 | uses: dorny/paths-filter@v3 138 | id: filter 139 | with: 140 | base: ${{ github.ref }} 141 | filters: | 142 | appsrc: 143 | - 'build/appsrc/**' 144 | 145 | - name: Replace placeholder in Program.cs 146 | if: steps.filter.outputs.appsrc == 'true' || env.image_exists == 'false' && github.ref == 'refs/heads/main' && github.event_name == 'push' 147 | run: sed -i 's/\$Environment/${{ vars.TF_VAR_ENVTAG }}/g' ./appsrc/Program.cs 148 | 149 | # https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 150 | - name: Setup .NET 151 | if: steps.filter.outputs.appsrc == 'true' || env.image_exists == 'false' && github.ref == 'refs/heads/main' && github.event_name == 'push' 152 | uses: actions/setup-dotnet@v4 153 | with: 154 | dotnet-version: 8.0.x 155 | 156 | - name: Build .NET 157 | if: steps.filter.outputs.appsrc == 'true' || env.image_exists == 'false' && github.ref == 'refs/heads/main' && github.event_name == 'push' 158 | run: dotnet publish ./appsrc -c Release --no-restore --property:PublishDir=${{ github.workspace }}/build/appbuild 159 | 160 | - name: Login to Amazon ECR 161 | if: steps.filter.outputs.appsrc == 'true' || env.image_exists == 'false' && github.ref == 'refs/heads/main' && github.event_name == 'push' 162 | id: login-ecr 163 | uses: aws-actions/amazon-ecr-login@v2 164 | 165 | - name: Build, tag, and push docker image to Amazon ECR 166 | if: steps.filter.outputs.appsrc == 'true' || env.image_exists == 'false' && github.ref == 'refs/heads/main' && github.event_name == 'push' 167 | env: 168 | REGISTRY: ${{ steps.login-ecr.outputs.registry }} 169 | REPOSITORY: ${{ vars.TF_VAR_ECRREPO }} 170 | IMAGE_TAG: ${{ vars.TF_VAR_IMAGETAG }} 171 | run: | 172 | docker build -t $REGISTRY/$REPOSITORY:$IMAGE_TAG -f ./appsrc/Dockerfile ./appbuild 173 | docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG 174 | 175 | - name: Print Web App URL in Job Summary 176 | if: github.ref == 'refs/heads/main' && github.event_name == 'push' 177 | run: echo "${{ vars.TF_VAR_ENVTAG }} environment URL http://$(terraform output -raw mswebapp_alb_dns_name) :rocket:" >> $GITHUB_STEP_SUMMARY -------------------------------------------------------------------------------- /.github/workflows/ghaw-tf-build-test.yml: -------------------------------------------------------------------------------- 1 | name: "Review and deploy Test environment" 2 | run-name: ${{ github.actor}} is running a Terraform review and deploy to Test 🚀 3 | on: 4 | push: 5 | branches: 6 | - test 7 | pull_request: 8 | branches: 9 | - test 10 | permissions: 11 | # id-token write required for OIDC, contents read required for actions/checkout, pull-requests write required to comment on PR 12 | id-token: write 13 | contents: read 14 | pull-requests: write 15 | env: 16 | TF_LOG: INFO 17 | AWS_REGION: ${{ vars.TF_VAR_REGION }} 18 | AWS_BUCKET_NAME: ${{ secrets.TF_STATE_BUCKET_NAME }} 19 | AWS_BUCKET_KEY_NAME: ${{ secrets.TF_STATE_BUCKET_KEY }} 20 | 21 | jobs: 22 | release_to_test: 23 | runs-on: ubuntu-latest 24 | defaults: 25 | run: 26 | working-directory: ./build 27 | environment: 28 | name: test 29 | env: 30 | TF_VAR_Region: ${{ vars.TF_VAR_REGION }} 31 | TF_VAR_AZ01: ${{ vars.TF_VAR_AZ01 }} 32 | TF_VAR_AZ02: ${{ vars.TF_VAR_AZ02 }} 33 | TF_VAR_PublicIP: ${{ vars.TF_VAR_PUBLICIP }} 34 | TF_VAR_Prefix: ${{ vars.TF_VAR_PREFIX }} 35 | TF_VAR_SolTag: ${{ vars.TF_VAR_SOLTAG }} 36 | TF_VAR_GitHubRepo: ${{ vars.TF_VAR_GITHUBREPO }} 37 | TF_VAR_EnvCode: ${{ vars.TF_VAR_ENVCODE }} 38 | TF_VAR_EnvTag: ${{ vars.TF_VAR_ENVTAG }} 39 | TF_VAR_VPCCIDR: ${{ vars.TF_VAR_VPCCIDR }} 40 | TF_VAR_FQDN: ${{ vars.TF_VAR_FQDN }} 41 | TF_VAR_ECRRepo: ${{ vars.TF_VAR_ECRREPO }} 42 | TF_VAR_ImageTag: ${{ vars.TF_VAR_IMAGETAG }} 43 | TF_ROOT: . 44 | steps: 45 | - name: Git checkout 46 | uses: actions/checkout@v4 47 | 48 | - name: Configure AWS credentials 49 | uses: aws-actions/configure-aws-credentials@v4 50 | with: 51 | aws-region: ${{ vars.TF_VAR_REGION }} 52 | role-to-assume: ${{ secrets.AWS_ROLE }} 53 | role-session-name: GitHub-Actions-OIDC-TERRAFORM 54 | 55 | - name: Infracost Setup 56 | # Refer to https://github.com/infracost/actions?tab=readme-ov-file#quick-start 57 | if: github.event_name == 'pull_request' 58 | uses: infracost/actions/setup@v2 59 | with: 60 | api-key: ${{ secrets.INFRACOST_API_KEY }} 61 | 62 | - name: Infracost checkout base branch 63 | if: github.event_name == 'pull_request' 64 | uses: actions/checkout@v4 65 | with: 66 | ref: '${{ github.event.pull_request.base.ref }}' 67 | 68 | - name: Infracost generate cost estimate baseline 69 | if: github.event_name == 'pull_request' 70 | run: | 71 | infracost breakdown --path=${TF_ROOT} \ 72 | --format=json \ 73 | --out-file=/tmp/infracost-base.json 74 | - name: Infracost checkout PR branch 75 | if: github.event_name == 'pull_request' 76 | uses: actions/checkout@v4 77 | with: 78 | ref: '${{ github.event.pull_request.head.ref }}' 79 | 80 | - name: Infracost generate Infracost diff 81 | if: github.event_name == 'pull_request' 82 | run: | 83 | infracost diff --path=${TF_ROOT} \ 84 | --format=json \ 85 | --compare-to=/tmp/infracost-base.json \ 86 | --out-file=/tmp/infracost.json 87 | 88 | - name: Infracost post comment 89 | if: github.event_name == 'pull_request' 90 | run: | 91 | infracost comment github --path=/tmp/infracost.json \ 92 | --repo=$GITHUB_REPOSITORY \ 93 | --github-token=${{github.token}} \ 94 | --pull-request=${{github.event.pull_request.number}} \ 95 | --behavior=update 96 | continue-on-error: true 97 | 98 | - name: Checkov Scan 99 | if: github.event_name == 'pull_request' 100 | id: checkov 101 | uses: bridgecrewio/checkov-action@master 102 | with: 103 | directory: . 104 | soft_fail: true 105 | framework: terraform 106 | continue-on-error: true 107 | 108 | - name: KICS Scan 109 | if: github.event_name == 'pull_request' 110 | uses: checkmarx/kics-github-action@v1.7.0 111 | with: 112 | path: '.' 113 | token: ${{ secrets.GITHUB_TOKEN }} 114 | ignore_on_exit: results 115 | output_path: myResults/ 116 | enable_comments: true 117 | 118 | - name: Terraform Setup 119 | uses: hashicorp/setup-terraform@v3 120 | with: 121 | terraform_version: 1.7.1 122 | 123 | - name: Terraform Format 124 | id: fmt 125 | run: terraform fmt -check -no-color 126 | continue-on-error: true 127 | 128 | - name: Terraform Init 129 | id: init 130 | run: terraform init -backend-config="bucket=${{ secrets.TF_STATE_BUCKET_NAME }}" -backend-config="key=${{ secrets.TF_STATE_BUCKET_KEY }}" -backend-config="region=${{ vars.TF_VAR_REGION }}" -backend-config="encrypt=true" -backend-config="dynamodb_table=${{ secrets.TF_STATE_DYNAMODB_TABLE }}" -input=false 131 | 132 | - name: Terraform Validate 133 | id: validate 134 | run: terraform validate -no-color 135 | 136 | - name: Terraform Plan 137 | id: plan 138 | run: terraform plan -no-color -input=false 139 | continue-on-error: true 140 | 141 | - name: Terraform plan PR comment 142 | uses: actions/github-script@v6 143 | if: github.event_name == 'pull_request' 144 | env: 145 | PLAN: "terraform\n${{ steps.plan.outputs.stdout }}" 146 | with: 147 | github-token: ${{ secrets.GITHUB_TOKEN }} 148 | script: | 149 | const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\` 150 | #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\` 151 | #### Terraform Validation 🤖\`${{ steps.validate.outcome }}\` 152 |
Validation Output 153 | 154 | \`\`\`\n 155 | ${{ steps.validate.outputs.stdout }} 156 | \`\`\` 157 | 158 |
159 | 160 | #### Terraform Plan 📖\`${{ steps.plan.outcome }}\` 161 | 162 |
Show Plan 163 | 164 | \`\`\`\n 165 | ${process.env.PLAN} 166 | \`\`\` 167 | 168 |
169 | 170 | *Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`, Working Directory: \`${{ env.tf_actions_working_dir }}\`, Workflow: \`${{ github.workflow }}\`*`; 171 | 172 | github.rest.issues.createComment({ 173 | issue_number: context.issue.number, 174 | owner: context.repo.owner, 175 | repo: context.repo.repo, 176 | body: output 177 | }) 178 | 179 | - name: Terraform Plan Status 180 | if: steps.plan.outcome == 'failure' 181 | run: exit 1 182 | 183 | - name: Terraform Apply 184 | if: github.ref == 'refs/heads/test' && github.event_name == 'push' 185 | id: base 186 | run: terraform apply -auto-approve -input=false 187 | 188 | - name: Check ECS image exists 189 | if: github.ref == 'refs/heads/test' && github.event_name == 'push' 190 | id: ecsimage 191 | run: | 192 | if aws ecr describe-images --repository-name ${{ vars.TF_VAR_ECRREPO }} --image-ids imageTag=${{ vars.TF_VAR_IMAGETAG }} >/dev/null 2>&1 193 | then 194 | echo "image_exists=true" >> "$GITHUB_ENV" 195 | else 196 | echo "image_exists=false" >> "$GITHUB_ENV" 197 | fi 198 | 199 | - name: Check for changes in ./appsrc 200 | if: github.ref == 'refs/heads/test' && github.event_name == 'push' 201 | uses: dorny/paths-filter@v3 202 | id: filter 203 | with: 204 | base: ${{ github.ref }} 205 | filters: | 206 | appsrc: 207 | - 'build/appsrc/**' 208 | 209 | - name: Replace placeholder in Program.cs 210 | if: steps.filter.outputs.appsrc == 'true' || env.image_exists == 'false' && github.ref == 'refs/heads/test' && github.event_name == 'push' 211 | run: sed -i 's/\$Environment/${{ vars.TF_VAR_ENVTAG }}/g' ./appsrc/Program.cs 212 | 213 | # https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 214 | - name: Setup .NET 215 | if: steps.filter.outputs.appsrc == 'true' || env.image_exists == 'false' && github.ref == 'refs/heads/test' && github.event_name == 'push' 216 | uses: actions/setup-dotnet@v4 217 | with: 218 | dotnet-version: 8.0.x 219 | 220 | - name: Build .NET 221 | if: steps.filter.outputs.appsrc == 'true' || env.image_exists == 'false' && github.ref == 'refs/heads/test' && github.event_name == 'push' 222 | run: dotnet publish ./appsrc -c Release --no-restore --property:PublishDir=${{ github.workspace }}/build/appbuild 223 | 224 | - name: Login to Amazon ECR 225 | if: steps.filter.outputs.appsrc == 'true' || env.image_exists == 'false' && github.ref == 'refs/heads/test' && github.event_name == 'push' 226 | id: login-ecr 227 | uses: aws-actions/amazon-ecr-login@v2 228 | 229 | - name: Build, tag, and push docker image to Amazon ECR 230 | if: steps.filter.outputs.appsrc == 'true' || env.image_exists == 'false' && github.ref == 'refs/heads/test' && github.event_name == 'push' 231 | env: 232 | REGISTRY: ${{ steps.login-ecr.outputs.registry }} 233 | REPOSITORY: ${{ vars.TF_VAR_ECRREPO }} 234 | IMAGE_TAG: ${{ vars.TF_VAR_IMAGETAG }} 235 | run: | 236 | docker build -t $REGISTRY/$REPOSITORY:$IMAGE_TAG -f ./appsrc/Dockerfile ./appbuild 237 | docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG 238 | 239 | - name: Print Web App URL in Job Summary 240 | if: github.ref == 'refs/heads/test' && github.event_name == 'push' 241 | run: echo "${{ vars.TF_VAR_ENVTAG }} environment URL http://$(terraform output -raw mswebapp_alb_dns_name) :rocket:" >> $GITHUB_STEP_SUMMARY -------------------------------------------------------------------------------- /.github/workflows/ghaw-tf-destroy-dev.yml: -------------------------------------------------------------------------------- 1 | # Run this workflow manually from GitHub to destroy Development environment 2 | name: "Destroy Development environment" 3 | run-name: ${{ github.actor}} is running a Terraform destroy on Dev 🔥 4 | on: 5 | workflow_dispatch: 6 | 7 | permissions: 8 | id-token: write 9 | contents: read 10 | pull-requests: write 11 | env: 12 | TF_LOG: INFO 13 | AWS_REGION: ${{ vars.TF_VAR_REGION }} 14 | AWS_BUCKET_NAME: ${{ secrets.TF_STATE_BUCKET_NAME }} 15 | AWS_BUCKET_KEY_NAME: ${{ secrets.TF_STATE_BUCKET_KEY }} 16 | 17 | jobs: 18 | destroy_development: 19 | runs-on: ubuntu-latest 20 | defaults: 21 | run: 22 | working-directory: ./build 23 | environment: 24 | name: dev 25 | env: 26 | TF_VAR_Region: ${{ vars.TF_VAR_REGION }} 27 | TF_VAR_AZ01: ${{ vars.TF_VAR_AZ01 }} 28 | TF_VAR_AZ02: ${{ vars.TF_VAR_AZ02 }} 29 | TF_VAR_PublicIP: ${{ vars.TF_VAR_PUBLICIP }} 30 | TF_VAR_Prefix: ${{ vars.TF_VAR_PREFIX }} 31 | TF_VAR_SolTag: ${{ vars.TF_VAR_SOLTAG }} 32 | TF_VAR_GitHubRepo: ${{ vars.TF_VAR_GITHUBREPO }} 33 | TF_VAR_EnvCode: ${{ vars.TF_VAR_ENVCODE }} 34 | TF_VAR_EnvTag: ${{ vars.TF_VAR_ENVTAG }} 35 | TF_VAR_VPCCIDR: ${{ vars.TF_VAR_VPCCIDR }} 36 | TF_VAR_FQDN: ${{ vars.TF_VAR_FQDN }} 37 | TF_VAR_ECRRepo: ${{ vars.TF_VAR_ECRREPO }} 38 | TF_VAR_ImageTag: ${{ vars.TF_VAR_IMAGETAG }} 39 | steps: 40 | - name: Git checkout 41 | uses: actions/checkout@v4 42 | 43 | - name: Configure AWS credentials 44 | uses: aws-actions/configure-aws-credentials@v4 45 | with: 46 | aws-region: ${{ vars.TF_VAR_REGION }} 47 | role-to-assume: ${{ secrets.AWS_ROLE }} 48 | role-session-name: GitHub-Actions-OIDC-TERRAFORM 49 | 50 | - name: Terraform Setup 51 | uses: hashicorp/setup-terraform@v3 52 | with: 53 | terraform_version: 1.7.1 54 | 55 | - name: Terraform Format 56 | id: fmt 57 | run: terraform fmt -check -no-color 58 | continue-on-error: true 59 | 60 | - name: Terraform Init 61 | id: init 62 | run: terraform init -backend-config="bucket=${{ secrets.TF_STATE_BUCKET_NAME }}" -backend-config="key=${{ secrets.TF_STATE_BUCKET_KEY }}" -backend-config="region=${{ vars.TF_VAR_REGION }}" -backend-config="encrypt=true" -backend-config="dynamodb_table=${{ secrets.TF_STATE_DYNAMODB_TABLE }}" -input=false 63 | 64 | - name: Terraform Validate 65 | id: validate 66 | run: terraform validate -no-color 67 | 68 | - name: Run Terraform plan 69 | id: plan 70 | run: terraform plan -destroy -no-color -input=false 71 | continue-on-error: true 72 | 73 | - name: Terraform Plan Status 74 | if: steps.plan.outcome == 'failure' 75 | run: exit 1 76 | 77 | - name: Terraform Destroy 78 | run: terraform destroy -auto-approve -input=false -------------------------------------------------------------------------------- /.github/workflows/ghaw-tf-destroy-prod.yml: -------------------------------------------------------------------------------- 1 | # Run this workflow manually from GitHub to destroy Production environment 2 | name: "Destroy Production environment" 3 | run-name: ${{ github.actor}} is running a Terraform destroy on Production 🔥 4 | on: 5 | workflow_dispatch: 6 | 7 | permissions: 8 | id-token: write 9 | contents: read 10 | pull-requests: write 11 | env: 12 | TF_LOG: INFO 13 | AWS_REGION: ${{ vars.TF_VAR_REGION }} 14 | AWS_BUCKET_NAME: ${{ secrets.TF_STATE_BUCKET_NAME }} 15 | AWS_BUCKET_KEY_NAME: ${{ secrets.TF_STATE_BUCKET_KEY }} 16 | 17 | jobs: 18 | destroy_production: 19 | runs-on: ubuntu-latest 20 | defaults: 21 | run: 22 | working-directory: ./build 23 | environment: 24 | name: prod 25 | env: 26 | TF_VAR_Region: ${{ vars.TF_VAR_REGION }} 27 | TF_VAR_AZ01: ${{ vars.TF_VAR_AZ01 }} 28 | TF_VAR_AZ02: ${{ vars.TF_VAR_AZ02 }} 29 | TF_VAR_PublicIP: ${{ vars.TF_VAR_PUBLICIP }} 30 | TF_VAR_Prefix: ${{ vars.TF_VAR_PREFIX }} 31 | TF_VAR_SolTag: ${{ vars.TF_VAR_SOLTAG }} 32 | TF_VAR_GitHubRepo: ${{ vars.TF_VAR_GITHUBREPO }} 33 | TF_VAR_EnvCode: ${{ vars.TF_VAR_ENVCODE }} 34 | TF_VAR_EnvTag: ${{ vars.TF_VAR_ENVTAG }} 35 | TF_VAR_VPCCIDR: ${{ vars.TF_VAR_VPCCIDR }} 36 | TF_VAR_FQDN: ${{ vars.TF_VAR_FQDN }} 37 | TF_VAR_ECRRepo: ${{ vars.TF_VAR_ECRREPO }} 38 | TF_VAR_ImageTag: ${{ vars.TF_VAR_IMAGETAG }} 39 | steps: 40 | - name: Git checkout 41 | uses: actions/checkout@v4 42 | 43 | - name: Configure AWS credentials 44 | uses: aws-actions/configure-aws-credentials@v4 45 | with: 46 | aws-region: ${{ vars.TF_VAR_REGION }} 47 | role-to-assume: ${{ secrets.AWS_ROLE }} 48 | role-session-name: GitHub-Actions-OIDC-TERRAFORM 49 | 50 | - name: Terraform Setup 51 | uses: hashicorp/setup-terraform@v3 52 | with: 53 | terraform_version: 1.7.1 54 | 55 | - name: Terraform Format 56 | id: fmt 57 | run: terraform fmt -check -no-color 58 | continue-on-error: true 59 | 60 | - name: Terraform Init 61 | id: init 62 | run: terraform init -backend-config="bucket=${{ secrets.TF_STATE_BUCKET_NAME }}" -backend-config="key=${{ secrets.TF_STATE_BUCKET_KEY }}" -backend-config="region=${{ vars.TF_VAR_REGION }}" -backend-config="encrypt=true" -backend-config="dynamodb_table=${{ secrets.TF_STATE_DYNAMODB_TABLE }}" -input=false 63 | 64 | - name: Terraform Validate 65 | id: validate 66 | run: terraform validate -no-color 67 | 68 | - name: Run Terraform plan 69 | id: plan 70 | run: terraform plan -destroy -no-color -input=false 71 | continue-on-error: true 72 | 73 | - name: Terraform Plan Status 74 | if: steps.plan.outcome == 'failure' 75 | run: exit 1 76 | 77 | - name: Terraform Destroy 78 | run: terraform destroy -auto-approve -input=false -------------------------------------------------------------------------------- /.github/workflows/ghaw-tf-destroy-test.yml: -------------------------------------------------------------------------------- 1 | # Run this workflow manually from GitHub to destroy Test environment 2 | name: "Destroy Test environment" 3 | run-name: ${{ github.actor}} is running a Terraform destroy on Test 🔥 4 | on: 5 | workflow_dispatch: 6 | 7 | permissions: 8 | id-token: write 9 | contents: read 10 | pull-requests: write 11 | env: 12 | TF_LOG: INFO 13 | AWS_REGION: ${{ vars.TF_VAR_REGION }} 14 | AWS_BUCKET_NAME: ${{ secrets.TF_STATE_BUCKET_NAME }} 15 | AWS_BUCKET_KEY_NAME: ${{ secrets.TF_STATE_BUCKET_KEY }} 16 | 17 | jobs: 18 | destroy_test: 19 | runs-on: ubuntu-latest 20 | defaults: 21 | run: 22 | working-directory: ./build 23 | environment: 24 | name: test 25 | env: 26 | TF_VAR_Region: ${{ vars.TF_VAR_REGION }} 27 | TF_VAR_AZ01: ${{ vars.TF_VAR_AZ01 }} 28 | TF_VAR_AZ02: ${{ vars.TF_VAR_AZ02 }} 29 | TF_VAR_PublicIP: ${{ vars.TF_VAR_PUBLICIP }} 30 | TF_VAR_Prefix: ${{ vars.TF_VAR_PREFIX }} 31 | TF_VAR_SolTag: ${{ vars.TF_VAR_SOLTAG }} 32 | TF_VAR_GitHubRepo: ${{ vars.TF_VAR_GITHUBREPO }} 33 | TF_VAR_EnvCode: ${{ vars.TF_VAR_ENVCODE }} 34 | TF_VAR_EnvTag: ${{ vars.TF_VAR_ENVTAG }} 35 | TF_VAR_VPCCIDR: ${{ vars.TF_VAR_VPCCIDR }} 36 | TF_VAR_FQDN: ${{ vars.TF_VAR_FQDN }} 37 | TF_VAR_ECRRepo: ${{ vars.TF_VAR_ECRREPO }} 38 | TF_VAR_ImageTag: ${{ vars.TF_VAR_IMAGETAG }} 39 | steps: 40 | - name: Git checkout 41 | uses: actions/checkout@v4 42 | 43 | - name: Configure AWS credentials 44 | uses: aws-actions/configure-aws-credentials@v4 45 | with: 46 | aws-region: ${{ vars.TF_VAR_REGION }} 47 | role-to-assume: ${{ secrets.AWS_ROLE }} 48 | role-session-name: GitHub-Actions-OIDC-TERRAFORM 49 | 50 | - name: Terraform Setup 51 | uses: hashicorp/setup-terraform@v3 52 | with: 53 | terraform_version: 1.7.1 54 | 55 | - name: Terraform Format 56 | id: fmt 57 | run: terraform fmt -check -no-color 58 | continue-on-error: true 59 | 60 | - name: Terraform Init 61 | id: init 62 | run: terraform init -backend-config="bucket=${{ secrets.TF_STATE_BUCKET_NAME }}" -backend-config="key=${{ secrets.TF_STATE_BUCKET_KEY }}" -backend-config="region=${{ vars.TF_VAR_REGION }}" -backend-config="encrypt=true" -backend-config="dynamodb_table=${{ secrets.TF_STATE_DYNAMODB_TABLE }}" -input=false 63 | 64 | - name: Terraform Validate 65 | id: validate 66 | run: terraform validate -no-color 67 | 68 | - name: Run Terraform plan 69 | id: plan 70 | run: terraform plan -destroy -no-color -input=false 71 | continue-on-error: true 72 | 73 | - name: Terraform Plan Status 74 | if: steps.plan.outcome == 'failure' 75 | run: exit 1 76 | 77 | - name: Terraform Destroy 78 | run: terraform destroy -auto-approve -input=false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # .tfstate files 5 | *.tfstate 6 | *.tfstate.* 7 | 8 | # Crash log files 9 | crash.log 10 | crash.*.log 11 | 12 | # Exclude all .tfvars files, which are likely to contain sensitive data, such as 13 | # password, private keys, and other secrets. These should not be part of version 14 | # control as they are data points which are potentially sensitive and subject 15 | # to change depending on the environment. 16 | #*.tfvars 17 | *.tfvars.json 18 | 19 | # Ignore override files as they are usually used to override resources locally and so 20 | # are not checked in 21 | override.tf 22 | override.tf.json 23 | *_override.tf 24 | *_override.tf.json 25 | 26 | # Include override files you do wish to add to version control using negated pattern 27 | # !example_override.tf 28 | 29 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 30 | # example: *tfplan* 31 | 32 | # Ignore CLI configuration files 33 | .terraformrc 34 | terraform.rc 35 | 36 | # Igonre lock files 37 | .terraform.lock.hcl 38 | -------------------------------------------------------------------------------- /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 | ## How to use GitHub Actions and Terraform to deploy a Microsoft Web Application 2 | 3 | This repository contains deployment templates used in the [How to use GitHub Actions and Terraform to deploy a Microsoft Web Application](https://aws.amazon.com/blogs/modernizing-with-aws/automate-microsoft-web-application-deployments-with-github-actions-and-terraform/) Amazon Web Services blog post. 4 | 5 | Watch the [live demo](https://www.twitch.tv/videos/2241783900) for a quick tour. 6 | 7 | ## Design 8 | 9 | ### Pipeline Components 10 | This code demonstrates how GitHub Actions and Terraform can automate the deployment of your AWS workloads across multiple environments. 11 | 12 | ![pipeline-components](img/pipeline.png) 13 | 14 | ### Workflow logic 15 | The following GitHub action workflow logic will be deployed. 16 | 17 | ![workflow-logic](img/workflowlogic.png) 18 | 19 | ### Sample Workload 20 | The sample workload runs in an Amazon VPC with a .NET Core web application hosted on Amazon ECS using AWS Fargate, spread across two Availability Zones, balanced by an ALB, with outbound communication via NAT Gateways, and external access through an Internet Gateway. It uses Amazon S3 for ALB logs, Amazon ECR for container images, AWS KMS for encryption, IAM for access management, CloudWatch for observability, and Resource Groups for efficient component management. 21 | 22 | ![sample-application](img/sampleapp.png) 23 | 24 | ## How to use 25 | 26 | Complete prerequisites section in [blog post](https://aws.amazon.com/blogs/modernizing-with-aws/automate-microsoft-web-application-deployments-with-github-actions-and-terraform/) and follow walkthrough. 27 | 28 | ## Security 29 | 30 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 31 | 32 | ## License 33 | 34 | This library is licensed under the MIT-0 License. See the LICENSE file. -------------------------------------------------------------------------------- /bootstrap/accountsetup/awsiamsetup.tf: -------------------------------------------------------------------------------- 1 | ### Create IAM policies required to bootstrap Terraform across multiple accounts 2 | data "aws_caller_identity" "primary" { 3 | provider = aws.primary 4 | } 5 | 6 | data "aws_caller_identity" "development" { 7 | provider = aws.development 8 | } 9 | 10 | data "aws_caller_identity" "testing" { 11 | provider = aws.testing 12 | } 13 | 14 | data "aws_caller_identity" "production" { 15 | provider = aws.production 16 | } 17 | 18 | module "assumerole_dev" { 19 | source = "./modules/assumerole" 20 | providers = { 21 | aws = aws.development 22 | } 23 | Region = var.Region 24 | Prefix = var.Prefix 25 | Environment = "dv" 26 | PrimaryAccount = data.aws_caller_identity.primary.arn 27 | TargetAccount = data.aws_caller_identity.development.account_id 28 | } 29 | 30 | module "assumerole_test" { 31 | source = "./modules/assumerole" 32 | providers = { 33 | aws = aws.testing 34 | } 35 | Region = var.Region 36 | Prefix = var.Prefix 37 | Environment = "ts" 38 | PrimaryAccount = data.aws_caller_identity.primary.arn 39 | TargetAccount = data.aws_caller_identity.testing.account_id 40 | } 41 | 42 | module "assumerole_prod" { 43 | source = "./modules/assumerole" 44 | providers = { 45 | aws = aws.production 46 | } 47 | Region = var.Region 48 | Prefix = var.Prefix 49 | Environment = "pd" 50 | PrimaryAccount = data.aws_caller_identity.primary.arn 51 | TargetAccount = data.aws_caller_identity.production.account_id 52 | } 53 | 54 | output "role_arn" { 55 | value = { 56 | dev_role_arn = module.assumerole_dev.role_arn 57 | test_role_arn = module.assumerole_test.role_arn 58 | prod_role_arn = module.assumerole_prod.role_arn 59 | } 60 | } -------------------------------------------------------------------------------- /bootstrap/accountsetup/modules/assumerole/main.tf: -------------------------------------------------------------------------------- 1 | ### This module creates IAM policies required to bootstrap Terraform across multiple accounts 2 | ### Code based on this tutorial: https://developer.hashicorp.com/terraform/tutorials/aws/aws-assumerole 3 | 4 | # IAM policy document allowing role assumption 5 | data "aws_iam_policy_document" "assumerole" { 6 | statement { 7 | actions = [ 8 | "sts:AssumeRole", 9 | "sts:TagSession", 10 | "sts:SetSourceIdentity" 11 | ] 12 | principals { 13 | type = "AWS" 14 | identifiers = ["${var.PrimaryAccount}"] 15 | } 16 | } 17 | } 18 | # IAM policy enabling AWS resource creation and deletion for Terraform state 19 | data "aws_iam_policy_document" "tfbootstrap" { 20 | statement { 21 | # https://docs.aws.amazon.com/servicecatalog/latest/adminguide/getstarted-launchrole-Terraform.html 22 | actions = [ 23 | "s3:CreateBucket*", 24 | "s3:DeleteBucket*", 25 | "s3:DeleteObject", 26 | "s3:DeleteObjectVersion", 27 | "s3:DeletePublicAccessBlock", 28 | "s3:Get*", 29 | "s3:List*", 30 | "s3:Put*" 31 | ] 32 | resources = ["arn:aws:s3:::*"] 33 | } 34 | statement { 35 | actions = [ 36 | "dynamodb:CreateTable", 37 | "dynamodb:DeleteTable", 38 | "dynamodb:Describe*", 39 | "dynamodb:List*", 40 | "dynamodb:Tag*", 41 | "dynamodb:UpdateContinuousBackups" 42 | ] 43 | resources = ["arn:aws:dynamodb:${var.Region}:${var.TargetAccount}:*"] 44 | } 45 | statement { 46 | actions = [ 47 | "iam:CreateOpenIDConnectProvider", 48 | "iam:GetOpenIDConnectProvider", 49 | "iam:DeleteOpenIDConnectProvider", 50 | "iam:TagOpenIDConnectProvider", 51 | "iam:CreateRole", 52 | "iam:GetRole", 53 | "iam:GetRolePolicy", 54 | "iam:DeleteRole", 55 | "iam:DeleteRolePolicy", 56 | "iam:ListAttachedRolePolicies", 57 | "iam:ListInstanceProfilesForRole", 58 | "iam:ListRolePolicies", 59 | "iam:PutRolePolicy", 60 | "iam:TagRole", 61 | "iam:UpdateAssumeRolePolicy" 62 | ] 63 | resources = ["arn:aws:iam::${var.TargetAccount}:*"] 64 | } 65 | } 66 | 67 | # IAM role for Terraform bootstrapping 68 | resource "aws_iam_role" "assumerole" { 69 | name = format("%s%s%s%s", var.Prefix, "iar", var.Environment, "tfassumerole") 70 | description = "This role can be assumed to setup tfboot components from a single primary account" 71 | assume_role_policy = data.aws_iam_policy_document.assumerole.json 72 | 73 | inline_policy { 74 | name = format("%s%s%s%s", var.Prefix, "iap", var.Environment, "tfassumerole") 75 | policy = data.aws_iam_policy_document.tfbootstrap.json 76 | } 77 | tags = { 78 | Name = format("%s%s%s%s", var.Prefix, "iar", var.Environment, "tfassumerole") 79 | rtype = "security" 80 | } 81 | } 82 | 83 | # Output role ARNs for entry into \pipelinebuild terraform.tfvars 84 | output "role_arn" { 85 | value = aws_iam_role.assumerole.arn 86 | } -------------------------------------------------------------------------------- /bootstrap/accountsetup/modules/assumerole/variables.tf: -------------------------------------------------------------------------------- 1 | # Account Settings 2 | variable "Region" { 3 | description = "AWS deployment region" 4 | type = string 5 | } 6 | variable "PrimaryAccount" { 7 | description = "ID of the AWS account where the bootstrap will be executed" 8 | type = string 9 | } 10 | variable "TargetAccount" { 11 | description = "ID of the AWS account where Terraform infrastructure will be deployed" 12 | type = string 13 | } 14 | 15 | # Tagging and naming 16 | variable "Prefix" { 17 | description = "Prefix used to name all resources" 18 | type = string 19 | } 20 | variable "Environment" { 21 | description = "Identifier for the environment" 22 | type = string 23 | } -------------------------------------------------------------------------------- /bootstrap/accountsetup/modules/assumerole/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 5.38.0" 6 | } 7 | } 8 | 9 | required_version = ">= 1.7.1" 10 | } -------------------------------------------------------------------------------- /bootstrap/accountsetup/provider.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | alias = "primary" 3 | profile = "default" 4 | region = var.Region 5 | } 6 | 7 | provider "aws" { 8 | alias = "development" 9 | profile = "" 10 | region = var.Region 11 | 12 | default_tags { 13 | tags = { 14 | Environment = "Development" 15 | Provisioner = "Terraform" 16 | Solution = "AWS-GHA-TF-MSFT" 17 | } 18 | } 19 | } 20 | 21 | provider "aws" { 22 | alias = "testing" 23 | profile = "" 24 | region = var.Region 25 | 26 | default_tags { 27 | tags = { 28 | Environment = "Testing" 29 | Provisioner = "Terraform" 30 | Solution = "AWS-GHA-TF-MSFT" 31 | } 32 | } 33 | } 34 | 35 | provider "aws" { 36 | alias = "production" 37 | profile = "" 38 | region = var.Region 39 | 40 | default_tags { 41 | tags = { 42 | Environment = "Production" 43 | Provisioner = "Terraform" 44 | Solution = "AWS-GHA-TF-MSFT" 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /bootstrap/accountsetup/terraform.tfvars: -------------------------------------------------------------------------------- 1 | # Regions 2 | # e.g. Region = "eu-west-1" 3 | Region = "" 4 | 5 | # Tagging and naming 6 | Prefix = "tfboot" -------------------------------------------------------------------------------- /bootstrap/accountsetup/variables.tf: -------------------------------------------------------------------------------- 1 | # Regions 2 | variable "Region" { 3 | description = "AWS deployment region" 4 | type = string 5 | } 6 | 7 | # Tagging and naming 8 | variable "Prefix" { 9 | description = "Prefix used to name all resources" 10 | type = string 11 | } -------------------------------------------------------------------------------- /bootstrap/pipelinebuild/awsresources.tf: -------------------------------------------------------------------------------- 1 | ### Create AWS resources for Terraform bootstrapping across multiple accounts 2 | module "tfbootstrap_dev" { 3 | source = "./modules/tfbootstrap" 4 | providers = { 5 | aws = aws.development 6 | } 7 | Region = var.Region 8 | Prefix = var.Prefix 9 | EnvCode = "dv" 10 | GitHubOrg = var.GitHubOrg 11 | GitHubRepo = var.GitHubRepo 12 | GitHubEnv = "dev" 13 | } 14 | 15 | module "tfbootstrap_test" { 16 | source = "./modules/tfbootstrap" 17 | providers = { 18 | aws = aws.testing 19 | } 20 | Region = var.Region 21 | Prefix = var.Prefix 22 | EnvCode = "ts" 23 | GitHubOrg = var.GitHubOrg 24 | GitHubRepo = var.GitHubRepo 25 | GitHubEnv = "test" 26 | } 27 | 28 | module "tfbootstrap_prod" { 29 | source = "./modules/tfbootstrap" 30 | providers = { 31 | aws = aws.production 32 | } 33 | Region = var.Region 34 | Prefix = var.Prefix 35 | EnvCode = "pd" 36 | GitHubOrg = var.GitHubOrg 37 | GitHubRepo = var.GitHubRepo 38 | GitHubEnv = "prod" 39 | } 40 | 41 | # DEBUGGING: Outputs for GitHub Action Secrets 42 | # output "gha_iam_role" { 43 | # value = { 44 | # dev = module.tfbootstrap_dev.gha_iam_role 45 | # test = module.tfbootstrap_test.gha_iam_role 46 | # prod = module.tfbootstrap_prod.gha_iam_role 47 | # } 48 | # } 49 | 50 | # output "tfstate_bucket_names" { 51 | # value = { 52 | # dev = module.tfbootstrap_dev.tfstate_bucket_name 53 | # test = module.tfbootstrap_test.tfstate_bucket_name 54 | # prod = module.tfbootstrap_prod.tfstate_bucket_name 55 | # } 56 | # } 57 | 58 | # output "tfstate_dynamodb_table" { 59 | # value = { 60 | # dev = module.tfbootstrap_dev.tfstate_dynamodb_table_name 61 | # test = module.tfbootstrap_test.tfstate_dynamodb_table_name 62 | # prod = module.tfbootstrap_prod.tfstate_dynamodb_table_name 63 | # } 64 | # } -------------------------------------------------------------------------------- /bootstrap/pipelinebuild/githubresources.tf: -------------------------------------------------------------------------------- 1 | ### Create GitHub pipeline resources 2 | 3 | # Create GitHub branch per environment. Existing Main branch is used for prod 4 | resource "github_branch" "dev" { 5 | repository = var.GitHubRepo 6 | branch = "dev" 7 | } 8 | 9 | resource "github_branch" "test" { 10 | repository = var.GitHubRepo 11 | branch = "test" 12 | } 13 | 14 | # Local variables used to define GitHub Environments and Secrets configuration 15 | locals { 16 | gha_environment = ["dev", "test", "prod"] 17 | 18 | gha_iam_role = { 19 | dev = module.tfbootstrap_dev.gha_iam_role 20 | test = module.tfbootstrap_test.gha_iam_role 21 | prod = module.tfbootstrap_prod.gha_iam_role 22 | } 23 | tfstate_bucket_name = { 24 | dev = module.tfbootstrap_dev.tfstate_bucket_name 25 | test = module.tfbootstrap_test.tfstate_bucket_name 26 | prod = module.tfbootstrap_prod.tfstate_bucket_name 27 | } 28 | tfstate_dynamodb_table = { 29 | dev = module.tfbootstrap_dev.tfstate_dynamodb_table_name 30 | test = module.tfbootstrap_test.tfstate_dynamodb_table_name 31 | prod = module.tfbootstrap_prod.tfstate_dynamodb_table_name 32 | } 33 | } 34 | 35 | # Create GitHub Environments 36 | resource "github_repository_environment" "env" { 37 | for_each = toset(local.gha_environment) 38 | 39 | environment = each.value 40 | repository = var.GitHubRepo 41 | 42 | deployment_branch_policy { 43 | protected_branches = false 44 | custom_branch_policies = true 45 | } 46 | } 47 | 48 | # Create GitHub environment branch policies 49 | resource "github_repository_environment_deployment_policy" "dev" { 50 | repository = var.GitHubRepo 51 | environment = github_repository_environment.env["dev"].environment 52 | branch_pattern = "dev*" 53 | } 54 | 55 | resource "github_repository_environment_deployment_policy" "dev2test" { 56 | repository = var.GitHubRepo 57 | environment = github_repository_environment.env["test"].environment 58 | branch_pattern = "dev*" 59 | } 60 | 61 | resource "github_repository_environment_deployment_policy" "test" { 62 | repository = var.GitHubRepo 63 | environment = github_repository_environment.env["test"].environment 64 | branch_pattern = "test*" 65 | } 66 | 67 | resource "github_repository_environment_deployment_policy" "test2prod" { 68 | repository = var.GitHubRepo 69 | environment = github_repository_environment.env["prod"].environment 70 | branch_pattern = "test*" 71 | } 72 | 73 | resource "github_repository_environment_deployment_policy" "prod" { 74 | repository = var.GitHubRepo 75 | environment = github_repository_environment.env["prod"].environment 76 | branch_pattern = "main" 77 | } 78 | 79 | # Create GitHub branch protection policy 80 | resource "github_branch_protection" "main" { 81 | repository_id = var.GitHubRepo 82 | pattern = "main" 83 | require_signed_commits = true 84 | 85 | required_pull_request_reviews { 86 | required_approving_review_count = 2 87 | require_code_owner_reviews = true 88 | } 89 | } 90 | 91 | ### Create GitHub Environment Secrets 92 | 93 | # IAM Role ARN used by GitHub Actions runner to deploy AWS resources 94 | resource "github_actions_environment_secret" "AWS_ROLE" { 95 | for_each = github_repository_environment.env 96 | 97 | repository = var.GitHubRepo 98 | environment = each.value.environment 99 | secret_name = "AWS_ROLE" 100 | plaintext_value = lookup(local.gha_iam_role, each.value.environment, null) 101 | } 102 | 103 | # Terraform state S3 bucket name 104 | resource "github_actions_environment_secret" "TF_STATE_BUCKET_NAME" { 105 | for_each = github_repository_environment.env 106 | 107 | repository = var.GitHubRepo 108 | environment = each.value.environment 109 | secret_name = "TF_STATE_BUCKET_NAME" 110 | plaintext_value = lookup(local.tfstate_bucket_name, each.value.environment, null) 111 | } 112 | 113 | # Terraform state S3 bucket key 114 | resource "github_actions_environment_secret" "TF_STATE_BUCKET_KEY" { 115 | for_each = github_repository_environment.env 116 | 117 | repository = var.GitHubRepo 118 | environment = each.value.environment 119 | secret_name = "TF_STATE_BUCKET_KEY" 120 | plaintext_value = "terraform/${each.value.environment}.tfstate" 121 | } 122 | 123 | # Terraform state locking DynamoDB table 124 | resource "github_actions_environment_secret" "TF_STATE_DYNAMODB_TABLE" { 125 | for_each = github_repository_environment.env 126 | 127 | repository = var.GitHubRepo 128 | environment = each.value.environment 129 | secret_name = "TF_STATE_DYNAMODB_TABLE" 130 | plaintext_value = lookup(local.tfstate_dynamodb_table, each.value.environment, null) 131 | } 132 | 133 | # Infracost API Key 134 | resource "github_actions_environment_secret" "INFRACOST_API_KEY" { 135 | 136 | repository = var.GitHubRepo 137 | environment = "test" 138 | secret_name = "INFRACOST_API_KEY" 139 | plaintext_value = var.InfraCostAPIKey 140 | 141 | depends_on = [github_repository_environment.env] 142 | } 143 | 144 | ### Create GitHub Environment Variables 145 | 146 | # Locals used for constructing GitHub Variables 147 | locals { 148 | # Declare GitHub Environments variables 149 | environment_variables_common = { 150 | # Deployment region e.g. eu-west-1 151 | TF_VAR_REGION = "" 152 | # Deployment Availability Zone 1 e.g. eu-west-1a 153 | TF_VAR_AZ01 = "" 154 | # Deployment Availability Zone 2 e.g. eu-west-1b 155 | TF_VAR_AZ02 = "" 156 | # The Public IP address from which the web application will be accessed e.g. x.x.x.x/32 157 | TF_VAR_PUBLICIP = "" 158 | # A prefix appended to the name of all AWS-created resources e.g. ghablog WARNING: use lowercase character only and no symbols 159 | TF_VAR_PREFIX = "" 160 | TF_VAR_SOLTAG = "AWS-GHA-TF-MSFT" 161 | TF_VAR_GITHUBREPO = format("%s%s%s", var.GitHubOrg, "/", var.GitHubRepo) 162 | # The first two octets of the CIDR IP address range e.g. 10.0 163 | TF_VAR_VPCCIDR = "" 164 | TF_VAR_ECRREPO = "mswebapp" 165 | TF_VAR_IMAGETAG = "1.0.0" 166 | } 167 | # Declare dev specific GitHub Environments variables 168 | environment_variables_dev = merge( 169 | local.environment_variables_common, 170 | { 171 | TF_VAR_ENVCODE = "dv" 172 | TF_VAR_ENVTAG = "Development" 173 | } 174 | ) 175 | # Declare test specific GitHub Environments variables 176 | environment_variables_test = merge( 177 | local.environment_variables_common, 178 | { 179 | TF_VAR_ENVCODE = "ts" 180 | TF_VAR_ENVTAG = "Testing" 181 | } 182 | ) 183 | # Declare prod specific GitHub Environments variables 184 | environment_variables_prod = merge( 185 | local.environment_variables_common, 186 | { 187 | TF_VAR_ENVCODE = "pd" 188 | TF_VAR_ENVTAG = "Production" 189 | } 190 | ) 191 | } 192 | 193 | # Create GitHub Environment Variables 194 | resource "github_actions_environment_variable" "dev" { 195 | for_each = local.environment_variables_dev 196 | 197 | repository = var.GitHubRepo 198 | environment = github_repository_environment.env["dev"].environment 199 | variable_name = each.key 200 | value = each.value 201 | } 202 | 203 | resource "github_actions_environment_variable" "test" { 204 | for_each = local.environment_variables_test 205 | 206 | repository = var.GitHubRepo 207 | environment = github_repository_environment.env["test"].environment 208 | variable_name = each.key 209 | value = each.value 210 | } 211 | 212 | resource "github_actions_environment_variable" "prod" { 213 | for_each = local.environment_variables_prod 214 | 215 | repository = var.GitHubRepo 216 | environment = github_repository_environment.env["prod"].environment 217 | variable_name = each.key 218 | value = each.value 219 | } -------------------------------------------------------------------------------- /bootstrap/pipelinebuild/modules/tfbootstrap/main.tf: -------------------------------------------------------------------------------- 1 | ### This module sets up AWS resources for Terraform bootstrapping across multiple accounts 2 | 3 | # Create Amazon S3 buckets for Terraform state file 4 | resource "aws_s3_bucket" "tfstate" { 5 | bucket_prefix = format("%s%s%s%s", var.Prefix, "sss", var.EnvCode, "tfstate") 6 | force_destroy = true 7 | 8 | tags = { 9 | Name = format("%s%s%s%s", var.Prefix, "sss", var.EnvCode, "tfstate"), 10 | rtype = "storage" 11 | codeblock = "infrastructure" 12 | } 13 | } 14 | 15 | # IAM Policy to enforce TLS 1.2 on Amazon S3 buckets 16 | data "aws_iam_policy_document" "S3tfstateTLS" { 17 | statement { 18 | sid = "Allow HTTPS only" 19 | effect = "Deny" 20 | 21 | principals { 22 | type = "*" 23 | identifiers = ["*"] 24 | } 25 | actions = [ 26 | "s3*" 27 | ] 28 | resources = [ 29 | "${aws_s3_bucket.tfstate.arn}", 30 | "${aws_s3_bucket.tfstate.arn}/*" 31 | ] 32 | condition { 33 | test = "Bool" 34 | variable = "aws:SecureTransport" 35 | values = [ 36 | "false" 37 | ] 38 | } 39 | } 40 | statement { 41 | sid = "Allow TLS 1.2 and above" 42 | effect = "Deny" 43 | 44 | principals { 45 | type = "*" 46 | identifiers = ["*"] 47 | } 48 | actions = [ 49 | "s3*" 50 | ] 51 | resources = [ 52 | "${aws_s3_bucket.tfstate.arn}", 53 | "${aws_s3_bucket.tfstate.arn}/*" 54 | ] 55 | condition { 56 | test = "NumericLessThan" 57 | variable = "s3:TlsVersion" 58 | values = [ 59 | "1.2" 60 | ] 61 | } 62 | } 63 | } 64 | 65 | # Apply policy to enforce TLS 1.2 on Amazon S3 buckets 66 | resource "aws_s3_bucket_policy" "tfstate" { 67 | bucket = aws_s3_bucket.tfstate.id 68 | policy = data.aws_iam_policy_document.S3tfstateTLS.json 69 | } 70 | 71 | # Enable Amazon S3 Bucket versioning 72 | resource "aws_s3_bucket_versioning" "tfstate" { 73 | bucket = aws_s3_bucket.tfstate.id 74 | 75 | versioning_configuration { 76 | status = "Enabled" 77 | } 78 | } 79 | 80 | # Block Amazon S3 Bucket public access 81 | resource "aws_s3_bucket_public_access_block" "tfstate" { 82 | bucket = aws_s3_bucket.tfstate.id 83 | block_public_acls = true 84 | block_public_policy = true 85 | ignore_public_acls = true 86 | restrict_public_buckets = true 87 | } 88 | 89 | # Create Amazon DynamoDB tables for Terraform state locking 90 | resource "aws_dynamodb_table" "tfstate" { 91 | name = format("%s%s%s%s", var.Prefix, "ddb", var.EnvCode, "tfstate") 92 | hash_key = "LockID" 93 | # Billing Mode depends on usage patterns. Change as appropriate 94 | # read_capacity = 20 95 | # write_capacity = 20 96 | billing_mode = "PAY_PER_REQUEST" 97 | 98 | attribute { 99 | name = "LockID" 100 | type = "S" 101 | } 102 | 103 | point_in_time_recovery { 104 | enabled = true 105 | } 106 | 107 | server_side_encryption { 108 | enabled = true 109 | } 110 | } 111 | 112 | # GitHub OIDC Configuration 113 | # https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services 114 | 115 | # Fetch latest TLS cert from GitHub to authenticate requests 116 | data "tls_certificate" "github" { 117 | url = "https://token.actions.githubusercontent.com/.well-known/openid-configuration" 118 | } 119 | 120 | # IAM policy to allow assume role 121 | data "aws_iam_policy_document" "ghaassumerole" { 122 | statement { 123 | actions = ["sts:AssumeRoleWithWebIdentity"] 124 | 125 | principals { 126 | type = "Federated" 127 | identifiers = ["${aws_iam_openid_connect_provider.github_actions.arn}"] 128 | } 129 | 130 | condition { 131 | test = "StringEquals" 132 | variable = "token.actions.githubusercontent.com:aud" 133 | values = ["sts.amazonaws.com"] 134 | } 135 | 136 | condition { 137 | test = "StringEquals" 138 | variable = "token.actions.githubusercontent.com:sub" 139 | values = ["repo:${var.GitHubOrg}/${var.GitHubRepo}:environment:${var.GitHubEnv}"] 140 | } 141 | } 142 | } 143 | 144 | # IAM policy allowing Github to create and manage AWS resources 145 | data "aws_iam_policy_document" "TerraformState" { 146 | # Terraform state Amazon S3 access 147 | statement { 148 | actions = [ 149 | "s3:List*", 150 | "s3:Get*", 151 | "s3:Put*", 152 | "s3:DeleteObject", 153 | "s3:DeleteObjectVersion" 154 | ] 155 | resources = [ 156 | "${aws_s3_bucket.tfstate.arn}", 157 | "${aws_s3_bucket.tfstate.arn}/*" 158 | ] 159 | } 160 | # Terraform state Amazon DynamoDB access 161 | statement { 162 | actions = [ 163 | "dynamodb:BatchGetItem", 164 | "dynamodb:Query", 165 | "dynamodb:PutItem", 166 | "dynamodb:UpdateItem", 167 | "dynamodb:DeleteItem", 168 | "dynamodb:BatchWriteItem", 169 | "dynamodb:Describe*", 170 | "dynamodb:Get*", 171 | "dynamodb:List*" 172 | ] 173 | resources = [ 174 | "${aws_dynamodb_table.tfstate.arn}" 175 | ] 176 | } 177 | } 178 | 179 | # Create OIDC Provider for GitHub Actions 180 | resource "aws_iam_openid_connect_provider" "github_actions" { 181 | url = "https://token.actions.githubusercontent.com" 182 | client_id_list = ["sts.amazonaws.com"] 183 | thumbprint_list = [data.tls_certificate.github.certificates[0].sha1_fingerprint] 184 | } 185 | 186 | # Define IAM Role configured for assuming the role with web identity, tailored for GitHub Actions 187 | resource "aws_iam_role" "github_actions" { 188 | name = format("%s%s%s%s", var.Prefix, "iar", var.EnvCode, "gha") 189 | assume_role_policy = data.aws_iam_policy_document.ghaassumerole.json 190 | 191 | inline_policy { 192 | name = format("%s%s%s%s", var.Prefix, "iap", var.EnvCode, "TerraformState") 193 | policy = data.aws_iam_policy_document.TerraformState.json 194 | } 195 | inline_policy { 196 | name = format("%s%s%s%s", var.Prefix, "iap", var.EnvCode, "SampleApp") 197 | policy = data.aws_iam_policy_document.SampleApp.json 198 | } 199 | 200 | tags = { 201 | Name = format("%s%s%s%s", var.Prefix, "iar", var.EnvCode, "gha") 202 | rtype = "security" 203 | } 204 | } 205 | 206 | # Outputs used to create GitHub resources 207 | output "gha_iam_role" { 208 | value = aws_iam_role.github_actions.arn 209 | } 210 | output "tfstate_bucket_name" { 211 | value = aws_s3_bucket.tfstate.bucket 212 | } 213 | output "tfstate_dynamodb_table_name" { 214 | value = aws_dynamodb_table.tfstate.name 215 | } -------------------------------------------------------------------------------- /bootstrap/pipelinebuild/modules/tfbootstrap/sampleappIAM.tf: -------------------------------------------------------------------------------- 1 | # This file contains IAM permissions required to deploy and destroy the sample .NET Core application. 2 | # WARNING: Consider restricting to actions and resources required by GitHub to deploy your solution through the GitHub Actions pipeline 3 | data "aws_caller_identity" "current" {} 4 | data "aws_iam_policy_document" "SampleApp" { 5 | statement { 6 | actions = [ 7 | "cloudformation:DescribeStacks", 8 | "ec2:CreateTags", 9 | "ec2:DescribeAccountAttributes", 10 | "ec2:DescribeAddresses", 11 | "ec2:DescribeFlowLogs", 12 | "ec2:DescribeInternetGateways", 13 | "ec2:DescribeNatGateways", 14 | "ec2:DescribeNetworkAcls", 15 | "ec2:DescribeNetworkInterfaces", 16 | "ec2:DescribeRouteTables", 17 | "ec2:DescribeSecurityGroups", 18 | "ec2:DescribeSubnets", 19 | "ec2:DescribeVpcs", 20 | "ec2:DisassociateAddress", 21 | "ec2:DisassociateRouteTable", 22 | "ec2:ReleaseAddress", 23 | "ecr:CreateRepository", 24 | "ecr:DescribeRepositories", 25 | "ecr:GetAuthorizationToken", 26 | "ecs:CreateCluster", 27 | "ecs:DescribeTaskDefinition", 28 | "ecs:DeregisterTaskDefinition", 29 | "elasticloadbalancing:CreateListener", 30 | "elasticloadbalancing:CreateLoadBalancer", 31 | "elasticloadbalancing:DescribeListeners", 32 | "elasticloadbalancing:DescribeLoadBalancerAttributes", 33 | "elasticloadbalancing:DescribeLoadBalancers", 34 | "elasticloadbalancing:DescribeTags", 35 | "elasticloadbalancing:DescribeTargetGroupAttributes", 36 | "elasticloadbalancing:DescribeTargetGroups", 37 | "kms:CreateKey", 38 | "kms:ListAliases", 39 | "logs:DescribeLogGroups", 40 | "resource-groups:CreateGroup" 41 | ] 42 | resources = [ 43 | "*" 44 | ] 45 | } 46 | statement { 47 | actions = [ 48 | "ec2:AllocateAddress", 49 | "ec2:CreateNatGateway" 50 | ] 51 | resources = [ 52 | "arn:aws:ec2:${var.Region}:${data.aws_caller_identity.current.account_id}:elastic-ip/*" 53 | ] 54 | } 55 | statement { 56 | actions = [ 57 | "ec2:DetachNetworkInterface" 58 | ] 59 | resources = [ 60 | "arn:aws:ec2:${var.Region}:${data.aws_caller_identity.current.account_id}:instance/*" 61 | ] 62 | } 63 | statement { 64 | actions = [ 65 | "ec2:AttachInternetGateway", 66 | "ec2:CreateInternetGateway", 67 | "ec2:DeleteInternetGateway", 68 | "ec2:DetachInternetGateway" 69 | ] 70 | resources = [ 71 | "arn:aws:ec2:${var.Region}:${data.aws_caller_identity.current.account_id}:internet-gateway/*" 72 | ] 73 | } 74 | statement { 75 | actions = [ 76 | "ec2:CreateNatGateway", 77 | "ec2:DeleteNatGateway" 78 | ] 79 | resources = [ 80 | "arn:aws:ec2:${var.Region}:${data.aws_caller_identity.current.account_id}:natgateway/*" 81 | ] 82 | } 83 | statement { 84 | actions = [ 85 | "ec2:DetachNetworkInterface" 86 | ] 87 | resources = [ 88 | "arn:aws:ec2:${var.Region}:${data.aws_caller_identity.current.account_id}:network-interface/*" 89 | ] 90 | } 91 | statement { 92 | actions = [ 93 | "ec2:AssociateRouteTable", 94 | "ec2:CreateRoute", 95 | "ec2:CreateRouteTable", 96 | "ec2:DeleteRouteTable" 97 | ] 98 | resources = [ 99 | "arn:aws:ec2:${var.Region}:${data.aws_caller_identity.current.account_id}:route-table/*" 100 | ] 101 | } 102 | statement { 103 | actions = [ 104 | "ec2:AuthorizeSecurityGroupEgress", 105 | "ec2:AuthorizeSecurityGroupIngress", 106 | "ec2:CreateSecurityGroup", 107 | "ec2:DeleteSecurityGroup", 108 | "ec2:RevokeSecurityGroupEgress" 109 | ] 110 | resources = [ 111 | "arn:aws:ec2:${var.Region}:${data.aws_caller_identity.current.account_id}:security-group/*" 112 | ] 113 | } 114 | statement { 115 | actions = [ 116 | "ec2:AssociateRouteTable", 117 | "ec2:CreateNatGateway", 118 | "ec2:CreateSubnet", 119 | "ec2:DeleteSubnet" 120 | ] 121 | resources = [ 122 | "arn:aws:ec2:${var.Region}:${data.aws_caller_identity.current.account_id}:subnet/*" 123 | ] 124 | } 125 | statement { 126 | actions = [ 127 | "ec2:CreateFlowLogs", 128 | "ec2:DeleteFlowLogs" 129 | ] 130 | resources = [ 131 | "arn:aws:ec2:${var.Region}:${data.aws_caller_identity.current.account_id}:vpc-flow-log/*" 132 | ] 133 | } 134 | statement { 135 | actions = [ 136 | "ec2:AttachInternetGateway", 137 | "ec2:CreateFlowLogs", 138 | "ec2:CreateRouteTable", 139 | "ec2:CreateSecurityGroup", 140 | "ec2:CreateSubnet", 141 | "ec2:CreateVpc", 142 | "ec2:DeleteVpc", 143 | "ec2:DescribeVpcAttribute", 144 | "ec2:DetachInternetGateway" 145 | ] 146 | resources = [ 147 | "arn:aws:ec2:${var.Region}:${data.aws_caller_identity.current.account_id}:vpc/*" 148 | ] 149 | } 150 | statement { 151 | actions = [ 152 | "ecr:BatchCheckLayerAvailability", 153 | "ecr:CompleteLayerUpload", 154 | "ecr:DeleteLifecyclePolicy", 155 | "ecr:DeleteRepository", 156 | "ecr:DescribeImages", 157 | "ecr:GetLifecyclePolicy", 158 | "ecr:InitiateLayerUpload", 159 | "ecr:ListTagsForResource", 160 | "ecr:PutImage", 161 | "ecr:PutLifecyclePolicy", 162 | "ecr:TagResource", 163 | "ecr:UploadLayerPart" 164 | ] 165 | resources = [ 166 | "arn:aws:ecr:${var.Region}:${data.aws_caller_identity.current.account_id}:repository/*" 167 | ] 168 | } 169 | statement { 170 | actions = [ 171 | "ecs:DeleteCluster", 172 | "ecs:DescribeClusters", 173 | "ecs:TagResource" 174 | ] 175 | resources = [ 176 | "arn:aws:ecs:${var.Region}:${data.aws_caller_identity.current.account_id}:cluster/*" 177 | ] 178 | } 179 | statement { 180 | actions = [ 181 | "ecs:CreateService", 182 | "ecs:DeleteService", 183 | "ecs:DescribeServices", 184 | "ecs:TagResource", 185 | "ecs:UpdateService" 186 | ] 187 | resources = [ 188 | "arn:aws:ecs:${var.Region}:${data.aws_caller_identity.current.account_id}:service/*/*" 189 | ] 190 | } 191 | statement { 192 | actions = [ 193 | "ecs:DeregisterTaskDefinition", 194 | "ecs:RegisterTaskDefinition", 195 | "ecs:TagResource" 196 | ] 197 | resources = [ 198 | "arn:aws:ecs:${var.Region}:${data.aws_caller_identity.current.account_id}:task-definition/*:*" 199 | ] 200 | } 201 | statement { 202 | actions = [ 203 | "elasticloadbalancing:AddTags", 204 | "elasticloadbalancing:DeleteListener", 205 | ] 206 | resources = [ 207 | "arn:aws:elasticloadbalancing:${var.Region}:${data.aws_caller_identity.current.account_id}:listener/app/*/*/*" 208 | ] 209 | } 210 | statement { 211 | actions = [ 212 | "elasticloadbalancing:AddTags", 213 | "elasticloadbalancing:DeleteLoadBalancer", 214 | "elasticloadbalancing:ModifyLoadBalancerAttributes" 215 | ] 216 | resources = [ 217 | "arn:aws:elasticloadbalancing:${var.Region}:${data.aws_caller_identity.current.account_id}:loadbalancer/*" 218 | ] 219 | } 220 | statement { 221 | actions = [ 222 | "elasticloadbalancing:AddTags", 223 | "elasticloadbalancing:CreateTargetGroup", 224 | "elasticloadbalancing:DeleteTargetGroup", 225 | "elasticloadbalancing:ModifyTargetGroupAttributes" 226 | ] 227 | resources = [ 228 | "arn:aws:elasticloadbalancing:${var.Region}:${data.aws_caller_identity.current.account_id}:targetgroup/*/*" 229 | ] 230 | } 231 | statement { 232 | actions = [ 233 | "iam:CreateServiceLinkedRole", 234 | "iam:CreateRole", 235 | "iam:DeleteRole", 236 | "iam:DeleteRolePolicy", 237 | "iam:GetRole", 238 | "iam:GetRolePolicy", 239 | "iam:ListAttachedRolePolicies", 240 | "iam:ListInstanceProfilesForRole", 241 | "iam:ListRolePolicies", 242 | "iam:PassRole", 243 | "iam:PutRolePolicy", 244 | "iam:TagRole" 245 | ] 246 | resources = [ 247 | "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/*" 248 | ] 249 | } 250 | statement { 251 | actions = [ 252 | "kms:CreateAlias", 253 | "kms:DeleteAlias" 254 | ] 255 | resources = [ 256 | "arn:aws:kms:${var.Region}:${data.aws_caller_identity.current.account_id}:alias/*" 257 | ] 258 | } 259 | statement { 260 | actions = [ 261 | "kms:CreateAlias", 262 | "kms:CreateGrant", 263 | "kms:Decrypt", 264 | "kms:DeleteAlias", 265 | "kms:DescribeKey", 266 | "kms:EnableKeyRotation", 267 | "kms:GetKeyPolicy", 268 | "kms:GetKeyRotationStatus", 269 | "kms:ListResourceTags", 270 | "kms:PutKeyPolicy", 271 | "kms:RetireGrant", 272 | "kms:ScheduleKeyDeletion", 273 | "kms:TagResource" 274 | ] 275 | resources = [ 276 | "arn:aws:kms:${var.Region}:${data.aws_caller_identity.current.account_id}:key/*" 277 | ] 278 | } 279 | statement { 280 | actions = [ 281 | "logs:CreateLogGroup", 282 | "logs:DeleteLogGroup", 283 | "logs:ListTagsLogGroup", 284 | "logs:PutRetentionPolicy", 285 | "logs:TagResource" 286 | ] 287 | resources = [ 288 | "arn:aws:logs:${var.Region}:${data.aws_caller_identity.current.account_id}:log-group:*" 289 | ] 290 | } 291 | statement { 292 | actions = [ 293 | "resource-groups:DeleteGroup", 294 | "resource-groups:GetGroup", 295 | "resource-groups:GetGroupConfiguration", 296 | "resource-groups:GetGroupQuery", 297 | "resource-groups:GetTags", 298 | "resource-groups:Tag" 299 | ] 300 | resources = [ 301 | "arn:aws:resource-groups:${var.Region}:${data.aws_caller_identity.current.account_id}:group/*" 302 | ] 303 | } 304 | statement { 305 | actions = [ 306 | "s3:CreateBucket", 307 | "s3:DeleteBucket", 308 | "s3:DeleteBucketPolicy", 309 | "s3:DeleteObject", 310 | "s3:DeleteObjectVersion", 311 | "s3:Get*", 312 | "s3:List*", 313 | "s3:Put*", 314 | ] 315 | resources = [ 316 | "arn:aws:s3:::*" 317 | ] 318 | } 319 | } 320 | 321 | # An example of a less restrictive policy WARNING: Use with caution 322 | # statement { 323 | # actions = [ 324 | # "resource-groups:*", 325 | # "s3:*", 326 | # "iam:*", 327 | # "kms:*", 328 | # "ec2:*", 329 | # "elasticloadbalancing:*", 330 | # "cloudwatch:*", 331 | # "logs:*", 332 | # "ecr:*", 333 | # "ecs:*" 334 | # ] 335 | # resources = [ 336 | # "*" 337 | # ] 338 | # } 339 | # } -------------------------------------------------------------------------------- /bootstrap/pipelinebuild/modules/tfbootstrap/variables.tf: -------------------------------------------------------------------------------- 1 | # Account Settings 2 | variable "Region" { 3 | description = "AWS deployment region" 4 | type = string 5 | } 6 | 7 | # Tagging and naming 8 | variable "Prefix" { 9 | description = "Prefix used to name all resources" 10 | type = string 11 | } 12 | variable "EnvCode" { 13 | description = "Identifier for the environment" 14 | type = string 15 | } 16 | 17 | # Github Settings 18 | variable "GitHubOrg" { 19 | description = "GitHub Organization / GitHub username" 20 | type = string 21 | } 22 | variable "GitHubRepo" { 23 | description = "GitHub repository name" 24 | type = string 25 | } 26 | variable "GitHubEnv" { 27 | description = "GitHub Environment for commits" 28 | type = string 29 | } -------------------------------------------------------------------------------- /bootstrap/pipelinebuild/modules/tfbootstrap/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 5.38.0" 6 | } 7 | } 8 | 9 | required_version = ">= 1.7.1" 10 | } -------------------------------------------------------------------------------- /bootstrap/pipelinebuild/provider.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | github = { 4 | source = "integrations/github" 5 | version = "~> 6.0.0" 6 | } 7 | } 8 | } 9 | 10 | provider "github" { 11 | owner = "" 12 | } 13 | 14 | provider "aws" { 15 | alias = "development" 16 | profile = "default" 17 | region = var.Region 18 | 19 | assume_role { 20 | 21 | role_arn = var.dev_role_arn 22 | } 23 | default_tags { 24 | tags = { 25 | Environment = "Development" 26 | Provisioner = "Terraform" 27 | Solution = "AWS-GHA-TF-MSFT" 28 | } 29 | } 30 | } 31 | 32 | provider "aws" { 33 | alias = "testing" 34 | profile = "default" 35 | region = var.Region 36 | 37 | assume_role { 38 | role_arn = var.test_role_arn 39 | } 40 | default_tags { 41 | tags = { 42 | Environment = "Testing" 43 | Provisioner = "Terraform" 44 | } 45 | } 46 | } 47 | 48 | provider "aws" { 49 | alias = "production" 50 | profile = "default" 51 | region = var.Region 52 | 53 | assume_role { 54 | role_arn = var.prod_role_arn 55 | } 56 | 57 | default_tags { 58 | tags = { 59 | Environment = "Production" 60 | Provisioner = "Terraform" 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /bootstrap/pipelinebuild/terraform.tfvars: -------------------------------------------------------------------------------- 1 | # Regions 2 | Region = "" 3 | 4 | # Tagging and Naming 5 | Prefix = "tfboot" 6 | 7 | # Assumed role ARNs 8 | dev_role_arn = "" 9 | test_role_arn = "" 10 | prod_role_arn = "" 11 | 12 | # GitHub Settings 13 | GitHubOrg = "" 14 | GitHubRepo = "" 15 | InfraCostAPIKey = "" -------------------------------------------------------------------------------- /bootstrap/pipelinebuild/variables.tf: -------------------------------------------------------------------------------- 1 | # Regions 2 | variable "Region" { 3 | description = "AWS deployment region" 4 | type = string 5 | } 6 | 7 | # Tagging and naming 8 | variable "Prefix" { 9 | description = "Prefix used to name all resources" 10 | type = string 11 | } 12 | 13 | # Assumed Role ARNs 14 | variable "dev_role_arn" { 15 | description = "Dev environment deployment role created in bootstrap/accountsetup" 16 | type = string 17 | } 18 | variable "test_role_arn" { 19 | description = "Test environment deployment role created in bootstrap/accountsetup" 20 | type = string 21 | } 22 | variable "prod_role_arn" { 23 | description = "Prod environment deployment role created in bootstrap/accountsetup" 24 | type = string 25 | } 26 | 27 | # GitHub Settings 28 | variable "GitHubOrg" { 29 | description = "GitHub Organization / GitHub username" 30 | type = string 31 | } 32 | variable "GitHubRepo" { 33 | description = "GitHub repository name" 34 | type = string 35 | } 36 | variable "InfraCostAPIKey" { 37 | description = "InfraCost API key follow instructions here: https://github.com/infracost/actions?tab=readme-ov-file#quick-start" 38 | type = string 39 | } -------------------------------------------------------------------------------- /build/appsrc/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official .NET Core runtime image for Linux as the base image 2 | FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine 3 | 4 | # Set the working directory inside the container 5 | WORKDIR /app 6 | 7 | # Copy the contents of your .NET web application to the container's working directory 8 | COPY . . 9 | 10 | # Expose port for the web application 11 | EXPOSE 8080 12 | 13 | # Set the entry point to run the web application when the container starts 14 | ENTRYPOINT ["dotnet", "mswebapp.dll"] -------------------------------------------------------------------------------- /build/appsrc/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.Http; 4 | 5 | var builder = WebApplication.CreateBuilder(args); 6 | var app = builder.Build(); 7 | 8 | app.MapGet("/", async context => 9 | { 10 | await context.Response.WriteAsync("Hello World! This is my $Environment Environment."); 11 | }); 12 | 13 | app.Run(); -------------------------------------------------------------------------------- /build/appsrc/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /build/appsrc/bin/Release/net8.0/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /build/appsrc/delete.me: -------------------------------------------------------------------------------- 1 | # I am a testing file. Edit me to trigger a new .NET build and publish Docker image to Amazon ECS. 2 | # The Moon is is approximately spherical in shape. -------------------------------------------------------------------------------- /build/appsrc/mswebapp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | a78a7b2a-b9cc-4124-9cef-5a19f78e6ef1 8 | Linux 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /build/appsrc/mswebapp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "mswebapp", "mswebapp.csproj", "{FBDD565F-0BD5-4244-BE5C-E67B83D37308}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(SolutionProperties) = preSolution 14 | HideSolutionNode = FALSE 15 | EndGlobalSection 16 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 17 | {FBDD565F-0BD5-4244-BE5C-E67B83D37308}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 18 | {FBDD565F-0BD5-4244-BE5C-E67B83D37308}.Debug|Any CPU.Build.0 = Debug|Any CPU 19 | {FBDD565F-0BD5-4244-BE5C-E67B83D37308}.Release|Any CPU.ActiveCfg = Release|Any CPU 20 | {FBDD565F-0BD5-4244-BE5C-E67B83D37308}.Release|Any CPU.Build.0 = Release|Any CPU 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /build/appsrc/obj/mswebapp.csproj.nuget.dgspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "format": 1, 3 | "restore": { 4 | "C:\\DATA\\REPO\\LIVE\\ghablog\\build\\appsrc\\mswebapp.csproj": {} 5 | }, 6 | "projects": { 7 | "C:\\DATA\\REPO\\LIVE\\ghablog\\build\\appsrc\\mswebapp.csproj": { 8 | "version": "1.0.0", 9 | "restore": { 10 | "projectUniqueName": "C:\\DATA\\REPO\\LIVE\\ghablog\\build\\appsrc\\mswebapp.csproj", 11 | "projectName": "mswebapp", 12 | "projectPath": "C:\\DATA\\REPO\\LIVE\\ghablog\\build\\appsrc\\mswebapp.csproj", 13 | "packagesPath": "C:\\Users\\momohob\\.nuget\\packages\\", 14 | "outputPath": "C:\\DATA\\REPO\\LIVE\\ghablog\\build\\appsrc\\obj\\", 15 | "projectStyle": "PackageReference", 16 | "configFilePaths": [ 17 | "C:\\Users\\momohob\\AppData\\Roaming\\NuGet\\NuGet.Config", 18 | "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config" 19 | ], 20 | "originalTargetFrameworks": [ 21 | "net8.0" 22 | ], 23 | "sources": { 24 | "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {} 25 | }, 26 | "frameworks": { 27 | "net8.0": { 28 | "targetAlias": "net8.0", 29 | "projectReferences": {} 30 | } 31 | }, 32 | "warningProperties": { 33 | "warnAsError": [ 34 | "NU1605" 35 | ] 36 | }, 37 | "restoreAuditProperties": { 38 | "enableAudit": "true", 39 | "auditLevel": "low", 40 | "auditMode": "direct" 41 | } 42 | }, 43 | "frameworks": { 44 | "net8.0": { 45 | "targetAlias": "net8.0", 46 | "imports": [ 47 | "net461", 48 | "net462", 49 | "net47", 50 | "net471", 51 | "net472", 52 | "net48", 53 | "net481" 54 | ], 55 | "assetTargetFallback": true, 56 | "warn": true, 57 | "frameworkReferences": { 58 | "Microsoft.AspNetCore.App": { 59 | "privateAssets": "none" 60 | }, 61 | "Microsoft.NETCore.App": { 62 | "privateAssets": "all" 63 | } 64 | }, 65 | "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.202/PortableRuntimeIdentifierGraph.json" 66 | } 67 | } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /build/appsrc/obj/mswebapp.csproj.nuget.g.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | True 5 | NuGet 6 | $(MSBuildThisFileDirectory)project.assets.json 7 | $(UserProfile)\.nuget\packages\ 8 | C:\Users\momohob\.nuget\packages\ 9 | PackageReference 10 | 6.9.1 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /build/appsrc/obj/mswebapp.csproj.nuget.g.targets: -------------------------------------------------------------------------------- 1 |  2 | -------------------------------------------------------------------------------- /build/appsrc/obj/project.assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "targets": { 4 | "net8.0": {} 5 | }, 6 | "libraries": {}, 7 | "projectFileDependencyGroups": { 8 | "net8.0": [] 9 | }, 10 | "packageFolders": { 11 | "C:\\Users\\momohob\\.nuget\\packages\\": {} 12 | }, 13 | "project": { 14 | "version": "1.0.0", 15 | "restore": { 16 | "projectUniqueName": "C:\\DATA\\REPO\\LIVE\\ghablog\\build\\appsrc\\mswebapp.csproj", 17 | "projectName": "mswebapp", 18 | "projectPath": "C:\\DATA\\REPO\\LIVE\\ghablog\\build\\appsrc\\mswebapp.csproj", 19 | "packagesPath": "C:\\Users\\momohob\\.nuget\\packages\\", 20 | "outputPath": "C:\\DATA\\REPO\\LIVE\\ghablog\\build\\appsrc\\obj\\", 21 | "projectStyle": "PackageReference", 22 | "configFilePaths": [ 23 | "C:\\Users\\momohob\\AppData\\Roaming\\NuGet\\NuGet.Config", 24 | "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config" 25 | ], 26 | "originalTargetFrameworks": [ 27 | "net8.0" 28 | ], 29 | "sources": { 30 | "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {} 31 | }, 32 | "frameworks": { 33 | "net8.0": { 34 | "targetAlias": "net8.0", 35 | "projectReferences": {} 36 | } 37 | }, 38 | "warningProperties": { 39 | "warnAsError": [ 40 | "NU1605" 41 | ] 42 | }, 43 | "restoreAuditProperties": { 44 | "enableAudit": "true", 45 | "auditLevel": "low", 46 | "auditMode": "direct" 47 | } 48 | }, 49 | "frameworks": { 50 | "net8.0": { 51 | "targetAlias": "net8.0", 52 | "imports": [ 53 | "net461", 54 | "net462", 55 | "net47", 56 | "net471", 57 | "net472", 58 | "net48", 59 | "net481" 60 | ], 61 | "assetTargetFallback": true, 62 | "warn": true, 63 | "frameworkReferences": { 64 | "Microsoft.AspNetCore.App": { 65 | "privateAssets": "none" 66 | }, 67 | "Microsoft.NETCore.App": { 68 | "privateAssets": "all" 69 | } 70 | }, 71 | "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.202/PortableRuntimeIdentifierGraph.json" 72 | } 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /build/ecscluster.tf: -------------------------------------------------------------------------------- 1 | ### Create Amazon ECS Cluster 2 | 3 | data "aws_caller_identity" "current" {} 4 | 5 | data "aws_iam_policy_document" "mswebappkms" { 6 | statement { 7 | # https://docs.aws.amazon.com/kms/latest/developerguide/key-policy-overview.html 8 | sid = "Enable IAM User Permissions" 9 | effect = "Allow" 10 | 11 | principals { 12 | type = "AWS" 13 | identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"] 14 | } 15 | actions = [ 16 | "kms*" 17 | ] 18 | resources = [ 19 | "*" 20 | ] 21 | } 22 | statement { 23 | # https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/encrypt-log-data-kms.html 24 | sid = "Allow Cloudwatch access to KMS Key" 25 | effect = "Allow" 26 | 27 | principals { 28 | type = "Service" 29 | identifiers = ["logs.${var.Region}.amazonaws.com"] 30 | } 31 | actions = [ 32 | "kms:Encrypt*", 33 | "kms:Decrypt*", 34 | "kms:ReEncrypt*", 35 | "kms:GenerateDataKey*", 36 | "kms:Describe*" 37 | ] 38 | resources = [ 39 | "*" 40 | ] 41 | condition { 42 | test = "ArnLike" 43 | variable = "kms:EncryptionContext:aws:logs:arn" 44 | values = [ 45 | "arn:aws:logs:${var.Region}:${data.aws_caller_identity.current.account_id}:*" 46 | ] 47 | } 48 | } 49 | } 50 | 51 | # Create KMS key for solution 52 | resource "aws_kms_key" "mswebapp" { 53 | description = "KMS key to secure various aspects of an example Microsoft .NET web application" 54 | deletion_window_in_days = 7 55 | enable_key_rotation = true 56 | policy = data.aws_iam_policy_document.mswebappkms.json 57 | 58 | tags = { 59 | Name = format("%s%s%s%s", var.Prefix, "kms", var.EnvCode, "mswebapp") 60 | resourcetype = "security" 61 | codeblock = "ecscluster" 62 | } 63 | } 64 | 65 | # Create KMS Alias. Only used in this context to provide a friendly display name 66 | resource "aws_kms_alias" "mswebapp" { 67 | name = "alias/mswebapp" 68 | target_key_id = aws_kms_key.mswebapp.key_id 69 | } 70 | 71 | # Create CloudWatch log group for ECS logs 72 | resource "aws_cloudwatch_log_group" "ecscluster" { 73 | name = format("%s%s%s%s", var.Prefix, "cwl", var.EnvCode, "ecscluster") 74 | retention_in_days = 90 75 | kms_key_id = aws_kms_key.mswebapp.arn 76 | 77 | tags = { 78 | Name = format("%s%s%s%s", var.Prefix, "cwl", var.EnvCode, "ecscluster") 79 | resourcetype = "monitor" 80 | codeblock = "ecscluster" 81 | } 82 | } 83 | 84 | # Create CloudWatch log group for Application logs 85 | resource "aws_cloudwatch_log_group" "mswebapp" { 86 | name = format("%s%s%s%s", var.Prefix, "cwl", var.EnvCode, "mswebapp") 87 | retention_in_days = 30 88 | kms_key_id = aws_kms_key.mswebapp.arn 89 | 90 | tags = { 91 | Name = format("%s%s%s%s", var.Prefix, "cwl", var.EnvCode, "mswebapp") 92 | resourcetype = "monitor" 93 | codeblock = "ecscluster" 94 | } 95 | } 96 | 97 | # Create Amazon ECR repository to store Docker image 98 | resource "aws_ecr_repository" "mswebapp" { 99 | name = var.ECRRepo 100 | image_tag_mutability = "MUTABLE" 101 | force_delete = true 102 | 103 | image_scanning_configuration { 104 | scan_on_push = true 105 | } 106 | 107 | encryption_configuration { 108 | encryption_type = "KMS" 109 | kms_key = aws_kms_key.mswebapp.arn 110 | } 111 | 112 | tags = { 113 | Name = format("%s%s%s%s", var.Prefix, "ecs", var.EnvCode, "mswebapp") 114 | resourcetype = "compute" 115 | codeblock = "ecscluster" 116 | } 117 | } 118 | 119 | # Create ECR lifecycle policy to delete untagged images after 1 day 120 | resource "aws_ecr_lifecycle_policy" "mswebapp" { 121 | repository = aws_ecr_repository.mswebapp.name 122 | 123 | policy = <