Updating an AutoScalingGroup with a new LaunchConfiguration in boto - amazon-ec2

I have a script that needs to update a named AutoScalingGroup with a new LaunchConfiguration for some new just-created AMI. Unfortunately the documentation isn't good, and I'm tired of trial-and-error. This is what I have so far:
build_autoscale_name = "build_autoscaling"
build_autoscale_lc = LaunchConfiguration(
...launch config stuff...
, image_id=imid # new AMI
)
as_conn.create_launch_configuration(build_autoscale_lc)
ag = AutoScalingGroup(
group_name=build_autoscale_name
, launch_config=build_autoscale_lc
...other ASG stuff...
)
as_conn.create_auto_scaling_group(ag)
The latest way this is failing is with:
Launch Configuration by this name already exists
If I comment out the create_launch_configuration() I then get:
AutoScalingGroup by this name already exists
I see AutoScalingGroup has an update method; do I need to perhaps get_all_groups() then do update with a new LaunchConfiguration with the same name? Or does it matter if I create a newly-named LaunchConfiguration every time (i.e. will I run into some limit)?

I was experiencing a similar problem, when trying to update an existing autoscaling group, and managed to sort it out with the the process you suggested in your original post: using get_all_groups() to fetch the autoscaling group, and then calling update() on the object after updating the attributes.
Full example:
autoscaling_group_name = 'my-test-asg'
launch_config_name = 'my-test-lc'
launch_config = LaunchConfiguration( name=launch_config_name,
image_id=image_id,
key_name=ssh_key_name,
security_groups=security_groups,
user_data=user_data,
instance_type=instance_type,
associate_public_ip_address=associate_public_ip )
as_group = as_conn.get_all_groups(names=[autoscaling_group_name])[0]
setattr(as_group, launch_config_name, launch_config)
as_group.update()

I am not familiar with boto, but I can clear few doubts about autoscaling in AWS. To update launch configuration of an autoscaling group you will have to create a new launch configuration and update the launch config for autoscaling group. You can either keep two names for launchconfig. So if the first name is in use then delete the launch config with the second name and create a new one with the second name after that update autoscaling group and same if launchconfig in use has the second name. So, you will have only two launch configs at a time.
Hope I have understood you problem correctly.

Related

AWS-CDK: Cross account Resource Access and Resource reference

