Category Archives: Uncategorized
Learning Rust: Look Ma, No Null!

Introduction

Rust is a systems programming language (think C-like) that makes it easier to perform memory-safe operations than languages like C or C++. It accomplishes this by making it harder to do memory-unsafe operations–and catching these sorts of issues at compile-time instead of runtime.

In order to accomplish this, Rust imposes some constraints on the engineer through the borrow checker and immutable-by-default types. I’m not going to write about those things here as they have been covered in depth by others.

My focus for this post (and other posts in this potential series) is to focus on other language features and idioms that may be unfamiliar to managed-language developers.

No Null

Rust does not have a the concept of null. Consider the following declaration of a struct:

let employee = Employee {
    first_name: "Chris",
    last_name: "McKenzie",
};

In this code sample, an Employee struct is immediately created in memory. There is no option to create a variable of type Employee and leave it unassigned. This might not seem terrible on its face, but what about data we don’t have? What if we add a birth date field to the struct but we don’t know what it is?

struct Employee {
    first_name: &str,
    last_name: &str,
    birth_date: &str,
}

let employee = Employee {
    first_name: "Chris",
    last_name: "McKenzie",
    birth_date: // not sure what to do here?
}

Option

For this scenario, Rust has a built-in enum called Option<T>. An option has two variants: Some and None which are automatically imported for you. To represent the above code, you would write this:

#[derive(Debug)]
struct Employee {
    first_name: String,
    last_name: String,
    birth_date: Option<String>
}

pub fn main() {
    let employee = Employee {
        first_name: "Chris".to_string(),
        last_name: "McKenzie".to_string(),
        birth_date: Option::None
    };

    println!("{:?}", employee);
}

or

#[derive(Debug)]
struct Employee {
    first_name: String,
    last_name: String,
    birth_date: Option<String>
}

pub fn main() {
    let employee = Employee {
        first_name: "Chris".to_string(),
        last_name: "McKenzie".to_string(),
        birth_date: Option::Some(<value>)
    };

    println!("{:?}", employee);
}

Since rust imports std::Option for you automatically, the above can be rewritten without the Option:: prefix:

birth_date: None
...
birth_date: Some(<value>)

Am I Cheating?

Wait! Isn’t “None” just null under the covers?

Well–no. To understand the difference, you first need to understand a few things about enums.

The members of an enum are called variants. Variants can carry data, and variants must be matched.

Let’s examine what this means in practice for a moment.

Building an Enum

Consider the requirement that we represent both salaried and non-salaried employee types. The annual salary must be stored for salaried employees. The hourly rate, overtime rate, and normal hours must be stored for the hourly employee.

In managed languages such as C# you might represent that as follows:

public enum EmployeeType {
    Salaried,
    Hourly,
}

public class Employee {
    public string FirstName {get; set;}
    public string LastName {get; set;}
    public DateTime? BirthDate {get; set;}
    public EmployeeType Type {get; set;}

    // should only be stored for Salaried employees
    public double AnnualSalary {get; set;} 

    // should only be stored for Hourly employees
    public double HourlyRate {get; set;} 
    // should only be stored for Hourly employees
    public double OvertimeRate {get; set;} 
    // should only be stored for Hourly employees
    public int NormalHours {get; set;} 
}

This structure is fine but has the drawback that it’s possible to populate the class with data that’s invalid–e.g., setting the HourlyRate for a Salaried employee is a violation of the business rules–but nothing in the structure of the code prevents it. It’s also possible to fail to provide any of the needed information.

Obviously, you can enforce these rules through additional code, but again–nothing in the structure of the code prevents it–or even communicates the requirements.

I could do exactly the same thing in Rust of course and it doesn’t look much different:

#[derive(Debug)]
pub enum EmployeeType {
    Salaried,
    Hourly,
}

#[derive(Debug)]
pub struct Employee {
    pub first_name: String,
    pub last_name: String,
    pub birth_date: Option<String>,
    pub employee_type: EmployeeType,

    // should only be stored for Salaried employees
    pub annual_salary: Option<f32>,

    // should only be stored for Hourly employees
    pub hourly_rate: Option<f32>, 
    // should only be stored for Hourly employees
    pub overtime_rate: Option<f32>, 
    // should only be stored for Hourly employees
    pub normal_hours: Option<u8>, 
}

