Serviceomat is a module to create basic infrastructure required to deploy a service with the Deployomat.
At a minimum it will create a template EC2 AutoScaling Group and EC2 Launch Template suitable for Deployomat.
This is a complete example of a minimal serviceomat setup for a service that does not receive inbound web requests.
provider "aws" {
alias = "meta"
}
data "aws_caller_identity" "current" {}
variable "subnet_ids" {
description = "A list of subnets to run the service in."
type = list(string)
}
# Skip this if you already have a configured omat organiztion prefix!
resource "aws_ssm_parameter" "org_prefix" {
name = "/omat/organization_prefix"
type = "String"
value = "myorg"
}
# Skip this if you have already have an accountomat configuration for the account you want the service in!
module "accountomat_parameters" {
source = "GoCarrot/accountomat/aws//modules/parameters"
account_id = data.aws_caller_identity.current.id
admin_role_name = "admin"
environment = "production"
name = "Account"
purpose = "Account"
slug = "Account"
depends_on = [aws_ssm_parameter.org_prefix]
}
module "example_service" {
source = "GoCarrot/serviceomat/aws"
providers = {
aws.meta = aws.meta
}
account_canonical_slug = module.accountomat_parameters.canonical_slug
service_name = "example"
subnet_ids = var.subnet_ids
instance_type = "t4g.small"
min_instances = 1
max_instances = 2
# By default serviceomat will look up and attach IAM policies named LogAccess and ConfigAccess
# to the role it creates
default_role_policies = []
}
This is will automatically provision an EC2 AutoScaling Group, an EC2 Launch Template, an IAM Role, and an IAM instance profile to attach that role to instances launched by the autoscaling group. The autoscaling group provisioned by this module will have its min_size and max_size set to 0 -- it will not boot any instances, and is intended for use as a template for Deployomat.
This is a complete example of a minimal serviceomat setup for a service that receives inbound web requests.
variable "subnet_ids" {
description = "A list of subnets to run the service in."
type = list(string)
}
variable "lb_listener_arns" {
description = "A map of Application Load Balancer listener ARNs that this service should receive traffic from. Key is whatever you want, value is the ARN."
type = map(string)
}
variable "lb_security_group_ids" {
description = "A map of security group ids attached to the load balancer. Key is whatever you want, value is the ID."
type = map(string)
}
variable "lb_priority" {
description = "The priority of this service the ALB rule list. Remember, lower == handled earlier in the pipeline."
type = number
}
variable "hosts" {
description = "A list of hostnames that the service handles requests for."
type = list(string)
}
# Skip this if you already have a configured omat organiztion prefix!
resource "aws_ssm_parameter" "org_prefix" {
name = "/omat/organization_prefix"
type = "String"
value = "myorg"
}
# Skip this if you have already have an accountomat configuration for the account you want the service in!
module "accountomat_parameters" {
source = "GoCarrot/accountomat/aws//modules/parameters"
account_id = data.aws_caller_identity.current.id
admin_role_name = "admin"
environment = "production"
name = "Account"
purpose = "Account"
slug = "Account"
depends_on = [aws_ssm_parameter.org_prefix]
}
module "example_service" {
source = "GoCarrot/serviceomat/aws"
providers = {
aws.meta = aws.meta
}
account_canonical_slug = module.accountomat_parameters.canonical_slug
service_name = "example"
subnet_ids = var.subnet_ids
instance_type = "t4g.small"
min_instances = 1
max_instances = 2
# By default serviceomat will look up and attach IAM policies named LogAccess and ConfigAccess
# to the role it creates
default_role_policies = []
lb_listener_arns = var.lb_listener_arns
lb_security_group_ids = var.lb_security_group_ids
lb_conditions = {
service = {
priority = var.lb_priority
conditions = [{
host_headers = var.hosts
}]
}
}
# By default serviceomat assumes that your web service will be listening for requests on port 80.
# port = 80
}
In addition to the standard resources provisioned, this will also create a VPC Security Group permitting ingress from all lb_security_group_ids to service instances, an Application Load Balancer Rule suitable for Deployomat to use as a template, and a Target Group suitable for Deployomat to use as a template.
Much more complex configuration is possible. This is an example of our development setup for a service which has both web frontends and background job workers.
data "aws_ssm_parameter" "subnet_ids" {
provider = aws.admin
name = "${local.param_prefix}/config/core/public_service_subnet_ids"
}
data "aws_security_group" "ssh" {
name = "AllowGlobalSSH"
}
data "aws_ssm_parameter" "lb_security_group_ids" {
provider = aws.admin
name = "${local.param_prefix}/config/core/lb_security_group_ids"
}
data "aws_ssm_parameter" "lb_listener_arns" {
provider = aws.admin
name = "${local.param_prefix}/config/core/listener_arns"
}
data "aws_key_pair" "new-laptop" {
key_name = "new-laptop"
}
data "aws_ssm_parameter" "ci-cd-account_id" {
provider = aws.admin
name = "/${var.organization_prefix}/${local.environment}/ci-cd/account_id"
}
data "aws_subnet" "exemplar" {
id = split(",", data.aws_ssm_parameter.subnet_ids.value)[0]
}
data "aws_key_pair" "admin-key" {
key_name = "admin-key"
}
resource "aws_security_group" "example" {
name = "example"
description = "Group for tagging and allows HTTPS egress to the internet because hey."
vpc_id = data.aws_subnet.exemplar.vpc_id
egress {
from_port = 443
to_port = 443
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
}
}
module "example_service" {
source = "GoCarrot/serviceomat/aws"
providers = {
aws.meta = aws.meta
}
account_canonical_slug = terraform.workspace
service_name = "example"
subnet_ids = split(",", data.aws_ssm_parameter.subnet_ids.value)
instance_type = var.instance_type
# This is part of a hack to allow setting the EBS volume size to lower than 8GiB, which is the
# default volume size for Debian AMIs. We don't need that much space.
ami_owner_id = data.aws_ssm_parameter.ci-cd-account_id.value
ami_name_regex = "${local.environment}_example.*"
lb_listener_arns = jsondecode(nonsensitive(data.aws_ssm_parameter.lb_listener_arns.value))
lb_security_group_ids = jsondecode(nonsensitive(data.aws_ssm_parameter.lb_security_group_ids.value))
lb_priority = var.lb_priority
lb_conditions = {
service = {
priority = var.lb_priority
conditions = [{
host_headers = var.hosts
}]
}
}
min_instances = var.min_size
max_instances = var.max_size
health_check = {
healthy_threshold = 2
unhealthy_threshold = 2
interval = 5
timeout = 4
path = "/health"
matcher = "200-299"
}
key_name = data.aws_key_pair.admin-key.key_name
instance_security_group_ids = [data.aws_security_group.ssh.id, aws_security_group.example.id]
user_data = <<-EOT
## template: jinja
#cloud-config
hostname: "{{ v1.availability_zone }}-{{ v1.local_hostname }}"
bootcmd:
- |
systemctl enable nginx.service teak-example@1.service --now --no-block
EOT
}
module "example-sidekiq_service" {
source = "GoCarrot/serviceomat/aws"
providers = {
aws.meta = aws.meta
}
account_canonical_slug = terraform.workspace
service_name = "example-sidekiq"
subnet_ids = split(",", data.aws_ssm_parameter.subnet_ids.value)
instance_type = var.instance_type
ami_owner_id = data.aws_ssm_parameter.ci-cd-account_id.value
ami_name_regex = "${local.environment}_example.*"
# We disable creating a role and instead reuse the instance profile created by the web service, since
# our sidekiq and web frontends interact with the same AWS resources in the same way.
create_role = false
iam_instance_profile = module.example_service.instance_profile.arn
min_instances = var.min_size
max_instances = var.max_size
key_name = data.aws_key_pair.admin-key.key_name
instance_security_group_ids = [data.aws_security_group.ssh.id, aws_security_group.example.id]
additional_tags_for_asg_instances = {
CostCenter = "example-sidekiq"
}
user_data = <<-EOT
## template: jinja
#cloud-config
hostname: "{{ v1.availability_zone }}-{{ v1.local_hostname }}"
bootcmd:
- |
${indent(5, file("${path.module}/dropins/bootcmd/sidekiq_per_core.sh"))}
write_files:
- path: /etc/fluent/conf.d/32_sidekiq_logs.conf
owner: root:root
content: |
${indent(5, file("${path.module}/dropins/fluentd/32_sidekiq_logs.conf"))}
permissions: '0644'
- path: /etc/teak-configurator/31_sidekiq.yml.conf
owner: root:root
content: |
${indent(5, file("${path.module}/dropins/teak-configurator/31_sidekiq.yml.conf"))}
EOT
}
All variables are well documented. Refer to the variable descriptions for additional details on how Serviceomat can be configured.