Archive for the ‘Systems’ Category.

A rock-solid setup for sending SMTP mail from your EC2 web server

(None of this is EC2-centric, but it’s particularly needed on EC2.)

A frequent topic of discussion on the EC2 forums is how to send email reliably, efficiently, and especially without it being marked as spam. I found that even with a valid SPF record most mail sent from an EC2 instance was marked as spam or silently discarded.

This is probably partly because of the lack of matching reverse DNS records. But spam filters can be a bit arbitrary and the easiest way is to relay outgoing mail through a good smtp provider. (I don’t recommend relaying outbound mail through Google Apps, they supposedly have a 500 messages/day limit according to many people on their forums, although I couldn’t find that published anywhere. UPDATE: The info is here, thanks John Ward.)

I have tried a couple of SMTP providers, and I recommend AuthSMTP. They are reliable, have good service, and our mail that’s delivered through them almost never gets marked as spam. Also, they have monthly quotas rather than daily, so you have a chance to increase it before you hit the limit.

Rather than deliver directly to the AuthSMTP mail server from your web app it’s a good idea to deliver to a local queueing mail server, which will forward via the AuthSMTP gateway. Your web app will deliver mail to localhost (or perhaps a dedicated instance if you prefer), port 25.

This has several advantages:

  1. Your web server can finish the request more quickly.
  2. There’s less chance that the mail server will be unavailable. At least the mail will be queued locally until the remote server becomes available again. AuthSMTP has proven to be quite reliable, but it has been unavailable on a couple of occasions.
  3. AuthSMTP limits the number of concurrent connections that you can make. You can easily configure your local mail server to limit the number of outgoing connections to the gateway.

Configuration

I recommend using Postfix, it’s fast, reliable and most importantly, easy to configure. Your Linux distribution will definitely have a Postfix package available (it comes pre-installed on EC2 on Rails). On Debian or Ubuntu install with:

sudo aptitude install postfix

Here’s the config file, /etc/postfix/main.cf:

myhostname = www.YOURDOMAIN.com
mydomain = YOURDOMAIN.com
myorigin = $mydomain

smtpd_banner = $myhostname ESMTP $mail_name
biff = no
append_dot_mydomain = no

alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
mydestination = localdomain, localhost, localhost.localdomain, localhost
mynetworks = 127.0.0.0/8
mailbox_size_limit = 0
recipient_delimiter = +

# SECURITY NOTE: Listening on all interfaces. Make sure your firewall is
# configured correctly
inet_interfaces = all

relayhost = [mail.authsmtp.com]
smtp_connection_cache_destinations = mail.authsmtp.com
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = static:YOUR_AUTHSMPT_USER_ID:YOUR_AUTHSMTP_PW
smtp_sasl_security_options = noanonymous

default_destination_concurrency_limit = 4

soft_bounce = yes

How simple is that?! Have you ever seen a sendmail config file?

soft_bounce is important because it means that postfix will queue the messages if they’re bounced by the remote gateway for any reason (this is only if it’s bounced by the gateway, not if it’s bounced by the destination server). This would usually be caused by some configuration problem like an authentication failure. If the message is bounced by the eventual destination server (e.g. the mailbox doesn’t exist or is full), or if the destination server can’t be contacted, your local server won’t know about it because the message has already been accepted by the gateway. (It’s probably a good idea to keep track of bounced messages returned by the eventual destination server, see “Don’t spoof the From field” below.)

default_destination_concurrency_limit is so you stay within AuthSMTP’s concurrent connection limit. If you have Postfix running on multiple instances you’ll need to adjust this accordingly.

To see mail that’s stuck in the queue:

mailq

Postfix will automatically try to resend it, but you can force it to be sent immediately using:

sudo postqueue -f

Monitoring

Of course you need to know if anything goes wrong with the mail delivery and it won’t be in your web app’s log. I use scripts in /etc/cron.hourly to check logs and mail me the output if there are errors. But when it comes you mail delivery failure you might have a bit of a chicken-and-egg problem: you can’t use postfix to send the mail if postfix is having problems. Here’s a simple ruby script to send emergency mail via a different mail server. It’s configured to use Google Apps (you’ll need to create a new account to send the mail from), if you don’t use Google Apps you can easily change this to use a different mail server.