pub fn main() {
    let employee = Employee {
        first_name: "Chris".to_string(),
        last_name: "McKenzie".to_string(),
        birth_date: Option::None,
        employee_type: EmployeeType::Salaried,
        annual_salary: None,
        hourly_rate: None,
        overtime_rate: None,
        normal_hours: None,
    };

    println!("{:?}", employee);
}

Ideally, we’d want to bind the annual salary information to the Salaried employee type, and the hourly information to the Hourly employee type. It turns out–we can!

#[derive(Debug)]
pub enum EmployeeType {
    Salaried { annual_salary: f32 },
    Hourly { rate: f32, overtime_rate: f32, normal_hours: u8 } ,
}

#[derive(Debug)]
pub struct Employee {
    pub first_name: String,
    pub last_name: String,
    pub birth_date: Option<String>,
    pub employee_type: EmployeeType,
}

pub fn main() {
    let salaried = Employee {
        first_name: "Chris".to_string(),
        last_name: "McKenzie".to_string(),
        birth_date: Option::None,
        employee_type: EmployeeType::Salaried {
            // one, MILLION DOLLARS! Mwa ha ha ha ha ha ha! (I wish!)
            annual_salary: 1_000_000.00
        },
    };

    println!("{:?}", salaried);

    let hourly = Employee {
        first_name: "Fred".to_string(),
        last_name: "Bob".to_string(),
        birth_date: Some("1/1/1970".to_string()),
        employee_type: EmployeeType::Hourly {
            rate: 20.00, 
            overtime_rate: 30.00, 
            normal_hours: 40
        }
    };

    println!("{:?}", hourly);
}

Suddenly things get very interesting because we can express through the structure of the language what fields are required for which enum variant. Since all fields of a struct must be filled out when it is created, the compiler is able to force us to provide the correct data for the correct variant.

Nice!

Reading our enum variant

The match statement in Rust roughly corresponds to switch or if-else in C# or Java. However, it becomes much more powerful in the context of enum variants that carry data.

#[derive(Debug)]
pub enum EmployeeType {
    Salaried { annual_salary: f32 },
    Hourly { rate: f32, overtime_rate: f32, normal_hours: u8 } ,
}

#[derive(Debug)]
pub struct Employee {
    pub first_name: String,
    pub last_name: String,
    pub birth_date: Option<String>,
    pub employee_type: EmployeeType,
}

pub fn main() {
    let salaried = Employee {
        first_name: "Chris".to_string(),
        last_name: "McKenzie".to_string(),
        birth_date: Option::None,
        employee_type: EmployeeType::Salaried {
            // one, MILLION DOLLARS! Mwa ha ha ha ha ha ha! (I wish!)
            annual_salary: 1_000_000.00
        },
    };


    let hourly = Employee {
        first_name: "Fred".to_string(),
        last_name: "Bob".to_string(),
        birth_date: Some("1/1/1970".to_string()),
        employee_type: EmployeeType::Hourly {
            rate: 20.00, 
            overtime_rate: 30.00, 
            normal_hours: 40
        }
    };

    let employees = vec![salaried, hourly];

    for item in employees {
        // match on employee type
        match item.employee_type {
            EmployeeType::Salaried {annual_salary} => {
                println!("Salaried! {} {}: {}", item.first_name, item.last_name, annual_salary);
            },
            EmployeeType::Hourly {rate, overtime_rate: _, normal_hours: _ } => {
                println!("Salaried! {} {}: {}", item.first_name, item.last_name, rate);
            }
        }

    }

}

I need to point out a couple of things about the match statement.

  1. Each variant must fully describe the data in the variant. I can’t simply match on EmployeeType::Salaried without bringing the rest of the data along.
  2. match requires that every variant of the enum be handled. It is possible to use “_" as a default handling block, but ordinarily you would just handle all of the variants.

Back to Option

The language definition for Option is as follows:

pub enum Option<T> {
    /// No value
    None,
    /// Some value `T`
    Some(T),
}

None is a variant with no data. Some is a variant carrying a tuple with a single value of type T.

