How to Keep Usage of Terraform aws_security_group DRY

You could look at using aws_security_group_rule instead of having your rules inline. You can then create a module like this:

module/sg/sg.tf

resource "aws_security_group" "default" {
  name        = "${var.security_group_name}"
  description = "${var.security_group_name} group managed by Terraform"

  vpc_id = "${var.vpc_id}"
}

resource "aws_security_group_rule" "egress" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
  description       = "All egress traffic"
  security_group_id = "${aws_security_group.default.id}"
}

resource "aws_security_group_rule" "tcp" {
  count             = "${var.tcp_ports == "default_null" ? 0 : length(split(",", var.tcp_ports))}"
  type              = "ingress"
  from_port         = "${element(split(",", var.tcp_ports), count.index)}"
  to_port           = "${element(split(",", var.tcp_ports), count.index)}"
  protocol          = "tcp"
  cidr_blocks       = ["${var.cidrs}"]
  description       = ""
  security_group_id = "${aws_security_group.default.id}"
}

resource "aws_security_group_rule" "udp" {
  count             = "${var.udp_ports == "default_null" ? 0 : length(split(",", var.udp_ports))}"
  type              = "ingress"
  from_port         = "${element(split(",", var.udp_ports), count.index)}"
  to_port           = "${element(split(",", var.udp_ports), count.index)}"
  protocol          = "udp"
  cidr_blocks       = ["${var.cidrs}"]
  description       = ""
  security_group_id = "${aws_security_group.default.id}"
}

modules/sg/variables.tf

variable "tcp_ports" {
  default = "default_null"
}

variable "udp_ports" {
  default = "default_null"
}

variable "cidrs" {
  type = "list"
}

variable "security_group_name" {}

variable "vpc_id" {}

Use the module in your main.tf

module "sg1" {
  source              = "modules/sg"
  tcp_ports           = "22,80,443"
  cidrs               = ["10.0.0.5/32", "10.0.0.50/32", "10.0.0.200/32"]
  security_group_name = "SomeGroup"
  vpc_id              = "${aws_vpc.this_vpc.id}"
}

module "sg2" {
  source              = "modules/sg"
  tcp_ports           = "22,80,443"
  cidrs               = ["10.0.0.5/32", "10.0.0.50/32", "10.0.0.200/32"]
  security_group_name = "SomeOtherGroup"
  vpc_id              = "${aws_vpc.this_vpc.id}"
}

References:

For why optionally excluding a resource with count looks like this (source):

count             = "${var.udp_ports == "default_null" ? 0 : length(split(",", var.udp_ports))}"

And the variable is set to:

variable "udp_ports" {
  default = "default_null"
}