I have a secret key-value pair in Secrets Manager in Account-1 in us-east-1. This secret is encrypted using a Customer managed KMS key - let's call it KMS-Account-1. All this has been created via console.
Now we turn to CDK. We have cdk.pipelines.CodePipeline which deploys Lambda to multiple stages/environments - so 1st to { Account-2, us-east-1 } then to { Account-3, eu-west-1 } and so on. This has been done.
The lambda code in all stages/environments above, now needs to be changed to use the secret key-value pair present with Account-1's us-east-1 SecretsManager by getting it via secretsmanager client. That code should probably look like this (python):
client = boto3.session.Session().client(
service_name = 'secretsmanager',
region_name = 'us-east-1'
)
resp = client.get_secret_value(
SecretId='arn:aws:secretsmanager:us-east-1:<ACCOUNT-1>:secret:name/of/the/secret'
)
secret = json.loads(resp['SecretString'])
All lambdas in various accounts and regions (ie. environments) will have the exact same code as above since the secret needs to be fetched from Account-1 in us-east-1.
Firstly I hope this is conceptually possible. Is that right?
Next how do I change the cdk code to facilitate this? How will the code-deploy in code-pipeline get permission to import this custom kms key and SecretManager' secretand apply correct permissions for cross account access by the lambdas that the cdk pipeline creates ?
Can someone please give some pointers?
This is bit tricky as CloudFormation, and hence CDK, doesn't allow cross account/cross stage references because CloudFormation export doesn't work cross account as far as my understanding goes. All these patterns of "centralised" resources fall into that category - ie. resource in one account (or a stage in CDK) referenced by other stages.
If the resource is created outside the context of CDK (like via console), then you might as well hardcode the names/arns/etc. throughout the CDK code where its used and that should be sufficient.
For resources that have the ability to hold resource based policies, it's simpler as you can just attach the cross-account access permissions to them directly - again, offline via console since you are maintaining it manually anyway. Each time you add a stage (account) to your pipeline, you will need to go to the resource and add cross-account permissions manually.
For resources that don't have resource based policies, like SSM for eg., things are a bit roundabout as you will need to create a Role that can be assumed cross-account and then access the resource. In that case you will have to separately maintain the IAM Role too and manually update the trust policy to other accounts as you add stages to your CDK pipeline. Then, as usual hardcode the role arn in your CDK code, assume it in some CustomResource lambda and use it.
It gets more interesting if the creation is also done in the CDK code itself (ie. managed by CloudFormation - not done separately via console/aws-cli etc.). In this case, many times you wouldn't "know" the exact ARNs as the physical-id would be generated by CloudFormation and likely be a part of the ARN. Even influencing the physical-id yourself (like by hardcoding the bucket name) might not solve it in all cases. Eg. KMS ARNs and SecretManager ARNs append unique-ids or some sort of hashes to the end of the ARN.
Instead of trying to work all that out, it would be best left untouched and let CFn generate whatever random name/arn it chooses. To then reference these constructs/ARNs, just put them into SSM Parameters in the source/central account. SSM doesn't have resource based policy that I know of. So additionally create a role in cdk that trusts the accounts in your cdk code. Once done, there is no more maintenance - each time you add new environments/accounts to CDK (assuming its a cdk pipeline here), the "loop" construct that you will create will automatically add the new account into the trust relationship.
Now all you need to do is to distribute this role-arn and the SSM Parameternames to other stages. Choose an explicit role-name and SSM Parameters. The manual ARN construction given a rolename is pretty straightforward. So distribute that and SSM Parameters around your CDK code to other stages (compile time strings instead of references). In target stages, create custom-resource(s) (AWSCustomResource) backed by AwsSdkCall lambda to simply assume this role-arn and make the SDK call to retrieve the SSM Parameter values. These values can be anything, like your KMS ARNs, SecretManager's full ARNs etc. which you couldn't easily guess. Now simply use these.
Roundabout way to do a simple thing, but so far that is all I could do to get this to work.
#You need to maintain this list no matter what you do - so it's nothing extra
all_other_accounts = [ <list of accounts that this cdk deploys to> ]
account_principals = [iam.AccountPrincipal(a) for a in all_other_account]
role = iam.Role(
assumed_by = iam.CompositePrincipal(*account_principals), #auto-updated as you change the list above
role_name = some_explicit_name,
...
)
role_arn = f'arn:aws:iam::<account-of-this-stack>:role/{some_explicit_name}'
kms0 = kms.Key(...)
kms0.grant_decrypt(role)
# Because KMS also needs explicit resource policy even if role policy allows access to it
kms0.add_to_role_policy(iam.PolicyStatement(principals = [iam.ArnPrincipal(role_arn)], actions = ...))
kms1 = kms.Key(...)
kms1.grant_decrypt(role)
kms0.add_to_role_policy(... same as above ...)
secrets0 = secretsmanager.Secret(...) #maybe this is based off kms0
secrets0.grant_read(role)
secrets1 = secretsmanager.Secret(...) #maybe this is based off kms1
secrets1.grant_read(role)
# You can turn all this into a loop ofc.
ssm0 = ssm.StingParameter(self, '...', parameter_name = 'kms0_arn', string_value = kms0.key_arn, ...)
ssm0.grant_read(role)
ssm1 = ssm.StingParameter(self, '...', parameter_name = 'kms1_arn', string_value = kms1.key_arn, ...)
ssm1.grant_read(role)
ssm2 = ssm.StingParameter(self, '...', parameter_name = 'secrets0_arn', string_value = secrets0.secret_full_arn, ...)
ssm2.grant_read(role)
...
#Now simply pass around the role and ssm parameter names
for env in environments:
MyApplicationStage(self, <...>, ..., role_arn = role_arn, params = [ 'kms0_arn', 'kms1_arn', ... ], ...)
And then in the target stage(s):
for parm in params:
fn = AwsSdkCall('ssm', 'get_parameter', { "Name": param }, ...)
acr = AwsCustomResource(..., on_create = fn, on_update = fn, ...)
collect['param'] = acr.get_response_field('Parameter.Value')
Now do whatever you want with the collected artifacts, including supplying them as environment variables to your main service lambda (which will be resolved at deploy time).
Remember they will all be Tokens and resolved only at deploy time, but that's true of any resource, whether or not via custom-resource and it shouldn't matter.
That's a generic pattern which should work for any case.
(GitHub link where this question was asked and I had answered it there too)