Anytime we match on an Option for processing, we are required to handle both cases. None is not the same as null. There will be no “Object reference not set to instance of object.” errors in Rust.

You can play with this code and run it in the playground.

Can This Idea Be Useful Elsewhere?

I’ve you’ve not spent a decent amount of time tracking down null reference exceptions in managed languages, this may not be that interesting to you. On the other hand, the Optional value concept can help drive semantic clarity in your code even if you’re not working in Rust. I’m not arguing that you should stop using null in C# & Java. I am arguing that you should add the concept of Optional values to your toolbox.

The Optional library for C# provides an implementation of this time. It’s worth reviewing the README. Java introduced the Optional type in version 8.

NBuilder 6.0.1 Released

RELEASE NOTES 6.0.1

This is a bug fix release.

  • Bug Guid.Empty being incremented. Solution was to disable NBuilder’s property name for static or read-only fields. Thanks to Dominic Hemken for the PR.
  • Bug CreateListOfSize had undefined behavior when called from the static Builder and executed on multiple threads at the same time. While the future of NBulider will be to remove the static builder, it’s a defect in the current implementation. Thanks to Ovidiu Rădoi for the PR.
Testable Component Design in Rust

I consider myself an advanced beginner in Rust. There is still much I’m wrapping my head around–and I still get caught off guard by the “move” and “mutability” rules Rust enforces. However, in keeping with my personal emphasis, I’ve devoted my efforts to learning how to create automated tests in Rust. The below guidelines are not exhaustive, but represent my learning so far. Feedback is welcome!

Engineering Values

  • Code should be clean.
  • Code should be covered by automated tests.
    • Tests should be relatively easy to write.
  • Dependencies should be configurable by the components that use them (see Depedency Inversion Principle and Ports & Adapters)

Achieving These Values in Rust Component Design

These are great engineering values, but how do we achieve them practically in Rust? Here are my thoughts so far.

Required for Unit Testing

  • The component should provide a stable contract composed of traits, structs, and enums.
  • Structs exposed in the contract layer should be easy to construct in a test.
  • All types exposed in the contract layer should implement derive(Clone, Debug) so that they can be easily mocked in tests.
    • This means that types like failure::Error should be converted to something that is cloneable.

Required for Configurable Dependencies

  • The contract layer should not reference any technology or framework unless it is specifically an extension for that technology or framework.

Empathy

  • Every effort should be made to make the public api surface of your component as easy to use and understand as possible.
  • The contract layer should minimize the use of generics.
    • Obvious exceptions are Result<T> and Option<T>.
    • Concepts like PagedResult<T> that are ubiquitous can also be excepted.
    • Using type aliases to hide the generics does not qualify since the generic constraits still have to be understood and honored in a test.
    • In general this advice amounts to “generics are nice, but harder to understand than flat types. Use with care in public facing contracts.”
  • If a trait exposes a Future as a return result, it should offer a synchronous version of the same operation. This allows the client to opt-in to futures if they need them and ignore that complexity if they don’t.
    • I understand that the client can add the .wait() call to the end of a Future. My point is that an “opt-in” model is friendlier than an “opt-out” model.

Example Hypothetical Contract Surface

#[derive(Clone, Debug)]
struct Employee {
    id: String,
    type: String,
    status: String,
    first_name: String,
    last_name: String,
    address: String,
    city: String,
    birth_date: UTC,
    // snipped for brevity
}

struct PagedResponse<T> { // exposes a generic, but the reason is warranted.
    page_number: i32,
    page_size: i32,
    items: Vec<T>
}


#[derive(Debug, Clone)]
enum MyComponentError {
    Error1(String), // If the context parameter is another struct, it must also derive Clone & Debug
    Error2(i32),
};

#[derive(Clone, Debug)]
struct EmployeesQuery {
    r#type: Option<String>,
    name: String, 
    types: Vec<String>, // matches any of the specified types,
    cities: Vec<String>, // matches any of the specified cities
}

type Result<T> : Result<T, MyComponentError>; // Component level Result. Type aliasing expected here.

trait EmployeeService {
    type Employees = PagedResponse<Employee>;

    // sync version of async_get()
    fn get(id: String) -> Result<Employee>{
        async_get(id).wait();
    }

