diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..5bef95b --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,21 @@ +#### ๐Ÿ”— Jira ticket + +CCAP-XXX + +#### โœ๏ธ Description + + + +#### ๐Ÿ“ท Design reference + + + +#### ๐Ÿงช Testing instructions + +- [ ] Step 1... +- [ ] Step 2... + +#### โœ… Completion tasks + +- [ ] Added relevant tests +- [ ] Meets acceptance criteria diff --git a/.github/workflows/branch.yml b/.github/workflows/branch.yml new file mode 100644 index 0000000..0a40df0 --- /dev/null +++ b/.github/workflows/branch.yml @@ -0,0 +1,117 @@ +name: Branch Checks + +on: + push: + branches-ignore: + - main + +jobs: + find-modules: + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Find all terraform modules + id: find + uses: bendrucker/find-terraform-modules@v1 + with: + working-directory: tofu + + - name: Show all matching modules + shell: bash + run: | + mods=(${{ join(fromJSON(steps.find.outputs.modules), ' ') }}) + printf "%s\n" "${mods[@]}" + + - name: Find all changed files + id: diff + uses: technote-space/get-diff-action@v6 + with: + FORMAT: json + + - name: Show changed files + run: | + echo "${{ steps.diff.outputs.diff }}" + + - name: Get the modified modules + id: modified + uses: actions/github-script@v7 + with: + script: | + const modules = ${{ steps.find.outputs.modules }} + const diff = ${{ steps.diff.outputs.diff }} + const modifiedModules = modules.filter( + (module) => { + return !!diff.find(file => new RegExp(`^${module}/.+`).test(file)) + } + ) + + core.setOutput('modules', modifiedModules) + + - name: Show modified modules + run: | + echo "${{ steps.modified.outputs.modules }}" + outputs: + modules: ${{ steps.modified.outputs.modules }} + + lint: + runs-on: ubuntu-latest + needs: find-modules + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - uses: actions/cache@v4 + name: Cache plugin directory + with: + path: ~/.tflint.d/plugins + key: tflint-${{ hashFiles('.tflint.hcl') }} + + - uses: terraform-linters/setup-tflint@v4 + name: Setup TFLint + + - name: Show version + run: tflint --version + + - name: Init TFLint + run: tflint --init + + # Use a bash script to run tflint on each modified module. + - name: Run TFLint + shell: bash + run: | + set +e + + exit_code=0 + modules=(${{ join(fromJSON(needs.find-modules.outputs.modules), ' ') }}) + for module in ${modules[@]} + do + echo "Linting module $module" + tflint --format compact --chdir $module + exit_code=$(( $? > exit_code ? $? : exit_code )) + done + + exit $exit_code + + trivy: + name: trivy + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v4 + - name: Run Trivy vulnarability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: config + ignore-unfixed: true + skip-dirs: '"**/*/.terraform"' + exit-code: 1 + format: sarif + output: 'trivy-results.sarif' + + - name: Parse SARIF file + if: always() + uses: Ayrx/sarif_to_github_annotations@v0.2.2 + with: + sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..e17d18d --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,45 @@ +name: Deploy pipeline + +on: + workflow_dispatch: + inputs: + environment: + description: 'Environment to deploy to' + default: 'staging' + required: true + type: environment + config: + description: 'The OpenTofu configuration to plan' + default: 'staging' + required: true + type: choice + options: + - staging + +jobs: + deploy: + runs-on: ubuntu-latest + environment: ${{ github.event.inputs.environment }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Setup OpenTofu + uses: opentofu/setup-opentofu@v1 + + - name: Initialize OpenTofu + working-directory: ./tofu/config/${{ inputs.config }} + run: tofu init + + # TODO: Add a manual approval step here. For now, we'll use GitHub + # Actions' environment protection feature for sensitive environments. + - name: Apply changes + working-directory: ./tofu/config/${{ inputs.config }} + run: tofu apply --auto-approve diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml new file mode 100644 index 0000000..0e2c086 --- /dev/null +++ b/.github/workflows/main.yaml @@ -0,0 +1,117 @@ +name: Main Checks + +on: + push: + branches: + - main + +jobs: + find-modules: + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Find all terraform modules + id: find + uses: bendrucker/find-terraform-modules@v1 + with: + working-directory: tofu + + - name: Show all matching modules + shell: bash + run: | + mods=(${{ join(fromJSON(steps.find.outputs.modules), ' ') }}) + printf "%s\n" "${mods[@]}" + + - name: Find all changed files + id: diff + uses: technote-space/get-diff-action@v6 + with: + FORMAT: json + + - name: Show changed files + run: | + echo "${{ steps.diff.outputs.diff }}" + + - name: Get the modified modules + id: modified + uses: actions/github-script@v7 + with: + script: | + const modules = ${{ steps.find.outputs.modules }} + const diff = ${{ steps.diff.outputs.diff }} + const modifiedModules = modules.filter( + (module) => { + return !!diff.find(file => new RegExp(`^${module}/.+`).test(file)) + } + ) + + core.setOutput('modules', modifiedModules) + + - name: Show modified modules + run: | + echo "${{ steps.modified.outputs.modules }}" + outputs: + modules: ${{ steps.modified.outputs.modules }} + + lint: + runs-on: ubuntu-latest + needs: find-modules + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - uses: actions/cache@v4 + name: Cache plugin directory + with: + path: ~/.tflint.d/plugins + key: tflint-${{ hashFiles('.tflint.hcl') }} + + - uses: terraform-linters/setup-tflint@v4 + name: Setup TFLint + + - name: Show version + run: tflint --version + + - name: Init TFLint + run: tflint --init + + # Use a bash script to run tflint on each modified module. + - name: Run TFLint + shell: bash + run: | + set +e + + exit_code=0 + modules=(${{ join(fromJSON(needs.find-modules.outputs.modules), ' ') }}) + for module in ${modules[@]} + do + echo "Linting module $module" + tflint --format compact --chdir $module + exit_code=$(( $? > exit_code ? $? : exit_code )) + done + + exit $exit_code + + trivy: + name: trivy + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v4 + - name: Run Trivy vulnarability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: config + ignore-unfixed: true + skip-dirs: '**/*/.terraform' + exit-code: 1 + format: sarif + output: 'trivy-results.sarif' + + - name: Parse SARIF file + if: always() + uses: Ayrx/sarif_to_github_annotations@v0.2.2 + with: + sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/plan.yaml b/.github/workflows/plan.yaml new file mode 100644 index 0000000..0302a1e --- /dev/null +++ b/.github/workflows/plan.yaml @@ -0,0 +1,69 @@ +name: Plan the deployment pipeline + +on: + workflow_call: + inputs: + environment: + description: 'Environment to plan on' + default: 'staging' + required: true + type: string + config: + description: 'The OpenTofu configuration to plan' + default: 'staging' + required: true + type: string + outputs: + plan: + description: "The plan output from the tofu plan command" + value: ${{ jobs.plan.outputs.plan }} + secrets: + AWS_ACCESS_KEY_ID: + required: true + AWS_SECRET_ACCESS_KEY: + required: true + workflow_dispatch: + inputs: + environment: + description: 'Environment to plan on' + default: 'staging' + required: true + type: environment + config: + description: 'The OpenTofu configuration to plan' + required: true + type: choice + options: + - staging + +jobs: + plan: + runs-on: ubuntu-latest + environment: ${{ inputs.environment }} + outputs: + plan: ${{ steps.plan.outputs.stdout }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Setup OpenTofu + uses: opentofu/setup-opentofu@v1 + + - name: Initialize OpenTofu + working-directory: ./tofu/config/${{ inputs.config }} + run: tofu init + + - name: Plan changes + id: plan + working-directory: ./tofu/config/${{ inputs.config }} + run: tofu plan -no-color + + - name: Display plan + run: echo "${{ steps.plan.outputs.stdout }}" diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml new file mode 100644 index 0000000..391040b --- /dev/null +++ b/.github/workflows/pull-request.yaml @@ -0,0 +1,53 @@ +name: Pull request checks + +on: + pull_request: + +jobs: + plan: + uses: ./.github/workflows/plan.yaml + with: + # TODO: Get the environments to plan on from the diff. + environment: staging + config: staging + secrets: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + + comment: + runs-on: ubuntu-latest + needs: plan + steps: + - uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + // Retrieve existing bot comments for the pull request. + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }) + const botComment = comments.find(comment => { + return comment.user.type === 'Bot' && comment.body.includes('## Plan output') + }) + + // Prepare the format of the comment. + const output = `## Plan output\n\n\`\`\`\n${{ needs.plan.outputs.plan }}\n\`\`\`` + + // If we have a comment, update it. Otherwise, create a new one. + if (botComment) { + github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: output + }) + } else { + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: output + }) + } diff --git a/.gitignore b/.gitignore index 9b8a46e..d2413c0 100644 --- a/.gitignore +++ b/.gitignore @@ -10,11 +10,12 @@ crash.log crash.*.log # Exclude all .tfvars files, which are likely to contain sensitive data, such as -# password, private keys, and other secrets. These should not be part of version -# control as they are data points which are potentially sensitive and subject +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject # to change depending on the environment. *.tfvars *.tfvars.json +.env # Ignore override files as they are usually used to override resources locally and so # are not checked in @@ -27,7 +28,7 @@ override.tf.json # !example_override.tf # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan -# example: *tfplan* +*tfplan* # Ignore CLI configuration files .terraformrc diff --git a/README.md b/README.md index f4fcc50..379e5c4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,65 @@ -# il-gcc-infra +# Illinois GetChildCare Infrastructure + Infrastructure configuration for the Illinois GetChildCare backend. + +## Requirements + +The configurations are written in [HCL] and support both [OpenTofu][tofu] and +the equivalent version of [Terraform]. + +## Usage + +### Local + +To run the configurations locally, you will need to have AWS credentials loaded +from [Identity Center][identity-center], and installed OpenTofu. + +Navigate to the configuration you would like to plan or apply, then run the +plan command to see what changes will be made: + +```bash +cd tofu/config/staging # Replace with the appropriate configuration +tofu init +tofu plan -out tfplan.out +``` + +Review the plan output. If the changes are acceptable, apply the changes: + +```bash +tofu apply tfplan.out +``` + +### GitHub Actions + +You can also run the configurations using GitHub Actions. There are two +workflows that can be called manually: [`plan.yaml`][plan] and +[`deploy.yaml`][deploy]. These workflows can be run directly from GitHub by +following their links. + +Additionally, these workflows can be triggered using the +[GitHub CLI][github-cli] (where `config` is the name of a directory under +`tofu/config/`): + +```bash +gh workflow run .yaml -f environment=staging -f config=staging +``` + +You can then run `gh workflow list --workflow plan.yaml` to see the status of +the execution and get its id. With this id, you can watch the run and get the +logs: + +```bash +gh run watch +gh run view --log +``` + +To run the workflow for a branch other than `main`, you can pass the +`--ref ` flag. + +[deploy]: https://github.com/codeforamerica/il-gcc-infra/actions/workflows/deploy.yaml +[github-cli]: https://cli.github.com/ +[hcl]: https://github.com/hashicorp/hcl +[identity-center]: https://www.notion.so/cfa/AWS-Identity-Center-e8a28122b2f44595a2ef56b46788ce2c +[plan]: https://github.com/codeforamerica/il-gcc-infra/actions/workflows/plan.yaml +[terraform]: https://www.terraform.io/ +[tofu]: https://opentofu.org/ diff --git a/tofu/config/staging/.terraform.lock.hcl b/tofu/config/staging/.terraform.lock.hcl new file mode 100644 index 0000000..9e91a95 --- /dev/null +++ b/tofu/config/staging/.terraform.lock.hcl @@ -0,0 +1,20 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/hashicorp/aws" { + version = "5.49.0" + constraints = ">= 5.44.0, ~> 5.44" + hashes = [ + "h1:AZ3scqlcBQlDVHe8nnUIpsE9VlcjD/uN86p9WSTPjfE=", + "zh:322e5ff7b3a1059b74d656eb16e56e4e0c5ac892fd80593959b4a1ffe558b794", + "zh:3a393b4b5b371dd390a1fc60d532cb0dd3bb4e092a3965f125f35e82ead704b1", + "zh:7b42169c170ce122ecdede71af8ab229b74b27be30f731a3e682db8d4bb80cb0", + "zh:7e2ac928ffcb4cf74eba3abb1e0b57fe3d19703a20068531c3329721dc0a4881", + "zh:9bc1df526041b3b0b773bb435015d04bdd12ae06e93a849cf60f0db5d8679971", + "zh:9db31718f4a1bb48633414ee960a13005d8c1f4504dbd8ec594b370f664ea94b", + "zh:abb6afcb0e16f0e1db7e122ae185cf214d7a3000eb9b223d980aed6e7a7b5853", + "zh:b78ebff83350ded37a7dbaf9124ce0fdf5e374eec645559cc0f8d72f2ac9327e", + "zh:cf0c600d1157487467df4813aa5b6e7201159e95ce7849f57bffedca92641bfa", + "zh:da8cd355307f653fb457b5e65a6bf465e1d88a36ef8388723833b3dce408d981", + ] +} diff --git a/tofu/config/staging/main.tf b/tofu/config/staging/main.tf new file mode 100644 index 0000000..87dc058 --- /dev/null +++ b/tofu/config/staging/main.tf @@ -0,0 +1,16 @@ +terraform { + backend "s3" { + bucket = "illinois-getchildcare-staging-tfstate" + key = "backend.tfstate" + region = "us-east-1" + } +} + +module "backend" { + # TODO: Create releases for tofu-modules and pin to a release. + # tflint-ignore: terraform_module_pinned_source + source = "github.com/codeforamerica/tofu-modules/aws/backend" + + project = "illinois-getchildcare" + environment = "staging" +} diff --git a/tofu/config/staging/providers.tf b/tofu/config/staging/providers.tf new file mode 100644 index 0000000..6a6da32 --- /dev/null +++ b/tofu/config/staging/providers.tf @@ -0,0 +1,10 @@ +provider "aws" { + region = "us-east-1" + + default_tags { + tags = { + project = "illinois-getchildcare" + environment = "staging" + } + } +} diff --git a/tofu/config/staging/versions.tf b/tofu/config/staging/versions.tf new file mode 100644 index 0000000..d31d078 --- /dev/null +++ b/tofu/config/staging/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.6" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.44" + } + } +}