terraform destroy doesn't delete the ec2 instance created using input parameters for variables

I tried launching an ec2 instance using input parameters for the variables in terraform apply command. This creates the instance successfully. However, when I try to delete the instance using terraform destory, it executes but nothing gets deleted.
So I have a region variable with a default value. When I pass a different region in this variable using input parameters,instance launchesjust fine in the provided region but I am not able to terminate it using terraform destroy.
main.tf
variable "region" {
default = "us-west-1"
}
variable "ami" {
type = "map"
default = {
us-east-2 = "ami-02e680c4540db351e"
us-west-1 = "ami-011b6930a81cd6aaf"
}
}
provider "aws" {
region = "${var.region}"
}
resource "aws_instance" "web" {
ami = "${lookup(var.ami,var.region)}"
instance_type = "t2.micro"
tags {
Name = "naxi"
}
}
Terraform apply:
terraform apply -var region=us-east-2
Output of terraform destroy :
aws_instance.web: Refreshing state... (ID: i-05ca0514f61dcaf16)
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
Destroy complete! Resources: 0 destroyed.
Though it's able to lookup the instance id in the correct region, my guess is that it is trying to terminate the instance from the default region and not from the one I supplied as parameter.
Is there a way I can supply a parameter -var region=something with terraform destroy?
Destroy works as expected if I use the default values and no input parameters.
EDIT---
As soon as I give the this command: terraform destroy -varfile=variables.tfvars, all the instance related information from terraform.tfstate file gets removed and all the previous content of this file gets saved as backup to terraform.tfstate.backup. But still the instance is not deleted.
I think this is your main problem:
You ran apply with your "aws" provider defined one way (via a variable), but then you ran destroy with the same "aws" provider defined differently (you let the "region" variable default instead of specifying it).
As a result, terraform destroy looked in the wrong place (wrong AWS region) for your created resources.
Since terraform destroy was looking in the wrong place, it found nothing there.
Therefore terraform destroy saw that it did not need to destroy anything, just update its locally stored state information to reflect the absence of the resources.
Try these steps instead:
terraform apply -var 'region=us-east-2'
terraform destroy -var 'region=us-east-2'
This works for me, Terraform v0.12.2 + provider.aws v2.16.0.
I am guessing slightly here, but it seems like the point is probably that you, the Terraform user, are responsible for making sure you destroy with the exact same provider definitions you apply'd with.
And if you're using any variables to help define your providers, then this is something you will need to be especially mindful of, since you are making it easy to accidentally change provider definitions.
As a side note, I ran into a similar confusion myself. It seems to me that HashiCorp's Getting Started guide, in its current state, could do a better job of warning about this. It walks newbies through a very similar setup to yours, and currently appears to say nothing about how to destroy properly, or any potential pitfalls.
Perhaps you have multiple providers set. Try aliasing your provider and passing that into the resource.
provider "aws" {
region = var.region
alias = "mine"
}
resource "aws_instance" "web" {
provider = aws.mine
}

Google Compute API: new instance with external IP