    fn async_get(id: String) -> Future<Item = Employee, Error = MyCompomentError>;

    // sync version of async_query()
    fn query(query: Option<EmployeesQuery>) -> Employees {
        async_query(id).wait()
    }

    fn async_query(query: Option<EmployeesQuery>) -> Future<Item = Transactions, Error = MyCompomentError>;

    // etc...
}
Non-Technical Engineering Quality Indicators

A PM I work with asked me the following question:
“How can someone who is not close to the engineering read the tea-leaves about the engineering quality of a given project.”

I love this question because it shows the PM cares about engineering quality despite not having engineering expertise. I’m much more accustomed to having to argue that non-technical folks should care about engineering quality. Now I have someone who does care and wants help knowing what to look for. How do I help this person? I have some starting ideas. Some of my favorite engineering manager metrics are easily adaptable for non-technical PM’s.

  1. Does the team rely on manual testing? In my opinion manual testing adds little value. It’s impossible to do a complete regression. There is no future benefit to the effort beyond the immediate release. It’s slow. If you want to remove waste from the delivery pipeline, invest in test automation.
  2. What is the defect/user story ratio in the backlog?
  3. How often are new defects discovered and added to the backlog?
  4. How does the team react to the idea of asking for a near-zero cycle time for defects? To achieve this the team would need:
    a. To have relatively few defects
    b. To receive new defects on an infrequent basis
    c. To have confidence that correcting any defect would take hours instead of days
    e. To have deep knowledge of a well-engineered system so that the exact nature of the problem can be identified quickly
    f. To have confidence that they can pass the system through their quality gates and get into production in less than an hour

The idea that you can have zero defects is sometimes shocking for both PM’s and engineers to consider. It can be an uphill battle to convince them that this is achievable in reality. If your team can’t accept this as a reality, see if they can accept is as a goal and move in this direction.

  1. Is Lead Time increasing? That could indicate that the team can not respond to work as quickly as it arrives. You will also need to know what Lead Time you want. If you want to plan your roadmap in three-month increments, your Lead Time should be between 45 and 180 days–depending on how often you update your roadmap.
  2. Is Cycle Time increasing? If so, it probably means the system is difficult to work in.
  3. Is Cycle Time close to Lead Time? If so, it could indicate a lack of planning, or an inability to work on the plan due to emergent issues.
  4. How long does it take to correct a defect in production? Ideally, this would be ~1hr from discovery.
  5. How much do we spend on support for this service through all channels? (E.g., service desk, engineering time, etc.)

This list is not exhaustive and none of these measurements would be conclusive on their own. As diagnostics however they could be quite useful to draw your attention to potential problems. Most would have to be tracked over a longer period of time to be meaningful.

If you’ve got ideas of your own, please leave them in the comments. This is an important piece of the communication between product management and engineering.

NBuilder 6.0.0 Released

Thank you to the contributors who submitted pull requests for the issues that were important to them. A summary of the changes for NBuilder 6 are as follows:

  • Breaking Change: WithConstructor
    • No longer takes an Expression<Func<T>>.
    • Takes a Func<T>.
    • Marked [Obsolete] in favor of WithFactory
    • This change was to address an issue in which the constructor expression was not being reevaluated for each item in a list.
  • Feature: @AdemCatamak Added support for IndexOf as part of the ListBuilder implementation.
var products = new Builder()
    .CreateListOfSize<Product>(10)
    .IndexOf(0, 2, 5)
    .With(x => x.Title = "A special title")
    .Build();
  • Feature: @PureKrome Added support for DateTimeKind to RandomGenerator
var result = randomGenerator.Next(DateTime.MinValue, DateTime.MaxValue, DateTimeKind.Utc);
  • Feature: Added DisablePropertyNamingFor(PropertyInfo) overload to BuilderSettings.
  • Feature: Added TheRest as an extension to the ListBuilder.
var results = new Builder()
        .CreateListOfSize<SimpleClass>(10)
        .TheFirst(2)
        .Do(row => row.String1 = "One")
        .TheRest()
        .Do(row => row.String1 = "Ten")
        .Build()
    ;
  • Bug: Last item in enum is never generated when generating property values randomly.
  • Bug: Lost strong name when porting to .NET Standard.
  • Bug: Non-deterministic behavior when calling TheLast multiple times for the same range.
