As a SW/Ops/DB Engineer

riywo’s technology memo

Terraform Module for Mesos + Ceph Cluster and Packer Template

riywo/mesos-ceph

I’ve just tried to use Terraform and Packer to create a Mesos + Ceph cluster in AWS. Yes, I know Mesosphere applications supporting deployment of Mesos cluster in some IaaS (see Getting Started), but I’d like to understand what’s going on there. So, I did it by Terraform and Packer. I’m gonna explain a little bit more about this.

Through this work, I’ve learned a lot of things. I will write something below too.

What is this?

This repository includes Terraform module and Packer template. Terraform module will manage a VPC subnet, igw, sg, etc. and instances below:

  • admin
    • ssh gateway
    • run ceph-deploy
  • master1, master2, master3
    • mesos master
    • marathon
    • ceph mon
    • ceph mds
    • mount cephfs
  • slaves (default 3)
    • mesos slave
    • ceph osd with EBS
    • mount cephfs

Why not only Mesos, did I manage Ceph cluster? In Mesos, I’d like to use a shared file system because:

  • Share executer files, etc.
  • Control cluster software on Mesos
  • Save uploaded files to Mesos tasks (ex. image files to blog apps)

Or just for fun :)

How to use?

Very simple, make your own terraform config like below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
provider "aws" {}

resource "aws_vpc" "default" {
        cidr_block           = "10.0.0.0/16"
        enable_dns_support   = true
        enable_dns_hostnames = true
}

module "mesos_ceph" {
        source                   = "github.com/riywo/mesos-ceph/terraform"
        vpc_id                   = "${aws_vpc.default.id}"
        key_name                 = "${var.key_name}"
        key_path                 = "${var.key_path}"
        subnet_availability_zone = "us-east-1e"
        subnet_cidr_block        = "10.0.1.0/24"
        master1_ip               = "10.0.1.11"
        master2_ip               = "10.0.1.12"
        master3_ip               = "10.0.1.13"
}

Note: You can use environment variables for AWS configuration and terraform.tfvars file for variables.

1
2
3
4
5
6
7
export AWS_REGION=us-east-1
export AWS_ACCESS_KEY=AAAAAAAAAA
export AWS_SECRET_KEY=000000000000000000

$ cat terraform.tfvars
key_name = "your key name"
key_path = "/path/to/private_pem_file"

First, you should download the module using terraform get

1
2
$ terraform get
Get: git::https://github.com/riywo/mesos-ceph.git

Then, you can check terraform plan. To show resources in module, you have to provide -module-depth option.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
$ terraform plan -module-depth -1
Refreshing Terraform state prior to plan...


The Terraform execution plan has been generated and is shown below.
Resources are shown in alphabetical order for quick scanning. Green resources
will be created (or destroyed and then created if an existing resource
exists), yellow resources are being changed in-place, and red resources
will be destroyed.

Note: You didn't specify an "-out" parameter to save this plan, so when
"apply" is called, Terraform can't guarantee this is what will execute.

+ aws_vpc.default
    cidr_block:           "" => "10.0.0.0/16"
    enable_dns_hostnames: "" => "1"
    enable_dns_support:   "" => "1"
    main_route_table_id:  "" => "<computed>"

+ module.mesos.aws_instance.admin
    ami:               "" => "ami-06ef816e"
    availability_zone: "" => "<computed>"
    instance_type:     "" => "t2.micro"
    key_name:          "" => "your key name"
    private_dns:       "" => "<computed>"
    private_ip:        "" => "<computed>"
    public_dns:        "" => "<computed>"
    public_ip:         "" => "<computed>"
    security_groups.#: "" => "<computed>"
    subnet_id:         "" => "${aws_subnet.public.id}"
    tenancy:           "" => "<computed>"

+ module.mesos.aws_instance.master1
    ami:               "" => "ami-06ef816e"
    availability_zone: "" => "<computed>"
    instance_type:     "" => "t2.micro"
    key_name:          "" => "your key name"
    private_dns:       "" => "<computed>"
    private_ip:        "" => "10.0.1.11"
    public_dns:        "" => "<computed>"
    public_ip:         "" => "<computed>"
    security_groups.#: "" => "<computed>"
    subnet_id:         "" => "${aws_subnet.public.id}"
    tenancy:           "" => "<computed>"