Save this as /usr/local/bin/emergency_mail_sender:

#!/usr/bin/env ruby

# This is a simple script to send mail via an alternate server when there are
# errors with the normal queueing mail sender
# The subject is the first command-line arg and the body is received on stdin

#################################

from_address             = "admin_mail_sender@YOURDOMAIN.com"
to_address               = "admin@YOURDOMAIN.com"
smtp_server              = "smtp.gmail.com"
smtp_port                = 587
smtp_mail_from_domain    = "YOURDOMAIN.com"
smtp_account_name        = "admin_mail_sender@YOURDOMAIN.com"
smtp_password            = "YOUR_PASSWORD"
smtp_authentication_type = :plain
debug                    = false

#################################

subject = ARGV[0]
body = $stdin.read

require 'rubygems'
require 'net/smtp'
require 'tlsmail'

exit if body.nil? || body == ""

msgstr = <<END_OF_MESSAGE
Subject: #{subject}

#{body}
END_OF_MESSAGE

Net::SMTP.enable_tls(OpenSSL::SSL::VERIFY_NONE)
  smtp = Net::SMTP.new(smtp_server, smtp_port)
  smtp.set_debug_output $stderr if debug
  smtp.start(smtp_mail_from_domain, smtp_account_name, smtp_password, smtp_authentication_type) do |s|
    s.send_message msgstr, from_address, to_address
end

Here’s a script that can be run by cron every hour to check for mail delivery problems, it uses the emergency_mail_sender script to notify you of the problem. It works on Ubuntu (but it needs the logtail package installed), it might not work on other systems. Save this as /etc/cron.hourly/check_mail_logs

#!/bin/sh
hostname=`hostname -s`
mailer = /usr/local/bin/emergency_mail_sender
/usr/sbin/logtail -f/var/log/mail.warn | $mailer "$hostname: mail warnings"
/usr/sbin/logtail -f/var/log/mail.err | $mailer "$hostname: mail errors"
/usr/sbin/logtail -f/var/log/syslog | grep 'status=' | egrep -v 'status=sent' | $mailer "$hostname: undelivered mail"

SPF

Here’s your SPF record:

v=spf1 include:authsmtp.com include:aspmx.googlemail.com ~all

If you’re not using Google Apps to send mail for your domain remove include:aspmx.googlemail.com. If you want to create your own SPF record there’s a good SPF record generator at spfwizard.com.

Don’t spoof the From field

You should only send mail from somebody@yourdomain.com. If you try to send mail from somebody@pauldowman.com, for example, the receiver will see that pauldowman.com has an SPF record, and that it doesn’t authorize your mail server. Then into the spam folder you go.

To get around this you can send from something like noreply@yourdomain.com, and set the Reply-To header to somebody@pauldowman.com. You can even set the name in the from field, for example: “Paul Dowman via yoursite” <noreply@pauldowman.com>. The Reply-To header will make sure that most people’s replies go to the correct address, but a few will inevitably end up at noreply@yourdomain.com so it’s probably a good idea to set up an autoresponder at that address, or at least make sure the message bounces so the user eventually realizes the mistake.

EC2 on Rails now with multiple instance support, Ubuntu 7.10, 64-bit version, Capistrano tasks

I’ve been working hard on EC2 on Rails, version 0.9.5 is now available. Since my last post here there have been some major changes:

Capistrano tasks

There is now a rubygem available that provides Capistrano tasks to manage the instance. There are tasks to set the server’s timezone, install packages and rubygems, backup, restore, create and delete the database, set the MySQL root password, and more. To use these in your Rails project type:

> sudo gem install ec2onrails

Put Capfile in the root of your rails folder, and put deploy.rb in the config folder.

Then, from the root of your project type:

> cap ec2onrails:setup

This automatically sets your server’s timezone, installs any custom rubygems and Ubuntu packages, and creates your database for you. You can now deploy your rails app as you normally would:

> cap deploy:migrations

Another useful task for testing is:

> cap ec2onrails:restore_db_and_deploy

This recreates the database, restores data from an S3 bucket (specified in your deploy.rb), and deploys the app. I use this to prepare a staging server with the current production data and current production version of the app. After running this task I have an exact copy of my production server. I then deploy the latest version to this server before deploying it to production. This is a good way to be really sure your production deployment won’t fail (especially your migrations).