A New Chapter at Microsoft

I am leaving Redacted Financial Services* in November to manage an IT team at Microsoft. I am changing the focus of my career from the day-to-day tech toward management–strategy over tactics. I’ll be bringing what I know about software engineering into the IT space as well as learning an entirely new set of disciplines.

I’ve worked at Redacted for 6 years. In that time I’ve enjoyed working with a motivated, dedicated group of Software Craftsmen. Other than getting my start writing software, it’s the best time of my professional life. I grew professionally in that time in no small part due to a manager who made room for me to explore my interests and found ways to capitalize on them for the benefit of the company. It is my goal to match his example.

One of the things I accomplished there was founding an internship program which became a feeder program into our development organization for up-and-coming developers. It had the unintended side-effect of creating a mechanism people within the company who had shown an interest in writing software could use to explore a career-change. I’ve worked with close to 30 interns. Some have stayed and worked with us. Others have gone on to companies like Visa, Google, Nordstrom, and Tableau. I’m proud to have played a part in their career development.

I took control of the hiring process for interns which expanded to include running the hiring process for our entire development organization. I learned that the largest impact I could have on my organization is through who I choose to hire. My wife works as an agency recruiter for accounting and finance professionals and with her help I learned how to work with agency recruiters to find the candidates I needed quickly. Hiring is hard and people are seldom properly trained how to do it. The end-result was that we spent less time sorting through resumes and interviewing dud-candidates. Instead, nearly every candidate we talked to was brought on-site. For the most part we were able to hire quickly with only a few cycles through the process.

A couple of years ago our DevOps initiative was going sideways. Known to be a passionate advocate for Software Craftsmanship, I was asked to ride-along with the DevOps group and make recommendations that would get us back on track. I ended up leading that group for the last year and a half. The improvements we made include tracking work in one place, identifying and eradicating root causes of common problems, clearly identifying our customers, identifying standard practices for common work, establishing a customer-centric mindset for the team, and practicing what we preach with respect to quality software. It’s a DevOps team, but we write tests for our scripts and services. In that time the stability and reliability of our production deployments increased dramatically.

In addition to being a technical leader on my team, I began managing other people. I always thought of this responsibility in servant-leadership terms. My role was to collaborate with the employee to make sure s/he is feeling challenged and growing. I learned to be free with my praise and politely direct with my critical feedback. I learned never to give critical feedback without also giving concrete examples of different behavior. I was able to coach my reports through some challenging scenarios and save them the effort of learning everything the hard way.

While I’m the one who did the work to learn these things, I was enabled by a phenomenal manager who gave me room to grow and challenge myself. He listened to my interests and made room for me to explore them–ever confident that it would pay off for the team. It did.

I was also challenged by a group of quality-focused engineers who accepted my ideas when they thought they were good, and who had the courage to speak up when they thought I was off the deep end. Some of my favorite people are my worst critics–and good friends.

Finally, I was aided by a wonderful wife with the highest emotional intelligence of any person I’ve ever encountered. I learned from her how critically important successful communication is and endeavored to apply that learning to my career. I’ve learned that I need to adapt my communication style to my audience–although putting that into practice is still a challenge!

I feel a swell of pride for having these people in my life and at the work we’ve accomplished together. To all of these people I feel a great debt of gratitude.

Thank you All.

 

* One of the interesting “perks” of working for a finance company is that some of them don’t want you to name your employer on social media. The rationale is that if you were to broadcast a stock purchase or otherwise comment on the markets it may be construed by someone else as Financial Advice which would in turn make the company potentially liable for the quality of that advice.
Stay Focused on the Goal, Not the Metrics

The goal is the thing you are trying to do.
The metric is how you are measuring your progress toward the thing you are trying to do. Metrics are only as good as their ability to measure progress toward the goal.

Don’t confuse them.

An Example

Imagine a sales team for an organization selling widgets has a goal to increase sales of a particular product line by 10%. The Sales Manager decides that the best way to achieve the goal is for the sales staff to make a certain number of cold-calls over the following months. After a few weeks, one of her sales staff is falling way behind in cold-calls. The wrinkle is that this salesman is the top biller in the department. Should the sales manager berate her top performer for not doing enough cold-calls?

