diff --git a/flake.nix b/flake.nix index 4b95a7c..b995b50 100644 --- a/flake.nix +++ b/flake.nix @@ -118,7 +118,7 @@ # Package servers into images with a generator packages.x86_64-linux = with inputs; { - aws = import ./generators/aws.nix { + aws = import ./generators/aws { inherit nixpkgs nixos-generators home-manager globals; system = "x86_64-linux"; }; diff --git a/generators/aws.nix b/generators/aws/default.nix similarity index 100% rename from generators/aws.nix rename to generators/aws/default.nix diff --git a/generators/aws/main.tf b/generators/aws/main.tf new file mode 100644 index 0000000..4fbb2ca --- /dev/null +++ b/generators/aws/main.tf @@ -0,0 +1,80 @@ +locals { + image_file = one(fileset(path.root, "result/nixos-amazon-image-*.vhd")) +} + +# Upload to S3 +resource "aws_s3_object" "image" { + bucket = "your_bucket_name" + key = basename(local.image_file) + source = local.image_file + etag = filemd5(local.image_file) +} + +# Setup IAM access for the VM Importer +data "aws_iam_policy_document" "vmimport_trust_policy" { + statement { + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["vmie.amazonaws.com"] + } + } +} + +data "aws_iam_policy_document" "vmimport" { + statement { + actions = [ + "s3:GetBucketLocation", + "s3:GetObject", + "s3:ListBucket", + ] + resources = [ + "arn:aws:s3:::${aws_s3_object.image.bucket}", + "arn:aws:s3:::${aws_s3_object.image.bucket}/*", + ] + } + statement { + actions = [ + "ec2:ModifySnapshotAttribute", + "ec2:CopySnapshot", + "ec2:RegisterImage", + "ec2:Describe*", + ] + resources = ["*"] + } +} + +resource "aws_iam_role" "vmimport" { + name = "vmimport" + assume_role_policy = data.aws_iam_policy_document.vmimport_trust_policy.json + inline_policy { + name = "vmimport" + policy = data.aws_iam_policy_document.vmimport.json + } +} + +# Import to EBS +resource "aws_ebs_snapshot_import" "image" { + disk_container { + format = "VHD" + user_bucket { + s3_bucket = aws_s3_object.image.bucket + s3_key = aws_s3_object.image.key + } + } + + role_name = aws_iam_role.vmimport.name +} + +# Convert to AMI +resource "aws_ami" "image" { + description = "Created with NixOS." + name = replace(basename(local.image_file), "/\\.vhd$/", "") + virtualization_type = "hvm" + + ebs_block_device { + device_name = "/dev/xvda" + snapshot_id = aws_ebs_snapshot_import.image.id + volume_size = 8 + } +} diff --git a/generators/aws/workflow.yml b/generators/aws/workflow.yml new file mode 100644 index 0000000..c0210e2 --- /dev/null +++ b/generators/aws/workflow.yml @@ -0,0 +1,260 @@ +name: 'Terraform' +env: + + + AWS_ACCOUNT_NUMBER: '' + AWS_PLAN_ROLE_NAME: github_actions_plan + AWS_APPLY_ROLE_NAME: github_actions_admin + + # Always required. Used for authenticating to AWS, but can also act as your + # default region if you don't want to specify in the provider configuration. + AWS_REGION: us-east-1 + + # You must change these to fit your project. + TF_VAR_project: change-me + TF_VAR_label: change-me + TF_VAR_owner: Your Name Here + + # If storing Terraform in a subdirectory, specify it here. + TERRAFORM_DIRECTORY: . + + # Pinned versions of tools to use. + # Check for new releases: + # - https://github.com/hashicorp/terraform/releases + # - https://github.com/fugue/regula/releases + # - https://github.com/terraform-linters/tflint/releases + TERRAFORM_VERSION: 1.2.6 + REGULA_VERSION: 2.9.0 + TFLINT_VERSION: 0.39.1 + + # Terraform configuration options + TERRAFORM_PARALLELISM: 10 + + # These variables are passed to Terraform based on GitHub information. + TF_VAR_repo: ${{ github.repository }} + +# This workflow is triggered in the following ways. +on: + + # Any push or merge to these branches. + push: + branches: + - dev + - prod + + # Any pull request targeting these branches (plan only). + pull_request: + branches: + - dev + - prod + + + # Any manual trigger on these branches. + workflow_dispatch: + branches: + - dev + - prod + +# ------------------------------------------------------------------- +# The rest of this workflow can operate without adjustments. Edit the +# below content at your own risk! +# ------------------------------------------------------------------- + +# Used to connect to AWS IAM +permissions: + id-token: write + contents: read + pull-requests: write + +# Only run one workflow at a time for each Terraform state. This prevents +# lockfile conflicts, especially during PR vs push. +concurrency: terraform-${{ github.base_ref || github.ref }} + +jobs: + terraform: + + name: 'Terraform' + + # Change this if you need to run your deployment on-prem. + runs-on: ubuntu-latest + + steps: + + # Downloads the current repo code to the runner. + - name: Checkout Repo Code + uses: actions/checkout@v2 + + # Install Nix + - name: Install Nix + uses: cachix/install-nix-action@v17 + + # Build the image + - name: Build Image + run: nix build .#aws + + # Login to AWS + - name: AWS Assume Role + uses: aws-actions/configure-aws-credentials@v1.6.1 + with: + role-to-assume: ${{ env.AWS_ROLE_ARN }} + aws-region: ${{ env.AWS_REGION }} + + # Exports all GitHub Secrets as environment variables prefixed by + # "TF_VAR_", which exposes them to Terraform. The name of each GitHub + # Secret must match its Terraform variable name exactly. + - name: Export Secrets to Terraform Variables + env: + ALL_SECRETS: ${{ toJson(secrets) }} + run: | + echo "$ALL_SECRETS" \ + | jq "to_entries | .[] | \"TF_VAR_\" + ( .key | ascii_downcase ) + \"=\" + .value" \ + | tr -d \" >> $GITHUB_ENV + + # Installs the Terraform binary and some other accessory functions. + - name: Setup Terraform + uses: hashicorp/setup-terraform@v2 + with: + terraform_version: ${{ env.TERRAFORM_VERSION }} + + # Checks whether Terraform is formatted properly. If this fails, you + # should install the pre-commit hook. + - name: Check Formatting + run: | + terraform fmt -no-color -check -diff -recursive + + # Downloads a Terraform code lint test. + - uses: terraform-linters/setup-tflint@v1 + name: Setup TFLint + with: + tflint_version: v${{ env.TFLINT_VERSION }} + + # Sets up linting with this codebase. + - name: Init TFLint + working-directory: ${{ env.TERRAFORM_DIRECTORY }} + run: tflint --init + + # Lints the current code. + - name: Run TFLint + working-directory: ${{ env.TERRAFORM_DIRECTORY }} + run: | + tflint -f compact + find ./modules/* -type d -maxdepth 0 | xargs -I __ tflint -f compact --disable-rule=terraform_required_providers --disable-rule=terraform_required_version __ + + # Connects to remote state backend and download providers. + - name: Terraform Init + working-directory: ${{ env.TERRAFORM_DIRECTORY }} + run: | + terraform init \ + -backend-config="role_arn=${{ env.AWS_STATE_ROLE_ARN }}" \ + -backend-config="region=us-east-1" \ + -backend-config="workspace_key_prefix=accounts/${{ env.AWS_ACCOUNT_NUMBER }}/${{ github.repository }}" \ + -backend-config="key=state.tfstate" \ + -backend-config="dynamodb_table=global-tf-state-lock" + + # Set the Terraform Workspace to the current branch name. + - name: Set Terraform Workspace + working-directory: ${{ env.TERRAFORM_DIRECTORY }} + shell: bash + run: | + export WORKSPACE=${{ github.base_ref || github.ref_name }} + terraform workspace select ${WORKSPACE} || terraform workspace new $WORKSPACE + echo "TF_WORKSPACE=$(echo ${WORKSPACE} | sed 's/\//_/g')" >> $GITHUB_ENV + + # Checks differences between current code and infrastructure state. + - name: Terraform Plan + id: plan + working-directory: ${{ env.TERRAFORM_DIRECTORY }} + run: | + terraform plan \ + -input=false \ + -no-color \ + -out=tfplan \ + -parallelism=${TERRAFORM_PARALLELISM} \ + -var-file=variables-${TF_WORKSPACE}.tfvars + + # Gets the results of the plan for pull requests. + - name: Terraform Show Plan + id: show + working-directory: ${{ env.TERRAFORM_DIRECTORY }} + run: terraform show -no-color tfplan + + # Adds the results of the plan to the pull request. + - name: Comment Plan + uses: actions/github-script@v6 + if: github.event_name == 'pull_request' + env: + STDOUT: "```terraform\n${{ steps.show.outputs.stdout }}```" + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + // 1. Retrieve existing bot comments for the PR + 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('Terraform Format and Style') + }) + + // 2. Prepare format of the comment + const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\` + #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\` + #### Terraform Validation 🤖\`${{ steps.validate.outcome }}\` +
Validation Output + + \`\`\`\n + ${{ steps.validate.outputs.stdout }} + \`\`\` + +
+ + #### Terraform Plan 📖\`${{ steps.plan.outcome }}\` + +
Show Plan + + \`\`\`\n + ${process.env.PLAN} + \`\`\` + +
+ + *Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`, Working Directory: \`${{ env.tf_actions_working_dir }}\`, Workflow: \`${{ github.workflow }}\`*`; + + // 3. 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 + }) + } + + # Downloads Regula and checks whether the plan meets compliance requirements. + - name: Regula Compliance Check + shell: bash + working-directory: ${{ env.TERRAFORM_DIRECTORY }} + run: | + REGULA_URL="https://github.com/fugue/regula/releases/download/v${REGULA_VERSION}/regula_${REGULA_VERSION}_Linux_x86_64.tar.gz" + curl -sL "$REGULA_URL" -o regula.tar.gz + tar xzf regula.tar.gz + terraform show -json tfplan | ./regula run + + # Deploys infrastructure or changes to infrastructure. + - name: Terraform Apply + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + working-directory: ${{ env.TERRAFORM_DIRECTORY }} + run: | + terraform apply \ + -auto-approve \ + -input=false \ + -parallelism=${TERRAFORM_PARALLELISM} \ + tfplan