Oddjob in the Cloud with Amazon Web Services

It’s quite straight forward to deploy a java process to an AWS EC2 instance. I followed the EC2 Getting Started Guide to create a Linux instance, installed java by following this blog article, uploaded the Oddjob tarball and started it from Putty and voila – Oddjob running on EC2.

Unfortunately it’s all a bit manual. Could automating the deployment of Oddjob be a Job for Oddjob? I think so. Time to spend two weeks automating a ten minute manual process. The result is 239 lines of XML configuration that can be seen here.

Here’s what that XML looks like in Oddjob Explorer, once it’s run:

Oddjob Deploying Oddjob to EC2

Let’s go through the main steps.

Some Setup

We declare some properties, check we have something to deploy and then declare some variables. Whereas properties are purely text items of reusable configuration, variable are more complicated elements of reusable configuration. Here we define a map that will be used tag the EC2 objects we create, and a filter that can be used to find those same objects. The three xml tasks to do this look like this.

<properties name="Setup Properties">
	<values>
		<value key="ojdeploy.aws.dir" value="${user.home}/.aws"/>
		<value key="ojdeploy.key.name" value="OddjobKeyPair"/>
		<file file="${ojdeploy.aws.dir}/${ojdeploy.key.name}.pem" key="ojdeploy.key.file"/>
		<value key="ojdeploy.instance.image.id" value="ami-09b89ad3c5769cca2"/>
		<value key="ojdeploy.instance.type" value="t2.micro"/>
		<value key="ojdeploy.security.group.name" value="Oddjob-Deploy-SG"/>
		<file file="${user.home}/projects/oj-assembly/work/ojdist/oddjob-1.6-SNAPSHOT.tar.gz" key="ojdeploy.oddjob.zip"/>
		<value key="ojdeploy.aws.user" value="ec2-user"/>
	</values>
</properties>
<exists file="${ojdeploy.oddjob.zip}" name="Check Oddjob Zip Exists"/>
<variables id="vars">
	<tags>
		<map>
			<values>
				<value key="purpose" value="OddjobDeploy"/>
			</values>
		</map>
	</tags>
	<filters>
		<list>
			<values>
				<aws:ec2-filter name="tag:purpose" values="OddjobDeploy" xmlns:aws="oddjob:aws"/>
			</values>
		</list>
	</filters>
</variables>

Security

Next is a job to create a key pair and save it as a file so that it can be used in the SSH steps later:

<aws:ec2-create-key-pair keyName="${ojdeploy.key.name}" name="Else Create Key Pair" xmlns:aws="oddjob:aws">
	<output>
		<file file="${ojdeploy.key.file}"/>
	</output>
	<tags>
		<value value="${vars.tags}"/>
	</tags>
</aws:ec2-create-key-pair>

And a few jobs to create a Security Group and Authorize our IP address:

<web:client id="myIp" name="Get Our Ip Address" url="http://bot.whatismyipaddress.com/" xmlns:web="oddjob:web"/>
<aws:ec2-create-security-group description="Oddjob Deploy Security Group " groupName="${ojdeploy.security.group.name}" id="createSecurityGroup" name="Create Security Group" xmlns:aws="oddjob:aws">
	<tags>
		<value value="${vars.tags}"/>
	</tags>
</aws:ec2-create-security-group>
<properties name="Extract Security Group Id as Property">
	<values>
		<value key="ojdeploy.security.group.id" value="${createSecurityGroup.groupId}"/>
	</values>
</properties>
<aws:ec2-authorize-security-group groupId="${ojdeploy.security.group.id}" name="Create Inbound Rules" xmlns:aws="oddjob:aws">
	<inboundPermissions>
		<list>
			<values>
				<aws:ec2-ip-permission ipProtocol="tcp" ipRanges="${myIp.responseBody}/32" portRange="22"/>
				<aws:ec2-ip-permission ipProtocol="tcp" ipRanges="${myIp.responseBody}/32" portRange="8080"/>
			</values>
		</list>
	</inboundPermissions>
</aws:ec2-authorize-security-group>

Because we have a dynamic IP address allocated by our internet provider we use the web:client job to get our IP address. We then use this in the authorise job that creates the rules. The other thing to note here is that once the Create Security Group job has run the Id can be accessed from the groupId property.

Creating The Instance