To see a list of all available Capistrano tasks:

> cap -T

New Ubuntu version

It’s now built with Ubuntu 7.10 “Gutsy”.

Support for new instance types

There are both i386 and x86_64 versions available to support the new EC2 instance types. So you can now use large and extra-large instances.

Multiple instances

The earlier versions only worked if your rails app was running on a single server. That was lame! Now you can have multiple instances using any combination of these roles: web server, app server, primary database. I’m working on adding a MySQL slave role and eventually a Memcache role.

For full instructions and details see the project web site.

EC2 server image update

Just a couple of announcements about my Ruby on Rails server image for EC2:

New version

There is a new server image with some minor changes and bug fixes. For details see the change log. I’ll keep the old image around for a while.

Mailing lists

I have created two Google groups, one for announcements only (new versions, etc.), and one for general discussion.

Ongoing work

I’m working on a build script so the image can be built from source. Then I will provide access to a subversion repository with the source. Some people have expressed an interest in contributing, which I’m really happy about! If you want to get involved, please join the mailing list!

I’ll set up your Ruby on Rails server for you. For free.

Need to set up a Ruby on Rails server? So do I, and chances are you need almost the same configuration as me. As of this writing 1845 people have bookmarked Coda Hale’s excellent guide to setting up Mongrel + Apache on del.icio.us. The article has 187 comments. Why should so many people each have to do the same thing?

The answer is a Rails server virtual appliance, and I’ve made one for Amazon’s EC2 hosting service.

A server is no longer a physical thing, and EC2 takes it one step further: they completely separate the concept of server images and instances. An image is like a snapshot of a server hard drive, stored in an unchangeable state in their S3 data storage service. An instance is a running server that was booted from a copy of this image. But the instance is transient; changes to it’s own working copy of the image don’t affect the original image that it was booted from, and because of this multiple server instances can be started from the same image. (Hence the “elastic” property, new instances can be created on demand, your server pool can grow and shrink dynamically.)

Now even the OS can be configured and tested once, and then simply deployed as part of the application. So how is that different from any of the third-party libraries and frameworks that we use as part of our applications? I assemble applications out of many pieces of third-party software. All of it is continually improved by other people, none of it requires painstaking building or configuration, and the OS can be used the same way.

So I’m going to continue working on my Ruby on Rails EC2 server image with the goal that you will be able to simply deploy your Rails app directly to it with Capistrano with little or no configuration.

Try it out, and tell me what you think!

Public Ubuntu & Ruby on Rails Image for Amazon EC2

UPDATE: I have created a new improved version of this image. The new image uses mongrel_cluster, and is more Capistrano-friendly. Skip this article and check out the new one!

I have created a ready-to run public Ubuntu and Ruby on Rails image for Amazon EC2. (You can easily remove the Ruby on Rails stuff and use this as a general-purpose Ubuntu image.) My goal was to make it as easy to use as possible, so it includes a script to automate the process of re-bundling it as your own after you make configuration changes.

This means if you have an Amazon EC2 account you can run this image, log in and customize it as desired, and then with one command save a copy of it as your own.

Features include:

  • Ubuntu 7.04 Feisty with Xen versions of standard libs (libc6-xen package) for better performance.
  • All EC2 command-line tools installed
  • Custom script to re-bundle, save and register your own copy of this image in one step
  • MySQL 5
  • Ruby 1.8.5
  • Ruby on Rails 1.2.3
  • Mongrel behind Apache 2.2, all pre-configured including /etc/init.d startup script
  • MySQL and Apache configured to write logs to /mnt/log so you don’t fill up the tiny root filesystem
  • Hostname set correctly to public hostname
  • NTP

Soon to be added:

  • Automatic MySQL backup to S3
  • Mongrel Cluster
  • Capistrano and elastic_rails support. I’d like it to be so that you can just start up an instance of this image and deploy to it.

The AMI id is currently ami-5d967334. UPDATE: I have created a new improved version of this image. Please try that one instead, this image will be removed soon.

Instructions to use this public AMI:

1. Sign up for an EC2 account

There is currently a waiting list, as of this writing in June 2007.

2. Install the command-line tools

The tools are installed on the image, but you need a way to start an instance of the image. See the getting started guide for details. The tools will need your Amazon access key identifiers.