Absolutely not. No manager should ever punish their reports for doing well (provided the means used are legal and ethical of course).

Punishing the salesman would send the message that billing isn’t the goal, but cold-calls are. Do you want a salesman who makes lots of cold-calls but can’t bill? Since sales staff are compensated via commission, punishing the salesman would introduce a division between his performance and his pay. In the best case, the salesman simply ignores the manager and continues to bill and get paid–benefiting the company in the process. In the worst case, the salesman leaves the company for greener pastures, depriving the company of it’s top biller.

The mistake here is that cold-calls are simply a form of measuring progress toward the goal–increased sales. Cold calls themselves are not the actual goal–they are a proxy for the goal. Further, they may not be the only possible proxy. Their value as a proxy is proportional to the relationship between cold-calls and increased sales. If a salesman is generating increased sales without cold-calls then there is either another possible metric or cold-calls are a poor metric.

It’s one thing to say that cold-calls are a proven way to generate increased sales. It’s quite another to ignore that there are other possible ways to do the same thing. It’s flat wrong to take the position that cold-calls are the only way to increase sales.

What is the appropriate response? Find out how the top biller is selling so well without cold-calls. Is the top biller doing something that no one else is doing? Is there something for the other sales staff to learn? Are there new, better metrics that can be introduced? Of course, it’s also possible that the top biller could bill even more if he did more cold-calls. Finding out will require collaboration between the salesman and the manager–but this is a process of active investigation instead of passive authoritarianism.

If the manager focuses on the metric instead of the goal, she is taking on the responsibility of having all the answers and dictating them to others. The proper approach is to adopt a learning stance toward the team’s work. If the team is doing well but the metrics aren’t being met, what can the manager learn from this? If the team meets the metrics, will they do better? If not–what good are they?

Choosing Metrics

When choosing metrics it’s important to consider that people will game the system. If you’re a software engineering manager and you make Lines of Code or Test Coverage the metric, people will write verbose code and create meaningless tests. A good metric will encourage people to game the system by focusing on the thing you want to achieve. I recently heard of an example in which the Product Owner threw out story sizing as part of their Scrum process. The only thing developers got credit for was the number of stories they completed. It didn’t matter how large or small–credit was only given when the story was completed an in production. The developers began gaming the system by reducing the story size to the smallest thing they could deliver.

Perfect.

NBuilder 5.0.0 Released: Now the .NET Standard 1.6 Support

NBuilder 5.0.0 is now available on NuGet.org.

Breaking Changes

We have dropped support for .NET 3.5. It is becoming cumbersome to support such an old framework in the build chain. We now support .NET 4.0 and above.

Exciting New Features

NBuilder is now available to .NET Core 1.1 applications via .NET Standard 1.6. This was an enormous amount of work made possible in a large part by the efforts of a contributor PureKrome. Thanks PureKrome!

NBuilder 4.0.0 Released

It’s been 5 years since a version of NBuilder was released. As happens to many of us, the original author got busy with life and was unable to spend the time brining it up to date. I volunteered to shepherd the project along, but I was also waylaid by life. However, I was recently able to spend some focused time fixing the final bugs and getting together an automated build with AppVeyor.

Given that it’s been 5 years, it’s impossible to know fully what has been changed. I did put together release notes for the things I know were changed.

My next task will be to port NBuilder to .NET Core

Release Notes for NBuilder 4.0.0

Breaking changes

1. Obsolete methods have been removed.

Any method previously marked with the Obsolete attribute has now been removed.

2. Silverlight No Longer Supported

As Silverlight is effective a dead technology, we have officially ended support for it. This will allow us to better focus on
a forthcoming release with .NET Core support.

New Features

1. Builder has a non-static implementation.

This will allow you to create customized BuilderSettings for different testing scenarios.

Old Code


var results = Builder<MyObject>.CreateListOfSize(10).Build();

New Code


var settings = new BuilderSettings() var results = new Builder(settings).CreateListOfSize<MyObject>(10).Build();

2. With and Do action now supports a signature that receives an index.

Example


