diff --git a/.tool-versions b/.tool-versions index 373c8064..6fc8f7d8 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ nodejs 18.12.1 terraform 1.13.3 -trivy 0.47.0 +trivy 0.69.2 diff --git a/scripts/generateAdvancedAWS.js b/scripts/generateAdvancedAWS.js index cc863236..974976e1 100644 --- a/scripts/generateAdvancedAWS.js +++ b/scripts/generateAdvancedAWS.js @@ -7,6 +7,7 @@ const { applyAwsRegion, applyAwsSecurityGroup, applyAwsVpc, + applyAwsIamRole, } = require('../dist/generators/addons/aws/modules/index.js'); const { applyAdvancedTemplate } = require('../dist/generators/addons/aws/advanced.js'); @@ -19,5 +20,6 @@ const { applyAdvancedTemplate } = require('../dist/generators/addons/aws/advance await applyAwsVpc(options); await applyAwsSecurityGroup(options); await applyAwsIamUserAndGroup(options); + await applyAwsIamRole(options); await applyAdvancedTemplate(options); })(); diff --git a/src/generators/addons/aws/dependencies.ts b/src/generators/addons/aws/dependencies.ts index c773a80b..eab533e1 100644 --- a/src/generators/addons/aws/dependencies.ts +++ b/src/generators/addons/aws/dependencies.ts @@ -13,6 +13,7 @@ import { applyAwsSecurityGroup, applyAwsSsm, applyAwsVpc, + applyAwsIamRole, } from '@/generators/addons/aws/modules'; import { containsContent, isExisting } from '@/helpers/file'; @@ -85,6 +86,12 @@ const AWS_MODULES: Record = { mainContent: 'module "ssm"', applyModuleFunction: (options: AwsOptions) => applyAwsSsm(options), }, + iamRole: { + name: 'iamRole', + path: 'modules/iam_role', + mainContent: 'module "iam_role"', + applyModuleFunction: (options: AwsOptions) => applyAwsIamRole(options), + }, }; const isAwsModuleAdded = ( diff --git a/src/generators/addons/aws/index.ts b/src/generators/addons/aws/index.ts index c4360650..11a90247 100644 --- a/src/generators/addons/aws/index.ts +++ b/src/generators/addons/aws/index.ts @@ -10,6 +10,7 @@ import { applyAwsRegion, applyAwsSecurityGroup, applyAwsVpc, + applyAwsIamRole, } from './modules'; const awsChoices = [ @@ -69,6 +70,7 @@ const generateAwsTemplate = async ( await applyAwsVpc(awsOptions); await applyAwsSecurityGroup(awsOptions); await applyAwsIamUserAndGroup(awsOptions); + await applyAwsIamRole(awsOptions); await applyAdvancedTemplate(awsOptions); break; diff --git a/src/generators/addons/aws/modules/bastion.ts b/src/generators/addons/aws/modules/bastion.ts index 845db3b7..5a341197 100644 --- a/src/generators/addons/aws/modules/bastion.ts +++ b/src/generators/addons/aws/modules/bastion.ts @@ -9,6 +9,7 @@ import { INFRA_CORE_LOCALS_PATH, INFRA_CORE_MAIN_PATH, INFRA_CORE_VARIABLES_PATH, + INFRA_CORE_DATA_PATH, } from '@/generators/terraform/constants'; import { appendToFile, copy } from '@/helpers/file'; @@ -18,19 +19,23 @@ import { AWS_TEMPLATE_PATH, } from '../constants'; +const bastionDataContent = dedent` + ### Begin Bastion Host ### + data "aws_iam_policy" "ssm_managed_instance_core" { + name = "AmazonSSMManagedInstanceCore" + } + ### End Bastion Host ###`; + const bastionLocalContent = dedent` ### Begin Bastion Host ### locals { - enable_bastion = true + enable_bastion = true + bastion_ssm_role_name = "\${local.env_namespace}-SSMInstanceRole" + bastion_ssm_policy_arns = [data.aws_iam_policy.ssm_managed_instance_core.arn] } ### End Bastion Host ###`; const bastionVariablesContent = dedent` - variable "bastion_image_id" { - description = "The AMI image ID for the bastion instance" - default = "ami-0801a1e12f4a9ccc0" - } - variable "bastion_instance_type" { description = "The bastion instance type" default = "t3.nano" @@ -52,6 +57,16 @@ const bastionVariablesContent = dedent` }`; const bastionModuleContent = dedent` + module "bastion_ssm_role" { + count = local.enable_bastion ? 1 : 0 + source = "../modules/iam_role" + + role_name = local.bastion_ssm_role_name + assume_role_services = ["ec2.amazonaws.com"] + policy_arns = local.bastion_ssm_policy_arns + create_instance_profile = true + } + module "bastion" { count = local.enable_bastion ? 1 : 0 source = "../modules/bastion" @@ -59,9 +74,9 @@ const bastionModuleContent = dedent` subnet_ids = module.vpc.public_subnet_ids instance_security_group_ids = module.security_group.bastion_security_group_ids - env_namespace = local.env_namespace - image_id = var.bastion_image_id - instance_type = var.bastion_instance_type + env_namespace = local.env_namespace + instance_type = var.bastion_instance_type + iam_instance_profile = module.bastion_ssm_role[0].instance_profile_name min_instance_count = var.bastion_min_instance_count max_instance_count = var.bastion_max_instance_count @@ -79,16 +94,6 @@ const bastionSGMainContent = dedent` } } - resource "aws_security_group_rule" "bastion_ingress_ssh_nimble" { - type = "ingress" - security_group_id = aws_security_group.bastion.id - from_port = 22 - to_port = 22 - protocol = "tcp" - cidr_blocks = ["\${var.nimble_office_ip}/32"] - description = "Nimble office" - } - resource "aws_security_group_rule" "bastion_egress_rds" { type = "egress" security_group_id = aws_security_group.bastion.id @@ -97,6 +102,17 @@ const bastionSGMainContent = dedent` protocol = "tcp" source_security_group_id = aws_security_group.rds.id description = "From RDS to bastion" + } + + # trivy:ignore:AVD-AWS-0104 + resource "aws_security_group_rule" "bastion_egress_ssm" { + type = "egress" + security_group_id = aws_security_group.bastion.id + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + description = "Allow outbound HTTPS traffic for SSM" }`; const bastionSGOutputsContent = dedent` @@ -121,6 +137,7 @@ const applyAwsBastion = async (options: AwsOptions) => { bastionLocalContent, options.projectName ); + appendToFile(INFRA_CORE_DATA_PATH, bastionDataContent, options.projectName); appendToFile( INFRA_CORE_VARIABLES_PATH, bastionVariablesContent, diff --git a/src/generators/addons/aws/modules/core/iamRole.test.ts b/src/generators/addons/aws/modules/core/iamRole.test.ts new file mode 100644 index 00000000..e920d5ee --- /dev/null +++ b/src/generators/addons/aws/modules/core/iamRole.test.ts @@ -0,0 +1,45 @@ +import { AwsOptions } from '@/generators/addons/aws'; +import { applyTerraformCore } from '@/generators/terraform'; +import { remove } from '@/helpers/file'; + +import applyAwsIamRole from './iamRole'; +import applyTerraformAwsProvider from './provider'; + +describe('IAM Role add-on', () => { + describe('given valid AWS options', () => { + const projectDir = 'iam-addon-test'; + + beforeAll(async () => { + const awsOptions: AwsOptions = { + projectName: projectDir, + provider: 'aws', + infrastructureType: 'advanced', + }; + + await applyTerraformCore(awsOptions); + await applyTerraformAwsProvider(awsOptions); + await applyAwsIamRole(awsOptions); + }); + + afterAll(() => { + jest.clearAllMocks(); + remove('/', projectDir); + }); + + it('creates expected files', () => { + const expectedFiles = [ + 'shared/main.tf', + 'shared/providers.tf', + 'shared/outputs.tf', + 'shared/variables.tf', + + 'modules/iam_role/data.tf', + 'modules/iam_role/variables.tf', + 'modules/iam_role/main.tf', + 'modules/iam_role/outputs.tf', + ]; + + expect(projectDir).toHaveFiles(expectedFiles); + }); + }); +}); diff --git a/src/generators/addons/aws/modules/core/iamRole.ts b/src/generators/addons/aws/modules/core/iamRole.ts new file mode 100644 index 00000000..4d39b294 --- /dev/null +++ b/src/generators/addons/aws/modules/core/iamRole.ts @@ -0,0 +1,18 @@ +import { AwsOptions } from '@/generators/addons/aws'; +import { AWS_TEMPLATE_PATH } from '@/generators/addons/aws/constants'; +import { isAwsModuleAdded } from '@/generators/addons/aws/dependencies'; +import { copy } from '@/helpers/file'; + +const applyAwsIamRole = async (options: AwsOptions) => { + if (isAwsModuleAdded('iamRole', options.projectName)) { + return; + } + + copy( + `${AWS_TEMPLATE_PATH}/modules/iam_role`, + 'modules/iam_role', + options.projectName + ); +}; + +export default applyAwsIamRole; diff --git a/src/generators/addons/aws/modules/index.ts b/src/generators/addons/aws/modules/index.ts index dfee3a4f..e0ac7213 100644 --- a/src/generators/addons/aws/modules/index.ts +++ b/src/generators/addons/aws/modules/index.ts @@ -1,6 +1,7 @@ import applyAwsAlb from './alb'; import applyAwsBastion from './bastion'; import applyAwsCloudwatch from './cloudwatch'; +import applyAwsIamRole from './core/iamRole'; import applyAwsIamUserAndGroup from './core/iamUserAndGroup'; import applyTerraformAwsProvider from './core/provider'; import applyAwsRegion from './core/region'; @@ -14,6 +15,7 @@ import applyAwsSsm from './ssm'; export { applyAwsAlb, + applyAwsIamRole, applyAwsBastion, applyTerraformAwsProvider, applyAwsCloudwatch, diff --git a/templates/addons/aws/modules/bastion/data.tf b/templates/addons/aws/modules/bastion/data.tf new file mode 100644 index 00000000..5cfee4cb --- /dev/null +++ b/templates/addons/aws/modules/bastion/data.tf @@ -0,0 +1,19 @@ +data "aws_ami" "amazon_linux_2023" { + most_recent = true + owners = ["amazon"] + + filter { + name = "name" + values = ["al2023-ami-2023.*-x86_64"] + } + + filter { + name = "architecture" + values = ["x86_64"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } +} diff --git a/templates/addons/aws/modules/bastion/main.tf b/templates/addons/aws/modules/bastion/main.tf index 66497d5a..78174ff4 100644 --- a/templates/addons/aws/modules/bastion/main.tf +++ b/templates/addons/aws/modules/bastion/main.tf @@ -1,9 +1,12 @@ # trivy:ignore:AVD-AWS-0009 resource "aws_launch_template" "bastion_instance" { name_prefix = "${local.name_prefix}-" - image_id = var.image_id + image_id = data.aws_ami.amazon_linux_2023.id instance_type = var.instance_type - key_name = var.key_name + + iam_instance_profile { + name = var.iam_instance_profile + } metadata_options { http_tokens = local.metadata_options.http_tokens diff --git a/templates/addons/aws/modules/bastion/variables.tf b/templates/addons/aws/modules/bastion/variables.tf index 2ca121e0..7b6fa71d 100644 --- a/templates/addons/aws/modules/bastion/variables.tf +++ b/templates/addons/aws/modules/bastion/variables.tf @@ -4,7 +4,7 @@ variable "env_namespace" { } variable "subnet_ids" { - description = "The public setnet IsD for the instance" + description = "The public subnet IDs for the instance" type = list(string) } @@ -13,11 +13,6 @@ variable "instance_security_group_ids" { type = list(string) } -variable "image_id" { - description = "The AMI image ID" - default = "ami-0801a1e12f4a9ccc0" -} - variable "instance_type" { description = "The instance type" default = "t3.nano" @@ -47,11 +42,11 @@ variable "volume_size" { variable "device_name" { description = "The device name for the EBS volume" type = string - default = "/dev/sdf" + default = "/dev/xvda" } -variable "key_name" { - description = "The name of the key pair to use for the instance" +variable "iam_instance_profile" { + description = "The name of the IAM instance profile for the instance" type = string default = "" } diff --git a/templates/addons/aws/modules/iam_role/data.tf b/templates/addons/aws/modules/iam_role/data.tf new file mode 100644 index 00000000..69766180 --- /dev/null +++ b/templates/addons/aws/modules/iam_role/data.tf @@ -0,0 +1,12 @@ +data "aws_iam_policy_document" "assume_role" { + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = var.assume_role_services + } + + actions = ["sts:AssumeRole"] + } +} diff --git a/templates/addons/aws/modules/iam_role/main.tf b/templates/addons/aws/modules/iam_role/main.tf new file mode 100644 index 00000000..0522acc3 --- /dev/null +++ b/templates/addons/aws/modules/iam_role/main.tf @@ -0,0 +1,26 @@ +resource "aws_iam_role" "role" { + name = var.role_name + assume_role_policy = data.aws_iam_policy_document.assume_role.json + + tags = { + Name = var.role_name + } +} + +resource "aws_iam_role_policy_attachment" "this" { + for_each = toset(var.policy_arns) + + role = aws_iam_role.role.name + policy_arn = each.value +} + +resource "aws_iam_instance_profile" "instance_profile" { + count = var.create_instance_profile ? 1 : 0 + + name = var.role_name + role = aws_iam_role.role.name + + tags = { + Name = var.role_name + } +} diff --git a/templates/addons/aws/modules/iam_role/outputs.tf b/templates/addons/aws/modules/iam_role/outputs.tf new file mode 100644 index 00000000..4945931f --- /dev/null +++ b/templates/addons/aws/modules/iam_role/outputs.tf @@ -0,0 +1,9 @@ +output "role_arn" { + description = "The ARN of the IAM role" + value = aws_iam_role.role.arn +} + +output "instance_profile_name" { + description = "The name of the IAM instance profile" + value = var.create_instance_profile ? aws_iam_instance_profile.instance_profile[0].name : null +} diff --git a/templates/addons/aws/modules/iam_role/variables.tf b/templates/addons/aws/modules/iam_role/variables.tf new file mode 100644 index 00000000..048da752 --- /dev/null +++ b/templates/addons/aws/modules/iam_role/variables.tf @@ -0,0 +1,22 @@ +variable "role_name" { + description = "The name of the IAM role" + type = string +} + +variable "assume_role_services" { + description = "List of AWS services that can assume this role" + type = list(string) + default = ["ec2.amazonaws.com"] +} + +variable "policy_arns" { + description = "List of IAM policy ARNs to attach to the role" + type = list(string) + default = [] +} + +variable "create_instance_profile" { + description = "Whether to create an IAM instance profile" + type = bool + default = false +} diff --git a/templates/terraform/.tool-versions b/templates/terraform/.tool-versions index ed068d5b..8072365a 100644 --- a/templates/terraform/.tool-versions +++ b/templates/terraform/.tool-versions @@ -1,2 +1,2 @@ terraform 1.13.3 -trivy 0.47.0 +trivy 0.69.2