3. Run the instance and log in via ssh

Again, the getting started guide describes this in detail. Obviously you can skip the “Finding a Suitable AMI” section.

NOTE: at boot time it retrieves your private key via HTTP using curl, and occasionally this fails. If you are prompted for a password and you’re sure that you’ve started the image with the right keypair and that you are providing the right private key try rebooting the image using “ec2reboot <image-id>“. UPDATE: this is fixed in the new version.

4. Get security updates

I recommend using aptitude for package management:

[server]# aptitude update
[server]# aptitude upgrade

5. (Optional) Remove unwanted packages
Again, use aptitude, this time run it with no arguments on the command-line. To completely remove a package including all it’s configuration and data select it and then press underscore (’_'). If you don’t want rails just remove the package “rubygems”. But don’t remove ruby itself because it’s needed by the Amazon tools.

6. Install your Rails app

Edit /etc/mongrel and set APP_DIR to the root of your rails directory. If you want multiple mongrel containers edit /etc/init.d/mongrel and add as many as you like to the start_cmd() and stop_cmd() sections. I assume this part will need some tweaking.

Be mindful of where your app writes log files and other data, it should go into /mnt because the root partition doesn’t have much space and you don’t want to fill it up.

7. (Optional) Create a new admin user (with sudo ability) and disable the root user

Ubuntu normally installs without a root user, by default you create a user with sudo access. This is standard best practice anyway so I recommend it.

NOTE: This new user will log in the normal way, i.e. with password rather than using the public key.

First, create the new user and add it to the admin group:

[server]# adduser myuserid
[server]# adduser myuserid admin

You now have a new user that can run sudo. You might want to add the following line to the end of the new user’s $HOME/.profile

source /usr/local/ec2/config

This will set up environment variables needed for the EC2 command-line tools, and add them to the path.

Now disable root login completely. Edit /etc/ssh/sshd_config and change the line

PermitRootLogin without-password

to the following:

PermitRootLogin no

Then edit /etc/passwd and change the first line to:

root:x:0:0:root:/root:/bin/false

(i.e. change “/bin/bash” to “/bin/false”). Now you can only log in using your newly created account.

8. Rebundle the image

I have included a script, rebundle.sh, that runs all the commands to bundle the new image, upload it to S3 and register it. It expects a directory, /mnt/ec2-config, that contains a config file and your AWS access identifiers. The contents of the directory are the following three files:

a) cert-XXXX.pem and pk-XXXX.pem. These are the X.509 certificate and private key files from your Amazon access key identifiers.

b) A config file named, strangely enough, config. It’s contents should look like the following:

AWS_ACCOUNT_ID=1234-1234-1234
KEY_FILE_NAME=pk-XXXX.pem
CERT_FILE_NAME=cert-XXXX.pem
AWS_ACCESS_KEY_ID=ABC0123
AWS_SECRET_ACCESS_KEY=abc0123abc0123abc0123
BUCKET_BASE_NAME=a-string-identifier

The values for those should be pretty self-explanatory except perhaps BUCKET_BASE_NAME. It’s value is used when generating a new S3 bucket to save your bundled image. The bucket name will be this string with a time/date stamp appended.

Once you’ve created /mnt/ec2-config and it’s contents one command will rebundle the current instance as a new AMI, save it to a new S3 bucket, and register it:

[server]# /usr/local/ec2/rebundle.sh

It takes a long time and there are long periods with no output so you might want to hit a key once in a while (or set “ServerAliveInterval 60″ in your ssh_config file) to avoid being disconnected while it’s running.

At the end you should see the id of your new AMI and the name of the S3 bucket that it’s stored in.

Phew. You’re done! :-)

A note on security

As is standard for public AMI’s, password-based logins for root are disabled. This is so that neither I nor anyone else (except you) can log into your running instance. You log in with your own public/private keypair (see step #3), otherwise there would be a window of opportunity between system startup and the moment you change the password. I have taken care to ensure that my rebundle script excludes sensitive data like your AWS access identifiers and root’s .ssh/authorized_keys file.

Acknowledgements

I used the instructions on the Atlantis Technology blog to get started with a working Ubuntu image and then many helpful posts on the Amazon Web Services developer forum to get the Amazon EC2 tools working.