I'm trying to dynamically create Compute instances on Google Cloud via PHP using API.
Using csmartinez Cloud API sample, I managed to create my instance and can see it running on Google Console.
I need to assign an external IP address to this newly created instance. Based on the API and Google PHP library, I then added:
$ipName = "foo";
$addr = new Google_Service_Compute_Address();
$addr->setName($ipName);
$response = $service->addresses->insert($project, $ipZone, $addr);
Since this call has a "pending" status at first, I added a sleep(5) so I can then get the newly created static IP:
$response = $service->addresses->get($project, $ipZone, $ipName);
$ip = $response->address;
which runs well and gives me the correct IP address. I then continue and try to create my instance while assigning the new IP:
$networkConfig = new Google_Service_Compute_AccessConfig();
$networkConfig->setNatIP($ip);
$networkConfig->setType("ONE_TO_ONE_NAT");
$networkConfig->setName("External NAT");
$googleNetworkInterfaceObj->setAccessConfigs($networkConfig);
The static IP is created, the instance is created, but IP is not assigned to the instance.
To remove my doubt about the pending status, I also tried to assign an already created static IP to my instance, thus using:
$networkConfig->setNatIP("xxx.xxx.xxx.xxx");
With no more success... What am I missing here ?
I think this line:
$googleNetworkInterfaceObj->setAccessConfigs($networkConfig);
should be:
$googleNetworkInterfaceObj->setAccessConfigs(array($networkConfig));
If this doesn't work, there might be another error somewhere else.
this would be the whole procedure, which assigns a network:
$instance = new Google_Instance();
$instance->setKind("compute#instance");
$accessConfig = new Google_AccessConfig();
$accessConfig->setName("External NAT");
$accessConfig->setType("ONE_TO_ONE_NAT");
$network = new Google_NetworkInterface();
$network->setNetwork($this->getObjectUrl($networkName, 'networks', $environment->getPlatformConfigValue(self::PROJECT_ID)));
$network->setAccessConfigs(array($accessConfig));
$instance->setNetworkInterfaces(array($network));
$addr->setName() is barely cosmetic; try $addr->setAddress($ipAddress).
guess the Google_NetworkInterface would need the $addr assigned.
sorry, currently have no spare IP and do not want to pay only to provide code.

AWS copying one object to another

I'm trying to copy data from one bucket to another using the ruby "aws-sdk" gem version 3.
My code is shown below:
temporary_object = #temporary_bucket.object(temporary_path)
permanent_object = #permanent_bucket.object(permanent_path)
temporary_object.copy_to(permanent_object)
However I keep getting the error Aws::S3::Errors::NoSuchKey: The specified key does not exist. Which makes sense as the permanent bucket doesn't exist at this moment however I thought that using copy_to creates the bucket if it does not exist.
Any advice would be very helpful.
Thanks

How do I set an alarm to terminate an EC2 instance using boto?

I have been unable to find a simple example which shows me how to use boto to terminate an Amazon EC2 instance using an alarm (without using AutoScaling). I want to terminate the specific instance that has a CPU usage less than 1% for 10 minutes.
Here is what I've tried so far:
import boto.ec2
import boto.ec2.cloudwatch
from boto.ec2.cloudwatch import MetricAlarm
conn = boto.ec2.connect_to_region("us-east-1", aws_access_key_id=ACCESS_KEY, aws_secret_access_key=SECRET_KEY)
cw = boto.ec2.cloudwatch.connect_to_region("us-east-1", aws_access_key_id=ACCESS_KEY, aws_secret_access_key=SECRET_KEY)
reservations = conn.get_all_instances()
for r in reservations:
for inst in r.instances:
alarm = boto.ec2.cloudwatch.MetricAlarm(name='TestAlarm', description='This is a test alarm.', namespace='AWS/EC2', metric='CPUUtilization', statistic='Average', comparison='<=', threshold=1, period=300, evaluation_periods=2, dimensions={'InstanceId':[inst.id]}, alarm_actions=['arn:aws:automate:us-east-1:ec2:terminate'])
cw.put_metric_alarm(alarm)
Unfortunately it gives me this error:
dimensions={'InstanceId':[inst.id]}, alarm_actions=['arn:aws:automate:us-east-1:ec2:terminate'])
TypeError: init() got an unexpected keyword argument 'alarm_actions'
I'm sure it's something simple I'm missing.
Also, I am not using CloudFormation, so I cannot use the AutoScaling feature. This is because I don't want the alarm to use a metric across the entire group, rather only for a specific instance, and only terminate that specific instance (not any instance in that group).
Thanks in advance for your help!
The alarm actions are not passed through dimensions but rather added as an attribute to the MetricAlarm object that you are using. In your code you need to do the following:
alarm = boto.ec2.cloudwatch.MetricAlarm(name='TestAlarm', description='This is a test alarm.', namespace='AWS/EC2', metric='CPUUtilization', statistic='Average', comparison='<=', threshold=1, period=300, evaluation_periods=2, dimensions={'InstanceId':[inst.id]})
alarm.add_alarm_action('arn:aws:automate:us-east-1:ec2:terminate')
cw.put_metric_alarm(alarm)
You can also see in the boto documentation here:
http://docs.pythonboto.org/en/latest/ref/cloudwatch.html#module-boto.ec2.cloudwatch.alarm

Resources