diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..0f49afe --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,35 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + commit-message: + prefix: "(GHA)" + reviewers: + - "UKHomeOffice/core-cloud-devops" + labels: + - "dependencies" + - "patch" + - package-ecosystem: "terraform" + directory: "/stateful" + schedule: + interval: "daily" + commit-message: + prefix: "(TF)" + reviewers: + - "UKHomeOffice/core-cloud-devops" + labels: + - "dependencies" + - "patch" + - package-ecosystem: "terraform" + directory: "/stateless" + schedule: + interval: "daily" + commit-message: + prefix: "(TF)" + reviewers: + - "UKHomeOffice/core-cloud-devops" + labels: + - "dependencies" + - "patch" diff --git a/.github/workflows/pull-request-sast.yaml b/.github/workflows/pull-request-sast.yaml new file mode 100644 index 0000000..39816b0 --- /dev/null +++ b/.github/workflows/pull-request-sast.yaml @@ -0,0 +1,27 @@ +name: Validate Terraform with Trivy + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + RunTerraformValidation: + name: Run Terraform SAST + runs-on: ubuntu-latest + + steps: + - name: Clone the Repository + uses: actions/checkout@v4 + + # Results have to be a table as the organisation does not have Advanced Security license. + - name: Terraform Trivy Scan + uses: aquasecurity/trivy-action@0.28.0 + with: + scan-type: 'config' + #trivyignores: ".trivyignore" + exit-code: '1' diff --git a/.github/workflows/pull-request-semver-label-check.yaml b/.github/workflows/pull-request-semver-label-check.yaml new file mode 100644 index 0000000..89fecce --- /dev/null +++ b/.github/workflows/pull-request-semver-label-check.yaml @@ -0,0 +1,39 @@ +name: 'Check PR for SemVer Label' +on: + pull_request: + types: + - labeled + - unlabeled + - opened + - reopened + - synchronize + branches: + - main + +permissions: + pull-requests: read + contents: read + +jobs: + semver-check: + name: 'Check PR for SemVer Label' + if: | + contains(github.event.pull_request.labels.*.name, 'skip-release') == false + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Parse the SemVer label + id: label + uses: UKHomeOffice/match-label-action@v1 + with: + labels: minor,major,patch + mode: singular + + - name: Calculate SemVer value + id: calculate + uses: UKHomeOffice/semver-calculate-action@v2 + with: + increment: ${{ steps.label.outputs.matchedLabels }} + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pull-request-semver-tag-merge.yaml b/.github/workflows/pull-request-semver-tag-merge.yaml new file mode 100644 index 0000000..146e0ca --- /dev/null +++ b/.github/workflows/pull-request-semver-tag-merge.yaml @@ -0,0 +1,46 @@ +name: 'SemVer Tag on Main Merge' +on: + pull_request: + types: + - closed + branches: + - main + +permissions: + pull-requests: read + contents: write + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +jobs: + semver-tag: + name: 'Tag Repository with SemVer' + if: | + github.event.pull_request.merged == true && + contains(github.event.pull_request.labels.*.name, 'skip-release') == false + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Parse the SemVer label + id: label + uses: UKHomeOffice/match-label-action@v1 + with: + labels: minor,major,patch + mode: singular + + - name: Calculate SemVer value + id: calculate + uses: UKHomeOffice/semver-calculate-action@v2 + with: + increment: ${{ steps.label.outputs.matchedLabels }} + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Tag Repository + uses: UKHomeOffice/semver-tag-action@v4 + with: + tag: ${{ steps.calculate.outputs.version }} + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index e69de29..950a50b 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,96 @@ +# core-cloud-ecr-tf-module +This module aims to provide a common pattern for deploying your AWS Elastic Container Registry (ECR) repositories on either a central AWS account or individual workload accounts. This module utilises the official TF module for ECR (https://registry.terraform.io/modules/terraform-aws-modules/ecr/aws/latest). + +By default the ECR repository creates a READ/WRITE policy that defaults to the AWS account. You will need to specify additional Amazon Resource Numbers (ARNs) in the respective readwrite/readonly lists. Should you need need to provide access to other AWS accounts. You may also consider matching the OU (Organizational unit) instead using custom policy statements. + +You may set common options and override them on a per-repository basis with an exception around Lambda Access. + +Lambda ARNS must be declared in a separate list that can only be defined at a per-repository level. This adds additional permissions that allow Lambda to access ECR repositories to use as a runtime container. + +## Expected YAML config with Explanations +``` +tenant: #This is used as a prefix for your ECR repo. i.e. / +common_options: # These are common options that can be re-used by all of your ECR repositories + create_lifecycle_policy: true # Defaults to false. If set to true you will need to specify repository_lifecycle_policy - this is done via filepath to a json file + repository_lifecycle_policy: ./policies/example_common_repo_lifecycle_policy.json + repository_read_write_access_arns: # These are sets of ARNs that are allowed READ WRITE access to your ECR Repo + - arn:aws:iam:::root + - ... + repository_read_access_arns: # These are sets of ARNs that are allowed READONLY access to your ECR Repos + - arn:aws:iam:::root + - ... + repository_policy_statements: # Custom policy statements to attach to your ECR repos, example shown below is an example to allow read only access to all AWS accounts belonging to a certain AWS organisation. + orgID_readonly: + sid: orgRO + actions: + - "ecr:GetAuthorizationToken" + - "ecr:BatchCheckLayerAvailability" + - "ecr:BatchGetImage" + - "ecr:DescribeImageScanFindings" + - "ecr:DescribeImages" + - "ecr:DescribeRepositories" + - "ecr:GetDownloadUrlForLayer" + - "ecr:GetLifecyclePolicy" + - "ecr:GetLifecyclePolicyPreview" + - "ecr:GetRepositoryPolicy" + - "ecr:ListImages" + - "ecr:ListTagsForResource" + resources: ["*"] + effect: Allow + conditions: + - orgMatch: + test: "StringLike" + variable: "aws:PrincipalOrgID" + values: + - o- + - ... + +repo_list: # This is where you will define your list of ECR repositories as keys. This can be done as `key: `or `key: ~` if there are no changes from the common options + hello-world: + foo-bar: + custom-oci: + repository_read_write_access_arns: # You can override the common options + - arn:aws:iam:::root + - ... + repository_read_access_arns: + - arn:aws:iam:::root + - ... + repository_lambda_read_access_arns: # This is where you'll list lambda arns that are allowed access to a particular ECR repos. This cannot be defined under common + - ... + repository_policy_statements: {} # Example to remove common repository_policy_statements if defined + +``` + +Please see example directory for an example usage in both Terraform and Terragrunt. + + + +## Requirements + +No requirements. + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [ecr](#module\_ecr) | terraform-aws-modules/ecr/aws | 2.3.1 | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [ecr\_config](#input\_ecr\_config) | PAth to YAML file that contains ECR repositories | `any` | n/a | yes | +| [tags](#input\_tags) | n/a | `map(string)` | `{}` | no | + +## Outputs + +No outputs. + \ No newline at end of file diff --git a/example/example-terraform.tf b/example/example-terraform.tf new file mode 100644 index 0000000..affe56e --- /dev/null +++ b/example/example-terraform.tf @@ -0,0 +1,13 @@ +module "ecr_repos" { + source = "../" + + ecr_config = yamldecode(file("./example_repos.yaml")) + + tags = { + cost-centre = "..." + finance-account-id = "..." + portfolio-id = "..." + project-id = "..." + service-id = "..." + } +} diff --git a/example/example-terragrunt.hcl b/example/example-terragrunt.hcl new file mode 100644 index 0000000..ae50635 --- /dev/null +++ b/example/example-terragrunt.hcl @@ -0,0 +1,15 @@ +terraform { + source = "../" +} + +inputs = { + association_config = yamldecode(file("./example_repos.yaml")) + + tags = { + cost-centre = "..." + finance-account-id = "..." + portfolio-id = "..." + project-id = "..." + service-id = "..." + } +} diff --git a/example/example_repos.yaml b/example/example_repos.yaml new file mode 100644 index 0000000..8b1deac --- /dev/null +++ b/example/example_repos.yaml @@ -0,0 +1,49 @@ +tenant: #This is used as a prefix for your ECR repo. i.e. / +common_options: # These are common options that can be re-used by all of your ECR repositories + create_lifecycle_policy: true # Defaults to false. If set to true you will need to specify repository_lifecycle_policy - this is done via filepath to a json file + repository_lifecycle_policy: ./policies/example_common_repo_lifecycle_policy.json + repository_read_write_access_arns: # These are sets of ARNs that are allowed READ WRITE access to your ECR Repo + - arn:aws:iam:::root + - ... + repository_read_access_arns: # These are sets of ARNs that are allowed READONLY access to your ECR Repos + - arn:aws:iam:::root + - ... + repository_policy_statements: # Custom policy statements to attach to your ECR repos, example shown below is an example to allow read only access to all AWS accounts belonging to a certain AWS organisation. + orgID_readonly: + sid: orgRO + actions: + - "ecr:GetAuthorizationToken" + - "ecr:BatchCheckLayerAvailability" + - "ecr:BatchGetImage" + - "ecr:DescribeImageScanFindings" + - "ecr:DescribeImages" + - "ecr:DescribeRepositories" + - "ecr:GetDownloadUrlForLayer" + - "ecr:GetLifecyclePolicy" + - "ecr:GetLifecyclePolicyPreview" + - "ecr:GetRepositoryPolicy" + - "ecr:ListImages" + - "ecr:ListTagsForResource" + resources: ["*"] + effect: Allow + conditions: + - orgMatch: + test: "StringLike" + variable: "aws:PrincipalOrgID" + values: + - o- + - ... + +repo_list: # This is where you will define your list of ECR repositories as keys. This can be done as `key: `or `key: ~` if there are no changes from the common options + hello-world: + foo-bar: + custom-oci: + repository_read_write_access_arns: # You can override the common options + - arn:aws:iam:::root + - ... + repository_read_access_arns: + - arn:aws:iam:::root + - ... + repository_lambda_read_access_arns: # This is where you'll list lambda arns that are allowed access to a particular ECR repos. This cannot be defined under common + - ... + repository_policy_statements: {} # Example to remove common repository_policy_statements if defined diff --git a/example/policies/example_common_repo_lifecycle_policy.json b/example/policies/example_common_repo_lifecycle_policy.json new file mode 100644 index 0000000..e025b41 --- /dev/null +++ b/example/policies/example_common_repo_lifecycle_policy.json @@ -0,0 +1,19 @@ +{ + "rules": [ + { + "rulePriority": 1, + "description": "Keep last 30 images", + "selection": { + "tagStatus": "tagged", + "tagPrefixList": [ + "v" + ], + "countType": "imageCountMoreThan", + "countNumber": 30 + }, + "action": { + "type": "expire" + } + } + ] +} diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..ef693a3 --- /dev/null +++ b/main.tf @@ -0,0 +1,19 @@ +module "ecr" { + source = "terraform-aws-modules/ecr/aws" + version = "2.3.1" + + for_each = try(var.ecr_config.repo_list, {}) + + repository_name = "${var.ecr_config.tenant}/${each.key}" + repository_type = "private" + + create_lifecycle_policy = try(each.value.create_lifecycle_policy, var.ecr_config.common_options.create_lifecycle_policy, false) + + repository_read_access_arns = try(each.value.repository_read_write_access_arns, var.ecr_config.common_options.repository_read_write_access_arns, []) + repository_read_write_access_arns = try(each.value.repository_read_access_arns, var.ecr_config.common_options.repository_read_access_arns, []) + repository_lambda_read_access_arns = try(each.value.repository_lambda_read_access_arns, []) # Lambda ECR access to be done on a repo by repo basis only + repository_policy_statements = try(each.value.repository_policy_statements, var.ecr_config.common_options.repository_policy_statements, {}) + repository_lifecycle_policy = try(file(each.value.repository_lifecycle_policy), file(var.ecr_config.common_options.repository_lifecycle_policy), null) + + tags = var.tags +} diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..1d85f04 --- /dev/null +++ b/variables.tf @@ -0,0 +1,9 @@ +variable "ecr_config" { + type = any + description = "PAth to YAML file that contains ECR repositories" +} + +variable "tags" { + type = map(string) + default = {} +}