Thursday 12 May 2022

Managing AWS IAM using Terraform

 

 
 
 

Creating a user 


 
From resource type aws_iam_user TF extracts the following information:
  • provider: aws
  • resource: iam_user
 
main.tf:
 
resource "aws_iam_user" "admin-user" {
    name = "Adam"
    tags = {
        Description = "Technical Team Lead"
    }
}
 
We can initialize the directory as provider (aws) is deducted from the resource type:
 
$ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding latest version of hashicorp/aws...
- Installing hashicorp/aws v4.13.0...
- Installed hashicorp/aws v4.13.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
 
If we have AWS CLI installed and we've already configured it by specifying access key ID, secret key and region (so they are store in /home/<user>/.aws/config/credentials and this is RECOMMENDED approach), we can execute terraform plan and apply:
 
$ terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_iam_user.admin-user will be created
  + resource "aws_iam_user" "admin-user" {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = "Adam"
      + path          = "/"
      + tags          = {
          + "Description" = "Technical Team Lead"
        }
      + tags_all      = {
          + "Description" = "Technical Team Lead"
        }
      + unique_id     = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
 
 
$ terraform apply

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_iam_user.admin-user will be created
  + resource "aws_iam_user" "admin-user" {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = "Adam"
      + path          = "/"
      + tags          = {
          + "Description" = "Technical Team Lead"
        }
      + tags_all      = {
          + "Description" = "Technical Team Lead"
        }
      + unique_id     = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_iam_user.admin-user: Creating...
aws_iam_user.admin-user: Creation complete after 2s [id=Adam]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
 
 
If we did't have AWS CLI configured, we would have had 2 options: to provide AWS configuration using environment variables or by using aws provider block in TF configuration file.

Approach with environment variables:

$ export AWS_ACCESS_KEY_ID="anaccesskey"
$ export AWS_SECRET_ACCESS_KEY="asecretkey"
$ export AWS_REGION="us-west-2"
$ terraform plan

 
 
Approach with aws block (NOT recommended!):
 
It contains AWS CLI configuration and needs to be added to TF config file (see Docs overview | hashicorp/aws | Terraform Registry):
 
Inside main.tf:
 
provider "aws" {
  region     = "us-west-2"
  access_key = "my-access-key"
  secret_key = "my-secret-key"
}
 
...
 
Note that it is NOT recommended to have hardcoded credentials in any file that gets checked in into repository and this includes .tf files.


Removing the added user



$ terraform destroy
aws_iam_user.admin-user: Refreshing state... [id=Adam]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # aws_iam_user.admin-user will be destroyed
  - resource "aws_iam_user" "admin-user" {
      - arn           = "arn:aws:iam::036201377220:user/Adam" -> null
      - force_destroy = false -> null
      - id            = "Adam" -> null
      - name          = "Adam" -> null
      - path          = "/" -> null
      - tags          = {
          - "Description" = "Technical Team Lead"
        } -> null
      - tags_all      = {
          - "Description" = "Technical Team Lead"
        } -> null
      - unique_id     = "AIDAQQ3OFFXCFCKBEV5KP" -> null
    }

Plan: 0 to add, 0 to change, 1 to destroy.

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

aws_iam_user.admin-user: Destroying... [id=Adam]
aws_iam_user.admin-user: Destruction complete after 2s

Destroy complete! Resources: 1 destroyed.


When user is added to AWS, they have no permissions attached. To add permissions to user, we need to attach IAM policies to them. 

Let's see how to attach a policy to a user we named Adam, via Terraform. To get an idea of some AWS-managed policies, see here: AWS managed policies for job functions - AWS Identity and Access Management.

For example, we want to attach AdministratorAccess policy. We'll need first to get policy definition in JSON format. If we log in to AWS Management Console and go to policies we can copy its JSON definition. TF resource type that we need to use is aws_iam_policy:

resource "aws_iam_policy" "admin-user-policy" {
  name        = "admin-user-policy"
  path        = "/"
  description = "Admin user"

  # Terraform's "jsonencode" function converts a
  # Terraform expression result to valid JSON syntax.
  policy = jsonencode({
    Version = "2022-05-13"
    Statement = [
      {
        Action = "*"
        Effect   = "Allow"
        Resource = "*"
      },
    ]
  })
}


Another way of embedding a JSON into .tf file is by using Here document (heredoc) which redirects a multiline string literal to the preceding command while preserving line breaks. Unix syntax for it is:

[command] <<DELIMITER
    First line.
    Second line.
    Third line.
    Fourth line.
DELIMITER

In our case:

resource "aws_iam_policy" "admin-user-policy" {
  ...
  policy = <<EOF
  {
    Version = "2022-05-13"
    Statement = [
      {
        Action = "*"
        Effect   = "Allow"
        Resource = "*"
      },
    ]
  }
  EOF
}


Probably more readable and convenient way to embed JSON is .tf file is to keep JSON string in a separate .json file (e.g. admin-policy.json in our case) which is located in the configuration directory and then simply use function file() to read the file content:

resource "aws_iam_policy" "admin-user-policy" {
  ...
  policy =  file("admin-policy.json")
}

Now we've provisioned a managed IAM policy, we need to attach it to the provisioned user. We need to use either aws_iam_policy_attachment resource which exclusively attaches managed IAM policy to users, groups or roles or aws_iam_user_policy_attachment which attaches a managed IAM Policy to a user only. 

NOTE: exclusive attachment means that the referenced policy can only be attached to a single role across your entire AWS account.

resource "aws_iam_user_policy_attachment" "adam-admin-user" {
    user = aws_iam_user.admin-user
    policy_arn = aws_iam_policy.admin-user-policy.arn
}

So, main.tf will look like this:

resource "aws_iam_user" "admin-user" {
    ...
}

resource "aws_iam_policy" "admin-user-policy" {
    ...
}

resource "aws_iam_user_policy_attachment" "adam-admin-user" {
    user = aws_iam_user.admin-user
    policy_arn = aws_iam_policy.admin-user-policy.arn
}

We can now execute terraform plan and apply


---

No comments: