torstai 10. lokakuuta 2013

Creating a bootable Xubuntu USB stick and installing Xubuntu on another USB stick

A friend of mine was complaining that his Asus Eee 1015BX is running too slow with Windows 7 Starter that came pre-installed with the laptop. It's no wonder that Windows is slow, because the laptop has only 1GB of memory. As a great (Ubuntu) Linux evangelist, I wanted to help the poor guy and give him a chance to test Ubuntu without any risks.

Because Ubuntu would probably be too heavy for such low end hardware, I thought that Xubuntu would serve better. I didn't want to change anything on the laptop. I just wanted to offer the Ubuntu experience without any risks. I decided that the best way to achieve this would be to install Xubuntu on USB disk.

To install Xubuntu on USB disk, you need installation media. Well, this laptop has only two USB ports (of course also LAN), so I cannot use DVD as in my previous installations. First problem was to create Xubuntu installation media. For some reason Ubuntu Startup Disk Creator doesn't allow user to create bootable USB stick from Xubuntu ISO image. Luckily there's an alternative, UNetbootin, which worked perfectly.

Next step was to run the installation software. You cannot set Eee's BIOS to boot from USB, but you can hit escape key during start-up to change booting options. After I got Eee to boot from USB, I inserted another USB stick, which would serve as the installation target media.

I'm not going through the details step-by-step, because installer is quite easy to use. I created /, /boot and /home partitions and no swap. It's important to notice that you shouldn't use USB stick to store a swap partition. Firstly, swap on USB is slow and secondly, it will - according to internet discussions - destroy your USB stick after some time. Installation took a lot of time, almost an hour. It would never take that long on a hard drive.

After the installation, there's some things you can do to optimise the speed of the system and the lifetime of the USB drive. Here's what I did.

Edit /etc/fstab. Add noatime flag to / and /home to avoid updating access times on files. This should speed things up  and help to avoid extra wearing of the USB stick. Here's an example.
# / was on /dev/sdb5 during installation
UUID=ae7eb15f-a65b-4e2b-a008-20a2b41ce72a /               ext4    noatime,errors=remount-ro 0       1
# /boot was on /dev/sdb1 during installation
UUID=190d4960-d773-451f-bbb3-b1a8bd39238b /boot           ext4    defaults        0       2
# /home was on /dev/sdb6 during installation
UUID=2fb3fc52-6155-4fdf-89f5-d991bca484fc /home           ext4    defaults,noatime        0       2

Disable mlocate so that it won't re-index the whole disk every day.
sudo chmod -x /etc/cron.daily/mlocate

Disable journaling from / and /home (you need to do these in single user mode or by starting Linux from some other media like the USB stick containing installation media, because the partitions cannot be mounted during the operation). By disabling journaling, you may end up with corrupted file system if the system crashes. On the other hand, you reduce the number of writes on the USB drive which should make things faster and prolong the lifetime of USB drive.
sudo tune2fs -O ^has_journal /dev/sdb5
sudo tune2fs -O ^has_journal /dev/sdb6