var builderSettings = new BuilderSettings(); var list = new Builder(builderSettings) .CreateListOfSize<MyClassWithConstructor>(10) .All() .Do((row, index) => row.Int = index*2) .WithConstructor(() => new MyClassWithConstructor(1, 2f)) .Build();

Bug Fixes

  • The decimal separator was wrong for some cultures.
  • Random number generation of decimals was sometimes incorrect.
  • Sequences were not created in the correct order.
  • Random strings were not always generated between the expected lengths.
10 Things I Wish I Had Known Before I Switched to DevOps

1. DevOps is hard

It might not seem like it, but DevOps is hard. A few years ago I thought to myself that it can’t be that difficult since installing an individual application isn’t that difficult. I was wrong in part because…

2. Security is hard

Production is scary. I’d rather not have access when possible. On the other hand the tools that we use will definitely need access to production since it’s kind of the reason they exist. This means we have to have very tight control over who has access to the credentials that the tools run under. We work to limit our own day-to-day accounts so that their access is limited as well.

As a developer I didn’t think much about Security. I pretty much just stuffed an AD Group in a config file somewhere when I was told to and I was done. As a DevOps engineer I have had (and will continue) to learn a lot more about security and its organization even though I don’t manage security for my organization. Security impacts deployments at every level so you will have to learn about security infrastructure in order to make safe and practical recommendations to your security administration group.

3. You are not Netflix (unless you are)

Our organization got excited about DevOps tools after seeing some compelling presentations by Netflix at QCon San Francisco. Netflix has the need for highly scaled web servers which fully embrace the “cattle vs. pets” philosophy because they have millions of concurrent users of a publicly facing service.

We are not Netflix. We have 50+ internal applications with usage rates measured in the 10’s. They’re important to us–they run our business–but our problems are not the same ones Netflix faces. The tools that Netflix uses are designed to solve problems Netflix has. That doesn’t necessarily make them a good fit for our needs. We lost a lot of time and effort trying to make Netflix solutions fit our problems.

4. Windows vs. Linux matters when choosing your tools.

There are basically 5 possibilities when it comes to your server topology:

  1. Windows Only
  2. Linux Only
  3. Windows Dominant
  4. Linux Dominant
  5. Hetergeneous

If you are managing a homogeneous ecosystem then it’s imperative that you use tools that natively support that system. Don’t try to use Linux tools to manage Windows and vice versa. If you do, you’re gonna have a bad time. If you are primarily deploying to Windows you should look at tools like Octopus Deploy or Build Master. If you’re managing a Linux ecosystem look into Chef, Puppet, or even Docker.

If you’re managing a mixed ecosystem where one OS was dominant, you should still use tools designed to support the dominant system. It may be worth the effort to see if your existing tools can also manage the subdominant system. In our case it’s not worth the effort so we have instead moved toward an “appliance” model for our Linux servers. What this means is instead of managing a bunch of code to deploy RabbitMQ to Linux, we’re instead creating VM Images for the Rabbit installation which we can hydrate at will. We have far fewer resources who know how to administer Linux so this model works better for us.

5. DevOps tools are in their infancy

DevOps tools are optimized for the problems their creators were facing. There are many more problems in the DevOps space than any of the dominant tools are capable of managing on their own.

For example, Chef wants to deploy a machine. It’s not primarily concerned about applications. The Chef model is to declare the state of the machine and then let Chef decide how to bring the machine to that state. This approach optimizes for horizontally scaling hundreds or thousands of identical nodes with very few commands. Awesome!

In our organization we see the world in terms of Applications–not machines. Our whole way of thinking about deployment is different than the way Chef looks at it. This isn’t a deficiency in Chef or in the way we look at the world, but when we started using Chef we weren’t aware of how fundamental that difference in perspective would actually be.

Because Chef looks at the world in terms of nodes, it has no built-in (or even recommended) solution for artifact and version management. We had to build that. We had to build solutions for managing cookbook versions, publishing artifact and cookbook versions into targeted environments, and forwarding changes to production to antecedent environments.

If you’re using Octopus (we’re migrating from Chef to Octopus) and looking at the world in terms of applications, you will have problems when you need to spin up new environments and whole machines with many applications pre-installed. Either way, you will have to build other tools to glue the off-the-shelf tools together.

(Aside: Though I am not personally a fan of Chef, I have heard of people using Chef to deploy their infrastructure and using Octopus to deploy applications.)

6. DevOps “best practices” are in their infancy

Chef likes to advertise “use Chef however you want! We’re flexible!” Great…. except Chef is complicated and I would like some guidance on how to use it! This isn’t so much a problem with Chef though–DevOps in general is a very young field so we don’t have the wealth of shared experience from which to draw generalized lessons. To the extent that there is guidance it’s basically cribbed from Software Engineering best practices and doesn’t always apply well.

Here are some of mine:

  1. Have a canary environment that rebuilds all machines and redeploys all software on a regularly scheduled basis. Use this environment to detect problems in your deployment tool chain early.
  2. Every developer should have an individual environment of their own to test deployments.
  3. Every team should have at least one environment for testing and/or UAT.
  4. Avoid “Standard Failures.” These are errors that occur often and either do not have a known solution or have a manual workaround. Identify the root cause of errors and address them. Incorporate manual workaround solutions into your automated solutions.
  5. Where possible, embed some sort of “health check” into your applications that you can invoke to have the application check it own configuration.
  6. Identify rollback strategies for your applications.

7. Developers will have to learn infrastructure

If you come from a development background you will have to learn about security, networking, hardware, virtual hardware, etc. This is the domain you are working in now. I’m still at the beginning of this process myself but I’m starting to see the size of how much I still have to learn. For example, if you’re deploying to the cloud you’ll have to learn the inner workings of your chosen cloud infrastructure.

8. Ops will have to learn development patterns and practices.

If you come from an Ops background you will have to learn Software Engineering patterns and practices. You are graduating from someone who writes the occasional script to someone who manages code. Writing some code that only has to be run once is easy. Writing code that has to work again and again and again as well as tolerate change is much, much harder. As the number of people, environments, and machines grow software engineering skills will become more and more important.

9. Don’t automate a bad process.

Consider this: Chef doesn’t provide a built-in way to define which artifacts should be deployed to which environments. To that end we built an “application versions” cookbook which contains a list of all applications, their version, and their artifact location. In order to start work a team must:

  1. Take a branch of the application versions cookbook.
  2. Edit the versions/artifact information.
  3. Upload the cookbook to Chef
  4. commit and push the changes back to github
  5. clone the chef-repo
  6. edit the affected environment to use the new version of the application versions cookbook.
  7. commit/push chef-repo
  8. upload the edited environment to Chef.

Does that sound like a good idea to you? It doesn’t to me–but it’s necessary if you’re going to use a Chef Cookbook as a source for environment application versions. Before you go and wrap some automation around this to make it “easier,” let’s challenge the basic assumption: should we maybe just store application versions by environment elsewhere? A json file on a network share would be easier than this.

When you automate a process (even to make it “easier”) you’re pouring a certain amount of lime over it. Be careful.

10. “Infrastructure as Code!” is not always a good idea.

Code != Artifacts != Configuration. The daily work of DevOps breaks down into basically three disciplines: Code, Configuration, and Artifact management. A change to one of these should not necessitate a change to the other. That means that Code, Configuration, and Artifacts should not live together in github.

Use a Package Manager for your artifacts. If you don’t know where to look check out Artifactory. It’s a versatile artifact repository that supports many different kinds of package managers. It’s API even understands version numbers and will let you identify and retrieve the “latest” version of your artifact. Let your CI server publish artifacts to your package manager and make it the canonical source for artifact retrieval.

Configuration should not be managed like code. Configuration data is any data required by applications to run. Examples are things like dns addresses, email addresses for notifications, database connection strings, api endpoints, etc.. Configuration data is just data about environments. Unlike code it does not need to be branched. It should be stored in some central repository and accessed directly by the deployment code.

The code that you use to execute your deployments is most emphatically and in every possible way code. This means it should be tested, stored in source control, subject to your company’s chosen branching strategies, built by a CI server, etc..

The “infrastructure as code” idea is a really great idea, but it applies only to the procedure of deploying hardware and software. It does not fit well with the metadata that describes which hardware and software should be deployed. Don’t use “infrastructure as code” as an execute to push square pegs into round holes.

Next Page