Full Blog TOC

Full Blog Table Of Content with Keywords Available HERE

Monday, October 18, 2021

Create AWS DynamoDB using CloudFormation and a Sample Golang Application



 


In this post we will use CloudFormation to setup a DynamoDB table, and then access it using a sample GO application.


To setup the DynamoDB we will use the following CloudFormation stack.


dynamoDBTable:
Type: AWS::DynamoDB::Table
Properties:
BillingMode: PAY_PER_REQUEST
TableName: "my-table"
AttributeDefinitions:
- AttributeName: "mykey"
AttributeType: "S"
KeySchema:
- AttributeName: "mykey"
KeyType: "HASH"


Notice that we specify the key attribute twice. The first time we define its type, which in this case is a string ("S").  The second time we specify that this is a key attribute. Other attributes will be automatically added by the DynamoDB once objects with new attributes are created. An exception for this is a "RANGE" attribute that if it is required, should be also specified here.

The billing mode is "PER REQUEST", which is great if you have no idea about the expected read/write load on the table.


Any other service that accesses the DynamoDB table, should be granted with permissions to access it, for example, to grant an ECS task role permission to access the DynamoDB table use the following rather too permissive  policy. In case of need, limit the actions to a smaller set, e.g:

  • dynamodb.GetItem
  • dynamodb.PutItem
  • dynamodb.Query


taskIamRole:
Type: AWS::IAM::Role
Properties:
RoleName: my-role
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: 'sts:AssumeRole'

taskIamPolicy:
Type: AWS::IAM::Policy
Properties:
PolicyName: my-policy
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- dynamodb:*
Resource: arn:aws:dynamodb:*:*:table/my-table
Roles:
- !Ref taskIamRole



To access the DynamoDB from a GO application, first get a DynamoDB API interface:



import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
)


config := aws.Config{
Region: aws.String("us-east-1"),
}

awsSession, err := session.NewSession(&config)
if err != nil {
panic(err)
}

dynamoDbApi := dynamodb.New(awsSession)



Now we can add items to the DynamoDB using the PutItem API. Notice that we define a structure with the names of the attributes that we want to have in the DynamoDB table.



item:= Item{
Key: "key1",
Data: 123,
}
mappedItem, err := dynamodbattribute.MarshalMap(item)
if err != nil {
panic(err)
}

query := dynamodb.PutItemInput{
Item: mappedItem,
TableName: aws.String("my-table"),
}

_, err = dynamoDbApi.PutItem(&query)
if err != nil {
panic(err)
}



Final Note


While it is not cheap, the AWS DynamoDB supplies an easy API, and great performance for an application. I recommend using it in case your DB API rate is moderate.

Wednesday, October 13, 2021

Build S3 Website



 


In this post we will review the steps to setup an AWS S3 based website.


First, let use a CloudFormation stack to setup the S3 bucket and configure it to have a public read access control.



Resources:
s3AgentSite:
Type: AWS::S3::Bucket
Properties:
AccessControl: PublicRead
BucketName: my-bucket-site-example
WebsiteConfiguration:
IndexDocument: index.html



Next, we can create a CodeBuild project to build our code. See my previous post about the setup of a CodeBuild project using CloudFormation. Notice that the CodeBuild project also requires permissions to update the S3 bucket, hence the CodeBuild stack should also include the following permissions:



- Effect: Allow
Action:
- s3:*
Resource:
- arn:aws:s3:::my-bucket-site-example/*



Now, let's handle the build itself. The buildspec.yaml specifies requirements to include nodejs as part of the build container.



version: 0.2

phases:
install:
runtime-versions:
nodejs: latest
build:
commands:
- ./build.sh



And the build script run the npm build, and then copies the results to the S3 bucket using aws sync CLI.



npm i
npm run build
aws s3 sync --acl public-read ./public s3://cto-c3-agent




Monday, October 11, 2021

Creating ECS service using CloudFormation




In this post we will review a CloudFormation stack to create an ECS service. 

Using ECS we can quickly deploy our docker based service on the cloud. In this example we will use the ECS Fargate mode which is a serverless deployment.


We start with a VPC deployment, including 2 subnets and and internet gateway allowing access to the internet. 



vpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
Tags:
- Key: Name
Value: backend-vpc

subnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref vpc
CidrBlock: 10.0.1.0/24
AvailabilityZone: us-east-1a
Tags:
- Key: Name
Value: backend-subnet1

subnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref vpc
CidrBlock: 10.0.2.0/24
AvailabilityZone: us-east-1b
Tags:
- Key: Name
Value: backend-subnet2

internetGateway:
Type: AWS::EC2::InternetGateway
DependsOn: vpc
Properties:
Tags:
- Key: Name
Value: backend-igw

attachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref vpc
InternetGatewayId: !Ref internetGateway

routeTable1:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref vpc
Tags:
- Key: Name
Value: cuustomer-route-table1

routeTable2:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref vpc
Tags:
- Key: Name
Value: backend-route-table2

routeTableAssociate1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref subnet1
RouteTableId: !Ref routeTable1

routeTableAssociate2:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref subnet2
RouteTableId: !Ref routeTable2

publicRoute1:
Type: AWS::EC2::Route
DependsOn: attachGateway
Properties:
RouteTableId: !Ref routeTable1
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref internetGateway

publicRoute2:
Type: AWS::EC2::Route
DependsOn: attachGateway
Properties:
RouteTableId: !Ref routeTable2
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref internetGateway

vpcSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
VpcId: !Ref vpc
GroupDescription: vpcSecurityGroup
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: backend-vpc-security-group



An optional, but recommended step, is to add a bastion server, that is a server we can bash into, and check connections within the vpc.



Parameters:
LatestAmiId:
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2

Resources:

bastionSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
VpcId: !Ref vpc
GroupDescription: bastionSecurityGroup
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: bastion-vpc-security-group

bastionServer1:
Type: AWS::EC2::Instance
Properties:
InstanceType: t2.micro
ImageId: !Ref LatestAmiId
SubnetId: !Ref subnet1
KeyName: ec2
SecurityGroupIds:
- !Ref bastionSecurityGroup
Tags:
- Key: Name
Value: bastion-server1

elasticIP1:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
InstanceId: !Ref bastionServer1
Tags:
- Key: Name
Value: bastion-elastic-ip1


The last thing we need to do is to create a load balancer allowing access to the service, and an ECS service that maintains our desired amount of containers. Note that the task definition includes the tag of the image that we want to run. In this case we use an image from AWS ECR. To view the logs of the containers, we configure a CloudWatch log group.

Notice that the security group enables access to the containers both from the load balancer, and from the bastion server.




loadBalancerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: loadBalancerSecurityGroup
VpcId: !Ref vpc
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0

loadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: backend-alb
Scheme: internet-facing
SecurityGroups:
- !Ref loadBalancerSecurityGroup
Subnets:
- !Ref subnet1
- !Ref subnet2

targetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Name: backend-vpc-target-group
Port: 8080
Protocol: HTTP
TargetType: ip
VpcId: !Ref vpc

loadBalancerListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref loadBalancer
Port: 80
Protocol: HTTP
DefaultActions:
- TargetGroupArn: !Ref targetGroup
Type: forward

taskExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: backend-task-role
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: 'sts:AssumeRole'
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

cloudwatchLogsGroup:
Type: 'AWS::Logs::LogGroup'
Properties:
LogGroupName: backend-log-group
RetentionInDays: 3

taskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
NetworkMode: awsvpc
ExecutionRoleArn: !Ref taskExecutionRole
RequiresCompatibilities:
- FARGATE
Cpu: 256
Memory: 512
ContainerDefinitions:
- Name: origin
Image: MY-ACCOUNT-NUMBER.dkr.ecr.us-east-1.amazonaws.com/MY-IMAGE:latest
PortMappings:
- ContainerPort: 8080
Protocol: tcp
Environment:
- Name: PORT
Value: 8080
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref cloudwatchLogsGroup
awslogs-region: us-east-1
awslogs-stream-prefix: origin

ecsCluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: backend-cluster
CapacityProviders:
- FARGATE
- FARGATE_SPOT
DefaultCapacityProviderStrategy:
- CapacityProvider: FARGATE
Weight: 1
- CapacityProvider: FARGATE_SPOT
Weight: 1

containerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: containerSecurityGroup
VpcId: !Ref vpc
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 8080
ToPort: 8080
SourceSecurityGroupId: !Ref loadBalancerSecurityGroup
- IpProtocol: tcp
FromPort: 8080
ToPort: 8080
SourceSecurityGroupId: !Ref bastionSecurityGroup

ecsService:
Type: AWS::ECS::Service
DependsOn: loadBalancerListener
Properties:
Cluster: !Ref ecsCluster
DesiredCount: 2
TaskDefinition: !Ref taskDefinition
LaunchType: FARGATE
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: ENABLED
Subnets:
- !Ref subnet1
- !Ref subnet2
SecurityGroups:
- !Ref containerSecurityGroup
LoadBalancers:
- ContainerName: origin
ContainerPort: 8080
TargetGroupArn: !Ref targetGroup



Final Notes


In ECS fargate mode, we do not need to manage the EC2 instances, but we still need to manage the VPC. Somehow I expected the VPC to be auto managed by AWS in Fargate mode.

Also notice the the CloudWatch logs will "suffer" from a few minutes delay.

Thursday, October 7, 2021

Building Docker Images Using CodeBuild


 


In the previous post, we have used the CloudFormation to create all the AWS entities required for our CodeBuild project.

Now, we can finally write the actual docker build code.

We'll start with the simplest form of buildspec.yaml, which run a shell file from the CodeCommit Git repository, which is great, since we can update the build without updating anything in AWS itself, but by simply updating the source in the Git.


buildspec.yaml

version: 0.2

phases:
build:
commands:
- ./build_image.sh



The build images shell file does the following:

  • Authenticate to the ECR
  • Pull the previous built docker image to enable using docker cache for the current build
  • Builds the current image
  • Push the image to the ECR


build_image.sh

#!/bin/bash

set -e

ecr=${awsAccount}.dkr.ecr.${awsRegion}.amazonaws.com
localTag=${imageName}:${imageVersion}
remoteTag=${ecr}/${imageName}:${imageVersion}


log(){
message=$@
NOW=$(date +"%m-%d-%Y %T")
echo "${NOW} ${message}"
}

build(){
log "Pull cached image"
set +e
docker pull ${remoteTag}
set -e

log "Build docker image"
docker build --cache-from ${remoteTag} --tag ${localTag} images/${imageName}
docker tag ${localTag} ${remoteTag}

log "Push image"
docker push ${remoteTag}
}

authenticate(){
log "Authenticate to docker"
aws ecr get-login-password --region ${awsRegion} | docker login --username AWS --password-stdin ${ecr}
}


log "Starting build of image ${remoteTag}"
authenticate
build
log "Done"



Final Note


There are other items to cover as part of AWS CodeBuild, such as build triggers, wrappers to run builds and more. I will cover these in future posts.



Wednesday, October 6, 2021

Setup CodeBuild Project using CloudFormation


 


In this post we will review a CloudFormation stack to create all the required AWS resources for a new CodeBuild project.


We assume that we already have a GIT repository stored in AWS CodeCommit, and an included Dockerfile that builds the related image.


First we specify the stack parameters: 

  • The AWS account
  • The region
  • The CodeCommit name 
  • The image name to be build from this project



Parameters:

ParameterAccountId:
Type: String
Default: "123456789012"
ParameterRegion:
Type: String
Default: us-east-1
ParameterCodeCommitName:
Type: String
Default: my
ParameterImageName:
Type: String
Default: my-image



The stack now specifies the resources. We will review each resource.



Resources:



The first resource is the ECR. We create a dedicated ECR for this project.



ecrRepository:
Type: AWS::ECR::Repository
Properties:
RepositoryName: !Ref ParameterImageName



Another requirement is the S3 bucket that is required for storing of the CodeBuild logs. We will use a life-cycle policy to remove logs older than a week.



s3LogsBucket:
Type: AWS::S3::Bucket
Properties:
AccessControl: Private
BucketName: !Join
- "-"
- - my-codebuild-logs
- !Ref ParameterImageName
LifecycleConfiguration:
Rules:
- Id: DeleteOldFiles
Status: Enabled
ExpirationInDays: 7



Now we can create a role that the CodeBuild project uses. It includes a policy allowing the following:

  • Pull image from the ECR (for caching of previous build)
  • Push Image to the ECR
  • Pull the code from the CodeCommit
  • Save logs to the S3 bucket



codebuildProjectRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Join
- "-"
- - my-cloudbuild-role
- !Ref ParameterImageName
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- sts:AssumeRole
Principal:
Service:
- codebuild.amazonaws.com
Policies:
- PolicyName: !Join
- "-"
- - my-bloudbuild-policy
- !Ref ParameterImageName
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- ecr:BatchCheckLayerAvailability
- ecr:BatchGetImage
- ecr:CompleteLayerUpload
- ecr:DescribeImages
- ecr:DescribeRepositories
- ecr:DescribeImageScanFindings
- ecr:GetAuthorizationToken
- ecr:GetDownloadUrlForLayer
- ecr:GetRepositoryPolicy
- ecr:InitiateLayerUpload
- ecr:ListImages
- ecr:ListTagsForResource
- ecr:PutImage
- ecr:UploadLayerPart
Resource:
- "*"
- Effect: Allow
Action:
- codecommit:GitPull
Resource:
- !Join
- ":"
- - arn:aws:codecommit
- !Ref ParameterRegion
- !Ref ParameterAccountId
- !Ref ParameterCodeCommitName
- Effect: Allow
Action:
- s3:*
Resource:
- !Join
- "/"
- - !GetAtt s3LogsBucket.Arn
- "*"



Finally we can configure the actual CodeBuild project, which uses the source from the CodeCommit, and run the actual CodeBuild according to the buildspec.yaml which is located in the root of the CodeCommit Git repository. We send environment variables for the buildspec.yaml, enabling it to use them in its shell commands.



codeBuildProject:
Type: AWS::CodeBuild::Project
Properties:
Name: !Join
- "-"
- - my-codebuild-project
- !Ref ParameterImageName
Source:
Type: CODECOMMIT
Location: !Join
- "/"
- - "https:/"
- !Join
- "."
- - git-codecommit
- !Ref ParameterRegion
- amazonaws.com
- v1/repos
- !Ref ParameterCodeCommitName

BuildSpec: buildspec.yaml
Artifacts:
Type: NO_ARTIFACTS
Environment:
Type: LINUX_CONTAINER
Image: aws/codebuild/standard:4.0
ComputeType: BUILD_GENERAL1_SMALL
PrivilegedMode: true
EnvironmentVariables:
- Name: awsRegion
Value: !Ref ParameterRegion
Type: PLAINTEXT
- Name: awsAccount
Value: !Ref ParameterAccountId
Type: PLAINTEXT
- Name: imageName
Value: !Ref ParameterImageName
Type: PLAINTEXT
- Name: imageVersion
Value: latest
Type: PLAINTEXT
ServiceRole: !GetAtt codebuildProjectRole.Arn
LogsConfig:
CloudWatchLogs:
Status: DISABLED
S3Logs:
Status: ENABLED
Location: !GetAtt s3LogsBucket.Arn
EncryptionDisabled: true


In the next post, I will provide an example for a buildspec.yaml, and an example of how build the image and push it to the ECR.



Wednesday, September 29, 2021

Evaluating AWS Macie for Sensitive Data Protection

 



In this blog I will review the Personal Identifiable Information (PII) discovery by AWS Macie.

Reading the AWS documentation I've found some encouraging information about Macie:

Macie discovers and protects sensitive data in AWS

 Macie is fully managed, so it is serverless


So, Macie can scan S3 buckets, look for PII, and dump a report to a another specific S3 bucket with the details information about the findings.

I have created a the following JSON file, and uploaded it to an existing S3 bucket:


{
"items": [
{
"id": 634287634,
"lastAccessTime": 2154376372,
"isAdmin": true,
"name": "John Doe",
"streetAddress": "Hasharon 1",
"city": "Ramat Gan",
"country": "ISRAEL",
"hasCreditCard": false,
"phone": "08-6466123",
"anotherSecret": "5555555555554444"
},
{
"id": 54354554,
"lastAccessTime": 5435,
"isAdmin": false,
"name": "Alice Bob",
"streetAddress": "Herzel 5",
"city": "Or Yehuda",
"country": "ISRAEL",
"hasCreditCard": true,
"phone": "08-1234456",
"anotherSecret": "5105105105105100"
},
{
"id": 543543,
"lastAccessTime": 54354357654,
"isAdmin": false,
"name": "Bob Mcgee",
"streetAddress": "Hashalom 54",
"city": "Tel Aviv",
"country": "ISRAEL",
"hasCreditCard": true,
"phone": "03-6478222",
"anotherSecret": "4111111111111111"
}
]
}


The data in the file includes several PII fields:

  • name
  • streetAddress
  • city
  • country
  • phone
  • anotherSecret (which actually contains credit cards numbers)


I've started configuring my first Macie Job to test it. But:

❌ I had to manually create a target S3 bucket for the Macie reports

 I had to manually update the target S3 bucket policy to allow Macie to write its report to it


This is disappointing. I had expected a "Create an S3 bucket for me" button, like other AWS S3 services. So I've created an S3 bucket, and set the following policy for it:


{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "macie.amazonaws.com",
"AWS": "arn:aws:iam::MY_ACCOUNT_NUMBER:user/MY_USER_NAME"
},
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::macie-output-reports-bucket",
"arn:aws:s3:::macie-output-reports-bucket/*"
]
}
]
}


Notice that the MY_ACCOUNT_NAME and MY_USER_NAME should be replaced to match you account and user.


Next Macie wanted me to do some more work for it to dump the report details:

❌ I had to manually create akey in KMS

❌ I had to provide permissions for Macie to use the key


Again, not "do it for me" button. And why am I forced to have the report encrypted? 

Somehow annoyed, I've created a key in KMS, and supply the following policy:


{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Enable IAM User Permissions",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::MY_ACCOUNT_NAME:root"
},
"Action": "kms:*",
"Resource": "*"
},
{
"Sid": "Allow use of the key",
"Effect": "Allow",
"Principal": {
"Service": "macie.amazonaws.com"
},
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:DescribeKey"
],
"Resource": "*"
}
]
}


Finally, I was able to run the job. The bucket contained the single small file that I've listed above, but still:

❌ I had to wait for several minutes until the job was complete


Finally, I got the results from the job, created in the target S3 bucket as ".gz" file. I expected a detailed report from Macie. According to the documentation, it reported up to 15 first locations of PII, but:

❌ The results were very partial


"sensitiveData": [
{
"category": "PERSONAL_INFORMATION",
"totalCount": "2",
"detections": [
{
"type": "NAME",
"count": "2",
"occurrences": {
"lineRanges": [],
"pages": [],
"records": [
{
"recordIndex": "0",
"jsonPath": "$.items[0].name"
},
{
"recordIndex": "0",
"jsonPath": "$.items[2].name"
}
],
"cells": []
}
}
]
}
]


The results included only the first and the third items name. For some unclear reason, Macie had skipped the second item. In addition, all the other fields were ignored.


Final Note


Well, in case you did not notice by now, I am very disappointed from Macie. I guess it is still in its non production phases, or else I did something really wrong.

In addition the cost of using this service is 1$/GB which is extremely high.

For now, I would recommend not to use this service.


Wednesday, September 22, 2021

How to Create a Customized AWS AMI

 

In this post we will review the steps to create a customize AWS AMI.

AWS AMI stands for Amazon Machine Image. It is a snapshot of an EC2 instance volumes, that is used to launch new EC2 instances.


We will start by launching a standard Linux x86 machine. Open the EC2 service in the AWS console, and click on the Launch Instances button.





Select the Amazon Linux 2 AMI, which is a basic empty Linux image.




Next we select the instance type. We will use the t2.micro which has 1 vCPU, and 1G RAM. Then click on the Configure Instance Details button.



Now we select the VPC where the instance is deployed, and click on the Review and Launch button, and then click on the Launch button.




Before the instance is launched we need to select a key pair allowing us to connect to the instance. In this case we will create a new key pair, and down it to be used later. Now we can click on the Launch Instance button.




Now that the instance is launched, we can check its status in the EC2 instance table. We need to wait until the status of the instance is Running.




Let's find the instance public IP by clicking on the EC2 instance.

Notice that in case you do not get a public IP, it means you have selected a subnet for instance which is a private subnet, and hence cannot be accessed from the internet. To create a public subnet, make sure to:

  • Add an Internet Gateway
  • Attach it to the VPC
  • Edit the defaulr Routing Table and add a 2nd rule to redirect all traffic to the internet gateway
  • Edit the subnet and use the Actions button to Enable Auto Assign IPv4 Addresses




To connect to the EC2 instance run the following commands:


chmod 400 my-key.pem
ssh -i my-key.pem ec2-user@44.195.128.228


Now we have a connection to the instance and we can configure it. For example, we can run packages installations, GIT connections, update OS configuration, and more.

In this simple example we will simply create a text file.


echo "Hello World" > /home/ec2-user/indication


Next we stop the EC2 instance, and create an image from it.





We can use the Images menu to track the image creation process.





Once the images is ready we can use it for a new EC2 instance creation.




That's all for this time!