By the way, you can also prepare the drives already beforehand and format the partitions for ext4 without journaling with the following command. I did this after my first installation attempt, because I hoped it would speed up the slow installation process (I'm not sure did it help).
sudo mke2fs -t ext4 -O ^has_journal /dev/sdb6

After editing the file system parameters, it's a good idea to check that the file system is ok (-f for forcing the check)
sudo e2fsck -f /dev/sdb5
sudo e2fsck -f /dev/sdb6


Here's what I learned from all of this. Xubuntu is much much faster than Windows 7 on the Asus Eee. That was no surprise. All the hardware worked properly. After installing the proprietary AMD display drivers, battery consumption went down and battery life is at least as good as on Windows. Although it takes some time to install Xubuntu from USB to USB, it's a very good way to make a test drive with Xubuntu. You don't risk anything and user can always go back to Windows without any extra hassle.

Although Xubuntu worked well, my USB stick didn't. I bought a tiny Kingston 32GB Datatraveller Micro to serve as installation target media. The Datatraveller Micro is so small that you can carry it attached to the laptop all the time. Unfortunately, it's also slow making Xubuntu work very slowly during disk intensive operations. I compared the write speed of Datatraveller Micro to Datatraveller 100 on USB 2 port. Datatraveller Micro's write speed was only 6MB/s whereas Datatraveller 100's write speed was 22MB/s. Although the physical dimensions of Datatraveller Micro are perfect for running laptop Linux from it, the speed of the drive makes it questionable for this purpose.

This story may have a happy ending, because it seems that Ubuntu is getting yet another happy user in the very near future (after I install Xubuntu on Eee's hard drive)!

lauantai 2. helmikuuta 2013

Easy JUnit testing with Elastic Search

It was quite difficult to find good examples on JUnit testing (this is more like integration testing than unit, but nevertheless JUnit is used) Elastic Search code. Here's my attempt to fix this issue.

The idea is to start a standalone Elastic Search instance in the test. This way we don't have to make sure that every developer has access to Elastic Search instance running somewhere out of build's control. The downside of starting an instance inside test is that it may get quite slow to run the test. However, that's another problem to tackle.

I'm using Jersey HTTP client in this example to connect to Elastic Search. Any other client works as well.

Maven POM 

Maven POM that's needed to execute my example.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>fi.elastic-search.test</groupId>
  <artifactId>elastic-junit</artifactId>
  <name>elastic-junit</name>
  <packaging>jar</packaging>
  <version>1.0.0-BUILD-SNAPSHOT</version>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.elasticsearch</groupId>
      <artifactId>elasticsearch</artifactId>
      <version>0.20.3</version>
    </dependency>
    <dependency>
      <groupId>com.sun.jersey</groupId>
      <artifactId>jersey-client</artifactId>
      <version>1.17</version>
</dependency> 
  </dependencies>
</project>

JUnit test code 

Here's the Java code starting standalone Elastic Search instance and creating an index.
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import javax.ws.rs.core.MediaType;

import org.elasticsearch.common.io.FileSystemUtils;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.node.Node;
import static org.elasticsearch.node.NodeBuilder.nodeBuilder;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.UniformInterfaceException;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.api.client.config.ClientConfig;
import com.sun.jersey.api.client.config.DefaultClientConfig;
import com.sun.jersey.api.client.filter.LoggingFilter;


public class ElasticSearchTest {
    private Client client;
    private static String testIndexName = "junitindexname";
    private static final String HTTP_BASE_URL = "http://localhost";
    private static final String HTTP_PORT = "9205";
    private static final String HTTP_TRANSPORT_PORT = "9305";
    private static final String elasticSearchBaseUrl = HTTP_BASE_URL + ":" + HTTP_PORT;

    private static Node node;

    @BeforeClass
    public static void startElasticSearch() throws Exception {
        final String nodeName = "junittestnode";

        Map settingsMap = new HashMap();
        // create all data directories under Maven build directory
        settingsMap.put("path.conf", "target");
        settingsMap.put("path.data", "target");
        settingsMap.put("path.work", "target");
        settingsMap.put("path.logs", "target");
        // set ports used by Elastic Search to something different than default
        settingsMap.put("http.port", HTTP_PORT);
        settingsMap.put("transport.tcp.port", HTTP_TRANSPORT_PORT);
        settingsMap.put("index.number_of_shards", "1");
        settingsMap.put("index.number_of_replicas", "0");
        // disable clustering
        settingsMap.put("discovery.zen.ping.multicast.enabled", "false");
        // disable automatic index creation
        settingsMap.put("action.auto_create_index", "false");
        // disable automatic type creation
        settingsMap.put("index.mapper.dynamic", "false");

        removeOldDataDir("target/" + nodeName);

        Settings settings = ImmutableSettings.settingsBuilder()
                .put(settingsMap).build();
        node = nodeBuilder().settings(settings).clusterName(nodeName)
                .client(false).node();
        node.start();
    }

    private static void removeOldDataDir(String datadir) throws Exception {
        File dataDir = new File(datadir);
        if (dataDir.exists()) {
            FileSystemUtils.deleteRecursively(dataDir, true);
        }
    }

    @AfterClass
    public static void stopElasticSearch() {
        node.close();
    }

    @Before
    public void initialize() {
        // create client for each test
        ClientConfig clientConfig = new DefaultClientConfig();
        client = Client.create(clientConfig);
        client.addFilter(new LoggingFilter(System.out));
    }
    
    @Test
    public void testCreateIndex() {
        WebResource service = client.resource(elasticSearchBaseUrl);
        // check first that does the index already exist
        ClientResponse clientResponse = service.path(testIndexName).head();
        if (clientResponse.getClientResponseStatus().equals(ClientResponse.Status.OK))
        {
            Assert.fail("Index exists already");
        }

        String indexJson = 
                "{\"settings\" : {\"index\" : {\"number_of_shards\" : 1,\"number_of_replicas\" : 0}}}";
        try {
            String response = 
                    service.path(testIndexName).
                    queryParam("refresh", "true").
                    queryParam("timeout","5m").
                    accept(MediaType.APPLICATION_JSON).
                    put(String.class, indexJson);
            if (!response.contains("ok")) {
                Assert.fail("Creating index failed. IndexName: " + testIndexName);
            }
        } catch (UniformInterfaceException e) {
            // failed due to Client side problem
            throw e;
        }

        // wait for Elastic Search to be ready for further processing
        HashMap statusParameters =
                new HashMap();
        final String timeout = "30s";
        statusParameters.put("timeout", timeout);
        statusParameters.put("wait_for_status", "green");
        String statusResponse = status(statusParameters);
        if (statusResponse.contains("red")) {
            Assert.fail("Failed to create index");
        }
    }
    
    public String status(final Map optionalParameters) {
        WebResource service = client.resource(elasticSearchBaseUrl);
        WebResource webResource = service.path("_cluster").path("/health");
        for (Entry entry : optionalParameters.entrySet()) {
            webResource = webResource.queryParam(entry.getKey(),entry.getValue());
        }
        return webResource.get(String.class);
    }

}

sunnuntai 19. elokuuta 2012

Some tomcat7-maven-plugin tricks

Here's some useful tomcat7-maven-plugin tricks that work also with other Maven plugins.

How to set java endorsed dir for tomcat7-maven-plugin

Sometimes you need to use Java endorsed standards override mechanism with Maven plugins. This can be accomplished simply by setting proper MAVEN_OPTS value. For example

export MAVEN_OPTS="-Djava.endorsed.dirs=/some/directory/where/endorsed/jars/are"

How to change tomcat7-maven-plugin classpath order

Very often you need to have the runtime classpath in certain order when running Tomcat or other Maven plugins. I found out by trial and error that the used classpath JAR ordering depends on the order of declared dependencies in pom.xml (most likely this is documented somewhere). So, if you need to get some libraries before others, just move them in the beginning of pom.xml. Of course this does not help, if you have many plugins that need a different order for each plugin.

How to list all loaded classes and their respective JAR files

This is something I had been looking for years and was extremely happy when I finally got to know it. At least Sun Java JRE provides a command line switch for showing all the classes that are loaded into JVM and the source JAR file of those classes. It's not always easy to know what classes are loaded, especially if your dependencies have dependencies to other JAR files that are somehow conflicting (for example different version) with JAR dependencies defined elsewhere. Anyway, to list the classes and their exact source, do the following.

export MAVEN_OPTS="-verbose:class"

The output is something like this.
[Loaded javax.xml.bind.annotation.XmlElement from /home/perttu/software/jdks/jdk1.6.0_31/jre/lib/rt.jar]
[Loaded javax.xml.bind.annotation.XmlElement$DEFAULT from /home/perttu/software/jdks/jdk1.6.0_31/jre/lib/rt.jar]
[Loaded javax.xml.ws.WebFault from /home/perttu/software/jdks/jdk1.6.0_31/jre/lib/rt.jar]

With these tools, any problem related to Tomcat Maven plugin and classpath should be easy to solve.

perjantai 25. toukokuuta 2012

Encrypting SWAP partition and taking previously encrypted home partition into use

Due to my personal Ubuntu release update process, I have to configure encryption of my boot and home partition manually every now and then. Because I do this regularly, I'm documenting my process here in my blog...

Here's my Ubuntu release update process.
  1. Make Ubuntu installation program to format the partition that held the previous Ubuntu version
  2. Format and configure existing boot partition as the new boot partition
  3. Configure existing swap partition as the new swap partition
  4. Install Ubuntu normally without dedicated home partition
After installation, my encrypted home partition is not accessible anymore, because it was not configured as part of the installation process. I haven't been able to configure either normal installer or alternative installer to take previously encrypted partitions into use. So, it has to be done after installation.

After installation has finished, install cryptsetup.

$ sudo apt-get install cryptsetup

Then configure /etc/fstab file by adding configuration for swap and home. For example.

/dev/mapper/sda7_crypt /home           ext4    defaults        0       2
/dev/mapper/sda5_crypt none            swap    sw              0       0

The actual values depend on your hard disk partitioning. Here's example from my laptop.

$ sudo fdisk -l

Disk /dev/sda: 500.1 GB, 500107862016 bytes
255 heads, 63 sectors/track, 60801 cylinders, total 976773168 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x7250e0b9

   Device Boot      Start         End      Blocks   Id  System
/dev/sda1            2048    24588287    12293120   27  Hidden NTFS WinRE
/dev/sda2        24588288    24793087      102400    7  HPFS/NTFS/exFAT
/dev/sda3   *    24793088    26746879      976896   83  Linux
/dev/sda4        26748926   976773119   475012097    5  Extended
/dev/sda5        26748928    34559999     3905536   82  Linux swap / Solaris
/dev/sda6        34562048   132216831    48827392   83  Linux
/dev/sda7       132218880   976773119   422277120   83  Linux

The next step is to configure /etc/crypttab by adding appropriate encryption setup. In my case it looks like this.

sda5_crypt /dev/sda5 /dev/urandom cipher=aes-cbc-essiv:sha256,size=256,swap
sda7_crypt UUID=5a9c38c3-2aa9-433b-9efd-c0e9357d0811 none luks

The swap partition setup is "universal" and it should work on any computer (of course the correct partition may differ from this example). The swap is encrypted with a key that is randomly generated on each system startup.

For the home partition you have to know the UUID of the partition. Here's one way to find it.

$ sudo blkid 
/dev/sda1: LABEL="Recovery" UUID="78F82CACF82C6A98" TYPE="ntfs" 
/dev/sda2: LABEL="System Reserved" UUID="62ACA8E6ACA8B5C7" TYPE="ntfs" 
/dev/sda3: UUID="7d526d52-018a-4b0c-9e26-64f1143cf0da" TYPE="ext4" 
/dev/sda5: UUID="0d0178b1-ade5-416f-8535-82455a8febd5" TYPE="swap" 
/dev/sda6: UUID="b0d7a0b7-0bb8-4cf9-978a-f7c6ebb2126f" TYPE="ext4" 
/dev/sda7: UUID="5a9c38c3-2aa9-433b-9efd-c0e9357d0811" TYPE="crypto_LUKS" 
/dev/sdb1: LABEL="siirtoNTFS" UUID="494640E516B11A6B" TYPE="ntfs" 
/dev/sdb2: LABEL="siirto" UUID="570857a6-4ab3-4d4b-99a2-88d383d3e588" TYPE="ext4" 

With this configuration, the system should ask for home partition's encryption key during system startup and everything should work as before.

sunnuntai 20. toukokuuta 2012

Jetty Maven Plugin with hot code replace



I'm often using Jetty Maven plugin to execute my web applications right from the build. It provides an easy and operating system independent way to execute the web app. You only have to check out the source code from version contol, execute Maven build and start Jetty with Maven command. What could be easier way to run your web application in development environment?

I want to point out the following benefits in using Maven Jetty plugin

  • IDE independent way to execute web application. Works on Eclipse, IDEA, NetBeans and whatever
  • Support hot code replace: you can change code inside methods without restarting the whole application
  • Works on Windows, Linux and Mac or any other Java compatible operating system. No need to setup the application server instance
  • Starts fast and it's easy to reload the application to the server after changes

Just add the XML snippet in the end of this post to your pom.xml and execute Maven with mvn jetty:run. As a result, you will get response from your web app in http://localhost:8180/example/. You can run the same goal from your favorite IDE and, thus, you get an IDE independent web app execution!

In case you want to try hot code replace, add the classpaths containing your code inside the extraclasspath-element. Then start the Maven build running Jetty in debug mode and connect to the debugging session with your favorite IDE. The easiest way to achieve this in Eclipse is to run the Maven build in debug mode and then connect to the process with Eclipse debugger. After Eclipse is properly connected to the Maven process running Jetty, all the code changes in method bodies are instantly visible in the running process  (of course the same code has to be modified by Eclipse so that the changes get to the classpath of Jetty).

If you prefer doing things in IDE, at least Eclipse has excellent Jetty plugin called Run Jetty Run (http://code.google.com/p/run-jetty-run/).

<build>
<plugins>
...
<plugin>
    <groupid>org.mortbay.jetty</groupid>
    <artifactid>jetty-maven-plugin</artifactid>
    <version>7.5.2.v20111006</version>
    <configuration>
        <stopport>9966</stopport>
        <stopkey>${project.artifactId}<stopkey>
        <!-- scanning is not used if reload is set to manual -->
        <scanintervalseconds>5</scanintervalseconds>
        <!-- application reloading by pressing enter in the console -->
        <reload>manual</reload>
        <webappconfig>
            <contextpath>/example</contextpath>
            <!-- Changes in these classes will be instantly applied to running Jetty process without restart -->
            <extraclasspath>target/classes;../dependant-project/target/classes;../another-dependant-project/target/classes</extraclasspath>
        </webappconfig>
        <!-- directories whose changes cause automated Jetty context reloading, not used if reload is manual -->
        <scantargets>
            <scantarget>../dependant-project/target/classes</scantarget>
        </scantargets>
        <connectors>
            <connector implementation="org.eclipse.jetty.server.nio.SelectChannelConnector">
                <port>8180</port>
                <maxidletime>60000</maxidletime>
            </connector>
        </connectors>
        <systemproperties>
            <!-- system properties that are used for running Jetty -->
            <systemproperty>
                <name>some.system.property</name>
                <value>somevalue</value>
            </systemproperty>
        </systemproperties>
    </configuration>
    <dependencies>
        <!-- dependencies added to Jetty's classpath -->
        <dependency>
            <groupid>log4j</groupid>
            <artifactid>log4j</artifactid>
            <version>${log4j.version}</version>
            <type>jar</type>
        </dependency>
    </dependencies>
</plugin>
...
</plugins>
</build>

maanantai 9. huhtikuuta 2012

Automatisoidun asennuksen autuus

Kuinka monesti olet omassa projektissasi joutunut rutiininomaisesti asentamaan tekemäsi sovelluksen testi- ja tuotantoympäristöihin yhä uudelleen ja uudelleen? Kuinka monta kertaa muistat tehneesi huolimattomuusvirheen asennuksen aikana? Montako kertaa olet ihmetellyt testi- ja tuotantoympäristön konfiguraatiota, jonka joku muu on muuttanut tietämättäsi ja sovelluksesi rikkoen? Oma vastaukseni edellisiin kysymyksiin on lähes joka projektissa ja erittäin monta kertaa. Onneksi poikkeuksiakin mahtuu joukkoon. Näissä poikkeustapauksissa asennusautomaatio on tehty enemmän tai vähemmän täydelliseksi. Keskityn tässä blogauksessa käsittelemään näitä poikkeusprojekteja ja syitä siihen, miksi jokaisen projektin pitäisi automatisoida sovelluksen ja käyttöympäristön asennukset. Poikkeuksesta pitäisi siis tulla pääsääntö!

Asennuksen eri tasot

Oman kokemukseni perusteella sovelluksien asentamiseen liittyy neljä eritasoista asiaa.
  1. Laitteisto, joko fyysinen tai virtuaalinen
  2. Käyttöjärjestelmä ja sen päälle asennettavat perusbinäärit, kuten WWW-palvelin
  3. Ympäristön yleiset konfiguraatiot, jotka liittyvät käyttöympäristön ylläpitoon. Esimerkkinä ajastetusti suoritettavat varmuuskopioinnit
  4. Itse asennettavat sovellukset ja niiden konfiguraatiot
Nähdäkseni tason 1 asennusautomaation tekeminen on mahdollista vain, mikäli laitteisto on virtualisoitu. Esimerkiksi Amazon Web Services mahdollistaa virtuaalisen laitteiston luomisen komentoriviskripteillä. Laitteisto-käsitteeseen liitän tässä yhteydessä kaiken "raudan", kuten laskentakapasiteetin, tallennuskapasiteetin, verkkoyhteydet ja vaikkapa varmuuskopiointivälineet. Kun näiden luomisen automatisoi, tietää tarkasti ympäristön rakenteen ja tarvittaessa voi luoda helposti uusia vastaavia ympäristöjä. Laitteiston asennusautomaatiolla vältytään luomasta ympäristöjä, jotka ajan kanssa kasvavat tuntemattomiksi viidakoiksi.

Tasolla 2 sijaitsevat sekä käyttöjärjestelmä että käyttöjärjestelmässä ajettavat natiivisovellukset. Jos projektissa tehtävä sovellus on natiivisovellus, ei sitä silti lasketa tälle tasolle kuuluvaksi. Koska käyttöjärjestelmän ja natiivisovelluksien asentamisen automatisointi on yleensä vaikeata, eikä siitä välttämättä ole juurikaan hyötyä, voi tämän tason automatisoinnissa helposti oikaista tekemällä käsin asennetusta ympäristöstä levykuvan (disk image) ja käyttää sitä muussa automaatiossa. Mikäli projektissa tehtävälle sovellukselle tai sovelluksille riittää yhdenlainen käyttöympäristö, on manuaalisesti ylläpidettävä levykuva helpoin ratkaisu. Jos tarvitaan useita erilaisia käyttöympäristöjä, on syytä automatisoida myös käyttöjärjestelmän ja natiivisovelluksien asentaminen. Ainakin useimmissa Linux-pohjaisissa käyttöjärjestelmissä tämä on helppoa.

Tason 3 konfiguroinnilla tarkoitan sellaisia käyttöympäristöön liittyviä tehtäviä, jotka eivät liity suoranaisesti sovellukseen, mutta joiden pitää olla muutettavissa helposti kulloiseenkin tilanteeseen sopivasti. Yhtenä esimerkkinä voisi olla tietokannasta otettavat ajastetut varmuuskopiot. Periaatteessa ajastuksen voisi laittaa jo tasolla 2 suoraan vaikkapa levykuvaan, mutta tämä ei ole kovin joustavaa. On nimittäin mahdollista, että sovellus ottaa käyttöön esimerkiksi uuden tietokannan, joka pitäisi myös varmuuskopioda ja levykuvan muokkaaminen ja uudelleenasennus on työläs tehtävä näin pienen muutoksen takia. Tasolla 3 tehtävät muutokset elävät useammin sovelluksen kanssa, mutta ovat toisaalta siitä täysin irrallaan.

Tasolla 4 asennetaan ja konfiguroidaan projektin tuottama sovellus. Varsinkin testiympäristöissä sovellusta joudutaan asentamaan ja konfiguroimaan jatkuvasti, kenties jopa muutaman minuutin välein. Parhaimmillaan uusi sovellusversio voidaan asentaa jokaisen versionhallintaan tehtävän muutoksen jälkeen, jolloin uusimmat muutokset ovat jatkuvasti testattavissa oikeassa testiympäristössä.

Eri tasoilla tapahtuu muutoksia eri tahdilla. On selvää, että tason 1 muutokset ovat harvinaisimpia, koska laitteistoa ei yleensä ihan joka päivä muuteta. Tasolla 2 muutokset tapahtuvat harvoin, ehkäpä vain esimerkiksi käyttöjärjestelmän ja natiivisovellusten turvallisuuspäivitysten takia. Tasolla 3 voi olla jo päivittäisiä muutoksia, mutta muutoksia on varmasti paljon harvemmin kuin tasolla 4. Onhan projekteissa yleensä itse sovellus suurimpien muutosten kohteena.

Koska on paras hetki automatisoida?

Asennusautomaation tekeminen kannattaa aloittaa heti projektin alussa. Ensin voi asentaa vaikkapa vain tyhjän sovelluksen, jolla voi todentaa, että sovelluksen tarvitsemat ympäristön tarjoamat palvelut toimivat. Kun automaatiota tekee heti projektin alussa, tulee sovelluksestakin sellainen, että sen asennuksen voi automatisoida. On helppoa kirjoittaa vahingossa sellainen sovellus, jonka automaattinen asennus ja ylläpito ei ole kenenkään mielestä mukavaa.

Itse toimin asennusautomaation kanssa yleensä niin, että teen ensin jossain sopivassa ympäristössä käsin tarvittavat muutokset, joilla voin varmistua muutosten oikeellisuudesta. Kun tiedän tarkalleen mitä pitää tehdä, muokkaan automaattista sovellusasennusta vastaavasti. Näin vältän turhien asioiden tekemisen automaattisesti, vaikka riskinä onkin se, että unohdan automatisoida kaikki käsin tehdyt asiat.

Ikinä ei kannata jättää asennusautomaation tekemistä projektin loppupuolelle tai siihen hetkeen, kun "ei ole muutakaan tekemistä". Jos näin toimii, jää automaatio varmasti tekemättä ja käsin tehtävät asennukset maksavat ajallisesti nopeasti enemmän kuin automaation luominen ja ylläpito. Valitettavasti käsin asentamisen ja automaation kustannusten vertailu on niiden luonteen takia hankalaa. Projektijohto ei siten lyhytnäköisyyttään välttämättä ymmärrä antaa aikaa automaation rakentamiselle projektin alkuvaiheessa. Silloin kun on kiire tehdä muutakin ja alussa asennukset on nopeaa ja helppoa tehdä käsinkin, koska kaikki on vielä tuoreessa muistissa eikä sovellus ole kasvanut isoksi.

Automaation kustannukset

Automaattisen ja käsin tehtävän asennuksen kustannusvertailussa kannattaa ottaa huomioon seuraavat tekijät.
  • Käsin tehtävän asennuksen dokumentointi. Asennusta ei voi toistaa ilman laadukasta dokumentaatiota
  • Käsin tehtyjen asennusten huolimattomuusvirheiden korjaaminen. Ihminen tekee huolimattomuusvirheitä, kone ei
  • Käsin tehtyjen muutosten seurannan mahdottomuus. Useamman ihmisen porukassa kenelläkään ei ole tietoa mitä tarkkaan ottaen on tehty eikä yksinkään toimiva ihminen muista kaikkea mitä on tehnyt
  • Käsin tehtyjen asennusten henkilöriippuvuus. Asennuksen taitavan ihmisen pitää olla paikalla jokaisessa asennuksessa
  • Käsin tehtävien asennusten hitaus ja skaalausongelmat. Yksi ympäristö on helppo hallita, kaksi vaikeata ja kymmenen alkaa lähentelemään mahdottomuutta, vaikka asentajien määrää lisäisi
  • Käsin tehtävien asennusten vaatimien pääsyoikeuksien hallinta. Ketkä kaikki saavat päivittää eri ympäristöjä ja millä oikeuksilla. Automaation kautta voidaan sallia vähäisilläkin pääsyoikeuksilla kokonaisten ympäristöjen luominen ilman tietoturvaheikennyksiä
Asennusautomaation tekemisen käynnistäminen vaatii projektilta ison alkupanoksen. Ensin pitää valita käytettävä automatisointitapa. Tehdäänkö automaatio komentoriviskripteillä vai käytetäänkö jotain asennusautomaation erityisesti suunniteltua ohjelmistoa (http://en.wikipedia.org/wiki/Comparison_of_open_source_configuration_management_software).

Kun on tiedossa millä välineellä automaatiota lähdetään tekemään, pitää alkaa tutkimaan käytössä olevien ohjelmistojen konfiguroimista ilman sovelluksien omia GUI-työkaluja. Kaikki ohjelmistot eivät välttämättä tee automatisointia helpoksi. Yksi pahimmista näkemistäni ongelmista on se, että ohjelmistoja voi konfiguroida vain ja ainoastaan GUI-työkaluilla, jotka tuottavat binäärimuotoisia konfiguraatiotiedostoja. Näitä ohjelmistoja kannattaa välttää, mikäli mahdollista.

Automaation ylläpitäminen on myös työlästä, eikä jokainen kehittäjä välttämättä jaksa opetella miten automatisointi toimii. Tämä saattaa hidastaa projektin toimintaa, mikäli kaikkien kehittäjien on kuitenkin pakko opetella automaation salat tai mikäli kehittäjien pitää odotella erikoistuneita asennusautomaation tekijöitä.

Puhtaan pöydän lähestyminen

Asennusautomaation voi hoitaa kahdella tavalla, joko 1) automatisoimalla asennukset aina niin, että kaikki tehdään täysin puhtaalta pöydältä tai 2) niin, että asennuksissa jatketaan aina edellisen asennuksen tuottamasta tilasta.

Jos suinkin vain on mahdollista, kannattaa asennukset suorittaa aina puhtaalta pöydältä. Aina tätä vaihtoehtoa ei varmasti ole, mutta mikäli asennusautomaation ei tarvitse välittää edeltäneistä asennuksista, saadaan varmemmin täsmälleen haluttu lopputulos. Mikäli uusimman version asennus riippuu aina edellisen version asennuksesta, on migraatioskriptien luominen työlästä ja virheherkkää. Lisäksi kaikki sovellusversiot pitää asentaa tietyssä järjestyksessä kaikkiin ympäristöihin, koska migraatioskriptejä ei yleensä voi laatia toimimaan yhteensopivasti minkä tahansa lähtötilanteen kanssa. 

Jos puhtaan pöydän lähestymistapaan päätyy, on syytä varoa kahta asiaa.
  • Sovelluksen pitää toimia, vaikka sovelluksella ei ole käytössä aiemmin kerättyä dataa. Sovelluksessa voi olla esimerkiksi sisäisiä tarkistuksia, jotka olettavat, että sovelluksella on tietty määrä vanhaa kertynyttä dataa käytössä, mutta näinhän ei ole puhtaalta pöydältä lähdettäessä
  • Mikäli sovellus tallettaa aiemman käytön perusteella mitä tahansa tilatietoja, täytyy tila siirtää puhtaalta pöydältä asennettuun ympäristöön. Tällaista tilaa voi olla esimerkiksi lokitiedot.
Mitenkä asennukset sitten valitseekaan tehtäväksi, kannattaa varoa tallettamasta tilatietoja liian moneen paikkaan. Esimerkiksi erillisen tietokantapalvelimen ylläpito on paljon helpompaa kuin pyörittää tietokantaa ja sovellusta samalla koneella. Mikäli koko sovellus halutaan esimerkiksi asentaa puhtaaseen ympäristöön, mutta vanha data halutaan säilyttää, pitää tieto siirtää vanhasta ympäristöstä uuteen asennuksen yhteydessä. Mikäli käytetään erillistä tietokantapalvelinta, ei uuden version asennuksessa tarvitse tehdä mitään vanhan tiedon siirtämistä. Sama ajatus pätee pienempiinkin ympäristöihin, joissa ei pelata erillisillä koneilla: mieti aina minne tilan tallennat.

Automaation hyödyt

Mielestäni suurin hyöty automatisoidusta asennuksesta on työn mielekkyyden ylläpitäminen. Harvaa kiinnostaa saman rutiiniasian huolellinen toistaminen päivästä toiseen. Tästä seuraa asennuksen muut hyvät puolet.
  • Koska rutiinin toistamiselta ihmisivoimin vältytään, asennuksien laatu paranee.
  • Automatisoitu asennustapa on aina nopein, koska hidasta ihmistä ei tarvitse painelemaan nappeja.
  • Asennuksien dokumentaatio on aina ajan tasalla, koska kenenkään ei tarvitse päivittää ihmisen seurattavaa dokumentaatiota: asennusskriptit ovat asennukselle sama asia kuin ohjelmakoodi sovellukselle!
  • Projektin riippuvuus yksittäisistä ihmisistä vähenee, koska uuden version asennus ei vaadi asentajien paikalla oloa
  • Virheet korjataan täsmälleen kerran asennusautomaatioon eikä virheitä ja niiden ratkaisuita tarvitse muistaa joka asennuksen yhteydessä
  • Osaamisen siirto on helppoa, koska osaamisen siirrossa riittää pitkälti se, että selittää miten asennusautomaatio toimii. Yksityiskohdat näkee skripteistä

Automaation ongelmat

Automaation näkyvin ongelma lienee se, että jostain pitää projektin alkupuolella löytää aika automaation toteuttamiseen ja sen jälkeen ylläpitoon. Väitän, että aikaa ei kokonaisuudessaan mene enempää kuin työn tekemiseen käsin joka kerta, mutta tämä on vain näppituntumatietoutta. 

Automatisoitujen asioiden muuttaminen projektin paniikkitilanteissa, kuten järjestelmän tai tuotantoasennuksen yhtäkkisen sekoamisen yhteydessä on aivan liian hidasta. Pelastuskeinona paniikkitilanteissa voi käyttää asioiden selvittämistä käsityönä ja automatisoimista myöhemmin. 

Automatisoinnissa on riskinä, että projektin alussa valitaan väärä automaatiotapa ja toteutus menee hukkaan. Mikäli automatisointitoteutus joudutaan vaihtamaan kesken projektin, on se kuitenkin helpompaa kuin automaation tuominen kokonaan puhtaalta pöydältä aiemmin käsin asennettuun sovellukseen. Näin siksi, että vanha, vaikkakin huono, automaatio sisältää kaiken tarvittavan tiedon uudelle toteutukselle.

Täydellisesti automatisoitu asennus voi mennä täydellisesti metsään, jos toteutus ei tarkista asennuksen onnistumista sen edetessä. Hyvä virheen käsittely toki auttaa useimmiten. Vanha totuus on kuitenkin se, että mitenkään ei voi päästä niin suuriin ongelmiin kuin automaattisesti etenemällä (vrt. autonavigaattori vs. paperikartta). Asennuksen pitää siis kertoa kohtaamistaan ongelmista heti ja jäädä odottamaan ylläpitäjän korjaustoimenpiteitä.

Automaation sopivuus erilaisiin projekteihin

Uskon, että asennusautomaatio maksaa itsensä takaisin projektissa kuin projektissa. Ainoa mieleen tuleva poikkeus voisi olla joku start-up-projekti, missä kerta kaikkiaan kaikki resurssit on pakko laittaa yhden ainoan sovelluksen mahdollisimman pikaiseen tuottamiseen. Itse en ole tällaista projektia ikinä kokenut, joten mahdoton sanoa onko näin. Projektin asennusautomaatio kannattaa hoitaa kuntoon, niin pääsee keskittymään projektin oikeisiin ongelmiin!

tiistai 3. huhtikuuta 2012

Using Apache as reverse proxy through HTTP and HTTPS


HTTP reverse proxying


The ultimate goal is to reverse proxy SSL secured web site over Apache installed on Ubuntu server. This means that we are using Apache to serve content from a remote web site in a way that browser thinks its getting the data from our Apache and the remote web site thinks our Apache is a browser accessing the site data.

Let's start with reverse proxying without SSL. These instructions work on a fresh Ubuntu 10.04 installation (I'm using an image from Amazon Web Services). First install Apache.
$ sudo apt-get install apache2

Install mod_proxy_html on Apache.
$ sudo apt-get install libapache2-mod-proxy-html

It seems that this command also enables the mod_proxy_html automatically:
$ ls /etc/apache2/mods-enabled/proxy_html.*
/etc/apache2/mods-enabled/proxy_html.conf  /etc/apache2/mods-enabled/proxy_html.load

Enable the modules needed by proxying.
$ sudo a2enmod proxy_http
$ sudo a2enmod headers

Disable default site that comes with Apache installation.
sudo a2dissite 000-default

Create reverse proxy configuration. Add the following to file /etc/apache2/sites-available/reverseproxy
<VirtualHost *:80>
  ServerAdmin webmaster@localhost

  ErrorLog /var/log/apache2/reverseproxy_error.log

  # Possible values include: debug, info, notice, warn, error, crit,
  # alert, emerg.
  LogLevel info

  CustomLog /var/log/apache2/access.log combined

  # We're not an open proxy
  ProxyRequests off

  # Proxying is available for anyone
  <Proxy *>
    Order deny,allow
    Allow from all
  </Proxy>

  # The site we're proxying through http://oursite.fi/proxytest/
  ProxyPass /proxytest/ http://www.iltalehti.fi/
  ProxyPassReverse /proxytest/ http://www.iltalehti.fi/

  # Use mod_proxy_html to rewrite URLs
  SetOutputFilter proxy-html
  ProxyHTMLURLMap http://www.iltalehti.fi /proxytest
  ProxyHTMLURLMap  /      /proxytest/

  # Disable compressed communication between Apache and target server
  RequestHeader    unset  Accept-Encoding
</VirtualHost>

Enable our reverse proxy site and restart Apache
$ sudo a2ensite reverseproxy
$ sudo service apache2 restart
Now you should be able to see Iltalehti (http://www.iltalehti.fi) through your site under /proxytest.

Securing proxied connection with SSL (HTTPS reverse proxying)


Create self signed certificates. These commands are explained on page https://help.ubuntu.com/10.04/serverguide/C/certificates-and-security.html
$ openssl genrsa -des3 -out server.key 1024
$ openssl rsa -in server.key -out server.key.insecure
$ mv server.key server.key.secure
$ mv server.key.insecure server.key
$ openssl req -new -key server.key -out server.csr
$ openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
$ sudo cp server.crt /etc/ssl/certs
$ sudo cp server.key /etc/ssl/private

Disable plain HTTP based reverse proxy.
$ sudo a2dissite reverseproxy

Add the following to file /etc/apache2/sites-available/reverseproxy-ssl.
<VirtualHost *:443>

  ServerAdmin webmaster@localhost

  ErrorLog /var/log/apache2/reverseproxy-ssl_error.log

  # Possible values include: debug, info, notice, warn, error, crit,
  # alert, emerg.
  LogLevel info

  CustomLog /var/log/apache2/access-ssl.log combined

  # We're not an open proxy
  ProxyRequests off

  # Proxying is available for anyone
  <Proxy *>
    Order deny,allow
    Allow from all
  </Proxy>

  # The site we're proxying through http://oursite.fi/proxytest/
  ProxyPass /proxytest/ https://www.veikkaus.fi/
  ProxyPassReverse /proxytest/ https://www.veikkaus.fi/

  # Use mod_proxy_html to rewrite URLs
  SetOutputFilter proxy-html
  ProxyHTMLURLMap https://www.veikkaus.fi:443 /proxytest
  ProxyHTMLURLMap https://www.veikkaus.fi /proxytest
  ProxyHTMLURLMap  /      /proxytest/

  # Disable compressed communication between Apache and target server
  RequestHeader    unset  Accept-Encoding

  #   SSL Engine Switch:
  #   Enable/Disable SSL for this virtual host.
  SSLEngine on

  # Allows the proxying of an SSL connection
  SSLProxyEngine On

  # A self-signed certificate
  SSLCertificateFile    /etc/ssl/certs/server.crt
  SSLCertificateKeyFile /etc/ssl/private/server.key
</VirtualHost>

Enable HTTPS based reverse proxy.
$ sudo a2enmod ssl
$ sudo a2ensite reverseproxy-ssl
$ sudo service apache2 restart

Now you should be able to see Veikkaus (https://www.veikkaus.fi) through your site under path /proxytest.