+ module.mesos.aws_instance.master2
    ami:               "" => "ami-06ef816e"
    availability_zone: "" => "<computed>"
    instance_type:     "" => "t2.micro"
    key_name:          "" => "your key name"
    private_dns:       "" => "<computed>"
    private_ip:        "" => "10.0.1.12"
    public_dns:        "" => "<computed>"
    public_ip:         "" => "<computed>"
    security_groups.#: "" => "<computed>"
    subnet_id:         "" => "${aws_subnet.public.id}"
    tenancy:           "" => "<computed>"

+ module.mesos.aws_instance.master3
    ami:               "" => "ami-06ef816e"
    availability_zone: "" => "<computed>"
    instance_type:     "" => "t2.micro"
    key_name:          "" => "your key name"
    private_dns:       "" => "<computed>"
    private_ip:        "" => "10.0.1.13"
    public_dns:        "" => "<computed>"
    public_ip:         "" => "<computed>"
    security_groups.#: "" => "<computed>"
    subnet_id:         "" => "${aws_subnet.public.id}"
    tenancy:           "" => "<computed>"

+ module.mesos.aws_instance.slaves.0
    ami:                                  "" => "ami-06ef816e"
    availability_zone:                    "" => "<computed>"
    block_device.#:                       "" => "1"
    block_device.0.delete_on_termination: "" => "1"
    block_device.0.device_name:           "" => "/dev/sdb"
    block_device.0.encrypted:             "" => ""
    block_device.0.snapshot_id:           "" => ""
    block_device.0.virtual_name:          "" => ""
    block_device.0.volume_size:           "" => "30"
    block_device.0.volume_type:           "" => ""
    instance_type:                        "" => "t2.micro"
    key_name:                             "" => "your key name"
    private_dns:                          "" => "<computed>"
    private_ip:                           "" => "<computed>"
    public_dns:                           "" => "<computed>"
    public_ip:                            "" => "<computed>"
    security_groups.#:                    "" => "<computed>"
    subnet_id:                            "" => "${aws_subnet.public.id}"
    tenancy:                              "" => "<computed>"

+ module.mesos.aws_instance.slaves.1
    ami:                                  "" => "ami-06ef816e"
    availability_zone:                    "" => "<computed>"
    block_device.#:                       "" => "1"
    block_device.0.delete_on_termination: "" => "1"
    block_device.0.device_name:           "" => "/dev/sdb"
    block_device.0.encrypted:             "" => ""
    block_device.0.snapshot_id:           "" => ""
    block_device.0.virtual_name:          "" => ""
    block_device.0.volume_size:           "" => "30"
    block_device.0.volume_type:           "" => ""
    instance_type:                        "" => "t2.micro"
    key_name:                             "" => "your key name"
    private_dns:                          "" => "<computed>"
    private_ip:                           "" => "<computed>"
    public_dns:                           "" => "<computed>"
    public_ip:                            "" => "<computed>"
    security_groups.#:                    "" => "<computed>"
    subnet_id:                            "" => "${aws_subnet.public.id}"
    tenancy:                              "" => "<computed>"

+ module.mesos.aws_instance.slaves.2
    ami:                                  "" => "ami-06ef816e"
    availability_zone:                    "" => "<computed>"
    block_device.#:                       "" => "1"
    block_device.0.delete_on_termination: "" => "1"
    block_device.0.device_name:           "" => "/dev/sdb"
    block_device.0.encrypted:             "" => ""
    block_device.0.snapshot_id:           "" => ""
    block_device.0.virtual_name:          "" => ""
    block_device.0.volume_size:           "" => "30"
    block_device.0.volume_type:           "" => ""
    instance_type:                        "" => "t2.micro"
    key_name:                             "" => "your key name"
    private_dns:                          "" => "<computed>"
    private_ip:                           "" => "<computed>"
    public_dns:                           "" => "<computed>"
    public_ip:                            "" => "<computed>"
    security_groups.#:                    "" => "<computed>"
    subnet_id:                            "" => "${aws_subnet.public.id}"
    tenancy:                              "" => "<computed>"

+ module.mesos.aws_internet_gateway.public
    vpc_id: "" => "${var.vpc_id}"

+ module.mesos.aws_route_table.public
    route.#:             "" => "1"
    route.0.cidr_block:  "" => "0.0.0.0/0"
    route.0.gateway_id:  "" => "${aws_internet_gateway.public.id}"
    route.0.instance_id: "" => ""
    vpc_id:              "" => "${var.vpc_id}"

+ module.mesos.aws_route_table_association.public
    route_table_id: "" => "${aws_route_table.public.id}"
    subnet_id:      "" => "${aws_subnet.public.id}"

+ module.mesos.aws_security_group.maintenance
    description:                 "" => "maintenance"
    ingress.#:                   "" => "1"
    ingress.0.cidr_blocks.#:     "" => "1"
    ingress.0.cidr_blocks.0:     "" => "0.0.0.0/0"
    ingress.0.from_port:         "" => "22"
    ingress.0.protocol:          "" => "tcp"
    ingress.0.security_groups.#: "" => "0"
    ingress.0.self:              "" => "0"
    ingress.0.to_port:           "" => "22"
    name:                        "" => "maintenance"
    owner_id:                    "" => "<computed>"
    vpc_id:                      "" => "${var.vpc_id}"

+ module.mesos.aws_security_group.master
    description:                 "" => "master"
    ingress.#:                   "" => "2"
    ingress.0.cidr_blocks.#:     "" => "1"
    ingress.0.cidr_blocks.0:     "" => "0.0.0.0/0"
    ingress.0.from_port:         "" => "8080"
    ingress.0.protocol:          "" => "tcp"
    ingress.0.security_groups.#: "" => "0"
    ingress.0.self:              "" => "0"
    ingress.0.to_port:           "" => "8080"
    ingress.1.cidr_blocks.#:     "" => "1"
    ingress.1.cidr_blocks.0:     "" => "0.0.0.0/0"
    ingress.1.from_port:         "" => "5050"
    ingress.1.protocol:          "" => "tcp"
    ingress.1.security_groups.#: "" => "0"
    ingress.1.self:              "" => "0"
    ingress.1.to_port:           "" => "5050"
    name:                        "" => "master"
    owner_id:                    "" => "<computed>"
    vpc_id:                      "" => "${var.vpc_id}"

+ module.mesos.aws_security_group.private
    description:                 "" => "private"
    ingress.#:                   "" => "3"
    ingress.0.cidr_blocks.#:     "" => "0"
    ingress.0.from_port:         "" => "0"
    ingress.0.protocol:          "" => "udp"
    ingress.0.security_groups.#: "" => "0"
    ingress.0.self:              "" => "1"
    ingress.0.to_port:           "" => "65535"
    ingress.1.cidr_blocks.#:     "" => "0"
    ingress.1.from_port:         "" => "-1"
    ingress.1.protocol:          "" => "icmp"
    ingress.1.security_groups.#: "" => "0"
    ingress.1.self:              "" => "1"
    ingress.1.to_port:           "" => "-1"
    ingress.2.cidr_blocks.#:     "" => "0"
    ingress.2.from_port:         "" => "0"
    ingress.2.protocol:          "" => "tcp"
    ingress.2.security_groups.#: "" => "0"
    ingress.2.self:              "" => "1"
    ingress.2.to_port:           "" => "65535"
    name:                        "" => "private"
    owner_id:                    "" => "<computed>"
    vpc_id:                      "" => "${var.vpc_id}"

+ module.mesos.aws_security_group.slave
    description:                 "" => "slave"
    ingress.#:                   "" => "1"
    ingress.0.cidr_blocks.#:     "" => "1"
    ingress.0.cidr_blocks.0:     "" => "0.0.0.0/0"
    ingress.0.from_port:         "" => "5051"
    ingress.0.protocol:          "" => "tcp"
    ingress.0.security_groups.#: "" => "0"
    ingress.0.self:              "" => "0"
    ingress.0.to_port:           "" => "5051"
    name:                        "" => "slave"
    owner_id:                    "" => "<computed>"
    vpc_id:                      "" => "${var.vpc_id}"

+ module.mesos.aws_subnet.public
    availability_zone:       "" => "us-east-1e"
    cidr_block:              "" => "10.0.1.0/24"
    map_public_ip_on_launch: "" => "1"
    vpc_id:                  "" => "${var.vpc_id}"

+ module.mesos.null_resource.init_ceph