<aws:ec2-run-instances id="createInstance" imageId="${ojdeploy.instance.image.id}" instanceType="${ojdeploy.instance.type}" keyName="${ojdeploy.key.name}" name="Create An Instance" securityGroupIds="${ojdeploy.security.group.id}" xmlns:aws="oddjob:aws">
	<tags>
		<value value="${vars.tags}"/>
	</tags>
</aws:ec2-run-instances>
<properties name="Extract Instance Id Property">
	<values>
		<value key="ojdeploy.instance.id" value="${createInstance.responseInstanceIds[0]}"/>
	</values>
</properties>

This creates the instance and extracts the newly created instance Id as a property.

We then use a nested Oddjob to wait until the instance is running:

<oddjob>
    <job>
        <cascade>
            <jobs>
                <scheduling:retry reset="HARD" xmlns:scheduling="http://rgordon.co.uk/oddjob/scheduling">
                    <schedule>
                        <schedules:count count="36" xmlns:schedules="http://rgordon.co.uk/oddjob/schedules">
                            <refinement>
                                <schedules:interval interval="0:00:05"/>
                            </refinement>
                        </schedules:count>
                    </schedule>
                    <job>
                        <sequential>
                            <jobs>
                                <aws:ec2-describe-instances id="describeInstance" instanceIds="${instance.id}" name="Describe Instance" xmlns:aws="oddjob:aws"/>
                                <check eq="${expected.state}" name="Check State ${expected.state}" value="${describeInstance.detailById(${instance.id}).state}"/>
                            </jobs>
                        </sequential>
                    </job>
                </scheduling:retry>
            </jobs>
        </cascade>
    </job>
</oddjob>

This checks the state every 5 seconds for 3 minutes. The state is passed in as the property expected.state. When the state is met the retry succeeds and terminates, and the next stage in the parent cascade runs.

Installing Oddjob

<ssh:cascade xmlns:ssh="oddjob:ssh">
	<connection>
		<ssh:connection host="${ojdeploy.instance.publicDns}" user="${ojdeploy.aws.user}">
			<keyIdentityProvider>
				<ssh:file-keypair>
					<keyFiles>
						<file file="${ojdeploy.key.file}"/>
					</keyFiles>
				</ssh:file-keypair>
			</keyIdentityProvider>
		</ssh:connection>
	</connection>
	<jobs>
		<ssh:scp name="Copy Oddjob" remote="oddjob.tar.gz">
			<from>
				<file file="${ojdeploy.oddjob.zip}"/>
			</from>
		</ssh:scp>
		<ssh:exec command="tar -zxvf oddjob.tar.gz" name="Unzip Oddjob"/>
		<ssh:exec command="sudo amazon-linux-extras enable corretto8" name="Enable Corretto8"/>
		<ssh:exec command="sudo yum clean metadata" name="Clean Metadata"/>
		<ssh:exec command="sudo yum install -y java-1.8.0-amazon-corretto" name="Install Corretto8"/>
	</jobs>
</ssh:cascade>

The ssh:cascade runs a number of child ssh jobs within the same session. What’s happening here is:

  • We copy the Oddball tar ball.
  • We extract it.
  • We run 3 command that we got off the AWS blog that set up java

Start Oddjob

<ssh:exec command="nohup java -jar oddjob/run-oddjob.jar -f oddjob/server-web.xml < /dev/null > nohup.out 2>&amp;1 &amp;" name="Run Oddjob" xmlns:ssh="oddjob:ssh">
	<connection>
		<ssh:connection host="${ojdeploy.instance.publicDns}" user="${ojdeploy.aws.user}">
			<keyIdentityProvider>
				<ssh:file-keypair>
					<keyFiles>
						<file file="${ojdeploy.key.file}"/>
					</keyFiles>
				</ssh:file-keypair>
			</keyIdentityProvider>
		</ssh:connection>
	</connection>
</ssh:exec>

This is a separate job because we might want to run it independently of installing Oddjob.

Finally Connect with our Browser

Browser view of Oddjob on EC2
Browser View of Oddjob on EC2

This is the result of pointing our browser at the public DNS name of our Amazon instance (It can be copied out of the log panel of the SSH job) and port 8080. Oddjob has started and has run our two echo jobs.

Conclusion

And that’s how to deploy Oddjob to EC2.

If you’d like to try it for yourself you will need a 1.6-SNAPSHOT version of Oddjob and have an AWS credentials file in the default location.