Now, you can do terraform apply and look into instances or Mesos/Marathon UI.

Inside Terraform module

There are a few techniques to avoid some difficulties in cluster management and more.

Ceph cluster initialization process

To start using Ceph cluster and Ceph FS, you have to run some procedures like this:

  • Create ceph.conf for the cluster
  • Deploy ceph.conf to mon servers, initialize each mon and gather keys generated on each servers
  • Deploy keys to osd servers and initialize each osd
  • Deploy keys to mds servers and initialize each mds
  • Create data and metadata OSD pool
  • Create Ceph FS using these pools
  • Mount Ceph FS pointing mon servers

Since they are too complicated and stateful, there is a big problem to manage Ceph cluster by Terraform or others.

ceph-deploy is a easy tool for these procedure, so I used it on the admin instance. But how can we run a initial procedure by Terraform?

null_resource is the best way. See terraform/init_ceph.tf

After spinning up all instances, this resource runs provisioners to initialize Ceph cluster like above. Once created this virtual resource, this procedure won’t be executed any more.

Ceph initializing and provisioning

Considering initializing process above, there are two types of resource creation; Initializing and provisioning.

Initializing means the completely initial timing. At this time, each instance doesn’t need to be provisioned because the cluster initialization process will do everything.

Provisioning, on the other hand, means after the cluster is initialized. For example, when a master/slave/admin instance is terminated, Terraform tries to re-create the instance and this is provisioning. It is a little bit different process than initializing because the cluster already exists.

See terraform/scripts/init_master.sh. if ceph_initialized; block is for this problem. So, all instances can be terminated anytime even after a Ceph cluster is initialized. Try terminate a instance and terraform apply!

SSH gateway and provisioners

Terraform provisioners assume the instance can be connected by ssh directly. In this module, I don’t open ssh port for cluster instances except admin instance, so it is a SSH gateway.

If you configure connection block to the gateway instead of the actual instance, you can use provisioners, but they run on the gateway.

So, I copy AWS key file into the admin instance to be able to login to other instances (there is not a way to use agent forward so far).

For each instance provisioning, I upload script files prefixed by the instance id to prevent overwriting race condition. See terraform/master1.tf. script_path option for connection is undocumented so far, btw.

Master IP addresses

I want to fix master IP addresses because they are hardcoded anywhere. I tried Terraform variable as list of IP addresses, but so far it was impossible.

Because of that, I fixed the number of masters and created each aws_instance resource.

When the list variable feature is implemented, I will refactor these configuration.

Concatenate provisioner scripts

I use bash script for provisioners and there are a lot of common functions, so I wrote a shared script terraform/scripts/header.sh.

To run each provisioner correctly, this file must be concatenated. Also, an entry point main must be concatenated. So I did like this:

1
2
3
4
5
provisioner "remote-exec" {
      inline = [
          "echo main foo bar | cat header.sh init_foo.sh - | bash"
      ]
}

I think there are much better ways, though…

Isolation between Terraform and Packer

There is a philosophy:

  • Packer
    • Install file resources from the Internet
  • Terraform
    • Install only runtime information, like IP address
      • Do not fetch file resources from the Internet

So, any apt-get install are done by Packer nor Terraform. This philosophy is like The Twelve-Factor App.

BTW, awk is great

I wanted a way to use the instance id built by Packer in Terraform automatically. I found that Packer has -machine-readable output option and it is CSV format. So, I started to write a processor script:

  • Print the machine readable output as well as normal Packer output
  • Write ami.tf file using the instance id built just now

See packer/process. This is my first awk executable script. awk was very nice in this case and I could find a lot of awk tips.

Understanding basystyle

progrium/bashstyle

To write complex shell script for Packer/Terraform, I read the bashstyle article above. I don’t understand all of them, but there are tons of best practice.

Conclusion

It was fun!

Hey, wait a moment… At the beginning, I just wanted to run many applications on Mesos using Docker, for example WordPress, MySQL Galera Cluster, etc. To implement them, I needed a shared file system, so I started to learn about Ceph which I had an eye on before. To deploy the cluster into AWS, I needed some orchestration software, then started to see Terraform, Packer to create AMI… Too long yak shaving it was ;(

Now, let’s start learning about Mesos/Marathon/Docker and many applications!

Comments